Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class ProductionUpdateInterface
virtual UnsignedInt countUnitTypeInQueue( const ThingTemplate *unitType ) const = 0;

virtual Bool queueCreateUnit( const ThingTemplate *unitType, ProductionID productionID ) = 0;
virtual void cancelUnitCreate( ProductionID productionID ) = 0;
virtual void cancelUnitCreate( ProductionID productionID, Bool forceCancel = FALSE ) = 0;
virtual void cancelAllUnitsOfType( const ThingTemplate *unitType) = 0;

virtual void cancelAndRefundAllProduction() = 0;
Expand Down Expand Up @@ -214,7 +214,7 @@ class ProductionUpdate : public UpdateModule, public ProductionUpdateInterface,
virtual UnsignedInt countUnitTypeInQueue( const ThingTemplate *unitType ) const override; ///< count number of units with matching unit type in the production queue

virtual Bool queueCreateUnit( const ThingTemplate *unitType, ProductionID productionID ) override; ///< queue unit to be produced
virtual void cancelUnitCreate( ProductionID productionID ) override; ///< cancel construction of unit with matching production ID
virtual void cancelUnitCreate( ProductionID productionID, Bool forceCancel = FALSE ) override; ///< cancel construction of unit with matching production ID
virtual void cancelAllUnitsOfType( const ThingTemplate *unitType) override; ///< cancel all production of type unitType

