-
Notifications
You must be signed in to change notification settings - Fork 167
Description
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
-
Build game with
DEBUGenabled and add detailed logging toAISkirmishPlayerinxfer(),loadPostProcess(),getAiEnemy(), andacquireEnemy()so logs show player index,m_currentEnemy, andm_frameToCheckEnemybefore save and after load -
Start a skirmish match with multiple AI players so each AI has a real choice between multiple enemies
-
Use Free For All or another setup with multiple skirmish AI players so enemy selection works through heuristics and not through one possible target
-
Let match run until skirmish AI has already selected enemies
-
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
-
Before saving check in log that one specific AI already has valid enemy selection state
-
For same player confirm:
currentEnemyis non defaultframeToCheckEnemyis non zero- target was not just cleared
- target was not just selected again
-
Save game at that point
-
Load newly created save right away
-
After loading inspect log for same AI player
-
In correct implementation post load values of
currentEnemyandframeToCheckEnemyshould match pre save values -
In current implementation logs in
AISkirmishPlayer::xfer()andAISkirmishPlayer::loadPostProcess()show:
currentEnemybecomes default or empty such as-1ornullptrframeToCheckEnemybecomes0
-
Inspect first
getAiEnemy()calls after load -
In current implementation you will see:
- immediate
recalc-trigger oldFrameToCheckEnemy=0- empty
currentEnemy - then
acquireEnemy() - then new enemy assignment through
change-target
- 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
-
Repeat same scenario more than once either by loading same save again or by saving and loading at different moments of same match
-
Result should stay same after each load:
m_currentEnemyis not restoredm_frameToCheckEnemyis 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=5player=4 frameToCheckEnemy=2550 currentEnemy=5player=5 frameToCheckEnemy=2550 currentEnemy=4player=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=-1player=4 frameToCheckEnemy=0 currentEnemy=-1player=5 frameToCheckEnemy=0 currentEnemy=-1player=9 frameToCheckEnemy=0 currentEnemy=-1
Right after that forced recalculation happens:
getAiEnemy ... recalc-trigger ... oldFrameToCheckEnemy=0 currentEnemy=-1acquireEnemy ... 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:
- before save enemy selection values are valid
- during save these values are still present in serialized object
- after load both values are gone
- AI right away recalculates enemy selection
The full log contains several save/load cycles.
A log file is attached here