Sam Afshari's Notes
  • GitHub
  • Twitter/X
  • All posts
  • Ed
  • NuGets
  • POIWorld
  • RedCorners

AI Decision Making in Generals

Thu, Jul 2, 2026

The AI in Command & Conquer: Generals is not one large planner looking at the map and inventing a strategy. It is a set of cooperating systems with different jobs.

The build list says what the base can contain and where each structure belongs. Team templates describe useful groups of units. Scripts decide when those teams should be produced and what mission they receive. C++ code handles the repeated work of checking money, prerequisites, factories, construction safety, target validity, movement, and weapon use.

That division is the key to understanding the AI. Most of its apparent strategy comes from authored data and scripts. The engine turns those instructions into legal game actions and keeps them running when factories are busy, builders die, targets disappear, or a team is only partly complete.

Four layers of AI

I find it useful to separate the system into four layers.

  1. AIData.ini contains global timing, economy, targeting, and faction data.
  2. AISkirmishPlayer manages the base, production queues, enemy selection, and completed teams.
  3. Skirmish scripts and team templates decide which team is useful and what it should do.
  4. Each object's AIUpdate state machine performs movement, guarding, hunting, and combat.

The global AI update runs pathfinding and then updates every player:

void AI::update(void)
{
    m_pathfinder->processPathfindQueue();
    ThePlayerList->UPDATE();
}

A skirmish computer player receives an AISkirmishPlayer. A scripted campaign computer player normally receives the more general AIPlayer. The skirmish subclass enables automatic unit production, chooses an enemy, installs the faction's skirmish build list, and adds skirmish-specific base behavior.

This distinction matters. Campaign AI is mostly scenario scripting. Skirmish AI has reusable economic and production loops, but it still depends heavily on scripts for strategic timing.

The base is a transformed template

The AI does not freely design a base from scratch. AIData.ini contains a SkirmishBuildList for every faction. Each entry identifies a structure, a position, an angle, whether it should be built automatically, and how many times it may be rebuilt.

A typical entry looks like this:

Structure AmericaCommandCenter
  Location = X:501.22 Y:546.25
  Rebuilds = -1
  Angle = -135.00
  InitiallyBuilt = No
  AutomaticallyBuild = Yes
END

When a match starts, AISkirmishPlayer::newMap() duplicates the correct faction list and transforms it around the player's actual starting position. That gives the AI a planned base layout without hard-coding one layout into each map.

This also explains some familiar behavior. The AI often rebuilds a structure in the same place because the location belongs to a persistent build-list slot. It is restoring the authored plan, not searching the map for a better design.

Scripts can add urgency by marking a missing slot as a priority build. Automatic slots are considered by the normal base loop. Non-automatic slots are skipped unless a script marks one as a priority or a special rule, such as the low-power override, promotes it.

When it constructs a building

Base construction is a guarded polling loop. The current skirmish code checks periodically rather than reconsidering the whole base every frame. Completing a unit or structure shortens the delay so that a changed economy or tech tree is noticed quickly.

When construction is allowed, the AI scans the build list and rejects slots that cannot be acted on. A candidate must be missing, safe, buildable, affordable through the normal construction rules, and have an available builder. The preferred location is checked for path clearance, terrain restrictions, and object overlap. If it is blocked, the skirmish AI searches a large expanding perimeter for a legal replacement point.

The selection order contains several deliberate exceptions:

  • A script-requested priority structure wins over an ordinary automatic structure.
  • If the player lacks sufficient power, a power-producing slot is forced ahead of the normal choice.
  • An unfinished structure is resumed, and a replacement dozer or worker is requested if its builder died.
  • A destroyed or captured structure waits for its rebuild delay before becoming eligible again.
  • Only one structure starts during a construction pass.

The power override is particularly direct:

if (powerPlan && powerInfo && !powerPlan->isEquivalentTo(bldgPlan)) {
    if (!powerUnderConstruction) {
        bldgPlan = powerPlan;
        bldgInfo = powerInfo;
    }
}

The final action is not a silent state edit. buildStructureWithDozer() creates the construction job and assigns a real dozer or worker. Normally the builder travels to the site and the ordinary construction state machine advances the structure to completion.

There is one old RTS-style escape hatch. If the chosen builder cannot path to an otherwise accepted site, this implementation teleports the builder to the construction point and continues. The AI therefore prefers legal, reachable placement, but it will not let a pathfinding failure permanently deadlock its base plan.

