Skip to content

Hardcoded 3-second timer cap in AISkirmishPlayer::doBaseBuilding() & AISkirmishPlayer::doTeamBuilding() overrides AIData.ini settings #2409

@diqezit

Description

@diqezit

Prerequisites

  • I have searched for similar issues and confirmed this is not a duplicate

Game Version

  • Command & Conquer Generals
  • Command & Conquer Generals: Zero Hour
  • Other (please specify below)

Bug Description

In AISkirmishPlayer::doTeamBuilding() and AISkirmishPlayer::doBaseBuilding(), a hardcoded 3-second cap is applied to m_teamTimer and m_structureTimer during their decrement loops. This silently overrides the dynamic AI build delays calculated from AIData.ini, making the entire AI economic scaling system (Wealthy/Normal/Poor modifiers) non-functional for any delay longer than 3 seconds.

File: GeneralsMD/Code/GameEngine/Source/GameLogic/AISkirmishPlayer.cpp

if (m_teamTimer > 3*LOGICFRAMES_PER_SECOND) {
    m_teamTimer = 3*LOGICFRAMES_PER_SECOND;
}

How the Timers are Used

1. Intended calculation (Based on AIData.ini settings):
When the AI finishes building a team, a base timer is calculated and divided by an economic modifier. For example, if the AI is "Wealthy", the delay is halved. If "Poor", the delay is increased.

TeamSeconds      = 10      ; Base 10 seconds
TeamsWealthyRate = 2.0     ; 10 / 2.0 = 5 seconds
TeamsPoorRate    = 0.6     ; 10 / 0.6 = 16.7 seconds

2. The Hardcap execution:
In the very next logic frame, the doTeamBuilding loop decrements the timer and instantly clamps it:

if (!m_readyToBuildTeam) {
    m_teamTimer--;
    if (m_teamTimer <= 0) { ... }
    
    // THE BUG:
    if (m_teamTimer > 3*LOGICFRAMES_PER_SECOND) {
        m_teamTimer = 3*LOGICFRAMES_PER_SECOND;
    }
}

Expected vs Actual Behavior

(Assuming standard AIData.ini values: Base = 10s, WealthyRate = 2.0, PoorRate = 0.6)

AI Economy State Credits Expected Build Delay Actual Build Delay
Wealthy > 7000 5.0 seconds (150 frames) 3.0 seconds (90 frames)
Normal 2000-7000 10.0 seconds (300 frames) 3.0 seconds (90 frames)
Poor < 2000 16.7 seconds (500 frames) 3.0 seconds (90 frames)

Impact on Gameplay

  1. Economic pressure is broken: Destroying the AI's resource gatherers (forcing them into the "Poor" state) does not slow down their decision-making rate. A poor AI attempts to build armies just as aggressively as a wealthy AI (every 3 seconds).
  2. INI configuration is ignored: Modders cannot increase AI wait times. Changing TeamSeconds = 20 in AIData.ini will still result in exactly 3 seconds in-game.
  3. 6 INI parameters are dead code: TeamSeconds, StructureSeconds, TeamsWealthyRate, TeamsPoorRate, StructuresWealthyRate, and StructuresPoorRate are effectively useless.

Proof via Debug Logging

Instrumenting the code reveals that the INI values are parsed and calculated perfectly, but destroyed on the very next frame:

[AI TEAM SELECTED] player=6 money=45900 state=WEALTHY m_teamTimer=150 (5.0 sec)
[AI TIMER BUG] doTeamBuilding: m_teamTimer=149 CAPPED to 90 (3.0 sec)

Suggested Fix

Remove the hardcap logic entirely from both functions.

1. In AISkirmishPlayer::doTeamBuilding(), remove these 3 lines:

if (m_teamTimer > 3*LOGICFRAMES_PER_SECOND) {
    m_teamTimer = 3*LOGICFRAMES_PER_SECOND;
}

2. In AISkirmishPlayer::doBaseBuilding(), remove these 3 lines:

if (m_structureTimer > 3*LOGICFRAMES_PER_SECOND) {
    m_structureTimer = 3*LOGICFRAMES_PER_SECOND;
}

Alternative approach for backwards compatibility: If a cap is strictly desired by design, it should be exposed as a new configurable parameter in AIData.ini (e.g., MaxTeamDelaySeconds) and evaluated only at the moment the timer is initialized, rather than hardcoded inside the per-frame decrement loop.

Six INI params form scaling system:

TeamSeconds      = 10
Wealthy          = 7000
Poor             = 2000
TeamsWealthyRate = 2.0    ; wealthy AI builds twice as fast: 10 / 2.0 = 5 sec
TeamsPoorRate    = 0.6    ; poor AI builds 40% slower: 10 / 0.6 = 16.7 sec

The initialization code implements this correctly:

m_teamTimer = m_teamSeconds * LOGICFRAMES_PER_SECOND;
if (money < m_resourcesPoor) {
    m_teamTimer = m_teamTimer / m_teamPoorMod;        // 500 frames (16.7 sec)
} else if (money > m_resourcesWealthy) {
    m_teamTimer = m_teamTimer / m_teamWealthyMod;     // 150 frames (5.0 sec)
}

One frame later the calculated value is overwritten:

if (m_teamTimer > 3 * LOGICFRAMES_PER_SECOND) {      // > 90 frames?
    m_teamTimer = 3 * LOGICFRAMES_PER_SECOND;         // replace with 90
}

Every possible result (150, 300, 500) is above 90 so every result gets replaced. The configuration is parsed, the math is performed, the output is correct, and then it is discarded.

Then why design a configurable system with three levels of economics if each result will be overwritten by a constant? It's not clear, but it's interesting.
If consider AIData.ini to be a contract and a configuration point, this is a bug because the behavior does not match the settings and makes them almost meaningless.

Reproduction Steps

  1. Start a skirmish game against an AI opponent.
  2. Use a mod or map editor to set TeamSeconds = 30 in AIData.ini.
  3. Give the AI 0 starting cash (to trigger the Poor state).
  4. Observe the AI's behavior: instead of waiting the expected 50 seconds (30 / 0.6), the AI will evaluate and attempt to build teams exactly every 3 seconds.

Additional Context

This was likely added as a quick, temporary fix during early game development because the AI felt 'too slow,' but maybe dev forgot adjust INI param and remove the hardcap. Another timer in the exact same system (RebuildDelayTimeSeconds = 30;) works ok because it lacks this decrement loop in clamp. Furthermore, adjacent comments in the code (e.g., m_TeamDelay = 2*LOGICFRAMES_PER_SECOND; // Check again in 5 seconds..) demonstrate that this section of code was modified hastily without updating documentation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    AIIs AI relatedBugSomething is not working right, typically is user facingMinorSeverity: Minor < Major < Critical < Blocker

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions