From 8005a7bae4e5addea9d226648588cea986c0f202 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 13 Apr 2025 11:16:09 +0200 Subject: [PATCH 001/112] Fix messy code and potential null pointer dereference in RecorderClass::appendNextCommand() --- .../Code/GameEngine/Source/Common/Recorder.cpp | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 46ebcaa78e9..6b891d549bc 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1276,16 +1276,6 @@ void RecorderClass::appendNextCommand() { } GameMessage *msg = newInstance(GameMessage)(type); - if (type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES || type == GameMessage::MSG_CLEAR_GAME_DATA) - { - } - else - { - if (!m_doingAnalysis) - { - TheCommandList->appendMessage(msg); - } - } #ifdef DEBUG_LOGGING AsciiString commandName = msg->getCommandAsAsciiString(); @@ -1357,13 +1347,11 @@ void RecorderClass::appendNextCommand() { } } - if (type == GameMessage::MSG_CLEAR_GAME_DATA || type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES) + if (type != GameMessage::MSG_BEGIN_NETWORK_MESSAGES && type != GameMessage::MSG_CLEAR_GAME_DATA && !m_doingAnalysis) { - msg->deleteInstance(); - msg = NULL; + TheCommandList->appendMessage(msg); } - - if (m_doingAnalysis) + else { msg->deleteInstance(); msg = NULL; From c2f8d14bac17cc5811b06228cb492a0f465dda21 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 13 Apr 2025 12:15:22 +0200 Subject: [PATCH 002/112] Fix Replay playback not working after running analysis on a replay. --- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 1 + GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 5 +++++ .../Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 1 + 3 files changed, 7 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 4ae303eeda6..6216a1b333e 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -74,6 +74,7 @@ class RecorderClass : public SubsystemInterface { #if defined _DEBUG || defined _INTERNAL Bool analyzeReplay( AsciiString filename ); Bool isAnalysisInProgress( void ); + void stopAnalysis(); #endif public: diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 6b891d549bc..efd56faf4ad 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -913,6 +913,11 @@ Bool RecorderClass::isAnalysisInProgress( void ) { return m_mode == RECORDERMODETYPE_PLAYBACK && m_nextFrame != -1; } + +void RecorderClass::stopAnalysis() +{ + m_doingAnalysis = FALSE; +} #endif AsciiString RecorderClass::getCurrentReplayFilename( void ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index f0166d24413..f5c00b59825 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -535,6 +535,7 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, { TheRecorder->update(); } while (TheRecorder->isAnalysisInProgress()); + TheRecorder->stopAnalysis(); } } } From 942e5e1eab56676b6778ed21fcda816f6e1b7b32 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 13 Apr 2025 12:36:47 +0200 Subject: [PATCH 003/112] Add replay simulation functionality # Conflicts: # GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp # GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp --- .../Code/GameEngine/Include/Common/Recorder.h | 3 ++ .../GameEngine/Source/Common/Recorder.cpp | 23 +++++++--- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 45 ++++++++++++++++++- .../Source/GameLogic/System/GameLogic.cpp | 38 ++++++++-------- .../GameLogic/System/GameLogicDispatch.cpp | 18 +++++--- 5 files changed, 98 insertions(+), 29 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 6216a1b333e..11cf8ab6aa5 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -48,6 +48,7 @@ class ReplayGameInfo : public GameInfo enum RecorderModeType CPP_11(: Int) { RECORDERMODETYPE_RECORD, RECORDERMODETYPE_PLAYBACK, + RECORDERMODETYPE_SIMULATION_PLAYBACK, // Play back replay without any graphics (TheSuperHackers @feature helmutbuhler 04/13/2025) RECORDERMODETYPE_NONE // this is a valid state to be in on the shell map, or in saved games }; @@ -71,6 +72,7 @@ class RecorderClass : public SubsystemInterface { Bool testVersionPlayback(AsciiString filename); ///< Returns if the playback is a valid playback file for this version or not. AsciiString getCurrentReplayFilename( void ); ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. + Bool simulateReplay(AsciiString filename); #if defined _DEBUG || defined _INTERNAL Bool analyzeReplay( AsciiString filename ); Bool isAnalysisInProgress( void ); @@ -107,6 +109,7 @@ class RecorderClass : public SubsystemInterface { Bool readReplayHeader( ReplayHeader& header ); RecorderModeType getMode(); ///< Returns the current operating mode. + Bool isPlaybackMode() { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; } void initControls(); ///< Show or Hide the Replay controls AsciiString getReplayDir(); ///< Returns the directory that holds the replay files. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index efd56faf4ad..1f792c10295 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -428,7 +428,7 @@ void RecorderClass::reset() { void RecorderClass::update() { if (m_mode == RECORDERMODETYPE_RECORD || m_mode == RECORDERMODETYPE_NONE) { updateRecord(); - } else if (m_mode == RECORDERMODETYPE_PLAYBACK) { + } else if (isPlaybackMode()) { updatePlayback(); } } @@ -902,6 +902,14 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header) return TRUE; } +Bool RecorderClass::simulateReplay(AsciiString filename) +{ + Bool success = playbackFile(filename); + if (success) + m_mode = RECORDERMODETYPE_SIMULATION_PLAYBACK; + return success; +} + #if defined _DEBUG || defined _INTERNAL Bool RecorderClass::analyzeReplay( AsciiString filename ) { @@ -911,7 +919,7 @@ Bool RecorderClass::analyzeReplay( AsciiString filename ) Bool RecorderClass::isAnalysisInProgress( void ) { - return m_mode == RECORDERMODETYPE_PLAYBACK && m_nextFrame != -1; + return isPlaybackMode() && m_nextFrame != -1; } void RecorderClass::stopAnalysis() @@ -922,7 +930,7 @@ void RecorderClass::stopAnalysis() AsciiString RecorderClass::getCurrentReplayFilename( void ) { - if (m_mode == RECORDERMODETYPE_PLAYBACK) + if (isPlaybackMode()) { return m_currentReplayFilename; } @@ -1186,12 +1194,17 @@ Bool RecorderClass::playbackFile(AsciiString filename) // send a message to the logic for a new game if (!m_doingAnalysis) { - GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); + // TheSuperHackers @info helmutbuhler 04/13/2025 + // We send the New Game message here directly to the command list and bypass the TheMessageStream. + // That's ok because Multiplayer is disabled during replay playback and is actually required + // during replay simulation because we don't update TheMessageStream during simulation. + GameMessage *msg = newInstance(GameMessage)(GameMessage::MSG_NEW_GAME); msg->appendIntegerArgument(GAME_REPLAY); msg->appendIntegerArgument(difficulty); msg->appendIntegerArgument(rankPoints); if( maxFPS != 0 ) msg->appendIntegerArgument(maxFPS); + TheCommandList->appendMessage( msg ); //InitGameLogicRandom( m_gameInfo.getSeed()); InitRandom( m_gameInfo.getSeed() ); } @@ -1616,7 +1629,7 @@ void RecorderClass::initControls() Bool RecorderClass::isMultiplayer( void ) { - if (m_mode == RECORDERMODETYPE_PLAYBACK) + if (isPlaybackMode()) { GameSlot *slot; for (int i=0; igogoGadgetPushButton( parentReplayMenu, WIN_STATUS_ENABLED | WIN_STATUS_IMAGE, 4, 4, 180, 26, &instData, NULL, TRUE ); + + instData.m_id = 1; + instData.m_textLabelString = "Debug: Simulate Replay"; + instData.setTooltipText(UnicodeString(L"Playback selected replay without graphics. Will block game until replay simulation is done!")); + buttonSimulateReplay = TheWindowManager->gogoGadgetPushButton( parentReplayMenu, + WIN_STATUS_ENABLED | WIN_STATUS_IMAGE, + 4, 40, + 180, 26, + &instData, NULL, TRUE ); #endif // show menu @@ -539,6 +554,34 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, } } } + else if( controlID == buttonSimulateReplay->winGetWindowId() ) + { + if(listboxReplayFiles) + { + Int selected; + GadgetListBoxGetSelected( listboxReplayFiles, &selected ); + if(selected < 0) + { + MessageBoxOk(UnicodeString(L"Blah Blah"),UnicodeString(L"Please select something munkee girl"), NULL); + break; + } + + filename = GetReplayFilenameFromListbox(listboxReplayFiles, selected); + + AsciiString asciiFilename; + asciiFilename.translate(filename); + if (TheRecorder->simulateReplay(asciiFilename)) + { + do + { + { + VERIFY_CRC + } + TheGameLogic->UPDATE(); + } while (TheRecorder->isAnalysisInProgress()); + } + } + } else #endif if( controlID == buttonLoadID ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 8234d5fdd51..af4360d3717 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -299,7 +299,7 @@ void GameLogic::setDefaults( Bool loadingSaveGame ) Bool GameLogic::isInSinglePlayerGame( void ) { return (m_gameMode == GAME_SINGLE_PLAYER || - (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK && TheRecorder->getGameMode() == GAME_SINGLE_PLAYER)); + (TheRecorder && TheRecorder->isPlaybackMode() && TheRecorder->getGameMode() == GAME_SINGLE_PLAYER)); } @@ -1204,7 +1204,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) } else { - if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) + if (TheRecorder && TheRecorder->isPlaybackMode()) { TheGameInfo = game = TheRecorder->getGameInfo(); } @@ -1268,7 +1268,8 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) //****************************// // Get the m_loadScreen for this kind of game - if(!m_loadScreen) + if(!m_loadScreen && !TheGlobalData->m_headless && + !(TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK)) { m_loadScreen = getLoadScreen( loadingSaveGame ); if(m_loadScreen) @@ -2188,7 +2189,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) } // if we're in a load game, don't fade yet - if( loadingSaveGame == FALSE ) + if(loadingSaveGame == FALSE && TheTransitionHandler != NULL && m_loadScreen) { TheTransitionHandler->setGroup("FadeWholeScreen"); while(!TheTransitionHandler->isFinished()) @@ -3673,20 +3674,21 @@ void GameLogic::update( void ) if (generateForSolo || generateForMP) { m_CRC = getCRC( CRC_RECALC ); - if (isMPGameOrReplay) - { - GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_LOGIC_CRC ); - msg->appendIntegerArgument( m_CRC ); - msg->appendBooleanArgument( (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) ); // playback CRC - DEBUG_LOG(("Appended CRC on frame %d: %8.8X\n", m_frame, m_CRC)); - } - else - { - GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_LOGIC_CRC ); - msg->appendIntegerArgument( m_CRC ); - msg->appendBooleanArgument( (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) ); // playback CRC - DEBUG_LOG(("Appended Playback CRC on frame %d: %8.8X\n", m_frame, m_CRC)); - } + bool isPlayback = (TheRecorder && TheRecorder->isPlaybackMode()); + + GameMessage *msg = newInstance(GameMessage)(GameMessage::MSG_LOGIC_CRC); + msg->appendIntegerArgument(m_CRC); + msg->appendBooleanArgument(isPlayback); + + // TheSuperHackers @info helmutbuhler 04/13/2025 + // During replay simulation, we bypass TheMessageStream and instead put the CRC message + // directly into TheCommandList because we don't update TheMessageStream during simulation. + GameMessageList *messageList = TheMessageStream; + if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK) + messageList = TheCommandList; + messageList->appendMessage(msg); + + DEBUG_LOG(("Appended %sCRC on frame %d: %8.8X\n", isPlayback ? "Playback " : "", m_frame, m_CRC)); } // collect stats diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index 2434afa6e6a..c92e792c797 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -269,8 +269,16 @@ void GameLogic::clearGameData( Bool showScoreScreen ) { shellGame = TRUE; TheTransitionHandler->setGroup("FadeWholeScreen"); - TheShell->push("Menus/ScoreScreen.wnd"); - TheShell->showShell(FALSE); // by passing in false, we don't want to run the Init on the shell screen we just pushed on + if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK) + { + // Get back directly to replay menu after simulation is complete + TheShell->showShell(TRUE); + } + else + { + TheShell->push("Menus/ScoreScreen.wnd"); + TheShell->showShell(FALSE); // by passing in false, we don't want to run the Init on the shell screen we just pushed on + } TheTransitionHandler->reverse("FadeWholeScreen"); void FixupScoreScreenMovieWindow( void ); @@ -1825,7 +1833,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) // -------------------------------------------------------------------------------------------- case GameMessage::MSG_SET_REPLAY_CAMERA: { - if (TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer) + if (TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer) { if (TheTacticalView->isCameraMovementFinished()) { @@ -1962,7 +1970,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) //thisPlayer->getPlayerDisplayName().str(), m_frame)); m_cachedCRCs[msg->getPlayerIndex()] = newCRC; // to mask problem: = (oldCRC < newCRC)?newCRC:oldCRC; } - else if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) + else if (TheRecorder && TheRecorder->isPlaybackMode()) { UnsignedInt newCRC = msg->getArgument(0)->integer; //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d\n", @@ -1992,7 +2000,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) } // end switch /**/ /// @todo: multiplayer semantics - if (currentlySelectedGroup && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer /*&& !TheRecorder->isMultiplayer()*/) + if (currentlySelectedGroup && TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar != NULL && TheControlBar->getObserverLookAtPlayer() == thisPlayer /*&& !TheRecorder->isMultiplayer()*/) { const VecObjectID& selectedObjects = currentlySelectedGroup->getAllIDs(); TheInGameUI->deselectAllDrawables(); From 120fa4b1c66f4eb0fb911cf30c4a42ed819dac78 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 13 Apr 2025 17:06:12 +0200 Subject: [PATCH 004/112] Add -simReplay command line option to run replays without graphics # Conflicts: # GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp --- .../GameEngine/Include/Common/GlobalData.h | 2 + .../Code/GameEngine/Include/Common/Recorder.h | 2 +- .../GameEngine/Source/Common/CommandLine.cpp | 38 +++++++++++++++++ .../GameEngine/Source/Common/GameMain.cpp | 41 ++++++++++++++++++- .../GameEngine/Source/Common/GlobalData.cpp | 2 + 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index e1c0c1547a6..35c9f4186a2 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -326,6 +326,8 @@ class GlobalData : public SubsystemInterface Bool m_buildMapCache; AsciiString m_initialFile; ///< If this is specified, load a specific map/replay from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start + + std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 04/13/2025) Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 11cf8ab6aa5..0bbdc5feb2d 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -113,7 +113,7 @@ class RecorderClass : public SubsystemInterface { void initControls(); ///< Show or Hide the Replay controls AsciiString getReplayDir(); ///< Returns the directory that holds the replay files. - AsciiString getReplayExtention(); ///< Returns the file extention for replay files. + static AsciiString getReplayExtention(); ///< Returns the file extention for replay files. AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay. GameInfo *getGameInfo( void ) { return &m_gameInfo; } ///< Returns the slot list for playback game start diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 87a5736682f..f27812e5c44 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -30,6 +30,7 @@ #include "Common/CRCDebug.h" #include "Common/LocalFileSystem.h" #include "Common/version.h" +#include "Common/Recorder.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -110,6 +111,22 @@ static void ConvertShortMapPathToLongMapPath(AsciiString &mapName) mapName = actualpath; } +static void ConvertShortReplayPathToLongReplayPath(AsciiString &filename) +{ + if (filename.find('\\') != NULL || filename.find('/') != NULL) + { + // Absolute paths not yet supported + DEBUG_CRASH(("Invalid replay name %s", filename.str())); + exit(1); + } + + if (!filename.endsWithNoCase(RecorderClass::getReplayExtention())) + { + DEBUG_CRASH(("Invalid replay name %s", filename.str())); + exit(1); + } +} + //============================================================================= //============================================================================= Int parseNoLogOrCrash(char *args[], int) @@ -416,6 +433,18 @@ Int parseMapName(char *args[], int num) return 1; } +Int parseSimReplay(char *args[], int num) +{ + if (TheWritableGlobalData && num > 1) + { + AsciiString filename = args[1]; + ConvertShortReplayPathToLongReplayPath(filename); + TheWritableGlobalData->m_simulateReplayList.push_back(filename); + return 2; + } + return 1; +} + Int parseXRes(char *args[], int num) { if (TheWritableGlobalData && num > 1) @@ -1205,6 +1234,15 @@ static CommandLineParam params[] = { "-noshaders", parseNoShaders }, { "-quickstart", parseQuickStart }, + // TheSuperHackers @feature helmutbuhler 04/11/2025 + // This runs the game without a window, graphics, input and audio. Used for testing. + { "-headless", parseHeadless }, + + // TheSuperHackers @feature helmutbuhler 04/13/2025 + // Simulate replay without graphics. Pass the filename including .rep afterwards. + // You can pass this multiple times to check multiple replays. + { "-simReplay", parseSimReplay }, + #if (defined(_DEBUG) || defined(_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index aef3d163e30..c894544fae7 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -29,7 +29,37 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "Common/GameEngine.h" +#include "Common/Recorder.h" +#include "Common/CRCDebug.h" +#include "GameLogic/GameLogic.h" +// TheSuperHackers @feature helmutbuhler 04/13/2025 +// Simulate a list of replays without graphics +void SimulateReplayList(const std::vector &filenames) +{ + for (size_t i = 0; i < TheGlobalData->m_simulateReplayList.size(); i++) + { + AsciiString filename = TheGlobalData->m_simulateReplayList[i]; + if (TheRecorder->simulateReplay(filename)) + { + do + { + { + VERIFY_CRC + } + TheGameLogic->UPDATE(); + } while (TheRecorder->isAnalysisInProgress()); + } + } + + // TheSuperHackers @todo helmutbuhler 04/13/2025 + // There is a bug somewhere in the destructor of TheGameEngine which doesn't properly + // clean up the players and causes a crash in debug unless this is called. + if (TheGameLogic->isInGame()) + { + TheGameLogic->clearGameData(); + } +} /** * This is the entry point for the game system. @@ -40,8 +70,15 @@ void GameMain( int argc, char *argv[] ) TheGameEngine = CreateGameEngine(); TheGameEngine->init(argc, argv); - // run it - TheGameEngine->execute(); + if (!TheGlobalData->m_simulateReplayList.empty()) + { + SimulateReplayList(TheGlobalData->m_simulateReplayList); + } + else + { + // run it + TheGameEngine->execute(); + } // since execute() returned, we are exiting the game delete TheGameEngine; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index fa65dc5407c..9dcb2363dec 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -955,6 +955,8 @@ GlobalData::GlobalData() m_buildMapCache = FALSE; m_initialFile.clear(); m_pendingFile.clear(); + + m_simulateReplayList.clear(); for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; From a77e2b3fe345b27f00b2edb72cd37b157e3ed436 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 13 Apr 2025 18:21:43 +0200 Subject: [PATCH 005/112] Return Exitcode 1 if mismatch is detected in a replay while simulating. # Conflicts: # GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp # GeneralsMD/Code/Main/WinMain.cpp --- .../GameEngine/Include/Common/GameEngine.h | 2 +- .../Code/GameEngine/Include/Common/Recorder.h | 1 + .../GameEngine/Source/Common/GameMain.cpp | 22 +++++++++--- .../GameEngine/Source/Common/Recorder.cpp | 34 +++++++++++++++++-- GeneralsMD/Code/Main/WinMain.cpp | 15 ++++---- 5 files changed, 59 insertions(+), 15 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h index 087e0e8aba7..cbd684023a6 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h @@ -114,6 +114,6 @@ extern GameEngine *TheGameEngine; extern GameEngine *CreateGameEngine( void ); /// The entry point for the game system -extern void GameMain( int argc, char *argv[] ); +extern Int GameMain( int argc, char *argv[] ); #endif // _GAME_ENGINE_H_ diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 0bbdc5feb2d..dcbd48ceefd 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -124,6 +124,7 @@ class RecorderClass : public SubsystemInterface { void logPlayerDisconnect(UnicodeString player, Int slot); void logCRCMismatch( void ); + Bool sawCRCMismatch(); void cleanUpReplayFile( void ); ///< after a crash, send replay/debug info to a central repository void stopRecording(); ///< Stop recording and close m_file. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index c894544fae7..1840f2d7577 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -34,12 +34,16 @@ #include "GameLogic/GameLogic.h" // TheSuperHackers @feature helmutbuhler 04/13/2025 -// Simulate a list of replays without graphics -void SimulateReplayList(const std::vector &filenames) +// Simulate a list of replays without graphics. +// Returns exitcode 1 if mismatch occured +int SimulateReplayList(const std::vector &filenames) { + // Note that we use printf here because this is run from cmd. + Bool sawCRCMismatch = false; for (size_t i = 0; i < TheGlobalData->m_simulateReplayList.size(); i++) { AsciiString filename = TheGlobalData->m_simulateReplayList[i]; + printf("Simulating Replay %s\n", filename.str()); if (TheRecorder->simulateReplay(filename)) { do @@ -48,9 +52,14 @@ void SimulateReplayList(const std::vector &filenames) VERIFY_CRC } TheGameLogic->UPDATE(); - } while (TheRecorder->isAnalysisInProgress()); + sawCRCMismatch = TheRecorder->sawCRCMismatch(); + } while (TheRecorder->isAnalysisInProgress() && !sawCRCMismatch); } + if (sawCRCMismatch) + break; } + if (!sawCRCMismatch) + printf("Successfully simulated all replays\n"); // TheSuperHackers @todo helmutbuhler 04/13/2025 // There is a bug somewhere in the destructor of TheGameEngine which doesn't properly @@ -59,20 +68,22 @@ void SimulateReplayList(const std::vector &filenames) { TheGameLogic->clearGameData(); } + return sawCRCMismatch ? 1 : 0; } /** * This is the entry point for the game system. */ -void GameMain( int argc, char *argv[] ) +Int GameMain( int argc, char *argv[] ) { + int exitcode = 0; // initialize the game engine using factory function TheGameEngine = CreateGameEngine(); TheGameEngine->init(argc, argv); if (!TheGlobalData->m_simulateReplayList.empty()) { - SimulateReplayList(TheGlobalData->m_simulateReplayList); + exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList); } else { @@ -84,5 +95,6 @@ void GameMain( int argc, char *argv[] ) delete TheGameEngine; TheGameEngine = NULL; + return exitcode; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 1f792c10295..7153dfda14c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1012,6 +1012,11 @@ UnsignedInt CRCInfo::readCRC(void) return val; } +Bool RecorderClass::sawCRCMismatch() +{ + return m_crcInfo->sawCRCMismatch(); +} + void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback) { if (fromPlayback) @@ -1031,8 +1036,9 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f if (samePlayer || (localPlayerIndex < 0)) { UnsignedInt playbackCRC = m_crcInfo->readCRC(); + Int mismatchFrame = TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1; //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Comparing CRCs of InGame:%8.8X Replay:%8.8X Frame:%d from Player %d\n", - // playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1, playerIndex)); + // playbackCRC, newCRC, mismatchFrame, playerIndex)); if (TheGameLogic->getFrame() > 0 && newCRC != playbackCRC && !m_crcInfo->sawCRCMismatch()) { m_crcInfo->setSawCRCMismatch(); @@ -1051,7 +1057,31 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f // Note: We subtract the queue size from the frame no. This way we calculate the correct frame // the mismatch first happened in case the NetCRCInterval is set to 1 during the game. DEBUG_CRASH(("Replay has gone out of sync! All bets are off!\nInGame:%8.8X Replay:%8.8X\nFrame:%d", - playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1)); + playbackCRC, newCRC, mismatchFrame)); + + // TheSuperHackers @info helmutbuhler 04/13/2025 + // Print Mismatch to console in case we are in SimulateReplayList + printf("CRC Mismatch in Frame %d\n", mismatchFrame); + + // dump GameLogic random seed + DEBUG_LOG(("GameLogic frame = %d\n", TheGameLogic->getFrame())); + DEBUG_LOG(("GetGameLogicRandomSeedCRC() = %d\n", GetGameLogicRandomSeedCRC())); + + // dump CRCs + { + DEBUG_LOG(("--- GameState Dump ---\n")); + #ifdef DEBUG_CRC + outputCRCDumpLines(); + #endif + DEBUG_LOG(("------ End Dump ------\n")); + } + { + DEBUG_LOG(("--- DebugInfo Dump ---\n")); + #ifdef DEBUG_CRC + outputCRCDebugLines(); + #endif + DEBUG_LOG(("------ End Dump ------\n")); + } } return; } diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index ca0ffcf6039..ce83ac5a3b3 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -840,6 +840,7 @@ static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5; Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, Int nCmdShow ) { + Int exitcode = 1; checkProtection(); #ifdef _PROFILE @@ -909,7 +910,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, DEBUG_LOG(("0x%x - %s, %s, line %d address 0x%x\n", pc, name, file, line, addr)); } DEBUG_LOG(("\n--- END OF DX STACK DUMP\n")); - return 0; + return exitcode; } #ifdef _DEBUG @@ -953,7 +954,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // register windows class and create application window if( initializeAppWindows( hInstance, nCmdShow, ApplicationIsWindowed) == false ) - return 0; + return exitcode; if (gLoadScreenBitmap!=NULL) { ::DeleteObject(gLoadScreenBitmap); @@ -983,7 +984,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, TheVersion = NULL; shutdownMemoryManager(); DEBUG_SHUTDOWN(); - return 0; + return exitcode; } #endif @@ -1011,7 +1012,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, TheVersion = NULL; shutdownMemoryManager(); DEBUG_SHUTDOWN(); - return 0; + return exitcode; } DEBUG_LOG(("Create GeneralsMutex okay.\n")); @@ -1023,14 +1024,14 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, TheVersion = NULL; shutdownMemoryManager(); DEBUG_SHUTDOWN(); - return 0; + return exitcode; } #endif DEBUG_LOG(("CRC message is %d\n", GameMessage::MSG_LOGIC_CRC)); // run the game main loop - GameMain(argc, argv); + exitcode = GameMain(argc, argv); #ifdef DO_COPY_PROTECTION // Clean up copy protection @@ -1063,7 +1064,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, TheDmaCriticalSection = NULL; TheMemoryPoolCriticalSection = NULL; - return 0; + return exitcode; } // end WinMain From f0057b7843b0aeb4154a382b61c63c6590688d98 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 13 Apr 2025 18:45:54 +0200 Subject: [PATCH 006/112] Make Replay Simulation compile in Release and remove VERIFY_CRC. # Conflicts: # GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp --- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 2 +- GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp | 6 +----- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 9 +++++---- .../GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 10 ++++------ 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index dcbd48ceefd..9b44dedfee6 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -75,9 +75,9 @@ class RecorderClass : public SubsystemInterface { Bool simulateReplay(AsciiString filename); #if defined _DEBUG || defined _INTERNAL Bool analyzeReplay( AsciiString filename ); - Bool isAnalysisInProgress( void ); void stopAnalysis(); #endif + Bool isPlaybackInProgress(); public: void handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 1840f2d7577..fa4f5f0b31c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -30,7 +30,6 @@ #include "Common/GameEngine.h" #include "Common/Recorder.h" -#include "Common/CRCDebug.h" #include "GameLogic/GameLogic.h" // TheSuperHackers @feature helmutbuhler 04/13/2025 @@ -48,12 +47,9 @@ int SimulateReplayList(const std::vector &filenames) { do { - { - VERIFY_CRC - } TheGameLogic->UPDATE(); sawCRCMismatch = TheRecorder->sawCRCMismatch(); - } while (TheRecorder->isAnalysisInProgress() && !sawCRCMismatch); + } while (TheRecorder->isPlaybackInProgress() && !sawCRCMismatch); } if (sawCRCMismatch) break; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 7153dfda14c..3aa2bcb7481 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -917,10 +917,6 @@ Bool RecorderClass::analyzeReplay( AsciiString filename ) return playbackFile(filename); } -Bool RecorderClass::isAnalysisInProgress( void ) -{ - return isPlaybackMode() && m_nextFrame != -1; -} void RecorderClass::stopAnalysis() { @@ -928,6 +924,11 @@ void RecorderClass::stopAnalysis() } #endif +Bool RecorderClass::isPlaybackInProgress( void ) +{ + return isPlaybackMode() && m_nextFrame != -1; +} + AsciiString RecorderClass::getCurrentReplayFilename( void ) { if (isPlaybackMode()) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 857ae253a14..d35d921529f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -37,7 +37,6 @@ #include "Common/GameState.h" #include "Common/Recorder.h" #include "Common/version.h" -#include "Common/CRCDebug.h" #include "GameClient/WindowLayout.h" #include "GameClient/Gadget.h" #include "GameClient/GadgetListBox.h" @@ -549,7 +548,7 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, do { TheRecorder->update(); - } while (TheRecorder->isAnalysisInProgress()); + } while (TheRecorder->isPlaybackInProgress()); TheRecorder->stopAnalysis(); } } @@ -574,11 +573,10 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, { do { - { - VERIFY_CRC - } TheGameLogic->UPDATE(); - } while (TheRecorder->isAnalysisInProgress()); + if (TheRecorder->sawCRCMismatch()) + break; + } while (TheRecorder->isPlaybackInProgress()); } } } From 3241aef1c0c3c64865d7f09b2e076fb67792fdf5 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 13 Apr 2025 19:37:55 +0200 Subject: [PATCH 007/112] Fix draw assignment in StealthUpdate::changeVisualDisguise() --- .../Source/GameLogic/Object/Update/StealthUpdate.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp index 260f86224cb..26c37c4b5c5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp @@ -1055,7 +1055,9 @@ void StealthUpdate::changeVisualDisguise() const ThingTemplate *tTemplate = self->getTemplate(); - TheThingFactory->newDrawable( tTemplate ); + // TheSuperHackers @bugfix helmutbuhler 04/13/2025 + // draw was originally not assigned here and potentially caused memory corruption + draw = TheThingFactory->newDrawable( tTemplate ); if( draw ) { TheGameLogic->bindObjectAndDrawable(self, draw); From 9fa390b6223919488582e861f37318d76fc766b9 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 18 Apr 2025 17:03:50 +0200 Subject: [PATCH 008/112] Show analyze and simulate button in replaymenu when DEBUG_LOGGING is defined (even in Release mode) --- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 2 +- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- .../Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 9b44dedfee6..8c3e34937a7 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -73,7 +73,7 @@ class RecorderClass : public SubsystemInterface { AsciiString getCurrentReplayFilename( void ); ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. Bool simulateReplay(AsciiString filename); -#if defined _DEBUG || defined _INTERNAL +#ifdef DEBUG_LOGGING Bool analyzeReplay( AsciiString filename ); void stopAnalysis(); #endif diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 3aa2bcb7481..1ffad5be908 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -910,7 +910,7 @@ Bool RecorderClass::simulateReplay(AsciiString filename) return success; } -#if defined _DEBUG || defined _INTERNAL +#ifdef DEBUG_LOGGING Bool RecorderClass::analyzeReplay( AsciiString filename ) { m_doingAnalysis = TRUE; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index d35d921529f..d823441a10a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -76,7 +76,7 @@ static Int initialGadgetDelay = 2; static Bool justEntered = FALSE; -#if defined _DEBUG || defined _INTERNAL +#ifdef DEBUG_LOGGING static GameWindow *buttonAnalyzeReplay = NULL; // TheSuperHackers @feature helmutbuhler 04/13/2025 @@ -296,7 +296,7 @@ void ReplayMenuInit( WindowLayout *layout, void *userData ) GadgetListBoxReset(listboxReplayFiles); PopulateReplayFileListbox(listboxReplayFiles); -#if defined _DEBUG || defined _INTERNAL +#ifdef DEBUG_LOGGING WinInstanceData instData; instData.init(); BitSet( instData.m_style, GWS_PUSH_BUTTON | GWS_MOUSE_TRACK ); @@ -526,7 +526,7 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, GameWindow *control = (GameWindow *)mData1; Int controlID = control->winGetWindowId(); -#if defined _DEBUG || defined _INTERNAL +#ifdef DEBUG_LOGGING if( controlID == buttonAnalyzeReplay->winGetWindowId() ) { if(listboxReplayFiles) From 7900ab1da466b22601e4fb0b6b6ef9b7faa44430 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 26 Apr 2025 10:19:57 +0200 Subject: [PATCH 009/112] Add code to write out replay list to textfile # Conflicts: # GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index d823441a10a..55867fa5e13 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -108,6 +108,52 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index) } + +void WriteOutReplayList() +{ + AsciiString fname; + fname.format("%sreplay_list.replist", TheRecorder->getReplayDir().str()); + FILE *fp = fopen(fname.str(), "wt"); + if (!fp) + return; + + AsciiString asciistr; + AsciiString asciisearch; + asciisearch = "*"; + asciisearch.concat(TheRecorder->getReplayExtention()); + + FilenameList replayFilenames; + TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenames, TRUE); + + for (FilenameListIter it = replayFilenames.begin(); it != replayFilenames.end(); ++it) + { + // just want the filename + asciistr.set((*it).reverseFind('\\') + 1); + RecorderClass::ReplayHeader header; + ReplayGameInfo info; + const MapMetaData *md; + Bool success = GetMapInfo(asciistr, &header, &info, &md); + if (!success) + continue; + + UnicodeString replayNameToShow = header.replayName; + AsciiString replayNameToShowAscii; + replayNameToShowAscii.translate(replayNameToShow); + + fprintf(fp, "%s #", replayNameToShowAscii.str()); + + if (!md) + fprintf(fp, " no map"); + fprintf(fp, header.localPlayerIndex >= 0 ? " MP" : " SP"); + if (header.quitEarly) + fprintf(fp, " quitearly"); + if (header.desyncGame) + fprintf(fp, " mismatch"); + fprintf(fp, "\n"); + } + fclose(fp); +} + //------------------------------------------------------------------------------------------------- /** Populate the listbox with the names of the available replay files */ //------------------------------------------------------------------------------------------------- @@ -268,6 +314,7 @@ void PopulateReplayFileListbox(GameWindow *listbox) } } GadgetListBoxSetSelected(listbox, 0); + WriteOutReplayList(); } //------------------------------------------------------------------------------------------------- From 3499ccd56ac35786714f777fd8ee408b25024212 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 27 Apr 2025 00:46:12 +0200 Subject: [PATCH 010/112] Update WriteOutReplayList() --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 55867fa5e13..1881b548518 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -117,7 +117,6 @@ void WriteOutReplayList() if (!fp) return; - AsciiString asciistr; AsciiString asciisearch; asciisearch = "*"; asciisearch.concat(TheRecorder->getReplayExtention()); @@ -127,29 +126,29 @@ void WriteOutReplayList() for (FilenameListIter it = replayFilenames.begin(); it != replayFilenames.end(); ++it) { - // just want the filename - asciistr.set((*it).reverseFind('\\') + 1); + AsciiString filename; + filename.set(it->reverseFind('\\') + 1); RecorderClass::ReplayHeader header; ReplayGameInfo info; const MapMetaData *md; - Bool success = GetMapInfo(asciistr, &header, &info, &md); + Bool success = GetMapInfo(filename, &header, &info, &md); if (!success) continue; - UnicodeString replayNameToShow = header.replayName; - AsciiString replayNameToShowAscii; - replayNameToShowAscii.translate(replayNameToShow); - fprintf(fp, "%s #", replayNameToShowAscii.str()); - + AsciiString extra; if (!md) - fprintf(fp, " no map"); - fprintf(fp, header.localPlayerIndex >= 0 ? " MP" : " SP"); + extra.concat(" no map"); + //extra.concat(header.localPlayerIndex >= 0 ? " MP" : " SP"); if (header.quitEarly) - fprintf(fp, " quitearly"); + extra.concat(" quitearly"); if (header.desyncGame) - fprintf(fp, " mismatch"); - fprintf(fp, "\n"); + extra.concat(" mismatch"); + + if (extra.getLength() != 0) + fprintf(fp, "%s #%s\n", filename.str(), extra.str()); + else + fprintf(fp, "%s\n", filename.str()); } fclose(fp); } From 818161f694916ec98617c8d32f2174334e0031ba Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Tue, 29 Apr 2025 22:22:58 +0200 Subject: [PATCH 011/112] Make Replay extra buttons show up always --- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 2 +- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- .../Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 8c3e34937a7..7c4a01942aa 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -73,7 +73,7 @@ class RecorderClass : public SubsystemInterface { AsciiString getCurrentReplayFilename( void ); ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. Bool simulateReplay(AsciiString filename); -#ifdef DEBUG_LOGGING +#if 1 Bool analyzeReplay( AsciiString filename ); void stopAnalysis(); #endif diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 1ffad5be908..1c0fd2819b3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -910,7 +910,7 @@ Bool RecorderClass::simulateReplay(AsciiString filename) return success; } -#ifdef DEBUG_LOGGING +#if 1 Bool RecorderClass::analyzeReplay( AsciiString filename ) { m_doingAnalysis = TRUE; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 1881b548518..1764a83da31 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -76,7 +76,7 @@ static Int initialGadgetDelay = 2; static Bool justEntered = FALSE; -#ifdef DEBUG_LOGGING +#if 1 static GameWindow *buttonAnalyzeReplay = NULL; // TheSuperHackers @feature helmutbuhler 04/13/2025 @@ -342,7 +342,7 @@ void ReplayMenuInit( WindowLayout *layout, void *userData ) GadgetListBoxReset(listboxReplayFiles); PopulateReplayFileListbox(listboxReplayFiles); -#ifdef DEBUG_LOGGING +#if 1 WinInstanceData instData; instData.init(); BitSet( instData.m_style, GWS_PUSH_BUTTON | GWS_MOUSE_TRACK ); @@ -572,7 +572,7 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, GameWindow *control = (GameWindow *)mData1; Int controlID = control->winGetWindowId(); -#ifdef DEBUG_LOGGING +#if 1 if( controlID == buttonAnalyzeReplay->winGetWindowId() ) { if(listboxReplayFiles) From 19b8a4248e0a2ef1d2e0d707d9b8939cebe68f16 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Tue, 29 Apr 2025 23:21:24 +0200 Subject: [PATCH 012/112] Add m_playbackFrameDuration to Recorder --- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 2 ++ GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 7c4a01942aa..ff8adfa901f 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -71,6 +71,7 @@ class RecorderClass : public SubsystemInterface { Bool playbackFile(AsciiString filename); ///< Starts playback of the specified file. Bool testVersionPlayback(AsciiString filename); ///< Returns if the playback is a valid playback file for this version or not. AsciiString getCurrentReplayFilename( void ); ///< valid during playback only + UnsignedInt getFrameDuration() { return m_playbackFrameDuration; } ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. Bool simulateReplay(AsciiString filename); #if 1 @@ -149,6 +150,7 @@ class RecorderClass : public SubsystemInterface { Int m_currentFilePosition; RecorderModeType m_mode; AsciiString m_currentReplayFilename; ///< valid during playback only + UnsignedInt m_playbackFrameDuration; ReplayGameInfo m_gameInfo; Bool m_wasDesync; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 1c0fd2819b3..34983ca2789 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -406,6 +406,7 @@ void RecorderClass::init() { m_gameInfo.setSeed(GetGameLogicRandomSeed()); m_wasDesync = FALSE; m_doingAnalysis = FALSE; + m_playbackFrameDuration = 0; } /** @@ -1241,6 +1242,7 @@ Bool RecorderClass::playbackFile(AsciiString filename) } m_currentReplayFilename = filename; + m_playbackFrameDuration = header.frameDuration; return TRUE; } From 4e562e291cd7b64b24aff1170d4422a7257d78f8 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Tue, 29 Apr 2025 23:23:46 +0200 Subject: [PATCH 013/112] Update WriteOutReplayList and add ReadReplayListFromCsv and -simReplayList option --- .../GameEngine/Source/Common/CommandLine.cpp | 16 +++ .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 124 ++++++++++++++++-- 2 files changed, 127 insertions(+), 13 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index f27812e5c44..af03db6130d 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -445,6 +445,18 @@ Int parseSimReplay(char *args[], int num) return 1; } +Int parseSimReplayList(char *args[], int num) +{ + if (TheWritableGlobalData && num > 1) + { + void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); + AsciiString filename = args[1]; + ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplayList); + return 2; + } + return 1; +} + Int parseXRes(char *args[], int num) { if (TheWritableGlobalData && num > 1) @@ -1243,6 +1255,10 @@ static CommandLineParam params[] = // You can pass this multiple times to check multiple replays. { "-simReplay", parseSimReplay }, + // TheSuperHackers @feature helmutbuhler 28/04/2025 + // Pass in a csv file to simulate multiple replays. The file must be in the replay folder. + { "-simReplayList", parseSimReplayList }, + #if (defined(_DEBUG) || defined(_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 1764a83da31..1355a27b6a4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -112,31 +112,51 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index) void WriteOutReplayList() { AsciiString fname; - fname.format("%sreplay_list.replist", TheRecorder->getReplayDir().str()); + fname.format("%sreplay_list.csv", TheRecorder->getReplayDir().str()); FILE *fp = fopen(fname.str(), "wt"); if (!fp) return; + // Get list of replay filenames AsciiString asciisearch; asciisearch = "*"; asciisearch.concat(TheRecorder->getReplayExtention()); + FilenameList replayFilenamesSet; + TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenamesSet, TRUE); + std::vector replayFilenames; + for (FilenameListIter it = replayFilenamesSet.begin(); it != replayFilenamesSet.end(); ++it) + { + replayFilenames.push_back(*it); + } - FilenameList replayFilenames; - TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenames, TRUE); - - for (FilenameListIter it = replayFilenames.begin(); it != replayFilenames.end(); ++it) + // Print out a line per filename. i = -1 is csv header. + for (int i = -1; i < (int)replayFilenames.size(); i++) { AsciiString filename; - filename.set(it->reverseFind('\\') + 1); RecorderClass::ReplayHeader header; ReplayGameInfo info; - const MapMetaData *md; - Bool success = GetMapInfo(filename, &header, &info, &md); - if (!success) - continue; + const MapMetaData *md = NULL; + if (i != -1) + { + filename.set(replayFilenames[i].reverseFind('\\') + 1); + Bool success = GetMapInfo(filename, &header, &info, &md); + if (!success) + continue; + } + bool check = md && !header.desyncGame && header.endTime != 0; + fprintf(fp, "%s", i == -1 ? "check" : check ? "1" : "0"); - - AsciiString extra; + if (i == -1) + fprintf(fp, ",filename"); + else + fprintf(fp, ",\"%s\"", filename.str()); + + fprintf(fp, ",%s", i == -1 ? "map_exists" : md ? "1" : "0"); + fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); + fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); + fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); + + /*AsciiString extra; if (!md) extra.concat(" no map"); //extra.concat(header.localPlayerIndex >= 0 ? " MP" : " SP"); @@ -148,7 +168,85 @@ void WriteOutReplayList() if (extra.getLength() != 0) fprintf(fp, "%s #%s\n", filename.str(), extra.str()); else - fprintf(fp, "%s\n", filename.str()); + fprintf(fp, "%s\n", filename.str());*/ + fprintf(fp, "\n"); + } + fclose(fp); +} + +bool ReadLineFromFile(FILE *fp, AsciiString *str) +{ + char buffer[124]; + if (fgets(buffer, 124, fp) == NULL) + { + str->clear(); + return false; + } + buffer[124-1] = 0; + str->set(buffer); + return true; +} + +void NextToken(AsciiString *string, AsciiString *token, char separator) +{ + const char *tokenStart = string->str(); + + const char *str = tokenStart; + bool inQuotationMarks = false; + while (*str) + { + if (*str == separator && !inQuotationMarks) + break; + if (*str == '\"') + inQuotationMarks = !inQuotationMarks; + str++; + } + const char *tokenEnd = str; + + Int len = tokenEnd - tokenStart; + char *tmp = token->getBufferForRead(len + 1); + memcpy(tmp, tokenStart, len); + tmp[len] = 0; + token->trim(); + + string->set(*tokenEnd == 0 ? tokenEnd : tokenEnd+1); +} + +void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) +{ + AsciiString fname; + fname.format("%s%s", TheRecorder->getReplayDir().str(), filename.str()); + FILE *fp = fopen(fname.str(), "rt"); + if (!fp) + return; + + // Parse header + AsciiString line, token; + ReadLineFromFile(fp, &line); + char separator = line.find(';') == NULL ? ',' : ';'; + + while (feof(fp) == 0) + { + ReadLineFromFile(fp, &line); + + // Parse check + NextToken(&line, &token, separator); + if (token != "1") + continue; + + // Parse filename + NextToken(&line, &token, separator); + if (token.isEmpty()) + continue; + if (token.getCharAt(0) == '\"' && token.getCharAt(token.getLength()-1) == '\"') + { + token.set(token.str()+1); + token.removeLastChar(); + } + if (!token.isEmpty()) + replayList->push_back(token); + + // Ignore remaining columns } fclose(fp); } From 60789e6a6075f49ebc9206ebfb90ba71bbe110fc Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Tue, 29 Apr 2025 23:27:07 +0200 Subject: [PATCH 014/112] Add more logging to SimulateReplayList and run each replay in separate process --- .../GameEngine/Source/Common/GameMain.cpp | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index fa4f5f0b31c..ffb7cd904bb 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -35,31 +35,99 @@ // TheSuperHackers @feature helmutbuhler 04/13/2025 // Simulate a list of replays without graphics. // Returns exitcode 1 if mismatch occured -int SimulateReplayList(const std::vector &filenames) +int SimulateReplayList(const std::vector &filenames, int argc, char *argv[]) { // Note that we use printf here because this is run from cmd. + Int fps = TheGlobalData->m_framesPerSecondLimit; Bool sawCRCMismatch = false; - for (size_t i = 0; i < TheGlobalData->m_simulateReplayList.size(); i++) + DWORD totalStartTime = GetTickCount(); + for (size_t i = 0; i < filenames.size(); i++) { - AsciiString filename = TheGlobalData->m_simulateReplayList[i]; - printf("Simulating Replay %s\n", filename.str()); - if (TheRecorder->simulateReplay(filename)) + AsciiString filename = filenames[i]; + if (filenames.size() == 1) { - do + printf("Simulating Replay \"%s\"\n", filename.str()); + fflush(stdout); + DWORD startTime = GetTickCount(); + if (TheRecorder->simulateReplay(filename)) { - TheGameLogic->UPDATE(); - sawCRCMismatch = TheRecorder->sawCRCMismatch(); - } while (TheRecorder->isPlaybackInProgress() && !sawCRCMismatch); + UnsignedInt totalTime = TheRecorder->getFrameDuration() / fps; + do + { + if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*fps) == 0) + { + UnsignedInt gameTime = TheGameLogic->getFrame() / fps; + UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + printf("Frame %02d:%02d/%02d:%02d RT: %02d:%02d\n", + gameTime/60, gameTime%60, totalTime/60, totalTime%60, realTime/60, realTime%60); + fflush(stdout); + } + TheGameLogic->UPDATE(); + sawCRCMismatch = TheRecorder->sawCRCMismatch(); + } while (TheRecorder->isPlaybackInProgress() && !sawCRCMismatch); + + UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + printf("GT: %02d:%02d RT: %02d:%02d\n", totalTime/60, totalTime%60, realTime/60, realTime%60); + fflush(stdout); + } + else + { + printf("Cannot open replay\n"); + sawCRCMismatch = true; + } + } + else + { + printf("%d/%d ", i+1, filenames.size()); + fflush(stdout); + + STARTUPINFO si = { sizeof(STARTUPINFO) }; + si.dwFlags = STARTF_FORCEOFFFEEDBACK; + PROCESS_INFORMATION pi = { 0 }; + AsciiString command; + command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); + //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); + fflush(stdout); + CreateProcessA(NULL, (LPSTR)command.str(), + NULL, NULL, TRUE, 0, + NULL, + 0, + &si, + &pi); + CloseHandle(pi.hThread); + WaitForSingleObject(pi.hProcess, INFINITE); + DWORD exitcode = 1; + GetExitCodeProcess(pi.hProcess, &exitcode); + CloseHandle(pi.hProcess); + sawCRCMismatch = sawCRCMismatch || exitcode; } if (sawCRCMismatch) break; + /*if (i == TheGlobalData->m_simulateReplayList.size()-1) + { + if (TheGameLogic->isInGame()) + { + TheGameLogic->clearGameData(); + } + delete TheGameEngine; + TheGameEngine = NULL; + TheGameEngine = CreateGameEngine(); + TheGameEngine->init(argc, argv); + }*/ + } + if (TheGlobalData->m_simulateReplayList.size() > 1) + { + if (!sawCRCMismatch) + printf("Successfully simulated all replays\n"); + + UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; + printf("Total Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); + fflush(stdout); } - if (!sawCRCMismatch) - printf("Successfully simulated all replays\n"); // TheSuperHackers @todo helmutbuhler 04/13/2025 // There is a bug somewhere in the destructor of TheGameEngine which doesn't properly - // clean up the players and causes a crash in debug unless this is called. + // clean up the players and causes a crash unless this is called. if (TheGameLogic->isInGame()) { TheGameLogic->clearGameData(); @@ -79,7 +147,7 @@ Int GameMain( int argc, char *argv[] ) if (!TheGlobalData->m_simulateReplayList.empty()) { - exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList); + exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList, argc, argv); } else { From 3852327ee99b9fb3d3cfdb2efcb646fe170a9d2e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Tue, 29 Apr 2025 23:44:10 +0200 Subject: [PATCH 015/112] SimulateReplayList: Count errors --- .../GameEngine/Source/Common/GameMain.cpp | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index ffb7cd904bb..a1a1d44291c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -39,7 +39,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char { // Note that we use printf here because this is run from cmd. Int fps = TheGlobalData->m_framesPerSecondLimit; - Bool sawCRCMismatch = false; + int numErrors = 0; DWORD totalStartTime = GetTickCount(); for (size_t i = 0; i < filenames.size(); i++) { @@ -52,7 +52,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char if (TheRecorder->simulateReplay(filename)) { UnsignedInt totalTime = TheRecorder->getFrameDuration() / fps; - do + while (TheRecorder->isPlaybackInProgress()) { if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*fps) == 0) { @@ -63,9 +63,12 @@ int SimulateReplayList(const std::vector &filenames, int argc, char fflush(stdout); } TheGameLogic->UPDATE(); - sawCRCMismatch = TheRecorder->sawCRCMismatch(); - } while (TheRecorder->isPlaybackInProgress() && !sawCRCMismatch); - + if (TheRecorder->sawCRCMismatch()) + { + numErrors++; + break; + } + } UnsignedInt realTime = (GetTickCount()-startTime) / 1000; printf("GT: %02d:%02d RT: %02d:%02d\n", totalTime/60, totalTime%60, realTime/60, realTime%60); fflush(stdout); @@ -73,7 +76,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char else { printf("Cannot open replay\n"); - sawCRCMismatch = true; + numErrors++; } } else @@ -99,10 +102,8 @@ int SimulateReplayList(const std::vector &filenames, int argc, char DWORD exitcode = 1; GetExitCodeProcess(pi.hProcess, &exitcode); CloseHandle(pi.hProcess); - sawCRCMismatch = sawCRCMismatch || exitcode; + numErrors += exitcode ? 1 : 0; } - if (sawCRCMismatch) - break; /*if (i == TheGlobalData->m_simulateReplayList.size()-1) { if (TheGameLogic->isInGame()) @@ -117,7 +118,9 @@ int SimulateReplayList(const std::vector &filenames, int argc, char } if (TheGlobalData->m_simulateReplayList.size() > 1) { - if (!sawCRCMismatch) + if (numErrors) + printf("Errors occured: %d\n", numErrors); + else printf("Successfully simulated all replays\n"); UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; @@ -132,7 +135,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char { TheGameLogic->clearGameData(); } - return sawCRCMismatch ? 1 : 0; + return numErrors != 0 ? 1 : 0; } /** From 8faca18391d58c2c5d7e9c7cde7dae20080ff7d2 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Wed, 30 Apr 2025 22:25:13 +0200 Subject: [PATCH 016/112] Factor CreateProcessA into helper function --- .../GameEngine/Source/Common/GameMain.cpp | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index a1a1d44291c..b03ac658683 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -32,9 +32,29 @@ #include "Common/Recorder.h" #include "GameLogic/GameLogic.h" +bool SimulateReplayInProcess(AsciiString filename) +{ + STARTUPINFO si = { sizeof(STARTUPINFO) }; + si.dwFlags = STARTF_FORCEOFFFEEDBACK; + PROCESS_INFORMATION pi = { 0 }; + AsciiString command; + command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); + //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); + //fflush(stdout); + CreateProcessA(NULL, (LPSTR)command.str(), + NULL, NULL, TRUE, 0, + NULL, 0, &si, &pi); + CloseHandle(pi.hThread); + WaitForSingleObject(pi.hProcess, INFINITE); + DWORD exitcode = 1; + GetExitCodeProcess(pi.hProcess, &exitcode); + CloseHandle(pi.hProcess); + return exitcode != 0; +} + // TheSuperHackers @feature helmutbuhler 04/13/2025 // Simulate a list of replays without graphics. -// Returns exitcode 1 if mismatch occured +// Returns exitcode 1 if mismatch or other error occured int SimulateReplayList(const std::vector &filenames, int argc, char *argv[]) { // Note that we use printf here because this is run from cmd. @@ -83,26 +103,8 @@ int SimulateReplayList(const std::vector &filenames, int argc, char { printf("%d/%d ", i+1, filenames.size()); fflush(stdout); - - STARTUPINFO si = { sizeof(STARTUPINFO) }; - si.dwFlags = STARTF_FORCEOFFFEEDBACK; - PROCESS_INFORMATION pi = { 0 }; - AsciiString command; - command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); - //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); - fflush(stdout); - CreateProcessA(NULL, (LPSTR)command.str(), - NULL, NULL, TRUE, 0, - NULL, - 0, - &si, - &pi); - CloseHandle(pi.hThread); - WaitForSingleObject(pi.hProcess, INFINITE); - DWORD exitcode = 1; - GetExitCodeProcess(pi.hProcess, &exitcode); - CloseHandle(pi.hProcess); - numErrors += exitcode ? 1 : 0; + bool error = SimulateReplayInProcess(filename); + numErrors += error ? 1 : 0; } /*if (i == TheGlobalData->m_simulateReplayList.size()-1) { From 6b23bf896710cb82e0060178472f09ac32b230c5 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 2 May 2025 14:29:07 +0200 Subject: [PATCH 017/112] Make Performance Timers work (WIP) --- .../Include/Common/CriticalSection.h | 1 + .../GameEngine/Include/Common/PerfTimer.h | 16 +++-- .../GameEngine/Source/Common/PerfTimer.cpp | 14 ++--- .../GameEngine/Source/Common/System/Debug.cpp | 2 +- .../Source/Common/System/FileSystem.cpp | 14 ++--- .../Source/Common/System/GameMemory.cpp | 58 +++++++++---------- .../Source/GameClient/GraphDraw.cpp | 2 + 7 files changed, 59 insertions(+), 48 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h b/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h index 37075830e53..7987c14117c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h @@ -33,6 +33,7 @@ #include "Common/PerfTimer.h" +#undef PERF_TIMERS #ifdef PERF_TIMERS extern PerfGather TheCritSecPerfGather; #endif diff --git a/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h b/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h index c7a952d1c69..92ab553ca20 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h @@ -38,9 +38,9 @@ NOTE NOTE NOTE: never check this in with this enabled, since there is a nonzero time penalty for running in this mode. Only enable it for local builds for testing purposes! (srj) */ - #define NO_PERF_TIMERS + #define PERF_TIMERS #else - #define NO_PERF_TIMERS + #define PERF_TIMERS #endif #include "Common/GameCommon.h" // ensure we get DUMP_PERF_STATS, or not @@ -58,7 +58,8 @@ class DebugDisplayInterface; //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- -#define NO_USE_QPF // non-QPF is much faster. +//#define NO_USE_QPF // non-QPF is much faster. +#define USE_QPF // non-QPF is much faster. #if defined(PERF_TIMERS) || defined(DUMP_PERF_STATS) //------------------------------------------------------------------------------------------------- @@ -89,7 +90,7 @@ class PerfGather { public: // If net only (default), subtract perf timers running inside. [8/12/2003] - PerfGather( const char *identifier, Bool netOnly=true ); + PerfGather( const char *identifier/*, Bool netOnly=true*/ ); virtual ~PerfGather( ); __forceinline void startTimer(); @@ -132,18 +133,24 @@ class PerfGather Bool m_ignore; Bool m_netTimeOnly; }; +extern DWORD theMainThreadID; //------------------------------------------------------------------------------------------------- void PerfGather::startTimer() { + DEBUG_ASSERTCRASH(theMainThreadID == GetCurrentThreadId(), ("PerfGather Start Thread")); *++m_activeHead = this; + DEBUG_ASSERTCRASH(m_activeHead >= &m_active[0] && m_activeHead <= &m_active[MAX_ACTIVE_STACK-1], ("active under/over flow")); + //DEBUG_LOG(("startTimer %s %d\n", m_identifier, int(m_activeHead-&m_active[0]))); GetPrecisionTimer(&m_startTime); } //------------------------------------------------------------------------------------------------- void PerfGather::stopTimer() { + //DEBUG_LOG(("stopTimer %s %d\n", m_identifier, int(m_activeHead-&m_active[0]))); DEBUG_ASSERTCRASH(this != NULL, ("I am null, uh oh")); + DEBUG_ASSERTCRASH(theMainThreadID == GetCurrentThreadId(), ("PerfGather Thread")); Int64 runTime; GetPrecisionTimer(&runTime); @@ -161,6 +168,7 @@ void PerfGather::stopTimer() DEBUG_ASSERTCRASH(m_activeHead >= &m_active[0] && m_activeHead <= &m_active[MAX_ACTIVE_STACK-1], ("active under/over flow")); #endif --m_activeHead; + DEBUG_ASSERTCRASH(m_activeHead >= &m_active[0] && m_activeHead <= &m_active[MAX_ACTIVE_STACK-1], ("active under/over flow")); if (*m_activeHead) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp b/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp index 0d4fcec12f1..ff2538720b2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp @@ -64,7 +64,7 @@ void GetPrecisionTimerTicksPerSec(Int64* t) } //Kris: Plugged in Martin's code to optimize timer setup. -#define HOFFESOMMER_REPLACEMENT_CODE +//#define HOFFESOMMER_REPLACEMENT_CODE //------------------------------------------------------------------------------------------------- void InitPrecisionTimer() @@ -246,7 +246,7 @@ class PerfMetricsOutput { if (m_outputStats[i].first == id) { - m_outputStats.erase(&m_outputStats[i]); + m_outputStats.erase(m_outputStats.begin()+i); return; } } @@ -375,7 +375,7 @@ void PerfGather::reset() Int64 start, end; PerfGather pf("timer"); GetPrecisionTimer(&start); - for (Int ii = 0; ii < ITERS; ++ii) + /*for (Int ii = 0; ii < ITERS; ++ii) { pf.startTimer(); pf.stopTimer(); pf.startTimer(); pf.stopTimer(); @@ -385,7 +385,7 @@ void PerfGather::reset() pf.startTimer(); pf.stopTimer(); pf.startTimer(); pf.stopTimer(); pf.startTimer(); pf.stopTimer(); - } + }*/ GetPrecisionTimer(&end); s_stopStartOverhead = (end - start) / (ITERS*8); DEBUG_LOG(("s_stopStartOverhead is %d (%f usec)\n",(int)s_stopStartOverhead,s_stopStartOverhead/s_ticksPerUSec)); @@ -408,7 +408,7 @@ void PerfGather::reset() return; } - if (frame >= 1 && frame <= 30) + if (frame >= 0 && frame <= 30) { // always skip the first second or so, since it loads everything and skews the results horribly } @@ -597,7 +597,7 @@ void PerfTimer::outputInfo( void ) return; } -#if defined(_DEBUG) || defined(_INTERNAL) +#if 1 double totalTimeInMS = 1000.0 * m_runningTime / s_ticksPerSec; double avgTimePerFrame = totalTimeInMS / (m_lastFrame - m_startFrame + 1); double avgTimePerCall = totalTimeInMS / m_callCount; @@ -635,7 +635,7 @@ void PerfTimer::outputInfo( void ) //------------------------------------------------------------------------------------------------- void PerfTimer::showMetrics( void ) { -#if defined(_DEBUG) || defined(_INTERNAL) +#if 1 double totalTimeInMS = 1000.0 * m_runningTime / s_ticksPerSec; double avgTimePerFrame = totalTimeInMS / (m_lastFrame - m_startFrame + 1); double avgTimePerCall = totalTimeInMS / m_callCount; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp index 40c29ed4055..163e8443342 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp @@ -112,7 +112,7 @@ static FILE *theLogFile = NULL; #define LARGE_BUFFER 8192 static char theBuffer[ LARGE_BUFFER ]; // make it big to avoid weird overflow bugs in debug mode static int theDebugFlags = 0; -static DWORD theMainThreadID = 0; +DWORD theMainThreadID = 0; // ---------------------------------------------------------------------------- // PUBLIC DATA // ---------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp index 1a59074af23..b53578cd3ae 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp @@ -151,7 +151,7 @@ void FileSystem::init( void ) void FileSystem::update( void ) { - USE_PERF_TIMER(FileSystem) + //USE_PERF_TIMER(FileSystem) TheLocalFileSystem->update(); TheArchiveFileSystem->update(); } @@ -162,7 +162,7 @@ void FileSystem::update( void ) void FileSystem::reset( void ) { - USE_PERF_TIMER(FileSystem) + //USE_PERF_TIMER(FileSystem) TheLocalFileSystem->reset(); TheArchiveFileSystem->reset(); } @@ -173,7 +173,7 @@ void FileSystem::reset( void ) File* FileSystem::openFile( const Char *filename, Int access ) { - USE_PERF_TIMER(FileSystem) + //USE_PERF_TIMER(FileSystem) File *file = NULL; if ( TheLocalFileSystem != NULL ) @@ -195,7 +195,7 @@ File* FileSystem::openFile( const Char *filename, Int access ) Bool FileSystem::doesFileExist(const Char *filename) const { - USE_PERF_TIMER(FileSystem) + //USE_PERF_TIMER(FileSystem) unsigned key=TheNameKeyGenerator->nameToLowercaseKey(filename); std::map::iterator i=m_fileExist.find(key); @@ -222,7 +222,7 @@ Bool FileSystem::doesFileExist(const Char *filename) const //============================================================================ void FileSystem::getFileListInDirectory(const AsciiString& directory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const { - USE_PERF_TIMER(FileSystem) + //USE_PERF_TIMER(FileSystem) TheLocalFileSystem->getFileListInDirectory(AsciiString(""), directory, searchName, filenameList, searchSubdirectories); TheArchiveFileSystem->getFileListInDirectory(AsciiString(""), directory, searchName, filenameList, searchSubdirectories); } @@ -232,7 +232,7 @@ void FileSystem::getFileListInDirectory(const AsciiString& directory, const Asci //============================================================================ Bool FileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo) const { - USE_PERF_TIMER(FileSystem) + //USE_PERF_TIMER(FileSystem) if (fileInfo == NULL) { return FALSE; } @@ -254,7 +254,7 @@ Bool FileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo) co //============================================================================ Bool FileSystem::createDirectory(AsciiString directory) { - USE_PERF_TIMER(FileSystem) + //USE_PERF_TIMER(FileSystem) if (TheLocalFileSystem != NULL) { return TheLocalFileSystem->createDirectory(directory); } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp index f34d17d8b4f..13f3e7039c8 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp @@ -243,10 +243,10 @@ static void* sysAllocateDoNotZero(Int numBytes) throw ERROR_OUT_OF_MEMORY; #ifdef MEMORYPOOL_DEBUG { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) #ifdef USE_FILLER_VALUE { - USE_PERF_TIMER(MemoryPoolInitFilling) + //USE_PERF_TIMER(MemoryPoolInitFilling) ::memset32(p, s_initFillerValue, ::GlobalSize(p)); } #endif @@ -269,7 +269,7 @@ static void sysFree(void* p) { #ifdef MEMORYPOOL_DEBUG { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) ::memset32(p, GARBAGE_FILL_VALUE, ::GlobalSize(p)); theTotalSystemAllocationInBytes -= ::GlobalSize(p); } @@ -859,7 +859,7 @@ void MemoryPoolSingleBlock::initBlock(Int logicalSize, MemoryPoolBlob *owningBlo #ifdef MEMORYPOOL_DEBUG { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) m_magicCookie = SINGLEBLOCK_MAGIC_COOKIE; m_debugFlags = 0; if (!theMainInitFlag) @@ -1037,7 +1037,7 @@ Int MemoryPoolSingleBlock::debugSingleBlockReportLeak(const char* owner) */ void MemoryPoolSingleBlock::debugVerifyBlock() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) DEBUG_ASSERTCRASH(this, ("null this")); DEBUG_ASSERTCRASH(m_magicCookie == SINGLEBLOCK_MAGIC_COOKIE, ("wrong cookie")); @@ -1060,7 +1060,7 @@ void MemoryPoolSingleBlock::debugVerifyBlock() */ void MemoryPoolSingleBlock::debugMarkBlockAsFree() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) ::memset32(getUserDataNoDbg(), GARBAGE_FILL_VALUE, m_logicalSize); m_debugLiteralTagString = FREE_SINGLEBLOCK_TAG_STRING; @@ -1078,7 +1078,7 @@ void MemoryPoolSingleBlock::debugMarkBlockAsFree() */ Bool MemoryPoolSingleBlock::debugCheckUnderrun() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) #ifdef MEMORYPOOL_BOUNDINGWALL Int *p = (Int*)(((char*)getUserDataNoDbg()) - WALLSIZE); @@ -1103,7 +1103,7 @@ Bool MemoryPoolSingleBlock::debugCheckUnderrun() */ Bool MemoryPoolSingleBlock::debugCheckOverrun() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) #ifdef MEMORYPOOL_BOUNDINGWALL Int *p = (Int*)(((char*)getUserDataNoDbg()) + m_logicalSize); @@ -1310,7 +1310,7 @@ void MemoryPoolBlob::freeSingleBlock(MemoryPoolSingleBlock *block) */ void MemoryPoolBlob::debugMemoryVerifyBlob() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) DEBUG_ASSERTCRASH(m_owningPool != NULL, ("bad owner")); DEBUG_ASSERTCRASH(m_usedBlocksInBlob >= 0 && m_usedBlocksInBlob <= m_totalBlocksInBlob, ("unlikely m_usedBlocksInBlob")); @@ -1351,7 +1351,7 @@ Int MemoryPoolBlob::debugBlobReportLeaks(const char* owner) */ Bool MemoryPoolBlob::debugIsBlockInBlob(void *pBlockPtr) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) MemoryPoolSingleBlock *block = MemoryPoolSingleBlock::recoverBlockFromUserData(pBlockPtr); Int rawBlockSize = MemoryPoolSingleBlock::calcRawBlockSize(m_owningPool->getAllocationSize()); @@ -1684,7 +1684,7 @@ void* MemoryPool::allocateBlockDoNotZeroImplementation(DECLARE_LITERALSTRING_ARG m_factory->adjustTotals(debugLiteralTagString, 1*getAllocationSize(), 0); #ifdef USE_FILLER_VALUE { - USE_PERF_TIMER(MemoryPoolInitFilling) + //USE_PERF_TIMER(MemoryPoolInitFilling) ::memset32(block->getUserData(), s_initFillerValue, getAllocationSize()); } #endif @@ -1903,7 +1903,7 @@ Int MemoryPool::debugPoolReportLeaks( const char* owner ) */ void MemoryPool::debugMemoryVerifyPool() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) Int used = 0; Int total = 0; @@ -1925,7 +1925,7 @@ void MemoryPool::debugMemoryVerifyPool() */ Bool MemoryPool::debugIsBlockInPool(void *pBlockPtr) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return false; @@ -1962,7 +1962,7 @@ Bool MemoryPool::debugIsBlockInPool(void *pBlockPtr) */ const char *MemoryPool::debugGetBlockTagString(void *pBlockPtr) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return FREE_SINGLEBLOCK_TAG_STRING; @@ -2129,7 +2129,7 @@ void DynamicMemoryAllocator::removeFromList(DynamicMemoryAllocator **pHead) #ifdef MEMORYPOOL_DEBUG void DynamicMemoryAllocator::debugIgnoreLeaksForThisBlock(void* pBlockPtr) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return; @@ -2178,7 +2178,7 @@ void *DynamicMemoryAllocator::allocateBytesDoNotZeroImplementation(Int numBytes result = pool->allocateBlockDoNotZeroImplementation(PASS_LITERALSTRING_ARG1); #ifdef MEMORYPOOL_DEBUG { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) waste = pool->getAllocationSize() - numBytes; MemoryPoolSingleBlock *wblock = MemoryPoolSingleBlock::recoverBlockFromUserData(result); wblock->debugSetWastedSize(waste); @@ -2216,7 +2216,7 @@ void *DynamicMemoryAllocator::allocateBytesDoNotZeroImplementation(Int numBytes #ifdef MEMORYPOOL_DEBUG { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) theTotalDMA += numBytes; if (thePeakDMA < theTotalDMA) thePeakDMA = theTotalDMA; @@ -2242,7 +2242,7 @@ void *DynamicMemoryAllocator::allocateBytesDoNotZeroImplementation(Int numBytes #ifdef MEMORYPOOL_DEBUG #ifdef USE_FILLER_VALUE { - USE_PERF_TIMER(MemoryPoolInitFilling) + //USE_PERF_TIMER(MemoryPoolInitFilling) ::memset32(result, s_initFillerValue, numBytes); } #endif @@ -2292,7 +2292,7 @@ void DynamicMemoryAllocator::freeBytes(void* pBlockPtr) const char* tagString; #endif { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) waste = 0; used = block->debugGetLogicalSize(); theTotalDMA -= used; @@ -2308,7 +2308,7 @@ void DynamicMemoryAllocator::freeBytes(void* pBlockPtr) { #ifdef MEMORYPOOL_DEBUG { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) DEBUG_ASSERTCRASH(findPoolForSize(used) == block->getOwningBlob()->getOwningPool(), ("pool mismatch")); #ifdef INTENSE_DMA_BOOKKEEPING if (doingIntenseDMA == 0) @@ -2400,7 +2400,7 @@ void DynamicMemoryAllocator::reset() */ Bool DynamicMemoryAllocator::debugIsPoolInDma(MemoryPool *pool) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (!pool) return false; @@ -2423,7 +2423,7 @@ Bool DynamicMemoryAllocator::debugIsPoolInDma(MemoryPool *pool) */ Bool DynamicMemoryAllocator::debugIsBlockInDma(void *pBlockPtr) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return false; @@ -2455,7 +2455,7 @@ Bool DynamicMemoryAllocator::debugIsBlockInDma(void *pBlockPtr) */ const char *DynamicMemoryAllocator::debugGetBlockTagString(void *pBlockPtr) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return FREE_SINGLEBLOCK_TAG_STRING; @@ -2477,7 +2477,7 @@ const char *DynamicMemoryAllocator::debugGetBlockTagString(void *pBlockPtr) */ void DynamicMemoryAllocator::debugMemoryVerifyDma() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) for (MemoryPoolSingleBlock *b = m_rawBlocks; b; b = b->getNextRawBlock()) { @@ -2509,7 +2509,7 @@ void DynamicMemoryAllocator::debugResetCheckpoints() */ Int DynamicMemoryAllocator::debugCalcRawBlockBytes(Int *numBlocks) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (numBlocks) *numBlocks = 0; @@ -2805,7 +2805,7 @@ static const char* s_specialPrefixes[MAX_SPECIAL_USED] = */ void MemoryPoolFactory::adjustTotals(const char* tagString, Int usedDelta, Int physDelta) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) m_usedBytes += usedDelta; m_physBytes += physDelta; @@ -2855,7 +2855,7 @@ void MemoryPoolFactory::debugSetInitFillerIndex(Int index) */ void MemoryPoolFactory::debugMemoryVerify() { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) Int used = 0, phys = 0; @@ -2887,7 +2887,7 @@ void MemoryPoolFactory::debugMemoryVerify() */ Bool MemoryPoolFactory::debugIsBlockInAnyPool(void *pBlock) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) #ifdef MEMORYPOOL_INTENSE_VERIFY debugMemoryVerify(); @@ -2917,7 +2917,7 @@ Bool MemoryPoolFactory::debugIsBlockInAnyPool(void *pBlock) */ const char *MemoryPoolFactory::debugGetBlockTagString(void *pBlockPtr) { - USE_PERF_TIMER(MemoryPoolDebugging) + //USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return FREE_SINGLEBLOCK_TAG_STRING; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp index 3db0c78a279..711e1f78c97 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp @@ -89,8 +89,10 @@ void GraphDraw::render() width -= start; Int height = TheDisplay->getHeight(); + (void)height; Int totalCount = m_graphEntries.size(); + (void)totalCount; DEBUG_ASSERTCRASH(totalCount < MAX_GRAPH_VALUES, ("MAX_GRAPH_VALUES must be increased, not all labels will appear (max %d, cur %d).\n",MAX_GRAPH_VALUES,totalCount)); DEBUG_ASSERTCRASH(BAR_HEIGHT * totalCount < height, ("BAR_HEIGHT must be reduced, as bars are being drawn off-screen.\n")); VecGraphEntriesIt it; From c0671bd3047e680a9be1fe6e3801d8625a23bf4f Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 2 May 2025 23:14:25 +0200 Subject: [PATCH 018/112] Make SimulateReplayList faster by calling TheParticleSystemManager->reset(); --- .../GameEngine/Source/Common/GameMain.cpp | 126 +++++++++++++++++- 1 file changed, 122 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index b03ac658683..080c6ca5a47 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -30,8 +30,13 @@ #include "Common/GameEngine.h" #include "Common/Recorder.h" +#include "Common/Radar.h" +#include "Common/PerfTimer.h" #include "GameLogic/GameLogic.h" +#include "GameClient/GameClient.h" +#include "GameClient/ParticleSys.h" +#if 1 bool SimulateReplayInProcess(AsciiString filename) { STARTUPINFO si = { sizeof(STARTUPINFO) }; @@ -51,6 +56,115 @@ bool SimulateReplayInProcess(AsciiString filename) CloseHandle(pi.hProcess); return exitcode != 0; } +#else +class ReplayProcess +{ +public: + ReplayProcess() + { + m_processHandle = INVALID_HANDLE; + } + bool StartProcess(AsciiString filename) + { + STARTUPINFO si = { sizeof(STARTUPINFO) }; + si.dwFlags = STARTF_FORCEOFFFEEDBACK; + PROCESS_INFORMATION pi = { 0 }; + AsciiString command; + command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); + //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); + //fflush(stdout); + if (!CreateProcessA(NULL, (LPSTR)command.str(), + NULL, NULL, TRUE, 0, + NULL, 0, &si, &pi)) + return false; + CloseHandle(pi.hThread); + m_processHandle = pi.hProcess; + /*WaitForSingleObject(pi.hProcess, INFINITE); + DWORD exitcode = 1; + GetExitCodeProcess(pi.hProcess, &exitcode); + CloseHandle(pi.hProcess);*/ + return exitcode != 0; + } + bool IsRunning() + { + return m_processHandle != INVALID_HANDLE; + } + bool IsDone(int *exitcode) + { + if (WaitForSingleObject(m_processHandle, 0) == WAIT_OBJECT_0) + { + GetExitCodeProcess(m_processHandle, exitcode); + return true; + } + return false; + } + +private: + HANDLE m_processHandle; +}; +#endif + +void ClientUpdate() +{ + // allow windows to perform regular windows maintenance stuff like msgs + TheGameEngine->serviceWindowsOS(); + + Drawable* draw = TheGameClient->firstDrawable(); + while (draw && 0) + { + Drawable* next = draw->getNextDrawable(); +#if 0 + if (0) + { //immobile objects need to take snapshots whenever they become fogged + //so need to refresh their status. We can't rely on external calls + //to getShroudStatus() because they are only made for visible on-screen + //objects. + Object *object=draw->getObject(); + if (object) + { +#ifdef DEBUG_FOG_MEMORY + Int *playerIndex=nonLocalPlayerIndices; + for (i=0; igetShroudedStatus(*playerIndex); +#endif + ObjectShroudStatus ss=object->getShroudedStatus(localPlayerIndex); + if (ss >= OBJECTSHROUD_FOGGED && draw->getShroudClearFrame()!=0) { + UnsignedInt limit = 2*LOGICFRAMES_PER_SECOND; + if (object->isEffectivelyDead()) { + // extend the time, so we can see the dead plane blow up & crash. + limit += 3*LOGICFRAMES_PER_SECOND; + } + if (TheGameLogic->getFrame() < limit + draw->getShroudClearFrame()) { + // It's been less than 2 seconds since we could see them clear, so keep showing them. + ss = OBJECTSHROUD_CLEAR; + } + } + draw->setFullyObscuredByShroud(ss >= OBJECTSHROUD_FOGGED); + } + } +#endif + draw->updateDrawable(); + draw = next; + } + //return; + /*TheRadar->UPDATE(); + TheAudio->UPDATE(); + TheMessageStream->propagateMessages(); + return;*/ + + TheRadar->UPDATE(); + + /// @todo Move audio init, update, etc, into GameClient update + + TheAudio->UPDATE(); + TheGameClient->UPDATE(); + TheMessageStream->propagateMessages(); + //if (TheNetwork != NULL) + { + // TheNetwork->UPDATE(); + } + //TheCDManager->UPDATE(); +} // TheSuperHackers @feature helmutbuhler 04/13/2025 // Simulate a list of replays without graphics. @@ -64,7 +178,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char for (size_t i = 0; i < filenames.size(); i++) { AsciiString filename = filenames[i]; - if (filenames.size() == 1) + if (1 || filenames.size() == 1) { printf("Simulating Replay \"%s\"\n", filename.str()); fflush(stdout); @@ -74,12 +188,16 @@ int SimulateReplayList(const std::vector &filenames, int argc, char UnsignedInt totalTime = TheRecorder->getFrameDuration() / fps; while (TheRecorder->isPlaybackInProgress()) { + //ClientUpdate(); + TheParticleSystemManager->reset(); + if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*fps) == 0) { + // Print progress report UnsignedInt gameTime = TheGameLogic->getFrame() / fps; UnsignedInt realTime = (GetTickCount()-startTime) / 1000; - printf("Frame %02d:%02d/%02d:%02d RT: %02d:%02d\n", - gameTime/60, gameTime%60, totalTime/60, totalTime%60, realTime/60, realTime%60); + printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", + realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); fflush(stdout); } TheGameLogic->UPDATE(); @@ -90,7 +208,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char } } UnsignedInt realTime = (GetTickCount()-startTime) / 1000; - printf("GT: %02d:%02d RT: %02d:%02d\n", totalTime/60, totalTime%60, realTime/60, realTime%60); + printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d\n", realTime/60, realTime%60, totalTime/60, totalTime%60); fflush(stdout); } else From dffca9e8d97066e825a387038d8eb4e5d107c284 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 3 May 2025 11:54:40 +0200 Subject: [PATCH 019/112] Add GameClient::updateHeadless() --- .../Include/GameClient/GameClient.h | 1 + .../GameEngine/Source/Common/GameMain.cpp | 65 +------------------ .../Source/GameClient/GameClient.cpp | 12 ++++ 3 files changed, 15 insertions(+), 63 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h index 72d73179a88..5f551d8e759 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h @@ -92,6 +92,7 @@ class GameClient : public SubsystemInterface, // subsystem methods virtual void init( void ); ///< Initialize resources virtual void update( void ); ///< Updates the GUI, display, audio, etc + void updateHeadless(); ///< Updates the GUI, display, audio, etc virtual void reset( void ); ///< reset system virtual void setFrame( UnsignedInt frame ) { m_frame = frame; } ///< Set the GameClient's internal frame number diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 080c6ca5a47..988f336c121 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -104,67 +104,6 @@ class ReplayProcess }; #endif -void ClientUpdate() -{ - // allow windows to perform regular windows maintenance stuff like msgs - TheGameEngine->serviceWindowsOS(); - - Drawable* draw = TheGameClient->firstDrawable(); - while (draw && 0) - { - Drawable* next = draw->getNextDrawable(); -#if 0 - if (0) - { //immobile objects need to take snapshots whenever they become fogged - //so need to refresh their status. We can't rely on external calls - //to getShroudStatus() because they are only made for visible on-screen - //objects. - Object *object=draw->getObject(); - if (object) - { -#ifdef DEBUG_FOG_MEMORY - Int *playerIndex=nonLocalPlayerIndices; - for (i=0; igetShroudedStatus(*playerIndex); -#endif - ObjectShroudStatus ss=object->getShroudedStatus(localPlayerIndex); - if (ss >= OBJECTSHROUD_FOGGED && draw->getShroudClearFrame()!=0) { - UnsignedInt limit = 2*LOGICFRAMES_PER_SECOND; - if (object->isEffectivelyDead()) { - // extend the time, so we can see the dead plane blow up & crash. - limit += 3*LOGICFRAMES_PER_SECOND; - } - if (TheGameLogic->getFrame() < limit + draw->getShroudClearFrame()) { - // It's been less than 2 seconds since we could see them clear, so keep showing them. - ss = OBJECTSHROUD_CLEAR; - } - } - draw->setFullyObscuredByShroud(ss >= OBJECTSHROUD_FOGGED); - } - } -#endif - draw->updateDrawable(); - draw = next; - } - //return; - /*TheRadar->UPDATE(); - TheAudio->UPDATE(); - TheMessageStream->propagateMessages(); - return;*/ - - TheRadar->UPDATE(); - - /// @todo Move audio init, update, etc, into GameClient update - - TheAudio->UPDATE(); - TheGameClient->UPDATE(); - TheMessageStream->propagateMessages(); - //if (TheNetwork != NULL) - { - // TheNetwork->UPDATE(); - } - //TheCDManager->UPDATE(); -} // TheSuperHackers @feature helmutbuhler 04/13/2025 // Simulate a list of replays without graphics. @@ -188,8 +127,8 @@ int SimulateReplayList(const std::vector &filenames, int argc, char UnsignedInt totalTime = TheRecorder->getFrameDuration() / fps; while (TheRecorder->isPlaybackInProgress()) { - //ClientUpdate(); - TheParticleSystemManager->reset(); + TheGameClient->updateHeadless(); + //TheParticleSystemManager->reset(); if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*fps) == 0) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index 178189044c3..dc4d462a274 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -776,6 +776,18 @@ void GameClient::update( void ) } } // end update +void GameClient::updateHeadless() +{ + // TheSuperHackers @info helmutbuhler 03/05/2025 + // When we play a replay back in headless mode, we want to skip the update of GameClient + // because it's not necessary for CRC checking. + // But we do reset the particles. The problem is that particles can be generated during + // GameLogic and are only cleaned up during rendering. If we don't clean this up here, + // the particles accumulate and slow things down a lot and can even cause a crash on + // longer replays. + TheParticleSystemManager->reset(); +} + /** ----------------------------------------------------------------------------------------------- * Call the given callback function for each object contained within the given region. */ From 6b3348faca2f7a056a26fa244acaa549851fe2a5 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 3 May 2025 11:57:09 +0200 Subject: [PATCH 020/112] Use pipes to retrieve worker process console output --- .../GameEngine/Source/Common/GameMain.cpp | 107 ++++++++++++++---- 1 file changed, 82 insertions(+), 25 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 988f336c121..ffcb8720818 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -30,13 +30,10 @@ #include "Common/GameEngine.h" #include "Common/Recorder.h" -#include "Common/Radar.h" -#include "Common/PerfTimer.h" #include "GameLogic/GameLogic.h" #include "GameClient/GameClient.h" -#include "GameClient/ParticleSys.h" -#if 1 +#if 0 bool SimulateReplayInProcess(AsciiString filename) { STARTUPINFO si = { sizeof(STARTUPINFO) }; @@ -62,45 +59,95 @@ class ReplayProcess public: ReplayProcess() { - m_processHandle = INVALID_HANDLE; + m_processHandle = INVALID_HANDLE_VALUE; + m_readHandle = INVALID_HANDLE_VALUE; } bool StartProcess(AsciiString filename) { + PROCESS_INFORMATION pi = { 0 }; + + SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; + saAttr.bInheritHandle = TRUE; + + HANDLE writeHandle = NULL; + CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0); + SetHandleInformation(m_readHandle, HANDLE_FLAG_INHERIT, 0); + STARTUPINFO si = { sizeof(STARTUPINFO) }; si.dwFlags = STARTF_FORCEOFFFEEDBACK; - PROCESS_INFORMATION pi = { 0 }; + si.hStdError = writeHandle; + si.hStdOutput = writeHandle; + si.dwFlags |= STARTF_USESTDHANDLES; + AsciiString command; command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); - //fflush(stdout); + fflush(stdout); if (!CreateProcessA(NULL, (LPSTR)command.str(), - NULL, NULL, TRUE, 0, - NULL, 0, &si, &pi)) - return false; + NULL, NULL, TRUE, 0, + NULL, 0, &si, &pi)) + return false; CloseHandle(pi.hThread); + CloseHandle(writeHandle); m_processHandle = pi.hProcess; - /*WaitForSingleObject(pi.hProcess, INFINITE); - DWORD exitcode = 1; - GetExitCodeProcess(pi.hProcess, &exitcode); - CloseHandle(pi.hProcess);*/ - return exitcode != 0; + m_stdOutput = ""; + return true; } bool IsRunning() { - return m_processHandle != INVALID_HANDLE; + return m_processHandle != INVALID_HANDLE_VALUE; } - bool IsDone(int *exitcode) + bool Update(DWORD *exitcode, AsciiString *stdOutput) { - if (WaitForSingleObject(m_processHandle, 0) == WAIT_OBJECT_0) + DEBUG_ASSERTCRASH(IsRunning(), ("process must be running")); + + while (true) { - GetExitCodeProcess(m_processHandle, exitcode); - return true; + // Call PeekNamedPipe to make sure ReadFile won't block + DWORD bytesAvailable = 0; + BOOL success = PeekNamedPipe(m_readHandle, NULL, 0, NULL, &bytesAvailable, NULL); + if (!success) + break; + if (bytesAvailable == 0) + { + // Child process is still running and we have all output so far + return false; + } + + DWORD readBytes = 0; + char buffer[1024]; + success = ReadFile(m_readHandle, buffer, 1024-1, &readBytes, NULL); + if (!success) + break; + DEBUG_ASSERTCRASH(readBytes != 0, ("expected readBytes to be non null")); + + // Remove \r, otherwise each new line it doubled when we output it again + for (int i = 0; i < readBytes; i++) + if (buffer[i] == '\r') + buffer[i] = ' '; + buffer[readBytes] = 0; + m_stdOutput.concat(buffer); } - return false; + + // Pipe broke, that means the process already exited. But we call this just to make sure + WaitForSingleObject(m_processHandle, INFINITE); + GetExitCodeProcess(m_processHandle, exitcode); + CloseHandle(m_processHandle); + m_processHandle = INVALID_HANDLE_VALUE; + + CloseHandle(m_readHandle); + m_readHandle = INVALID_HANDLE_VALUE; + + *stdOutput = m_stdOutput; + m_stdOutput = ""; + + return true; } private: HANDLE m_processHandle; + HANDLE m_readHandle; + AsciiString m_stdOutput; }; #endif @@ -117,7 +164,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char for (size_t i = 0; i < filenames.size(); i++) { AsciiString filename = filenames[i]; - if (1 || filenames.size() == 1) + if (filenames.size() == 1) { printf("Simulating Replay \"%s\"\n", filename.str()); fflush(stdout); @@ -146,8 +193,10 @@ int SimulateReplayList(const std::vector &filenames, int argc, char break; } } + UnsignedInt gameTime = TheGameLogic->getFrame() / fps; UnsignedInt realTime = (GetTickCount()-startTime) / 1000; - printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d\n", realTime/60, realTime%60, totalTime/60, totalTime%60); + printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", + realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); fflush(stdout); } else @@ -160,8 +209,16 @@ int SimulateReplayList(const std::vector &filenames, int argc, char { printf("%d/%d ", i+1, filenames.size()); fflush(stdout); - bool error = SimulateReplayInProcess(filename); - numErrors += error ? 1 : 0; + //bool error = SimulateReplayInProcess(filename); + ReplayProcess p; + p.StartProcess(filename); + DWORD exitcode; + AsciiString stdOutput; + while (!p.Update(&exitcode, &stdOutput)) + {} + printf("%s", stdOutput.str()); + fflush(stdout); + numErrors += exitcode == 0 ? 0 : 1; } /*if (i == TheGlobalData->m_simulateReplayList.size()-1) { From a2e7c5a30494937bdeb32aa295b3a73193b0f2e4 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 3 May 2025 19:13:41 +0200 Subject: [PATCH 021/112] Add SimulateReplayListMultiProcess. It seems to work now! --- .../GameEngine/Source/Common/GameMain.cpp | 128 ++++++++++++++++-- 1 file changed, 116 insertions(+), 12 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index ffcb8720818..4958786d588 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -61,9 +61,14 @@ class ReplayProcess { m_processHandle = INVALID_HANDLE_VALUE; m_readHandle = INVALID_HANDLE_VALUE; + m_exitcode = 0; + m_isDone = false; } bool StartProcess(AsciiString filename) { + m_stdOutput = ""; + m_isDone = false; + PROCESS_INFORMATION pi = { 0 }; SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; @@ -86,20 +91,24 @@ class ReplayProcess if (!CreateProcessA(NULL, (LPSTR)command.str(), NULL, NULL, TRUE, 0, NULL, 0, &si, &pi)) - return false; + { + printf("Couldn't start exe: %s\n", command.str()); + fflush(stdout); + return false; + } CloseHandle(pi.hThread); CloseHandle(writeHandle); m_processHandle = pi.hProcess; - m_stdOutput = ""; return true; } - bool IsRunning() + bool IsRunning() const { return m_processHandle != INVALID_HANDLE_VALUE; } - bool Update(DWORD *exitcode, AsciiString *stdOutput) + void Update() { - DEBUG_ASSERTCRASH(IsRunning(), ("process must be running")); + if (!IsRunning()) + return; while (true) { @@ -111,7 +120,7 @@ class ReplayProcess if (bytesAvailable == 0) { // Child process is still running and we have all output so far - return false; + return; } DWORD readBytes = 0; @@ -131,32 +140,57 @@ class ReplayProcess // Pipe broke, that means the process already exited. But we call this just to make sure WaitForSingleObject(m_processHandle, INFINITE); - GetExitCodeProcess(m_processHandle, exitcode); + GetExitCodeProcess(m_processHandle, &m_exitcode); CloseHandle(m_processHandle); m_processHandle = INVALID_HANDLE_VALUE; CloseHandle(m_readHandle); m_readHandle = INVALID_HANDLE_VALUE; + m_isDone = true; + } + bool IsDone(DWORD *exitcode, AsciiString *stdOutput) + { + *exitcode = m_exitcode; *stdOutput = m_stdOutput; - m_stdOutput = ""; - - return true; + return m_isDone; } + void Close() + { + if (m_processHandle != INVALID_HANDLE_VALUE) + { + TerminateProcess(m_processHandle, 1); + CloseHandle(m_processHandle); + m_processHandle = INVALID_HANDLE_VALUE; + } + CloseHandle(m_readHandle); + m_readHandle = INVALID_HANDLE_VALUE; + + m_stdOutput = ""; + m_isDone = false; + } private: HANDLE m_processHandle; HANDLE m_readHandle; AsciiString m_stdOutput; + DWORD m_exitcode; + bool m_isDone; }; #endif +int SimulateReplayListMultiProcess(const std::vector &filenames); // TheSuperHackers @feature helmutbuhler 04/13/2025 // Simulate a list of replays without graphics. // Returns exitcode 1 if mismatch or other error occured int SimulateReplayList(const std::vector &filenames, int argc, char *argv[]) { + if (filenames.size() != 1) + { + return SimulateReplayListMultiProcess(filenames); + } + // Note that we use printf here because this is run from cmd. Int fps = TheGlobalData->m_framesPerSecondLimit; int numErrors = 0; @@ -207,7 +241,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char } else { - printf("%d/%d ", i+1, filenames.size()); + /*printf("%d/%d ", i+1, filenames.size()); fflush(stdout); //bool error = SimulateReplayInProcess(filename); ReplayProcess p; @@ -218,7 +252,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char {} printf("%s", stdOutput.str()); fflush(stdout); - numErrors += exitcode == 0 ? 0 : 1; + numErrors += exitcode == 0 ? 0 : 1;*/ } /*if (i == TheGlobalData->m_simulateReplayList.size()-1) { @@ -254,6 +288,76 @@ int SimulateReplayList(const std::vector &filenames, int argc, char return numErrors != 0 ? 1 : 0; } +int SimulateReplayListMultiProcess(const std::vector &filenames) +{ + DWORD totalStartTime = GetTickCount(); + + std::vector processes; + const int maxProcesses = 4; + int filenamePositionStarted = 0; + int filenamePositionDone = 0; + int numErrors = 0; + + while (true) + { + int i; + for (i = 0; i < processes.size(); i++) + processes[i].Update(); + + // Get result of finished processes and print output in order + while (processes.size() != 0) + { + DWORD exitcode; + AsciiString stdOutput; + if (!processes[0].IsDone(&exitcode, &stdOutput)) + break; + printf("%d/%d %s", filenamePositionDone+1, filenames.size(), stdOutput.str()); + fflush(stdout); + numErrors += exitcode == 0 ? 0 : 1; + processes.erase(processes.begin()); + filenamePositionDone++; + } + + // Count how many processes are running + int numProcessesRunning = 0; + for (i = 0; i < processes.size(); i++) + { + if (processes[i].IsRunning()) + numProcessesRunning++; + } + + // Add new processes when we are below the limit and there are replays left + while (numProcessesRunning < maxProcesses && filenamePositionStarted < filenames.size()) + { + ReplayProcess p; + p.StartProcess(filenames[filenamePositionStarted]); + processes.push_back(p); + filenamePositionStarted++; + numProcessesRunning++; + } + + if (processes.empty()) + break; + + // Don't waste CPU here, our workers need every bit of CPU time they can get + Sleep(100); + } + + DEBUG_ASSERTCRASH(filenamePositionStarted == filenames.size(), ("inconsistent file position 1")); + DEBUG_ASSERTCRASH(filenamePositionDone == filenames.size(), ("inconsistent file position 2")); + + if (numErrors) + printf("Errors occured: %d\n", numErrors); + else + printf("Successfully simulated all replays\n"); + + UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; + printf("Total Wall Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); + fflush(stdout); + + return numErrors != 0 ? 1 : 0; +} + /** * This is the entry point for the game system. */ From efd7c1450b5f8bc963da8f3c8b986cf8d9c9d458 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 4 May 2025 16:21:28 +0200 Subject: [PATCH 022/112] Use TerminateProcess to work around crash --- .../GameEngine/Source/Common/GameMain.cpp | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 4958786d588..7d91103992f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -155,7 +155,7 @@ class ReplayProcess *stdOutput = m_stdOutput; return m_isDone; } - void Close() + void Cancel() { if (m_processHandle != INVALID_HANDLE_VALUE) { @@ -192,7 +192,6 @@ int SimulateReplayList(const std::vector &filenames, int argc, char } // Note that we use printf here because this is run from cmd. - Int fps = TheGlobalData->m_framesPerSecondLimit; int numErrors = 0; DWORD totalStartTime = GetTickCount(); for (size_t i = 0; i < filenames.size(); i++) @@ -205,16 +204,16 @@ int SimulateReplayList(const std::vector &filenames, int argc, char DWORD startTime = GetTickCount(); if (TheRecorder->simulateReplay(filename)) { - UnsignedInt totalTime = TheRecorder->getFrameDuration() / fps; + UnsignedInt totalTime = TheRecorder->getFrameDuration() / LOGICFRAMES_PER_SECOND; while (TheRecorder->isPlaybackInProgress()) { TheGameClient->updateHeadless(); //TheParticleSystemManager->reset(); - if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*fps) == 0) + if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*LOGICFRAMES_PER_SECOND) == 0) { // Print progress report - UnsignedInt gameTime = TheGameLogic->getFrame() / fps; + UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; UnsignedInt realTime = (GetTickCount()-startTime) / 1000; printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); @@ -227,7 +226,7 @@ int SimulateReplayList(const std::vector &filenames, int argc, char break; } } - UnsignedInt gameTime = TheGameLogic->getFrame() / fps; + UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; UnsignedInt realTime = (GetTickCount()-startTime) / 1000; printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); @@ -278,6 +277,11 @@ int SimulateReplayList(const std::vector &filenames, int argc, char fflush(stdout); } + // TheSuperHackers @todo helmutbuhler 04/05/2025 + // Some replays cause a crash inside TheGameLogic->clearGameData(). + // It likely has to do with Contains. We really gotta fix this, but for now we just terminate. + TerminateProcess(GetCurrentProcess(), numErrors != 0 ? 1 : 0); + // TheSuperHackers @todo helmutbuhler 04/13/2025 // There is a bug somewhere in the destructor of TheGameEngine which doesn't properly // clean up the players and causes a crash unless this is called. @@ -293,7 +297,7 @@ int SimulateReplayListMultiProcess(const std::vector &filenames) DWORD totalStartTime = GetTickCount(); std::vector processes; - const int maxProcesses = 4; + const int maxProcesses = 20; int filenamePositionStarted = 0; int filenamePositionDone = 0; int numErrors = 0; @@ -312,6 +316,8 @@ int SimulateReplayListMultiProcess(const std::vector &filenames) if (!processes[0].IsDone(&exitcode, &stdOutput)) break; printf("%d/%d %s", filenamePositionDone+1, filenames.size(), stdOutput.str()); + if (exitcode != 0) + printf("Error!\n"); fflush(stdout); numErrors += exitcode == 0 ? 0 : 1; processes.erase(processes.begin()); From 467394459934244571da2d720d4cb682ddf7928b Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 4 May 2025 16:22:39 +0200 Subject: [PATCH 023/112] Expand columns in WriteOutReplayList and fix ReadLineFromFile --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 110 ++++++++++++++---- 1 file changed, 87 insertions(+), 23 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 1355a27b6a4..3991ce4de87 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -122,13 +122,15 @@ void WriteOutReplayList() asciisearch = "*"; asciisearch.concat(TheRecorder->getReplayExtention()); FilenameList replayFilenamesSet; - TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenamesSet, TRUE); + TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenamesSet, FALSE); std::vector replayFilenames; for (FilenameListIter it = replayFilenamesSet.begin(); it != replayFilenamesSet.end(); ++it) { replayFilenames.push_back(*it); } + std::set foundSeeds; + // Print out a line per filename. i = -1 is csv header. for (int i = -1; i < (int)replayFilenames.size(); i++) { @@ -143,7 +145,40 @@ void WriteOutReplayList() if (!success) continue; } - bool check = md && !header.desyncGame && header.endTime != 0; + int numHumans = 0, numAIs = 0; + for (int slot = 0; slot < MAX_SLOTS; slot++) + { + SlotState state = info.getSlot(slot)->getState(); + numHumans += state == SLOT_PLAYER ? 1 : 0; + numAIs += state >= SLOT_EASY_AI && state <= SLOT_BRUTAL_AI ? 1 : 0; + } + + bool compatibleVersion = + header.versionString == UnicodeString(L"Version 1.4") || + header.versionString == UnicodeString(L"Version 1.04") || + header.versionString == UnicodeString(L"Version 1.05") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.04") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.05") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.4") || + header.versionString == UnicodeString(L"Versi\x00F3n 1.04") || + header.versionString == UnicodeString(L"Versi\x00F3n 1.05"); + + // Some versions, e.g. "Zero Hour 1.04 The Ultimate Collection" have a different ini crc and are + // actually incompatible. Mark them as incompatible + compatibleVersion = compatibleVersion && header.iniCRC == 0xfeaae3f3; + + // Check whether random seed appears multiple times. This can be used to check only one replay + // per game in case multiple replays by different players of the same game are in the list. + int seed = info.getSeed(); + bool uniqueSeed = foundSeeds.find(seed) == foundSeeds.end(); + if (uniqueSeed) + foundSeeds.insert(seed); + + // When a csv file is loaded with -simReplayList, check indicates whether the replay should be simulated. + // If you want to check replays with certain properties, you can change this expression + // or change the csv file manually. + bool check = md && !header.desyncGame && header.endTime != 0 && + compatibleVersion && numHumans > 1 && numAIs > 0 && uniqueSeed; fprintf(fp, "%s", i == -1 ? "check" : check ? "1" : "0"); if (i == -1) @@ -154,36 +189,65 @@ void WriteOutReplayList() fprintf(fp, ",%s", i == -1 ? "map_exists" : md ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); - fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); - - /*AsciiString extra; - if (!md) - extra.concat(" no map"); - //extra.concat(header.localPlayerIndex >= 0 ? " MP" : " SP"); - if (header.quitEarly) - extra.concat(" quitearly"); - if (header.desyncGame) - extra.concat(" mismatch"); - - if (extra.getLength() != 0) - fprintf(fp, "%s #%s\n", filename.str(), extra.str()); + //fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); + + UnsignedInt gameTime = header.frameDuration / LOGICFRAMES_PER_SECOND; + fprintf(fp, i == -1 ? ",time" : ",%02d:%02d", gameTime/60, gameTime%60); + + fprintf(fp, i == -1 ? ",numHumans" : ",%d", numHumans); + fprintf(fp, i == -1 ? ",numAIs" : ",%d", numAIs); + + AsciiString tmp; + tmp.translate(header.versionString); + if (i == -1) + fprintf(fp, ",version"); else - fprintf(fp, "%s\n", filename.str());*/ + fprintf(fp, ",\"%s\"", tmp.str()); + //fprintf(fp, i == -1 ? ",exeCRC" : ",0x%08x", header.exeCRC); + //fprintf(fp, i == -1 ? ",iniCRC" : ",0x%08x", header.iniCRC); + + fprintf(fp, ",%s", i == -1 ? "compatibleVersion" : compatibleVersion ? "1" : "0"); + + //fprintf(fp, i == -1 ? ",crcInterval" : ",%d", info.getCRCInterval()); + //fprintf(fp, i == -1 ? ",seed" : ",0x%08x", seed); + fprintf(fp, "\n"); + + +#if 0 + if (i != -1 && check) + { + AsciiString sourceFilename = replayFilenames[i]; + + AsciiString targetFilename; + targetFilename = TheRecorder->getReplayDir(); + targetFilename.concat("filter/"); + targetFilename.concat(filename); + + CopyFile(sourceFilename.str(), targetFilename.str(), FALSE); + } +#endif } fclose(fp); } bool ReadLineFromFile(FILE *fp, AsciiString *str) { - char buffer[124]; - if (fgets(buffer, 124, fp) == NULL) + const int bufferSize = 128; + char buffer[bufferSize]; + str->clear(); + while (true) { - str->clear(); - return false; + if (fgets(buffer, bufferSize, fp) == NULL) + { + str->clear(); + return false; + } + buffer[bufferSize-1] = 0; // Should be already nul-terminated, just to be sure + str->concat(buffer); + if (strlen(buffer) != bufferSize-1 || buffer[bufferSize-2] == '\n') + break; } - buffer[124-1] = 0; - str->set(buffer); return true; } @@ -283,7 +347,7 @@ void PopulateReplayFileListbox(GameWindow *listbox) FilenameList replayFilenames; FilenameListIter it; - TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenames, TRUE); + TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenames, FALSE); TheMapCache->updateCache(); From 9b7eadccd867042d901410f51198d8fc22496d7e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 4 May 2025 18:20:00 +0200 Subject: [PATCH 024/112] Add Job stuff to ReplayProcess --- .../GameEngine/Source/Common/GameMain.cpp | 105 ++++++++++++++++-- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 7d91103992f..110782b345b 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -54,16 +54,54 @@ bool SimulateReplayInProcess(AsciiString filename) return exitcode != 0; } #else + +// We need Job-related functions, but these arn't defined in the Windows-headers that VC6 uses +// So we define them here and load them dynamically +#if defined(_MSC_VER) && (_MSC_VER <= 1200) +typedef struct _IO_COUNTERS +{ + ULONGLONG ReadOperationCount; + ULONGLONG WriteOperationCount; + ULONGLONG OtherOperationCount; + ULONGLONG ReadTransferCount; + ULONGLONG WriteTransferCount; + ULONGLONG OtherTransferCount; +} IO_COUNTERS; +typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION +{ + JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + IO_COUNTERS IoInfo; + SIZE_T ProcessMemoryLimit; + SIZE_T JobMemoryLimit; + SIZE_T PeakProcessMemoryUsed; + SIZE_T PeakJobMemoryUsed; +} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION; + +#define JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 0x00002000 +const int JobObjectExtendedLimitInformation = 9; + +typedef HANDLE (WINAPI *PFN_CreateJobObjectW)(LPSECURITY_ATTRIBUTES, LPCWSTR); +typedef BOOL (WINAPI *PFN_SetInformationJobObject)(HANDLE, JOBOBJECTINFOCLASS, LPVOID, DWORD); +typedef BOOL (WINAPI *PFN_AssignProcessToJobObject)(HANDLE, HANDLE); + +static PFN_CreateJobObjectW CreateJobObjectW = (PFN_CreateJobObjectW)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateJobObjectW"); +static PFN_SetInformationJobObject SetInformationJobObject = (PFN_SetInformationJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetInformationJobObject"); +static PFN_AssignProcessToJobObject AssignProcessToJobObject = (PFN_AssignProcessToJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "AssignProcessToJobObject"); + +#endif + class ReplayProcess { public: ReplayProcess() { - m_processHandle = INVALID_HANDLE_VALUE; - m_readHandle = INVALID_HANDLE_VALUE; + m_processHandle = NULL; + m_readHandle = NULL; + m_jobHandle = NULL; m_exitcode = 0; m_isDone = false; } + bool StartProcess(AsciiString filename) { m_stdOutput = ""; @@ -72,10 +110,11 @@ class ReplayProcess PROCESS_INFORMATION pi = { 0 }; SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; - saAttr.bInheritHandle = TRUE; - + saAttr.bInheritHandle = TRUE; + HANDLE writeHandle = NULL; CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0); + SetHandleInformation(m_readHandle, HANDLE_FLAG_INHERIT, 0); STARTUPINFO si = { sizeof(STARTUPINFO) }; @@ -88,23 +127,43 @@ class ReplayProcess command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); fflush(stdout); + if (!CreateProcessA(NULL, (LPSTR)command.str(), NULL, NULL, TRUE, 0, NULL, 0, &si, &pi)) { printf("Couldn't start exe: %s\n", command.str()); fflush(stdout); + CloseHandle(writeHandle); + CloseHandle(m_readHandle); + m_readHandle = NULL; return false; } + CloseHandle(pi.hThread); CloseHandle(writeHandle); m_processHandle = pi.hProcess; + + // We want to make sure that when our process is killed, our workers automatically terminate as well. + // In Windows, the way to do this is to attach the worker to a job we own. + m_jobHandle = CreateJobObjectW(NULL, NULL); + if (m_jobHandle != NULL) + { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = { 0 }; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject(m_jobHandle, (JOBOBJECTINFOCLASS)JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)); + + AssignProcessToJobObject(m_jobHandle, m_processHandle); + } + return true; } + bool IsRunning() const { - return m_processHandle != INVALID_HANDLE_VALUE; + return m_processHandle != NULL; } + void Update() { if (!IsRunning()) @@ -130,7 +189,7 @@ class ReplayProcess break; DEBUG_ASSERTCRASH(readBytes != 0, ("expected readBytes to be non null")); - // Remove \r, otherwise each new line it doubled when we output it again + // Remove \r, otherwise each new line is doubled when we output it again for (int i = 0; i < readBytes; i++) if (buffer[i] == '\r') buffer[i] = ' '; @@ -142,41 +201,63 @@ class ReplayProcess WaitForSingleObject(m_processHandle, INFINITE); GetExitCodeProcess(m_processHandle, &m_exitcode); CloseHandle(m_processHandle); - m_processHandle = INVALID_HANDLE_VALUE; + m_processHandle = NULL; CloseHandle(m_readHandle); - m_readHandle = INVALID_HANDLE_VALUE; + m_readHandle = NULL; + + if (m_jobHandle != NULL) + { + CloseHandle(m_jobHandle); // This kills the process if still running + m_jobHandle = NULL; + } m_isDone = true; } + bool IsDone(DWORD *exitcode, AsciiString *stdOutput) { *exitcode = m_exitcode; *stdOutput = m_stdOutput; return m_isDone; } + void Cancel() { - if (m_processHandle != INVALID_HANDLE_VALUE) + if (m_processHandle != NULL) { TerminateProcess(m_processHandle, 1); CloseHandle(m_processHandle); - m_processHandle = INVALID_HANDLE_VALUE; + m_processHandle = NULL; } - CloseHandle(m_readHandle); - m_readHandle = INVALID_HANDLE_VALUE; + if (m_readHandle != NULL) + { + CloseHandle(m_readHandle); + m_readHandle = NULL; + } + + if (m_jobHandle != NULL) + { + CloseHandle(m_jobHandle); + m_jobHandle = NULL; + } m_stdOutput = ""; m_isDone = false; } + private: HANDLE m_processHandle; HANDLE m_readHandle; + HANDLE m_jobHandle; AsciiString m_stdOutput; DWORD m_exitcode; bool m_isDone; + }; + + #endif int SimulateReplayListMultiProcess(const std::vector &filenames); From a82de6171124dabe1ef2230bcf3e06f7abe061de Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Mon, 5 May 2025 00:05:52 +0200 Subject: [PATCH 025/112] Fix Jobs for VC6 --- .../GameEngine/Source/Common/GameMain.cpp | 80 +++++++++++-------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 110782b345b..83fadae356e 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -55,27 +55,39 @@ bool SimulateReplayInProcess(AsciiString filename) } #else -// We need Job-related functions, but these arn't defined in the Windows-headers that VC6 uses -// So we define them here and load them dynamically -#if defined(_MSC_VER) && (_MSC_VER <= 1200) -typedef struct _IO_COUNTERS +// We need Job-related functions, but these aren't defined in the Windows-headers that VC6 uses. +// So we define them here and load them dynamically. +#if defined(_MSC_VER) && _MSC_VER < 1300 +struct JOBOBJECT_BASIC_LIMIT_INFORMATION2 { - ULONGLONG ReadOperationCount; - ULONGLONG WriteOperationCount; - ULONGLONG OtherOperationCount; + LARGE_INTEGER PerProcessUserTimeLimit; + LARGE_INTEGER PerJobUserTimeLimit; + DWORD LimitFlags; + SIZE_T MinimumWorkingSetSize; + SIZE_T MaximumWorkingSetSize; + DWORD ActiveProcessLimit; + ULONG_PTR Affinity; + DWORD PriorityClass; + DWORD SchedulingClass; +}; +struct IO_COUNTERS +{ + ULONGLONG ReadOperationCount; + ULONGLONG WriteOperationCount; + ULONGLONG OtherOperationCount; ULONGLONG ReadTransferCount; ULONGLONG WriteTransferCount; ULONGLONG OtherTransferCount; -} IO_COUNTERS; -typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION +}; +struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { - JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + JOBOBJECT_BASIC_LIMIT_INFORMATION2 BasicLimitInformation; IO_COUNTERS IoInfo; SIZE_T ProcessMemoryLimit; SIZE_T JobMemoryLimit; SIZE_T PeakProcessMemoryUsed; SIZE_T PeakJobMemoryUsed; -} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION; +}; #define JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 0x00002000 const int JobObjectExtendedLimitInformation = 9; @@ -87,7 +99,6 @@ typedef BOOL (WINAPI *PFN_AssignProcessToJobObject)(HANDLE, HANDLE); static PFN_CreateJobObjectW CreateJobObjectW = (PFN_CreateJobObjectW)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateJobObjectW"); static PFN_SetInformationJobObject SetInformationJobObject = (PFN_SetInformationJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetInformationJobObject"); static PFN_AssignProcessToJobObject AssignProcessToJobObject = (PFN_AssignProcessToJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "AssignProcessToJobObject"); - #endif class ReplayProcess @@ -111,29 +122,30 @@ class ReplayProcess SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; saAttr.bInheritHandle = TRUE; - HANDLE writeHandle = NULL; CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0); - SetHandleInformation(m_readHandle, HANDLE_FLAG_INHERIT, 0); - STARTUPINFO si = { sizeof(STARTUPINFO) }; - si.dwFlags = STARTF_FORCEOFFFEEDBACK; + STARTUPINFOW si = { sizeof(STARTUPINFOW) }; + si.dwFlags = STARTF_FORCEOFFFEEDBACK; // Prevent cursor wait animation si.hStdError = writeHandle; si.hStdOutput = writeHandle; si.dwFlags |= STARTF_USESTDHANDLES; - AsciiString command; - command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); - //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); - fflush(stdout); + WideChar exePath[1024]; + GetModuleFileNameW(NULL, exePath, 1024); + UnicodeString filenameWide; + filenameWide.translate(filename); + + UnicodeString command; + command.format(L"\"%s\" -win -xres 800 -yres 600 -simReplay \"%s\"", exePath, filenameWide.str()); + //wprintf(L"Starting Exe for Replay \"%s\": %s\n", filenameWide.str(), command.str()); + //fflush(stdout); - if (!CreateProcessA(NULL, (LPSTR)command.str(), - NULL, NULL, TRUE, 0, + if (!CreateProcessW(NULL, (LPWSTR)command.str(), + NULL, NULL, /*bInheritHandles=*/TRUE, 0, NULL, 0, &si, &pi)) { - printf("Couldn't start exe: %s\n", command.str()); - fflush(stdout); CloseHandle(writeHandle); CloseHandle(m_readHandle); m_readHandle = NULL; @@ -152,7 +164,6 @@ class ReplayProcess JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = { 0 }; jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; SetInformationJobObject(m_jobHandle, (JOBOBJECTINFOCLASS)JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)); - AssignProcessToJobObject(m_jobHandle, m_processHandle); } @@ -163,6 +174,13 @@ class ReplayProcess { return m_processHandle != NULL; } + + bool IsDone(DWORD *exitcode, AsciiString *stdOutput) + { + *exitcode = m_exitcode; + *stdOutput = m_stdOutput; + return m_isDone; + } void Update() { @@ -215,13 +233,6 @@ class ReplayProcess m_isDone = true; } - bool IsDone(DWORD *exitcode, AsciiString *stdOutput) - { - *exitcode = m_exitcode; - *stdOutput = m_stdOutput; - return m_isDone; - } - void Cancel() { if (m_processHandle != NULL) @@ -254,7 +265,6 @@ class ReplayProcess AsciiString m_stdOutput; DWORD m_exitcode; bool m_isDone; - }; @@ -265,7 +275,7 @@ int SimulateReplayListMultiProcess(const std::vector &filenames); // TheSuperHackers @feature helmutbuhler 04/13/2025 // Simulate a list of replays without graphics. // Returns exitcode 1 if mismatch or other error occured -int SimulateReplayList(const std::vector &filenames, int argc, char *argv[]) +int SimulateReplayList(const std::vector &filenames) { if (filenames.size() != 1) { @@ -457,7 +467,7 @@ Int GameMain( int argc, char *argv[] ) if (!TheGlobalData->m_simulateReplayList.empty()) { - exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList, argc, argv); + exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList); } else { From 4835c7f4d90502d7d0da34e1c29979e958c9b2a1 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 8 May 2025 22:23:42 +0200 Subject: [PATCH 026/112] Revert "Make Performance Timers work (WIP)" This reverts commit 6b23bf896710cb82e0060178472f09ac32b230c5. --- .../Include/Common/CriticalSection.h | 1 - .../GameEngine/Include/Common/PerfTimer.h | 16 ++--- .../GameEngine/Source/Common/PerfTimer.cpp | 14 ++--- .../GameEngine/Source/Common/System/Debug.cpp | 2 +- .../Source/Common/System/FileSystem.cpp | 14 ++--- .../Source/Common/System/GameMemory.cpp | 58 +++++++++---------- .../Source/GameClient/GraphDraw.cpp | 2 - 7 files changed, 48 insertions(+), 59 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h b/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h index 7987c14117c..37075830e53 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h @@ -33,7 +33,6 @@ #include "Common/PerfTimer.h" -#undef PERF_TIMERS #ifdef PERF_TIMERS extern PerfGather TheCritSecPerfGather; #endif diff --git a/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h b/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h index 92ab553ca20..c7a952d1c69 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/PerfTimer.h @@ -38,9 +38,9 @@ NOTE NOTE NOTE: never check this in with this enabled, since there is a nonzero time penalty for running in this mode. Only enable it for local builds for testing purposes! (srj) */ - #define PERF_TIMERS + #define NO_PERF_TIMERS #else - #define PERF_TIMERS + #define NO_PERF_TIMERS #endif #include "Common/GameCommon.h" // ensure we get DUMP_PERF_STATS, or not @@ -58,8 +58,7 @@ class DebugDisplayInterface; //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- -//#define NO_USE_QPF // non-QPF is much faster. -#define USE_QPF // non-QPF is much faster. +#define NO_USE_QPF // non-QPF is much faster. #if defined(PERF_TIMERS) || defined(DUMP_PERF_STATS) //------------------------------------------------------------------------------------------------- @@ -90,7 +89,7 @@ class PerfGather { public: // If net only (default), subtract perf timers running inside. [8/12/2003] - PerfGather( const char *identifier/*, Bool netOnly=true*/ ); + PerfGather( const char *identifier, Bool netOnly=true ); virtual ~PerfGather( ); __forceinline void startTimer(); @@ -133,24 +132,18 @@ class PerfGather Bool m_ignore; Bool m_netTimeOnly; }; -extern DWORD theMainThreadID; //------------------------------------------------------------------------------------------------- void PerfGather::startTimer() { - DEBUG_ASSERTCRASH(theMainThreadID == GetCurrentThreadId(), ("PerfGather Start Thread")); *++m_activeHead = this; - DEBUG_ASSERTCRASH(m_activeHead >= &m_active[0] && m_activeHead <= &m_active[MAX_ACTIVE_STACK-1], ("active under/over flow")); - //DEBUG_LOG(("startTimer %s %d\n", m_identifier, int(m_activeHead-&m_active[0]))); GetPrecisionTimer(&m_startTime); } //------------------------------------------------------------------------------------------------- void PerfGather::stopTimer() { - //DEBUG_LOG(("stopTimer %s %d\n", m_identifier, int(m_activeHead-&m_active[0]))); DEBUG_ASSERTCRASH(this != NULL, ("I am null, uh oh")); - DEBUG_ASSERTCRASH(theMainThreadID == GetCurrentThreadId(), ("PerfGather Thread")); Int64 runTime; GetPrecisionTimer(&runTime); @@ -168,7 +161,6 @@ void PerfGather::stopTimer() DEBUG_ASSERTCRASH(m_activeHead >= &m_active[0] && m_activeHead <= &m_active[MAX_ACTIVE_STACK-1], ("active under/over flow")); #endif --m_activeHead; - DEBUG_ASSERTCRASH(m_activeHead >= &m_active[0] && m_activeHead <= &m_active[MAX_ACTIVE_STACK-1], ("active under/over flow")); if (*m_activeHead) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp b/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp index ff2538720b2..0d4fcec12f1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/PerfTimer.cpp @@ -64,7 +64,7 @@ void GetPrecisionTimerTicksPerSec(Int64* t) } //Kris: Plugged in Martin's code to optimize timer setup. -//#define HOFFESOMMER_REPLACEMENT_CODE +#define HOFFESOMMER_REPLACEMENT_CODE //------------------------------------------------------------------------------------------------- void InitPrecisionTimer() @@ -246,7 +246,7 @@ class PerfMetricsOutput { if (m_outputStats[i].first == id) { - m_outputStats.erase(m_outputStats.begin()+i); + m_outputStats.erase(&m_outputStats[i]); return; } } @@ -375,7 +375,7 @@ void PerfGather::reset() Int64 start, end; PerfGather pf("timer"); GetPrecisionTimer(&start); - /*for (Int ii = 0; ii < ITERS; ++ii) + for (Int ii = 0; ii < ITERS; ++ii) { pf.startTimer(); pf.stopTimer(); pf.startTimer(); pf.stopTimer(); @@ -385,7 +385,7 @@ void PerfGather::reset() pf.startTimer(); pf.stopTimer(); pf.startTimer(); pf.stopTimer(); pf.startTimer(); pf.stopTimer(); - }*/ + } GetPrecisionTimer(&end); s_stopStartOverhead = (end - start) / (ITERS*8); DEBUG_LOG(("s_stopStartOverhead is %d (%f usec)\n",(int)s_stopStartOverhead,s_stopStartOverhead/s_ticksPerUSec)); @@ -408,7 +408,7 @@ void PerfGather::reset() return; } - if (frame >= 0 && frame <= 30) + if (frame >= 1 && frame <= 30) { // always skip the first second or so, since it loads everything and skews the results horribly } @@ -597,7 +597,7 @@ void PerfTimer::outputInfo( void ) return; } -#if 1 +#if defined(_DEBUG) || defined(_INTERNAL) double totalTimeInMS = 1000.0 * m_runningTime / s_ticksPerSec; double avgTimePerFrame = totalTimeInMS / (m_lastFrame - m_startFrame + 1); double avgTimePerCall = totalTimeInMS / m_callCount; @@ -635,7 +635,7 @@ void PerfTimer::outputInfo( void ) //------------------------------------------------------------------------------------------------- void PerfTimer::showMetrics( void ) { -#if 1 +#if defined(_DEBUG) || defined(_INTERNAL) double totalTimeInMS = 1000.0 * m_runningTime / s_ticksPerSec; double avgTimePerFrame = totalTimeInMS / (m_lastFrame - m_startFrame + 1); double avgTimePerCall = totalTimeInMS / m_callCount; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp index 163e8443342..40c29ed4055 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp @@ -112,7 +112,7 @@ static FILE *theLogFile = NULL; #define LARGE_BUFFER 8192 static char theBuffer[ LARGE_BUFFER ]; // make it big to avoid weird overflow bugs in debug mode static int theDebugFlags = 0; -DWORD theMainThreadID = 0; +static DWORD theMainThreadID = 0; // ---------------------------------------------------------------------------- // PUBLIC DATA // ---------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp index b53578cd3ae..1a59074af23 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp @@ -151,7 +151,7 @@ void FileSystem::init( void ) void FileSystem::update( void ) { - //USE_PERF_TIMER(FileSystem) + USE_PERF_TIMER(FileSystem) TheLocalFileSystem->update(); TheArchiveFileSystem->update(); } @@ -162,7 +162,7 @@ void FileSystem::update( void ) void FileSystem::reset( void ) { - //USE_PERF_TIMER(FileSystem) + USE_PERF_TIMER(FileSystem) TheLocalFileSystem->reset(); TheArchiveFileSystem->reset(); } @@ -173,7 +173,7 @@ void FileSystem::reset( void ) File* FileSystem::openFile( const Char *filename, Int access ) { - //USE_PERF_TIMER(FileSystem) + USE_PERF_TIMER(FileSystem) File *file = NULL; if ( TheLocalFileSystem != NULL ) @@ -195,7 +195,7 @@ File* FileSystem::openFile( const Char *filename, Int access ) Bool FileSystem::doesFileExist(const Char *filename) const { - //USE_PERF_TIMER(FileSystem) + USE_PERF_TIMER(FileSystem) unsigned key=TheNameKeyGenerator->nameToLowercaseKey(filename); std::map::iterator i=m_fileExist.find(key); @@ -222,7 +222,7 @@ Bool FileSystem::doesFileExist(const Char *filename) const //============================================================================ void FileSystem::getFileListInDirectory(const AsciiString& directory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const { - //USE_PERF_TIMER(FileSystem) + USE_PERF_TIMER(FileSystem) TheLocalFileSystem->getFileListInDirectory(AsciiString(""), directory, searchName, filenameList, searchSubdirectories); TheArchiveFileSystem->getFileListInDirectory(AsciiString(""), directory, searchName, filenameList, searchSubdirectories); } @@ -232,7 +232,7 @@ void FileSystem::getFileListInDirectory(const AsciiString& directory, const Asci //============================================================================ Bool FileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo) const { - //USE_PERF_TIMER(FileSystem) + USE_PERF_TIMER(FileSystem) if (fileInfo == NULL) { return FALSE; } @@ -254,7 +254,7 @@ Bool FileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo) co //============================================================================ Bool FileSystem::createDirectory(AsciiString directory) { - //USE_PERF_TIMER(FileSystem) + USE_PERF_TIMER(FileSystem) if (TheLocalFileSystem != NULL) { return TheLocalFileSystem->createDirectory(directory); } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp index 13f3e7039c8..f34d17d8b4f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/GameMemory.cpp @@ -243,10 +243,10 @@ static void* sysAllocateDoNotZero(Int numBytes) throw ERROR_OUT_OF_MEMORY; #ifdef MEMORYPOOL_DEBUG { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) #ifdef USE_FILLER_VALUE { - //USE_PERF_TIMER(MemoryPoolInitFilling) + USE_PERF_TIMER(MemoryPoolInitFilling) ::memset32(p, s_initFillerValue, ::GlobalSize(p)); } #endif @@ -269,7 +269,7 @@ static void sysFree(void* p) { #ifdef MEMORYPOOL_DEBUG { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) ::memset32(p, GARBAGE_FILL_VALUE, ::GlobalSize(p)); theTotalSystemAllocationInBytes -= ::GlobalSize(p); } @@ -859,7 +859,7 @@ void MemoryPoolSingleBlock::initBlock(Int logicalSize, MemoryPoolBlob *owningBlo #ifdef MEMORYPOOL_DEBUG { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) m_magicCookie = SINGLEBLOCK_MAGIC_COOKIE; m_debugFlags = 0; if (!theMainInitFlag) @@ -1037,7 +1037,7 @@ Int MemoryPoolSingleBlock::debugSingleBlockReportLeak(const char* owner) */ void MemoryPoolSingleBlock::debugVerifyBlock() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) DEBUG_ASSERTCRASH(this, ("null this")); DEBUG_ASSERTCRASH(m_magicCookie == SINGLEBLOCK_MAGIC_COOKIE, ("wrong cookie")); @@ -1060,7 +1060,7 @@ void MemoryPoolSingleBlock::debugVerifyBlock() */ void MemoryPoolSingleBlock::debugMarkBlockAsFree() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) ::memset32(getUserDataNoDbg(), GARBAGE_FILL_VALUE, m_logicalSize); m_debugLiteralTagString = FREE_SINGLEBLOCK_TAG_STRING; @@ -1078,7 +1078,7 @@ void MemoryPoolSingleBlock::debugMarkBlockAsFree() */ Bool MemoryPoolSingleBlock::debugCheckUnderrun() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) #ifdef MEMORYPOOL_BOUNDINGWALL Int *p = (Int*)(((char*)getUserDataNoDbg()) - WALLSIZE); @@ -1103,7 +1103,7 @@ Bool MemoryPoolSingleBlock::debugCheckUnderrun() */ Bool MemoryPoolSingleBlock::debugCheckOverrun() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) #ifdef MEMORYPOOL_BOUNDINGWALL Int *p = (Int*)(((char*)getUserDataNoDbg()) + m_logicalSize); @@ -1310,7 +1310,7 @@ void MemoryPoolBlob::freeSingleBlock(MemoryPoolSingleBlock *block) */ void MemoryPoolBlob::debugMemoryVerifyBlob() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) DEBUG_ASSERTCRASH(m_owningPool != NULL, ("bad owner")); DEBUG_ASSERTCRASH(m_usedBlocksInBlob >= 0 && m_usedBlocksInBlob <= m_totalBlocksInBlob, ("unlikely m_usedBlocksInBlob")); @@ -1351,7 +1351,7 @@ Int MemoryPoolBlob::debugBlobReportLeaks(const char* owner) */ Bool MemoryPoolBlob::debugIsBlockInBlob(void *pBlockPtr) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) MemoryPoolSingleBlock *block = MemoryPoolSingleBlock::recoverBlockFromUserData(pBlockPtr); Int rawBlockSize = MemoryPoolSingleBlock::calcRawBlockSize(m_owningPool->getAllocationSize()); @@ -1684,7 +1684,7 @@ void* MemoryPool::allocateBlockDoNotZeroImplementation(DECLARE_LITERALSTRING_ARG m_factory->adjustTotals(debugLiteralTagString, 1*getAllocationSize(), 0); #ifdef USE_FILLER_VALUE { - //USE_PERF_TIMER(MemoryPoolInitFilling) + USE_PERF_TIMER(MemoryPoolInitFilling) ::memset32(block->getUserData(), s_initFillerValue, getAllocationSize()); } #endif @@ -1903,7 +1903,7 @@ Int MemoryPool::debugPoolReportLeaks( const char* owner ) */ void MemoryPool::debugMemoryVerifyPool() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) Int used = 0; Int total = 0; @@ -1925,7 +1925,7 @@ void MemoryPool::debugMemoryVerifyPool() */ Bool MemoryPool::debugIsBlockInPool(void *pBlockPtr) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return false; @@ -1962,7 +1962,7 @@ Bool MemoryPool::debugIsBlockInPool(void *pBlockPtr) */ const char *MemoryPool::debugGetBlockTagString(void *pBlockPtr) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return FREE_SINGLEBLOCK_TAG_STRING; @@ -2129,7 +2129,7 @@ void DynamicMemoryAllocator::removeFromList(DynamicMemoryAllocator **pHead) #ifdef MEMORYPOOL_DEBUG void DynamicMemoryAllocator::debugIgnoreLeaksForThisBlock(void* pBlockPtr) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return; @@ -2178,7 +2178,7 @@ void *DynamicMemoryAllocator::allocateBytesDoNotZeroImplementation(Int numBytes result = pool->allocateBlockDoNotZeroImplementation(PASS_LITERALSTRING_ARG1); #ifdef MEMORYPOOL_DEBUG { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) waste = pool->getAllocationSize() - numBytes; MemoryPoolSingleBlock *wblock = MemoryPoolSingleBlock::recoverBlockFromUserData(result); wblock->debugSetWastedSize(waste); @@ -2216,7 +2216,7 @@ void *DynamicMemoryAllocator::allocateBytesDoNotZeroImplementation(Int numBytes #ifdef MEMORYPOOL_DEBUG { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) theTotalDMA += numBytes; if (thePeakDMA < theTotalDMA) thePeakDMA = theTotalDMA; @@ -2242,7 +2242,7 @@ void *DynamicMemoryAllocator::allocateBytesDoNotZeroImplementation(Int numBytes #ifdef MEMORYPOOL_DEBUG #ifdef USE_FILLER_VALUE { - //USE_PERF_TIMER(MemoryPoolInitFilling) + USE_PERF_TIMER(MemoryPoolInitFilling) ::memset32(result, s_initFillerValue, numBytes); } #endif @@ -2292,7 +2292,7 @@ void DynamicMemoryAllocator::freeBytes(void* pBlockPtr) const char* tagString; #endif { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) waste = 0; used = block->debugGetLogicalSize(); theTotalDMA -= used; @@ -2308,7 +2308,7 @@ void DynamicMemoryAllocator::freeBytes(void* pBlockPtr) { #ifdef MEMORYPOOL_DEBUG { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) DEBUG_ASSERTCRASH(findPoolForSize(used) == block->getOwningBlob()->getOwningPool(), ("pool mismatch")); #ifdef INTENSE_DMA_BOOKKEEPING if (doingIntenseDMA == 0) @@ -2400,7 +2400,7 @@ void DynamicMemoryAllocator::reset() */ Bool DynamicMemoryAllocator::debugIsPoolInDma(MemoryPool *pool) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (!pool) return false; @@ -2423,7 +2423,7 @@ Bool DynamicMemoryAllocator::debugIsPoolInDma(MemoryPool *pool) */ Bool DynamicMemoryAllocator::debugIsBlockInDma(void *pBlockPtr) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return false; @@ -2455,7 +2455,7 @@ Bool DynamicMemoryAllocator::debugIsBlockInDma(void *pBlockPtr) */ const char *DynamicMemoryAllocator::debugGetBlockTagString(void *pBlockPtr) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return FREE_SINGLEBLOCK_TAG_STRING; @@ -2477,7 +2477,7 @@ const char *DynamicMemoryAllocator::debugGetBlockTagString(void *pBlockPtr) */ void DynamicMemoryAllocator::debugMemoryVerifyDma() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) for (MemoryPoolSingleBlock *b = m_rawBlocks; b; b = b->getNextRawBlock()) { @@ -2509,7 +2509,7 @@ void DynamicMemoryAllocator::debugResetCheckpoints() */ Int DynamicMemoryAllocator::debugCalcRawBlockBytes(Int *numBlocks) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (numBlocks) *numBlocks = 0; @@ -2805,7 +2805,7 @@ static const char* s_specialPrefixes[MAX_SPECIAL_USED] = */ void MemoryPoolFactory::adjustTotals(const char* tagString, Int usedDelta, Int physDelta) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) m_usedBytes += usedDelta; m_physBytes += physDelta; @@ -2855,7 +2855,7 @@ void MemoryPoolFactory::debugSetInitFillerIndex(Int index) */ void MemoryPoolFactory::debugMemoryVerify() { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) Int used = 0, phys = 0; @@ -2887,7 +2887,7 @@ void MemoryPoolFactory::debugMemoryVerify() */ Bool MemoryPoolFactory::debugIsBlockInAnyPool(void *pBlock) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) #ifdef MEMORYPOOL_INTENSE_VERIFY debugMemoryVerify(); @@ -2917,7 +2917,7 @@ Bool MemoryPoolFactory::debugIsBlockInAnyPool(void *pBlock) */ const char *MemoryPoolFactory::debugGetBlockTagString(void *pBlockPtr) { - //USE_PERF_TIMER(MemoryPoolDebugging) + USE_PERF_TIMER(MemoryPoolDebugging) if (!pBlockPtr) return FREE_SINGLEBLOCK_TAG_STRING; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp index 711e1f78c97..3db0c78a279 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GraphDraw.cpp @@ -89,10 +89,8 @@ void GraphDraw::render() width -= start; Int height = TheDisplay->getHeight(); - (void)height; Int totalCount = m_graphEntries.size(); - (void)totalCount; DEBUG_ASSERTCRASH(totalCount < MAX_GRAPH_VALUES, ("MAX_GRAPH_VALUES must be increased, not all labels will appear (max %d, cur %d).\n",MAX_GRAPH_VALUES,totalCount)); DEBUG_ASSERTCRASH(BAR_HEIGHT * totalCount < height, ("BAR_HEIGHT must be reduced, as bars are being drawn off-screen.\n")); VecGraphEntriesIt it; From 7352e5b40a84b162a65909f020a9d3049f42ab6c Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 8 May 2025 22:36:41 +0200 Subject: [PATCH 027/112] Fix date formats in comments --- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 2 +- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 2 +- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 4 ++-- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- .../Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 2 +- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 35c9f4186a2..4a3e7650d54 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -327,7 +327,7 @@ class GlobalData : public SubsystemInterface AsciiString m_initialFile; ///< If this is specified, load a specific map/replay from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start - std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 04/13/2025) + std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index ff8adfa901f..1065e450bb5 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -48,7 +48,7 @@ class ReplayGameInfo : public GameInfo enum RecorderModeType CPP_11(: Int) { RECORDERMODETYPE_RECORD, RECORDERMODETYPE_PLAYBACK, - RECORDERMODETYPE_SIMULATION_PLAYBACK, // Play back replay without any graphics (TheSuperHackers @feature helmutbuhler 04/13/2025) + RECORDERMODETYPE_SIMULATION_PLAYBACK, // Play back replay without any graphics (TheSuperHackers @feature helmutbuhler 13/04/2025) RECORDERMODETYPE_NONE // this is a valid state to be in on the shell map, or in saved games }; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index af03db6130d..1f31c9dcea9 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -1246,11 +1246,11 @@ static CommandLineParam params[] = { "-noshaders", parseNoShaders }, { "-quickstart", parseQuickStart }, - // TheSuperHackers @feature helmutbuhler 04/11/2025 + // TheSuperHackers @feature helmutbuhler 11/04/2025 // This runs the game without a window, graphics, input and audio. Used for testing. { "-headless", parseHeadless }, - // TheSuperHackers @feature helmutbuhler 04/13/2025 + // TheSuperHackers @feature helmutbuhler 13/04/2025 // Simulate replay without graphics. Pass the filename including .rep afterwards. // You can pass this multiple times to check multiple replays. { "-simReplay", parseSimReplay }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 34983ca2789..ae55bfed6c5 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1226,7 +1226,7 @@ Bool RecorderClass::playbackFile(AsciiString filename) // send a message to the logic for a new game if (!m_doingAnalysis) { - // TheSuperHackers @info helmutbuhler 04/13/2025 + // TheSuperHackers @info helmutbuhler 13/04/2025 // We send the New Game message here directly to the command list and bypass the TheMessageStream. // That's ok because Multiplayer is disabled during replay playback and is actually required // during replay simulation because we don't update TheMessageStream during simulation. diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 2b3cc71191d..306183d9461 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -79,7 +79,7 @@ static Bool justEntered = FALSE; #if 1 static GameWindow *buttonAnalyzeReplay = NULL; -// TheSuperHackers @feature helmutbuhler 04/13/2025 +// TheSuperHackers @feature helmutbuhler 13/04/2025 // Button to simulate replay without graphics static GameWindow *buttonSimulateReplay = NULL; #endif diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index dbdf54ffb4a..310058d41f7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -3678,7 +3678,7 @@ void GameLogic::update( void ) msg->appendIntegerArgument(m_CRC); msg->appendBooleanArgument(isPlayback); - // TheSuperHackers @info helmutbuhler 04/13/2025 + // TheSuperHackers @info helmutbuhler 13/04/2025 // During replay simulation, we bypass TheMessageStream and instead put the CRC message // directly into TheCommandList because we don't update TheMessageStream during simulation. GameMessageList *messageList = TheMessageStream; From 2f01c18ed59913a2b827c420f54c0f1cdaeba71e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 8 May 2025 22:36:59 +0200 Subject: [PATCH 028/112] Remove copied comment --- GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h index aa443a56abb..e6d266581bc 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h @@ -94,7 +94,7 @@ class GameClient : public SubsystemInterface, // subsystem methods virtual void init( void ); ///< Initialize resources virtual void update( void ); ///< Updates the GUI, display, audio, etc - void updateHeadless(); ///< Updates the GUI, display, audio, etc + void updateHeadless(); virtual void reset( void ); ///< reset system virtual void setFrame( UnsignedInt frame ) { m_frame = frame; } ///< Set the GameClient's internal frame number From 08fad6f7c19a9a871e0f6a413577bf2474543a7a Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 8 May 2025 22:38:43 +0200 Subject: [PATCH 029/112] Remove bugfix that doesn't belong here --- .../Source/GameLogic/Object/Update/StealthUpdate.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp index 26c37c4b5c5..260f86224cb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp @@ -1055,9 +1055,7 @@ void StealthUpdate::changeVisualDisguise() const ThingTemplate *tTemplate = self->getTemplate(); - // TheSuperHackers @bugfix helmutbuhler 04/13/2025 - // draw was originally not assigned here and potentially caused memory corruption - draw = TheThingFactory->newDrawable( tTemplate ); + TheThingFactory->newDrawable( tTemplate ); if( draw ) { TheGameLogic->bindObjectAndDrawable(self, draw); From 3873f6949cdf4c8896da65857f03779471ff0673 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 8 May 2025 22:45:51 +0200 Subject: [PATCH 030/112] Remove added dump code in RecorderClass::handleCRCMessage --- .../GameEngine/Source/Common/Recorder.cpp | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index ae55bfed6c5..1baa866b849 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1061,29 +1061,9 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f DEBUG_CRASH(("Replay has gone out of sync! All bets are off!\nInGame:%8.8X Replay:%8.8X\nFrame:%d", playbackCRC, newCRC, mismatchFrame)); - // TheSuperHackers @info helmutbuhler 04/13/2025 + // TheSuperHackers @info helmutbuhler 13/04/2025 // Print Mismatch to console in case we are in SimulateReplayList printf("CRC Mismatch in Frame %d\n", mismatchFrame); - - // dump GameLogic random seed - DEBUG_LOG(("GameLogic frame = %d\n", TheGameLogic->getFrame())); - DEBUG_LOG(("GetGameLogicRandomSeedCRC() = %d\n", GetGameLogicRandomSeedCRC())); - - // dump CRCs - { - DEBUG_LOG(("--- GameState Dump ---\n")); - #ifdef DEBUG_CRC - outputCRCDumpLines(); - #endif - DEBUG_LOG(("------ End Dump ------\n")); - } - { - DEBUG_LOG(("--- DebugInfo Dump ---\n")); - #ifdef DEBUG_CRC - outputCRCDebugLines(); - #endif - DEBUG_LOG(("------ End Dump ------\n")); - } } return; } From eac7e889ac609e7a01356dbcdb7cbb86b44b0faf Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 8 May 2025 23:23:45 +0200 Subject: [PATCH 031/112] Remove merged headless references --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 4 ---- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 1f31c9dcea9..3a10608a984 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -1246,10 +1246,6 @@ static CommandLineParam params[] = { "-noshaders", parseNoShaders }, { "-quickstart", parseQuickStart }, - // TheSuperHackers @feature helmutbuhler 11/04/2025 - // This runs the game without a window, graphics, input and audio. Used for testing. - { "-headless", parseHeadless }, - // TheSuperHackers @feature helmutbuhler 13/04/2025 // Simulate replay without graphics. Pass the filename including .rep afterwards. // You can pass this multiple times to check multiple replays. diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 310058d41f7..76fdb559091 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1266,7 +1266,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) //****************************// // Get the m_loadScreen for this kind of game - if(!m_loadScreen && !TheGlobalData->m_headless && + if(!m_loadScreen && !(TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK)) { m_loadScreen = getLoadScreen( loadingSaveGame ); From c6ede9d5d213cda5226b5351208f521f5ac0fe79 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 9 May 2025 13:10:13 +0200 Subject: [PATCH 032/112] Fix simulation loop in ReplayMenu --- .../Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 306183d9461..06952eb311c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -37,6 +37,7 @@ #include "Common/GameState.h" #include "Common/Recorder.h" #include "Common/version.h" +#include "GameClient/GameClient.h" #include "GameClient/WindowLayout.h" #include "GameClient/Gadget.h" #include "GameClient/GadgetListBox.h" @@ -784,10 +785,13 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, { do { + TheGameClient->updateHeadless(); TheGameLogic->UPDATE(); if (TheRecorder->sawCRCMismatch()) break; } while (TheRecorder->isPlaybackInProgress()); + if (TheGameLogic->isInGame()) + TheGameLogic->clearGameData(); } } } From 8c6bfefd3e004383206f68e50e0ecc1c4c300882 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Mon, 12 May 2025 12:41:40 +0200 Subject: [PATCH 033/112] Make ReadReplayListFromCsv work in subfolders --- .../GameEngine/Source/Common/CommandLine.cpp | 7 ------- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 21 ++++++++++++++++++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 3a10608a984..1cf15c3439a 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -113,13 +113,6 @@ static void ConvertShortMapPathToLongMapPath(AsciiString &mapName) static void ConvertShortReplayPathToLongReplayPath(AsciiString &filename) { - if (filename.find('\\') != NULL || filename.find('/') != NULL) - { - // Absolute paths not yet supported - DEBUG_CRASH(("Invalid replay name %s", filename.str())); - exit(1); - } - if (!filename.endsWithNoCase(RecorderClass::getReplayExtention())) { DEBUG_CRASH(("Invalid replay name %s", filename.str())); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 06952eb311c..d844550f5ed 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -279,6 +279,21 @@ void NextToken(AsciiString *string, AsciiString *token, char separator) void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) { + // Get path of csv file relative to replay folder. + // Later we will search for replays in that path. + AsciiString relativeFolder = filename; + { + int len = relativeFolder.getLength(); + while (len) + { + char c = relativeFolder.getCharAt(len-1); + if (c == '/' || c == '\\') + break; + relativeFolder.removeLastChar(); + len--; + } + } + AsciiString fname; fname.format("%s%s", TheRecorder->getReplayDir().str(), filename.str()); FILE *fp = fopen(fname.str(), "rt"); @@ -309,7 +324,11 @@ void ReadReplayListFromCsv(AsciiString filename, std::vector* repla token.removeLastChar(); } if (!token.isEmpty()) - replayList->push_back(token); + { + AsciiString path; + path.format("%s%s", relativeFolder.str(), token.str()); + replayList->push_back(path); + } // Ignore remaining columns } From 95e6769b5bb331c01ec6c3df93f048430192dd6e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Mon, 12 May 2025 12:42:31 +0200 Subject: [PATCH 034/112] Make subprocesses call with -headless --- GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 83fadae356e..6100f7cd36b 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -138,7 +138,7 @@ class ReplayProcess filenameWide.translate(filename); UnicodeString command; - command.format(L"\"%s\" -win -xres 800 -yres 600 -simReplay \"%s\"", exePath, filenameWide.str()); + command.format(L"\"%s\" -win -xres 800 -yres 600 -ignoreAsserts -headless -simReplay \"%s\"", exePath, filenameWide.str()); //wprintf(L"Starting Exe for Replay \"%s\": %s\n", filenameWide.str(), command.str()); //fflush(stdout); From 9820db27512395fd513b07b3f279474d3ff9d747 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Mon, 12 May 2025 12:43:43 +0200 Subject: [PATCH 035/112] Allow more iniCRCs in WriteOutReplayList --- .../GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index d844550f5ed..ed9886e9fa4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -166,7 +166,8 @@ void WriteOutReplayList() // Some versions, e.g. "Zero Hour 1.04 The Ultimate Collection" have a different ini crc and are // actually incompatible. Mark them as incompatible - compatibleVersion = compatibleVersion && header.iniCRC == 0xfeaae3f3; + compatibleVersion = compatibleVersion && + (header.iniCRC == 0xfeaae3f3 || header.iniCRC == 0xb859d2f9); // Check whether random seed appears multiple times. This can be used to check only one replay // per game in case multiple replays by different players of the same game are in the list. @@ -179,7 +180,7 @@ void WriteOutReplayList() // If you want to check replays with certain properties, you can change this expression // or change the csv file manually. bool check = md && !header.desyncGame && header.endTime != 0 && - compatibleVersion && numHumans > 1 && numAIs > 0 && uniqueSeed; + compatibleVersion && uniqueSeed;// && numHumans > 1 && numAIs > 0; fprintf(fp, "%s", i == -1 ? "check" : check ? "1" : "0"); if (i == -1) @@ -190,7 +191,7 @@ void WriteOutReplayList() fprintf(fp, ",%s", i == -1 ? "map_exists" : md ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); - //fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); + fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); UnsignedInt gameTime = header.frameDuration / LOGICFRAMES_PER_SECOND; fprintf(fp, i == -1 ? ",time" : ",%02d:%02d", gameTime/60, gameTime%60); From 1582909aa0064d87c5811b5addc2b7443979b4ae Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Mon, 19 May 2025 23:05:33 +0200 Subject: [PATCH 036/112] Move Worker Process stuff into separate file --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/Common/WorkerProcess.h | 48 +++ .../GameEngine/Source/Common/GameMain.cpp | 329 ++---------------- .../Source/Common/WorkerProcess.cpp | 221 ++++++++++++ 4 files changed, 304 insertions(+), 296 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h create mode 100644 GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 4e9ec56354d..60488184aae 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -129,6 +129,7 @@ set(GAMEENGINE_SRC Include/Common/UserPreferences.h Include/Common/version.h Include/Common/WellKnownKeys.h + Include/Common/WorkerProcess.h Include/Common/Xfer.h Include/Common/XferCRC.h Include/Common/XferDeepCRC.h @@ -676,6 +677,7 @@ set(GAMEENGINE_SRC Source/Common/Thing/ThingTemplate.cpp Source/Common/UserPreferences.cpp Source/Common/version.cpp + Source/Common/WorkerProcess.cpp Source/GameClient/Color.cpp Source/GameClient/Credits.cpp Source/GameClient/Display.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h new file mode 100644 index 00000000000..7a539441e16 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h @@ -0,0 +1,48 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +// Helper class that allows you to start a worker process and retrieve its exit code +// and console output as a string. +// It also makes sure that the started process is killed in case our process exits in any way. +class WorkerProcess +{ +public: + WorkerProcess(); + + bool StartProcess(AsciiString filename); + + bool IsRunning() const; + + // returns true iff the process exited. + // Sets the parameters if return value is true. + bool IsDone(DWORD *exitcode, AsciiString *stdOutput) const; + void Update(); + + // Terminate Process if it's running + void Kill(); + +private: + HANDLE m_processHandle; + HANDLE m_readHandle; + HANDLE m_jobHandle; + AsciiString m_stdOutput; + DWORD m_exitcode; + bool m_isDone; +}; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 6100f7cd36b..fe6dcc3c83b 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -33,246 +33,12 @@ #include "GameLogic/GameLogic.h" #include "GameClient/GameClient.h" -#if 0 -bool SimulateReplayInProcess(AsciiString filename) -{ - STARTUPINFO si = { sizeof(STARTUPINFO) }; - si.dwFlags = STARTF_FORCEOFFFEEDBACK; - PROCESS_INFORMATION pi = { 0 }; - AsciiString command; - command.format("generalszh.exe -win -xres 800 -yres 600 -simReplay \"%s\"", filename.str()); - //printf("Starting Exe for Replay \"%s\": %s\n", filename.str(), command.str()); - //fflush(stdout); - CreateProcessA(NULL, (LPSTR)command.str(), - NULL, NULL, TRUE, 0, - NULL, 0, &si, &pi); - CloseHandle(pi.hThread); - WaitForSingleObject(pi.hProcess, INFINITE); - DWORD exitcode = 1; - GetExitCodeProcess(pi.hProcess, &exitcode); - CloseHandle(pi.hProcess); - return exitcode != 0; -} -#else - -// We need Job-related functions, but these aren't defined in the Windows-headers that VC6 uses. -// So we define them here and load them dynamically. -#if defined(_MSC_VER) && _MSC_VER < 1300 -struct JOBOBJECT_BASIC_LIMIT_INFORMATION2 -{ - LARGE_INTEGER PerProcessUserTimeLimit; - LARGE_INTEGER PerJobUserTimeLimit; - DWORD LimitFlags; - SIZE_T MinimumWorkingSetSize; - SIZE_T MaximumWorkingSetSize; - DWORD ActiveProcessLimit; - ULONG_PTR Affinity; - DWORD PriorityClass; - DWORD SchedulingClass; -}; -struct IO_COUNTERS -{ - ULONGLONG ReadOperationCount; - ULONGLONG WriteOperationCount; - ULONGLONG OtherOperationCount; - ULONGLONG ReadTransferCount; - ULONGLONG WriteTransferCount; - ULONGLONG OtherTransferCount; -}; -struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION -{ - JOBOBJECT_BASIC_LIMIT_INFORMATION2 BasicLimitInformation; - IO_COUNTERS IoInfo; - SIZE_T ProcessMemoryLimit; - SIZE_T JobMemoryLimit; - SIZE_T PeakProcessMemoryUsed; - SIZE_T PeakJobMemoryUsed; -}; - -#define JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 0x00002000 -const int JobObjectExtendedLimitInformation = 9; - -typedef HANDLE (WINAPI *PFN_CreateJobObjectW)(LPSECURITY_ATTRIBUTES, LPCWSTR); -typedef BOOL (WINAPI *PFN_SetInformationJobObject)(HANDLE, JOBOBJECTINFOCLASS, LPVOID, DWORD); -typedef BOOL (WINAPI *PFN_AssignProcessToJobObject)(HANDLE, HANDLE); - -static PFN_CreateJobObjectW CreateJobObjectW = (PFN_CreateJobObjectW)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateJobObjectW"); -static PFN_SetInformationJobObject SetInformationJobObject = (PFN_SetInformationJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetInformationJobObject"); -static PFN_AssignProcessToJobObject AssignProcessToJobObject = (PFN_AssignProcessToJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "AssignProcessToJobObject"); -#endif - -class ReplayProcess -{ -public: - ReplayProcess() - { - m_processHandle = NULL; - m_readHandle = NULL; - m_jobHandle = NULL; - m_exitcode = 0; - m_isDone = false; - } - - bool StartProcess(AsciiString filename) - { - m_stdOutput = ""; - m_isDone = false; - - PROCESS_INFORMATION pi = { 0 }; - - SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; - saAttr.bInheritHandle = TRUE; - HANDLE writeHandle = NULL; - CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0); - SetHandleInformation(m_readHandle, HANDLE_FLAG_INHERIT, 0); - - STARTUPINFOW si = { sizeof(STARTUPINFOW) }; - si.dwFlags = STARTF_FORCEOFFFEEDBACK; // Prevent cursor wait animation - si.hStdError = writeHandle; - si.hStdOutput = writeHandle; - si.dwFlags |= STARTF_USESTDHANDLES; - - WideChar exePath[1024]; - GetModuleFileNameW(NULL, exePath, 1024); - UnicodeString filenameWide; - filenameWide.translate(filename); - - UnicodeString command; - command.format(L"\"%s\" -win -xres 800 -yres 600 -ignoreAsserts -headless -simReplay \"%s\"", exePath, filenameWide.str()); - //wprintf(L"Starting Exe for Replay \"%s\": %s\n", filenameWide.str(), command.str()); - //fflush(stdout); - - if (!CreateProcessW(NULL, (LPWSTR)command.str(), - NULL, NULL, /*bInheritHandles=*/TRUE, 0, - NULL, 0, &si, &pi)) - { - CloseHandle(writeHandle); - CloseHandle(m_readHandle); - m_readHandle = NULL; - return false; - } - - CloseHandle(pi.hThread); - CloseHandle(writeHandle); - m_processHandle = pi.hProcess; - - // We want to make sure that when our process is killed, our workers automatically terminate as well. - // In Windows, the way to do this is to attach the worker to a job we own. - m_jobHandle = CreateJobObjectW(NULL, NULL); - if (m_jobHandle != NULL) - { - JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = { 0 }; - jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; - SetInformationJobObject(m_jobHandle, (JOBOBJECTINFOCLASS)JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)); - AssignProcessToJobObject(m_jobHandle, m_processHandle); - } - - return true; - } - - bool IsRunning() const - { - return m_processHandle != NULL; - } - - bool IsDone(DWORD *exitcode, AsciiString *stdOutput) - { - *exitcode = m_exitcode; - *stdOutput = m_stdOutput; - return m_isDone; - } - - void Update() - { - if (!IsRunning()) - return; - - while (true) - { - // Call PeekNamedPipe to make sure ReadFile won't block - DWORD bytesAvailable = 0; - BOOL success = PeekNamedPipe(m_readHandle, NULL, 0, NULL, &bytesAvailable, NULL); - if (!success) - break; - if (bytesAvailable == 0) - { - // Child process is still running and we have all output so far - return; - } +#include "Common/WorkerProcess.h" - DWORD readBytes = 0; - char buffer[1024]; - success = ReadFile(m_readHandle, buffer, 1024-1, &readBytes, NULL); - if (!success) - break; - DEBUG_ASSERTCRASH(readBytes != 0, ("expected readBytes to be non null")); - - // Remove \r, otherwise each new line is doubled when we output it again - for (int i = 0; i < readBytes; i++) - if (buffer[i] == '\r') - buffer[i] = ' '; - buffer[readBytes] = 0; - m_stdOutput.concat(buffer); - } - - // Pipe broke, that means the process already exited. But we call this just to make sure - WaitForSingleObject(m_processHandle, INFINITE); - GetExitCodeProcess(m_processHandle, &m_exitcode); - CloseHandle(m_processHandle); - m_processHandle = NULL; - - CloseHandle(m_readHandle); - m_readHandle = NULL; - - if (m_jobHandle != NULL) - { - CloseHandle(m_jobHandle); // This kills the process if still running - m_jobHandle = NULL; - } - - m_isDone = true; - } - - void Cancel() - { - if (m_processHandle != NULL) - { - TerminateProcess(m_processHandle, 1); - CloseHandle(m_processHandle); - m_processHandle = NULL; - } - - if (m_readHandle != NULL) - { - CloseHandle(m_readHandle); - m_readHandle = NULL; - } - - if (m_jobHandle != NULL) - { - CloseHandle(m_jobHandle); - m_jobHandle = NULL; - } - - m_stdOutput = ""; - m_isDone = false; - } - -private: - HANDLE m_processHandle; - HANDLE m_readHandle; - HANDLE m_jobHandle; - AsciiString m_stdOutput; - DWORD m_exitcode; - bool m_isDone; -}; - - -#endif int SimulateReplayListMultiProcess(const std::vector &filenames); -// TheSuperHackers @feature helmutbuhler 04/13/2025 +// TheSuperHackers @feature helmutbuhler 13/04/2025 // Simulate a list of replays without graphics. // Returns exitcode 1 if mismatch or other error occured int SimulateReplayList(const std::vector &filenames) @@ -288,73 +54,44 @@ int SimulateReplayList(const std::vector &filenames) for (size_t i = 0; i < filenames.size(); i++) { AsciiString filename = filenames[i]; - if (filenames.size() == 1) + printf("Simulating Replay \"%s\"\n", filename.str()); + fflush(stdout); + DWORD startTime = GetTickCount(); + if (TheRecorder->simulateReplay(filename)) { - printf("Simulating Replay \"%s\"\n", filename.str()); - fflush(stdout); - DWORD startTime = GetTickCount(); - if (TheRecorder->simulateReplay(filename)) + UnsignedInt totalTime = TheRecorder->getFrameDuration() / LOGICFRAMES_PER_SECOND; + while (TheRecorder->isPlaybackInProgress()) { - UnsignedInt totalTime = TheRecorder->getFrameDuration() / LOGICFRAMES_PER_SECOND; - while (TheRecorder->isPlaybackInProgress()) - { - TheGameClient->updateHeadless(); - //TheParticleSystemManager->reset(); + TheGameClient->updateHeadless(); + //TheParticleSystemManager->reset(); - if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*LOGICFRAMES_PER_SECOND) == 0) - { - // Print progress report - UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTime = (GetTickCount()-startTime) / 1000; - printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", - realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); - fflush(stdout); - } - TheGameLogic->UPDATE(); - if (TheRecorder->sawCRCMismatch()) - { - numErrors++; - break; - } + if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*LOGICFRAMES_PER_SECOND) == 0) + { + // Print progress report + UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; + UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", + realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); + fflush(stdout); + } + TheGameLogic->UPDATE(); + if (TheRecorder->sawCRCMismatch()) + { + numErrors++; + break; } - UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTime = (GetTickCount()-startTime) / 1000; - printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", - realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); - fflush(stdout); - } - else - { - printf("Cannot open replay\n"); - numErrors++; } + UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; + UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", + realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); + fflush(stdout); } else { - /*printf("%d/%d ", i+1, filenames.size()); - fflush(stdout); - //bool error = SimulateReplayInProcess(filename); - ReplayProcess p; - p.StartProcess(filename); - DWORD exitcode; - AsciiString stdOutput; - while (!p.Update(&exitcode, &stdOutput)) - {} - printf("%s", stdOutput.str()); - fflush(stdout); - numErrors += exitcode == 0 ? 0 : 1;*/ + printf("Cannot open replay\n"); + numErrors++; } - /*if (i == TheGlobalData->m_simulateReplayList.size()-1) - { - if (TheGameLogic->isInGame()) - { - TheGameLogic->clearGameData(); - } - delete TheGameEngine; - TheGameEngine = NULL; - TheGameEngine = CreateGameEngine(); - TheGameEngine->init(argc, argv); - }*/ } if (TheGlobalData->m_simulateReplayList.size() > 1) { @@ -387,7 +124,7 @@ int SimulateReplayListMultiProcess(const std::vector &filenames) { DWORD totalStartTime = GetTickCount(); - std::vector processes; + std::vector processes; const int maxProcesses = 20; int filenamePositionStarted = 0; int filenamePositionDone = 0; @@ -426,7 +163,7 @@ int SimulateReplayListMultiProcess(const std::vector &filenames) // Add new processes when we are below the limit and there are replays left while (numProcessesRunning < maxProcesses && filenamePositionStarted < filenames.size()) { - ReplayProcess p; + WorkerProcess p; p.StartProcess(filenames[filenamePositionStarted]); processes.push_back(p); filenamePositionStarted++; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp new file mode 100644 index 00000000000..1f6ca01ca2a --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp @@ -0,0 +1,221 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include "Common/WorkerProcess.h" + +// We need Job-related functions, but these aren't defined in the Windows-headers that VC6 uses. +// So we define them here and load them dynamically. +#if defined(_MSC_VER) && _MSC_VER < 1300 +struct JOBOBJECT_BASIC_LIMIT_INFORMATION2 +{ + LARGE_INTEGER PerProcessUserTimeLimit; + LARGE_INTEGER PerJobUserTimeLimit; + DWORD LimitFlags; + SIZE_T MinimumWorkingSetSize; + SIZE_T MaximumWorkingSetSize; + DWORD ActiveProcessLimit; + ULONG_PTR Affinity; + DWORD PriorityClass; + DWORD SchedulingClass; +}; +struct IO_COUNTERS +{ + ULONGLONG ReadOperationCount; + ULONGLONG WriteOperationCount; + ULONGLONG OtherOperationCount; + ULONGLONG ReadTransferCount; + ULONGLONG WriteTransferCount; + ULONGLONG OtherTransferCount; +}; +struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION +{ + JOBOBJECT_BASIC_LIMIT_INFORMATION2 BasicLimitInformation; + IO_COUNTERS IoInfo; + SIZE_T ProcessMemoryLimit; + SIZE_T JobMemoryLimit; + SIZE_T PeakProcessMemoryUsed; + SIZE_T PeakJobMemoryUsed; +}; + +#define JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 0x00002000 +const int JobObjectExtendedLimitInformation = 9; + +typedef HANDLE (WINAPI *PFN_CreateJobObjectW)(LPSECURITY_ATTRIBUTES, LPCWSTR); +typedef BOOL (WINAPI *PFN_SetInformationJobObject)(HANDLE, JOBOBJECTINFOCLASS, LPVOID, DWORD); +typedef BOOL (WINAPI *PFN_AssignProcessToJobObject)(HANDLE, HANDLE); + +static PFN_CreateJobObjectW CreateJobObjectW = (PFN_CreateJobObjectW)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateJobObjectW"); +static PFN_SetInformationJobObject SetInformationJobObject = (PFN_SetInformationJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetInformationJobObject"); +static PFN_AssignProcessToJobObject AssignProcessToJobObject = (PFN_AssignProcessToJobObject)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "AssignProcessToJobObject"); +#endif + +WorkerProcess::WorkerProcess() +{ + m_processHandle = NULL; + m_readHandle = NULL; + m_jobHandle = NULL; + m_exitcode = 0; + m_isDone = false; +} + +bool WorkerProcess::StartProcess(AsciiString filename) +{ + m_stdOutput = ""; + m_isDone = false; + + PROCESS_INFORMATION pi = { 0 }; + + SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; + saAttr.bInheritHandle = TRUE; + HANDLE writeHandle = NULL; + CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0); + SetHandleInformation(m_readHandle, HANDLE_FLAG_INHERIT, 0); + + STARTUPINFOW si = { sizeof(STARTUPINFOW) }; + si.dwFlags = STARTF_FORCEOFFFEEDBACK; // Prevent cursor wait animation + si.hStdError = writeHandle; + si.hStdOutput = writeHandle; + si.dwFlags |= STARTF_USESTDHANDLES; + + WideChar exePath[1024]; + GetModuleFileNameW(NULL, exePath, 1024); + UnicodeString filenameWide; + filenameWide.translate(filename); + + UnicodeString command; + command.format(L"\"%s\" -win -xres 800 -yres 600 -ignoreAsserts -headless -simReplay \"%s\"", exePath, filenameWide.str()); + //wprintf(L"Starting Exe for Replay \"%s\": %s\n", filenameWide.str(), command.str()); + //fflush(stdout); + + if (!CreateProcessW(NULL, (LPWSTR)command.str(), + NULL, NULL, /*bInheritHandles=*/TRUE, 0, + NULL, 0, &si, &pi)) + { + CloseHandle(writeHandle); + CloseHandle(m_readHandle); + m_readHandle = NULL; + return false; + } + + CloseHandle(pi.hThread); + CloseHandle(writeHandle); + m_processHandle = pi.hProcess; + + // We want to make sure that when our process is killed, our workers automatically terminate as well. + // In Windows, the way to do this is to attach the worker to a job we own. + m_jobHandle = CreateJobObjectW(NULL, NULL); + if (m_jobHandle != NULL) + { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = { 0 }; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject(m_jobHandle, (JOBOBJECTINFOCLASS)JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)); + AssignProcessToJobObject(m_jobHandle, m_processHandle); + } + + return true; +} + +bool WorkerProcess::IsRunning() const +{ + return m_processHandle != NULL; +} + +bool WorkerProcess::IsDone(DWORD *exitcode, AsciiString *stdOutput) const +{ + *exitcode = m_exitcode; + *stdOutput = m_stdOutput; + return m_isDone; +} + +void WorkerProcess::Update() +{ + if (!IsRunning()) + return; + + while (true) + { + // Call PeekNamedPipe to make sure ReadFile won't block + DWORD bytesAvailable = 0; + BOOL success = PeekNamedPipe(m_readHandle, NULL, 0, NULL, &bytesAvailable, NULL); + if (!success) + break; + if (bytesAvailable == 0) + { + // Child process is still running and we have all output so far + return; + } + + DWORD readBytes = 0; + char buffer[1024]; + success = ReadFile(m_readHandle, buffer, 1024-1, &readBytes, NULL); + if (!success) + break; + DEBUG_ASSERTCRASH(readBytes != 0, ("expected readBytes to be non null")); + + // Remove \r, otherwise each new line is doubled when we output it again + for (int i = 0; i < readBytes; i++) + if (buffer[i] == '\r') + buffer[i] = ' '; + buffer[readBytes] = 0; + m_stdOutput.concat(buffer); + } + + // Pipe broke, that means the process already exited. But we call this just to make sure + WaitForSingleObject(m_processHandle, INFINITE); + GetExitCodeProcess(m_processHandle, &m_exitcode); + CloseHandle(m_processHandle); + m_processHandle = NULL; + + CloseHandle(m_readHandle); + m_readHandle = NULL; + + CloseHandle(m_jobHandle); + m_jobHandle = NULL; + + m_isDone = true; +} + +void WorkerProcess::Kill() +{ + if (!IsRunning()) + return; + + if (m_processHandle != NULL) + { + TerminateProcess(m_processHandle, 1); + CloseHandle(m_processHandle); + m_processHandle = NULL; + } + + if (m_readHandle != NULL) + { + CloseHandle(m_readHandle); + m_readHandle = NULL; + } + + if (m_jobHandle != NULL) + { + CloseHandle(m_jobHandle); + m_jobHandle = NULL; + } + + m_stdOutput = ""; + m_isDone = false; +} + From dbeb14949151e77b103eaec982ac70dcb136f4fb Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Mon, 19 May 2025 23:20:11 +0200 Subject: [PATCH 037/112] Move command creation out of WorkerProcess::StartProcess --- .../GameEngine/Include/Common/WorkerProcess.h | 2 +- .../GameEngine/Source/Common/GameMain.cpp | 11 ++++++++- .../Source/Common/WorkerProcess.cpp | 23 ++++++------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h index 7a539441e16..9701116b08a 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h @@ -26,7 +26,7 @@ class WorkerProcess public: WorkerProcess(); - bool StartProcess(AsciiString filename); + bool StartProcess(UnicodeString command); bool IsRunning() const; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index fe6dcc3c83b..787f1eb76c6 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -124,6 +124,9 @@ int SimulateReplayListMultiProcess(const std::vector &filenames) { DWORD totalStartTime = GetTickCount(); + WideChar exePath[1024]; + GetModuleFileNameW(NULL, exePath, 1024); + std::vector processes; const int maxProcesses = 20; int filenamePositionStarted = 0; @@ -163,9 +166,15 @@ int SimulateReplayListMultiProcess(const std::vector &filenames) // Add new processes when we are below the limit and there are replays left while (numProcessesRunning < maxProcesses && filenamePositionStarted < filenames.size()) { + UnicodeString filenameWide; + filenameWide.translate(filenames[filenamePositionStarted]); + UnicodeString command; + command.format(L"\"%s\" -headless -simReplay \"%s\"", exePath, filenameWide.str()); + WorkerProcess p; - p.StartProcess(filenames[filenamePositionStarted]); + p.StartProcess(command); processes.push_back(p); + filenamePositionStarted++; numProcessesRunning++; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp index 1f6ca01ca2a..8c267761a36 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp @@ -74,13 +74,12 @@ WorkerProcess::WorkerProcess() m_isDone = false; } -bool WorkerProcess::StartProcess(AsciiString filename) +bool WorkerProcess::StartProcess(UnicodeString command) { m_stdOutput = ""; m_isDone = false; - PROCESS_INFORMATION pi = { 0 }; - + // Create pipe for reading console output SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; saAttr.bInheritHandle = TRUE; HANDLE writeHandle = NULL; @@ -89,19 +88,11 @@ bool WorkerProcess::StartProcess(AsciiString filename) STARTUPINFOW si = { sizeof(STARTUPINFOW) }; si.dwFlags = STARTF_FORCEOFFFEEDBACK; // Prevent cursor wait animation + si.dwFlags |= STARTF_USESTDHANDLES; si.hStdError = writeHandle; si.hStdOutput = writeHandle; - si.dwFlags |= STARTF_USESTDHANDLES; - - WideChar exePath[1024]; - GetModuleFileNameW(NULL, exePath, 1024); - UnicodeString filenameWide; - filenameWide.translate(filename); - - UnicodeString command; - command.format(L"\"%s\" -win -xres 800 -yres 600 -ignoreAsserts -headless -simReplay \"%s\"", exePath, filenameWide.str()); - //wprintf(L"Starting Exe for Replay \"%s\": %s\n", filenameWide.str(), command.str()); - //fflush(stdout); + + PROCESS_INFORMATION pi = { 0 }; if (!CreateProcessW(NULL, (LPWSTR)command.str(), NULL, NULL, /*bInheritHandles=*/TRUE, 0, @@ -119,7 +110,7 @@ bool WorkerProcess::StartProcess(AsciiString filename) // We want to make sure that when our process is killed, our workers automatically terminate as well. // In Windows, the way to do this is to attach the worker to a job we own. - m_jobHandle = CreateJobObjectW(NULL, NULL); + m_jobHandle = CreateJobObjectW != NULL ? CreateJobObjectW(NULL, NULL) : NULL; if (m_jobHandle != NULL) { JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = { 0 }; @@ -167,7 +158,7 @@ void WorkerProcess::Update() if (!success) break; DEBUG_ASSERTCRASH(readBytes != 0, ("expected readBytes to be non null")); - + // Remove \r, otherwise each new line is doubled when we output it again for (int i = 0; i < readBytes; i++) if (buffer[i] == '\r') From b924e23401856b320670c7ca88c7fa835cacd857 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 22 May 2025 21:43:50 +0200 Subject: [PATCH 038/112] Put Replay Simulation Code into own file --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../Include/Common/ReplaySimulation.h | 25 +++ .../GameEngine/Source/Common/GameMain.cpp | 171 +--------------- .../Source/Common/ReplaySimulation.cpp | 187 ++++++++++++++++++ 4 files changed, 215 insertions(+), 170 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h create mode 100644 GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 60488184aae..f198d0cc335 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -94,6 +94,7 @@ set(GAMEENGINE_SRC Include/Common/RAMFile.h Include/Common/RandomValue.h Include/Common/Recorder.h + Include/Common/ReplaySimulation.h Include/Common/Registry.h Include/Common/ResourceGatheringManager.h Include/Common/Science.h @@ -607,6 +608,7 @@ set(GAMEENGINE_SRC Source/Common/PerfTimer.cpp Source/Common/RandomValue.cpp Source/Common/Recorder.cpp + Source/Common/ReplaySimulation.cpp Source/Common/RTS/AcademyStats.cpp Source/Common/RTS/ActionManager.cpp Source/Common/RTS/Energy.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h new file mode 100644 index 00000000000..8ebe80a4887 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h @@ -0,0 +1,25 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +// TheSuperHackers @feature helmutbuhler 13/04/2025 +// Simulate a list of replays without graphics. +// Returns exitcode 1 if mismatch or other error occured +// Returns exitcode 0 if all replays were successfully simulated without mismatches +int SimulateReplayList(const std::vector &filenames); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 787f1eb76c6..21e785041fa 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -29,177 +29,8 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "Common/GameEngine.h" -#include "Common/Recorder.h" -#include "GameLogic/GameLogic.h" -#include "GameClient/GameClient.h" +#include "Common/ReplaySimulation.h" -#include "Common/WorkerProcess.h" - - -int SimulateReplayListMultiProcess(const std::vector &filenames); - -// TheSuperHackers @feature helmutbuhler 13/04/2025 -// Simulate a list of replays without graphics. -// Returns exitcode 1 if mismatch or other error occured -int SimulateReplayList(const std::vector &filenames) -{ - if (filenames.size() != 1) - { - return SimulateReplayListMultiProcess(filenames); - } - - // Note that we use printf here because this is run from cmd. - int numErrors = 0; - DWORD totalStartTime = GetTickCount(); - for (size_t i = 0; i < filenames.size(); i++) - { - AsciiString filename = filenames[i]; - printf("Simulating Replay \"%s\"\n", filename.str()); - fflush(stdout); - DWORD startTime = GetTickCount(); - if (TheRecorder->simulateReplay(filename)) - { - UnsignedInt totalTime = TheRecorder->getFrameDuration() / LOGICFRAMES_PER_SECOND; - while (TheRecorder->isPlaybackInProgress()) - { - TheGameClient->updateHeadless(); - //TheParticleSystemManager->reset(); - - if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*LOGICFRAMES_PER_SECOND) == 0) - { - // Print progress report - UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTime = (GetTickCount()-startTime) / 1000; - printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", - realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); - fflush(stdout); - } - TheGameLogic->UPDATE(); - if (TheRecorder->sawCRCMismatch()) - { - numErrors++; - break; - } - } - UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTime = (GetTickCount()-startTime) / 1000; - printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", - realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); - fflush(stdout); - } - else - { - printf("Cannot open replay\n"); - numErrors++; - } - } - if (TheGlobalData->m_simulateReplayList.size() > 1) - { - if (numErrors) - printf("Errors occured: %d\n", numErrors); - else - printf("Successfully simulated all replays\n"); - - UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; - printf("Total Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); - fflush(stdout); - } - - // TheSuperHackers @todo helmutbuhler 04/05/2025 - // Some replays cause a crash inside TheGameLogic->clearGameData(). - // It likely has to do with Contains. We really gotta fix this, but for now we just terminate. - TerminateProcess(GetCurrentProcess(), numErrors != 0 ? 1 : 0); - - // TheSuperHackers @todo helmutbuhler 04/13/2025 - // There is a bug somewhere in the destructor of TheGameEngine which doesn't properly - // clean up the players and causes a crash unless this is called. - if (TheGameLogic->isInGame()) - { - TheGameLogic->clearGameData(); - } - return numErrors != 0 ? 1 : 0; -} - -int SimulateReplayListMultiProcess(const std::vector &filenames) -{ - DWORD totalStartTime = GetTickCount(); - - WideChar exePath[1024]; - GetModuleFileNameW(NULL, exePath, 1024); - - std::vector processes; - const int maxProcesses = 20; - int filenamePositionStarted = 0; - int filenamePositionDone = 0; - int numErrors = 0; - - while (true) - { - int i; - for (i = 0; i < processes.size(); i++) - processes[i].Update(); - - // Get result of finished processes and print output in order - while (processes.size() != 0) - { - DWORD exitcode; - AsciiString stdOutput; - if (!processes[0].IsDone(&exitcode, &stdOutput)) - break; - printf("%d/%d %s", filenamePositionDone+1, filenames.size(), stdOutput.str()); - if (exitcode != 0) - printf("Error!\n"); - fflush(stdout); - numErrors += exitcode == 0 ? 0 : 1; - processes.erase(processes.begin()); - filenamePositionDone++; - } - - // Count how many processes are running - int numProcessesRunning = 0; - for (i = 0; i < processes.size(); i++) - { - if (processes[i].IsRunning()) - numProcessesRunning++; - } - - // Add new processes when we are below the limit and there are replays left - while (numProcessesRunning < maxProcesses && filenamePositionStarted < filenames.size()) - { - UnicodeString filenameWide; - filenameWide.translate(filenames[filenamePositionStarted]); - UnicodeString command; - command.format(L"\"%s\" -headless -simReplay \"%s\"", exePath, filenameWide.str()); - - WorkerProcess p; - p.StartProcess(command); - processes.push_back(p); - - filenamePositionStarted++; - numProcessesRunning++; - } - - if (processes.empty()) - break; - - // Don't waste CPU here, our workers need every bit of CPU time they can get - Sleep(100); - } - - DEBUG_ASSERTCRASH(filenamePositionStarted == filenames.size(), ("inconsistent file position 1")); - DEBUG_ASSERTCRASH(filenamePositionDone == filenames.size(), ("inconsistent file position 2")); - - if (numErrors) - printf("Errors occured: %d\n", numErrors); - else - printf("Successfully simulated all replays\n"); - - UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; - printf("Total Wall Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); - fflush(stdout); - - return numErrors != 0 ? 1 : 0; -} /** * This is the entry point for the game system. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp new file mode 100644 index 00000000000..654c05d2c7d --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -0,0 +1,187 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine + +#include "Common/Recorder.h" +#include "Common/WorkerProcess.h" +#include "GameLogic/GameLogic.h" +#include "GameClient/GameClient.h" + +static int SimulateReplayListSingleProcess(const std::vector &filenames) +{ + // Note that we use printf here because this is run from cmd. + int numErrors = 0; + DWORD totalStartTime = GetTickCount(); + for (size_t i = 0; i < filenames.size(); i++) + { + AsciiString filename = filenames[i]; + printf("Simulating Replay \"%s\"\n", filename.str()); + fflush(stdout); + DWORD startTime = GetTickCount(); + if (TheRecorder->simulateReplay(filename)) + { + UnsignedInt totalTime = TheRecorder->getFrameDuration() / LOGICFRAMES_PER_SECOND; + while (TheRecorder->isPlaybackInProgress()) + { + TheGameClient->updateHeadless(); + //TheParticleSystemManager->reset(); + + if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*LOGICFRAMES_PER_SECOND) == 0) + { + // Print progress report + UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; + UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", + realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); + fflush(stdout); + } + TheGameLogic->UPDATE(); + if (TheRecorder->sawCRCMismatch()) + { + numErrors++; + break; + } + } + UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; + UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", + realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); + fflush(stdout); + } + else + { + printf("Cannot open replay\n"); + numErrors++; + } + } + if (TheGlobalData->m_simulateReplayList.size() > 1) + { + if (numErrors) + printf("Errors occured: %d\n", numErrors); + else + printf("Successfully simulated all replays\n"); + + UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; + printf("Total Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); + fflush(stdout); + } + + // TheSuperHackers @todo helmutbuhler 04/05/2025 + // Some replays cause a crash inside TheGameLogic->clearGameData(). + // It likely has to do with Contains. We really gotta fix this, but for now we just terminate. + TerminateProcess(GetCurrentProcess(), numErrors != 0 ? 1 : 0); + + // TheSuperHackers @todo helmutbuhler 04/13/2025 + // There is a bug somewhere in the destructor of TheGameEngine which doesn't properly + // clean up the players and causes a crash unless this is called. + if (TheGameLogic->isInGame()) + { + TheGameLogic->clearGameData(); + } + return numErrors != 0 ? 1 : 0; +} + +static int SimulateReplayListMultiProcess(const std::vector &filenames) +{ + DWORD totalStartTime = GetTickCount(); + + WideChar exePath[1024]; + GetModuleFileNameW(NULL, exePath, 1024); + + std::vector processes; + const int maxProcesses = 20; + int filenamePositionStarted = 0; + int filenamePositionDone = 0; + int numErrors = 0; + + while (true) + { + int i; + for (i = 0; i < processes.size(); i++) + processes[i].Update(); + + // Get result of finished processes and print output in order + while (processes.size() != 0) + { + DWORD exitcode; + AsciiString stdOutput; + if (!processes[0].IsDone(&exitcode, &stdOutput)) + break; + printf("%d/%d %s", filenamePositionDone+1, filenames.size(), stdOutput.str()); + if (exitcode != 0) + printf("Error!\n"); + fflush(stdout); + numErrors += exitcode == 0 ? 0 : 1; + processes.erase(processes.begin()); + filenamePositionDone++; + } + + // Count how many processes are running + int numProcessesRunning = 0; + for (i = 0; i < processes.size(); i++) + { + if (processes[i].IsRunning()) + numProcessesRunning++; + } + + // Add new processes when we are below the limit and there are replays left + while (numProcessesRunning < maxProcesses && filenamePositionStarted < filenames.size()) + { + UnicodeString filenameWide; + filenameWide.translate(filenames[filenamePositionStarted]); + UnicodeString command; + command.format(L"\"%s\" -headless -simReplay \"%s\"", exePath, filenameWide.str()); + + WorkerProcess p; + p.StartProcess(command); + processes.push_back(p); + + filenamePositionStarted++; + numProcessesRunning++; + } + + if (processes.empty()) + break; + + // Don't waste CPU here, our workers need every bit of CPU time they can get + Sleep(100); + } + + DEBUG_ASSERTCRASH(filenamePositionStarted == filenames.size(), ("inconsistent file position 1")); + DEBUG_ASSERTCRASH(filenamePositionDone == filenames.size(), ("inconsistent file position 2")); + + if (numErrors) + printf("Errors occured: %d\n", numErrors); + else + printf("Successfully simulated all replays\n"); + + UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; + printf("Total Wall Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); + fflush(stdout); + + return numErrors != 0 ? 1 : 0; +} + +int SimulateReplayList(const std::vector &filenames) +{ + if (filenames.size() == 1) + return SimulateReplayListSingleProcess(filenames); + else + return SimulateReplayListMultiProcess(filenames); +} From 8875ac2484aae87673206f3f7178271ed58aaee8 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 23 May 2025 08:48:32 +0200 Subject: [PATCH 039/112] Put ReadReplayListFromCsv and WriteOutReplayList into separate file --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/Common/ReplayListCsv.h | 23 ++ .../GameEngine/Source/Common/CommandLine.cpp | 2 +- .../Source/Common/ReplayListCsv.cpp | 276 ++++++++++++++++++ .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 227 -------------- 5 files changed, 302 insertions(+), 228 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h create mode 100644 GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index f198d0cc335..63ee4e93073 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -94,6 +94,7 @@ set(GAMEENGINE_SRC Include/Common/RAMFile.h Include/Common/RandomValue.h Include/Common/Recorder.h + Include/Common/ReplayListCsv.h Include/Common/ReplaySimulation.h Include/Common/Registry.h Include/Common/ResourceGatheringManager.h @@ -608,6 +609,7 @@ set(GAMEENGINE_SRC Source/Common/PerfTimer.cpp Source/Common/RandomValue.cpp Source/Common/Recorder.cpp + Source/Common/ReplayListCsv.cpp Source/Common/ReplaySimulation.cpp Source/Common/RTS/AcademyStats.cpp Source/Common/RTS/ActionManager.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h new file mode 100644 index 00000000000..d185dab89b5 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h @@ -0,0 +1,23 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + + +void WriteOutReplayList(); +void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 1cf15c3439a..7ce44bfe125 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -31,6 +31,7 @@ #include "Common/LocalFileSystem.h" #include "Common/version.h" #include "Common/Recorder.h" +#include "Common/ReplayListCsv.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -442,7 +443,6 @@ Int parseSimReplayList(char *args[], int num) { if (TheWritableGlobalData && num > 1) { - void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); AsciiString filename = args[1]; ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplayList); return 2; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp new file mode 100644 index 00000000000..21cd31db677 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp @@ -0,0 +1,276 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine + +#include "Common/ReplayListCsv.h" +#include "Common/Recorder.h" +#include "Common/FileSystem.h" +#include "GameClient/MapUtil.h" + + +Bool GetReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader *headerOut, ReplayGameInfo *infoOut, const MapMetaData **mdOut) +{ + // lets get some info about the replay + RecorderClass::ReplayHeader header; + header.forPlayback = FALSE; + header.filename = filename; + Bool success = TheRecorder && TheMapCache && TheRecorder->readReplayHeader( header ); + if (!success) + return false; + + ReplayGameInfo info; + if (!ParseAsciiStringToGameInfo( &info, header.gameOptions )) + return false; + + header.replayName.translate(filename); + for (Int tmp=0; tmp < TheRecorder->getReplayExtention().getLength(); ++tmp) + header.replayName.removeLastChar(); + + if (headerOut) *headerOut = header; + if (infoOut) *infoOut = info; + if (mdOut) *mdOut = TheMapCache->findMap(info.getMap()); + return true; +} + +void WriteOutReplayList() +{ + AsciiString fname; + fname.format("%sreplay_list.csv", TheRecorder->getReplayDir().str()); + FILE *fp = fopen(fname.str(), "wt"); + if (!fp) + return; + + // Get list of replay filenames + AsciiString asciisearch; + asciisearch = "*"; + asciisearch.concat(TheRecorder->getReplayExtention()); + FilenameList replayFilenamesSet; + TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenamesSet, FALSE); + std::vector replayFilenames; + for (FilenameListIter it = replayFilenamesSet.begin(); it != replayFilenamesSet.end(); ++it) + { + replayFilenames.push_back(*it); + } + + TheMapCache->updateCache(); + + std::set foundSeeds; + + // Print out a line per filename. i = -1 is csv header. + for (int i = -1; i < (int)replayFilenames.size(); i++) + { + AsciiString filename; + RecorderClass::ReplayHeader header; + ReplayGameInfo info; + const MapMetaData *md = NULL; + if (i != -1) + { + filename.set(replayFilenames[i].reverseFind('\\') + 1); + Bool success = GetReplayMapInfo(filename, &header, &info, &md); + if (!success) + continue; + } + int numHumans = 0, numAIs = 0; + for (int slot = 0; slot < MAX_SLOTS; slot++) + { + SlotState state = info.getSlot(slot)->getState(); + numHumans += state == SLOT_PLAYER ? 1 : 0; + numAIs += state >= SLOT_EASY_AI && state <= SLOT_BRUTAL_AI ? 1 : 0; + } + + bool compatibleVersion = + header.versionString == UnicodeString(L"Version 1.4") || + header.versionString == UnicodeString(L"Version 1.04") || + header.versionString == UnicodeString(L"Version 1.05") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.04") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.05") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.4") || + header.versionString == UnicodeString(L"Versi\x00F3n 1.04") || + header.versionString == UnicodeString(L"Versi\x00F3n 1.05"); + + // Some versions, e.g. "Zero Hour 1.04 The Ultimate Collection" have a different ini crc and are + // actually incompatible. Mark them as incompatible + compatibleVersion = compatibleVersion && + (header.iniCRC == 0xfeaae3f3 || header.iniCRC == 0xb859d2f9); + + // Check whether random seed appears multiple times. This can be used to check only one replay + // per game in case multiple replays by different players of the same game are in the list. + int seed = info.getSeed(); + bool uniqueSeed = foundSeeds.find(seed) == foundSeeds.end(); + if (uniqueSeed) + foundSeeds.insert(seed); + + // When a csv file is loaded with -simReplayList, check indicates whether the replay should be simulated. + // If you want to check replays with certain properties, you can change this expression + // or change the csv file manually. + bool check = md && !header.desyncGame && header.endTime != 0 && + compatibleVersion && uniqueSeed;// && numHumans > 1 && numAIs > 0; + fprintf(fp, "%s", i == -1 ? "check" : check ? "1" : "0"); + + if (i == -1) + fprintf(fp, ",filename"); + else + fprintf(fp, ",\"%s\"", filename.str()); + + fprintf(fp, ",%s", i == -1 ? "map_exists" : md ? "1" : "0"); + fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); + fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); + fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); + + UnsignedInt gameTime = header.frameDuration / LOGICFRAMES_PER_SECOND; + fprintf(fp, i == -1 ? ",time" : ",%02d:%02d", gameTime/60, gameTime%60); + + fprintf(fp, i == -1 ? ",numHumans" : ",%d", numHumans); + fprintf(fp, i == -1 ? ",numAIs" : ",%d", numAIs); + + AsciiString tmp; + tmp.translate(header.versionString); + if (i == -1) + fprintf(fp, ",version"); + else + fprintf(fp, ",\"%s\"", tmp.str()); + //fprintf(fp, i == -1 ? ",exeCRC" : ",0x%08x", header.exeCRC); + //fprintf(fp, i == -1 ? ",iniCRC" : ",0x%08x", header.iniCRC); + + fprintf(fp, ",%s", i == -1 ? "compatibleVersion" : compatibleVersion ? "1" : "0"); + + //fprintf(fp, i == -1 ? ",crcInterval" : ",%d", info.getCRCInterval()); + //fprintf(fp, i == -1 ? ",seed" : ",0x%08x", seed); + + fprintf(fp, "\n"); + +#if 0 + if (i != -1 && check) + { + AsciiString sourceFilename = replayFilenames[i]; + + AsciiString targetFilename; + targetFilename = TheRecorder->getReplayDir(); + targetFilename.concat("filter/"); + targetFilename.concat(filename); + + CopyFile(sourceFilename.str(), targetFilename.str(), FALSE); + } +#endif + } + fclose(fp); +} + +static bool ReadLineFromFile(FILE *fp, AsciiString *str) +{ + const int bufferSize = 128; + char buffer[bufferSize]; + str->clear(); + while (true) + { + if (fgets(buffer, bufferSize, fp) == NULL) + { + str->clear(); + return false; + } + buffer[bufferSize-1] = 0; // Should be already nul-terminated, just to be sure + str->concat(buffer); + if (strlen(buffer) != bufferSize-1 || buffer[bufferSize-2] == '\n') + break; + } + return true; +} + +static void NextToken(AsciiString *string, AsciiString *token, char separator) +{ + const char *tokenStart = string->str(); + + const char *str = tokenStart; + bool inQuotationMarks = false; + while (*str) + { + if (*str == separator && !inQuotationMarks) + break; + if (*str == '\"') + inQuotationMarks = !inQuotationMarks; + str++; + } + const char *tokenEnd = str; + + Int len = tokenEnd - tokenStart; + char *tmp = token->getBufferForRead(len + 1); + memcpy(tmp, tokenStart, len); + tmp[len] = 0; + token->trim(); + + string->set(*tokenEnd == 0 ? tokenEnd : tokenEnd+1); +} + +void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) +{ + // Get path of csv file relative to replay folder. + // Later we will search for replays in that path. + AsciiString relativeFolder = filename; + { + int len = relativeFolder.getLength(); + while (len) + { + char c = relativeFolder.getCharAt(len-1); + if (c == '/' || c == '\\') + break; + relativeFolder.removeLastChar(); + len--; + } + } + + AsciiString fname; + fname.format("%s%s", TheRecorder->getReplayDir().str(), filename.str()); + FILE *fp = fopen(fname.str(), "rt"); + if (!fp) + return; + + // Parse header + AsciiString line, token; + ReadLineFromFile(fp, &line); + char separator = line.find(';') == NULL ? ',' : ';'; + + while (feof(fp) == 0) + { + ReadLineFromFile(fp, &line); + + // Parse check + NextToken(&line, &token, separator); + if (token != "1") + continue; + + // Parse filename + NextToken(&line, &token, separator); + if (token.isEmpty()) + continue; + if (token.getCharAt(0) == '\"' && token.getCharAt(token.getLength()-1) == '\"') + { + token.set(token.str()+1); + token.removeLastChar(); + } + if (!token.isEmpty()) + { + AsciiString path; + path.format("%s%s", relativeFolder.str(), token.str()); + replayList->push_back(path); + } + + // Ignore remaining columns + } + fclose(fp); +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index ed9886e9fa4..4f46186da50 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -110,232 +110,6 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index) -void WriteOutReplayList() -{ - AsciiString fname; - fname.format("%sreplay_list.csv", TheRecorder->getReplayDir().str()); - FILE *fp = fopen(fname.str(), "wt"); - if (!fp) - return; - - // Get list of replay filenames - AsciiString asciisearch; - asciisearch = "*"; - asciisearch.concat(TheRecorder->getReplayExtention()); - FilenameList replayFilenamesSet; - TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenamesSet, FALSE); - std::vector replayFilenames; - for (FilenameListIter it = replayFilenamesSet.begin(); it != replayFilenamesSet.end(); ++it) - { - replayFilenames.push_back(*it); - } - - std::set foundSeeds; - - // Print out a line per filename. i = -1 is csv header. - for (int i = -1; i < (int)replayFilenames.size(); i++) - { - AsciiString filename; - RecorderClass::ReplayHeader header; - ReplayGameInfo info; - const MapMetaData *md = NULL; - if (i != -1) - { - filename.set(replayFilenames[i].reverseFind('\\') + 1); - Bool success = GetMapInfo(filename, &header, &info, &md); - if (!success) - continue; - } - int numHumans = 0, numAIs = 0; - for (int slot = 0; slot < MAX_SLOTS; slot++) - { - SlotState state = info.getSlot(slot)->getState(); - numHumans += state == SLOT_PLAYER ? 1 : 0; - numAIs += state >= SLOT_EASY_AI && state <= SLOT_BRUTAL_AI ? 1 : 0; - } - - bool compatibleVersion = - header.versionString == UnicodeString(L"Version 1.4") || - header.versionString == UnicodeString(L"Version 1.04") || - header.versionString == UnicodeString(L"Version 1.05") || - header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.04") || - header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.05") || - header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.4") || - header.versionString == UnicodeString(L"Versi\x00F3n 1.04") || - header.versionString == UnicodeString(L"Versi\x00F3n 1.05"); - - // Some versions, e.g. "Zero Hour 1.04 The Ultimate Collection" have a different ini crc and are - // actually incompatible. Mark them as incompatible - compatibleVersion = compatibleVersion && - (header.iniCRC == 0xfeaae3f3 || header.iniCRC == 0xb859d2f9); - - // Check whether random seed appears multiple times. This can be used to check only one replay - // per game in case multiple replays by different players of the same game are in the list. - int seed = info.getSeed(); - bool uniqueSeed = foundSeeds.find(seed) == foundSeeds.end(); - if (uniqueSeed) - foundSeeds.insert(seed); - - // When a csv file is loaded with -simReplayList, check indicates whether the replay should be simulated. - // If you want to check replays with certain properties, you can change this expression - // or change the csv file manually. - bool check = md && !header.desyncGame && header.endTime != 0 && - compatibleVersion && uniqueSeed;// && numHumans > 1 && numAIs > 0; - fprintf(fp, "%s", i == -1 ? "check" : check ? "1" : "0"); - - if (i == -1) - fprintf(fp, ",filename"); - else - fprintf(fp, ",\"%s\"", filename.str()); - - fprintf(fp, ",%s", i == -1 ? "map_exists" : md ? "1" : "0"); - fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); - fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); - fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); - - UnsignedInt gameTime = header.frameDuration / LOGICFRAMES_PER_SECOND; - fprintf(fp, i == -1 ? ",time" : ",%02d:%02d", gameTime/60, gameTime%60); - - fprintf(fp, i == -1 ? ",numHumans" : ",%d", numHumans); - fprintf(fp, i == -1 ? ",numAIs" : ",%d", numAIs); - - AsciiString tmp; - tmp.translate(header.versionString); - if (i == -1) - fprintf(fp, ",version"); - else - fprintf(fp, ",\"%s\"", tmp.str()); - //fprintf(fp, i == -1 ? ",exeCRC" : ",0x%08x", header.exeCRC); - //fprintf(fp, i == -1 ? ",iniCRC" : ",0x%08x", header.iniCRC); - - fprintf(fp, ",%s", i == -1 ? "compatibleVersion" : compatibleVersion ? "1" : "0"); - - //fprintf(fp, i == -1 ? ",crcInterval" : ",%d", info.getCRCInterval()); - //fprintf(fp, i == -1 ? ",seed" : ",0x%08x", seed); - - fprintf(fp, "\n"); - - -#if 0 - if (i != -1 && check) - { - AsciiString sourceFilename = replayFilenames[i]; - - AsciiString targetFilename; - targetFilename = TheRecorder->getReplayDir(); - targetFilename.concat("filter/"); - targetFilename.concat(filename); - - CopyFile(sourceFilename.str(), targetFilename.str(), FALSE); - } -#endif - } - fclose(fp); -} - -bool ReadLineFromFile(FILE *fp, AsciiString *str) -{ - const int bufferSize = 128; - char buffer[bufferSize]; - str->clear(); - while (true) - { - if (fgets(buffer, bufferSize, fp) == NULL) - { - str->clear(); - return false; - } - buffer[bufferSize-1] = 0; // Should be already nul-terminated, just to be sure - str->concat(buffer); - if (strlen(buffer) != bufferSize-1 || buffer[bufferSize-2] == '\n') - break; - } - return true; -} - -void NextToken(AsciiString *string, AsciiString *token, char separator) -{ - const char *tokenStart = string->str(); - - const char *str = tokenStart; - bool inQuotationMarks = false; - while (*str) - { - if (*str == separator && !inQuotationMarks) - break; - if (*str == '\"') - inQuotationMarks = !inQuotationMarks; - str++; - } - const char *tokenEnd = str; - - Int len = tokenEnd - tokenStart; - char *tmp = token->getBufferForRead(len + 1); - memcpy(tmp, tokenStart, len); - tmp[len] = 0; - token->trim(); - - string->set(*tokenEnd == 0 ? tokenEnd : tokenEnd+1); -} - -void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) -{ - // Get path of csv file relative to replay folder. - // Later we will search for replays in that path. - AsciiString relativeFolder = filename; - { - int len = relativeFolder.getLength(); - while (len) - { - char c = relativeFolder.getCharAt(len-1); - if (c == '/' || c == '\\') - break; - relativeFolder.removeLastChar(); - len--; - } - } - - AsciiString fname; - fname.format("%s%s", TheRecorder->getReplayDir().str(), filename.str()); - FILE *fp = fopen(fname.str(), "rt"); - if (!fp) - return; - - // Parse header - AsciiString line, token; - ReadLineFromFile(fp, &line); - char separator = line.find(';') == NULL ? ',' : ';'; - - while (feof(fp) == 0) - { - ReadLineFromFile(fp, &line); - - // Parse check - NextToken(&line, &token, separator); - if (token != "1") - continue; - - // Parse filename - NextToken(&line, &token, separator); - if (token.isEmpty()) - continue; - if (token.getCharAt(0) == '\"' && token.getCharAt(token.getLength()-1) == '\"') - { - token.set(token.str()+1); - token.removeLastChar(); - } - if (!token.isEmpty()) - { - AsciiString path; - path.format("%s%s", relativeFolder.str(), token.str()); - replayList->push_back(path); - } - - // Ignore remaining columns - } - fclose(fp); -} - //------------------------------------------------------------------------------------------------- /** Populate the listbox with the names of the available replay files */ //------------------------------------------------------------------------------------------------- @@ -499,7 +273,6 @@ void PopulateReplayFileListbox(GameWindow *listbox) } } GadgetListBoxSetSelected(listbox, 0); - WriteOutReplayList(); } //------------------------------------------------------------------------------------------------- From e49355712ec17b89703d5601ee6c1a34909b043f Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 23 May 2025 09:14:20 +0200 Subject: [PATCH 040/112] Add -jobs commandline --- .../GameEngine/Include/Common/GlobalData.h | 1 + .../Include/Common/ReplaySimulation.h | 2 +- .../GameEngine/Source/Common/CommandLine.cpp | 21 +++++++++++++++++++ .../GameEngine/Source/Common/GameMain.cpp | 2 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/ReplaySimulation.cpp | 9 ++++---- 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 4a3e7650d54..8a854abfb42 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -328,6 +328,7 @@ class GlobalData : public SubsystemInterface AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) + Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or -1 for sequential simulation Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h index 8ebe80a4887..dcb2d488272 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h @@ -22,4 +22,4 @@ // Simulate a list of replays without graphics. // Returns exitcode 1 if mismatch or other error occured // Returns exitcode 0 if all replays were successfully simulated without mismatches -int SimulateReplayList(const std::vector &filenames); +int SimulateReplayList(const std::vector &filenames, int maxProcesses); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 7ce44bfe125..3e5b71316c0 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -450,6 +450,21 @@ Int parseSimReplayList(char *args[], int num) return 1; } +Int parseJobs(char *args[], int num) +{ + if (TheWritableGlobalData && num > 1) + { + TheWritableGlobalData->m_simulateReplayJobs = atoi(args[1]); + if (TheGlobalData->m_simulateReplayJobs < -1 || TheGlobalData->m_simulateReplayJobs == 0) + { + DEBUG_CRASH(("Invalid number of jobs %d", TheGlobalData->m_simulateReplayJobs)); + exit(1); + } + return 2; + } + return 1; +} + Int parseXRes(char *args[], int num) { if (TheWritableGlobalData && num > 1) @@ -1248,6 +1263,12 @@ static CommandLineParam params[] = // Pass in a csv file to simulate multiple replays. The file must be in the replay folder. { "-simReplayList", parseSimReplayList }, + // TheSuperHackers @feature helmutbuhler 23/05/2025 + // Simulate each replay in a separate process and use up two N processes at the same time. + // (If you have 4 cores, call it with -jobs 4) + // If you do not call this, all replays will be simulated in sequence in the same process. + { "-jobs", parseJobs }, + #if (defined(_DEBUG) || defined(_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 21e785041fa..ce35f4bd46c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -44,7 +44,7 @@ Int GameMain( int argc, char *argv[] ) if (!TheGlobalData->m_simulateReplayList.empty()) { - exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList); + exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList, TheGlobalData->m_simulateReplayJobs); } else { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 9dcb2363dec..d96e7a2e864 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -957,6 +957,7 @@ GlobalData::GlobalData() m_pendingFile.clear(); m_simulateReplayList.clear(); + m_simulateReplayJobs = -1; for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 654c05d2c7d..727933947bc 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -97,7 +97,7 @@ static int SimulateReplayListSingleProcess(const std::vector &filen return numErrors != 0 ? 1 : 0; } -static int SimulateReplayListMultiProcess(const std::vector &filenames) +static int SimulateReplayListMultiProcess(const std::vector &filenames, int maxProcesses) { DWORD totalStartTime = GetTickCount(); @@ -105,7 +105,6 @@ static int SimulateReplayListMultiProcess(const std::vector &filena GetModuleFileNameW(NULL, exePath, 1024); std::vector processes; - const int maxProcesses = 20; int filenamePositionStarted = 0; int filenamePositionDone = 0; int numErrors = 0; @@ -178,10 +177,10 @@ static int SimulateReplayListMultiProcess(const std::vector &filena return numErrors != 0 ? 1 : 0; } -int SimulateReplayList(const std::vector &filenames) +int SimulateReplayList(const std::vector &filenames, int maxProcesses) { - if (filenames.size() == 1) + if (maxProcesses == -1) return SimulateReplayListSingleProcess(filenames); else - return SimulateReplayListMultiProcess(filenames); + return SimulateReplayListMultiProcess(filenames, maxProcesses); } From 4f5991610b839d1e3709e7aa3b13daeadb5cf02f Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 23 May 2025 12:24:21 +0200 Subject: [PATCH 041/112] Fix whitespace and revert TheFileSystem->getFileListInDirectory fix --- .../Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 4f46186da50..c4b07f108b4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -109,7 +109,6 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index) } - //------------------------------------------------------------------------------------------------- /** Populate the listbox with the names of the available replay files */ //------------------------------------------------------------------------------------------------- @@ -142,7 +141,7 @@ void PopulateReplayFileListbox(GameWindow *listbox) FilenameList replayFilenames; FilenameListIter it; - TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenames, FALSE); + TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenames, TRUE); TheMapCache->updateCache(); From 03bf7b3741c095f741453ae30e3c025fd8463903 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 23 May 2025 12:36:56 +0200 Subject: [PATCH 042/112] Fix wrong merge --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 739ac9b07a3..c389f4d8488 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -1282,7 +1282,7 @@ static CommandLineParam params[] = // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, -#if (defined(_DEBUG) || defined(_INTERNAL)) +#if (defined(RTS_DEBUG) || defined(RTS_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, { "-nomusic", parseNoMusic }, From 7f44033a4db3b9329c5d6a800b4a7c912897385c Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 23 May 2025 17:18:35 +0200 Subject: [PATCH 043/112] Don't check for multiple instances in headless mode --- GeneralsMD/Code/Main/WinMain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 17f2e4fbe59..07aaf5bec75 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -1005,7 +1005,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // TheSuperHackers @refactor The instance mutex now lives in its own class. - if (!rts::ClientInstance::initialize()) + if (!headless && !rts::ClientInstance::initialize()) { HWND ccwindow = FindWindow(rts::ClientInstance::getFirstInstanceName(), NULL); if (ccwindow) From 0b156148a2fb38aec7afe1a35962e8f2afcf8827 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 12:49:12 +0200 Subject: [PATCH 044/112] Add -writeReplayList commandline and make it work with relative paths --- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/ReplayListCsv.h | 2 +- .../GameEngine/Source/Common/CommandLine.cpp | 14 +++++++++++++ .../GameEngine/Source/Common/GameMain.cpp | 6 ++++++ .../GameEngine/Source/Common/GlobalData.cpp | 2 ++ .../Source/Common/ReplayListCsv.cpp | 20 ++++++++++++------- .../Source/Common/ReplaySimulation.cpp | 2 +- 7 files changed, 38 insertions(+), 9 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 376450a68b4..61fba518caa 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -334,6 +334,7 @@ class GlobalData : public SubsystemInterface std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or -1 for sequential simulation + AsciiString m_writeReplayList; ///< If not empty, write out list of replays in this subfolder into a csv file (TheSuperHackers @feature helmutbuhler 24/05/2025) Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h index d185dab89b5..b0140b341e0 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h @@ -19,5 +19,5 @@ #pragma once -void WriteOutReplayList(); +bool WriteOutReplayList(AsciiString relativeFolder); void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index c389f4d8488..4e47ce8252d 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -461,6 +461,15 @@ Int parseSimReplayList(char *args[], int num) return 1; } +Int parseWriteReplayList(char *args[], int num) +{ + if (TheWritableGlobalData && num > 1) + { + TheWritableGlobalData->m_writeReplayList = args[1]; + } + return 1; +} + Int parseJobs(char *args[], int num) { if (TheWritableGlobalData && num > 1) @@ -1282,6 +1291,11 @@ static CommandLineParam params[] = // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, + // TheSuperHackers @feature helmutbuhler 23/05/2025 + // Write replay list into a csv file. Append a subfolder of the replay folder or . for all + // replays in the replay folder. + { "-writeReplayList", parseWriteReplayList }, + #if (defined(RTS_DEBUG) || defined(RTS_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index ce35f4bd46c..7d73f6e68dd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -30,6 +30,7 @@ #include "Common/GameEngine.h" #include "Common/ReplaySimulation.h" +#include "Common/ReplayListCsv.h" /** @@ -46,6 +47,11 @@ Int GameMain( int argc, char *argv[] ) { exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList, TheGlobalData->m_simulateReplayJobs); } + else if (!TheGlobalData->m_writeReplayList.isEmpty()) + { + bool success = WriteOutReplayList(TheGlobalData->m_writeReplayList); + exitcode = success ? 0 : 1; + } else { // run it diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index cc88caf14e6..304cc2705d8 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -960,6 +960,8 @@ GlobalData::GlobalData() m_simulateReplayList.clear(); m_simulateReplayJobs = -1; + m_writeReplayList = ""; + for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp index 21cd31db677..56f6889e37b 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp @@ -48,26 +48,30 @@ Bool GetReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader * return true; } -void WriteOutReplayList() +bool WriteOutReplayList(AsciiString relativeFolder) { + AsciiString dir; + dir.format("%s%s", TheRecorder->getReplayDir().str(), relativeFolder.str()); + if (!dir.endsWith("\\") && !dir.endsWith("/")) + dir.concat('/'); AsciiString fname; - fname.format("%sreplay_list.csv", TheRecorder->getReplayDir().str()); + fname.format("%s/replay_list.csv", dir.str()); FILE *fp = fopen(fname.str(), "wt"); if (!fp) - return; + return false; // Get list of replay filenames AsciiString asciisearch; asciisearch = "*"; asciisearch.concat(TheRecorder->getReplayExtention()); FilenameList replayFilenamesSet; - TheFileSystem->getFileListInDirectory(TheRecorder->getReplayDir(), asciisearch, replayFilenamesSet, FALSE); + TheFileSystem->getFileListInDirectory(dir, asciisearch, replayFilenamesSet, FALSE); std::vector replayFilenames; for (FilenameListIter it = replayFilenamesSet.begin(); it != replayFilenamesSet.end(); ++it) { replayFilenames.push_back(*it); } - + TheMapCache->updateCache(); std::set foundSeeds; @@ -81,10 +85,11 @@ void WriteOutReplayList() const MapMetaData *md = NULL; if (i != -1) { - filename.set(replayFilenames[i].reverseFind('\\') + 1); + filename.set(replayFilenames[i].str() + TheRecorder->getReplayDir().getLength()); Bool success = GetReplayMapInfo(filename, &header, &info, &md); if (!success) continue; + filename.set(replayFilenames[i].str() + dir.getLength()); } int numHumans = 0, numAIs = 0; for (int slot = 0; slot < MAX_SLOTS; slot++) @@ -128,7 +133,7 @@ void WriteOutReplayList() else fprintf(fp, ",\"%s\"", filename.str()); - fprintf(fp, ",%s", i == -1 ? "map_exists" : md ? "1" : "0"); + fprintf(fp, ",%s", i == -1 ? "mapExists" : md ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); @@ -170,6 +175,7 @@ void WriteOutReplayList() #endif } fclose(fp); + return true; } static bool ReadLineFromFile(FILE *fp, AsciiString *str) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 727933947bc..67b6aaae39c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -70,7 +70,7 @@ static int SimulateReplayListSingleProcess(const std::vector &filen numErrors++; } } - if (TheGlobalData->m_simulateReplayList.size() > 1) + if (filenames.size() > 1) { if (numErrors) printf("Errors occured: %d\n", numErrors); From 417cc0153752c1c10d2cc860c77637a164cb4c3d Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 13:06:43 +0200 Subject: [PATCH 045/112] Use headless mode for -writeReplayList, -simReplay, -simReplayList. Remove -headless option --- .../Code/GameEngine/Source/Common/CommandLine.cpp | 15 +++------------ .../GameEngine/Source/Common/ReplaySimulation.cpp | 2 +- GeneralsMD/Code/Main/WinMain.cpp | 8 ++++++-- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 4e47ce8252d..1703f38c2e3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -445,6 +445,7 @@ Int parseSimReplay(char *args[], int num) AsciiString filename = args[1]; ConvertShortReplayPathToLongReplayPath(filename); TheWritableGlobalData->m_simulateReplayList.push_back(filename); + TheWritableGlobalData->m_headless = TRUE; return 2; } return 1; @@ -456,6 +457,7 @@ Int parseSimReplayList(char *args[], int num) { AsciiString filename = args[1]; ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplayList); + TheWritableGlobalData->m_headless = TRUE; return 2; } return 1; @@ -466,6 +468,7 @@ Int parseWriteReplayList(char *args[], int num) if (TheWritableGlobalData && num > 1) { TheWritableGlobalData->m_writeReplayList = args[1]; + TheWritableGlobalData->m_headless = TRUE; } return 1; } @@ -920,15 +923,6 @@ Int parseQuickStart( char *args[], int num ) return 1; } -Int parseHeadless( char *args[], int num ) -{ - if (TheWritableGlobalData) - { - TheWritableGlobalData->m_headless = TRUE; - } - return 1; -} - Int parseConstantDebug( char *args[], int num ) { if (TheWritableGlobalData) @@ -1273,9 +1267,6 @@ static CommandLineParam params[] = { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, - // TheSuperHackers @feature helmutbuhler 11/04/2025 - // This runs the game without a window, graphics, input and audio. Used for testing. - { "-headless", parseHeadless }, // TheSuperHackers @feature helmutbuhler 13/04/2025 // Simulate replay without graphics. Pass the filename including .rep afterwards. // You can pass this multiple times to check multiple replays. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 67b6aaae39c..df9d6d51fa2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -145,7 +145,7 @@ static int SimulateReplayListMultiProcess(const std::vector &filena UnicodeString filenameWide; filenameWide.translate(filenames[filenamePositionStarted]); UnicodeString command; - command.format(L"\"%s\" -headless -simReplay \"%s\"", exePath, filenameWide.str()); + command.format(L"\"%s\" -simReplay \"%s\"", exePath, filenameWide.str()); WorkerProcess p; p.StartProcess(command); diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 07aaf5bec75..0f06aab5447 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -901,8 +901,12 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, if (stricmp(token, "-win") == 0) ApplicationIsWindowed = true; - // preparse for headless as well. We need to know about this before we create the window. - if (stricmp(token, "-headless") == 0) + // preparse for headless commandline options as well. We need to know about this before we create the window. + if (stricmp(token, "-simReplay") == 0) + headless = true; + if (stricmp(token, "-simReplayList") == 0) + headless = true; + if (stricmp(token, "-writeReplayList") == 0) headless = true; token = nextParam(NULL, "\" "); From 208365afc734d6a61224e50646c0303cfff6988f Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 13:07:21 +0200 Subject: [PATCH 046/112] Remove obsolete headless checks --- .../Code/GameEngine/Source/GameClient/GameClient.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index 59e7dbd0cb5..c072e3d0d03 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -538,7 +538,7 @@ void GameClient::update( void ) //Initial Game Codition. We must show the movie first and then we can display the shell if(TheGlobalData->m_afterIntro && !TheDisplay->isMoviePlaying()) { - if( playSizzle && TheGlobalData->m_playSizzle && !TheGlobalData->m_headless )// Remove headless-check with Replay Simulation PR + if( playSizzle && TheGlobalData->m_playSizzle ) { TheWritableGlobalData->m_allowExitOutOfMovies = TRUE; if(TheGameLODManager && TheGameLODManager->didMemPass()) @@ -629,11 +629,8 @@ void GameClient::update( void ) if(TheGlobalData->m_playIntro || TheGlobalData->m_afterIntro) { // redraw all views, update the GUI - if (!TheGlobalData->m_headless)// Remove headless-check with Replay Simulation PR - { - TheDisplay->DRAW(); - TheDisplay->UPDATE(); - } + TheDisplay->DRAW(); + TheDisplay->UPDATE(); return; } @@ -751,12 +748,10 @@ void GameClient::update( void ) } // update display - if (!TheGlobalData->m_headless)// Remove headless-check with Replay Simulation PR { TheDisplay->UPDATE(); } - if (!TheGlobalData->m_headless)// Remove headless-check with Replay Simulation PR { USE_PERF_TIMER(GameClient_draw) From cc1c16388e8576ce0f21725346cccc2af5b74206 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 13:08:43 +0200 Subject: [PATCH 047/112] After using simulation button, return to scorescreen instead of Replay menu --- .../Source/GameLogic/System/GameLogicDispatch.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index 2edca575773..54beb39e390 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -269,16 +269,8 @@ void GameLogic::clearGameData( Bool showScoreScreen ) { shellGame = TRUE; TheTransitionHandler->setGroup("FadeWholeScreen"); - if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK) - { - // Get back directly to replay menu after simulation is complete - TheShell->showShell(TRUE); - } - else - { - TheShell->push("Menus/ScoreScreen.wnd"); - TheShell->showShell(FALSE); // by passing in false, we don't want to run the Init on the shell screen we just pushed on - } + TheShell->push("Menus/ScoreScreen.wnd"); + TheShell->showShell(FALSE); // by passing in false, we don't want to run the Init on the shell screen we just pushed on TheTransitionHandler->reverse("FadeWholeScreen"); void FixupScoreScreenMovieWindow( void ); From 889fd8e6db655042989502b468056fa0289f74aa Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 16:38:54 +0200 Subject: [PATCH 048/112] Remove obsolete headless check in GameLogic::startNewGame --- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index b29f05a27f7..af00e97b3d8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1269,7 +1269,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) //****************************// // Get the m_loadScreen for this kind of game - if(!m_loadScreen && !TheGlobalData->m_headless && + if(!m_loadScreen && !(TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK)) { m_loadScreen = getLoadScreen( loadingSaveGame ); From a969df5a3f5a54352dd4155ea967ce1de819b9b9 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 16:40:20 +0200 Subject: [PATCH 049/112] Remove Int mismatchFrame simplification in Recorder --- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 3a3b5334fff..1ba0e0074f4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1043,9 +1043,8 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f if (samePlayer || (localPlayerIndex < 0)) { UnsignedInt playbackCRC = m_crcInfo->readCRC(); - Int mismatchFrame = TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1; //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Comparing CRCs of InGame:%8.8X Replay:%8.8X Frame:%d from Player %d\n", - // playbackCRC, newCRC, mismatchFrame, playerIndex)); + // playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1, playerIndex)); if (TheGameLogic->getFrame() > 0 && newCRC != playbackCRC && !m_crcInfo->sawCRCMismatch()) { m_crcInfo->setSawCRCMismatch(); @@ -1064,11 +1063,11 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f // Note: We subtract the queue size from the frame no. This way we calculate the correct frame // the mismatch first happened in case the NetCRCInterval is set to 1 during the game. DEBUG_CRASH(("Replay has gone out of sync! All bets are off!\nInGame:%8.8X Replay:%8.8X\nFrame:%d", - playbackCRC, newCRC, mismatchFrame)); + playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1)); // TheSuperHackers @info helmutbuhler 13/04/2025 // Print Mismatch to console in case we are in SimulateReplayList - printf("CRC Mismatch in Frame %d\n", mismatchFrame); + printf("CRC Mismatch in Frame %d\n", TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1); // TheSuperHackers @tweak Pause the game on mismatch. TheGameLogic->setGamePaused(true); From cd802df08638594fd06a5f5ff4b189cadb976ef3 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 16:41:49 +0200 Subject: [PATCH 050/112] Remove -writeReplayList and -simReplayList commandline options (goes into other PR) --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 - .../GameEngine/Include/Common/GlobalData.h | 1 - .../GameEngine/Include/Common/ReplayListCsv.h | 23 -- .../GameEngine/Source/Common/CommandLine.cpp | 32 -- .../GameEngine/Source/Common/GameMain.cpp | 6 - .../GameEngine/Source/Common/GlobalData.cpp | 2 - .../Source/Common/ReplayListCsv.cpp | 282 ------------------ 7 files changed, 348 deletions(-) delete mode 100644 GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h delete mode 100644 GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index a45e990c8db..3ca6a1c9bfc 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -94,7 +94,6 @@ set(GAMEENGINE_SRC Include/Common/RAMFile.h Include/Common/RandomValue.h Include/Common/Recorder.h - Include/Common/ReplayListCsv.h Include/Common/ReplaySimulation.h Include/Common/Registry.h Include/Common/ResourceGatheringManager.h @@ -610,7 +609,6 @@ set(GAMEENGINE_SRC Source/Common/PerfTimer.cpp Source/Common/RandomValue.cpp Source/Common/Recorder.cpp - Source/Common/ReplayListCsv.cpp Source/Common/ReplaySimulation.cpp Source/Common/RTS/AcademyStats.cpp Source/Common/RTS/ActionManager.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 61fba518caa..376450a68b4 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -334,7 +334,6 @@ class GlobalData : public SubsystemInterface std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or -1 for sequential simulation - AsciiString m_writeReplayList; ///< If not empty, write out list of replays in this subfolder into a csv file (TheSuperHackers @feature helmutbuhler 24/05/2025) Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h deleted file mode 100644 index b0140b341e0..00000000000 --- a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -#pragma once - - -bool WriteOutReplayList(AsciiString relativeFolder); -void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 1703f38c2e3..7fdcbd49408 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -31,7 +31,6 @@ #include "Common/LocalFileSystem.h" #include "Common/version.h" #include "Common/Recorder.h" -#include "Common/ReplayListCsv.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -451,28 +450,6 @@ Int parseSimReplay(char *args[], int num) return 1; } -Int parseSimReplayList(char *args[], int num) -{ - if (TheWritableGlobalData && num > 1) - { - AsciiString filename = args[1]; - ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplayList); - TheWritableGlobalData->m_headless = TRUE; - return 2; - } - return 1; -} - -Int parseWriteReplayList(char *args[], int num) -{ - if (TheWritableGlobalData && num > 1) - { - TheWritableGlobalData->m_writeReplayList = args[1]; - TheWritableGlobalData->m_headless = TRUE; - } - return 1; -} - Int parseJobs(char *args[], int num) { if (TheWritableGlobalData && num > 1) @@ -1272,21 +1249,12 @@ static CommandLineParam params[] = // You can pass this multiple times to check multiple replays. { "-simReplay", parseSimReplay }, - // TheSuperHackers @feature helmutbuhler 28/04/2025 - // Pass in a csv file to simulate multiple replays. The file must be in the replay folder. - { "-simReplayList", parseSimReplayList }, - // TheSuperHackers @feature helmutbuhler 23/05/2025 // Simulate each replay in a separate process and use up two N processes at the same time. // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, - // TheSuperHackers @feature helmutbuhler 23/05/2025 - // Write replay list into a csv file. Append a subfolder of the replay folder or . for all - // replays in the replay folder. - { "-writeReplayList", parseWriteReplayList }, - #if (defined(RTS_DEBUG) || defined(RTS_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 7d73f6e68dd..ce35f4bd46c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -30,7 +30,6 @@ #include "Common/GameEngine.h" #include "Common/ReplaySimulation.h" -#include "Common/ReplayListCsv.h" /** @@ -47,11 +46,6 @@ Int GameMain( int argc, char *argv[] ) { exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList, TheGlobalData->m_simulateReplayJobs); } - else if (!TheGlobalData->m_writeReplayList.isEmpty()) - { - bool success = WriteOutReplayList(TheGlobalData->m_writeReplayList); - exitcode = success ? 0 : 1; - } else { // run it diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 304cc2705d8..cc88caf14e6 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -960,8 +960,6 @@ GlobalData::GlobalData() m_simulateReplayList.clear(); m_simulateReplayJobs = -1; - m_writeReplayList = ""; - for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp deleted file mode 100644 index 56f6889e37b..00000000000 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp +++ /dev/null @@ -1,282 +0,0 @@ -/* -** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine - -#include "Common/ReplayListCsv.h" -#include "Common/Recorder.h" -#include "Common/FileSystem.h" -#include "GameClient/MapUtil.h" - - -Bool GetReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader *headerOut, ReplayGameInfo *infoOut, const MapMetaData **mdOut) -{ - // lets get some info about the replay - RecorderClass::ReplayHeader header; - header.forPlayback = FALSE; - header.filename = filename; - Bool success = TheRecorder && TheMapCache && TheRecorder->readReplayHeader( header ); - if (!success) - return false; - - ReplayGameInfo info; - if (!ParseAsciiStringToGameInfo( &info, header.gameOptions )) - return false; - - header.replayName.translate(filename); - for (Int tmp=0; tmp < TheRecorder->getReplayExtention().getLength(); ++tmp) - header.replayName.removeLastChar(); - - if (headerOut) *headerOut = header; - if (infoOut) *infoOut = info; - if (mdOut) *mdOut = TheMapCache->findMap(info.getMap()); - return true; -} - -bool WriteOutReplayList(AsciiString relativeFolder) -{ - AsciiString dir; - dir.format("%s%s", TheRecorder->getReplayDir().str(), relativeFolder.str()); - if (!dir.endsWith("\\") && !dir.endsWith("/")) - dir.concat('/'); - AsciiString fname; - fname.format("%s/replay_list.csv", dir.str()); - FILE *fp = fopen(fname.str(), "wt"); - if (!fp) - return false; - - // Get list of replay filenames - AsciiString asciisearch; - asciisearch = "*"; - asciisearch.concat(TheRecorder->getReplayExtention()); - FilenameList replayFilenamesSet; - TheFileSystem->getFileListInDirectory(dir, asciisearch, replayFilenamesSet, FALSE); - std::vector replayFilenames; - for (FilenameListIter it = replayFilenamesSet.begin(); it != replayFilenamesSet.end(); ++it) - { - replayFilenames.push_back(*it); - } - - TheMapCache->updateCache(); - - std::set foundSeeds; - - // Print out a line per filename. i = -1 is csv header. - for (int i = -1; i < (int)replayFilenames.size(); i++) - { - AsciiString filename; - RecorderClass::ReplayHeader header; - ReplayGameInfo info; - const MapMetaData *md = NULL; - if (i != -1) - { - filename.set(replayFilenames[i].str() + TheRecorder->getReplayDir().getLength()); - Bool success = GetReplayMapInfo(filename, &header, &info, &md); - if (!success) - continue; - filename.set(replayFilenames[i].str() + dir.getLength()); - } - int numHumans = 0, numAIs = 0; - for (int slot = 0; slot < MAX_SLOTS; slot++) - { - SlotState state = info.getSlot(slot)->getState(); - numHumans += state == SLOT_PLAYER ? 1 : 0; - numAIs += state >= SLOT_EASY_AI && state <= SLOT_BRUTAL_AI ? 1 : 0; - } - - bool compatibleVersion = - header.versionString == UnicodeString(L"Version 1.4") || - header.versionString == UnicodeString(L"Version 1.04") || - header.versionString == UnicodeString(L"Version 1.05") || - header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.04") || - header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.05") || - header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.4") || - header.versionString == UnicodeString(L"Versi\x00F3n 1.04") || - header.versionString == UnicodeString(L"Versi\x00F3n 1.05"); - - // Some versions, e.g. "Zero Hour 1.04 The Ultimate Collection" have a different ini crc and are - // actually incompatible. Mark them as incompatible - compatibleVersion = compatibleVersion && - (header.iniCRC == 0xfeaae3f3 || header.iniCRC == 0xb859d2f9); - - // Check whether random seed appears multiple times. This can be used to check only one replay - // per game in case multiple replays by different players of the same game are in the list. - int seed = info.getSeed(); - bool uniqueSeed = foundSeeds.find(seed) == foundSeeds.end(); - if (uniqueSeed) - foundSeeds.insert(seed); - - // When a csv file is loaded with -simReplayList, check indicates whether the replay should be simulated. - // If you want to check replays with certain properties, you can change this expression - // or change the csv file manually. - bool check = md && !header.desyncGame && header.endTime != 0 && - compatibleVersion && uniqueSeed;// && numHumans > 1 && numAIs > 0; - fprintf(fp, "%s", i == -1 ? "check" : check ? "1" : "0"); - - if (i == -1) - fprintf(fp, ",filename"); - else - fprintf(fp, ",\"%s\"", filename.str()); - - fprintf(fp, ",%s", i == -1 ? "mapExists" : md ? "1" : "0"); - fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); - fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); - fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); - - UnsignedInt gameTime = header.frameDuration / LOGICFRAMES_PER_SECOND; - fprintf(fp, i == -1 ? ",time" : ",%02d:%02d", gameTime/60, gameTime%60); - - fprintf(fp, i == -1 ? ",numHumans" : ",%d", numHumans); - fprintf(fp, i == -1 ? ",numAIs" : ",%d", numAIs); - - AsciiString tmp; - tmp.translate(header.versionString); - if (i == -1) - fprintf(fp, ",version"); - else - fprintf(fp, ",\"%s\"", tmp.str()); - //fprintf(fp, i == -1 ? ",exeCRC" : ",0x%08x", header.exeCRC); - //fprintf(fp, i == -1 ? ",iniCRC" : ",0x%08x", header.iniCRC); - - fprintf(fp, ",%s", i == -1 ? "compatibleVersion" : compatibleVersion ? "1" : "0"); - - //fprintf(fp, i == -1 ? ",crcInterval" : ",%d", info.getCRCInterval()); - //fprintf(fp, i == -1 ? ",seed" : ",0x%08x", seed); - - fprintf(fp, "\n"); - -#if 0 - if (i != -1 && check) - { - AsciiString sourceFilename = replayFilenames[i]; - - AsciiString targetFilename; - targetFilename = TheRecorder->getReplayDir(); - targetFilename.concat("filter/"); - targetFilename.concat(filename); - - CopyFile(sourceFilename.str(), targetFilename.str(), FALSE); - } -#endif - } - fclose(fp); - return true; -} - -static bool ReadLineFromFile(FILE *fp, AsciiString *str) -{ - const int bufferSize = 128; - char buffer[bufferSize]; - str->clear(); - while (true) - { - if (fgets(buffer, bufferSize, fp) == NULL) - { - str->clear(); - return false; - } - buffer[bufferSize-1] = 0; // Should be already nul-terminated, just to be sure - str->concat(buffer); - if (strlen(buffer) != bufferSize-1 || buffer[bufferSize-2] == '\n') - break; - } - return true; -} - -static void NextToken(AsciiString *string, AsciiString *token, char separator) -{ - const char *tokenStart = string->str(); - - const char *str = tokenStart; - bool inQuotationMarks = false; - while (*str) - { - if (*str == separator && !inQuotationMarks) - break; - if (*str == '\"') - inQuotationMarks = !inQuotationMarks; - str++; - } - const char *tokenEnd = str; - - Int len = tokenEnd - tokenStart; - char *tmp = token->getBufferForRead(len + 1); - memcpy(tmp, tokenStart, len); - tmp[len] = 0; - token->trim(); - - string->set(*tokenEnd == 0 ? tokenEnd : tokenEnd+1); -} - -void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) -{ - // Get path of csv file relative to replay folder. - // Later we will search for replays in that path. - AsciiString relativeFolder = filename; - { - int len = relativeFolder.getLength(); - while (len) - { - char c = relativeFolder.getCharAt(len-1); - if (c == '/' || c == '\\') - break; - relativeFolder.removeLastChar(); - len--; - } - } - - AsciiString fname; - fname.format("%s%s", TheRecorder->getReplayDir().str(), filename.str()); - FILE *fp = fopen(fname.str(), "rt"); - if (!fp) - return; - - // Parse header - AsciiString line, token; - ReadLineFromFile(fp, &line); - char separator = line.find(';') == NULL ? ',' : ';'; - - while (feof(fp) == 0) - { - ReadLineFromFile(fp, &line); - - // Parse check - NextToken(&line, &token, separator); - if (token != "1") - continue; - - // Parse filename - NextToken(&line, &token, separator); - if (token.isEmpty()) - continue; - if (token.getCharAt(0) == '\"' && token.getCharAt(token.getLength()-1) == '\"') - { - token.set(token.str()+1); - token.removeLastChar(); - } - if (!token.isEmpty()) - { - AsciiString path; - path.format("%s%s", relativeFolder.str(), token.str()); - replayList->push_back(path); - } - - // Ignore remaining columns - } - fclose(fp); -} From f640caa0003e943eb5bfdbba62c2306e7e0def50 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 16:52:01 +0200 Subject: [PATCH 051/112] Remove "Debug: Simulate Replay"-button in replaymenu (goes into other PR) --- .../Code/GameEngine/Include/Common/Recorder.h | 3 +- .../GameEngine/Source/Common/Recorder.cpp | 7 +-- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 54 ++----------------- 3 files changed, 7 insertions(+), 57 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 1065e450bb5..4754b5bb2b8 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -74,9 +74,8 @@ class RecorderClass : public SubsystemInterface { UnsignedInt getFrameDuration() { return m_playbackFrameDuration; } ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. Bool simulateReplay(AsciiString filename); -#if 1 +#if defined RTS_DEBUG || defined RTS_INTERNAL Bool analyzeReplay( AsciiString filename ); - void stopAnalysis(); #endif Bool isPlaybackInProgress(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 1ba0e0074f4..33dd344836e 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -916,7 +916,7 @@ Bool RecorderClass::simulateReplay(AsciiString filename) return success; } -#if 1 +#if defined RTS_DEBUG || defined RTS_INTERNAL Bool RecorderClass::analyzeReplay( AsciiString filename ) { m_doingAnalysis = TRUE; @@ -924,10 +924,7 @@ Bool RecorderClass::analyzeReplay( AsciiString filename ) } -void RecorderClass::stopAnalysis() -{ - m_doingAnalysis = FALSE; -} + #endif Bool RecorderClass::isPlaybackInProgress( void ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 3c59a8b4c7a..d913856b660 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -37,7 +37,6 @@ #include "Common/GameState.h" #include "Common/Recorder.h" #include "Common/version.h" -#include "GameClient/GameClient.h" #include "GameClient/WindowLayout.h" #include "GameClient/Gadget.h" #include "GameClient/GadgetListBox.h" @@ -48,7 +47,6 @@ #include "GameClient/MapUtil.h" #include "GameClient/GameText.h" #include "GameClient/GameWindowTransitions.h" -#include "GameLogic/GameLogic.h" #ifdef RTS_INTERNAL // for occasional debugging... @@ -77,12 +75,8 @@ static Int initialGadgetDelay = 2; static Bool justEntered = FALSE; -#if 1 +#if defined RTS_DEBUG || defined RTS_INTERNAL static GameWindow *buttonAnalyzeReplay = NULL; - -// TheSuperHackers @feature helmutbuhler 13/04/2025 -// Button to simulate replay without graphics -static GameWindow *buttonSimulateReplay = NULL; #endif void deleteReplay( void ); @@ -300,26 +294,17 @@ void ReplayMenuInit( WindowLayout *layout, void *userData ) GadgetListBoxReset(listboxReplayFiles); PopulateReplayFileListbox(listboxReplayFiles); -#if 1 +#if defined RTS_DEBUG || defined RTS_INTERNAL WinInstanceData instData; instData.init(); BitSet( instData.m_style, GWS_PUSH_BUTTON | GWS_MOUSE_TRACK ); instData.m_textLabelString = "Debug: Analyze Replay"; - instData.setTooltipText(UnicodeString(L"Dump commands stored in selected replay into log")); + instData.setTooltipText(UnicodeString(L"Only Used in Debug and Internal!")); buttonAnalyzeReplay = TheWindowManager->gogoGadgetPushButton( parentReplayMenu, WIN_STATUS_ENABLED | WIN_STATUS_IMAGE, 4, 4, 180, 26, &instData, NULL, TRUE ); - - instData.m_id = 1; - instData.m_textLabelString = "Debug: Simulate Replay"; - instData.setTooltipText(UnicodeString(L"Playback selected replay without graphics. Will block game until replay simulation is done!")); - buttonSimulateReplay = TheWindowManager->gogoGadgetPushButton( parentReplayMenu, - WIN_STATUS_ENABLED | WIN_STATUS_IMAGE, - 4, 40, - 180, 26, - &instData, NULL, TRUE ); #endif // show menu @@ -530,7 +515,7 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, GameWindow *control = (GameWindow *)mData1; Int controlID = control->winGetWindowId(); -#if 1 +#if defined RTS_DEBUG || defined RTS_INTERNAL if( controlID == buttonAnalyzeReplay->winGetWindowId() ) { if(listboxReplayFiles) @@ -553,37 +538,6 @@ WindowMsgHandledType ReplayMenuSystem( GameWindow *window, UnsignedInt msg, { TheRecorder->update(); } while (TheRecorder->isPlaybackInProgress()); - TheRecorder->stopAnalysis(); - } - } - } - else if( controlID == buttonSimulateReplay->winGetWindowId() ) - { - if(listboxReplayFiles) - { - Int selected; - GadgetListBoxGetSelected( listboxReplayFiles, &selected ); - if(selected < 0) - { - MessageBoxOk(UnicodeString(L"Blah Blah"),UnicodeString(L"Please select something munkee girl"), NULL); - break; - } - - filename = GetReplayFilenameFromListbox(listboxReplayFiles, selected); - - AsciiString asciiFilename; - asciiFilename.translate(filename); - if (TheRecorder->simulateReplay(asciiFilename)) - { - do - { - TheGameClient->updateHeadless(); - TheGameLogic->UPDATE(); - if (TheRecorder->sawCRCMismatch()) - break; - } while (TheRecorder->isPlaybackInProgress()); - if (TheGameLogic->isInGame()) - TheGameLogic->clearGameData(); } } } From 12e00f772d81ce9cdebec07d2e73e0f4e8a9a3ea Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 16:58:36 +0200 Subject: [PATCH 052/112] Add -writeReplayList and -simReplayList commandline options --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/ReplayListCsv.h | 23 ++ .../GameEngine/Source/Common/CommandLine.cpp | 32 ++ .../GameEngine/Source/Common/GameMain.cpp | 6 + .../GameEngine/Source/Common/GlobalData.cpp | 2 + .../Source/Common/ReplayListCsv.cpp | 282 ++++++++++++++++++ 7 files changed, 348 insertions(+) create mode 100644 GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h create mode 100644 GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 3ca6a1c9bfc..a45e990c8db 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -94,6 +94,7 @@ set(GAMEENGINE_SRC Include/Common/RAMFile.h Include/Common/RandomValue.h Include/Common/Recorder.h + Include/Common/ReplayListCsv.h Include/Common/ReplaySimulation.h Include/Common/Registry.h Include/Common/ResourceGatheringManager.h @@ -609,6 +610,7 @@ set(GAMEENGINE_SRC Source/Common/PerfTimer.cpp Source/Common/RandomValue.cpp Source/Common/Recorder.cpp + Source/Common/ReplayListCsv.cpp Source/Common/ReplaySimulation.cpp Source/Common/RTS/AcademyStats.cpp Source/Common/RTS/ActionManager.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 376450a68b4..61fba518caa 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -334,6 +334,7 @@ class GlobalData : public SubsystemInterface std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or -1 for sequential simulation + AsciiString m_writeReplayList; ///< If not empty, write out list of replays in this subfolder into a csv file (TheSuperHackers @feature helmutbuhler 24/05/2025) Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h new file mode 100644 index 00000000000..b0140b341e0 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h @@ -0,0 +1,23 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + + +bool WriteOutReplayList(AsciiString relativeFolder); +void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 7fdcbd49408..1703f38c2e3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -31,6 +31,7 @@ #include "Common/LocalFileSystem.h" #include "Common/version.h" #include "Common/Recorder.h" +#include "Common/ReplayListCsv.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -450,6 +451,28 @@ Int parseSimReplay(char *args[], int num) return 1; } +Int parseSimReplayList(char *args[], int num) +{ + if (TheWritableGlobalData && num > 1) + { + AsciiString filename = args[1]; + ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplayList); + TheWritableGlobalData->m_headless = TRUE; + return 2; + } + return 1; +} + +Int parseWriteReplayList(char *args[], int num) +{ + if (TheWritableGlobalData && num > 1) + { + TheWritableGlobalData->m_writeReplayList = args[1]; + TheWritableGlobalData->m_headless = TRUE; + } + return 1; +} + Int parseJobs(char *args[], int num) { if (TheWritableGlobalData && num > 1) @@ -1249,12 +1272,21 @@ static CommandLineParam params[] = // You can pass this multiple times to check multiple replays. { "-simReplay", parseSimReplay }, + // TheSuperHackers @feature helmutbuhler 28/04/2025 + // Pass in a csv file to simulate multiple replays. The file must be in the replay folder. + { "-simReplayList", parseSimReplayList }, + // TheSuperHackers @feature helmutbuhler 23/05/2025 // Simulate each replay in a separate process and use up two N processes at the same time. // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, + // TheSuperHackers @feature helmutbuhler 23/05/2025 + // Write replay list into a csv file. Append a subfolder of the replay folder or . for all + // replays in the replay folder. + { "-writeReplayList", parseWriteReplayList }, + #if (defined(RTS_DEBUG) || defined(RTS_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index ce35f4bd46c..7d73f6e68dd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -30,6 +30,7 @@ #include "Common/GameEngine.h" #include "Common/ReplaySimulation.h" +#include "Common/ReplayListCsv.h" /** @@ -46,6 +47,11 @@ Int GameMain( int argc, char *argv[] ) { exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList, TheGlobalData->m_simulateReplayJobs); } + else if (!TheGlobalData->m_writeReplayList.isEmpty()) + { + bool success = WriteOutReplayList(TheGlobalData->m_writeReplayList); + exitcode = success ? 0 : 1; + } else { // run it diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index cc88caf14e6..304cc2705d8 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -960,6 +960,8 @@ GlobalData::GlobalData() m_simulateReplayList.clear(); m_simulateReplayJobs = -1; + m_writeReplayList = ""; + for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp new file mode 100644 index 00000000000..56f6889e37b --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp @@ -0,0 +1,282 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine + +#include "Common/ReplayListCsv.h" +#include "Common/Recorder.h" +#include "Common/FileSystem.h" +#include "GameClient/MapUtil.h" + + +Bool GetReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader *headerOut, ReplayGameInfo *infoOut, const MapMetaData **mdOut) +{ + // lets get some info about the replay + RecorderClass::ReplayHeader header; + header.forPlayback = FALSE; + header.filename = filename; + Bool success = TheRecorder && TheMapCache && TheRecorder->readReplayHeader( header ); + if (!success) + return false; + + ReplayGameInfo info; + if (!ParseAsciiStringToGameInfo( &info, header.gameOptions )) + return false; + + header.replayName.translate(filename); + for (Int tmp=0; tmp < TheRecorder->getReplayExtention().getLength(); ++tmp) + header.replayName.removeLastChar(); + + if (headerOut) *headerOut = header; + if (infoOut) *infoOut = info; + if (mdOut) *mdOut = TheMapCache->findMap(info.getMap()); + return true; +} + +bool WriteOutReplayList(AsciiString relativeFolder) +{ + AsciiString dir; + dir.format("%s%s", TheRecorder->getReplayDir().str(), relativeFolder.str()); + if (!dir.endsWith("\\") && !dir.endsWith("/")) + dir.concat('/'); + AsciiString fname; + fname.format("%s/replay_list.csv", dir.str()); + FILE *fp = fopen(fname.str(), "wt"); + if (!fp) + return false; + + // Get list of replay filenames + AsciiString asciisearch; + asciisearch = "*"; + asciisearch.concat(TheRecorder->getReplayExtention()); + FilenameList replayFilenamesSet; + TheFileSystem->getFileListInDirectory(dir, asciisearch, replayFilenamesSet, FALSE); + std::vector replayFilenames; + for (FilenameListIter it = replayFilenamesSet.begin(); it != replayFilenamesSet.end(); ++it) + { + replayFilenames.push_back(*it); + } + + TheMapCache->updateCache(); + + std::set foundSeeds; + + // Print out a line per filename. i = -1 is csv header. + for (int i = -1; i < (int)replayFilenames.size(); i++) + { + AsciiString filename; + RecorderClass::ReplayHeader header; + ReplayGameInfo info; + const MapMetaData *md = NULL; + if (i != -1) + { + filename.set(replayFilenames[i].str() + TheRecorder->getReplayDir().getLength()); + Bool success = GetReplayMapInfo(filename, &header, &info, &md); + if (!success) + continue; + filename.set(replayFilenames[i].str() + dir.getLength()); + } + int numHumans = 0, numAIs = 0; + for (int slot = 0; slot < MAX_SLOTS; slot++) + { + SlotState state = info.getSlot(slot)->getState(); + numHumans += state == SLOT_PLAYER ? 1 : 0; + numAIs += state >= SLOT_EASY_AI && state <= SLOT_BRUTAL_AI ? 1 : 0; + } + + bool compatibleVersion = + header.versionString == UnicodeString(L"Version 1.4") || + header.versionString == UnicodeString(L"Version 1.04") || + header.versionString == UnicodeString(L"Version 1.05") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.04") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.05") || + header.versionString == UnicodeString(L"\x0412\x0435\x0440\x0441\x0438\x044f 1.4") || + header.versionString == UnicodeString(L"Versi\x00F3n 1.04") || + header.versionString == UnicodeString(L"Versi\x00F3n 1.05"); + + // Some versions, e.g. "Zero Hour 1.04 The Ultimate Collection" have a different ini crc and are + // actually incompatible. Mark them as incompatible + compatibleVersion = compatibleVersion && + (header.iniCRC == 0xfeaae3f3 || header.iniCRC == 0xb859d2f9); + + // Check whether random seed appears multiple times. This can be used to check only one replay + // per game in case multiple replays by different players of the same game are in the list. + int seed = info.getSeed(); + bool uniqueSeed = foundSeeds.find(seed) == foundSeeds.end(); + if (uniqueSeed) + foundSeeds.insert(seed); + + // When a csv file is loaded with -simReplayList, check indicates whether the replay should be simulated. + // If you want to check replays with certain properties, you can change this expression + // or change the csv file manually. + bool check = md && !header.desyncGame && header.endTime != 0 && + compatibleVersion && uniqueSeed;// && numHumans > 1 && numAIs > 0; + fprintf(fp, "%s", i == -1 ? "check" : check ? "1" : "0"); + + if (i == -1) + fprintf(fp, ",filename"); + else + fprintf(fp, ",\"%s\"", filename.str()); + + fprintf(fp, ",%s", i == -1 ? "mapExists" : md ? "1" : "0"); + fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); + fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); + fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); + + UnsignedInt gameTime = header.frameDuration / LOGICFRAMES_PER_SECOND; + fprintf(fp, i == -1 ? ",time" : ",%02d:%02d", gameTime/60, gameTime%60); + + fprintf(fp, i == -1 ? ",numHumans" : ",%d", numHumans); + fprintf(fp, i == -1 ? ",numAIs" : ",%d", numAIs); + + AsciiString tmp; + tmp.translate(header.versionString); + if (i == -1) + fprintf(fp, ",version"); + else + fprintf(fp, ",\"%s\"", tmp.str()); + //fprintf(fp, i == -1 ? ",exeCRC" : ",0x%08x", header.exeCRC); + //fprintf(fp, i == -1 ? ",iniCRC" : ",0x%08x", header.iniCRC); + + fprintf(fp, ",%s", i == -1 ? "compatibleVersion" : compatibleVersion ? "1" : "0"); + + //fprintf(fp, i == -1 ? ",crcInterval" : ",%d", info.getCRCInterval()); + //fprintf(fp, i == -1 ? ",seed" : ",0x%08x", seed); + + fprintf(fp, "\n"); + +#if 0 + if (i != -1 && check) + { + AsciiString sourceFilename = replayFilenames[i]; + + AsciiString targetFilename; + targetFilename = TheRecorder->getReplayDir(); + targetFilename.concat("filter/"); + targetFilename.concat(filename); + + CopyFile(sourceFilename.str(), targetFilename.str(), FALSE); + } +#endif + } + fclose(fp); + return true; +} + +static bool ReadLineFromFile(FILE *fp, AsciiString *str) +{ + const int bufferSize = 128; + char buffer[bufferSize]; + str->clear(); + while (true) + { + if (fgets(buffer, bufferSize, fp) == NULL) + { + str->clear(); + return false; + } + buffer[bufferSize-1] = 0; // Should be already nul-terminated, just to be sure + str->concat(buffer); + if (strlen(buffer) != bufferSize-1 || buffer[bufferSize-2] == '\n') + break; + } + return true; +} + +static void NextToken(AsciiString *string, AsciiString *token, char separator) +{ + const char *tokenStart = string->str(); + + const char *str = tokenStart; + bool inQuotationMarks = false; + while (*str) + { + if (*str == separator && !inQuotationMarks) + break; + if (*str == '\"') + inQuotationMarks = !inQuotationMarks; + str++; + } + const char *tokenEnd = str; + + Int len = tokenEnd - tokenStart; + char *tmp = token->getBufferForRead(len + 1); + memcpy(tmp, tokenStart, len); + tmp[len] = 0; + token->trim(); + + string->set(*tokenEnd == 0 ? tokenEnd : tokenEnd+1); +} + +void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) +{ + // Get path of csv file relative to replay folder. + // Later we will search for replays in that path. + AsciiString relativeFolder = filename; + { + int len = relativeFolder.getLength(); + while (len) + { + char c = relativeFolder.getCharAt(len-1); + if (c == '/' || c == '\\') + break; + relativeFolder.removeLastChar(); + len--; + } + } + + AsciiString fname; + fname.format("%s%s", TheRecorder->getReplayDir().str(), filename.str()); + FILE *fp = fopen(fname.str(), "rt"); + if (!fp) + return; + + // Parse header + AsciiString line, token; + ReadLineFromFile(fp, &line); + char separator = line.find(';') == NULL ? ',' : ';'; + + while (feof(fp) == 0) + { + ReadLineFromFile(fp, &line); + + // Parse check + NextToken(&line, &token, separator); + if (token != "1") + continue; + + // Parse filename + NextToken(&line, &token, separator); + if (token.isEmpty()) + continue; + if (token.getCharAt(0) == '\"' && token.getCharAt(token.getLength()-1) == '\"') + { + token.set(token.str()+1); + token.removeLastChar(); + } + if (!token.isEmpty()) + { + AsciiString path; + path.format("%s%s", relativeFolder.str(), token.str()); + replayList->push_back(path); + } + + // Ignore remaining columns + } + fclose(fp); +} From e8b6be5ac6b2b4d36fd8e846d65be2641e6b71eb Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 24 May 2025 18:04:53 +0200 Subject: [PATCH 053/112] Remove unnecessary TheControlBar != NULL check --- .../GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index 54beb39e390..f9110044300 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -1992,7 +1992,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) } // end switch /**/ /// @todo: multiplayer semantics - if (currentlySelectedGroup && TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar != NULL && TheControlBar->getObserverLookAtPlayer() == thisPlayer /*&& !TheRecorder->isMultiplayer()*/) + if (currentlySelectedGroup && TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer /*&& !TheRecorder->isMultiplayer()*/) { const VecObjectID& selectedObjects = currentlySelectedGroup->getAllIDs(); TheInGameUI->deselectAllDrawables(); From 121954250afc7f9cadd94dfe253c288daacfcf37 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 5 Jun 2025 23:23:55 +0200 Subject: [PATCH 054/112] Remove refactoring out of RecorderClass::appendNextCommand() --- .../Code/GameEngine/Source/Common/Recorder.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 7055c13a8d2..e6d72791e85 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1330,6 +1330,16 @@ void RecorderClass::appendNextCommand() { } GameMessage *msg = newInstance(GameMessage)(type); + if (type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES || type == GameMessage::MSG_CLEAR_GAME_DATA) + { + } + else + { + if (!m_doingAnalysis) + { + TheCommandList->appendMessage(msg); + } + } #ifdef DEBUG_LOGGING AsciiString commandName = msg->getCommandAsAsciiString(); @@ -1401,11 +1411,13 @@ void RecorderClass::appendNextCommand() { } } - if (type != GameMessage::MSG_BEGIN_NETWORK_MESSAGES && type != GameMessage::MSG_CLEAR_GAME_DATA && !m_doingAnalysis) + if (type == GameMessage::MSG_CLEAR_GAME_DATA || type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES) { - TheCommandList->appendMessage(msg); + deleteInstance(msg); + msg = NULL; } - else + + if (m_doingAnalysis) { deleteInstance(msg); msg = NULL; From 4eb89881b66df899c14f2763534b23626da428bd Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 6 Jun 2025 17:20:49 +0200 Subject: [PATCH 055/112] Add stupid const --- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 8 ++++---- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 616316b761b..b0cbb1e72a3 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -75,13 +75,13 @@ class RecorderClass : public SubsystemInterface { Bool replayMatchesGameVersion(AsciiString filename); ///< Returns true if the playback is a valid playback file for this version. static Bool replayMatchesGameVersion(const ReplayHeader& header); ///< Returns true if the playback is a valid playback file for this version. AsciiString getCurrentReplayFilename( void ); ///< valid during playback only - UnsignedInt getFrameDuration() { return m_playbackFrameDuration; } ///< valid during playback only + UnsignedInt getFrameDuration() const { return m_playbackFrameDuration; } ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. Bool simulateReplay(AsciiString filename); #if defined RTS_DEBUG || defined RTS_INTERNAL Bool analyzeReplay( AsciiString filename ); #endif - Bool isPlaybackInProgress(); + Bool isPlaybackInProgress() const; public: void handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback); @@ -113,7 +113,7 @@ class RecorderClass : public SubsystemInterface { Bool readReplayHeader( ReplayHeader& header ); RecorderModeType getMode(); ///< Returns the current operating mode. - Bool isPlaybackMode() { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; } + Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; } void initControls(); ///< Show or Hide the Replay controls AsciiString getReplayDir(); ///< Returns the directory that holds the replay files. @@ -128,7 +128,7 @@ class RecorderClass : public SubsystemInterface { void logPlayerDisconnect(UnicodeString player, Int slot); void logCRCMismatch( void ); - Bool sawCRCMismatch(); + Bool sawCRCMismatch() const; void cleanUpReplayFile( void ); ///< after a crash, send replay/debug info to a central repository void stopRecording(); ///< Stop recording and close m_file. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index e6d72791e85..e3470853371 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -936,7 +936,7 @@ Bool RecorderClass::analyzeReplay( AsciiString filename ) #endif -Bool RecorderClass::isPlaybackInProgress( void ) +Bool RecorderClass::isPlaybackInProgress( void ) const { return isPlaybackMode() && m_nextFrame != -1; } @@ -977,7 +977,7 @@ class CRCInfo UnsignedInt getLocalPlayer(void) { return m_localPlayer; } void setSawCRCMismatch(void) { m_sawCRCMismatch = TRUE; } - Bool sawCRCMismatch(void) { return m_sawCRCMismatch; } + Bool sawCRCMismatch(void) const { return m_sawCRCMismatch; } protected: @@ -1025,7 +1025,7 @@ UnsignedInt CRCInfo::readCRC(void) return val; } -Bool RecorderClass::sawCRCMismatch() +Bool RecorderClass::sawCRCMismatch() const { return m_crcInfo->sawCRCMismatch(); } From 03c530b50f9d3452f8a971794c25b5a1c7da568e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 6 Jun 2025 17:27:19 +0200 Subject: [PATCH 056/112] Rename Replay Simulation stuff --- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 2 +- .../Code/GameEngine/Include/Common/ReplaySimulation.h | 2 +- .../Code/GameEngine/Source/Common/CommandLine.cpp | 2 +- GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp | 4 ++-- .../Code/GameEngine/Source/Common/GlobalData.cpp | 2 +- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- .../Code/GameEngine/Source/Common/ReplaySimulation.cpp | 10 +++++----- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 376450a68b4..88e6312b701 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -332,7 +332,7 @@ class GlobalData : public SubsystemInterface AsciiString m_initialFile; ///< If this is specified, load a specific map/replay from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start - std::vector m_simulateReplayList; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) + std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or -1 for sequential simulation Int m_maxParticleCount; ///< maximum number of particles that can exist diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h index dcb2d488272..0ef27f9236f 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h @@ -22,4 +22,4 @@ // Simulate a list of replays without graphics. // Returns exitcode 1 if mismatch or other error occured // Returns exitcode 0 if all replays were successfully simulated without mismatches -int SimulateReplayList(const std::vector &filenames, int maxProcesses); +int SimulateReplays(const std::vector &filenames, int maxProcesses); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 7fdcbd49408..527c7aa99c8 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -443,7 +443,7 @@ Int parseSimReplay(char *args[], int num) { AsciiString filename = args[1]; ConvertShortReplayPathToLongReplayPath(filename); - TheWritableGlobalData->m_simulateReplayList.push_back(filename); + TheWritableGlobalData->m_simulateReplays.push_back(filename); TheWritableGlobalData->m_headless = TRUE; return 2; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index ce35f4bd46c..2b09f5cebbd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -42,9 +42,9 @@ Int GameMain( int argc, char *argv[] ) TheGameEngine = CreateGameEngine(); TheGameEngine->init(argc, argv); - if (!TheGlobalData->m_simulateReplayList.empty()) + if (!TheGlobalData->m_simulateReplays.empty()) { - exitcode = SimulateReplayList(TheGlobalData->m_simulateReplayList, TheGlobalData->m_simulateReplayJobs); + exitcode = SimulateReplays(TheGlobalData->m_simulateReplays, TheGlobalData->m_simulateReplayJobs); } else { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 24445b3e880..b636b799d53 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -957,7 +957,7 @@ GlobalData::GlobalData() m_initialFile.clear(); m_pendingFile.clear(); - m_simulateReplayList.clear(); + m_simulateReplays.clear(); m_simulateReplayJobs = -1; for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index e3470853371..76ff8a039da 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1072,7 +1072,7 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1)); // TheSuperHackers @info helmutbuhler 13/04/2025 - // Print Mismatch to console in case we are in SimulateReplayList + // Print Mismatch to console in case we are in SimulateReplays printf("CRC Mismatch in Frame %d\n", TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1); // TheSuperHackers @tweak Pause the game on mismatch. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index df9d6d51fa2..9e3d8005c09 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -23,7 +23,7 @@ #include "GameLogic/GameLogic.h" #include "GameClient/GameClient.h" -static int SimulateReplayListSingleProcess(const std::vector &filenames) +static int SimulateReplaysInThisProcess(const std::vector &filenames) { // Note that we use printf here because this is run from cmd. int numErrors = 0; @@ -97,7 +97,7 @@ static int SimulateReplayListSingleProcess(const std::vector &filen return numErrors != 0 ? 1 : 0; } -static int SimulateReplayListMultiProcess(const std::vector &filenames, int maxProcesses) +static int SimulateReplaysInWorkerProcesses(const std::vector &filenames, int maxProcesses) { DWORD totalStartTime = GetTickCount(); @@ -177,10 +177,10 @@ static int SimulateReplayListMultiProcess(const std::vector &filena return numErrors != 0 ? 1 : 0; } -int SimulateReplayList(const std::vector &filenames, int maxProcesses) +int SimulateReplays(const std::vector &filenames, int maxProcesses) { if (maxProcesses == -1) - return SimulateReplayListSingleProcess(filenames); + return SimulateReplaysInThisProcess(filenames); else - return SimulateReplayListMultiProcess(filenames, maxProcesses); + return SimulateReplaysInWorkerProcesses(filenames, maxProcesses); } From 928c6243da4bde839ee25df1d11b426bf4f23360 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 6 Jun 2025 17:40:30 +0200 Subject: [PATCH 057/112] Fix minor syntax stuff --- .../GameEngine/Include/GameClient/GameClient.h | 3 ++- .../GameEngine/Source/Common/CommandLine.cpp | 17 ++++++----------- .../Code/GameEngine/Source/Common/Recorder.cpp | 3 +-- .../Source/GameLogic/System/GameLogic.cpp | 3 +-- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h index 0b4124ae19c..a6f60f36bf6 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h @@ -94,12 +94,13 @@ class GameClient : public SubsystemInterface, // subsystem methods virtual void init( void ); ///< Initialize resources virtual void update( void ); ///< Updates the GUI, display, audio, etc - void updateHeadless(); virtual void reset( void ); ///< reset system virtual void setFrame( UnsignedInt frame ) { m_frame = frame; } ///< Set the GameClient's internal frame number virtual void registerDrawable( Drawable *draw ); ///< Given a drawable, register it with the GameClient and give it a unique ID + void updateHeadless(); + void addDrawableToLookupTable( Drawable *draw ); ///< add drawable ID to hash lookup table void removeDrawableFromLookupTable( Drawable *draw ); ///< remove drawable ID from hash lookup table diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 527c7aa99c8..cfd0b622e87 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -29,8 +29,8 @@ #include "Common/CommandLine.h" #include "Common/CRCDebug.h" #include "Common/LocalFileSystem.h" -#include "Common/version.h" #include "Common/Recorder.h" +#include "Common/version.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -111,15 +111,6 @@ static void ConvertShortMapPathToLongMapPath(AsciiString &mapName) mapName = actualpath; } -static void ConvertShortReplayPathToLongReplayPath(AsciiString &filename) -{ - if (!filename.endsWithNoCase(RecorderClass::getReplayExtention())) - { - DEBUG_CRASH(("Invalid replay name %s", filename.str())); - exit(1); - } -} - //============================================================================= //============================================================================= Int parseNoLogOrCrash(char *args[], int) @@ -442,7 +433,11 @@ Int parseSimReplay(char *args[], int num) if (TheWritableGlobalData && num > 1) { AsciiString filename = args[1]; - ConvertShortReplayPathToLongReplayPath(filename); + if (!filename.endsWithNoCase(RecorderClass::getReplayExtention())) + { + DEBUG_CRASH(("Invalid replay name \"%s\"", filename.str())); + exit(1); + } TheWritableGlobalData->m_simulateReplays.push_back(filename); TheWritableGlobalData->m_headless = TRUE; return 2; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 76ff8a039da..7036ec4cee3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1071,8 +1071,7 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f DEBUG_CRASH(("Replay has gone out of sync! All bets are off!\nInGame:%8.8X Replay:%8.8X\nFrame:%d", playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1)); - // TheSuperHackers @info helmutbuhler 13/04/2025 - // Print Mismatch to console in case we are in SimulateReplays + // Print Mismatch in case we are simulating replays from console. printf("CRC Mismatch in Frame %d\n", TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1); // TheSuperHackers @tweak Pause the game on mismatch. diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index d4701c323c3..2af586894cf 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1289,8 +1289,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) //****************************// // Get the m_loadScreen for this kind of game - if(!m_loadScreen && - !(TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK)) + if(!m_loadScreen && !(TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK)) { m_loadScreen = getLoadScreen( loadingSaveGame ); if(m_loadScreen) From a872b9377d4451109328fcedc6f7ef0ccd417fa9 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 6 Jun 2025 17:41:06 +0200 Subject: [PATCH 058/112] Use TheCommandList in RecorderClass::stopPlayback() for consistency --- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 7036ec4cee3..4fdc4a0083e 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -477,11 +477,11 @@ void RecorderClass::stopPlayback() { m_file = NULL; } m_fileName.clear(); - // Don't clear the game data if the replay is over - let things continue -//#ifdef DEBUG_CRC + if (!m_doingAnalysis) - TheMessageStream->appendMessage(GameMessage::MSG_CLEAR_GAME_DATA); -//#endif + { + TheCommandList->appendMessage(newInstance(GameMessage)(GameMessage::MSG_CLEAR_GAME_DATA)); + } } /** From c9378134a70571a167996d77c8397fccd807347d Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 6 Jun 2025 18:01:18 +0200 Subject: [PATCH 059/112] Rename Replay Header member FrameDuration to FrameCount --- .../Code/GameEngine/Include/Common/Recorder.h | 6 +++--- .../GameEngine/Source/Common/Recorder.cpp | 20 +++++++++---------- .../Source/Common/ReplaySimulation.cpp | 2 +- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index b0cbb1e72a3..db60f65c4db 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -75,7 +75,7 @@ class RecorderClass : public SubsystemInterface { Bool replayMatchesGameVersion(AsciiString filename); ///< Returns true if the playback is a valid playback file for this version. static Bool replayMatchesGameVersion(const ReplayHeader& header); ///< Returns true if the playback is a valid playback file for this version. AsciiString getCurrentReplayFilename( void ); ///< valid during playback only - UnsignedInt getFrameDuration() const { return m_playbackFrameDuration; } ///< valid during playback only + UnsignedInt getPlaybackFrameCount() const { return m_playbackFrameCount; } ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. Bool simulateReplay(AsciiString filename); #if defined RTS_DEBUG || defined RTS_INTERNAL @@ -103,7 +103,7 @@ class RecorderClass : public SubsystemInterface { UnsignedInt iniCRC; time_t startTime; time_t endTime; - UnsignedInt frameDuration; + UnsignedInt frameCount; Bool quitEarly; Bool desyncGame; Bool playerDiscons[MAX_SLOTS]; @@ -159,7 +159,7 @@ class RecorderClass : public SubsystemInterface { Int m_currentFilePosition; RecorderModeType m_mode; AsciiString m_currentReplayFilename; ///< valid during playback only - UnsignedInt m_playbackFrameDuration; + UnsignedInt m_playbackFrameCount; ReplayGameInfo m_gameInfo; Bool m_wasDesync; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 4fdc4a0083e..360e1ed70ed 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -67,8 +67,8 @@ typedef int32_t replay_time_t; static time_t startTime; static const UnsignedInt startTimeOffset = 6; static const UnsignedInt endTimeOffset = startTimeOffset + sizeof(replay_time_t); -static const UnsignedInt framesOffset = endTimeOffset + sizeof(replay_time_t); -static const UnsignedInt desyncOffset = framesOffset + sizeof(UnsignedInt); +static const UnsignedInt frameCountOffset = endTimeOffset + sizeof(replay_time_t); +static const UnsignedInt desyncOffset = frameCountOffset + sizeof(UnsignedInt); static const UnsignedInt quitEarlyOffset = desyncOffset + sizeof(Bool); static const UnsignedInt disconOffset = quitEarlyOffset + sizeof(Bool); @@ -232,7 +232,7 @@ void RecorderClass::logGameEnd( void ) time_t t; time(&t); - UnsignedInt duration = TheGameLogic->getFrame(); + UnsignedInt frameCount = TheGameLogic->getFrame(); UnsignedInt fileSize = ftell(m_file); // move to appropriate offset if (!fseek(m_file, endTimeOffset, SEEK_SET)) @@ -242,10 +242,10 @@ void RecorderClass::logGameEnd( void ) fwrite(&tmp, sizeof(replay_time_t), 1, m_file); } // move to appropriate offset - if (!fseek(m_file, framesOffset, SEEK_SET)) + if (!fseek(m_file, frameCountOffset, SEEK_SET)) { - // save off duration - fwrite(&duration, sizeof(UnsignedInt), 1, m_file); + // save off frameCount + fwrite(&frameCount, sizeof(UnsignedInt), 1, m_file); } // move back to end of stream #ifdef DEBUG_CRASHING @@ -272,7 +272,7 @@ void RecorderClass::logGameEnd( void ) if (logFP) { struct tm *t2 = localtime(&t); - duration = t - startTime; + time_t duration = t - startTime; Int minutes = duration/60; Int seconds = duration%60; fprintf(logFP, "Game end at %s(%d:%2.2d elapsed time)\n", asctime(t2), minutes, seconds); @@ -408,7 +408,7 @@ void RecorderClass::init() { m_gameInfo.setSeed(GetGameLogicRandomSeed()); m_wasDesync = FALSE; m_doingAnalysis = FALSE; - m_playbackFrameDuration = 0; + m_playbackFrameCount = 0; } /** @@ -854,7 +854,7 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header) fread(&tmp, sizeof(replay_time_t), 1, m_file); header.endTime = tmp; - fread(&header.frameDuration, sizeof(UnsignedInt), 1, m_file); + fread(&header.frameCount, sizeof(UnsignedInt), 1, m_file); fread(&header.desyncGame, sizeof(Bool), 1, m_file); fread(&header.quitEarly, sizeof(Bool), 1, m_file); @@ -1244,7 +1244,7 @@ Bool RecorderClass::playbackFile(AsciiString filename) } m_currentReplayFilename = filename; - m_playbackFrameDuration = header.frameDuration; + m_playbackFrameCount = header.frameCount; return TRUE; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 9e3d8005c09..bd7d43a67d1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -36,7 +36,7 @@ static int SimulateReplaysInThisProcess(const std::vector &filename DWORD startTime = GetTickCount(); if (TheRecorder->simulateReplay(filename)) { - UnsignedInt totalTime = TheRecorder->getFrameDuration() / LOGICFRAMES_PER_SECOND; + UnsignedInt totalTime = TheRecorder->getPlaybackFrameCount() / LOGICFRAMES_PER_SECOND; while (TheRecorder->isPlaybackInProgress()) { TheGameClient->updateHeadless(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 388d8ea79fc..4cec955318b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -199,7 +199,7 @@ void PopulateReplayFileListbox(GameWindow *listbox) // time_t totalSeconds = header.endTime - header.startTime; // Int mins = totalSeconds/60; // Int secs = totalSeconds%60; -// Real fps = header.frameDuration/totalSeconds; +// Real fps = header.frameCount/totalSeconds; // extraStr.format(L"%d:%d (%g fps) %hs", mins, secs, fps, header.desyncGame?"OOS ":""); // // for (Int i=0; i Date: Fri, 6 Jun 2025 18:22:30 +0200 Subject: [PATCH 060/112] Make methods of WorkerProcess lowercase --- .../GameEngine/Include/Common/WorkerProcess.h | 10 +++++----- .../Source/Common/ReplaySimulation.cpp | 8 ++++---- .../GameEngine/Source/Common/WorkerProcess.cpp | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h index 9701116b08a..28817ac4903 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h @@ -26,17 +26,17 @@ class WorkerProcess public: WorkerProcess(); - bool StartProcess(UnicodeString command); + bool startProcess(UnicodeString command); - bool IsRunning() const; + bool isRunning() const; // returns true iff the process exited. // Sets the parameters if return value is true. - bool IsDone(DWORD *exitcode, AsciiString *stdOutput) const; - void Update(); + bool isDone(DWORD *exitcode, AsciiString *stdOutput) const; + void update(); // Terminate Process if it's running - void Kill(); + void kill(); private: HANDLE m_processHandle; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index bd7d43a67d1..d460e26db81 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -113,14 +113,14 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file { int i; for (i = 0; i < processes.size(); i++) - processes[i].Update(); + processes[i].update(); // Get result of finished processes and print output in order while (processes.size() != 0) { DWORD exitcode; AsciiString stdOutput; - if (!processes[0].IsDone(&exitcode, &stdOutput)) + if (!processes[0].isDone(&exitcode, &stdOutput)) break; printf("%d/%d %s", filenamePositionDone+1, filenames.size(), stdOutput.str()); if (exitcode != 0) @@ -135,7 +135,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file int numProcessesRunning = 0; for (i = 0; i < processes.size(); i++) { - if (processes[i].IsRunning()) + if (processes[i].isRunning()) numProcessesRunning++; } @@ -148,7 +148,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file command.format(L"\"%s\" -simReplay \"%s\"", exePath, filenameWide.str()); WorkerProcess p; - p.StartProcess(command); + p.startProcess(command); processes.push_back(p); filenamePositionStarted++; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp index 8c267761a36..441fb193d67 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp @@ -74,9 +74,9 @@ WorkerProcess::WorkerProcess() m_isDone = false; } -bool WorkerProcess::StartProcess(UnicodeString command) +bool WorkerProcess::startProcess(UnicodeString command) { - m_stdOutput = ""; + m_stdOutput.clear(); m_isDone = false; // Create pipe for reading console output @@ -122,21 +122,21 @@ bool WorkerProcess::StartProcess(UnicodeString command) return true; } -bool WorkerProcess::IsRunning() const +bool WorkerProcess::isRunning() const { return m_processHandle != NULL; } -bool WorkerProcess::IsDone(DWORD *exitcode, AsciiString *stdOutput) const +bool WorkerProcess::isDone(DWORD *exitcode, AsciiString *stdOutput) const { *exitcode = m_exitcode; *stdOutput = m_stdOutput; return m_isDone; } -void WorkerProcess::Update() +void WorkerProcess::update() { - if (!IsRunning()) + if (!isRunning()) return; while (true) @@ -182,9 +182,9 @@ void WorkerProcess::Update() m_isDone = true; } -void WorkerProcess::Kill() +void WorkerProcess::kill() { - if (!IsRunning()) + if (!isRunning()) return; if (m_processHandle != NULL) @@ -206,7 +206,7 @@ void WorkerProcess::Kill() m_jobHandle = NULL; } - m_stdOutput = ""; + m_stdOutput.clear(); m_isDone = false; } From 8208f9d5a1b8d2ddf3d54a8ee6da3cce32493e95 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 13:36:34 +0200 Subject: [PATCH 061/112] Create helper method fetchStdOutput() in WorkerProcess --- .../GameEngine/Include/Common/WorkerProcess.h | 5 ++++ .../Source/Common/WorkerProcess.cpp | 23 +++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h index 28817ac4903..8f4cc535ba6 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h @@ -38,6 +38,11 @@ class WorkerProcess // Terminate Process if it's running void kill(); +private: + // returns true if all output has been received + // returns false if the worker is still running + bool fetchStdOutput(); + private: HANDLE m_processHandle; HANDLE m_readHandle; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp index 441fb193d67..abfe338bf15 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp @@ -134,29 +134,26 @@ bool WorkerProcess::isDone(DWORD *exitcode, AsciiString *stdOutput) const return m_isDone; } -void WorkerProcess::update() +bool WorkerProcess::fetchStdOutput() { - if (!isRunning()) - return; - while (true) { // Call PeekNamedPipe to make sure ReadFile won't block DWORD bytesAvailable = 0; BOOL success = PeekNamedPipe(m_readHandle, NULL, 0, NULL, &bytesAvailable, NULL); if (!success) - break; + return true; if (bytesAvailable == 0) { // Child process is still running and we have all output so far - return; + return false; } DWORD readBytes = 0; char buffer[1024]; success = ReadFile(m_readHandle, buffer, 1024-1, &readBytes, NULL); if (!success) - break; + return true; DEBUG_ASSERTCRASH(readBytes != 0, ("expected readBytes to be non null")); // Remove \r, otherwise each new line is doubled when we output it again @@ -166,6 +163,18 @@ void WorkerProcess::update() buffer[readBytes] = 0; m_stdOutput.concat(buffer); } +} + +void WorkerProcess::update() +{ + if (!isRunning()) + return; + + if (!fetchStdOutput()) + { + // There is still potential output pending + return; + } // Pipe broke, that means the process already exited. But we call this just to make sure WaitForSingleObject(m_processHandle, INFINITE); From a3c07e9a8a64c60f279d810da70f64dca1152c2b Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 13:42:26 +0200 Subject: [PATCH 062/112] Some minor changes --- GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp | 1 - GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index d460e26db81..666fe8f1bd1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -40,7 +40,6 @@ static int SimulateReplaysInThisProcess(const std::vector &filename while (TheRecorder->isPlaybackInProgress()) { TheGameClient->updateHeadless(); - //TheParticleSystemManager->reset(); if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*LOGICFRAMES_PER_SECOND) == 0) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp index abfe338bf15..560ed60e36e 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp @@ -151,7 +151,7 @@ bool WorkerProcess::fetchStdOutput() DWORD readBytes = 0; char buffer[1024]; - success = ReadFile(m_readHandle, buffer, 1024-1, &readBytes, NULL); + success = ReadFile(m_readHandle, buffer, ARRAY_SIZE(buffer)-1, &readBytes, NULL); if (!success) return true; DEBUG_ASSERTCRASH(readBytes != 0, ("expected readBytes to be non null")); From 1b715d21078a939b1d1681af0e5e64348fefdf17 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 13:43:29 +0200 Subject: [PATCH 063/112] "-simReplayList" checks belong in other PR --- GeneralsMD/Code/Main/WinMain.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 705e92c024b..b545c8efdaf 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -875,10 +875,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // preparse for headless commandline options as well. We need to know about this before we create the window. if (stricmp(token, "-simReplay") == 0) headless = true; - if (stricmp(token, "-simReplayList") == 0) - headless = true; - if (stricmp(token, "-writeReplayList") == 0) - headless = true; token = nextParam(NULL, "\" "); } From 115b153e604293d0d153a0bddf3a4f896a17bf1c Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 14:22:50 +0200 Subject: [PATCH 064/112] Remove out parameters in WorkerProcess::isDone --- .../Code/GameEngine/Include/Common/WorkerProcess.h | 9 ++++++--- .../GameEngine/Source/Common/ReplaySimulation.cpp | 6 +++--- .../GameEngine/Source/Common/WorkerProcess.cpp | 14 +++++++++++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h index 8f4cc535ba6..0a0b104f64b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h @@ -27,13 +27,16 @@ class WorkerProcess WorkerProcess(); bool startProcess(UnicodeString command); + + void update(); bool isRunning() const; // returns true iff the process exited. - // Sets the parameters if return value is true. - bool isDone(DWORD *exitcode, AsciiString *stdOutput) const; - void update(); + bool isDone() const; + + DWORD getExitCode() const; + AsciiString getStdOutput() const; // Terminate Process if it's running void kill(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 666fe8f1bd1..6b85fa6b293 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -117,11 +117,11 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file // Get result of finished processes and print output in order while (processes.size() != 0) { - DWORD exitcode; - AsciiString stdOutput; - if (!processes[0].isDone(&exitcode, &stdOutput)) + if (!processes[0].isDone()) break; + AsciiString stdOutput = processes[0].getStdOutput(); printf("%d/%d %s", filenamePositionDone+1, filenames.size(), stdOutput.str()); + DWORD exitcode = processes[0].getExitCode(); if (exitcode != 0) printf("Error!\n"); fflush(stdout); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp index 560ed60e36e..df8d238971f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp @@ -127,13 +127,21 @@ bool WorkerProcess::isRunning() const return m_processHandle != NULL; } -bool WorkerProcess::isDone(DWORD *exitcode, AsciiString *stdOutput) const +bool WorkerProcess::isDone() const { - *exitcode = m_exitcode; - *stdOutput = m_stdOutput; return m_isDone; } +DWORD WorkerProcess::getExitCode() const +{ + return m_exitcode; +} + +AsciiString WorkerProcess::getStdOutput() const +{ + return m_stdOutput; +} + bool WorkerProcess::fetchStdOutput() { while (true) From c2d58acfb16f30fc1271926f601d9b83e5e68079 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 14:36:03 +0200 Subject: [PATCH 065/112] variable renaming --- .../Source/Common/ReplaySimulation.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 6b85fa6b293..786b925bab4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -36,18 +36,19 @@ static int SimulateReplaysInThisProcess(const std::vector &filename DWORD startTime = GetTickCount(); if (TheRecorder->simulateReplay(filename)) { - UnsignedInt totalTime = TheRecorder->getPlaybackFrameCount() / LOGICFRAMES_PER_SECOND; + UnsignedInt totalTimeSec = TheRecorder->getPlaybackFrameCount() / LOGICFRAMES_PER_SECOND; while (TheRecorder->isPlaybackInProgress()) { TheGameClient->updateHeadless(); - if (TheGameLogic->getFrame() && TheGameLogic->getFrame() % (600*LOGICFRAMES_PER_SECOND) == 0) + const int progressFrameInterval = 10*60*LOGICFRAMES_PER_SECOND; + if (TheGameLogic->getFrame() != 0 && TheGameLogic->getFrame() % progressFrameInterval == 0) { // Print progress report - UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; + UnsignedInt realTimeSec = (GetTickCount()-startTime) / 1000; printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", - realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); + realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60); fflush(stdout); } TheGameLogic->UPDATE(); @@ -57,10 +58,10 @@ static int SimulateReplaysInThisProcess(const std::vector &filename break; } } - UnsignedInt gameTime = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTime = (GetTickCount()-startTime) / 1000; + UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; + UnsignedInt realTimeSec = (GetTickCount()-startTime) / 1000; printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", - realTime/60, realTime%60, gameTime/60, gameTime%60, totalTime/60, totalTime%60); + realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60); fflush(stdout); } else From 283e5c6e822f1aaee9228a6fe35f09c9adb5fc4c Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 14:37:34 +0200 Subject: [PATCH 066/112] More renaming --- .../GameEngine/Source/Common/ReplaySimulation.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 786b925bab4..a17b4b4b168 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -27,13 +27,13 @@ static int SimulateReplaysInThisProcess(const std::vector &filename { // Note that we use printf here because this is run from cmd. int numErrors = 0; - DWORD totalStartTime = GetTickCount(); + DWORD totalStartTimeMillis = GetTickCount(); for (size_t i = 0; i < filenames.size(); i++) { AsciiString filename = filenames[i]; printf("Simulating Replay \"%s\"\n", filename.str()); fflush(stdout); - DWORD startTime = GetTickCount(); + DWORD startTimeMillis = GetTickCount(); if (TheRecorder->simulateReplay(filename)) { UnsignedInt totalTimeSec = TheRecorder->getPlaybackFrameCount() / LOGICFRAMES_PER_SECOND; @@ -46,7 +46,7 @@ static int SimulateReplaysInThisProcess(const std::vector &filename { // Print progress report UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTimeSec = (GetTickCount()-startTime) / 1000; + UnsignedInt realTimeSec = (GetTickCount()-startTimeMillis) / 1000; printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60); fflush(stdout); @@ -59,7 +59,7 @@ static int SimulateReplaysInThisProcess(const std::vector &filename } } UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND; - UnsignedInt realTimeSec = (GetTickCount()-startTime) / 1000; + UnsignedInt realTimeSec = (GetTickCount()-startTimeMillis) / 1000; printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n", realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60); fflush(stdout); @@ -77,7 +77,7 @@ static int SimulateReplaysInThisProcess(const std::vector &filename else printf("Successfully simulated all replays\n"); - UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; + UnsignedInt realTime = (GetTickCount()-totalStartTimeMillis) / 1000; printf("Total Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); fflush(stdout); } @@ -99,7 +99,7 @@ static int SimulateReplaysInThisProcess(const std::vector &filename static int SimulateReplaysInWorkerProcesses(const std::vector &filenames, int maxProcesses) { - DWORD totalStartTime = GetTickCount(); + DWORD totalStartTimeMillis = GetTickCount(); WideChar exePath[1024]; GetModuleFileNameW(NULL, exePath, 1024); @@ -170,7 +170,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file else printf("Successfully simulated all replays\n"); - UnsignedInt realTime = (GetTickCount()-totalStartTime) / 1000; + UnsignedInt realTime = (GetTickCount()-totalStartTimeMillis) / 1000; printf("Total Wall Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); fflush(stdout); From 8c9612bc19cfde45a91bfaa1a987215ff5d04b29 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 14:38:46 +0200 Subject: [PATCH 067/112] remove hacks --- .../GameEngine/Source/Common/ReplaySimulation.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index a17b4b4b168..84f2b5ff5b4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -82,18 +82,6 @@ static int SimulateReplaysInThisProcess(const std::vector &filename fflush(stdout); } - // TheSuperHackers @todo helmutbuhler 04/05/2025 - // Some replays cause a crash inside TheGameLogic->clearGameData(). - // It likely has to do with Contains. We really gotta fix this, but for now we just terminate. - TerminateProcess(GetCurrentProcess(), numErrors != 0 ? 1 : 0); - - // TheSuperHackers @todo helmutbuhler 04/13/2025 - // There is a bug somewhere in the destructor of TheGameEngine which doesn't properly - // clean up the players and causes a crash unless this is called. - if (TheGameLogic->isInGame()) - { - TheGameLogic->clearGameData(); - } return numErrors != 0 ? 1 : 0; } From 6a362bdea2606a336dee8ca5b87a816b5c848c36 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 14:52:00 +0200 Subject: [PATCH 068/112] Add SIMULATE_REPLAYS_SEQUENTIAL and some refactoring --- .../Code/GameEngine/Include/Common/GlobalData.h | 3 ++- .../Code/GameEngine/Source/Common/CommandLine.cpp | 2 +- .../Code/GameEngine/Source/Common/GlobalData.cpp | 2 +- .../GameEngine/Source/Common/ReplaySimulation.cpp | 14 +++++--------- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 88e6312b701..74fbc5ae784 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -54,6 +54,7 @@ enum AIDebugOptions CPP_11(: Int); // PUBLIC ///////////////////////////////////////////////////////////////////////////////////////// const Int MAX_GLOBAL_LIGHTS = 3; +const Int SIMULATE_REPLAYS_SEQUENTIAL = -1; //------------------------------------------------------------------------------------------------- /** Global data container class @@ -333,7 +334,7 @@ class GlobalData : public SubsystemInterface AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) - Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or -1 for sequential simulation + Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index cfd0b622e87..7b03575b4ee 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -450,7 +450,7 @@ Int parseJobs(char *args[], int num) if (TheWritableGlobalData && num > 1) { TheWritableGlobalData->m_simulateReplayJobs = atoi(args[1]); - if (TheGlobalData->m_simulateReplayJobs < -1 || TheGlobalData->m_simulateReplayJobs == 0) + if (TheGlobalData->m_simulateReplayJobs < SIMULATE_REPLAYS_SEQUENTIAL || TheGlobalData->m_simulateReplayJobs == 0) { DEBUG_CRASH(("Invalid number of jobs %d", TheGlobalData->m_simulateReplayJobs)); exit(1); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index b636b799d53..a25cc9c0ae0 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -958,7 +958,7 @@ GlobalData::GlobalData() m_pendingFile.clear(); m_simulateReplays.clear(); - m_simulateReplayJobs = -1; + m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 84f2b5ff5b4..83d17a369cd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -90,7 +90,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file DWORD totalStartTimeMillis = GetTickCount(); WideChar exePath[1024]; - GetModuleFileNameW(NULL, exePath, 1024); + GetModuleFileNameW(NULL, exePath, ARRAY_SIZE(exePath)); std::vector processes; int filenamePositionStarted = 0; @@ -135,9 +135,8 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file UnicodeString command; command.format(L"\"%s\" -simReplay \"%s\"", exePath, filenameWide.str()); - WorkerProcess p; - p.startProcess(command); - processes.push_back(p); + processes.push_back(WorkerProcess()); + processes.back().startProcess(command); filenamePositionStarted++; numProcessesRunning++; @@ -153,10 +152,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file DEBUG_ASSERTCRASH(filenamePositionStarted == filenames.size(), ("inconsistent file position 1")); DEBUG_ASSERTCRASH(filenamePositionDone == filenames.size(), ("inconsistent file position 2")); - if (numErrors) - printf("Errors occured: %d\n", numErrors); - else - printf("Successfully simulated all replays\n"); + printf("Simulation of all replays completed. Errors occured: %d\n", numErrors); UnsignedInt realTime = (GetTickCount()-totalStartTimeMillis) / 1000; printf("Total Wall Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); @@ -167,7 +163,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file int SimulateReplays(const std::vector &filenames, int maxProcesses) { - if (maxProcesses == -1) + if (maxProcesses == SIMULATE_REPLAYS_SEQUENTIAL) return SimulateReplaysInThisProcess(filenames); else return SimulateReplaysInWorkerProcesses(filenames, maxProcesses); From 1558a3a0dbd7922b3204c2337742bd8d4e9d7c88 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 20:18:20 +0200 Subject: [PATCH 069/112] more minor stuff --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 2 +- GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 7b03575b4ee..2f215dc43bb 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -452,7 +452,7 @@ Int parseJobs(char *args[], int num) TheWritableGlobalData->m_simulateReplayJobs = atoi(args[1]); if (TheGlobalData->m_simulateReplayJobs < SIMULATE_REPLAYS_SEQUENTIAL || TheGlobalData->m_simulateReplayJobs == 0) { - DEBUG_CRASH(("Invalid number of jobs %d", TheGlobalData->m_simulateReplayJobs)); + DEBUG_CRASH(("Invalid number of jobs: %d", TheGlobalData->m_simulateReplayJobs)); exit(1); } return 2; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp index 83d17a369cd..f41b2f9ccca 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp @@ -104,12 +104,12 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file processes[i].update(); // Get result of finished processes and print output in order - while (processes.size() != 0) + while (!processes.empty()) { if (!processes[0].isDone()) break; AsciiString stdOutput = processes[0].getStdOutput(); - printf("%d/%d %s", filenamePositionDone+1, filenames.size(), stdOutput.str()); + printf("%d/%d %s", filenamePositionDone+1, (int)filenames.size(), stdOutput.str()); DWORD exitcode = processes[0].getExitCode(); if (exitcode != 0) printf("Error!\n"); From 568832b60ee6748fc64dca5aa27361d5cbd54f21 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 20:53:26 +0200 Subject: [PATCH 070/112] Update comment --- GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index db60f65c4db..a29187c96a9 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -48,7 +48,7 @@ class ReplayGameInfo : public GameInfo enum RecorderModeType CPP_11(: Int) { RECORDERMODETYPE_RECORD, RECORDERMODETYPE_PLAYBACK, - RECORDERMODETYPE_SIMULATION_PLAYBACK, // Play back replay without any graphics (TheSuperHackers @feature helmutbuhler 13/04/2025) + RECORDERMODETYPE_SIMULATION_PLAYBACK, // Play back replay without any graphics RECORDERMODETYPE_NONE // this is a valid state to be in on the shell map, or in saved games }; From d90e11af6d89f7da60685ca22d6198bb6b661f06 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 7 Jun 2025 21:07:39 +0200 Subject: [PATCH 071/112] Move new files to Core --- Core/GameEngine/CMakeLists.txt | 4 ++++ .../GameEngine/Include/Common/ReplaySimulation.h | 0 .../GameEngine/Include/Common/WorkerProcess.h | 0 .../GameEngine/Source/Common/ReplaySimulation.cpp | 0 .../GameEngine/Source/Common/WorkerProcess.cpp | 0 GeneralsMD/Code/GameEngine/CMakeLists.txt | 8 ++++---- 6 files changed, 8 insertions(+), 4 deletions(-) rename {GeneralsMD/Code => Core}/GameEngine/Include/Common/ReplaySimulation.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Include/Common/WorkerProcess.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/Common/ReplaySimulation.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/Common/WorkerProcess.cpp (100%) diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 35fc9a97d1e..56b8b90cfac 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -95,6 +95,7 @@ set(GAMEENGINE_SRC # Include/Common/RandomValue.h # Include/Common/Recorder.h # Include/Common/Registry.h + Include/Common/ReplaySimulation.h # Include/Common/ResourceGatheringManager.h # Include/Common/Science.h # Include/Common/ScopedMutex.h @@ -129,6 +130,7 @@ set(GAMEENGINE_SRC # Include/Common/UserPreferences.h # Include/Common/version.h # Include/Common/WellKnownKeys.h + Include/Common/WorkerProcess.h Include/Common/Xfer.h Include/Common/XferCRC.h Include/Common/XferDeepCRC.h @@ -607,6 +609,7 @@ set(GAMEENGINE_SRC # Source/Common/PerfTimer.cpp # Source/Common/RandomValue.cpp # Source/Common/Recorder.cpp + Source/Common/ReplaySimulation.cpp # Source/Common/RTS/AcademyStats.cpp # Source/Common/RTS/ActionManager.cpp # Source/Common/RTS/Energy.cpp @@ -676,6 +679,7 @@ set(GAMEENGINE_SRC # Source/Common/Thing/ThingTemplate.cpp # Source/Common/UserPreferences.cpp # Source/Common/version.cpp + Source/Common/WorkerProcess.cpp # Source/GameClient/ClientInstance.cpp # Source/GameClient/Color.cpp # Source/GameClient/Credits.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h b/Core/GameEngine/Include/Common/ReplaySimulation.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/Common/ReplaySimulation.h rename to Core/GameEngine/Include/Common/ReplaySimulation.h diff --git a/GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h b/Core/GameEngine/Include/Common/WorkerProcess.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/Common/WorkerProcess.h rename to Core/GameEngine/Include/Common/WorkerProcess.h diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/Common/ReplaySimulation.cpp rename to Core/GameEngine/Source/Common/ReplaySimulation.cpp diff --git a/GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp b/Core/GameEngine/Source/Common/WorkerProcess.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/Common/WorkerProcess.cpp rename to Core/GameEngine/Source/Common/WorkerProcess.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 68849e8e73a..e2bea3ec545 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -94,7 +94,7 @@ set(GAMEENGINE_SRC Include/Common/RAMFile.h Include/Common/RandomValue.h Include/Common/Recorder.h - Include/Common/ReplaySimulation.h +# Include/Common/ReplaySimulation.h Include/Common/Registry.h Include/Common/ResourceGatheringManager.h Include/Common/Science.h @@ -130,7 +130,7 @@ set(GAMEENGINE_SRC Include/Common/UserPreferences.h Include/Common/version.h Include/Common/WellKnownKeys.h - Include/Common/WorkerProcess.h +# Include/Common/WorkerProcess.h # Include/Common/Xfer.h # Include/Common/XferCRC.h # Include/Common/XferDeepCRC.h @@ -609,7 +609,7 @@ set(GAMEENGINE_SRC Source/Common/PerfTimer.cpp Source/Common/RandomValue.cpp Source/Common/Recorder.cpp - Source/Common/ReplaySimulation.cpp +# Source/Common/ReplaySimulation.cpp Source/Common/RTS/AcademyStats.cpp Source/Common/RTS/ActionManager.cpp Source/Common/RTS/Energy.cpp @@ -679,7 +679,7 @@ set(GAMEENGINE_SRC Source/Common/Thing/ThingTemplate.cpp Source/Common/UserPreferences.cpp Source/Common/version.cpp - Source/Common/WorkerProcess.cpp +# Source/Common/WorkerProcess.cpp Source/GameClient/ClientInstance.cpp Source/GameClient/Color.cpp Source/GameClient/Credits.cpp From d660a9378a136960b8bbfbc96127c4611b94d921 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 14 Jun 2025 21:28:38 +0200 Subject: [PATCH 072/112] Make -simReplay work without headless # Conflicts: # GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h # GeneralsMD/Code/Main/WinMain.cpp --- .../Source/Common/ReplaySimulation.cpp | 26 +++++++++++- .../GameEngine/Include/Common/GlobalData.h | 3 +- .../GameEngine/Source/Common/CommandLine.cpp | 6 ++- .../GameEngine/Source/Common/GameEngine.cpp | 6 +-- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../GUI/GUICallbacks/Menus/ScoreScreen.cpp | 40 ++++++++++++------- .../Source/GameClient/GUI/Shell/Shell.cpp | 4 +- GeneralsMD/Code/Main/WinMain.cpp | 2 - 8 files changed, 61 insertions(+), 27 deletions(-) diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index f41b2f9ccca..15e395f296f 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -22,11 +22,29 @@ #include "Common/WorkerProcess.h" #include "GameLogic/GameLogic.h" #include "GameClient/GameClient.h" +#include "Common/GameEngine.h" static int SimulateReplaysInThisProcess(const std::vector &filenames) { - // Note that we use printf here because this is run from cmd. int numErrors = 0; + if (!TheGlobalData->m_headless) + { + // If we are not in headless mode, we need to run the replay in the engine. + for (size_t i = 0; i < filenames.size(); i++) + { + AsciiString filename = filenames[i]; + TheRecorder->playbackFile(filenames[i]); + TheWritableGlobalData->m_showReplayContinueButton = i != filenames.size()-1; + TheGameEngine->execute(); + if (TheRecorder->sawCRCMismatch()) + numErrors++; + if (!TheGlobalData->m_showReplayContinueButton) + break; + TheGameEngine->setQuitting(FALSE); + } + return numErrors != 0 ? 1 : 0; + } + // Note that we use printf here because this is run from cmd. DWORD totalStartTimeMillis = GetTickCount(); for (size_t i = 0; i < filenames.size(); i++) { @@ -133,7 +151,11 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file UnicodeString filenameWide; filenameWide.translate(filenames[filenamePositionStarted]); UnicodeString command; - command.format(L"\"%s\" -simReplay \"%s\"", exePath, filenameWide.str()); + command.format(L"\"%s\"%s%s -ignoreAsserts -simReplay \"%s\"", + exePath, + TheGlobalData->m_windowed ? L" -win" : L"", + TheGlobalData->m_headless ? L" -headless" : L"", + filenameWide.str()); processes.push_back(WorkerProcess()); processes.back().startProcess(command); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 74fbc5ae784..1ccea41a738 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -330,11 +330,12 @@ class GlobalData : public SubsystemInterface Real m_cameraAdjustSpeed; ///< Rate at which we adjust camera height Bool m_enforceMaxCameraHeight; ///< Enfoce max camera height while scrolling? Bool m_buildMapCache; - AsciiString m_initialFile; ///< If this is specified, load a specific map/replay from the command-line + AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation + Bool m_showReplayContinueButton; Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 2f215dc43bb..84512a902e4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -439,7 +439,11 @@ Int parseSimReplay(char *args[], int num) exit(1); } TheWritableGlobalData->m_simulateReplays.push_back(filename); - TheWritableGlobalData->m_headless = TRUE; + + TheWritableGlobalData->m_playIntro = FALSE; + TheWritableGlobalData->m_afterIntro = TRUE; + TheWritableGlobalData->m_playSizzle = FALSE; + TheWritableGlobalData->m_shellMapOn = FALSE; return 2; } return 1; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index d371e2665b1..75332f59bcd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -610,7 +610,7 @@ void GameEngine::init( int argc, char *argv[] ) // load the initial shell screen //TheShell->push( AsciiString("Menus/MainMenu.wnd") ); - // This allows us to run a map/reply from the command line + // This allows us to run a map from the command line if (TheGlobalData->m_initialFile.isEmpty() == FALSE) { AsciiString fname = TheGlobalData->m_initialFile; @@ -632,10 +632,6 @@ void GameEngine::init( int argc, char *argv[] ) msg->appendIntegerArgument(0); InitRandom(0); } - else if (fname.endsWithNoCase(".rep")) - { - TheRecorder->playbackFile(fname); - } } // diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index a25cc9c0ae0..c1d011fbef6 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -959,6 +959,7 @@ GlobalData::GlobalData() m_simulateReplays.clear(); m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; + m_showReplayContinueButton = FALSE; for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index 786ba75d532..96e208fd246 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -541,23 +541,35 @@ WindowMsgHandledType ScoreScreenSystem( GameWindow *window, UnsignedInt msg, { TheShell->pop(); TheCampaignManager->setCampaign(AsciiString::TheEmptyString); + if (!TheGlobalData->m_simulateReplays.empty()) + { + TheWritableGlobalData->m_showReplayContinueButton = FALSE; + TheGameEngine->setQuitting(TRUE); + } } else if ( controlID == buttonContinueID ) { - if(!buttonIsFinishCampaign) - ReplayWasPressed = TRUE; - if( screenType == SCORESCREEN_SINGLEPLAYER) + if (TheGlobalData->m_showReplayContinueButton) { - AsciiString mapName = TheCampaignManager->getCurrentMap(); - - if( mapName.isEmpty() ) - { - ReplayWasPressed = FALSE; - TheShell->pop(); - } - else + TheGameEngine->setQuitting(TRUE); + } + else + { + if(!buttonIsFinishCampaign) + ReplayWasPressed = TRUE; + if( screenType == SCORESCREEN_SINGLEPLAYER) { - CheckForCDAtGameStart( startNextCampaignGame ); + AsciiString mapName = TheCampaignManager->getCurrentMap(); + + if( mapName.isEmpty() ) + { + ReplayWasPressed = FALSE; + TheShell->pop(); + } + else + { + CheckForCDAtGameStart( startNextCampaignGame ); + } } } } @@ -984,7 +996,7 @@ void initReplaySinglePlayer( void ) if (chatBoxBorder) chatBoxBorder->winHide(TRUE); if (buttonContinue) - buttonContinue->winHide(TRUE); + buttonContinue->winHide(!TheGlobalData->m_showReplayContinueButton); if (buttonBuddies) buttonBuddies->winHide(TRUE); if (listboxChatWindowScoreScreen) @@ -1091,7 +1103,7 @@ void initReplayMultiPlayer(void) if (chatBoxBorder) chatBoxBorder->winHide(TRUE); if (buttonContinue) - buttonContinue->winHide(TRUE); + buttonContinue->winHide(!TheGlobalData->m_showReplayContinueButton); if (buttonBuddies) buttonBuddies->winHide(TRUE); // if (buttonRehost) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index ad8954ae150..93a900509a9 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -410,7 +410,7 @@ void Shell::showShell( Bool runInit ) { DEBUG_LOG(("Shell:showShell() - %s (%s)\n", TheGlobalData->m_initialFile.str(), (top())?top()->getFilename().str():"no top screen")); - if(!TheGlobalData->m_initialFile.isEmpty()) + if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty()) { return; } @@ -472,7 +472,7 @@ void Shell::showShell( Bool runInit ) void Shell::showShellMap(Bool useShellMap ) { // we don't want any of this to show if we're loading straight into a file - if(TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic ) + if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty()) return; if(useShellMap && TheGlobalData->m_shellMapOn) { diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index b545c8efdaf..af26d053d3e 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -873,8 +873,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, ApplicationIsWindowed = true; // preparse for headless commandline options as well. We need to know about this before we create the window. - if (stricmp(token, "-simReplay") == 0) - headless = true; token = nextParam(NULL, "\" "); } From 020ee42b792669bdbf8514fdcdbc12fc08114b43 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 14 Jun 2025 21:40:10 +0200 Subject: [PATCH 073/112] Add -headless option back # Conflicts: # GeneralsMD/Code/Main/WinMain.cpp --- .../GameEngine/Source/Common/CommandLine.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 84512a902e4..533b1169aee 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -428,6 +428,18 @@ Int parseMapName(char *args[], int num) return 1; } +Int parseHeadless( char *args[], int num ) +{ + if (TheWritableGlobalData) + { + TheWritableGlobalData->m_headless = TRUE; + TheWritableGlobalData->m_playIntro = FALSE; + TheWritableGlobalData->m_afterIntro = TRUE; + TheWritableGlobalData->m_playSizzle = FALSE; + } + return 1; +} + Int parseSimReplay(char *args[], int num) { if (TheWritableGlobalData && num > 1) @@ -1243,6 +1255,10 @@ static CommandLineParam params[] = { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, + // TheSuperHackers @feature helmutbuhler 11/04/2025 + // This runs the game without a window, graphics, input and audio. You can combine this with -replay + { "-headless", parseHeadless }, + // TheSuperHackers @feature helmutbuhler 13/04/2025 // Simulate replay without graphics. Pass the filename including .rep afterwards. // You can pass this multiple times to check multiple replays. From b7d559d3b8972abb4cfebbf0d087d658a7b9ec3b Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sat, 14 Jun 2025 21:43:42 +0200 Subject: [PATCH 074/112] Rename -simReplay to -replay --- Core/GameEngine/Source/Common/ReplaySimulation.cpp | 2 +- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index 15e395f296f..3d59afb3207 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -151,7 +151,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file UnicodeString filenameWide; filenameWide.translate(filenames[filenamePositionStarted]); UnicodeString command; - command.format(L"\"%s\"%s%s -ignoreAsserts -simReplay \"%s\"", + command.format(L"\"%s\"%s%s -replay \"%s\"", exePath, TheGlobalData->m_windowed ? L" -win" : L"", TheGlobalData->m_headless ? L" -headless" : L"", diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 533b1169aee..67c3c3b1301 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -440,7 +440,7 @@ Int parseHeadless( char *args[], int num ) return 1; } -Int parseSimReplay(char *args[], int num) +Int parseReplay(char *args[], int num) { if (TheWritableGlobalData && num > 1) { @@ -1262,7 +1262,7 @@ static CommandLineParam params[] = // TheSuperHackers @feature helmutbuhler 13/04/2025 // Simulate replay without graphics. Pass the filename including .rep afterwards. // You can pass this multiple times to check multiple replays. - { "-simReplay", parseSimReplay }, + { "-replay", parseReplay }, // TheSuperHackers @feature helmutbuhler 23/05/2025 // Simulate each replay in a separate process and use up two N processes at the same time. From 702dfa0b5d87da39293a6039e08a3c958fdaadcc Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 15 Jun 2025 01:45:43 +0200 Subject: [PATCH 075/112] Fix merge --- GeneralsMD/Code/Main/WinMain.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index af26d053d3e..4c056be5711 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -873,6 +873,8 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, ApplicationIsWindowed = true; // preparse for headless commandline options as well. We need to know about this before we create the window. + if (stricmp(token, "-headless") == 0) + headless = true; token = nextParam(NULL, "\" "); } From 6ddfdf75f7683d1b5733f707aacb71edabe520d5 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Tue, 17 Jun 2025 08:57:36 +0200 Subject: [PATCH 076/112] Add m_avoidFirstInstance and m_multiInstance and make -replay work while running retail game and without RTS_MULTI_INSTANCE --- .../GameEngine/Include/Common/GlobalData.h | 3 + .../GameEngine/Source/Common/CommandLine.cpp | 14 +++++ .../GameEngine/Source/Common/GlobalData.cpp | 6 ++ .../Source/GameClient/ClientInstance.cpp | 59 +++++++++++-------- GeneralsMD/Code/Main/WinMain.cpp | 2 +- 5 files changed, 58 insertions(+), 26 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 60fee17b602..d7ba3cc09d7 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -124,6 +124,9 @@ class GlobalData : public SubsystemInterface // Run game without graphics, input or audio. Bool m_headless; + Bool m_multiInstance; ///< Allow multiple instances of the game + Bool m_avoidFirstInstance; ///< Avoid mutex of original game + Bool m_windowed; Int m_xResolution; Int m_yResolution; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index fac8a6430a9..cdaf828f6ec 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -431,6 +431,11 @@ Int parseReplay(char *args[], int num) TheWritableGlobalData->m_afterIntro = TRUE; TheWritableGlobalData->m_playSizzle = FALSE; TheWritableGlobalData->m_shellMapOn = FALSE; + + // Make replay playback possible while other clients (possible retail) are running + TheWritableGlobalData->m_multiInstance = TRUE; + TheWritableGlobalData->m_avoidFirstInstance = TRUE; + return 2; } return 1; @@ -451,6 +456,12 @@ Int parseJobs(char *args[], int num) return 1; } +Int parseMultiInstance(char *args[], int num) +{ + TheWritableGlobalData->m_multiInstance = TRUE; + return 1; +} + Int parseXRes(char *args[], int num) { if (num > 1) @@ -1155,6 +1166,9 @@ static CommandLineParam paramsForStartup[] = // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, + + // Allows multiple instances to run + { "-multiInstance", parseMultiInstance }, }; // These Params are parsed during Engine Init before INI data is loaded diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e93fe94a7c7..607c76644fd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -614,6 +614,12 @@ GlobalData::GlobalData() m_framesPerSecondLimit = 0; m_chipSetType = 0; m_headless = FALSE; +#if defined(RTS_MULTI_INSTANCE) + m_multiInstance = TRUE; +#else + m_multiInstance = FALSE; +#endif + m_avoidFirstInstance = FALSE; m_windowed = 0; m_xResolution = 800; m_yResolution = 600; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp index 2ac1ff33988..6bb51b9d109 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp @@ -16,6 +16,7 @@ ** along with this program. If not, see . */ #include "PreRTS.h" +#include "Common/CommandLine.h" #include "GameClient/ClientInstance.h" #define GENERALS_GUID "685EAFF2-3216-4265-B047-251C5F4B82F3" @@ -31,44 +32,52 @@ bool ClientInstance::initialize() { return true; } + + CommandLine::parseCommandLineForStartup(); + + if (TheGlobalData->m_avoidFirstInstance) + ++s_instanceIndex; // Create a mutex with a unique name to Generals in order to determine if our app is already running. // WARNING: DO NOT use this number for any other application except Generals. while (true) { -#if defined(RTS_MULTI_INSTANCE) - std::string guidStr = getFirstInstanceName(); - if (s_instanceIndex > 0u) - { - char idStr[33]; - itoa(s_instanceIndex, idStr, 10); - guidStr.push_back('-'); - guidStr.append(idStr); - } - s_mutexHandle = CreateMutex(NULL, FALSE, guidStr.c_str()); - if (GetLastError() == ERROR_ALREADY_EXISTS) + if (TheGlobalData->m_multiInstance) { - if (s_mutexHandle != NULL) + std::string guidStr = getFirstInstanceName(); + if (s_instanceIndex > 0u) + { + char idStr[33]; + itoa(s_instanceIndex, idStr, 10); + guidStr.push_back('-'); + guidStr.append(idStr); + } + s_mutexHandle = CreateMutex(NULL, FALSE, guidStr.c_str()); + if (GetLastError() == ERROR_ALREADY_EXISTS) { - CloseHandle(s_mutexHandle); - s_mutexHandle = NULL; + if (s_mutexHandle != NULL) + { + CloseHandle(s_mutexHandle); + s_mutexHandle = NULL; + } + // Try again with a new instance. + ++s_instanceIndex; + continue; } - // Try again with a new instance. - ++s_instanceIndex; - continue; } -#else - s_mutexHandle = CreateMutex(NULL, FALSE, getFirstInstanceName()); - if (GetLastError() == ERROR_ALREADY_EXISTS) + else { - if (s_mutexHandle != NULL) + s_mutexHandle = CreateMutex(NULL, FALSE, getFirstInstanceName()); + if (GetLastError() == ERROR_ALREADY_EXISTS) { - CloseHandle(s_mutexHandle); - s_mutexHandle = NULL; + if (s_mutexHandle != NULL) + { + CloseHandle(s_mutexHandle); + s_mutexHandle = NULL; + } + return false; } - return false; } -#endif break; } diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index e8ed4998bf4..71ab4932a67 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -868,7 +868,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // TheSuperHackers @refactor The instance mutex now lives in its own class. - if (!TheGlobalData->m_headless && !rts::ClientInstance::initialize()) + if (!rts::ClientInstance::initialize()) { HWND ccwindow = FindWindow(rts::ClientInstance::getFirstInstanceName(), NULL); if (ccwindow) From b24eca88f3f56d7974962ec6bfaf00681766575e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Tue, 17 Jun 2025 09:30:10 +0200 Subject: [PATCH 077/112] Remove now unused GameMain( int argc, char *argv[] ) parameters --- GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h | 3 +-- GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp | 3 +-- GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp | 4 ++-- GeneralsMD/Code/Main/WinMain.cpp | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h index cbd684023a6..995817c7a2b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h @@ -67,7 +67,6 @@ class GameEngine : public SubsystemInterface virtual ~GameEngine(); virtual void init( void ); ///< Init engine by creating client and logic - virtual void init( int argc, char *argv[] ); ///< Init engine by creating client and logic virtual void reset( void ); ///< reset system to starting state virtual void update( void ); ///< per frame update @@ -114,6 +113,6 @@ extern GameEngine *TheGameEngine; extern GameEngine *CreateGameEngine( void ); /// The entry point for the game system -extern Int GameMain( int argc, char *argv[] ); +extern Int GameMain(); #endif // _GAME_ENGINE_H_ diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 417e4492082..f95a046a494 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -251,8 +251,7 @@ void GameEngine::setFramesPerSecondLimit( Int fps ) /** ----------------------------------------------------------------------------------------------- * Initialize the game engine by initializing the GameLogic and GameClient. */ -void GameEngine::init( void ) {} /// @todo: I changed this to take argc & argv so we can parse those after the GDF is loaded. We need to rethink this immediately as it is a nasty hack -void GameEngine::init( int argc, char *argv[] ) +void GameEngine::init() { try { //create an INI object to use for loading stuff diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index 2b09f5cebbd..d667eab4c09 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -35,12 +35,12 @@ /** * This is the entry point for the game system. */ -Int GameMain( int argc, char *argv[] ) +Int GameMain() { int exitcode = 0; // initialize the game engine using factory function TheGameEngine = CreateGameEngine(); - TheGameEngine->init(argc, argv); + TheGameEngine->init(); if (!TheGlobalData->m_simulateReplays.empty()) { diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 71ab4932a67..c2754bc5417 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -889,7 +889,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, DEBUG_LOG(("CRC message is %d\n", GameMessage::MSG_LOGIC_CRC)); // run the game main loop - exitcode = GameMain(0, NULL); + exitcode = GameMain(); delete TheVersion; TheVersion = NULL; From d3febeaf6c1dde183a0ed9ec299014822909ca73 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Wed, 18 Jun 2025 07:59:57 +0200 Subject: [PATCH 078/112] Changed error handling of -replay commandline parsing to output something on the console --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index cdaf828f6ec..4cdf0f3cfcd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -406,7 +406,7 @@ Int parseMapName(char *args[], int num) return 1; } -Int parseHeadless( char *args[], int num ) +Int parseHeadless(char *args[], int num) { TheWritableGlobalData->m_headless = TRUE; TheWritableGlobalData->m_playIntro = FALSE; @@ -422,7 +422,7 @@ Int parseReplay(char *args[], int num) AsciiString filename = args[1]; if (!filename.endsWithNoCase(RecorderClass::getReplayExtention())) { - DEBUG_CRASH(("Invalid replay name \"%s\"", filename.str())); + printf("Invalid replay name \"%s\"\n", filename.str()); exit(1); } TheWritableGlobalData->m_simulateReplays.push_back(filename); @@ -448,7 +448,7 @@ Int parseJobs(char *args[], int num) TheWritableGlobalData->m_simulateReplayJobs = atoi(args[1]); if (TheGlobalData->m_simulateReplayJobs < SIMULATE_REPLAYS_SEQUENTIAL || TheGlobalData->m_simulateReplayJobs == 0) { - DEBUG_CRASH(("Invalid number of jobs: %d", TheGlobalData->m_simulateReplayJobs)); + printf("Invalid number of jobs: %d\n", TheGlobalData->m_simulateReplayJobs); exit(1); } return 2; From b3243c84742781a78b076af06b81e0d6845ec2fc Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Wed, 18 Jun 2025 09:01:07 +0200 Subject: [PATCH 079/112] Allow wildcards in -replay commandline --- .../Source/Common/ReplaySimulation.cpp | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index 3d59afb3207..1d9d76c3267 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -18,11 +18,12 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include "Common/GameEngine.h" +#include "Common/LocalFileSystem.h" #include "Common/Recorder.h" #include "Common/WorkerProcess.h" #include "GameLogic/GameLogic.h" #include "GameClient/GameClient.h" -#include "Common/GameEngine.h" static int SimulateReplaysInThisProcess(const std::vector &filenames) { @@ -183,10 +184,53 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file return numErrors != 0 ? 1 : 0; } +std::vector ResolveFilenameWildcards(const std::vector &filenames) +{ + // If some filename contains wildcards, search for actual filenames. + // Note that we cannot do this in parseReplay because we require TheLocalFileSystem initialized. + std::vector filenamesResolved; + for (std::vector::const_iterator filename = filenames.begin(); filename != filenames.end(); ++filename) + { + if (filename->find('*') || filename->find('?')) + { + AsciiString dir1 = TheRecorder->getReplayDir(); + AsciiString dir2 = *filename; + AsciiString wildcard = *filename; + { + int len = dir2.getLength(); + while (len) + { + char c = dir2.getCharAt(len-1); + if (c == '/' || c == '\\') + { + wildcard.set(wildcard.str()+dir2.getLength()); + break; + } + dir2.removeLastChar(); + len--; + } + } + + FilenameList files; + TheLocalFileSystem->getFileListInDirectory(dir2.str(), dir1.str(), wildcard, files, FALSE); + for (FilenameList::iterator it = files.begin(); it != files.end(); ++it) + { + AsciiString file; + file.set(it->str() + dir1.getLength()); + filenamesResolved.push_back(file); + } + } + else + filenamesResolved.push_back(*filename); + } + return filenamesResolved; +} + int SimulateReplays(const std::vector &filenames, int maxProcesses) { + std::vector filenamesResolved = ResolveFilenameWildcards(filenames); if (maxProcesses == SIMULATE_REPLAYS_SEQUENTIAL) - return SimulateReplaysInThisProcess(filenames); + return SimulateReplaysInThisProcess(filenamesResolved); else - return SimulateReplaysInWorkerProcesses(filenames, maxProcesses); + return SimulateReplaysInWorkerProcesses(filenamesResolved, maxProcesses); } From c353b947982aecb07c26aaa6ae6b02200849c7ab Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Wed, 18 Jun 2025 09:01:20 +0200 Subject: [PATCH 080/112] Update -replay comment --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 4cdf0f3cfcd..d5311c7992f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -1157,8 +1157,9 @@ static CommandLineParam paramsForStartup[] = { "-headless", parseHeadless }, // TheSuperHackers @feature helmutbuhler 13/04/2025 - // Simulate replay without graphics. Pass the filename including .rep afterwards. - // You can pass this multiple times to check multiple replays. + // Play back a replay. Pass the filename including .rep afterwards. + // You can pass this multiple times to play back multiple replays. + // You can also include wildcards. The file must be in the replay folder or in a subfolder. { "-replay", parseReplay }, // TheSuperHackers @feature helmutbuhler 23/05/2025 From 0a63c92cb42663dd1453d3bcb90837d520d4e715 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Wed, 18 Jun 2025 09:22:26 +0200 Subject: [PATCH 081/112] Use mismatchFrame to simplify printf --- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 0567ab11991..92c5f840ec6 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1078,7 +1078,7 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f playbackCRC, newCRC, mismatchFrame)); // Print Mismatch in case we are simulating replays from console. - printf("CRC Mismatch in Frame %d\n", TheGameLogic->getFrame()-m_crcInfo->GetQueueSize()-1); + printf("CRC Mismatch in Frame %d\n", mismatchFrame); // TheSuperHackers @tweak Pause the game on mismatch. Bool pause = TRUE; From 5fced5ba501619a3e131fba04aea3f68b461b024 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 15:42:39 +0200 Subject: [PATCH 082/112] Move m_multiInstance and m_avoidFirstInstance to ClientInstance --- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 3 --- .../GameEngine/Include/GameClient/ClientInstance.h | 3 +++ .../Code/GameEngine/Source/Common/CommandLine.cpp | 7 ++++--- .../Code/GameEngine/Source/Common/GlobalData.cpp | 6 ------ .../GameEngine/Source/GameClient/ClientInstance.cpp | 10 ++++++++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index d7ba3cc09d7..60fee17b602 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -124,9 +124,6 @@ class GlobalData : public SubsystemInterface // Run game without graphics, input or audio. Bool m_headless; - Bool m_multiInstance; ///< Allow multiple instances of the game - Bool m_avoidFirstInstance; ///< Avoid mutex of original game - Bool m_windowed; Int m_xResolution; Int m_yResolution; diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h index 74e7add404b..6ef2c90cf92 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h @@ -39,6 +39,9 @@ class ClientInstance // Returns the instance name of the first game client. static const char* getFirstInstanceName(); + static Bool s_multiInstance; // Allow multiple instances of the game + static Bool s_avoidFirstInstance; // Avoid mutex of original game + private: static HANDLE s_mutexHandle; static UnsignedInt s_instanceIndex; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index d5311c7992f..b30e7d835a4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -31,6 +31,7 @@ #include "Common/LocalFileSystem.h" #include "Common/Recorder.h" #include "Common/version.h" +#include "GameClient/ClientInstance.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -433,8 +434,8 @@ Int parseReplay(char *args[], int num) TheWritableGlobalData->m_shellMapOn = FALSE; // Make replay playback possible while other clients (possible retail) are running - TheWritableGlobalData->m_multiInstance = TRUE; - TheWritableGlobalData->m_avoidFirstInstance = TRUE; + rts::ClientInstance::s_multiInstance = TRUE; + rts::ClientInstance::s_avoidFirstInstance = TRUE; return 2; } @@ -458,7 +459,7 @@ Int parseJobs(char *args[], int num) Int parseMultiInstance(char *args[], int num) { - TheWritableGlobalData->m_multiInstance = TRUE; + rts::ClientInstance::s_multiInstance = TRUE; return 1; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 607c76644fd..e93fe94a7c7 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -614,12 +614,6 @@ GlobalData::GlobalData() m_framesPerSecondLimit = 0; m_chipSetType = 0; m_headless = FALSE; -#if defined(RTS_MULTI_INSTANCE) - m_multiInstance = TRUE; -#else - m_multiInstance = FALSE; -#endif - m_avoidFirstInstance = FALSE; m_windowed = 0; m_xResolution = 800; m_yResolution = 600; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp index 6bb51b9d109..0e6450b37df 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp @@ -25,6 +25,12 @@ namespace rts { HANDLE ClientInstance::s_mutexHandle = NULL; UnsignedInt ClientInstance::s_instanceIndex = 0; +#if defined(RTS_MULTI_INSTANCE) +Bool ClientInstance::s_multiInstance = TRUE; +#else +Bool ClientInstance::s_multiInstance = FALSE; +#endif +Bool ClientInstance::s_avoidFirstInstance = FALSE; bool ClientInstance::initialize() { @@ -35,14 +41,14 @@ bool ClientInstance::initialize() CommandLine::parseCommandLineForStartup(); - if (TheGlobalData->m_avoidFirstInstance) + if (s_avoidFirstInstance) ++s_instanceIndex; // Create a mutex with a unique name to Generals in order to determine if our app is already running. // WARNING: DO NOT use this number for any other application except Generals. while (true) { - if (TheGlobalData->m_multiInstance) + if (s_multiInstance) { std::string guidStr = getFirstInstanceName(); if (s_instanceIndex > 0u) From 2d2576fec900d6514c473866769ee62b3b25a16d Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 15:45:47 +0200 Subject: [PATCH 083/112] Take out -multiInstance --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index b30e7d835a4..a0f1cd50013 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -457,12 +457,6 @@ Int parseJobs(char *args[], int num) return 1; } -Int parseMultiInstance(char *args[], int num) -{ - rts::ClientInstance::s_multiInstance = TRUE; - return 1; -} - Int parseXRes(char *args[], int num) { if (num > 1) @@ -1168,9 +1162,6 @@ static CommandLineParam paramsForStartup[] = // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, - - // Allows multiple instances to run - { "-multiInstance", parseMultiInstance }, }; // These Params are parsed during Engine Init before INI data is loaded From 89431bf49238becd4345f92027011f742745386e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 15:48:51 +0200 Subject: [PATCH 084/112] Change comment --- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 60fee17b602..732200ff234 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -351,7 +351,7 @@ class GlobalData : public SubsystemInterface AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start - std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. (TheSuperHackers @feature helmutbuhler 13/04/2025) + std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation Bool m_showReplayContinueButton; From 87462b7490da4bfd0a583c6c0b4907971f13b3ce Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 15:55:22 +0200 Subject: [PATCH 085/112] Move CommandLine::parseCommandLineForStartup(); to DebugInit --- GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp | 4 +++- .../Code/GameEngine/Source/GameClient/ClientInstance.cpp | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp index f9090dfa94e..173c496ad4c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp @@ -58,6 +58,7 @@ #ifdef DEBUG_THREADSAFE #include "Common/CriticalSection.h" #endif +#include "Common/CommandLine.h" #include "Common/Debug.h" #include "Common/CRCDebug.h" #include "Common/SystemInfo.h" @@ -366,7 +367,8 @@ void DebugInit(int flags) #ifdef DEBUG_LOGGING // TheSuperHackers @info Debug initialization can happen very early. - // Therefore, initialize the client instance now. + // Therefore, parse initial commandline and initialize the client instance now. + CommandLine::parseCommandLineForStartup(); if (!rts::ClientInstance::initialize()) return; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp index 0e6450b37df..9cb0cf16bca 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp @@ -16,7 +16,6 @@ ** along with this program. If not, see . */ #include "PreRTS.h" -#include "Common/CommandLine.h" #include "GameClient/ClientInstance.h" #define GENERALS_GUID "685EAFF2-3216-4265-B047-251C5F4B82F3" @@ -39,8 +38,6 @@ bool ClientInstance::initialize() return true; } - CommandLine::parseCommandLineForStartup(); - if (s_avoidFirstInstance) ++s_instanceIndex; From dbf9d82d3f6a0a5075be879403994860e178ccfb Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 16:00:11 +0200 Subject: [PATCH 086/112] Convert one missed #if defined(RTS_MULTI_INSTANCE) to use new global --- .../Include/GameClient/ClientInstance.h | 1 + .../Source/GameNetwork/IPEnumeration.cpp | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h index 6ef2c90cf92..8edf6bb6da7 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h @@ -39,6 +39,7 @@ class ClientInstance // Returns the instance name of the first game client. static const char* getFirstInstanceName(); +public: static Bool s_multiInstance; // Allow multiple instances of the game static Bool s_avoidFirstInstance; // Avoid mutex of original game diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp index f485c603417..0e31d4018ea 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp @@ -97,15 +97,16 @@ EnumeratedIP * IPEnumeration::getAddresses( void ) return NULL; } -#if defined(RTS_MULTI_INSTANCE) // TheSuperHackers @feature Add one unique local host IP address for each multi client instance. - const UnsignedInt id = rts::ClientInstance::getInstanceId(); - addNewIP( - 127, - (UnsignedByte)(id >> 16), - (UnsignedByte)(id >> 8), - (UnsignedByte)(id)); -#endif + if (rts::ClientInstance::s_multiInstance) + { + const UnsignedInt id = rts::ClientInstance::getInstanceId(); + addNewIP( + 127, + (UnsignedByte)(id >> 16), + (UnsignedByte)(id >> 8), + (UnsignedByte)(id)); + } // construct a list of addresses int numAddresses = 0; From a1266af1eb8e9908d25a979153433821a0d889ed Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 16:14:27 +0200 Subject: [PATCH 087/112] spelling occured -> occurred --- Core/GameEngine/Include/Common/ReplaySimulation.h | 2 +- Core/GameEngine/Source/Common/ReplaySimulation.cpp | 7 ++----- GeneralsMD/Code/GameEngine/Include/Common/file.h | 6 +++--- GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h | 2 +- .../Source/W3DDevice/GameClient/W3DRoadBuffer.cpp | 2 +- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Core/GameEngine/Include/Common/ReplaySimulation.h b/Core/GameEngine/Include/Common/ReplaySimulation.h index 0ef27f9236f..48a82402432 100644 --- a/Core/GameEngine/Include/Common/ReplaySimulation.h +++ b/Core/GameEngine/Include/Common/ReplaySimulation.h @@ -20,6 +20,6 @@ // TheSuperHackers @feature helmutbuhler 13/04/2025 // Simulate a list of replays without graphics. -// Returns exitcode 1 if mismatch or other error occured +// Returns exitcode 1 if mismatch or other error occurred // Returns exitcode 0 if all replays were successfully simulated without mismatches int SimulateReplays(const std::vector &filenames, int maxProcesses); diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index 1d9d76c3267..00c64de066b 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -91,10 +91,7 @@ static int SimulateReplaysInThisProcess(const std::vector &filename } if (filenames.size() > 1) { - if (numErrors) - printf("Errors occured: %d\n", numErrors); - else - printf("Successfully simulated all replays\n"); + printf("Simulation of all replays completed. Errors occurred: %d\n", numErrors); UnsignedInt realTime = (GetTickCount()-totalStartTimeMillis) / 1000; printf("Total Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); @@ -175,7 +172,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file DEBUG_ASSERTCRASH(filenamePositionStarted == filenames.size(), ("inconsistent file position 1")); DEBUG_ASSERTCRASH(filenamePositionDone == filenames.size(), ("inconsistent file position 2")); - printf("Simulation of all replays completed. Errors occured: %d\n", numErrors); + printf("Simulation of all replays completed. Errors occurred: %d\n", numErrors); UnsignedInt realTime = (GetTickCount()-totalStartTimeMillis) / 1000; printf("Total Wall Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/file.h b/GeneralsMD/Code/GameEngine/Include/Common/file.h index c81c8c247a8..8e9609978b4 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/file.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/file.h @@ -131,15 +131,15 @@ class File : public MemoryPoolObject virtual Int read( void *buffer, Int bytes ) = NULL ; /**< Read the specified number of bytes from the file in to the * memory pointed at by buffer. Returns the number of bytes read. - * Returns -1 if an error occured. + * Returns -1 if an error occurred. */ virtual Int write( const void *buffer, Int bytes ) = NULL ; /**< Write the specified number of bytes from the * memory pointed at by buffer to the file. Returns the number of bytes written. - * Returns -1 if an error occured. + * Returns -1 if an error occurred. */ virtual Int seek( Int bytes, seekMode mode = CURRENT ) = NULL; /**< Sets the file position of the next read/write operation. Returns the new file * position as the number of bytes from the start of the file. - * Returns -1 if an error occured. + * Returns -1 if an error occurred. * * seekMode determines how the seek is done: * diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h index 1b886c5f058..28ff62ec2e7 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h @@ -344,7 +344,7 @@ class Mouse : public SubsystemInterface Int m_maxY; ///< mouse is locked to this region UnsignedInt m_inputFrame; ///< frame input was gathered on - UnsignedInt m_deadInputFrame; ///< Frame which last input occured + UnsignedInt m_deadInputFrame; ///< Frame which last input occurred Bool m_inputMovesAbsolute; /**< if TRUE, when processing mouse position chanages the movement will be done treating diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp index b9760e04194..4b64881e241 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp @@ -3245,7 +3245,7 @@ void W3DRoadBuffer::updateLighting(void) Reactor.) Result: - As soon as the fence is set up, the player who Alt-tab'd would get Zero Hour crashing to desktop with Serious Error occured. + As soon as the fence is set up, the player who Alt-tab'd would get Zero Hour crashing to desktop with Serious Error occurred. */ if( !m_roads ) { From 6d5b60c0dfb39cea47938585e8c9329893d2afa4 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 16:15:11 +0200 Subject: [PATCH 088/112] Remove unused var --- Core/GameEngine/Source/Common/ReplaySimulation.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index 00c64de066b..7f3a81a74c4 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -33,7 +33,6 @@ static int SimulateReplaysInThisProcess(const std::vector &filename // If we are not in headless mode, we need to run the replay in the engine. for (size_t i = 0; i < filenames.size(); i++) { - AsciiString filename = filenames[i]; TheRecorder->playbackFile(filenames[i]); TheWritableGlobalData->m_showReplayContinueButton = i != filenames.size()-1; TheGameEngine->execute(); From 754b0541d7bd7390101fe6bcd4029c90e48bb9fb Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 16:19:46 +0200 Subject: [PATCH 089/112] Add comment --- .../Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index 96e208fd246..a6f114f8093 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -543,7 +543,7 @@ WindowMsgHandledType ScoreScreenSystem( GameWindow *window, UnsignedInt msg, TheCampaignManager->setCampaign(AsciiString::TheEmptyString); if (!TheGlobalData->m_simulateReplays.empty()) { - TheWritableGlobalData->m_showReplayContinueButton = FALSE; + TheWritableGlobalData->m_showReplayContinueButton = FALSE; // Tell GameEngine to quit. TheGameEngine->setQuitting(TRUE); } } From 7951bc4edb2e0ec3ec7f180d7963f8ec9398783f Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 16:40:50 +0200 Subject: [PATCH 090/112] Fix wrong automatic merges. --- .../Code/GameEngine/Source/Common/CommandLine.cpp | 10 +++++++--- .../Code/GameEngine/Source/Common/ReplayListCsv.cpp | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 39371567520..f1d0e03f874 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -445,10 +445,10 @@ Int parseReplay(char *args[], int num) Int parseSimReplayList(char *args[], int num) { - if (TheWritableGlobalData && num > 1) + if (num > 1) { AsciiString filename = args[1]; - ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplayList); + ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplays); TheWritableGlobalData->m_headless = TRUE; return 2; } @@ -457,7 +457,7 @@ Int parseSimReplayList(char *args[], int num) Int parseWriteReplayList(char *args[], int num) { - if (TheWritableGlobalData && num > 1) + if (num > 1) { TheWritableGlobalData->m_writeReplayList = args[1]; TheWritableGlobalData->m_headless = TRUE; @@ -1179,6 +1179,10 @@ static CommandLineParam paramsForStartup[] = // You can pass this multiple times to play back multiple replays. // You can also include wildcards. The file must be in the replay folder or in a subfolder. { "-replay", parseReplay }, + + // TheSuperHackers @feature helmutbuhler 28/04/2025 + // Pass in a csv file to play back multiple replays. The file must be in the replay folder. + { "-replayList", parseSimReplayList }, // TheSuperHackers @feature helmutbuhler 23/05/2025 // Simulate each replay in a separate process and use up two N processes at the same time. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp index 56f6889e37b..a5f043c19c4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp @@ -136,9 +136,9 @@ bool WriteOutReplayList(AsciiString relativeFolder) fprintf(fp, ",%s", i == -1 ? "mapExists" : md ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "crash" : header.endTime == 0 ? "1" : "0"); - fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameDuration); + fprintf(fp, i == -1 ? ",frames" : ",%d", header.frameCount); - UnsignedInt gameTime = header.frameDuration / LOGICFRAMES_PER_SECOND; + UnsignedInt gameTime = header.frameCount / LOGICFRAMES_PER_SECOND; fprintf(fp, i == -1 ? ",time" : ",%02d:%02d", gameTime/60, gameTime%60); fprintf(fp, i == -1 ? ",numHumans" : ",%d", numHumans); From 104a25bef5791d94770c61bdf64555fd8af58ca7 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 18:43:37 +0200 Subject: [PATCH 091/112] Add console output in case map is not found --- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 630b3973ad4..7c3b1e3dcf8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -839,9 +839,11 @@ static void populateRandomStartPosition( GameInfo *game ) Int i; Int numPlayers = MAX_SLOTS; const MapMetaData *md = TheMapCache->findMap( game->getMap() ); - DEBUG_ASSERTCRASH( md , ("Could not find map %s in the mapcache", game->getMap().str())); if (md) numPlayers = md->m_numPlayers; + else + printf("Could not find map \"%s\"\n", game->getMap().str()); + DEBUG_ASSERTCRASH( md , ("Could not find map %s in the mapcache", game->getMap().str())); // generate a map of start spot distances Real startSpotDistance[MAX_SLOTS][MAX_SLOTS]; From a3a21b96ff49741d67989948b969f55d565b5d19 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:48:45 +0200 Subject: [PATCH 092/112] Revert "spelling occured -> occurred" in part This reverts commit a1266af1eb8e9908d25a979153433821a0d889ed. --- GeneralsMD/Code/GameEngine/Include/Common/file.h | 6 +++--- GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h | 2 +- .../Source/W3DDevice/GameClient/W3DRoadBuffer.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/file.h b/GeneralsMD/Code/GameEngine/Include/Common/file.h index 8e9609978b4..c81c8c247a8 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/file.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/file.h @@ -131,15 +131,15 @@ class File : public MemoryPoolObject virtual Int read( void *buffer, Int bytes ) = NULL ; /**< Read the specified number of bytes from the file in to the * memory pointed at by buffer. Returns the number of bytes read. - * Returns -1 if an error occurred. + * Returns -1 if an error occured. */ virtual Int write( const void *buffer, Int bytes ) = NULL ; /**< Write the specified number of bytes from the * memory pointed at by buffer to the file. Returns the number of bytes written. - * Returns -1 if an error occurred. + * Returns -1 if an error occured. */ virtual Int seek( Int bytes, seekMode mode = CURRENT ) = NULL; /**< Sets the file position of the next read/write operation. Returns the new file * position as the number of bytes from the start of the file. - * Returns -1 if an error occurred. + * Returns -1 if an error occured. * * seekMode determines how the seek is done: * diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h index 28ff62ec2e7..1b886c5f058 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Mouse.h @@ -344,7 +344,7 @@ class Mouse : public SubsystemInterface Int m_maxY; ///< mouse is locked to this region UnsignedInt m_inputFrame; ///< frame input was gathered on - UnsignedInt m_deadInputFrame; ///< Frame which last input occurred + UnsignedInt m_deadInputFrame; ///< Frame which last input occured Bool m_inputMovesAbsolute; /**< if TRUE, when processing mouse position chanages the movement will be done treating diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp index 4b64881e241..b9760e04194 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DRoadBuffer.cpp @@ -3245,7 +3245,7 @@ void W3DRoadBuffer::updateLighting(void) Reactor.) Result: - As soon as the fence is set up, the player who Alt-tab'd would get Zero Hour crashing to desktop with Serious Error occurred. + As soon as the fence is set up, the player who Alt-tab'd would get Zero Hour crashing to desktop with Serious Error occured. */ if( !m_roads ) { From cb021ff8da1a90c41697acbf45b595443ecde778 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:10:37 +0200 Subject: [PATCH 093/112] Refactor ClientInstance code --- .../Include/GameClient/ClientInstance.h | 14 +++++--- .../GameEngine/Source/Common/CommandLine.cpp | 4 +-- .../Source/GameClient/ClientInstance.cpp | 36 +++++++++++++++---- .../Source/GameNetwork/IPEnumeration.cpp | 2 +- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h index 8edf6bb6da7..1f1ada1daca 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h @@ -30,6 +30,15 @@ class ClientInstance static bool isInitialized(); + static bool isMultiInstance(); + + // Change multi instance on runtime. Must be called before initialize. + static void setMultiInstance(bool v); + + // Skips using the primary instance. Must be called before initialize. + // Useful when the new process is not meant to collide with another normal Generals process. + static void skipPrimaryInstance(); + // Returns the instance index of this game client. Starts at 0. static UnsignedInt getInstanceIndex(); @@ -39,13 +48,10 @@ class ClientInstance // Returns the instance name of the first game client. static const char* getFirstInstanceName(); -public: - static Bool s_multiInstance; // Allow multiple instances of the game - static Bool s_avoidFirstInstance; // Avoid mutex of original game - private: static HANDLE s_mutexHandle; static UnsignedInt s_instanceIndex; + static Bool s_isMultiInstance; }; } // namespace rts diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index a0f1cd50013..a21562aceb3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -434,8 +434,8 @@ Int parseReplay(char *args[], int num) TheWritableGlobalData->m_shellMapOn = FALSE; // Make replay playback possible while other clients (possible retail) are running - rts::ClientInstance::s_multiInstance = TRUE; - rts::ClientInstance::s_avoidFirstInstance = TRUE; + rts::ClientInstance::setMultiInstance(TRUE); + rts::ClientInstance::skipPrimaryInstance(); return 2; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp index 9cb0cf16bca..174301bab11 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp @@ -24,12 +24,12 @@ namespace rts { HANDLE ClientInstance::s_mutexHandle = NULL; UnsignedInt ClientInstance::s_instanceIndex = 0; + #if defined(RTS_MULTI_INSTANCE) -Bool ClientInstance::s_multiInstance = TRUE; +Bool ClientInstance::s_isMultiInstance = true; #else -Bool ClientInstance::s_multiInstance = FALSE; +Bool ClientInstance::s_isMultiInstance = false; #endif -Bool ClientInstance::s_avoidFirstInstance = FALSE; bool ClientInstance::initialize() { @@ -38,14 +38,11 @@ bool ClientInstance::initialize() return true; } - if (s_avoidFirstInstance) - ++s_instanceIndex; - // Create a mutex with a unique name to Generals in order to determine if our app is already running. // WARNING: DO NOT use this number for any other application except Generals. while (true) { - if (s_multiInstance) + if (isMultiInstance()) { std::string guidStr = getFirstInstanceName(); if (s_instanceIndex > 0u) @@ -92,6 +89,31 @@ bool ClientInstance::isInitialized() return s_mutexHandle != NULL; } +bool ClientInstance::isMultiInstance() +{ + return s_isMultiInstance; +} + +void ClientInstance::setMultiInstance(bool v) +{ + if (isInitialized()) + { + DEBUG_CRASH(("ClientInstance::setMultiInstance(%d) - cannot set multi instance after initialization", (int)v)); + return; + } + s_isMultiInstance = v; +} + +void ClientInstance::skipPrimaryInstance() +{ + if (isInitialized()) + { + DEBUG_CRASH(("ClientInstance::skipPrimaryInstance(%d) - cannot skip primary instance after initialization", (int)v)); + return; + } + s_instanceIndex = 1; +} + UnsignedInt ClientInstance::getInstanceIndex() { DEBUG_ASSERTLOG(isInitialized(), ("ClientInstance::isInitialized() failed")); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp index 0e31d4018ea..0d0a43018fe 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp @@ -98,7 +98,7 @@ EnumeratedIP * IPEnumeration::getAddresses( void ) } // TheSuperHackers @feature Add one unique local host IP address for each multi client instance. - if (rts::ClientInstance::s_multiInstance) + if (rts::ClientInstance::isMultiInstance()) { const UnsignedInt id = rts::ClientInstance::getInstanceId(); addNewIP( From a4bf57f00f427be0345a91b25506f141f3734b56 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:03:28 +0200 Subject: [PATCH 094/112] Remove GlobalData::m_showReplayContinueButton --- .../Include/Common/ReplaySimulation.h | 34 ++++++++++++++--- .../Source/Common/ReplaySimulation.cpp | 37 +++++++++++++------ .../GameEngine/Include/Common/GlobalData.h | 1 - .../GameEngine/Source/Common/GameMain.cpp | 2 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 - .../GUI/GUICallbacks/Menus/ScoreScreen.cpp | 17 ++++++--- 6 files changed, 68 insertions(+), 24 deletions(-) diff --git a/Core/GameEngine/Include/Common/ReplaySimulation.h b/Core/GameEngine/Include/Common/ReplaySimulation.h index 48a82402432..5118f337141 100644 --- a/Core/GameEngine/Include/Common/ReplaySimulation.h +++ b/Core/GameEngine/Include/Common/ReplaySimulation.h @@ -18,8 +18,32 @@ #pragma once -// TheSuperHackers @feature helmutbuhler 13/04/2025 -// Simulate a list of replays without graphics. -// Returns exitcode 1 if mismatch or other error occurred -// Returns exitcode 0 if all replays were successfully simulated without mismatches -int SimulateReplays(const std::vector &filenames, int maxProcesses); +class ReplaySimulation +{ +public: + + // TheSuperHackers @feature helmutbuhler 13/04/2025 + // Simulate a list of replays without graphics. + // Returns exit code 1 if mismatch or other error occurred + // Returns exit code 0 if all replays were successfully simulated without mismatches + static int simulateReplays(const std::vector &filenames, int maxProcesses); + + static void stop() { s_isRunning = false; } + + static Bool isRunning() { return s_isRunning; } + static UnsignedInt getCurrentReplayIndex() { return s_replayIndex; } + static UnsignedInt getReplayCount() { return s_replayCount; } + +private: + + static int simulateReplaysInThisProcess(const std::vector &filenames); + static int simulateReplaysInWorkerProcesses(const std::vector &filenames, int maxProcesses); + static std::vector resolveFilenameWildcards(const std::vector &filenames); + + +private: + + static Bool s_isRunning; + static UnsignedInt s_replayIndex; + static UnsignedInt s_replayCount; +}; diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index 7f3a81a74c4..dfe6ac332f5 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -18,6 +18,8 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include "Common/ReplaySimulation.h" + #include "Common/GameEngine.h" #include "Common/LocalFileSystem.h" #include "Common/Recorder.h" @@ -25,23 +27,36 @@ #include "GameLogic/GameLogic.h" #include "GameClient/GameClient.h" -static int SimulateReplaysInThisProcess(const std::vector &filenames) + +Bool ReplaySimulation::s_isRunning = false; +UnsignedInt ReplaySimulation::s_replayIndex = 0; +UnsignedInt ReplaySimulation::s_replayCount = 0; + + +int ReplaySimulation::simulateReplaysInThisProcess(const std::vector &filenames) { int numErrors = 0; + if (!TheGlobalData->m_headless) { + s_isRunning = true; + s_replayIndex = 0; + s_replayCount = static_cast(filenames.size()); + // If we are not in headless mode, we need to run the replay in the engine. - for (size_t i = 0; i < filenames.size(); i++) + for (; s_replayIndex < s_replayCount; ++s_replayIndex) { - TheRecorder->playbackFile(filenames[i]); - TheWritableGlobalData->m_showReplayContinueButton = i != filenames.size()-1; + TheRecorder->playbackFile(filenames[s_replayIndex]); TheGameEngine->execute(); if (TheRecorder->sawCRCMismatch()) numErrors++; - if (!TheGlobalData->m_showReplayContinueButton) + if (!s_isRunning) break; TheGameEngine->setQuitting(FALSE); } + s_isRunning = false; + s_replayIndex = 0; + s_replayCount = 0; return numErrors != 0 ? 1 : 0; } // Note that we use printf here because this is run from cmd. @@ -100,7 +115,7 @@ static int SimulateReplaysInThisProcess(const std::vector &filename return numErrors != 0 ? 1 : 0; } -static int SimulateReplaysInWorkerProcesses(const std::vector &filenames, int maxProcesses) +int ReplaySimulation::simulateReplaysInWorkerProcesses(const std::vector &filenames, int maxProcesses) { DWORD totalStartTimeMillis = GetTickCount(); @@ -180,7 +195,7 @@ static int SimulateReplaysInWorkerProcesses(const std::vector &file return numErrors != 0 ? 1 : 0; } -std::vector ResolveFilenameWildcards(const std::vector &filenames) +std::vector ReplaySimulation::resolveFilenameWildcards(const std::vector &filenames) { // If some filename contains wildcards, search for actual filenames. // Note that we cannot do this in parseReplay because we require TheLocalFileSystem initialized. @@ -222,11 +237,11 @@ std::vector ResolveFilenameWildcards(const std::vector return filenamesResolved; } -int SimulateReplays(const std::vector &filenames, int maxProcesses) +int ReplaySimulation::simulateReplays(const std::vector &filenames, int maxProcesses) { - std::vector filenamesResolved = ResolveFilenameWildcards(filenames); + std::vector filenamesResolved = resolveFilenameWildcards(filenames); if (maxProcesses == SIMULATE_REPLAYS_SEQUENTIAL) - return SimulateReplaysInThisProcess(filenamesResolved); + return simulateReplaysInThisProcess(filenamesResolved); else - return SimulateReplaysInWorkerProcesses(filenamesResolved, maxProcesses); + return simulateReplaysInWorkerProcesses(filenamesResolved, maxProcesses); } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 732200ff234..b99762db294 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -353,7 +353,6 @@ class GlobalData : public SubsystemInterface std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation - Bool m_showReplayContinueButton; Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index d667eab4c09..7ee88428c06 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -44,7 +44,7 @@ Int GameMain() if (!TheGlobalData->m_simulateReplays.empty()) { - exitcode = SimulateReplays(TheGlobalData->m_simulateReplays, TheGlobalData->m_simulateReplayJobs); + exitcode = ReplaySimulation::simulateReplays(TheGlobalData->m_simulateReplays, TheGlobalData->m_simulateReplayJobs); } else { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e93fe94a7c7..11af51a8109 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -959,7 +959,6 @@ GlobalData::GlobalData() m_simulateReplays.clear(); m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; - m_showReplayContinueButton = FALSE; for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index a6f114f8093..b32a8dc1b32 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -67,6 +67,7 @@ #include "Common/PlayerTemplate.h" #include "Common/RandomValue.h" #include "Common/Recorder.h" +#include "Common/ReplaySimulation.h" #include "Common/ScoreKeeper.h" #include "Common/SkirmishBattleHonors.h" #include "Common/ThingFactory.h" @@ -503,6 +504,11 @@ WindowMsgHandledType ScoreScreenInput( GameWindow *window, UnsignedInt msg, } // end MainMenuInput +static Bool showButtonContinue() +{ + return ReplaySimulation::getCurrentReplayIndex() != ReplaySimulation::getReplayCount()-1; +} + /** System Function for the ScoreScreen */ //------------------------------------------------------------------------------------------------- WindowMsgHandledType ScoreScreenSystem( GameWindow *window, UnsignedInt msg, @@ -541,15 +547,16 @@ WindowMsgHandledType ScoreScreenSystem( GameWindow *window, UnsignedInt msg, { TheShell->pop(); TheCampaignManager->setCampaign(AsciiString::TheEmptyString); - if (!TheGlobalData->m_simulateReplays.empty()) + + if ( ReplaySimulation::getReplayCount() > 0 ) { - TheWritableGlobalData->m_showReplayContinueButton = FALSE; // Tell GameEngine to quit. + ReplaySimulation::stop(); TheGameEngine->setQuitting(TRUE); } } else if ( controlID == buttonContinueID ) { - if (TheGlobalData->m_showReplayContinueButton) + if( ReplaySimulation::getReplayCount() > 0 ) { TheGameEngine->setQuitting(TRUE); } @@ -996,7 +1003,7 @@ void initReplaySinglePlayer( void ) if (chatBoxBorder) chatBoxBorder->winHide(TRUE); if (buttonContinue) - buttonContinue->winHide(!TheGlobalData->m_showReplayContinueButton); + buttonContinue->winHide(!showButtonContinue()); if (buttonBuddies) buttonBuddies->winHide(TRUE); if (listboxChatWindowScoreScreen) @@ -1103,7 +1110,7 @@ void initReplayMultiPlayer(void) if (chatBoxBorder) chatBoxBorder->winHide(TRUE); if (buttonContinue) - buttonContinue->winHide(!TheGlobalData->m_showReplayContinueButton); + buttonContinue->winHide(!showButtonContinue()); if (buttonBuddies) buttonBuddies->winHide(TRUE); // if (buttonRehost) From a0ecf5aab3230f465a61f3e48a5453f826a5f422 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:11:26 +0200 Subject: [PATCH 095/112] Fix SIMULATE_REPLAYS_SEQUENTIAL constexpr'ness --- Dependencies/Utility/Utility/CppMacros.h | 6 ++++-- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Dependencies/Utility/Utility/CppMacros.h b/Dependencies/Utility/Utility/CppMacros.h index 57aaa75abc8..b1babaa2797 100644 --- a/Dependencies/Utility/Utility/CppMacros.h +++ b/Dependencies/Utility/Utility/CppMacros.h @@ -33,9 +33,11 @@ #endif #if __cplusplus >= 201103L - #define CPP_11(code) code + #define CPP_11(code) code + #define CONSTEXPR constexpr #else - #define CPP_11(code) + #define CPP_11(code) + #define CONSTEXPR constexpr #endif #if __cplusplus >= 201703L diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index b99762db294..27b15997d3c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -54,8 +54,8 @@ enum AIDebugOptions CPP_11(: Int); // PUBLIC ///////////////////////////////////////////////////////////////////////////////////////// -const Int MAX_GLOBAL_LIGHTS = 3; -const Int SIMULATE_REPLAYS_SEQUENTIAL = -1; +CONSTEXPR const Int MAX_GLOBAL_LIGHTS = 3; +CONSTEXPR const Int SIMULATE_REPLAYS_SEQUENTIAL = -1; //------------------------------------------------------------------------------------------------- class CommandLineData From ee4a5546c351c43261a35b2cd463cb5b8982776c Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:16:33 +0200 Subject: [PATCH 096/112] Return WorkerProcess::startProcess if CreatePipe has failed --- Core/GameEngine/Source/Common/WorkerProcess.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/Common/WorkerProcess.cpp b/Core/GameEngine/Source/Common/WorkerProcess.cpp index df8d238971f..ff55eeb7c29 100644 --- a/Core/GameEngine/Source/Common/WorkerProcess.cpp +++ b/Core/GameEngine/Source/Common/WorkerProcess.cpp @@ -83,7 +83,8 @@ bool WorkerProcess::startProcess(UnicodeString command) SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; saAttr.bInheritHandle = TRUE; HANDLE writeHandle = NULL; - CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0); + if (!CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0)) + return false; SetHandleInformation(m_readHandle, HANDLE_FLAG_INHERIT, 0); STARTUPINFOW si = { sizeof(STARTUPINFOW) }; From f80d4324efd63cef4566341cfb99aa4f1fa61af8 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:26:49 +0200 Subject: [PATCH 097/112] Add assert for m_readHandle --- Core/GameEngine/Source/Common/WorkerProcess.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/GameEngine/Source/Common/WorkerProcess.cpp b/Core/GameEngine/Source/Common/WorkerProcess.cpp index ff55eeb7c29..5514c4c2882 100644 --- a/Core/GameEngine/Source/Common/WorkerProcess.cpp +++ b/Core/GameEngine/Source/Common/WorkerProcess.cpp @@ -149,6 +149,7 @@ bool WorkerProcess::fetchStdOutput() { // Call PeekNamedPipe to make sure ReadFile won't block DWORD bytesAvailable = 0; + DEBUG_ASSERTCRASH(m_readHandle != NULL, ("Is not expected NULL")); BOOL success = PeekNamedPipe(m_readHandle, NULL, 0, NULL, &bytesAvailable, NULL); if (!success) return true; From 6b72afd92994568704220841798f8d9cbc74ece9 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:27:20 +0200 Subject: [PATCH 098/112] Clarify -jobs range in comment --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index a21562aceb3..dc967b179c4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -1158,7 +1158,7 @@ static CommandLineParam paramsForStartup[] = { "-replay", parseReplay }, // TheSuperHackers @feature helmutbuhler 23/05/2025 - // Simulate each replay in a separate process and use up two N processes at the same time. + // Simulate each replay in a separate process and use 1..N processes at the same time. // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, From 7dbe6f7e198a07a077c015125faae87407baa835 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:33:37 +0200 Subject: [PATCH 099/112] Move logic to count running processes into separate function --- .../Include/Common/ReplaySimulation.h | 1 - .../Source/Common/ReplaySimulation.cpp | 22 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Core/GameEngine/Include/Common/ReplaySimulation.h b/Core/GameEngine/Include/Common/ReplaySimulation.h index 5118f337141..219f6233709 100644 --- a/Core/GameEngine/Include/Common/ReplaySimulation.h +++ b/Core/GameEngine/Include/Common/ReplaySimulation.h @@ -40,7 +40,6 @@ class ReplaySimulation static int simulateReplaysInWorkerProcesses(const std::vector &filenames, int maxProcesses); static std::vector resolveFilenameWildcards(const std::vector &filenames); - private: static Bool s_isRunning; diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index dfe6ac332f5..354925c8fcd 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -32,6 +32,20 @@ Bool ReplaySimulation::s_isRunning = false; UnsignedInt ReplaySimulation::s_replayIndex = 0; UnsignedInt ReplaySimulation::s_replayCount = 0; +namespace +{ +int countProcessesRunning(const std::vector& processes) +{ + int numProcessesRunning = 0; + size_t i = 0; + for (; i < processes.size(); ++i) + { + if (processes[i].isRunning()) + ++numProcessesRunning; + } + return numProcessesRunning; +} +} // namespace int ReplaySimulation::simulateReplaysInThisProcess(const std::vector &filenames) { @@ -149,13 +163,7 @@ int ReplaySimulation::simulateReplaysInWorkerProcesses(const std::vector Date: Fri, 20 Jun 2025 12:15:40 +0200 Subject: [PATCH 100/112] Fix constexpr in VC6 build --- Dependencies/Utility/Utility/CppMacros.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dependencies/Utility/Utility/CppMacros.h b/Dependencies/Utility/Utility/CppMacros.h index b1babaa2797..8043b54b25d 100644 --- a/Dependencies/Utility/Utility/CppMacros.h +++ b/Dependencies/Utility/Utility/CppMacros.h @@ -37,7 +37,7 @@ #define CONSTEXPR constexpr #else #define CPP_11(code) - #define CONSTEXPR constexpr + #define CONSTEXPR #endif #if __cplusplus >= 201703L From 118ef4d850e45ebb007838581a818b78b168839d Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:20:26 +0200 Subject: [PATCH 101/112] Fix debug compile error --- GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp index 174301bab11..59edcc3c325 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/ClientInstance.cpp @@ -108,7 +108,7 @@ void ClientInstance::skipPrimaryInstance() { if (isInitialized()) { - DEBUG_CRASH(("ClientInstance::skipPrimaryInstance(%d) - cannot skip primary instance after initialization", (int)v)); + DEBUG_CRASH(("ClientInstance::skipPrimaryInstance() - cannot skip primary instance after initialization")); return; } s_instanceIndex = 1; From 05ca54e1815e2ad0e5129cc905c8b6f92ad95fbe Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 00:00:51 +0200 Subject: [PATCH 102/112] Auto Sync with Generals --- Generals/Code/GameEngine/CMakeLists.txt | 3 + .../GameEngine/Include/Common/GameEngine.h | 3 +- .../GameEngine/Include/Common/GlobalData.h | 8 +- .../Code/GameEngine/Include/Common/Recorder.h | 12 ++- .../Include/GameClient/ClientInstance.h | 10 +++ .../Include/GameClient/GameClient.h | 2 + .../GameEngine/Source/Common/CommandLine.cpp | 73 ++++++++++++++-- .../GameEngine/Source/Common/GameEngine.cpp | 9 +- .../GameEngine/Source/Common/GameMain.cpp | 20 +++-- .../GameEngine/Source/Common/GlobalData.cpp | 3 + .../GameEngine/Source/Common/Recorder.cpp | 66 ++++++++++----- .../GameEngine/Source/Common/System/Debug.cpp | 3 +- .../Source/GameClient/ClientInstance.cpp | 84 +++++++++++++------ .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 6 +- .../GUI/GUICallbacks/Menus/ScoreScreen.cpp | 47 +++++++---- .../Source/GameClient/GUI/Shell/Shell.cpp | 4 +- .../Source/GameClient/GameClient.cpp | 21 +++-- .../Source/GameLogic/System/GameLogic.cpp | 37 ++++---- .../GameLogic/System/GameLogicDispatch.cpp | 6 +- .../Source/GameNetwork/IPEnumeration.cpp | 17 ++-- Generals/Code/Main/WinMain.cpp | 4 +- 21 files changed, 309 insertions(+), 129 deletions(-) diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt index f1fe224b5b2..eed55eae5b6 100644 --- a/Generals/Code/GameEngine/CMakeLists.txt +++ b/Generals/Code/GameEngine/CMakeLists.txt @@ -91,6 +91,7 @@ set(GAMEENGINE_SRC Include/Common/RAMFile.h Include/Common/RandomValue.h Include/Common/Recorder.h +# Include/Common/ReplaySimulation.h Include/Common/Registry.h Include/Common/ResourceGatheringManager.h Include/Common/Science.h @@ -123,6 +124,7 @@ set(GAMEENGINE_SRC Include/Common/UserPreferences.h Include/Common/version.h Include/Common/WellKnownKeys.h +# Include/Common/WorkerProcess.h # Include/Common/Xfer.h # Include/Common/XferCRC.h # Include/Common/XferDeepCRC.h @@ -633,6 +635,7 @@ set(GAMEENGINE_SRC Source/Common/Thing/ThingTemplate.cpp Source/Common/UserPreferences.cpp Source/Common/version.cpp +# Source/Common/WorkerProcess.cpp Source/GameClient/ClientInstance.cpp Source/GameClient/Color.cpp Source/GameClient/Credits.cpp diff --git a/Generals/Code/GameEngine/Include/Common/GameEngine.h b/Generals/Code/GameEngine/Include/Common/GameEngine.h index e483db6548d..0b2ab9f6137 100644 --- a/Generals/Code/GameEngine/Include/Common/GameEngine.h +++ b/Generals/Code/GameEngine/Include/Common/GameEngine.h @@ -67,7 +67,6 @@ class GameEngine : public SubsystemInterface virtual ~GameEngine(); virtual void init( void ); ///< Init engine by creating client and logic - virtual void init( int argc, char *argv[] ); ///< Init engine by creating client and logic virtual void reset( void ); ///< reset system to starting state virtual void update( void ); ///< per frame update @@ -115,6 +114,6 @@ extern GameEngine *TheGameEngine; extern GameEngine *CreateGameEngine( void ); /// The entry point for the game system -extern void GameMain( int argc, char *argv[] ); +extern Int GameMain(); #endif // _GAME_ENGINE_H_ diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 4d06757f7f3..5cbd6975ef4 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -52,7 +52,8 @@ enum AIDebugOptions CPP_11(: Int); // PUBLIC ///////////////////////////////////////////////////////////////////////////////////////// -const Int MAX_GLOBAL_LIGHTS = 3; +CONSTEXPR const Int MAX_GLOBAL_LIGHTS = 3; +CONSTEXPR const Int SIMULATE_REPLAYS_SEQUENTIAL = -1; //------------------------------------------------------------------------------------------------- class CommandLineData @@ -338,9 +339,12 @@ class GlobalData : public SubsystemInterface Real m_cameraAdjustSpeed; ///< Rate at which we adjust camera height Bool m_enforceMaxCameraHeight; ///< Enfoce max camera height while scrolling? Bool m_buildMapCache; - AsciiString m_initialFile; ///< If this is specified, load a specific map/replay from the command-line + AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start + std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. + Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation + Int m_maxParticleCount; ///< maximum number of particles that can exist Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly) WeaponBonusSet* m_weaponBonusSet; diff --git a/Generals/Code/GameEngine/Include/Common/Recorder.h b/Generals/Code/GameEngine/Include/Common/Recorder.h index fefd82c89d4..7663b8dc919 100644 --- a/Generals/Code/GameEngine/Include/Common/Recorder.h +++ b/Generals/Code/GameEngine/Include/Common/Recorder.h @@ -48,6 +48,7 @@ class ReplayGameInfo : public GameInfo enum RecorderModeType CPP_11(: Int) { RECORDERMODETYPE_RECORD, RECORDERMODETYPE_PLAYBACK, + RECORDERMODETYPE_SIMULATION_PLAYBACK, // Play back replay without any graphics RECORDERMODETYPE_NONE // this is a valid state to be in on the shell map, or in saved games }; @@ -74,11 +75,13 @@ class RecorderClass : public SubsystemInterface { Bool replayMatchesGameVersion(AsciiString filename); ///< Returns true if the playback is a valid playback file for this version. static Bool replayMatchesGameVersion(const ReplayHeader& header); ///< Returns true if the playback is a valid playback file for this version. AsciiString getCurrentReplayFilename( void ); ///< valid during playback only + UnsignedInt getPlaybackFrameCount() const { return m_playbackFrameCount; } ///< valid during playback only void stopPlayback(); ///< Stops playback. Its fine to call this even if not playing back a file. + Bool simulateReplay(AsciiString filename); #if defined RTS_DEBUG || defined RTS_INTERNAL Bool analyzeReplay( AsciiString filename ); - Bool isAnalysisInProgress( void ); #endif + Bool isPlaybackInProgress() const; public: void handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback); @@ -100,7 +103,7 @@ class RecorderClass : public SubsystemInterface { UnsignedInt iniCRC; time_t startTime; time_t endTime; - UnsignedInt frameDuration; + UnsignedInt frameCount; Bool quitEarly; Bool desyncGame; Bool playerDiscons[MAX_SLOTS]; @@ -110,10 +113,11 @@ class RecorderClass : public SubsystemInterface { Bool readReplayHeader( ReplayHeader& header ); RecorderModeType getMode(); ///< Returns the current operating mode. + Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; } void initControls(); ///< Show or Hide the Replay controls AsciiString getReplayDir(); ///< Returns the directory that holds the replay files. - AsciiString getReplayExtention(); ///< Returns the file extention for replay files. + static AsciiString getReplayExtention(); ///< Returns the file extention for replay files. AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay. GameInfo *getGameInfo( void ) { return &m_gameInfo; } ///< Returns the slot list for playback game start @@ -124,6 +128,7 @@ class RecorderClass : public SubsystemInterface { void logPlayerDisconnect(UnicodeString player, Int slot); void logCRCMismatch( void ); + Bool sawCRCMismatch() const; void cleanUpReplayFile( void ); ///< after a crash, send replay/debug info to a central repository void stopRecording(); ///< Stop recording and close m_file. @@ -154,6 +159,7 @@ class RecorderClass : public SubsystemInterface { Int m_currentFilePosition; RecorderModeType m_mode; AsciiString m_currentReplayFilename; ///< valid during playback only + UnsignedInt m_playbackFrameCount; ReplayGameInfo m_gameInfo; Bool m_wasDesync; diff --git a/Generals/Code/GameEngine/Include/GameClient/ClientInstance.h b/Generals/Code/GameEngine/Include/GameClient/ClientInstance.h index 74e7add404b..1f1ada1daca 100644 --- a/Generals/Code/GameEngine/Include/GameClient/ClientInstance.h +++ b/Generals/Code/GameEngine/Include/GameClient/ClientInstance.h @@ -30,6 +30,15 @@ class ClientInstance static bool isInitialized(); + static bool isMultiInstance(); + + // Change multi instance on runtime. Must be called before initialize. + static void setMultiInstance(bool v); + + // Skips using the primary instance. Must be called before initialize. + // Useful when the new process is not meant to collide with another normal Generals process. + static void skipPrimaryInstance(); + // Returns the instance index of this game client. Starts at 0. static UnsignedInt getInstanceIndex(); @@ -42,6 +51,7 @@ class ClientInstance private: static HANDLE s_mutexHandle; static UnsignedInt s_instanceIndex; + static Bool s_isMultiInstance; }; } // namespace rts diff --git a/Generals/Code/GameEngine/Include/GameClient/GameClient.h b/Generals/Code/GameEngine/Include/GameClient/GameClient.h index d599305b21c..f0c6a708029 100644 --- a/Generals/Code/GameEngine/Include/GameClient/GameClient.h +++ b/Generals/Code/GameEngine/Include/GameClient/GameClient.h @@ -95,6 +95,8 @@ class GameClient : public SubsystemInterface, virtual void setFrame( UnsignedInt frame ) { m_frame = frame; } ///< Set the GameClient's internal frame number virtual void registerDrawable( Drawable *draw ); ///< Given a drawable, register it with the GameClient and give it a unique ID + void updateHeadless(); + void addDrawableToLookupTable( Drawable *draw ); ///< add drawable ID to hash lookup table void removeDrawableFromLookupTable( Drawable *draw ); ///< remove drawable ID from hash lookup table diff --git a/Generals/Code/GameEngine/Source/Common/CommandLine.cpp b/Generals/Code/GameEngine/Source/Common/CommandLine.cpp index b01b0d10bdb..ec75d89b2fc 100644 --- a/Generals/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/Generals/Code/GameEngine/Source/Common/CommandLine.cpp @@ -29,7 +29,9 @@ #include "Common/CommandLine.h" #include "Common/CRCDebug.h" #include "Common/LocalFileSystem.h" +#include "Common/Recorder.h" #include "Common/version.h" +#include "GameClient/ClientInstance.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" @@ -405,6 +407,56 @@ Int parseMapName(char *args[], int num) return 1; } +Int parseHeadless(char *args[], int num) +{ + TheWritableGlobalData->m_headless = TRUE; + TheWritableGlobalData->m_playIntro = FALSE; + TheWritableGlobalData->m_afterIntro = TRUE; + TheWritableGlobalData->m_playSizzle = FALSE; + return 1; +} + +Int parseReplay(char *args[], int num) +{ + if (num > 1) + { + AsciiString filename = args[1]; + if (!filename.endsWithNoCase(RecorderClass::getReplayExtention())) + { + printf("Invalid replay name \"%s\"\n", filename.str()); + exit(1); + } + TheWritableGlobalData->m_simulateReplays.push_back(filename); + + TheWritableGlobalData->m_playIntro = FALSE; + TheWritableGlobalData->m_afterIntro = TRUE; + TheWritableGlobalData->m_playSizzle = FALSE; + TheWritableGlobalData->m_shellMapOn = FALSE; + + // Make replay playback possible while other clients (possible retail) are running + rts::ClientInstance::setMultiInstance(TRUE); + rts::ClientInstance::skipPrimaryInstance(); + + return 2; + } + return 1; +} + +Int parseJobs(char *args[], int num) +{ + if (num > 1) + { + TheWritableGlobalData->m_simulateReplayJobs = atoi(args[1]); + if (TheGlobalData->m_simulateReplayJobs < SIMULATE_REPLAYS_SEQUENTIAL || TheGlobalData->m_simulateReplayJobs == 0) + { + printf("Invalid number of jobs: %d\n", TheGlobalData->m_simulateReplayJobs); + exit(1); + } + return 2; + } + return 1; +} + Int parseXRes(char *args[], int num) { if (num > 1) @@ -782,13 +834,6 @@ Int parseQuickStart( char *args[], int num ) return 1; } -Int parseHeadless( char *args[], int num ) -{ - TheWritableGlobalData->m_headless = TRUE; - - return 1; -} - Int parseConstantDebug( char *args[], int num ) { TheWritableGlobalData->m_constantDebugUpdate = TRUE; @@ -1103,8 +1148,20 @@ static CommandLineParam paramsForStartup[] = { "-fullscreen", parseNoWin }, // TheSuperHackers @feature helmutbuhler 11/04/2025 - // This runs the game without a window, graphics, input and audio. Used for testing. + // This runs the game without a window, graphics, input and audio. You can combine this with -replay { "-headless", parseHeadless }, + + // TheSuperHackers @feature helmutbuhler 13/04/2025 + // Play back a replay. Pass the filename including .rep afterwards. + // You can pass this multiple times to play back multiple replays. + // You can also include wildcards. The file must be in the replay folder or in a subfolder. + { "-replay", parseReplay }, + + // TheSuperHackers @feature helmutbuhler 23/05/2025 + // Simulate each replay in a separate process and use 1..N processes at the same time. + // (If you have 4 cores, call it with -jobs 4) + // If you do not call this, all replays will be simulated in sequence in the same process. + { "-jobs", parseJobs }, }; // These Params are parsed during Engine Init before INI data is loaded diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index f6a97e93b21..4542419d928 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -250,8 +250,7 @@ void GameEngine::setFramesPerSecondLimit( Int fps ) /** ----------------------------------------------------------------------------------------------- * Initialize the game engine by initializing the GameLogic and GameClient. */ -void GameEngine::init( void ) {} /// @todo: I changed this to take argc & argv so we can parse those after the GDF is loaded. We need to rethink this immediately as it is a nasty hack -void GameEngine::init( int argc, char *argv[] ) +void GameEngine::init() { try { //create an INI object to use for loading stuff @@ -442,7 +441,7 @@ void GameEngine::init( int argc, char *argv[] ) // load the initial shell screen //TheShell->push( AsciiString("Menus/MainMenu.wnd") ); - // This allows us to run a map/reply from the command line + // This allows us to run a map from the command line if (TheGlobalData->m_initialFile.isEmpty() == FALSE) { AsciiString fname = TheGlobalData->m_initialFile; @@ -464,10 +463,6 @@ void GameEngine::init( int argc, char *argv[] ) msg->appendIntegerArgument(0); InitRandom(0); } - else if (fname.endsWithNoCase(".rep")) - { - TheRecorder->playbackFile(fname); - } } // diff --git a/Generals/Code/GameEngine/Source/Common/GameMain.cpp b/Generals/Code/GameEngine/Source/Common/GameMain.cpp index a8cbc291f91..cf5f2e3be84 100644 --- a/Generals/Code/GameEngine/Source/Common/GameMain.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameMain.cpp @@ -29,23 +29,33 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "Common/GameEngine.h" +#include "Common/ReplaySimulation.h" /** * This is the entry point for the game system. */ -void GameMain( int argc, char *argv[] ) +Int GameMain() { + int exitcode = 0; // initialize the game engine using factory function TheGameEngine = CreateGameEngine(); - TheGameEngine->init(argc, argv); - - // run it - TheGameEngine->execute(); + TheGameEngine->init(); + + if (!TheGlobalData->m_simulateReplays.empty()) + { + exitcode = ReplaySimulation::simulateReplays(TheGlobalData->m_simulateReplays, TheGlobalData->m_simulateReplayJobs); + } + else + { + // run it + TheGameEngine->execute(); + } // since execute() returned, we are exiting the game delete TheGameEngine; TheGameEngine = NULL; + return exitcode; } diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index b7f04ba32c2..5f9e9cd2942 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -948,6 +948,9 @@ GlobalData::GlobalData() m_initialFile.clear(); m_pendingFile.clear(); + m_simulateReplays.clear(); + m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; + for (i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) m_healthBonus[i] = 1.0f; diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index dacd872a046..bd1b458c7b5 100644 --- a/Generals/Code/GameEngine/Source/Common/Recorder.cpp +++ b/Generals/Code/GameEngine/Source/Common/Recorder.cpp @@ -67,8 +67,8 @@ typedef int32_t replay_time_t; static time_t startTime; static const UnsignedInt startTimeOffset = 6; static const UnsignedInt endTimeOffset = startTimeOffset + sizeof(replay_time_t); -static const UnsignedInt framesOffset = endTimeOffset + sizeof(replay_time_t); -static const UnsignedInt desyncOffset = framesOffset + sizeof(UnsignedInt); +static const UnsignedInt frameCountOffset = endTimeOffset + sizeof(replay_time_t); +static const UnsignedInt desyncOffset = frameCountOffset + sizeof(UnsignedInt); static const UnsignedInt quitEarlyOffset = desyncOffset + sizeof(Bool); static const UnsignedInt disconOffset = quitEarlyOffset + sizeof(Bool); @@ -232,7 +232,7 @@ void RecorderClass::logGameEnd( void ) time_t t; time(&t); - UnsignedInt duration = TheGameLogic->getFrame(); + UnsignedInt frameCount = TheGameLogic->getFrame(); UnsignedInt fileSize = ftell(m_file); // move to appropriate offset if (!fseek(m_file, endTimeOffset, SEEK_SET)) @@ -242,10 +242,10 @@ void RecorderClass::logGameEnd( void ) fwrite(&tmp, sizeof(replay_time_t), 1, m_file); } // move to appropriate offset - if (!fseek(m_file, framesOffset, SEEK_SET)) + if (!fseek(m_file, frameCountOffset, SEEK_SET)) { - // save off duration - fwrite(&duration, sizeof(UnsignedInt), 1, m_file); + // save off frameCount + fwrite(&frameCount, sizeof(UnsignedInt), 1, m_file); } // move back to end of stream #ifdef DEBUG_CRASHING @@ -272,7 +272,7 @@ void RecorderClass::logGameEnd( void ) if (logFP) { struct tm *t2 = localtime(&t); - duration = t - startTime; + time_t duration = t - startTime; Int minutes = duration/60; Int seconds = duration%60; fprintf(logFP, "Game end at %s(%d:%2.2d elapsed time)\n", asctime(t2), minutes, seconds); @@ -408,6 +408,7 @@ void RecorderClass::init() { m_gameInfo.setSeed(GetGameLogicRandomSeed()); m_wasDesync = FALSE; m_doingAnalysis = FALSE; + m_playbackFrameCount = 0; } /** @@ -430,7 +431,7 @@ void RecorderClass::reset() { void RecorderClass::update() { if (m_mode == RECORDERMODETYPE_RECORD || m_mode == RECORDERMODETYPE_NONE) { updateRecord(); - } else if (m_mode == RECORDERMODETYPE_PLAYBACK) { + } else if (isPlaybackMode()) { updatePlayback(); } } @@ -476,11 +477,11 @@ void RecorderClass::stopPlayback() { m_file = NULL; } m_fileName.clear(); - // Don't clear the game data if the replay is over - let things continue -//#ifdef DEBUG_CRC + if (!m_doingAnalysis) - TheMessageStream->appendMessage(GameMessage::MSG_CLEAR_GAME_DATA); -//#endif + { + TheCommandList->appendMessage(newInstance(GameMessage)(GameMessage::MSG_CLEAR_GAME_DATA)); + } } /** @@ -852,7 +853,7 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header) fread(&tmp, sizeof(replay_time_t), 1, m_file); header.endTime = tmp; - fread(&header.frameDuration, sizeof(UnsignedInt), 1, m_file); + fread(&header.frameCount, sizeof(UnsignedInt), 1, m_file); fread(&header.desyncGame, sizeof(Bool), 1, m_file); fread(&header.quitEarly, sizeof(Bool), 1, m_file); @@ -915,6 +916,14 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header) return TRUE; } +Bool RecorderClass::simulateReplay(AsciiString filename) +{ + Bool success = playbackFile(filename); + if (success) + m_mode = RECORDERMODETYPE_SIMULATION_PLAYBACK; + return success; +} + #if defined RTS_DEBUG || defined RTS_INTERNAL Bool RecorderClass::analyzeReplay( AsciiString filename ) { @@ -922,15 +931,18 @@ Bool RecorderClass::analyzeReplay( AsciiString filename ) return playbackFile(filename); } -Bool RecorderClass::isAnalysisInProgress( void ) + + +#endif + +Bool RecorderClass::isPlaybackInProgress( void ) const { - return m_mode == RECORDERMODETYPE_PLAYBACK && m_nextFrame != -1; + return isPlaybackMode() && m_nextFrame != -1; } -#endif AsciiString RecorderClass::getCurrentReplayFilename( void ) { - if (m_mode == RECORDERMODETYPE_PLAYBACK) + if (isPlaybackMode()) { return m_currentReplayFilename; } @@ -964,7 +976,7 @@ class CRCInfo UnsignedInt getLocalPlayer(void) { return m_localPlayer; } void setSawCRCMismatch(void) { m_sawCRCMismatch = TRUE; } - Bool sawCRCMismatch(void) { return m_sawCRCMismatch; } + Bool sawCRCMismatch(void) const { return m_sawCRCMismatch; } protected: @@ -1012,6 +1024,11 @@ UnsignedInt CRCInfo::readCRC(void) return val; } +Bool RecorderClass::sawCRCMismatch() const +{ + return m_crcInfo->sawCRCMismatch(); +} + void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback) { if (fromPlayback) @@ -1058,6 +1075,9 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f DEBUG_LOG(("Replay has gone out of sync!\nInGame:%8.8X Replay:%8.8X\nFrame:%d\n", playbackCRC, newCRC, mismatchFrame)); + // Print Mismatch in case we are simulating replays from console. + printf("CRC Mismatch in Frame %d\n", mismatchFrame); + // TheSuperHackers @tweak Pause the game on mismatch. Bool pause = TRUE; Bool pauseMusic = FALSE; @@ -1212,17 +1232,23 @@ Bool RecorderClass::playbackFile(AsciiString filename) // send a message to the logic for a new game if (!m_doingAnalysis) { - GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); + // TheSuperHackers @info helmutbuhler 13/04/2025 + // We send the New Game message here directly to the command list and bypass the TheMessageStream. + // That's ok because Multiplayer is disabled during replay playback and is actually required + // during replay simulation because we don't update TheMessageStream during simulation. + GameMessage *msg = newInstance(GameMessage)(GameMessage::MSG_NEW_GAME); msg->appendIntegerArgument(GAME_REPLAY); msg->appendIntegerArgument(difficulty); msg->appendIntegerArgument(rankPoints); if( maxFPS != 0 ) msg->appendIntegerArgument(maxFPS); + TheCommandList->appendMessage( msg ); //InitGameLogicRandom( m_gameInfo.getSeed()); InitRandom( m_gameInfo.getSeed() ); } m_currentReplayFilename = filename; + m_playbackFrameCount = header.frameCount; return TRUE; } @@ -1660,7 +1686,7 @@ void RecorderClass::initControls() Bool RecorderClass::isMultiplayer( void ) { - if (m_mode == RECORDERMODETYPE_PLAYBACK) + if (isPlaybackMode()) { GameSlot *slot; for (int i=0; i 0u) - { - char idStr[33]; - itoa(s_instanceIndex, idStr, 10); - guidStr.push_back('-'); - guidStr.append(idStr); - } - s_mutexHandle = CreateMutex(NULL, FALSE, guidStr.c_str()); - if (GetLastError() == ERROR_ALREADY_EXISTS) + if (isMultiInstance()) { - if (s_mutexHandle != NULL) + std::string guidStr = getFirstInstanceName(); + if (s_instanceIndex > 0u) { - CloseHandle(s_mutexHandle); - s_mutexHandle = NULL; + char idStr[33]; + itoa(s_instanceIndex, idStr, 10); + guidStr.push_back('-'); + guidStr.append(idStr); + } + s_mutexHandle = CreateMutex(NULL, FALSE, guidStr.c_str()); + if (GetLastError() == ERROR_ALREADY_EXISTS) + { + if (s_mutexHandle != NULL) + { + CloseHandle(s_mutexHandle); + s_mutexHandle = NULL; + } + // Try again with a new instance. + ++s_instanceIndex; + continue; } - // Try again with a new instance. - ++s_instanceIndex; - continue; } -#else - s_mutexHandle = CreateMutex(NULL, FALSE, getFirstInstanceName()); - if (GetLastError() == ERROR_ALREADY_EXISTS) + else { - if (s_mutexHandle != NULL) + s_mutexHandle = CreateMutex(NULL, FALSE, getFirstInstanceName()); + if (GetLastError() == ERROR_ALREADY_EXISTS) { - CloseHandle(s_mutexHandle); - s_mutexHandle = NULL; + if (s_mutexHandle != NULL) + { + CloseHandle(s_mutexHandle); + s_mutexHandle = NULL; + } + return false; } - return false; } -#endif break; } @@ -80,6 +89,31 @@ bool ClientInstance::isInitialized() return s_mutexHandle != NULL; } +bool ClientInstance::isMultiInstance() +{ + return s_isMultiInstance; +} + +void ClientInstance::setMultiInstance(bool v) +{ + if (isInitialized()) + { + DEBUG_CRASH(("ClientInstance::setMultiInstance(%d) - cannot set multi instance after initialization", (int)v)); + return; + } + s_isMultiInstance = v; +} + +void ClientInstance::skipPrimaryInstance() +{ + if (isInitialized()) + { + DEBUG_CRASH(("ClientInstance::skipPrimaryInstance() - cannot skip primary instance after initialization")); + return; + } + s_instanceIndex = 1; +} + UnsignedInt ClientInstance::getInstanceIndex() { DEBUG_ASSERTLOG(isInitialized(), ("ClientInstance::isInitialized() failed")); diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 93fbffe9039..21bad58720a 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -248,7 +248,7 @@ void PopulateReplayFileListbox(GameWindow *listbox) // time_t totalSeconds = header.endTime - header.startTime; // Int mins = totalSeconds/60; // Int secs = totalSeconds%60; -// Real fps = header.frameDuration/totalSeconds; +// Real fps = header.frameCount/totalSeconds; // extraStr.format(L"%d:%d (%g fps) %hs", mins, secs, fps, header.desyncGame?"OOS ":""); // // for (Int i=0; iupdate(); - } while (TheRecorder->isAnalysisInProgress()); + } while (TheRecorder->isPlaybackInProgress()); } } } diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index 59f9c092c80..cea961ebbd4 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -64,6 +64,7 @@ #include "Common/PlayerTemplate.h" #include "Common/RandomValue.h" #include "Common/Recorder.h" +#include "Common/ReplaySimulation.h" #include "Common/ScoreKeeper.h" #include "Common/SkirmishBattleHonors.h" #include "Common/ThingFactory.h" @@ -411,6 +412,11 @@ WindowMsgHandledType ScoreScreenInput( GameWindow *window, UnsignedInt msg, } // end MainMenuInput +static Bool showButtonContinue() +{ + return ReplaySimulation::getCurrentReplayIndex() != ReplaySimulation::getReplayCount()-1; +} + /** System Function for the ScoreScreen */ //------------------------------------------------------------------------------------------------- WindowMsgHandledType ScoreScreenSystem( GameWindow *window, UnsignedInt msg, @@ -449,23 +455,36 @@ WindowMsgHandledType ScoreScreenSystem( GameWindow *window, UnsignedInt msg, { TheShell->pop(); TheCampaignManager->setCampaign(AsciiString::TheEmptyString); + + if ( ReplaySimulation::getReplayCount() > 0 ) + { + ReplaySimulation::stop(); + TheGameEngine->setQuitting(TRUE); + } } else if ( controlID == buttonContinueID ) { - if(!buttonIsFinishCampaign) - ReplayWasPressed = TRUE; - if( screenType == SCORESCREEN_SINGLEPLAYER) + if( ReplaySimulation::getReplayCount() > 0 ) { - AsciiString mapName = TheCampaignManager->getCurrentMap(); - - if( mapName.isEmpty() ) - { - ReplayWasPressed = FALSE; - TheShell->pop(); - } - else + TheGameEngine->setQuitting(TRUE); + } + else + { + if(!buttonIsFinishCampaign) + ReplayWasPressed = TRUE; + if( screenType == SCORESCREEN_SINGLEPLAYER) { - CheckForCDAtGameStart( startNextCampaignGame ); + AsciiString mapName = TheCampaignManager->getCurrentMap(); + + if( mapName.isEmpty() ) + { + ReplayWasPressed = FALSE; + TheShell->pop(); + } + else + { + CheckForCDAtGameStart( startNextCampaignGame ); + } } } } @@ -800,7 +819,7 @@ void initReplaySinglePlayer( void ) if (chatBoxBorder) chatBoxBorder->winHide(TRUE); if (buttonContinue) - buttonContinue->winHide(TRUE); + buttonContinue->winHide(!showButtonContinue()); if (buttonBuddies) buttonBuddies->winHide(TRUE); if (listboxChatWindowScoreScreen) @@ -886,7 +905,7 @@ void initReplayMultiPlayer(void) if (chatBoxBorder) chatBoxBorder->winHide(TRUE); if (buttonContinue) - buttonContinue->winHide(TRUE); + buttonContinue->winHide(!showButtonContinue()); if (buttonBuddies) buttonBuddies->winHide(TRUE); // if (buttonRehost) diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index 2633a328c8b..164dbc47d9b 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -408,7 +408,7 @@ void Shell::showShell( Bool runInit ) { DEBUG_LOG(("Shell:showShell() - %s (%s)\n", TheGlobalData->m_initialFile.str(), (top())?top()->getFilename().str():"no top screen")); - if(!TheGlobalData->m_initialFile.isEmpty()) + if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty()) { return; } @@ -465,7 +465,7 @@ void Shell::showShell( Bool runInit ) void Shell::showShellMap(Bool useShellMap ) { // we don't want any of this to show if we're loading straight into a file - if(TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic ) + if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty()) return; if(useShellMap && TheGlobalData->m_shellMapOn) { diff --git a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp index 8964e037870..00c2515d7a3 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -591,11 +591,8 @@ void GameClient::update( void ) if(TheGlobalData->m_playIntro || TheGlobalData->m_afterIntro) { // redraw all views, update the GUI - if (!TheGlobalData->m_headless)// Remove headless-check with Replay Simulation PR - { - TheDisplay->DRAW(); - TheDisplay->UPDATE(); - } + TheDisplay->DRAW(); + TheDisplay->UPDATE(); return; } @@ -713,12 +710,10 @@ void GameClient::update( void ) } // update display - if (!TheGlobalData->m_headless)// Remove headless-check with Replay Simulation PR { TheDisplay->UPDATE(); } - if (!TheGlobalData->m_headless)// Remove headless-check with Replay Simulation PR { USE_PERF_TIMER(GameClient_draw) @@ -744,6 +739,18 @@ void GameClient::update( void ) } } // end update +void GameClient::updateHeadless() +{ + // TheSuperHackers @info helmutbuhler 03/05/2025 + // When we play a replay back in headless mode, we want to skip the update of GameClient + // because it's not necessary for CRC checking. + // But we do reset the particles. The problem is that particles can be generated during + // GameLogic and are only cleaned up during rendering. If we don't clean this up here, + // the particles accumulate and slow things down a lot and can even cause a crash on + // longer replays. + TheParticleSystemManager->reset(); +} + /** ----------------------------------------------------------------------------------------------- * Call the given callback function for each object contained within the given region. */ diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index dcc23721d5b..6b1d1977301 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -287,7 +287,7 @@ void GameLogic::setDefaults( Bool saveGame ) Bool GameLogic::isInSinglePlayerGame( void ) { return (m_gameMode == GAME_SINGLE_PLAYER || - (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK && TheRecorder->getGameMode() == GAME_SINGLE_PLAYER)); + (TheRecorder && TheRecorder->isPlaybackMode() && TheRecorder->getGameMode() == GAME_SINGLE_PLAYER)); } @@ -794,9 +794,11 @@ static void populateRandomStartPosition( GameInfo *game ) Int i; Int numPlayers = MAX_SLOTS; const MapMetaData *md = TheMapCache->findMap( game->getMap() ); - DEBUG_ASSERTCRASH( md , ("Could not find map %s in the mapcache", game->getMap().str())); if (md) numPlayers = md->m_numPlayers; + else + printf("Could not find map \"%s\"\n", game->getMap().str()); + DEBUG_ASSERTCRASH( md , ("Could not find map %s in the mapcache", game->getMap().str())); // generate a map of start spot distances Real startSpotDistance[MAX_SLOTS][MAX_SLOTS]; @@ -1086,7 +1088,7 @@ void GameLogic::startNewGame( Bool saveGame ) } else { - if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) + if (TheRecorder && TheRecorder->isPlaybackMode()) { TheGameInfo = game = TheRecorder->getGameInfo(); } @@ -3168,20 +3170,21 @@ void GameLogic::update( void ) if (generateForSolo || generateForMP) { m_CRC = getCRC( CRC_RECALC ); - if (isMPGameOrReplay) - { - GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_LOGIC_CRC ); - msg->appendIntegerArgument( m_CRC ); - msg->appendBooleanArgument( (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) ); // playback CRC - DEBUG_LOG(("Appended CRC on frame %d: %8.8X\n", m_frame, m_CRC)); - } - else - { - GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_LOGIC_CRC ); - msg->appendIntegerArgument( m_CRC ); - msg->appendBooleanArgument( (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) ); // playback CRC - DEBUG_LOG(("Appended Playback CRC on frame %d: %8.8X\n", m_frame, m_CRC)); - } + bool isPlayback = (TheRecorder && TheRecorder->isPlaybackMode()); + + GameMessage *msg = newInstance(GameMessage)(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 + // directly into TheCommandList because we don't update TheMessageStream during simulation. + GameMessageList *messageList = TheMessageStream; + if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK) + messageList = TheCommandList; + messageList->appendMessage(msg); + + DEBUG_LOG(("Appended %sCRC on frame %d: %8.8X\n", isPlayback ? "Playback " : "", m_frame, m_CRC)); } // collect stats diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index cf835cb3733..eeeefd1b2a0 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -1799,7 +1799,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) // -------------------------------------------------------------------------------------------- case GameMessage::MSG_SET_REPLAY_CAMERA: { - if (TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer) + if (TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer) { if (TheTacticalView->isCameraMovementFinished()) { @@ -1936,7 +1936,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) //thisPlayer->getPlayerDisplayName().str(), m_frame)); m_cachedCRCs[msg->getPlayerIndex()] = newCRC; // to mask problem: = (oldCRC < newCRC)?newCRC:oldCRC; } - else if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) + else if (TheRecorder && TheRecorder->isPlaybackMode()) { UnsignedInt newCRC = msg->getArgument(0)->integer; //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d\n", @@ -1966,7 +1966,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) } // end switch /**/ /// @todo: multiplayer semantics - if (currentlySelectedGroup && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer /*&& !TheRecorder->isMultiplayer()*/) + if (currentlySelectedGroup && TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == thisPlayer /*&& !TheRecorder->isMultiplayer()*/) { const VecObjectID& selectedObjects = currentlySelectedGroup->getAllIDs(); TheInGameUI->deselectAllDrawables(); diff --git a/Generals/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp b/Generals/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp index e6fab46eff0..72c556e5f15 100644 --- a/Generals/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp +++ b/Generals/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp @@ -97,15 +97,16 @@ EnumeratedIP * IPEnumeration::getAddresses( void ) return NULL; } -#if defined(RTS_MULTI_INSTANCE) // TheSuperHackers @feature Add one unique local host IP address for each multi client instance. - const UnsignedInt id = rts::ClientInstance::getInstanceId(); - addNewIP( - 127, - (UnsignedByte)(id >> 16), - (UnsignedByte)(id >> 8), - (UnsignedByte)(id)); -#endif + if (rts::ClientInstance::isMultiInstance()) + { + const UnsignedInt id = rts::ClientInstance::getInstanceId(); + addNewIP( + 127, + (UnsignedByte)(id >> 16), + (UnsignedByte)(id >> 8), + (UnsignedByte)(id)); + } // construct a list of addresses int numAddresses = 0; diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index 1f959a5bdf9..5b2c60cc1b3 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -794,7 +794,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // register windows class and create application window if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) - return 0; + return exitcode; // save our application instance for future use ApplicationHInstance = hInstance; @@ -870,7 +870,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, TheDmaCriticalSection = NULL; TheMemoryPoolCriticalSection = NULL; - return 0; + return exitcode; } // end WinMain From df74c7f2876b1a35be4aaf4c5a43c59fd03a3ae4 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 00:17:50 +0200 Subject: [PATCH 103/112] Apply manual sync for failed autosync --- Generals/Code/GameEngine/Source/Common/System/Debug.cpp | 1 + Generals/Code/GameEngine/Source/GameClient/GameClient.cpp | 2 +- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 4 ++-- .../Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp | 2 +- Generals/Code/Main/WinMain.cpp | 5 +++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/System/Debug.cpp b/Generals/Code/GameEngine/Source/Common/System/Debug.cpp index 703dc756741..ff40ce6389f 100644 --- a/Generals/Code/GameEngine/Source/Common/System/Debug.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/Debug.cpp @@ -54,6 +54,7 @@ // deterministic or the same on all peers in multiplayer games. //#define INCLUDE_DEBUG_LOG_IN_CRC_LOG +#include "Common/CommandLine.h" #define DEBUG_THREADSAFE #ifdef DEBUG_THREADSAFE #include "Common/CriticalSection.h" diff --git a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp index 00c2515d7a3..07ac79f01f5 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -517,7 +517,7 @@ void GameClient::update( void ) //Initial Game Codition. We must show the movie first and then we can display the shell if(TheGlobalData->m_afterIntro && !TheDisplay->isMoviePlaying()) { - if( playSizzle && TheGlobalData->m_playSizzle && !TheGlobalData->m_headless )// Remove headless-check with Replay Simulation PR) + if( playSizzle && TheGlobalData->m_playSizzle ) { TheWritableGlobalData->m_allowExitOutOfMovies = TRUE; if(TheGameLODManager && TheGameLODManager->didMemPass()) diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 6b1d1977301..e90f56366bf 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1132,7 +1132,7 @@ void GameLogic::startNewGame( Bool saveGame ) //****************************// // Get the m_loadScreen for this kind of game - if(!m_loadScreen) + if(!m_loadScreen && !(TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_SIMULATION_PLAYBACK)) { m_loadScreen = getLoadScreen( saveGame ); if(m_loadScreen && !TheGlobalData->m_headless) @@ -1916,7 +1916,7 @@ void GameLogic::startNewGame( Bool saveGame ) } // if we're in a load game, don't fade yet - if(saveGame == FALSE && TheTransitionHandler != NULL) + if(saveGame == FALSE && TheTransitionHandler != NULL && m_loadScreen) { TheTransitionHandler->setGroup("FadeWholeScreen"); while(!TheTransitionHandler->isFinished()) diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp index e24aef6a2bc..b8187e07502 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp @@ -979,7 +979,7 @@ void TextureLoadTaskClass::Init(TextureBaseClass* tc,bool high_priority) { // Make sure texture has a filename. REF_PTR_SET(Texture,tc); - WWASSERT(Texture->Get_Full_Path() != NULL); + //WWASSERT(Texture->Get_Full_Path() != NULL); Reduction=Texture->Get_Reduction(); HighPriorityRequested=high_priority; diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index 5b2c60cc1b3..8a3f00888c3 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -740,6 +740,7 @@ static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5; Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, Int nCmdShow ) { + Int exitcode = 1; try { _set_se_translator( DumpExceptionInfo ); // Hook that allows stack trace. @@ -835,13 +836,13 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, TheVersion = NULL; shutdownMemoryManager(); DEBUG_SHUTDOWN(); - return 0; + return exitcode; } DEBUG_LOG(("Create Generals Mutex okay.\n")); DEBUG_LOG(("CRC message is %d\n", GameMessage::MSG_LOGIC_CRC)); // run the game main loop - GameMain(0, NULL); + exitcode = GameMain(); delete TheVersion; TheVersion = NULL; From 1a7d27a9db88f4423fc6efee78d85ccffbe795b2 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 00:45:41 +0200 Subject: [PATCH 104/112] Revert TheCommandList back to TheMessageStream in RecorderClass::stopPlayback(). This was causing the last gametime to be incorrect in console output --- Generals/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index bd1b458c7b5..a1fa13adab6 100644 --- a/Generals/Code/GameEngine/Source/Common/Recorder.cpp +++ b/Generals/Code/GameEngine/Source/Common/Recorder.cpp @@ -480,7 +480,7 @@ void RecorderClass::stopPlayback() { if (!m_doingAnalysis) { - TheCommandList->appendMessage(newInstance(GameMessage)(GameMessage::MSG_CLEAR_GAME_DATA)); + TheMessageStream->appendMessage(GameMessage::MSG_CLEAR_GAME_DATA); } } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 08e94400a55..8261cbbe7f5 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -480,7 +480,7 @@ void RecorderClass::stopPlayback() { if (!m_doingAnalysis) { - TheCommandList->appendMessage(newInstance(GameMessage)(GameMessage::MSG_CLEAR_GAME_DATA)); + TheMessageStream->appendMessage(GameMessage::MSG_CLEAR_GAME_DATA); } } From a417c7a352cb55441d459d6600e1e36c1159721e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 09:24:00 +0200 Subject: [PATCH 105/112] Fix wrong merge --- .../Code/GameEngine/Include/Common/GlobalData.h | 3 +++ .../Code/GameEngine/Source/Common/CommandLine.cpp | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 081f55cc675..47bcbfef654 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -350,6 +350,9 @@ class GlobalData : public SubsystemInterface Bool m_buildMapCache; AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start + + std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. + Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation AsciiString m_writeReplayList; ///< If not empty, write out list of replays in this subfolder into a csv file (TheSuperHackers @feature helmutbuhler 24/05/2025) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 58b00ebbb6e..ac58ee291b1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -31,7 +31,7 @@ #include "Common/LocalFileSystem.h" #include "Common/Recorder.h" #include "Common/version.h" - +#include "GameClient/ClientInstance.h" #include "Common/ReplayListCsv.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" @@ -1152,6 +1152,18 @@ static CommandLineParam paramsForStartup[] = // This runs the game without a window, graphics, input and audio. You can combine this with -replay { "-headless", parseHeadless }, + // TheSuperHackers @feature helmutbuhler 13/04/2025 + // Play back a replay. Pass the filename including .rep afterwards. + // You can pass this multiple times to play back multiple replays. + // You can also include wildcards. The file must be in the replay folder or in a subfolder. + { "-replay", parseReplay }, + + // TheSuperHackers @feature helmutbuhler 23/05/2025 + // Simulate each replay in a separate process and use 1..N processes at the same time. + // (If you have 4 cores, call it with -jobs 4) + // If you do not call this, all replays will be simulated in sequence in the same process. + { "-jobs", parseJobs }, + // TheSuperHackers @feature helmutbuhler 28/04/2025 // Pass in a csv file to play back multiple replays. The file must be in the replay folder. { "-replayList", parseSimReplayList }, From 0f9a0cdca9ad3ceb7937da95d9cfdd07ca498c62 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 18:42:17 +0200 Subject: [PATCH 106/112] Move typename for VS2019 compatibility --- Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h b/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h index fe4601177a9..afb21ab2cab 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h @@ -163,7 +163,7 @@ class LERPAnimationChannelClass : public PrimitiveAnimationChannelClass using PrimitiveAnimationChannelClass::m_Data; using PrimitiveAnimationChannelClass::m_LastIndex; public: - using typename PrimitiveAnimationChannelClass::KeyClass; + using /*typename*/ PrimitiveAnimationChannelClass::KeyClass; public: @@ -187,7 +187,7 @@ int PrimitiveAnimationChannelClass::Get_Key_Count (void) const // Set_Key_Value ///////////////////////////////////////////////////////// template -const PrimitiveAnimationChannelClass::KeyClass &PrimitiveAnimationChannelClass::Get_Key (int index) const +typename const PrimitiveAnimationChannelClass::KeyClass &PrimitiveAnimationChannelClass::Get_Key (int index) const { return m_Data[index]; } From aaaff0c199662bfa7bd3015058c007810013da3e Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 09:29:29 +0200 Subject: [PATCH 107/112] More merge fixes --- .../GameEngine/Source/Common/CommandLine.cpp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index ac58ee291b1..4b2a97a8b3d 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -443,6 +443,35 @@ Int parseReplay(char *args[], int num) return 1; } +Int parseSimReplayList(char *args[], int num) +{ + if (num > 1) + { + AsciiString filename = args[1]; + ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplays); + TheWritableGlobalData->m_playIntro = FALSE; + TheWritableGlobalData->m_afterIntro = TRUE; + TheWritableGlobalData->m_playSizzle = FALSE; + TheWritableGlobalData->m_shellMapOn = FALSE; + + // Make replay playback possible while other clients (possible retail) are running + rts::ClientInstance::setMultiInstance(TRUE); + rts::ClientInstance::skipPrimaryInstance(); + return 2; + } + return 1; +} + +Int parseWriteReplayList(char *args[], int num) +{ + if (num > 1) + { + TheWritableGlobalData->m_writeReplayList = args[1]; + TheWritableGlobalData->m_headless = TRUE; + } + return 1; +} + Int parseJobs(char *args[], int num) { if (num > 1) From c6e803bc9aff9af1cb7016b58aff08208abe6f90 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Thu, 19 Jun 2025 17:32:23 +0200 Subject: [PATCH 108/112] Add error checking for ReadReplayListFromCsv and fix up parseSimReplayList # Conflicts: # GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp --- .../Code/GameEngine/Include/Common/ReplayListCsv.h | 2 +- .../Code/GameEngine/Source/Common/CommandLine.cpp | 11 ++++++++--- .../Code/GameEngine/Source/Common/ReplayListCsv.cpp | 5 +++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h index b0140b341e0..b9e1a9cdc70 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ReplayListCsv.h @@ -20,4 +20,4 @@ bool WriteOutReplayList(AsciiString relativeFolder); -void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); +bool ReadReplayListFromCsv(AsciiString filename, std::vector* replayList); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 4b2a97a8b3d..b07cc752845 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -448,15 +448,20 @@ Int parseSimReplayList(char *args[], int num) if (num > 1) { AsciiString filename = args[1]; - ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplays); + bool success = ReadReplayListFromCsv(filename, &TheWritableGlobalData->m_simulateReplays); + if (!success) + { + printf("Cannot open csv file: \"%s\"\n", filename.str()); + exit(1); + } TheWritableGlobalData->m_playIntro = FALSE; TheWritableGlobalData->m_afterIntro = TRUE; TheWritableGlobalData->m_playSizzle = FALSE; TheWritableGlobalData->m_shellMapOn = FALSE; // Make replay playback possible while other clients (possible retail) are running - rts::ClientInstance::setMultiInstance(TRUE); - rts::ClientInstance::skipPrimaryInstance(); + rts::ClientInstance::s_multiInstance = TRUE; + rts::ClientInstance::s_avoidFirstInstance = TRUE; return 2; } return 1; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp index a5f043c19c4..7b9587f8d1c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp @@ -223,7 +223,7 @@ static void NextToken(AsciiString *string, AsciiString *token, char separator) string->set(*tokenEnd == 0 ? tokenEnd : tokenEnd+1); } -void ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) +bool ReadReplayListFromCsv(AsciiString filename, std::vector* replayList) { // Get path of csv file relative to replay folder. // Later we will search for replays in that path. @@ -244,7 +244,7 @@ void ReadReplayListFromCsv(AsciiString filename, std::vector* repla fname.format("%s%s", TheRecorder->getReplayDir().str(), filename.str()); FILE *fp = fopen(fname.str(), "rt"); if (!fp) - return; + return false; // Parse header AsciiString line, token; @@ -279,4 +279,5 @@ void ReadReplayListFromCsv(AsciiString filename, std::vector* repla // Ignore remaining columns } fclose(fp); + return true; } From b2ab15e6b05ba9b76bd9ad18a344f7e1b325a7d3 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Fri, 20 Jun 2025 00:16:53 +0200 Subject: [PATCH 109/112] Include mapname in csv file --- GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp index 7b9587f8d1c..c4b584ce3fe 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ReplayListCsv.cpp @@ -132,6 +132,9 @@ bool WriteOutReplayList(AsciiString relativeFolder) fprintf(fp, ",filename"); else fprintf(fp, ",\"%s\"", filename.str()); + + const char* mapName = info.getMap().reverseFind('\\'); + fprintf(fp, ",%s", i == -1 ? "map" : mapName ? mapName+1 : ""); fprintf(fp, ",%s", i == -1 ? "mapExists" : md ? "1" : "0"); fprintf(fp, ",%s", i == -1 ? "mismatch" : header.desyncGame ? "1" : "0"); From 939cda67a156d65ad14e70614a78a8dd5c460245 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 09:32:01 +0200 Subject: [PATCH 110/112] Move -writeReplayList to paramsForStartup --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index b07cc752845..916dd9c14d5 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -460,8 +460,8 @@ Int parseSimReplayList(char *args[], int num) TheWritableGlobalData->m_shellMapOn = FALSE; // Make replay playback possible while other clients (possible retail) are running - rts::ClientInstance::s_multiInstance = TRUE; - rts::ClientInstance::s_avoidFirstInstance = TRUE; + rts::ClientInstance::setMultiInstance(TRUE); + rts::ClientInstance::skipPrimaryInstance(); return 2; } return 1; @@ -1201,6 +1201,8 @@ static CommandLineParam paramsForStartup[] = // TheSuperHackers @feature helmutbuhler 28/04/2025 // Pass in a csv file to play back multiple replays. The file must be in the replay folder. { "-replayList", parseSimReplayList }, + + { "-writeReplayList", parseWriteReplayList }, }; // These Params are parsed during Engine Init before INI data is loaded @@ -1218,8 +1220,6 @@ static CommandLineParam paramsForEngineInit[] = { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, - { "-writeReplayList", parseWriteReplayList }, - #if (defined(RTS_DEBUG) || defined(RTS_INTERNAL)) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, From 97d90b7ba4930e1c1e0eb81d7e9a65e656688cc0 Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 09:38:57 +0200 Subject: [PATCH 111/112] Add comment --- GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 916dd9c14d5..6185d9be60d 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -1202,6 +1202,12 @@ static CommandLineParam paramsForStartup[] = // Pass in a csv file to play back multiple replays. The file must be in the replay folder. { "-replayList", parseSimReplayList }, + // TheSuperHackers @feature helmutbuhler 28/04/2025 + // Write out information about all replays in a folder to a csv file. + // Call it with -writeReplayList . for all replays in the replay folder. + // Call it with -writeReplayList folder for all replays in the folder subfolder. + // The result will be saved in replay_list.csv in that folder. + // todo: this is a bit unintuitive. Maybe use ReplaySimulation::resolveFilenameWildcards for this? { "-writeReplayList", parseWriteReplayList }, }; From 609d08b56d28eb7bcf8563e3071eb6b08daebb3d Mon Sep 17 00:00:00 2001 From: Helmut Buhler Date: Sun, 22 Jun 2025 09:43:44 +0200 Subject: [PATCH 112/112] Revert "Move typename for VS2019 compatibility" This reverts commit 0f9a0cdca9ad3ceb7937da95d9cfdd07ca498c62. --- Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h b/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h index afb21ab2cab..fe4601177a9 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/prim_anim.h @@ -163,7 +163,7 @@ class LERPAnimationChannelClass : public PrimitiveAnimationChannelClass using PrimitiveAnimationChannelClass::m_Data; using PrimitiveAnimationChannelClass::m_LastIndex; public: - using /*typename*/ PrimitiveAnimationChannelClass::KeyClass; + using typename PrimitiveAnimationChannelClass::KeyClass; public: @@ -187,7 +187,7 @@ int PrimitiveAnimationChannelClass::Get_Key_Count (void) const // Set_Key_Value ///////////////////////////////////////////////////////// template -typename const PrimitiveAnimationChannelClass::KeyClass &PrimitiveAnimationChannelClass::Get_Key (int index) const +const PrimitiveAnimationChannelClass::KeyClass &PrimitiveAnimationChannelClass::Get_Key (int index) const { return m_Data[index]; }