diff --git a/Core/GameEngine/Include/Common/MessageStream.h b/Core/GameEngine/Include/Common/MessageStream.h index e5dc1fc0896..2be7c68bff7 100644 --- a/Core/GameEngine/Include/Common/MessageStream.h +++ b/Core/GameEngine/Include/Common/MessageStream.h @@ -150,6 +150,8 @@ class GameMessage : public MemoryPoolObject MSG_CLEAR_GAME_DATA, ///< Clear all game data in memory MSG_NEW_GAME, ///< Start a new game + MSG_LOGIC_CRC_PLAYBACK, ///< TheSuperHackers @tweak CRC from the logic used internally for replay playback. + // "meta" messages should be thought of as "virtual keystrokes" -- they exist // solely to provide an abstraction layer useful for keyboard/mouse remapping. // they should NEVER be sent over the network. @@ -601,8 +603,8 @@ class GameMessage : public MemoryPoolObject MSG_SET_REPLAY_CAMERA, ///< Track camera pos for replays MSG_SELF_DESTRUCT, ///< Destroys a player's units (for copy protection or to quit to observer) MSG_CREATE_FORMATION, ///< Creates a formation. - MSG_LOGIC_CRC, ///< CRC from the logic passed around in a network game :) - MSG_SET_MINE_CLEARING_DETAIL, ///< CRC from the logic passed around in a network game :) + MSG_LOGIC_CRC, ///< CRC from the logic passed around in a network game + MSG_SET_MINE_CLEARING_DETAIL, ///< Order selected unit(s) to clear mines MSG_ENABLE_RETALIATION_MODE, ///< Turn retaliation mode on or off. MSG_BEGIN_DEBUG_NETWORK_MESSAGES = 1900, ///< network messages that exist only in debug/internal builds. all grouped separately. diff --git a/Core/GameEngine/Source/Common/MessageStream.cpp b/Core/GameEngine/Source/Common/MessageStream.cpp index 9980021c6ee..b8fde37e1c7 100644 --- a/Core/GameEngine/Source/Common/MessageStream.cpp +++ b/Core/GameEngine/Source/Common/MessageStream.cpp @@ -236,6 +236,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_MOUSE_RIGHT_DOUBLE_CLICK) CASE_LABEL(MSG_CLEAR_GAME_DATA) CASE_LABEL(MSG_NEW_GAME) + CASE_LABEL(MSG_LOGIC_CRC_PLAYBACK) CASE_LABEL(MSG_BEGIN_META_MESSAGES) CASE_LABEL(MSG_META_SAVE_VIEW1) CASE_LABEL(MSG_META_SAVE_VIEW2) diff --git a/Core/GameEngine/Source/GameClient/GameClientDispatch.cpp b/Core/GameEngine/Source/GameClient/GameClientDispatch.cpp index cde639ccc56..eca2592add4 100644 --- a/Core/GameEngine/Source/GameClient/GameClientDispatch.cpp +++ b/Core/GameEngine/Source/GameClient/GameClientDispatch.cpp @@ -43,14 +43,18 @@ GameMessageDisposition GameClientMessageDispatcher::translateGameMessage(const G { if (msg->getType() >= GameMessage::MSG_BEGIN_NETWORK_MESSAGES && msg->getType() <= GameMessage::MSG_END_NETWORK_MESSAGES) return KEEP_MESSAGE; - if (msg->getType() == GameMessage::MSG_NEW_GAME || msg->getType() == GameMessage::MSG_CLEAR_GAME_DATA) - return KEEP_MESSAGE; - if (msg->getType() == GameMessage::MSG_FRAME_TICK) + switch (msg->getType()) + { + case GameMessage::MSG_FRAME_TICK: + case GameMessage::MSG_CLEAR_GAME_DATA: + case GameMessage::MSG_NEW_GAME: + case GameMessage::MSG_LOGIC_CRC_PLAYBACK: return KEEP_MESSAGE; + } //DEBUG_LOG(("GameClientMessageDispatcher::translateGameMessage() - eating a %s on frame %d", - //((GameMessage *)msg)->getCommandAsString(), TheGameClient->getFrame())); + //msg->getCommandAsString(), TheGameClient->getFrame())); return DESTROY_MESSAGE; } diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index c2835253d30..d9e7f1383ed 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -265,8 +265,6 @@ bool changeLogicTimeScale(FpsValueChange change) } -static Bool isSystemMessage( const GameMessage *msg ); - enum{ DROPPED_MAX_PARTICLE_COUNT = 1000}; static Bool canSelectionSalvage( const Object *targetObj) @@ -2538,11 +2536,24 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { GameMessage::Type t = msg->getType(); GameMessageDisposition disp = KEEP_MESSAGE; + // We want to always be able to get to the options menu even during no input times and a clear game data message should always go through - if (t != GameMessage::MSG_META_OPTIONS && t != GameMessage::MSG_CLEAR_GAME_DATA && - !TheInGameUI->getInputEnabled() && !isSystemMessage(msg)) + switch (t) { - return DESTROY_MESSAGE; + case GameMessage::MSG_FRAME_TICK: + case GameMessage::MSG_CLEAR_GAME_DATA: + case GameMessage::MSG_LOGIC_CRC_PLAYBACK: + case GameMessage::MSG_META_OPTIONS: + case GameMessage::MSG_META_DEMO_INSTANT_QUIT: + case GameMessage::MSG_DESTROY_SELECTED_GROUP: + case GameMessage::MSG_SET_REPLAY_CAMERA: + case GameMessage::MSG_LOGIC_CRC: + break; + default: + if (!TheInGameUI->getInputEnabled()) + return DESTROY_MESSAGE; + + break; } #if defined(RTS_DEBUG) @@ -5599,22 +5610,3 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage return disp; } - -static Bool isSystemMessage( const GameMessage *msg ) -{ - if (!msg) { - return false; - } - GameMessage::Type msgType = msg->getType(); - - switch (msgType) - { - case GameMessage::MSG_DESTROY_SELECTED_GROUP: - case GameMessage::MSG_LOGIC_CRC: - case GameMessage::MSG_SET_REPLAY_CAMERA: - case GameMessage::MSG_FRAME_TICK: - case GameMessage::MSG_META_DEMO_INSTANT_QUIT: - return TRUE; - } - return FALSE; -} diff --git a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index fd9733f25ac..07dae5e074a 100644 --- a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -430,14 +430,19 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) GameMessage::Type msgType = msg->getType(); switch( msgType ) { + case GameMessage::MSG_CLEAR_GAME_DATA: + { + onClearGameData(msg, currentlySelectedGroup); + break; + } case GameMessage::MSG_NEW_GAME: { onNewGame(msg); break; } - case GameMessage::MSG_CLEAR_GAME_DATA: + case GameMessage::MSG_LOGIC_CRC_PLAYBACK: { - onClearGameData(msg, currentlySelectedGroup); + onLogicCrcForPlayback(msg); break; } case GameMessage::MSG_META_BEGIN_PATH_BUILD: @@ -917,6 +922,17 @@ bool GameLogic::onClearGameData(MAYBE_UNUSED GameMessage *msg, AIGroupPtr &curre return true; } +bool GameLogic::onLogicCrcForPlayback(MAYBE_UNUSED GameMessage *msg) +{ + const UnsignedInt newCRC = msg->getArgument(0)->integer; + //DEBUG_LOG(("Saw CRC of %X from playback. Our CRC is %X. Arg count is %d", + //newCRC, getCRC(), msg->getArgumentCount())); + + TheRecorder->handleCRCMessage(newCRC, -1, true); + + return true; +} + bool GameLogic::onBeginPathBuild(MAYBE_UNUSED GameMessage *msg) { DEBUG_LOG(("META: begin path build")); @@ -2418,7 +2434,7 @@ bool GameLogic::onLogicCrc(MAYBE_UNUSED GameMessage *msg) //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d", //newCRC, msgPlayer->getPlayerIndex(), getCRC(), msg->getArgumentCount())); - TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), (msg->getArgument(1)->boolean)); + TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), false); } return true; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h index 8192d3fbe68..f2424cc5d15 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -297,6 +297,7 @@ class GameLogic : public SubsystemInterface, public Snapshot bool onNewGame(GameMessage *msg); bool onClearGameData(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onLogicCrcForPlayback(GameMessage *msg); bool onBeginPathBuild(GameMessage *msg); bool onEndPathBuild(GameMessage *msg); bool onSetRallyPoint(GameMessage *msg); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 68b31530c64..63d2b7d0598 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1587,9 +1587,10 @@ RecorderClass::CullBadCommandsResult RecorderClass::cullBadCommands() { while (msg != nullptr) { next = msg->next(); - if ((msg->getType() > GameMessage::MSG_BEGIN_NETWORK_MESSAGES) && - (msg->getType() < GameMessage::MSG_END_NETWORK_MESSAGES) && - (msg->getType() != GameMessage::MSG_LOGIC_CRC)) { + if (msg->getType() > GameMessage::MSG_BEGIN_NETWORK_MESSAGES && msg->getType() < GameMessage::MSG_END_NETWORK_MESSAGES) + { + DEBUG_ASSERTCRASH(msg->getType() != GameMessage::MSG_LOGIC_CRC, + ("Use MSG_LOGIC_CRC_PLAYBACK instead of MSG_LOGIC_CRC during replay playback")); deleteInstance(msg); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index dec2e14cc76..7f5ab8337f2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -3768,11 +3768,13 @@ void GameLogic::update() if (generateForSolo || generateForMP) { m_CRC = getCRC( CRC_RECALC ); - bool isPlayback = (TheRecorder && TheRecorder->isPlaybackMode()); + const bool isPlayback = (TheRecorder && TheRecorder->isPlaybackMode()); - GameMessage *msg = newInstance(GameMessage)(GameMessage::MSG_LOGIC_CRC); + // TheSuperHackers @tweak Caball009 30/09/2026 Playback CRC is now separate from regular CRC, + // removing the need for the second parameter used to distinguish them. Older clients can still + // parse newer replays because GameMessage::getArgument returns false for the missing playback argument. + GameMessage *msg = newInstance(GameMessage)(isPlayback ? GameMessage::MSG_LOGIC_CRC_PLAYBACK : GameMessage::MSG_LOGIC_CRC); msg->appendIntegerArgument(m_CRC); - msg->appendBooleanArgument(isPlayback); // TheSuperHackers @info helmutbuhler 13/04/2025 // During replay simulation, we bypass TheMessageStream and instead put the CRC message