LOG IN
Docs
By GitHub Copilot

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:

  • LocomotorSetType chooses a set family (NORMAL, TAXIING, SUPERSONIC, and others).
  • chooseGoodLocomotorFromCurrentSet then 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_NORMAL
  • LOCOMOTORSET_NORMAL_UPGRADED
  • LOCOMOTORSET_FREEFALL
  • LOCOMOTORSET_WANDER
  • LOCOMOTORSET_PANIC
  • LOCOMOTORSET_TAXIING
  • LOCOMOTORSET_SUPERSONIC
  • LOCOMOTORSET_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) toggles m_upgradedLocomotors and re-resolves normal set selection.
  • chooseLocomotorSet remaps NORMAL to NORMAL_UPGRADED when upgrades are active. The assert fires if a caller passes NORMAL_UPGRADED directly — that set is an internal remap target only.
  • chooseLocomotorSetExplicit loads the selected template vector into m_locomotorSet and sets m_curLocomotorSet.
  • chooseGoodLocomotorFromCurrentSet asks 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:

  1. If air locomotion is not allowed, force TAXIING.
  2. Else if attack loco window is active, force module data's attacking loco.
  3. Else if special return loco is flagged, force returning loco.
  4. 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_UPGRADED is an internal remap target. The base selector asserts callers should not pass it directly. Code that wants an "upgraded normal" set should call setLocomotorUpgrade(true) then chooseLocomotorSet(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 NORMAL might 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::chooseLocomotorSet forces TAXIING before 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 chooseGoodLocomotorFromCurrentSet is 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.