virtual void cancelAndRefundAllProduction() override; ///< cancel and refund anything in the production queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ Bool ProductionUpdate::queueCreateUnit( const ThingTemplate *unitType, Productio
//-------------------------------------------------------------------------------------------------
/** Cancel the construction of the unit with the matching production ID */
//-------------------------------------------------------------------------------------------------
void ProductionUpdate::cancelUnitCreate( ProductionID productionID )
void ProductionUpdate::cancelUnitCreate( ProductionID productionID, Bool forceCancel )
{

// search for the production entry in our queue
Expand All @@ -470,8 +470,16 @@ void ProductionUpdate::cancelUnitCreate( ProductionID productionID )
if( production->m_productionID == productionID )
{

// give the player the cost of the object back
Player *player = getObject()->getControllingPlayer();
#if !RETAIL_COMPATIBLE_CRC
// TheSuperHackers @bugfix arcticdolphin 08/03/2026 Prevent cancel once units are produced to avoid free unit exploit
if( !forceCancel && production->getProductionQuantityRemaining() < production->getProductionQuantity() )
{
return;
}
#endif

// give the player the cost of the object back
Money *money = player->getMoney();
money->deposit( production->m_objectToProduce->calcCostToBuild( player ), TRUE, FALSE );

Expand Down Expand Up @@ -693,10 +701,17 @@ UpdateSleepTime ProductionUpdate::update()
// Don't cancel dozers in the queue. jba.
if (!production->getProductionObject()->isKindOf(KINDOF_DOZER))
{

#if RETAIL_COMPATIBLE_CRC
cancelUnitCreate(production->getProductionID());
return UPDATE_SLEEP_NONE;

#else
// TheSuperHackers @bugfix arcticdolphin 13/03/2026 Let partial production finish naturally when script-disallowed.
if( production->getProductionQuantityRemaining() == production->getProductionQuantity() )
{
cancelUnitCreate(production->getProductionID());
return UPDATE_SLEEP_NONE;
}
Comment on lines +709 to +713

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop partial batches when unit production is disabled

When a script or AI calls setCanBuildUnits(false) after the first member of a quantity-modified batch has exited, allowedToBuild() is false but this condition is also false, so update() falls through and continues spawning every remaining unit. Previously this path canceled the entry immediately, so missions that disable production can now receive additional Red Guards or other batched units; preserving the anti-refund behavior requires a cancellation path that removes the remainder without granting an exploitable refund rather than allowing disallowed production to finish.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, forceCancel should force removal from the queue, not force a refund. For non-retail-compatible behavior, I think the cases should be:

  • partial batch + player cancellation: reject the cancellation and let the batch continue;
  • partial batch + forced cleanup: remove the remainder without refund;
  • nothing produced yet: remove and refund normally.

Also should update the PR description, because it currently says the script-disallow path uses forceCancel, while the implementation lets the remaining production finish. And tests should cover cancellation before/after the first unit, selling or destroying the producer mid-batch, and disabling production mid-batch.

#endif
}

}
Expand Down Expand Up @@ -1141,7 +1156,7 @@ void ProductionUpdate::cancelAndRefundAllProduction()
if( m_productionQueue )
{
if( m_productionQueue->getProductionType() == PRODUCTION_UNIT )
cancelUnitCreate( m_productionQueue->getProductionID() );
cancelUnitCreate( m_productionQueue->getProductionID(), TRUE );
else if( m_productionQueue->getProductionType() == PRODUCTION_UPGRADE )
cancelUpgrade( m_productionQueue->getProductionUpgrade() );
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class ProductionUpdateInterface
virtual UnsignedInt countUnitTypeInQueue( const ThingTemplate *unitType ) const = 0;

virtual Bool queueCreateUnit( const ThingTemplate *unitType, ProductionID productionID ) = 0;
virtual void cancelUnitCreate( ProductionID productionID ) = 0;
virtual void cancelUnitCreate( ProductionID productionID, Bool forceCancel = FALSE ) = 0;
virtual void cancelAllUnitsOfType( const ThingTemplate *unitType) = 0;

virtual void cancelAndRefundAllProduction() = 0;
Expand Down Expand Up @@ -214,7 +214,7 @@ class ProductionUpdate : public UpdateModule, public ProductionUpdateInterface,
virtual UnsignedInt countUnitTypeInQueue( const ThingTemplate *unitType ) const override; ///< count number of units with matching unit type in the production queue

virtual Bool queueCreateUnit( const ThingTemplate *unitType, ProductionID productionID ) override; ///< queue unit to be produced
virtual void cancelUnitCreate( ProductionID productionID ) override; ///< cancel construction of unit with matching production ID
virtual void cancelUnitCreate( ProductionID productionID, Bool forceCancel = FALSE ) override; ///< cancel construction of unit with matching production ID
virtual void cancelAllUnitsOfType( const ThingTemplate *unitType) override; ///< cancel all production of type unitType

virtual void cancelAndRefundAllProduction() override; ///< cancel and refund anything in the production queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ Bool ProductionUpdate::queueCreateUnit( const ThingTemplate *unitType, Productio
//-------------------------------------------------------------------------------------------------
/** Cancel the construction of the unit with the matching production ID */
//-------------------------------------------------------------------------------------------------
void ProductionUpdate::cancelUnitCreate( ProductionID productionID )
void ProductionUpdate::cancelUnitCreate( ProductionID productionID, Bool forceCancel )
{

// search for the production entry in our queue
Expand All @@ -470,8 +470,16 @@ void ProductionUpdate::cancelUnitCreate( ProductionID productionID )
if( production->m_productionID == productionID )
{

// give the player the cost of the object back
Player *player = getObject()->getControllingPlayer();
#if !RETAIL_COMPATIBLE_CRC
// TheSuperHackers @bugfix arcticdolphin 08/03/2026 Prevent cancel once units are produced to avoid free unit exploit
if( !forceCancel && production->getProductionQuantityRemaining() < production->getProductionQuantity() )
{
return;
}
#endif

// give the player the cost of the object back
Money *money = player->getMoney();
money->deposit( production->m_objectToProduce->calcCostToBuild( player ), TRUE, FALSE );

Expand Down Expand Up @@ -693,10 +701,17 @@ UpdateSleepTime ProductionUpdate::update()
// Don't cancel dozers in the queue. jba.
if (!production->getProductionObject()->isKindOf(KINDOF_DOZER))
{

#if RETAIL_COMPATIBLE_CRC
cancelUnitCreate(production->getProductionID());
return UPDATE_SLEEP_NONE;

#else
// TheSuperHackers @bugfix arcticdolphin 13/03/2026 Let partial production finish naturally when script-disallowed.
if( production->getProductionQuantityRemaining() == production->getProductionQuantity() )
{
cancelUnitCreate(production->getProductionID());
return UPDATE_SLEEP_NONE;
}
#endif
}

}
Expand Down Expand Up @@ -1145,7 +1160,7 @@ void ProductionUpdate::cancelAndRefundAllProduction()
if( m_productionQueue )
{
if( m_productionQueue->getProductionType() == PRODUCTION_UNIT )
cancelUnitCreate( m_productionQueue->getProductionID() );
cancelUnitCreate( m_productionQueue->getProductionID(), TRUE );
else if( m_productionQueue->getProductionType() == PRODUCTION_UPGRADE )
cancelUpgrade( m_productionQueue->getProductionUpgrade() );
else
Expand Down
Loading