Skip to content

AISkirmishPlayer::m_currentEnemy and AISkirmishPlayer::m_frameToCheckEnemy are not restored after save/load #2420

@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

There is a bug in AISkirmishPlayer that affects save and load of skirmish AI state

The issue affects at least two fields used in enemy selection: m_currentEnemy and m_frameToCheckEnemy

Expected result

If skirmish AI has already selected its current main enemy and already set the next frame for enemy recheck then after loading a save this state should stay unchanged

If before saving AI had target player 5 and next enemy check was set to frame 2550 then after loading same AI should still have same m_currentEnemy and same m_frameToCheckEnemy

Actual result

After loading a save AISkirmishPlayer does not restore these values

m_currentEnemy stays null or default
m_frameToCheckEnemy becomes zero

Because of this first call to getAiEnemy() after loading treats enemy timer as expired and calls acquireEnemy()

This means skirmish AI does not continue saved enemy selection state and instead selects enemy again after load

Log evidence

Before saving affected skirmish AI had valid enemy selection state

Example from one run
player=3 frameToCheckEnemy=3450 currentEnemy=5

Other AI also had real non default enemies at same time
player=4 currentEnemy=5
player=5 currentEnemy=4
player=9 currentEnemy=2

This shows that at save time enemy selection was already set

After loading same save state is different

In snapshot read logs and in loadPostProcess() they show
currentEnemy=-1
frameToCheckEnemy=0

Right after that log shows
getAiEnemy recalc-trigger ... oldFrameToCheckEnemy=0 currentEnemy=-1
then
acquireEnemy begin ... currentEnemy=-1
then only after that new target is assigned through change-target

This proves previous enemy was not restored from save and was selected again after load


m_currentEnemy stores result of earlier AI decision about current enemy
m_frameToCheckEnemy stores frame for next recheck of that decision

When both values are lost current target state is lost and enemy selection timing is also lost

Because of this save and load itself changes AI logic

One more important detail is that m_currentEnemy is not used only inside AISkirmishPlayer

It is also exposed through Player::getCurrentEnemy()

This means losing this value after load can affect not only internal AI heuristics but also other game systems engine logic or scripts that query current enemy of skirmish AI player

So this issue affects more than one local method


AISkirmishPlayer::xfer() does not serialize m_currentEnemy
AISkirmishPlayer::xfer() does not serialize m_frameToCheckEnemy
AISkirmishPlayer::loadPostProcess() does not restore these values after load

Because of that after load AISkirmishPlayer object exists but part of its enemy selection state remains at constructor or default values instead of saved values


It is possible that this may break old saves. In this case, for the new version maybe to to save and load m_frameToCheckEnemy and the enemy index PlayerIndex instead of m_currentEnemy.

And for the old version set it to 0 or nullptr then when got a new build will be able to load old saves. This is onnly thing is that the old build will not be able to load new saves with the new version.


The essence of the bug is that after loading the save, the bot forgets who it has already chosen as its main enemy and forgets when it was supposed to check on that enemy next.

Because of this, after loading, the game behaves as if the bot has just started selecting a target again. Instead of continuing from the same place where the save was made, it looks at all opponents again and selects an enemy again.

This manifests itself as follows:
before saving, the bot is already targeting someone and holding that enemy,
after loading, this state disappears,
immediately after loading, the bot starts selecting an enemy again.

Sometimes it selects the same enemy, and then the bug is almost imperceptible. But this is still a new selection, not a restoration of the old state. And sometimes it selects a different enemy, and then its actions after loading change directly because of the very fact of loading the game.

Example from logs
before saving player=9 had currentEnemy=2
after loading same state was lost and AI selected player=8


Possible relation to issue

There is an indirect link to #2413

This issue clears saved enemy state after load and forces a new call to acquireEnemy()

Issue #2413 affects scoring inside acquireEnemy()

Because of that load can trigger a new enemy selection pass that is also affected by #2413

