Locomotor Set Transitions
How AIUpdate parses multiple locomotor sets, switches between them for upgrades and flight phases, and picks the active locomotor for the current cell.
Locomotion in this engine is not one static speed profile per unit. Objects can carry multiple named locomotor sets and switch between them at runtime as AI state changes.
The important distinction is:
LocomotorSetTypechooses a set family (NORMAL,TAXIING,SUPERSONIC, and others).chooseGoodLocomotorFromCurrentSetthen chooses the concrete locomotor inside that set for the current layer and cell.
The Set Types And INI Contract
LocomotorSetType is defined in GeneralsMD/Code/GameEngine/Include/GameLogic/Module/AIUpdate.h and is savegame-stable. The enum includes:
LOCOMOTORSET_NORMALLOCOMOTORSET_NORMAL_UPGRADEDLOCOMOTORSET_FREEFALLLOCOMOTORSET_WANDERLOCOMOTORSET_PANICLOCOMOTORSET_TAXIINGLOCOMOTORSET_SUPERSONICLOCOMOTORSET_SLUGGISH
INI parsing enters through AIUpdateModuleData::parseLocomotorSet in GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp.
ThingTemplate maps the Locomotor field to that parser, and each parsed set is stored in AIUpdateModuleData::m_locomotorTemplates. The parser resolves locomotor template names through TheLocomotorStore, skips None, and supports replacement when a set is re-specified.
A noteworthy compatibility behavior is in that parser: if a mod defines Locomotor on an object without an explicit AIUpdate block, it auto-creates default AI module data so the locomotor definition can still be consumed.
Runtime Selection In AIUpdate
AIUpdateInterface constructs with chooseLocomotorSet(LOCOMOTORSET_NORMAL).
Core switching logic:
void AIUpdateInterface::setLocomotorUpgrade(Bool set) { m_upgradedLocomotors = set; if (m_curLocomotorSet == LOCOMOTORSET_NORMAL || m_curLocomotorSet == LOCOMOTORSET_NORMAL_UPGRADED) chooseLocomotorSet(LOCOMOTORSET_NORMAL); } Bool AIUpdateInterface::chooseLocomotorSet(LocomotorSetType wst) { DEBUG_ASSERTCRASH(wst != LOCOMOTORSET_NORMAL_UPGRADED, ("never pass LOCOMOTORSET_NORMAL_UPGRADED here")); if (wst == LOCOMOTORSET_NORMAL && m_upgradedLocomotors) wst = LOCOMOTORSET_NORMAL_UPGRADED; if (wst == m_curLocomotorSet) return TRUE; if (chooseLocomotorSetExplicit(wst)) { chooseGoodLocomotorFromCurrentSet(); return TRUE; } return FALSE; }
setLocomotorUpgrade(true/false)togglesm_upgradedLocomotorsand re-resolves normal set selection.chooseLocomotorSetremapsNORMALtoNORMAL_UPGRADEDwhen upgrades are active. The assert fires if a caller passesNORMAL_UPGRADEDdirectly — that set is an internal remap target only.chooseLocomotorSetExplicitloads the selected template vector intom_locomotorSetand setsm_curLocomotorSet.chooseGoodLocomotorFromCurrentSetasks pathfinding to pick the best concrete locomotor for current layer/position.
The last step is cell-cached: if layer and pathfind cell did not change, it reuses current locomotor and avoids repeated re-selection.
If no locomotor is valid in the current cell, fallback policy is conservative:
- Prefer previous locomotor if one exists.
- Otherwise attempt ground locomotor from the set.
That protects against transient impossible cells and spawn-in-obstacle edge cases.
Upgrade-Driven Transition
LocomotorSetUpgrade (GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Upgrade/LocomotorSetUpgrade.cpp) is intentionally small. On upgrade application it calls:
ai->setLocomotorUpgrade(true)
From there AIUpdate remaps NORMAL to NORMAL_UPGRADED automatically. No module-specific locomotor-selection logic is duplicated in the upgrade module itself.
Flight/Taxi Phase Overrides
Specialized AI updates override chooseLocomotorSet to enforce phase-specific locomotion.
JetAIUpdate::chooseLocomotorSet (.../AIUpdate/JetAIUpdate.cpp) applies a priority chain:
- If air locomotion is not allowed, force
TAXIING. - Else if attack loco window is active, force module data's attacking loco.
- Else if special return loco is flagged, force returning loco.
- Else fall back to base AIUpdate behavior.
ChinookAIUpdate::chooseLocomotorSet (.../AIUpdate/ChinookAIUpdate.cpp) similarly forces TAXIING when flight status is landed, then delegates to parent behavior.
So the effective set is not just command-driven. It is also flight-state and mode-window driven.
Why Commands No Longer Reset Locomotor Automatically
Multiple movement command handlers in AIUpdate.cpp contain commented historical reset calls and an explanatory note: resetting to normal locomotor on every move command was moved out of generic command flow.
Reason given in code comments:
- Some systems intentionally change locomotor for behavior-specific reasons.
- Automatic reset on unrelated move orders caused incorrect mode loss.
This is why script or behavior logic now owns explicit reset decisions.
Quirks
LOCOMOTORSET_NORMAL_UPGRADEDis an internal remap target. The base selector asserts callers should not pass it directly. Code that wants an "upgraded normal" set should callsetLocomotorUpgrade(true)thenchooseLocomotorSet(LOCOMOTORSET_NORMAL). The two-call dance exists so upgrades are a state attribute, not a new code path.- Save/load serializes both set and locomotor pointers. Compatibility handling for older saves restores by set name because the concrete-locomotor pointer might not be valid post-load. New saves record the pointer so skips the set-type query if possible.
- Concrete locomotor selection is pathfind-position aware. Not merely set-type aware — crossing terrain/layer boundaries can switch the active locomotor even without a set-type change. A hovering unit crossing from land to water within
NORMALmight hot-swap between hover-ground and hover-water locomotors. - Air units can be logically in taxi states. Specialized AI state machines still avoid treating taxi phases as normal airborne movement semantics.
JetAIUpdate::chooseLocomotorSetforcesTAXIINGbefore delegating to the base, so weapon fire/AI behavior on a taxiing jet doesn't accidentally inherit airborne attack locomotion. - Commands no longer reset locomotor automatically. Multiple movement command handlers have commented-out reset calls because unrelated move orders were stripping behavior-specific locomotor changes (flight phases, panic states). Explicit reset decisions now live in the calling behavior, not the generic command dispatch.
- Cell-cache on
chooseGoodLocomotorFromCurrentSetis a performance-critical shortcut. If layer and pathfind cell haven't changed, the current locomotor is reused without re-selection. Without this cache, every AI tick would rerun layer queries against the pathfinder — cheap individually but 30 × N units × per-frame = visible CPU cost.