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