This is more visible in skirmish Free For All where AI has multiple enemy candidates


Reproduction Steps

  1. Build game with DEBUG enabled and add detailed logging to AISkirmishPlayer in xfer(), loadPostProcess(), getAiEnemy(), and acquireEnemy() so logs show player index, m_currentEnemy, and m_frameToCheckEnemy before save and after load

  2. Start a skirmish match with multiple AI players so each AI has a real choice between multiple enemies

  3. Use Free For All or another setup with multiple skirmish AI players so enemy selection works through heuristics and not through one possible target

  4. Let match run until skirmish AI has already selected enemies

  5. Verify this in log by looking for:

    • change-target
    • later getAiEnemy() calls
    • same AI with non default currentEnemy
    • non zero future value in m_frameToCheckEnemy
  6. Before saving check in log that one specific AI already has valid enemy selection state

  7. For same player confirm:

    • currentEnemy is non default
    • frameToCheckEnemy is non zero
    • target was not just cleared
    • target was not just selected again
  8. Save game at that point

  9. Load newly created save right away

  10. After loading inspect log for same AI player

  11. In correct implementation post load values of currentEnemy and frameToCheckEnemy should match pre save values

  12. In current implementation logs in AISkirmishPlayer::xfer() and AISkirmishPlayer::loadPostProcess() show:

  • currentEnemy becomes default or empty such as -1 or nullptr
  • frameToCheckEnemy becomes 0
  1. Inspect first getAiEnemy() calls after load

  2. In current implementation you will see:

  • immediate recalc-trigger
  • oldFrameToCheckEnemy=0
  • empty currentEnemy
  • then acquireEnemy()
  • then new enemy assignment through change-target
  1. For final confirmation compare same AI before save and after load:
  • before save it already had a specific enemy and valid next enemy check frame
  • after load both values are lost and AI selects enemy again from scratch
  1. Repeat same scenario more than once either by loading same save again or by saving and loading at different moments of same match

  2. Result should stay same after each load:

  • m_currentEnemy is not restored
  • m_frameToCheckEnemy is not restored
  • enemy selection starts again

Additional Context

Issue is confirmed by a full MiniLog captured from same skirmish match before save and right after load

Before save skirmish AI already has a valid selected enemy and a valid timer for next enemy check

Example from pre save logs:

  • player=3 frameToCheckEnemy=2550 currentEnemy=5
  • player=4 frameToCheckEnemy=2550 currentEnemy=5
  • player=5 frameToCheckEnemy=2550 currentEnemy=4
  • player=9 frameToCheckEnemy=2550 currentEnemy=8

This means that at save time AI already had enemy selection state set

After loading same save file same AI players lose that state

Snapshot read logs and loadPostProcess() show:

  • player=3 frameToCheckEnemy=0 currentEnemy=-1
  • player=4 frameToCheckEnemy=0 currentEnemy=-1
  • player=5 frameToCheckEnemy=0 currentEnemy=-1
  • player=9 frameToCheckEnemy=0 currentEnemy=-1

Right after that forced recalculation happens:

  • getAiEnemy ... recalc-trigger ... oldFrameToCheckEnemy=0 currentEnemy=-1
  • acquireEnemy ... begin ... currentEnemy=-1
  • and only after that change-target ...

This confirms that after load AI does not restore previous enemy selection state

It loses both current enemy and next enemy check frame and then runs a new enemy selection pass right away

Logs also show full state transition chain:

  1. before save enemy selection values are valid
  2. during save these values are still present in serialized object
  3. after load both values are gone
  4. AI right away recalculates enemy selection

The full log contains several save/load cycles.
A log file is attached here

MiniLog.txt

Metadata

Metadata

Assignees

No one assigned

    Labels

    AIIs AI relatedBugSomething is not working right, typically is user facingMinorSeverity: Minor < Major < Critical < BlockerSaveloadIs Saveload/Xfer related

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions