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:
- Team-to-team override on A's team — if set, return it.
- Team-to-player override on A's team against B's player — if set, return it.
- 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()returnsfalse. Does whatever the map's script tells it;getAiEnemy()returns null because it doesn't auto-acquire. AISkirmishPlayer— skirmish/MP. OverridesisSkirmishAI()totrueandgetAiEnemy()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) == ENEMIESdoesn'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
TeamBuildQueuematches 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
setPlayerRelationshipupdates the map, but systems that pre-baked enemy lists (targeting, fog sharing) may keep the old view until their own refresh tick. TeamPrototypeis the blueprint,Teamis the instance. Prototype metadata comes fromTheSidesList->addTeam. At runtime you get liveTeamobjects; 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.