Skip to content

perf(productionupdate): Simplify and correct implementations of cancel functions in ProductionUpdate - #3270

Merged
xezon merged 2 commits into
TheSuperHackers:mainfrom
xezon:xezon/refactor-productionupdate
Sep 12, 2026
Merged

xezon merged 2 commits into
TheSuperHackers:mainfrom
xezon:xezon/refactor-productionupdate

Conversation

@xezon

@xezon xezon commented Sep 8, 2026

Copy link
Copy Markdown

This change simplifies the implementations of ProductionUpdate::cancelUnitCreate and ProductionUpdate::cancelUpgrade so that internally they can operate straight on the ProductionEntry pointer instead of looking for it in the list again by its ID.

And it reverts ProductionUpdate::cancelAndRefundAllProduction closer to what it did originally to prevent potential CRC mismatches (if cancelUpgrade would fail somehow) after #2399.

TODO

  • Replicate in Generals
  • Test against many replays

@xezon xezon added Minor Severity: Minor < Major < Critical < Blocker Performance Is a performance concern Gen Relates to Generals ZH Relates to Zero Hour Fix Is fixing something, but is not user facing labels Sep 8, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Simplify production cancellation and preserve CRC-safe cleanup

🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Cancels queued units and upgrades directly from their production entries.
• Returns cancellation outcomes through the upgrade cancellation interface.
• Restores head-first cleanup behavior to reduce CRC divergence after cancellation failures.
Diagram

graph TD
  Bulk["Bulk Cleanup"] --> Head["Queue Head"] --> Cancel["Direct Entry Cancel"] --> Success{"Cancellation Succeeds?"}
  API["Public Cancel API"] --> Lookup["Queue Lookup"] --> Cancel
  Success -->|Yes| Refund["Refund and Remove"]
  Success -->|No| Policy["CRC Failure Policy"]
Loading
High-Level Assessment

The chosen approach is appropriate: public callers keep stable identifier/template-based APIs, while internal traversals use existing ProductionEntry pointers to avoid redundant searches. Retaining lookup-based internal cancellation would add unnecessary queue scans, and the restored head-first bulk loop better preserves deterministic CRC behavior when cancellation fails.

Files changed (2) +95 / -71

Bug fix (1) +90 / -68
ProductionUpdate.cppCancel production entries directly with CRC-safe bulk cleanup +90/-68

Cancel production entries directly with CRC-safe bulk cleanup

• Moves unit and upgrade cancellation logic into pointer-based helpers while preserving public queue lookup behavior. Bulk cancellation now repeatedly processes the queue head and explicitly handles failures according to CRC compatibility mode.

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp

Refactor (1) +5 / -3
ProductionUpdate.hExpose upgrade cancellation results and entry-based helpers +5/-3

Expose upgrade cancellation results and entry-based helpers

• Changes the upgrade cancellation interface to return success or failure. Declares protected overloads that cancel units and upgrades directly from ProductionEntry pointers.

GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ProductionUpdate.h

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refactors production cancellation in both Generals variants to operate directly on queue entries, returns cancellation success for upgrades, and adjusts complete-queue cleanup to preserve retail-compatible behavior.

  • Adds protected pointer-based cancellation helpers while retaining the public ID/template interfaces.
  • Avoids redundant queue searches at internal call sites.
  • Makes cancel-and-refund processing repeatedly consume the queue head and explicitly handles failed cancellation.
  • Keeps the Generals and GeneralsMD implementations synchronized.

Confidence Score: 5/5

The PR appears safe to merge; no actionable regression or outstanding repository-rule violation was identified.

Internal callers supply live entries from the owning production queue, the public signature change is consistent across all implementations and callers, and the revised failure handling does not establish a new incorrect state.

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Include/GameLogic/Module/ProductionUpdate.h Changes upgrade cancellation to return success and declares internal queue-entry cancellation helpers.
Generals/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp Centralizes unit and upgrade cancellation around live queue-entry pointers and revises complete-queue cleanup.
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ProductionUpdate.h Mirrors the Generals interface and helper declaration changes.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp Mirrors the Generals pointer-based cancellation and cleanup implementation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Cancellation request] --> B{Public or internal caller?}
    B -->|Public| C[Locate matching queue entry]
    B -->|Internal| D[Use current queue entry]
    C --> E{Entry found?}
    E -->|No| F[Return FALSE]
    E -->|Yes| D
    D --> G{Cancellation allowed?}
    G -->|No| F
    G -->|Yes| H[Refund production cost]
    H --> I[Remove entry from queue]
    I --> J[Delete entry]
    J --> K[Clear player upgrade state when applicable]
    K --> L[Return TRUE]
