diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt
index 35fc9a97d1..56b8b90cfa 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/Core/GameEngine/Include/Common/ReplaySimulation.h b/Core/GameEngine/Include/Common/ReplaySimulation.h
new file mode 100644
index 0000000000..219f623370
--- /dev/null
+++ b/Core/GameEngine/Include/Common/ReplaySimulation.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
+
+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/Include/Common/WorkerProcess.h b/Core/GameEngine/Include/Common/WorkerProcess.h
new file mode 100644
index 0000000000..0a0b104f64
--- /dev/null
+++ b/Core/GameEngine/Include/Common/WorkerProcess.h
@@ -0,0 +1,56 @@
+/*
+** 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(UnicodeString command);
+
+ void update();
+
+ bool isRunning() const;
+
+ // returns true iff the process exited.
+ bool isDone() const;
+
+ DWORD getExitCode() const;
+ AsciiString getStdOutput() const;
+
+ // 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;
+ HANDLE m_jobHandle;
+ AsciiString m_stdOutput;
+ DWORD m_exitcode;
+ bool m_isDone;
+};
diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp
new file mode 100644
index 0000000000..354925c8fc
--- /dev/null
+++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp
@@ -0,0 +1,255 @@
+/*
+** 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/ReplaySimulation.h"
+
+#include "Common/GameEngine.h"
+#include "Common/LocalFileSystem.h"
+#include "Common/Recorder.h"
+#include "Common/WorkerProcess.h"
+#include "GameLogic/GameLogic.h"
+#include "GameClient/GameClient.h"
+
+
+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)
+{
+ 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 (; s_replayIndex < s_replayCount; ++s_replayIndex)
+ {
+ TheRecorder->playbackFile(filenames[s_replayIndex]);
+ TheGameEngine->execute();
+ if (TheRecorder->sawCRCMismatch())
+ numErrors++;
+ 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.
+ 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 startTimeMillis = GetTickCount();
+ if (TheRecorder->simulateReplay(filename))
+ {
+ UnsignedInt totalTimeSec = TheRecorder->getPlaybackFrameCount() / LOGICFRAMES_PER_SECOND;
+ while (TheRecorder->isPlaybackInProgress())
+ {
+ TheGameClient->updateHeadless();
+
+ const int progressFrameInterval = 10*60*LOGICFRAMES_PER_SECOND;
+ if (TheGameLogic->getFrame() != 0 && TheGameLogic->getFrame() % progressFrameInterval == 0)
+ {
+ // Print progress report
+ UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND;
+ 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);
+ }
+ TheGameLogic->UPDATE();
+ if (TheRecorder->sawCRCMismatch())
+ {
+ numErrors++;
+ break;
+ }
+ }
+ UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND;
+ 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);
+ }
+ else
+ {
+ printf("Cannot open replay\n");
+ numErrors++;
+ }
+ }
+ if (filenames.size() > 1)
+ {
+ 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);
+ fflush(stdout);
+ }
+
+ return numErrors != 0 ? 1 : 0;
+}
+
+int ReplaySimulation::simulateReplaysInWorkerProcesses(const std::vector &filenames, int maxProcesses)
+{
+ DWORD totalStartTimeMillis = GetTickCount();
+
+ WideChar exePath[1024];
+ GetModuleFileNameW(NULL, exePath, ARRAY_SIZE(exePath));
+
+ std::vector processes;
+ 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.empty())
+ {
+ if (!processes[0].isDone())
+ break;
+ AsciiString stdOutput = processes[0].getStdOutput();
+ printf("%d/%d %s", filenamePositionDone+1, (int)filenames.size(), stdOutput.str());
+ DWORD exitcode = processes[0].getExitCode();
+ if (exitcode != 0)
+ printf("Error!\n");
+ fflush(stdout);
+ numErrors += exitcode == 0 ? 0 : 1;
+ processes.erase(processes.begin());
+ filenamePositionDone++;
+ }
+
+ int numProcessesRunning = countProcessesRunning(processes);
+
+ // 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\"%s%s -replay \"%s\"",
+ exePath,
+ TheGlobalData->m_windowed ? L" -win" : L"",
+ TheGlobalData->m_headless ? L" -headless" : L"",
+ filenameWide.str());
+
+ processes.push_back(WorkerProcess());
+ processes.back().startProcess(command);
+
+ 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"));
+
+ 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);
+ fflush(stdout);
+
+ return numErrors != 0 ? 1 : 0;
+}
+
+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.
+ 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 ReplaySimulation::simulateReplays(const std::vector &filenames, int maxProcesses)
+{
+ std::vector filenamesResolved = resolveFilenameWildcards(filenames);
+ if (maxProcesses == SIMULATE_REPLAYS_SEQUENTIAL)
+ return simulateReplaysInThisProcess(filenamesResolved);
+ else
+ return simulateReplaysInWorkerProcesses(filenamesResolved, maxProcesses);
+}
diff --git a/Core/GameEngine/Source/Common/WorkerProcess.cpp b/Core/GameEngine/Source/Common/WorkerProcess.cpp
new file mode 100644
index 0000000000..5514c4c288
--- /dev/null
+++ b/Core/GameEngine/Source/Common/WorkerProcess.cpp
@@ -0,0 +1,231 @@
+/*
+** 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(UnicodeString command)
+{
+ m_stdOutput.clear();
+ m_isDone = false;
+
+ // Create pipe for reading console output
+ SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) };
+ saAttr.bInheritHandle = TRUE;
+ HANDLE writeHandle = NULL;
+ if (!CreatePipe(&m_readHandle, &writeHandle, &saAttr, 0))
+ return false;
+ SetHandleInformation(m_readHandle, HANDLE_FLAG_INHERIT, 0);
+
+ STARTUPINFOW si = { sizeof(STARTUPINFOW) };
+ si.dwFlags = STARTF_FORCEOFFFEEDBACK; // Prevent cursor wait animation
+ si.dwFlags |= STARTF_USESTDHANDLES;
+ si.hStdError = writeHandle;
+ si.hStdOutput = writeHandle;
+
+ PROCESS_INFORMATION pi = { 0 };
+
+ 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 ? CreateJobObjectW(NULL, 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() const
+{
+ return m_isDone;
+}
+
+DWORD WorkerProcess::getExitCode() const
+{
+ return m_exitcode;
+}
+
+AsciiString WorkerProcess::getStdOutput() const
+{
+ return m_stdOutput;
+}
+
+bool WorkerProcess::fetchStdOutput()
+{
+ while (true)
+ {
+ // 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;
+ 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, ARRAY_SIZE(buffer)-1, &readBytes, NULL);
+ if (!success)
+ 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
+ for (int i = 0; i < readBytes; i++)
+ if (buffer[i] == '\r')
+ buffer[i] = ' ';
+ 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);
+ 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.clear();
+ m_isDone = false;
+}
+
diff --git a/Dependencies/Utility/Utility/CppMacros.h b/Dependencies/Utility/Utility/CppMacros.h
index 9c0ed332cb..59a5ae09af 100644
--- a/Dependencies/Utility/Utility/CppMacros.h
+++ b/Dependencies/Utility/Utility/CppMacros.h
@@ -37,9 +37,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
#endif
#if __cplusplus < 201103L
diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt
index f1fe224b5b..eed55eae5b 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 e483db6548..0b2ab9f613 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 4d06757f7f..5cbd6975ef 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 fefd82c89d..7663b8dc91 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 74e7add404..1f1ada1dac 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 d599305b21..f0c6a70802 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 b01b0d10bd..ec75d89b2f 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 f6a97e93b2..4542419d92 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 a8cbc291f9..cf5f2e3be8 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 b7f04ba32c..5f9e9cd294 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 dacd872a04..a1fa13adab 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
+ }
}
/**
@@ -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 93fbffe903..21bad58720 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 59f9c092c8..cea961ebbd 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 2633a328c8..164dbc47d9 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 8964e03787..07ac79f01f 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())
@@ -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 dcc23721d5..e90f56366b 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();
}
@@ -1130,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)
@@ -1914,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())
@@ -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 cf835cb373..eeeefd1b2a 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 e6fab46eff..72c556e5f1 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/Libraries/Source/WWVegas/WW3D2/textureloader.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp
index e24aef6a2b..b8187e0750 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 1f959a5bdf..8a3f00888c 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.
@@ -794,7 +795,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;
@@ -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;
@@ -870,7 +871,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
TheDmaCriticalSection = NULL;
TheMemoryPoolCriticalSection = NULL;
- return 0;
+ return exitcode;
} // end WinMain
diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt
index 343a0fe28f..e2bea3ec54 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
@@ -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/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h
index 087e0e8aba..995817c7a2 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 void GameMain( int argc, char *argv[] );
+extern Int GameMain();
#endif // _GAME_ENGINE_H_
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
index fba180d123..c4e33a6962 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
@@ -54,7 +54,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
@@ -347,8 +348,11 @@ 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)
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h
index b776e6acf4..a29187c96a 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
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/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ClientInstance.h
index 74e7add404..1f1ada1dac 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();
@@ -42,6 +51,7 @@ class ClientInstance
private:
static HANDLE s_mutexHandle;
static UnsignedInt s_instanceIndex;
+ static Bool s_isMultiInstance;
};
} // namespace rts
diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h
index 3411ab8def..a6f60f36bf 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h
@@ -99,6 +99,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/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp
index 1b31d5028a..dc967b179c 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp
+++ b/GeneralsMD/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/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp
index e4382cf813..f95a046a49 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
@@ -613,7 +612,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;
@@ -635,10 +634,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/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp
index aef3d163e3..7ee88428c0 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp
+++ b/GeneralsMD/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/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
index 6f94d7d59a..11af51a810 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -956,6 +956,9 @@ GlobalData::GlobalData()
m_buildMapCache = FALSE;
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/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp
index a8873aefd5..8261cbbe7f 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,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
+ }
}
/**
@@ -854,7 +855,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);
@@ -917,6 +918,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 )
{
@@ -924,15 +933,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;
}
@@ -966,7 +978,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:
@@ -1014,6 +1026,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)
@@ -1061,6 +1078,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;
@@ -1215,17 +1235,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;
}
@@ -1663,7 +1689,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)
+ {
+ 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;
}
@@ -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/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp
index ab4aa1ad44..caa355f0c3 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp
+++ b/GeneralsMD/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/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp
index 23e3960ebc..5e6ad64c59 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,23 +547,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 );
+ }
}
}
}
@@ -984,7 +1003,7 @@ void initReplaySinglePlayer( void )
if (chatBoxBorder)
chatBoxBorder->winHide(TRUE);
if (buttonContinue)
- buttonContinue->winHide(TRUE);
+ buttonContinue->winHide(!showButtonContinue());
if (buttonBuddies)
buttonBuddies->winHide(TRUE);
if (listboxChatWindowScoreScreen)
@@ -1091,7 +1110,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/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp
index ea50ed4e8f..0b7148098c 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/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp
index 8c6e4076c8..c072e3d0d0 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)
@@ -782,6 +777,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/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
index dfc1909655..7c3b1e3dcf 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
@@ -302,7 +302,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));
}
@@ -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];
@@ -1228,7 +1230,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame )
}
else
{
- if (TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK)
+ if (TheRecorder && TheRecorder->isPlaybackMode())
{
TheGameInfo = game = TheRecorder->getGameInfo();
}
@@ -1292,7 +1294,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 );
if(m_loadScreen)
@@ -2218,7 +2220,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame )
}
// if we're in a load game, don't fade yet
- if(loadingSaveGame == FALSE && TheTransitionHandler != NULL)
+ if(loadingSaveGame == FALSE && TheTransitionHandler != NULL && m_loadScreen)
{
TheTransitionHandler->setGroup("FadeWholeScreen");
while(!TheTransitionHandler->isFinished())
@@ -3706,20 +3708,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/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp
index 7003423f03..75ad17edb3 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp
@@ -1827,7 +1827,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())
{
@@ -1964,7 +1964,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",
@@ -1994,7 +1994,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/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/IPEnumeration.cpp
index f485c60341..0d0a43018f 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::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/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp
index 95196922a8..c2754bc541 100644
--- a/GeneralsMD/Code/Main/WinMain.cpp
+++ b/GeneralsMD/Code/Main/WinMain.cpp
@@ -762,6 +762,7 @@ static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5;
Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, Int nCmdShow )
{
+ Int exitcode = 1;
#ifdef RTS_PROFILE
Profile::StartRange("init");
@@ -840,7 +841,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;
@@ -881,14 +882,14 @@ 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;
@@ -916,7 +917,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
TheDmaCriticalSection = NULL;
TheMemoryPoolCriticalSection = NULL;
- return 0;
+ return exitcode;
} // end WinMain