fix(overlord-contain): restore Overlord upgrades on POSIX port (macOS/Linux)#96
Merged
Conversation
- Clear DISABLED_HELD for non-garrisonable portable structures (Gatling, Propaganda) after being contained by Overlord so they remain active - Keep DISABLED_HELD for garrisonable portables (BattleBunker) to preserve their infantry containment logic - Add update() override to sync portable rider position/orientation with host Overlord each frame - Add runtime instrumentation [GX_OVERLORD_TRACE] for diagnostics (WIP) GeneralsX @BugFix copilot 19/04/2026 Overlord upgrades not activating on POSIX port
…m change [WIP] When the Overlord moves, containReactToTransformChange() triggers redeployOccupants() which repositions riders via bone queries. On POSIX, W3D bone queries (getMultiLogicalBonePosition) return wrong world coords, placing the Propaganda Tower at an incorrect position for that frame. If PropagandaTowerBehavior::update() runs in that same frame, doScan() uses the wrong position and finds no allies in range. It clears m_insideList, stopping healing for up to scanDelayInFrames frames (~3s). Fix: override containReactToTransformChange() to immediately correct the portable's position after the base-class bone-based placement. Also refactor position sync into syncPortablePosition() private method shared by update() and containReactToTransformChange(). GeneralsX @BugFix copilot 19/04/2026 Propaganda Tower scan uses wrong position after Overlord move
…s from exiting [WIP] When DISABLED_HELD was cleared on Gatling/Propaganda Tower portables, their locomotors became active. TransportAIUpdate::privateAttackPosition forwards attack commands to all passengers including portables. With an active locomotor, portables receiving an aiAttackPosition on empty ground would try to move to the target, exit the Overlord's contain list, and disappear permanently. HelixContain explicitly keeps DISABLED_HELD on its portables and they still fire and function correctly. The upstream code (TheSuperHackers) never clears DISABLED_HELD for portables either. The real fix for portables not working on POSIX was the position sync introduced in the previous commit (containReactToTransformChange / syncPortablePosition). The clearDisabled was unnecessary and harmful. GeneralsX @BugFix copilot 19/04/2026 Overlord portables disappear on force-attack-position
…verlord [WIP] On Windows/VC6, DISABLED_HELD caused getCurLocomotor() to return null in TransportContain::isSpecificRiderFreeToExit(), which naturally blocked portables (Gatling, Propaganda Tower) from ever exiting their container. On POSIX the locomotor pointer remains valid even when the object is HELD, so portables could pass the exit check and leave the Overlord when receiving aiForceAttackObject or aiAttackPosition commands (e.g., Ctrl+force-attack). Fix: override isSpecificRiderFreeToExit() in OverlordContain to return FALSE for any KINDOF_PORTABLE_STRUCTURE. Portables are permanent upgrades — they must never leave the Overlord under any circumstances while it is alive. GeneralsX @BugFix copilot 19/04/2026 Portables exit Overlord on force-attack
AIExitInstantlyState::onEnter() calls exitObjectViaDoor(obj, DOOR_1) directly without checking reserveDoorForExit, completely bypassing the isSpecificRiderFreeToExit guard added in the previous commit. The Gatling Cannon disappears on force-attack to empty terrain because: 1. AI enters AI_ATTACK_POSITION state 2. AttackStateMachine uses ContinueState for CHASE_TARGET (portable can't move) 3. ContinueState exits with FAILURE -> EXIT_MACHINE_WITH_FAILURE 4. AI_EXIT_INSTANTLY is triggered 5. AIExitInstantlyState::onEnter() calls exitObjectViaDoor(DOOR_1) directly The Propaganda Tower never hits this path since it has no weapons and never enters attack states. Override exitObjectViaDoor in OverlordContain to unconditionally reject exit requests for KINDOF_PORTABLE_STRUCTURE, covering both the normal (AIExitState via reserveDoorForExit) and instant-exit paths.
Instrumentation added during investigation of issue #95 (Gatling force-attack exit bug). Silenced via early return in logOverlordPortableEvent() to avoid log noise in production. To re-enable: remove the 'return;' at the top of logOverlordPortableEvent() in OverlordContain.cpp (GeneralsMD). Search for GX_OVERLORD_TRACE to locate all call sites.
There was a problem hiding this comment.
Pull request overview
Restores Overlord upgrade behavior (portable structures: Gatling/Propaganda/Battle Bunker) on POSIX builds by preventing portable riders from exiting and keeping their transforms synchronized with the host Overlord, matching legacy Windows behavior.
Changes:
- Add transform syncing for the portable rider via
syncPortablePosition()called fromupdate()and after transform changes. - Add explicit engine-level exit guards for
KINDOF_PORTABLE_STRUCTUREviaisSpecificRiderFreeToExit()andexitObjectViaDoor(). - Apply the same behavioral fixes to both Zero Hour (
GeneralsMD/) and base Generals (Generals/) variants.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OverlordContain.cpp | Adds portable transform sync + exit blocking; adds (currently disabled) trace instrumentation and extra logging. |
| GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OverlordContain.h | Declares new overrides and the private sync helper. |
| Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OverlordContain.cpp | Mirrors portable transform sync + exit blocking for base Generals. |
| Generals/Code/GameEngine/Include/GameLogic/Module/OverlordContain.h | Declares the new overrides and sync helper for base Generals. |
fbraz3
added a commit
that referenced
this pull request
Apr 20, 2026
- Replace early-return dead-code pattern in logOverlordPortableEvent
with #if defined(GX_OVERLORD_TRACE) / inline no-op stub, keeping
the body reachable when the flag is on (avoids dead-code warnings
and drift)
- Fix logPassenger condition in isPassengerAllowedToFire: was only
TRUE for portable/infantry, making the rejectKind trace unreachable
since that branch fires for non-portable/non-infantry passengers.
Now set to (passenger != nullptr) so all rejection paths are
traceable when GX_OVERLORD_TRACE is defined
- Remove empty if (KINDOF_PORTABLE_STRUCTURE) {} block in
Generals/OverlordContain.cpp that had no effect
- Replace early-return dead-code pattern in logOverlordPortableEvent
with #if defined(GX_OVERLORD_TRACE) / inline no-op stub, keeping
the body reachable when the flag is on (avoids dead-code warnings
and drift)
- Fix logPassenger condition in isPassengerAllowedToFire: was only
TRUE for portable/infantry, making the rejectKind trace unreachable
since that branch fires for non-portable/non-infantry passengers.
Now set to (passenger != nullptr) so all rejection paths are
traceable when GX_OVERLORD_TRACE is defined
- Remove empty if (KINDOF_PORTABLE_STRUCTURE) {} block in
Generals/OverlordContain.cpp that had no effect
1201fa7 to
9302f72
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OverlordContain.h:103
- The new
private:at line 95 makes several overridden container APIs (isValidContainerFor,addToContain,removeFromContain, etc.) private inOverlordContain. This is a behavioral API change (and differs from the Generals mirror) and can break any call sites that use anOverlordContain*(or reference) rather than the base interface. Move theprivate:down so onlysyncPortablePosition()is private (or add an explicitpublic:before the override block).
private:
void syncPortablePosition(); ///< Sync portable rider position/orientation to the host Overlord.
virtual Bool isValidContainerFor(const Object* obj, Bool checkCapacity) const override;
virtual void addToContain( Object *obj ) override; ///< add 'obj' to contain list
virtual void addToContainList( Object *obj ); ///< The part of AddToContain that inheritors can override (Can't do whole thing because of all the private stuff involved)
virtual void removeFromContain( Object *obj, Bool exposeStealthUnits = FALSE ) override; ///< remove 'obj' from contain list
virtual void removeAllContained( Bool exposeStealthUnits = FALSE ) override; ///< remove all objects on contain list
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes Overlord tank upgrades (Gatling Cannon, Propaganda Tower, BattleBunker) on the macOS/Linux POSIX port.
On POSIX builds, portable riders were able to hit engine paths that should never apply to permanent Overlord upgrades. This PR restores expected behavior by hard-blocking portable exit paths and preventing portable fire-point redeploy during turret transform updates.
Closes #95
Root Cause
Two behavior differences combined to break upgrade permanence on POSIX:
DISABLED_HELDeffectively blocked rider exits via locomotor state.OverlordContain::containReactToTransformChange()triggered baseOpenContain::redeployOccupants().putObjAtNextFirePoint()on portable riders, which calledsetTransformMatrix()and then riderreactToTransformChange().GameLogic::destroyObject).Final Changes
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OverlordContain.cppsyncPortablePosition()and wired it into:update()containReactToTransformChange()isSpecificRiderFreeToExit()returnsFALSEforKINDOF_PORTABLE_STRUCTUREexitObjectViaDoor()early-returns forKINDOF_PORTABLE_STRUCTUREredeployOccupants()override:KINDOF_PORTABLE_STRUCTUREridersclearDisabled(DISABLED_HELD)fromonContaining()GX_OVERLORD_TRACEinstrumentation used for diagnosis was removed after validationGeneralsMD/Code/GameEngine/Include/GameLogic/Module/OverlordContain.hupdate()containReactToTransformChange()isSpecificRiderFreeToExit()exitObjectViaDoor()redeployOccupants()syncPortablePosition()Generals/mirror changesSession 2026-04-20 Validation
Platform: macOS ARM64 (
macos-vulkanpreset)Observed in gameplay:
GameLogic::reset/clearGameData)Build validation:
cmake --build build/macos-vulkan --target z_generalscompleted (warnings only, no new errors)Notes
This fix keeps the determinism-sensitive gameplay model intact by constraining behavior inside
OverlordContain(backend/platform adaptation layer), without introducing gameplay feature changes.