Loading

Reviews (2): Last reviewed commit: "Replicate in Generals" | Re-trigger Greptile

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Players lose queued-production refunds 🐞 Bug ≡ Correctness
Description
cancelAndRefundAllProduction returns immediately when the head player upgrade cannot be cancelled,
leaving every later production entry untouched. When hasUpgradeInProduction is false for that head
entry, selling or destroying the producer eventually deletes the remaining entries without issuing
their promised refunds.
Code

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp[R1180-1182]

+#if RETAIL_COMPATIBLE_CRC
+				// Cannot cancel the head production... this loop is stuck now and can quit.
+				return;
Evidence
The helper explicitly fails when a queued player upgrade is absent from the player's in-production
state, and the newly added retail branch returns instead of visiting later entries. The method
promises to refund each production item, is called during selling, and the destructor merely deletes
any queue entries left behind, proving that those later items lose their refunds.

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp[1050-1060]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp[1151-1187]
GeneralsMD/Code/GameEngine/Source/Common/System/BuildAssistant.cpp[1576-1581]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp[203-216]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`cancelAndRefundAllProduction` returns on a failed head-upgrade cancellation in retail-compatible builds, so all later queue entries remain unrefunded. Preserve the failed entry if required for compatibility, but continue cancelling and refunding subsequent entries safely.
## Issue Context
`cancelUpgrade(ProductionEntry*)` can return `FALSE` when a player upgrade is no longer marked in production. Selling calls this method before object destruction, whose destructor removes leftover queue entries without refunding them.
## Fix Focus Areas
- GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp[1155-1196]
- GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp[1050-1060]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@Skyaero42

Copy link
Copy Markdown

I'll see if I can run some replays against this.

@Caball009

Copy link
Copy Markdown

Can the title be simplified too? :)

@xezon xezon changed the title perf(productionupdate): Simplify implementations of ProductionUpdate::cancelUnitCreate, ProductionUpdate::cancelUpgrade and revert ProductionUpdate::cancelAndRefundAllProduction closer to what it did originally perf(productionupdate): Simplify and correct implementations of cancel functions in ProductionUpdate Sep 11, 2026
@xezon

xezon commented Sep 11, 2026

Copy link
Copy Markdown
Author

Can the title be simplified too? :)

Simplified :)

@Skyaero42

Copy link
Copy Markdown

I'll see if I can run some replays against this.

4,000 replays run. No issues.
haven't reviewed the code itself yet.

@Skyaero42 Skyaero42 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code looks fine to me.

…:cancelUnitCreate, ProductionUpdate::cancelUpgrade and revert ProductionUpdate::cancelAndRefundAllProduction closer to what it did originally
@xezon
xezon force-pushed the xezon/refactor-productionupdate branch from 52ee8a4 to cde7151 Compare September 12, 2026 08:35
@xezon

xezon commented Sep 12, 2026

Copy link
Copy Markdown
Author

Replicated in Generals with 1 conflict.

D:\Projects\TheSuperHackers\GeneralsGameCode>FOR /F "delims=" %b IN ('git merge-base --fork-point main') DO git diff %b  1>changes.patch

D:\Projects\TheSuperHackers\GeneralsGameCode>git diff 0b9cf8b3e4be1dfc894ff8afe82c1afd98c10886  1>changes.patch

D:\Projects\TheSuperHackers\GeneralsGameCode>git apply -p2 --directory=Generals --reject --whitespace=fix changes.patch
Checking patch Generals/Code/GameEngine/Include/GameLogic/Module/ProductionUpdate.h...
Checking patch Generals/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp...
Hunk #6 succeeded at 1042 (offset -4 lines).
error: while searching for:
{
        // Empirically, in release the code can loop forever.  So we limit to 100 passes. jba. [8/31/2003]
        const Int productionLimit = 100;// With luck, we never queue up 100 units. [8/31/2003]

        Int i = 0;
        ProductionEntry *production = m_productionQueue;
        while( production != nullptr && i < productionLimit )
        {
                ProductionEntry *nextProduction = production->m_next;

                if( production->getProductionType() == PRODUCTION_UNIT )
                {
                        if( !cancelUnitCreate( production->getProductionID() ) )
                        {
                                removeFromProductionQueue( production );
                                deleteInstance( production );
                        }
                }
                else if( production->getProductionType() == PRODUCTION_UPGRADE )
                        cancelUpgrade( production->getProductionUpgrade() );
                else
                {
                        // unknown production type
                        DEBUG_CRASH(( "ProductionUpdate::cancelAndRefundAllProduction - Unknown production type '%d'", production->getProductionType() ));
                        return;
                }

                production = nextProduction;
                ++i;
        }
}


