LOG IN
Docs
By Opus

Players, Teams, And AI

How Player, Team, and AIPlayer work together — who owns units, how enemy relationships are determined, how skirmish AI runs.

Ownership in Generals is a three-layer stack. Objects (units, structures, rubble) belong to a Team. Teams belong to a Player. Two players default to a Relationship that then cascades down to their teams. An AIPlayer is an optional brain bolted onto a Player — if it's missing, the Player just sits there and waits for scripts or a human to move its units.

That's the whole model. Almost everything else — targeting, line-of-sight ally sharing, money, science — is built on those four words: object, team, player, relationship.

Teams own objects, Players own teams

An Object is assigned to a Team, and the team holds the canonical list:

// GeneralsMD/Code/GameEngine/Include/Common/Team.h
class Team : public MemoryPoolObject, public Snapshot
{
    // ...
    TeamPrototype  *m_proto;           ///< the prototype used to create this Team
    TeamID          m_id;              ///< unique team id

    MAKE_DLINK_HEAD(Object, TeamMemberList)   ///< the members of this team
    // ...
    TeamRelationMap   *m_teamRelations;    ///< override allies & enemies
    PlayerRelationMap *m_playerRelations;  ///< override allies & enemies
};

TeamPrototype is the blueprint — parsed from the map's SidesList. A Team is a live instance built from that prototype. A single Player routinely owns many Teams: one per task force, plus a default team for anything unassigned. Team::getControllingPlayer() walks up to that Player, which is why asking "whose unit is this?" always terminates at a Player, not a Team.

See Team.h at lines 183-244, and Player.h around line 206.

Relationship resolution

There are exactly three relationships:

// Core/GameEngine/Include/Common/GameCommon.h
enum Relationship : Int
{
    ENEMIES,
    NEUTRAL,
    ALLIES,

    RELATIONSHIP_COUNT
};

When code asks "is A hostile to B?", the lookup cascades in this order:

  1. Team-to-team override on A's team — if set, return it.
  2. Team-to-player override on A's team against B's player — if set, return it.
  3. Player-to-player default — A's player looks up B's player in its relation map.

The comment in Team::getRelationship in Team.h spells it out: if there's a TeamRelationship between the two teams, return it; otherwise fall back to the player relationship. Player::getRelationship(const Team *that) at line 559 is the usual entry point from gameplay code.

Semantics:

  • ENEMIES — valid attack target; fire-at-will and auto-acquire pick from here.
  • NEUTRAL — don't shoot, don't share vision. Civilians and scenery live here.
  • ALLIES — don't target, share radar/sight per faction rules, can be healed and garrisoned.

AIPlayer: the brain

AIPlayer (at AIPlayer.h around line 149) is attached to a Player in its constructor: AIPlayer(Player *p). Each logic tick, the virtual update() runs, which internally drives:

  • doBaseBuilding() — walks the build list, finds dozers, places structures.
  • checkQueuedTeams() / checkReadyTeams() — the two-stage team pipeline.
  • doTeamBuilding() — pushes production orders to idle factories.
  • doUpgradesAndSkills() — spends science points and buys upgrades.

The teams-in-flight live on two linked lists:

MAKE_DLINK_HEAD(TeamInQueue, TeamBuildQueue);   ///< List of teams being built
MAKE_DLINK_HEAD(TeamInQueue, TeamReadyQueue);   ///< List of teams built, waiting to reach rally point.

A team is queued, its units are trained one by one via onUnitProduced, and when complete it moves to the ready queue until it reaches the rally point.

Two flavors ship:

  • Base AIPlayer — scripted campaign AI. isSkirmishAI() returns false. Does whatever the map's script tells it; getAiEnemy() returns null because it doesn't auto-acquire.
  • AISkirmishPlayer — skirmish/MP. Overrides isSkirmishAI() to true and getAiEnemy() to return a real target. Auto-picks an enemy, scales with difficulty, cycles through personality skillsets.

Civilian players and neutrals

The civilian player owns trees, rubble, debris, stray pedestrians. It's a full Player — it has teams, it has an index in ThePlayerList — but it has no AIPlayer attached. Nothing thinks on its behalf. Civilian units move only because a BehaviorModule like CivilianWander drives them locally.

This is why destroying a civilian map prop doesn't trigger AI retribution: there's no brain to retribute. It's also why civilian-owned objects default to NEUTRAL against everyone — killing them counts for collateral-damage scoring but never makes the civilian player "aware" of the attacker.

Quirks

  • Same side, different Players. Two human allies in MP are separate Players who happen to be ALLIES. They each have their own economy, science, and team list. Alliance is a relationship, not a merger.
  • Relationships are directional. A.getRelationship(B) == ENEMIES doesn't imply the reverse. Scripts that break an alliance one-way will produce odd behavior if only one side checks. Always look up from both ends when it matters.
  • A stalled build queue idles silently. If no entry in TeamBuildQueue matches a buildable structure — maybe the factory got sniped — the AI keeps iterating and keeps doing nothing. No warning, no fallback. Map scripts have to notice and restart.
  • Mid-game relationship changes don't invalidate caches. Calling setPlayerRelationship updates the map, but systems that pre-baked enemy lists (targeting, fog sharing) may keep the old view until their own refresh tick.
  • TeamPrototype is the blueprint, Team is the instance. Prototype metadata comes from TheSidesList->addTeam. At runtime you get live Team objects; editing the prototype after load does nothing to already-instanced teams.
  • Civilian player has no AI. If a map author wants civilians to flee or retaliate, it's scripted at the trigger level or baked into the unit's BehaviorModule list. The Player itself is inert.