The AI also checks whether a location is safe before rebuilding there. Its safety query ignores dead units, undetected stealth units, insignificant structures, harvesters, and dozers, then looks for a meaningful enemy within the configured radius. A structure slot near active enemy forces is postponed instead of fed into the construction queue immediately.

Economy changes the cadence

AIData.ini defines “poor” and “wealthy” cash levels, along with rate modifiers for structures and teams. After a successful selection, the next timer is adjusted using those bands. A wealthy AI attempts another team sooner. A poor AI waits longer.

The current data sets the normal team interval to ten seconds, with a 2.0 wealthy modifier and a 0.6 poor modifier. These are not guarantees that a team will begin every ten seconds. They only decide when the next attempt is allowed. The attempt can still fail because a factory is absent, busy, technologically blocked, or short of money.

Supply production has a separate feedback loop. When a supply center finishes, the AI records a desired gatherer count from faction and difficulty data. It counts surviving harvesters assigned to that center and queues replacements while nearby supplies still contain value. This is why the AI can recover workers or trucks without a strategy script naming each replacement.

Difficulty directly changes those target counts. In the current data, higher tiers assign more gatherers per supply center. That gives harder players a larger economic pipeline without changing the core queue algorithm.

The current Generals64 fork also makes Nightmare explicit rather than subtle. It permanently reveals the map for that AI, shortens normal rebuilding to five seconds, and refunds structure cost so legal dozer construction nets to zero. In other words, the normal decision path is still used, but this tier deliberately gives it information, recovery, and economic advantages.

Units are built as teams

The strategic production unit is not an individual tank or infantryman. It is a TeamPrototype.

A prototype describes a composition using minimum and maximum counts, a production priority, a maximum number of simultaneous instances, optional automatic reinforcement, scripts, and a production condition. A team might require a core composition and request extra optional units if there is time and production capacity.

Before a team can be selected, the engine checks all of these conditions:

if (!proto->evaluateProductionCondition())
    return false;

if (proto->countTeamInstances() >=
    proto->getTemplateInfo()->m_maxInstances)
    return false;

if (!isPossibleToBuildTeam(proto, true, needMoney))
    return false;

isPossibleToBuildTeam() verifies that suitable factories exist, that at least one is idle when required, and that the AI has a configurable fraction of the estimated team cost. The default TeamResourcesToStart value is 0.1, so the AI may start organizing a team before it has cash for the full maximum composition. Each actual production order still passes through the factory and economy rules.

Among all valid prototypes, the AI finds the highest production priority. If several have the same priority, it uses the synchronized game-logic random generator to choose one. Before creating a new team, it may reinforce an existing high-priority team that has fallen below its requested composition.

This is a simple but effective policy. Conditions establish context, priority expresses importance, and random selection prevents equal-priority armies from always appearing in the same order.

The priorities can also change during a match. Script actions can increase a prototype's production priority after success or decrease it after failure:

void TeamPrototype::increaseAIPriorityForSuccess() const
{
    m_teamTemplate.m_productionPriority +=
        m_teamTemplate.m_productionPrioritySuccessIncrease;
}

That is authored feedback, not machine learning. The designer decides what counts as success or failure and how much the priority changes.

Work orders connect strategy to factories

After choosing a prototype, the AI expands its composition into WorkOrder records. Required units use the minimum counts. Optional orders cover the distance from each minimum to its maximum.

For every waiting order, the AI first tries to recruit a matching idle unit near the team's home position. If none is available, it finds a compatible production building and queues the unit through ProductionUpdateInterface:

Object *factory = findFactory(order->m_thing, busyOK);
ProductionUpdateInterface *pu =
    factory ? factory->getProductionUpdateInterface() : NULL;

if (pu && pu->queueCreateUnit(order->m_thing,
                              pu->requestUniqueUnitID())) {
    order->m_factoryID = factory->getID();
}

New units are assigned directly to the inactive team. If a factory is destroyed while training a member, the work order notices that its factory ID is no longer valid and can be assigned again.

A team does not have to reach its ideal maximum before acting. If its build time expires after the minimum composition exists, it stops waiting for optional members. If even the minimum cannot be built, the incomplete team is disbanded.

Completed teams move to a ready queue. The engine waits for the members to become idle at their rally point, but it also has a sixty-second escape hatch so a confused member cannot hold the team forever. The team is then marked active.

Scripts decide when the attack begins

The production manager builds forces, but it usually does not invent their mission. Team and player scripts provide the strategic trigger.

Each team prototype can name scripts for creation, idle state, enemy sighting, unit loss, and destruction. When a completed team becomes active, Team::updateState() runs its creation script:

if (m_created) {
    m_created = false;
    if (!pInfo->m_scriptOnCreate.isEmpty())
        TheScriptEngine->runScript(pInfo->m_scriptOnCreate, this);
}

That script can tell the team to attack another team, attack an area, follow an attack waypoint path, hunt, guard, load transports, use a command button, or wait for another condition. The script system also exposes actions for building a named team, requesting a structure, changing production priority, and enabling or disabling production.

This is where most answers to “why did the AI attack now?” live. A script condition became true, a team reached its minimum viable composition, and activation ran an action. The engine does not apply one universal formula such as “attack when army value exceeds 5,000.” Different factions and team templates can have different conditions and different missions.

An idle script can send a surviving team back into action after its first target or waypoint sequence ends. Enemy-sighted and unit-destroyed hooks can redirect behavior. The script layer turns production events and battlefield events into strategic orders.

Choosing an enemy player

Skirmish AI also maintains a current enemy. It rechecks periodically and normally keeps its existing target while that player still has units and a build facility.

When it needs a new enemy, it starts with distance between base centers. It then applies two social adjustments. A crippled opponent is deprioritized so the AI concentrates on a healthier threat. An opponent already targeted by another skirmish AI receives a penalty, reducing dogpiling. An AI that is attacking this player gets a small preference in return.

The result is still a heuristic, but it produces understandable strategic behavior: attack a nearby viable opponent, spread AI players across targets when practical, and slightly favor retaliation.

Choosing a target inside the battle

Once a team has an attack or hunt order, object-level AI takes over. It filters the world before selecting a target. Candidates normally must be living, on the same map layer, hostile, attackable by the unit, and not hidden by stealth. Line of sight, fog, insignificant buildings, and whether buildings may be attacked are controlled by flags and AI data.

Without a custom attack-priority set, the closest valid enemy wins. With one, the engine combines the authored priority with distance:

Int curPriority = info->getPriority(theEnemy->getTemplate());
Real dist = sqrt(ThePartitionManager->getDistanceSquared(
    me, theEnemy, FROM_BOUNDINGSPHERE_2D));
Int modifier = dist /
    TheAI->getAiData()->m_attackPriorityDistanceModifier;
Int modPriority = curPriority - modifier;
if (modPriority < 1)
    modPriority = 1;

A distant high-value target can beat a nearby low-value target, but distance gradually erodes its score. Occupants also matter: if a garrisoned building or vehicle contains a higher-priority unit, the container inherits that higher value during selection.

The attack itself is a state machine. It approaches, chooses a usable weapon, fires, and reacts to movement or loss of the target. When the victim is gone, hunt-style behavior selects another valid victim. When none remains, the state completes and the team's idle script may decide what comes next.

Difficulty can change target coordination. In squad attacks, Easy selects a random member of the victim squad, Normal selects the closest, and Hard or above makes attackers choose the same member. Higher difficulty therefore concentrates fire without requiring a different movement or weapon system.

General powers and superweapons

General powers use the same data-driven approach. Each faction has one or more ordered skill sets in AIData.ini. A skirmish player randomly selects one defined set, then spends available science points on the first entries it is currently capable of purchasing.

Superweapon placement uses a value search rather than firing at the first visible building. The AI samples a coarse grid over the enemy's structure bounds, scores the objects covered by the weapon radius, and then performs a finer search around the best coarse point. Normal destructive powers favor expensive concentrations. Sneak Attack reverses the value of military units and base defenses, so it looks for a valuable but weakly defended insertion point. Cluster mines are handled separately and are placed near an approach to the AI's own base.

What the AI is really doing

The Generals AI is best described as a deterministic production and behavior engine driven by authored plans.

It constructs because a build-list slot is eligible, safe, legal, and currently important. It produces units because a team condition is true and that prototype has the best available priority. It attacks because a team script starts a mission after the team becomes viable. It chooses individual victims by filtering what the unit can perceive and attack, then applying distance and optional target priorities.

There is no single strategic brain. The behavior emerges from the boundary between data and code:

  • data defines faction economics, base slots, team compositions, and tuning;
  • scripts define intent, timing, and reactions;
  • AIPlayer turns intent into construction and production queues;
  • AIUpdate turns orders into movement and combat.

That structure is old-fashioned, but it is practical. Designers can change strategy without rewriting pathfinding or combat code, while programmers can improve construction recovery, target filtering, and state machines without rewriting every faction's plan. It also makes the AI debuggable. When it does something strange, there is usually a specific build-list flag, team condition, script action, timer, or state transition to inspect.

Back to Home


© Sam Afshari 2026