error: patch failed: Generals/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp:1145
Applied patch Generals/Code/GameEngine/Include/GameLogic/Module/ProductionUpdate.h cleanly.
Applying patch Generals/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp with 1 reject...
Hunk #1 applied cleanly.
Hunk #2 applied cleanly.
Hunk #3 applied cleanly.
Hunk #4 applied cleanly.
Hunk #5 applied cleanly.
Hunk #6 applied cleanly.
Rejected hunk #7.

@xezon
xezon merged commit 36a1a68 into TheSuperHackers:main Sep 12, 2026
23 checks passed
@xezon
xezon deleted the xezon/refactor-productionupdate branch September 12, 2026 08:50
fbraz3 added a commit to fbraz3/GeneralsX that referenced this pull request Sep 16, 2026
* bugfix(gamewindow): Remove destroyed windows from the modal stack and prevent duplicate modals for the same window (TheSuperHackers#3224)

* feat(commandline): Add working directory command line options (TheSuperHackers#3149)

Append -useCwd to apply the startup working directory, -setCwd "path" to apply a custom working directory, otherwise it falls back to the default executable working directory

* bugfix(neutronmissile): Fix and improve Nuke Missile damage for large objects inside the outer blast radius (TheSuperHackers#3161)

* bugfix(dozeraiupdate): Fix issue where builders could resume completed tasks after being disabled (TheSuperHackers#2793)

* refactor(milesaudiomanager): Use consistent variable names for PlayingAudio in MilesAudioManager (TheSuperHackers#3254)

* refactor(milesaudiomanager): Simplify MilesAudioManager::notifyOfAudioCompletion() (TheSuperHackers#3254)

* refactor(milesaudiomanager): Simplify MilesAudioManager::findLowestPrioritySound() (TheSuperHackers#3254)

* bugfix(milesaudiomanager): Fix premature 2d and 3d sound cancellations from MilesAudioManager::stopAudioEvent() (TheSuperHackers#3254)

* bugfix(milesaudiomanager): No longer use stopped audio in queries and updates (TheSuperHackers#3254)

* refactor(bink): Replace the Bink SDK stub with a Bink runtime loader (TheSuperHackers#3272)

The Bink SDK stub was linked as an import library, so binkw32.dll had to be
resolvable while the process image was still loading, long before WinMain and
therefore long before the command line was parsed. That is why -setCwd could not
point a build at a retail installation: the working directory it selects is set
far too late to influence how the library is found.

BinkLoader loads binkw32.dll explicitly once BinkVideoPlayer is initialized, at
which point the working directory is final. The Bink functions declared in bink.h
are now ordinary functions that forward to the matching export of the loaded
module, so no call site changes. An unresolved function returns the same neutral
value the stub library returned, which means a missing binkw32.dll disables video
playback instead of preventing the game from starting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(miles): Replace the Miles SDK stub with a Miles runtime loader (TheSuperHackers#3272)

The Miles SDK stub was linked as an import library, so mss32.dll had to be
resolvable while the process image was still loading, long before WinMain and
therefore long before the command line was parsed. That is why -setCwd could not
point a build at a retail installation: the working directory it selects is set
far too late to influence how the library is found.

MilesLoader loads mss32.dll explicitly once the audio device is opened, at which
point the working directory is final. The Miles functions declared in mss/mss.h
are now ordinary functions that forward to the matching export of the loaded
module, so no call site changes. An unresolved function returns the same neutral
value the stub library returned, which means a missing mss32.dll turns audio off
instead of preventing the game from starting.

Nine declarations were dropped along the way, because the retail mss32.dll does
not export them and nothing has called them since they were replaced by their
volume_pan counterparts: AIL_sample_volume, AIL_set_sample_volume, AIL_sample_pan,
AIL_set_sample_pan and the four stream equivalents, plus AIL_open_stream_by_sample.
The MSS_auto_cleanup hook was dropped as well, because its atexit handler would
have called AIL_shutdown after the module was already freed. All 92 remaining
exports were verified to resolve against the retail mss32.dll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(miles): Fix the written primitive types in mss.h and all its call sites; no ABI changes (TheSuperHackers#3272)

* perf(productionupdate): Simplify and correct implementations of cancel functions in ProductionUpdate (TheSuperHackers#3270)

* chore(gamememory): Compile out the memory link tester in Release (TheSuperHackers#3266)

* perf(gamememory): Early exit delete and free functions on null pointer (TheSuperHackers#3266)

* perf(gamememory): Inline preMainInitMemoryManager (TheSuperHackers#3266)

* perf(gamememory): Add overloads for the deletes with size_t argument (TheSuperHackers#3266)

* chore(gamememory): Remove superfluous extern keywords from operator overloads (TheSuperHackers#3266)

* perf(gamememory): Remove unnecessary calls to preMainInitMemoryManager from delete and free functions and make freeBytes noexcept to get rid of EH frame (TheSuperHackers#3266)

* fix(gamefont): Ceil font glyph buffer size to the actual glyph size to prevent a buffer write overflow (TheSuperHackers#3268)

* refactor(particlesys): Parse IsGroundAligned as an enum instead of a boolean (TheSuperHackers#3265)

* ci(release): Stop requesting permissions from the reusable workflow (TheSuperHackers#3276)

* refactor(basetype): Add utility functions to Region and Coord types (TheSuperHackers#3271)

New functions are:
intersectWith, uniteWith for IRegion3D, IRegion2D, Region3D, Region2D
updateMin, updateMax for ICoord3D, ICoord2D, Coord3D, Coord2D
asICoord2D, asCoord2D for ICoord3D, Coord3D

* build(cmake): Add retail compatibility option in CMake config (TheSuperHackers#2379)

RTS_BUILD_OPTION_RETAIL_COMPATIBLE_GAME=DEFAULT/ON/OFF

* bugfix(meshmatdesc): Fix mesh material color processing (TheSuperHackers#3246)

* ci: Restore CI workflow permission compatibility (TheSuperHackers#3286)

* chore: Remove trailing commas in braced initializers that break clang-format's compact layout (TheSuperHackers#3274)

Scoped to comment-free array/struct literals (BorderColors, TeamGeneric,
BezierSegment, GameMemoryInitPools, BFISH, Properties, Scripts) where
clang-format explodes each element onto its own line without this.

* chore(license): Add SPDX-License-Identifier to LICENSE.md (TheSuperHackers#3290)

Helps github detect the license version

* bugfix(pathfinder): Restore Generals retail compatibility after crash fix changes to Pathfinder::findAttackPath (TheSuperHackers#3289)

* feat(recorder): Play a replay file from the command line (TheSuperHackers#3227)

Use -loadreplay <file> as a command line argument to load the replay with full game context

* fix(audio): Copy SoundSceneObjClass state safely (TheSuperHackers#3247)

* fix(hash): Fix initialization of HashTableIteratorClass and make it work with an empty HashTableClass (TheSuperHackers#3284)

* chore(pathfinder): Remove superfluous CPOP_STARTS_FROM_PREV_SEG macro (TheSuperHackers#3295)

* fix(milesaudiomanager): Prevent heap-buffer-overflow read in MilesAudioManager::selectProvider() (TheSuperHackers#3281)

* bugfix(filesystem): Preserve write paths with missing directories (TheSuperHackers#3104)

* perf(pathfinder): Optimize appending node to end of the path (TheSuperHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.

* perf(pathfinder): Take parents cell's position outside of for-loop for optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.

* perf(pathfinder): Remove redundant isCrusher recomputation for optimization (TheSuperHackers#3198)

* ci(windows): make bink and miles runtime stubs optional in build artifacts

* fix(platform): preserve POSIX startup working directory and set Flatpak asset paths

* docs(worklog): document CI fixes and verification for upstream sync PR 304

---------

Co-authored-by: ArcticDolphin <5984296+tintinhamans@users.noreply.github.com>
Co-authored-by: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com>
Co-authored-by: xezon <4720891+xezon@users.noreply.github.com>
Co-authored-by: Stubbjax <11547761+Stubbjax@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: stm <14291421+stephanmeesters@users.noreply.github.com>
Co-authored-by: mirelle7 <115191165+mirelle7@users.noreply.github.com>
Co-authored-by: Caball009 <82909616+Caball009@users.noreply.github.com>
Co-authored-by: Bobby Battista <bobtista@gmail.com>
Co-authored-by: SkyAero <21192585+Skyaero42@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix Is fixing something, but is not user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker Performance Is a performance concern ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants