bugfix(ai): Fix AICommandParmsStorage::doXfer to transfer the command source - #3156
bugfix(ai): Fix AICommandParmsStorage::doXfer to transfer the command source#3156bas-slats wants to merge 1 commit into
Conversation
PR Summary by QodoFix AICommandParmsStorage save/load to transfer command source
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1. Invalid cmdSource from old saves
|
| // TheSuperHackers @bugfix Transfer the command source instead of the command twice. | ||
| xfer->xferUser(&m_cmdSource, sizeof(m_cmdSource)); |
There was a problem hiding this comment.
1. Invalid cmdsource from old saves 🐞 Bug ☼ Reliability
When loading a save created before this fix, the slot now read into m_cmdSource contains the old duplicated m_cmd bytes, producing out-of-range CommandSourceType values. That invalid value is later used unchecked (e.g., (1 << cmdSource) in weapon selection), which can cause undefined behavior/crashes and incorrect command-source filtering.
Agent Prompt
### Issue description
After this PR, `AICommandParmsStorage::doXfer` correctly transfers `m_cmdSource`, but **old saves written by the previous code** stored a duplicate of `m_cmd` in the second field. Loading those saves will therefore set `m_cmdSource` to an invalid `CommandSourceType` value.
This becomes dangerous because other code paths assume `CommandSourceType` is in-range and use it in bit operations (e.g., `1 << cmdSource`), which can become undefined behavior if `cmdSource` is large.
### Issue Context
- `CommandSourceType` has only a few valid values (`CMD_FROM_PLAYER`..`CMD_DEFAULT_SWITCH_WEAPON`, then `COMMAND_SOURCE_TYPE_COUNT`).
- `AICommandType` has many values and can exceed the bit-width safe range for shifting.
- `AICommandParmsStorage::doXfer` currently does not validate `m_cmdSource` after loading.
### Fix Focus Areas
- Add a post-load validation step in `AICommandParmsStorage::doXfer` to clamp/normalize `m_cmdSource` when `xfer->getXferMode() == XFER_LOAD`.
- Example approach: after `xferUser(&m_cmdSource, ...)`, check `static_cast<Int>(m_cmdSource)` is within `[0, COMMAND_SOURCE_TYPE_COUNT)`; if not, set to a safe default (likely `CMD_FROM_AI`).
- Apply the same fix in both game variants.
Recommended code shape (illustrative):
```cpp
xfer->xferUser(&m_cmdSource, sizeof(m_cmdSource));
if (xfer->getXferMode() == XFER_LOAD) {
const Int cs = static_cast<Int>(m_cmdSource);
if (cs < 0 || cs >= COMMAND_SOURCE_TYPE_COUNT) {
m_cmdSource = CMD_FROM_AI;
}
}
```
### Fix Focus Areas (exact locations)
- Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp[126-133]
- GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp[129-136]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
ace16e8 to
250f374
Compare
|
Good catch - verified: |
|
Is this retail compatible? |
|
I'd expect the fix to look something like this: // ------------------------------------------------------------------------------------------------
/** Xfer method
* Version Info:
* 1: TheSuperHackers @fix Add version control and fix xfer of m_cmdSource
*/
// ------------------------------------------------------------------------------------------------
void AICommandParmsStorage::doXfer(Xfer *xfer)
{
// version
#if RETAIL_COMPATIBLE_XFER_SAVE
const XferVersion version = 0;
#else
const XferVersion currentVersion = 1;
XferVersion version = currentVersion;
xfer->xferVersion(&version, currentVersion);
#endif
xfer->xferUser(&m_cmd, sizeof(m_cmd));
if (version >= 1)
{
xfer->xferUser(&m_cmdSource, sizeof(m_cmdSource));
}
else
{
xfer->xferUser(&m_cmd, sizeof(m_cmdSource));
}
...
} |
AICommandParmsStorage::doXferdoesn't save command source #766AICommandParmsStorage::doXfertransfersm_cmdtwice —xfer->xferUser(&m_cmd, sizeof(m_cmdSource))on the second line — and never transfersm_cmdSource.AICommandParmsStoragehas no constructor, so when a saved game with a pending AI command is loaded, the command executes with an uninitialized command source. This change transfersm_cmdSourceon the second line, for both Zero Hour and Generals.The save record layout is unchanged:
AICommandTypeandCommandSourceTypeare both enum-sized and the second field is already written withsizeof(m_cmdSource). Loading a save created before this change reads the duplicated command bytes intom_cmdSource, which can be out of range forCommandSourceType— and downstream code shifts by it unchecked (okSrcs & (1 << cmdSource)inWeaponSet.cpp). Loads therefore validate the value and fall back toCMD_FROM_AIwhen it is out of range. This also covers the pre-fix hazard, wherem_cmdSourcewas left as uninitialized memory after loading.Testing
Save/load compatibility is tested manually in game (Zero Hour,
win32preset build, retail Steam 1.04 data):m_cmdSource.There is no unit-test harness in the repository, so the semantic fix itself (the correct value arriving in
m_cmdSource) is verified by inspection: the write and read sides are the same line, anddoXferruns only during save/load, so replays and normal simulation are unaffected.Compiles for both games with the
win32preset (VS2019 16.11, Ninja Multi-Config, Release):generalszh.exeandgeneralsv.exelink.This change was developed with AI assistance (Claude), human-directed: the duplicated transfer, the missing constructor initialization, and the record-size equivalence were verified by hand against the class definition in
AI.h.