Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions Generals/Code/GameEngine/Source/Common/Recorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1149,16 +1149,21 @@ Bool RecorderClass::playbackFile(AsciiString filename)
}
#endif

Bool isMultiplayer = m_gameInfo.getSlot(header.localPlayerIndex)->getIP() != 0;
m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer);
REPLAY_CRC_INTERVAL = m_gameInfo.getCRCInterval();
DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL));

Int difficulty = 0;
m_file->read(&difficulty, sizeof(difficulty));

m_file->read(&m_originalGameMode, sizeof(m_originalGameMode));

// TheSuperHackers @bugfix bobtista 30/08/2026 A replay header is allowed to carry a local player
// index of -1 and getSlot returns NULL for it, so reading the slot to tell a network game from a
// local one dereferenced NULL. The recorded game mode answers the same question directly, so the
// crc queue is now primed from the mode and the local slot is no longer read here.
const Bool isMultiplayer = m_originalGameMode == GAME_LAN || m_originalGameMode == GAME_INTERNET;
m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer);
DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL));

Int rankPoints = 0;
m_file->read(&rankPoints, sizeof(rankPoints));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,11 @@ void GameLogic::tryStartNewGame( Bool loadingSaveGame )
d.setInt(TheKey_multiplayerStartIndex, slot->getStartPos());
// d.setBool(TheKey_multiplayerIsLocal, slot->isLocalPlayer());
// d.setBool(TheKey_multiplayerIsLocal, slot->getIP() == game->getLocalIP());
d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && (slot->getName().compare(TheGameInfo->getSlot(TheGameInfo->getLocalSlotNum())->getName().str()) == 0));
// TheSuperHackers @bugfix bobtista 30/08/2026 A replay recorded without a local player has no
// local slot number and getSlot returns NULL for it, so no slot can be the local one.
const Int localSlotNum = TheGameInfo->getLocalSlotNum();
const GameSlot *localGameSlot = localSlotNum >= 0 ? TheGameInfo->getSlot(localSlotNum) : nullptr;
d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && localGameSlot != nullptr && (slot->getName().compare(localGameSlot->getName().str()) == 0));
Comment on lines +1320 to +1322

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. No-player replays inherit old viewpoints 🐞 Bug ≡ Correctness

tryStartNewGame trusts getLocalSlotNum() even though replay loading does not clear or replace
m_localIP when the recorded index is -1. After an earlier replay leaves a matching IP behind,
the affected replay marks that human as local and uses their starting position and player state
instead of taking the deterministic no-local fallback.
Agent Prompt
## Issue description
Replay-local player lookup remains IP-based when the replay header explicitly records no local player. Because replay resets preserve the prior local IP, a subsequent replay can resolve an unrelated slot as local.

## Fix Focus Areas
- Generals/Code/GameEngine/Include/Common/Recorder.h[36-47]
- Generals/Code/GameEngine/Source/Common/Recorder.cpp[916-931]
- Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp[1320-1347]
- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp[918-933]
- GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp[1481-1508]

## Recommended Fix
Store the replay header's local slot index explicitly in `ReplayGameInfo` and override its local-slot lookup to return that index, including `-1`, rather than deriving replay locality from retained IP state. Apply the same implementation to both game variants and use the explicit result for local-player flags and the later `localSlot` assignment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


/*
if (slot->getIP() == game->getLocalIP())
Expand Down
11 changes: 8 additions & 3 deletions GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1152,16 +1152,21 @@ Bool RecorderClass::playbackFile(AsciiString filename)
}
#endif

Bool isMultiplayer = m_gameInfo.getSlot(header.localPlayerIndex)->getIP() != 0;
m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer);
REPLAY_CRC_INTERVAL = m_gameInfo.getCRCInterval();
DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL));

Int difficulty = 0;
m_file->read(&difficulty, sizeof(difficulty));

m_file->read(&m_originalGameMode, sizeof(m_originalGameMode));

// TheSuperHackers @bugfix bobtista 30/08/2026 A replay header is allowed to carry a local player
// index of -1 and getSlot returns NULL for it, so reading the slot to tell a network game from a
// local one dereferenced NULL. The recorded game mode answers the same question directly, so the
// crc queue is now primed from the mode and the local slot is no longer read here.
const Bool isMultiplayer = m_originalGameMode == GAME_LAN || m_originalGameMode == GAME_INTERNET;
m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer);
DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL));

Int rankPoints = 0;
m_file->read(&rankPoints, sizeof(rankPoints));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1476,7 +1476,11 @@ void GameLogic::tryStartNewGame( Bool loadingSaveGame )
d.setInt(TheKey_multiplayerStartIndex, slot->getStartPos());
// d.setBool(TheKey_multiplayerIsLocal, slot->isLocalPlayer());
// d.setBool(TheKey_multiplayerIsLocal, slot->getIP() == game->getLocalIP());
d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && (slot->getName().compare(TheGameInfo->getSlot(TheGameInfo->getLocalSlotNum())->getName().str()) == 0));
// TheSuperHackers @bugfix bobtista 30/08/2026 A replay recorded without a local player has no
// local slot number and getSlot returns NULL for it, so no slot can be the local one.
const Int localSlotNum = TheGameInfo->getLocalSlotNum();
const GameSlot *localGameSlot = localSlotNum >= 0 ? TheGameInfo->getSlot(localSlotNum) : nullptr;
d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && localGameSlot != nullptr && (slot->getName().compare(localGameSlot->getName().str()) == 0));

/*
if (slot->getIP() == game->getLocalIP())
Expand Down
Loading