bugfix(production): Prevent cancel if units were already produced#2399
bugfix(production): Prevent cancel if units were already produced#2399tintinhamans wants to merge 1 commit intoTheSuperHackers:mainfrom
Conversation
|
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp
Show resolved
Hide resolved
8adfb95 to
9f84a82
Compare
| Money *money = player->getMoney(); | ||
| money->deposit( production->m_objectToProduce->calcCostToBuild( player ), TRUE, FALSE ); | ||
| #else | ||
| // TheSuperHackers @bugfix arcticdolphin 04/03/2026 Do not refund if units were already produced from this entry |
There was a problem hiding this comment.
Does this mean when cancelling the Red Guard after 1 guy exited, no refund is made, but only guy is given? This would be not great.
It better refunds the remaining fair value.
Or it should not be possible to cancel as soon as partial production has finished.
There was a problem hiding this comment.
It better refunds the remaining fair value.
I feel like this could be abused, you could get 1 Red Guard then for $150 if we simply halved refund.
Or it should not be possible to cancel as soon as partial production has finished.
Yea, that was my other proposal - noop cancelling if production->getProductionQuantityRemaining() < production->getProductionQuantity()
There was a problem hiding this comment.
I feel like this could be abused, you could get 1 Red Guard then for $150 if we simply halved refund.
Maybe this is fine. Adds a new skill to the game? For example if the China guy just needs 1 Red Guard to capture an Oil Derrick nearby. China can need any help it can get. And it does not affect the stronger China Infantry General which is good too.
@Stubbjax thoughts?
There was a problem hiding this comment.
China can need any help it can get.
Except free units via well-timed production queue cancellations. 😛
It does feel like a rather unorthodox place to intentionally install a skill differentiator. Is a reasonable player likely to deliberately risk cancelling the whole unit for the sake of saving $150? I imagine it's more commonly a fluke that leads to confusion or surprise, which is generally better to minimise where possible. It is fairly trivial and would be incredibly rare either way so I don't feel too strongly about it in this regard.
The main reservation I have with the partial refund approach is when it comes to an indivisible BuildCost ÷ QuantityModifier. Tank General's Red Guard costs $375 - how would half of this be refunded without $1 being unaccounted for?
Preventing cancellation once the first unit has exited seems like the cleanest approach to me.
There was a problem hiding this comment.
OK I have updated this now to prevent cancelling entirely if production->getProductionQuantityRemaining() < production->getProductionQuantity().
AFAIK this time window is always quite short anyways so should have minimal impact.
9f84a82 to
466c11e
Compare
Generals/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp
Outdated
Show resolved
Hide resolved
Signed-off-by: tintinhamans <[email protected]>
466c11e to
3c23298
Compare
| #if !RETAIL_COMPATIBLE_CRC | ||
| // TheSuperHackers @bugfix arcticdolphin 04/03/2026 Do not cancel if units were already produced from this entry | ||
| if( production->getProductionQuantityRemaining() < production->getProductionQuantity() ) | ||
| { | ||
| return; | ||
| } | ||
| #endif |
There was a problem hiding this comment.
Critical: Early return breaks cancelAndRefundAllProduction and script-disable cleanup
The early return at line 477 exits without removing the entry from m_productionQueue. This breaks two callers that depend on cancelUnitCreate always removing the entry:
-
cancelAndRefundAllProduction(line 1140): This function loops up to 100 times, callingcancelUnitCreate(m_productionQueue->getProductionID())and expecting the head entry to be removed each iteration. With this early return,m_productionQueuenever changes — the loop exhausts all 100 iterations on the same stuck entry without clearing the queue. Partially-produced batch entries become permanent orphans when the building is sold/destroyed. -
Script-disabled unit path (line 696–709): When a script disables a unit type mid-production, this code calls
cancelUnitCreateto flush the entry. If one unit was already produced, the early return leaves the entry active. The next frame, the update loop hits this condition again and callscancelUnitCreaterepeatedly on the same entry — a busy-loop / stuck production state.
The exploit fix is logically correct (no refund for partially-produced units), but the implementation should still remove the entry. The safest fix is to refund conditionally but always remove:
if( production->getProductionQuantityRemaining() == production->getProductionQuantity() )
{
Money *money = player->getMoney();
money->deposit( production->m_objectToProduce->calcCostToBuild( player ), TRUE, FALSE );
}
removeFromProductionQueue( production );
deleteInstance(production);
return;Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp
Line: 473-479
Comment:
**Critical: Early return breaks `cancelAndRefundAllProduction` and script-disable cleanup**
The early `return` at line 477 exits without removing the entry from `m_productionQueue`. This breaks two callers that depend on `cancelUnitCreate` always removing the entry:
1. **`cancelAndRefundAllProduction`** (line 1140): This function loops up to 100 times, calling `cancelUnitCreate(m_productionQueue->getProductionID())` and expecting the head entry to be removed each iteration. With this early return, `m_productionQueue` never changes — the loop exhausts all 100 iterations on the same stuck entry without clearing the queue. Partially-produced batch entries become permanent orphans when the building is sold/destroyed.
2. **Script-disabled unit path** (line 696–709): When a script disables a unit type mid-production, this code calls `cancelUnitCreate` to flush the entry. If one unit was already produced, the early return leaves the entry active. The next frame, the update loop hits this condition again and calls `cancelUnitCreate` repeatedly on the same entry — a busy-loop / stuck production state.
The exploit fix is logically correct (no refund for partially-produced units), but the implementation should still remove the entry. The safest fix is to refund conditionally but always remove:
```cpp
if( production->getProductionQuantityRemaining() == production->getProductionQuantity() )
{
Money *money = player->getMoney();
money->deposit( production->m_objectToProduce->calcCostToBuild( player ), TRUE, FALSE );
}
removeFromProductionQueue( production );
deleteInstance(production);
return;
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Hmm, maybe I need to add a forceCancel parameter for these cases.
There was a problem hiding this comment.
That's a solid approach! A forceCancel parameter would give you explicit control over the two different scenarios:
- Player-initiated cancel (default,
forceCancel=false): Block if units already produced (your current bugfix intent) - System cleanup (
forceCancel=true): Always remove the entry regardless of production state, used bycancelAndRefundAllProductionand the script-disable path
The implementation would look something like:
void ProductionUpdate::cancelUnitCreate( ProductionID productionID, bool forceCancel = false )
{
// ... find production ...
Player *player = getObject()->getControllingPlayer();
#if !RETAIL_COMPATIBLE_CRC
bool partiallyProduced = production->getProductionQuantityRemaining() < production->getProductionQuantity();
// Block player-initiated cancel if units already produced
if( !forceCancel && partiallyProduced )
{
return;
}
// Only refund if nothing was produced yet
if( !partiallyProduced )
{
Money *money = player->getMoney();
money->deposit( production->m_objectToProduce->calcCostToBuild( player ), TRUE, FALSE );
}
#endif
// Always remove the entry (either forceCancel=true or full refund case)
removeFromProductionQueue( production );
deleteInstance(production);
}Then call sites:
cancelAndRefundAllProduction:cancelUnitCreate(id, true)- Script-disable path:
cancelUnitCreate(id, true) - Player UI cancel:
cancelUnitCreate(id, false)or justcancelUnitCreate(id)(default)
This preserves the exploit fix while handling the cleanup scenarios correctly.
This pull request updates the logic for refunding player resources when canceling unit production, addressing a bug where players could receive a refund even after some units had already been produced.
Once
production->getProductionQuantityRemaining() < production->getProductionQuantity(), it is no longer possible to cancel production.Resolves #133 (China Red Guard can be built for free when construction is cancelled in last moment)
Before:
mRefund_before_FFB.mp4
After :
coming soon