Skip to content

fix(overlord-contain): restore Overlord upgrades on POSIX port (macOS/Linux)#96

Merged
fbraz3 merged 11 commits into
mainfrom
fix/overlord-upgrades-not-applying
Apr 21, 2026
Merged

fix(overlord-contain): restore Overlord upgrades on POSIX port (macOS/Linux)#96
fbraz3 merged 11 commits into
mainfrom
fix/overlord-upgrades-not-applying

Conversation

@fbraz3

@fbraz3 fbraz3 commented Apr 20, 2026

Copy link
Copy Markdown
Owner

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:

  1. Exit path mismatch:
  • On Windows/VC6, DISABLED_HELD effectively blocked rider exits via locomotor state.
  • On POSIX, locomotor state remained available in cases where legacy code expected a block, allowing portable riders to leave through AI-driven exit flows.
  1. Turret-rotation redeploy path:
  • During turret turning, OverlordContain::containReactToTransformChange() triggered base OpenContain::redeployOccupants().
  • That path used putObjAtNextFirePoint() on portable riders, which called setTransformMatrix() and then rider reactToTransformChange().
  • Portable riders could then be destroyed by normal object-destruction flow (GameLogic::destroyObject).

Final Changes

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OverlordContain.cpp

  • Added syncPortablePosition() and wired it into:
    • update()
    • containReactToTransformChange()
  • Added portable exit guards:
    • isSpecificRiderFreeToExit() returns FALSE for KINDOF_PORTABLE_STRUCTURE
    • exitObjectViaDoor() early-returns for KINDOF_PORTABLE_STRUCTURE
  • Added redeployOccupants() override:
    • skips fire-point redeploy for KINDOF_PORTABLE_STRUCTURE riders
    • keeps base redeploy behavior for non-portable occupants
  • Removed erroneous clearDisabled(DISABLED_HELD) from onContaining()
  • Temporary GX_OVERLORD_TRACE instrumentation used for diagnosis was removed after validation

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

  • Added declarations for:
    • update()
    • containReactToTransformChange()
    • isSpecificRiderFreeToExit()
    • exitObjectViaDoor()
    • redeployOccupants()
    • syncPortablePosition()

Generals/ mirror changes

  • Equivalent fixes were kept in the base-game path for consistency where behavior is shared.

Session 2026-04-20 Validation

Platform: macOS ARM64 (macos-vulkan preset)

Observed in gameplay:

  • Gatling Cannon upgrade remains attached and firing during force-attack/turret rotation
  • Propaganda Tower remains attached and healing
  • BattleBunker remains attached and retains occupants
  • No gameplay-time portable removal events in the final trace window
  • Removal events observed at match reset/score transition map to expected cleanup (GameLogic::reset / clearGameData)

Build validation:

  • cmake --build build/macos-vulkan --target z_generals completed (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.

fbraz3 added 6 commits April 19, 2026 20:23
- 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.

Copilot AI 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.

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 from update() and after transform changes.
  • Add explicit engine-level exit guards for KINDOF_PORTABLE_STRUCTURE via isSpecificRiderFreeToExit() and exitObjectViaDoor().
  • 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.

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OverlordContain.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OverlordContain.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OverlordContain.cpp Outdated
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
@fbraz3 fbraz3 force-pushed the fix/overlord-upgrades-not-applying branch from 1201fa7 to 9302f72 Compare April 20, 2026 03:02

Copilot AI 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.

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 in OverlordContain. This is a behavioral API change (and differs from the Generals mirror) and can break any call sites that use an OverlordContain* (or reference) rather than the base interface. Move the private: down so only syncPortablePosition() is private (or add an explicit public: 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

Comment thread Generals/Code/GameEngine/Include/GameLogic/Module/OverlordContain.h
Comment thread docs/WORKDIR/lessons/2026-04-LESSONS.md Outdated
Comment thread .github/instructions/git-commit.instructions.md Outdated
fbraz3 and others added 3 commits April 20, 2026 20:27
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@fbraz3 fbraz3 merged commit e52dfcf into main Apr 21, 2026
8 checks passed
@fbraz3 fbraz3 deleted the fix/overlord-upgrades-not-applying branch April 21, 2026 03:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Overlord Gatling Cannon disappears on force-attack empty terrain (POSIX port)

2 participants