Skip to content

Commit 5104ace

Browse files
authored
[GEN][ZH] Add Replay Simulation feature (#923)
Invoked with command line arguments -headless -jobs 1..N -replay ReplayName1 -replay ReplayName2
1 parent 0279a8c commit 5104ace

File tree

49 files changed

+1231
-270
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1231
-270
lines changed

Core/GameEngine/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ set(GAMEENGINE_SRC
9595
# Include/Common/RandomValue.h
9696
# Include/Common/Recorder.h
9797
# Include/Common/Registry.h
98+
Include/Common/ReplaySimulation.h
9899
# Include/Common/ResourceGatheringManager.h
99100
# Include/Common/Science.h
100101
# Include/Common/ScopedMutex.h
@@ -129,6 +130,7 @@ set(GAMEENGINE_SRC
129130
# Include/Common/UserPreferences.h
130131
# Include/Common/version.h
131132
# Include/Common/WellKnownKeys.h
133+
Include/Common/WorkerProcess.h
132134
Include/Common/Xfer.h
133135
Include/Common/XferCRC.h
134136
Include/Common/XferDeepCRC.h
@@ -607,6 +609,7 @@ set(GAMEENGINE_SRC
607609
# Source/Common/PerfTimer.cpp
608610
# Source/Common/RandomValue.cpp
609611
# Source/Common/Recorder.cpp
612+
Source/Common/ReplaySimulation.cpp
610613
# Source/Common/RTS/AcademyStats.cpp
611614
# Source/Common/RTS/ActionManager.cpp
612615
# Source/Common/RTS/Energy.cpp
@@ -676,6 +679,7 @@ set(GAMEENGINE_SRC
676679
# Source/Common/Thing/ThingTemplate.cpp
677680
# Source/Common/UserPreferences.cpp
678681
# Source/Common/version.cpp
682+
Source/Common/WorkerProcess.cpp
679683
# Source/GameClient/ClientInstance.cpp
680684
# Source/GameClient/Color.cpp
681685
# Source/GameClient/Credits.cpp
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#pragma once
20+
21+
class ReplaySimulation
22+
{
23+
public:
24+
25+
// TheSuperHackers @feature helmutbuhler 13/04/2025
26+
// Simulate a list of replays without graphics.
27+
// Returns exit code 1 if mismatch or other error occurred
28+
// Returns exit code 0 if all replays were successfully simulated without mismatches
29+
static int simulateReplays(const std::vector<AsciiString> &filenames, int maxProcesses);
30+
31+
static void stop() { s_isRunning = false; }
32+
33+
static Bool isRunning() { return s_isRunning; }
34+
static UnsignedInt getCurrentReplayIndex() { return s_replayIndex; }
35+
static UnsignedInt getReplayCount() { return s_replayCount; }
36+
37+
private:
38+
39+
static int simulateReplaysInThisProcess(const std::vector<AsciiString> &filenames);
40+
static int simulateReplaysInWorkerProcesses(const std::vector<AsciiString> &filenames, int maxProcesses);
41+
static std::vector<AsciiString> resolveFilenameWildcards(const std::vector<AsciiString> &filenames);
42+
43+
private:
44+
45+
static Bool s_isRunning;
46+
static UnsignedInt s_replayIndex;
47+
static UnsignedInt s_replayCount;
48+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#pragma once
20+
21+
// Helper class that allows you to start a worker process and retrieve its exit code
22+
// and console output as a string.
23+
// It also makes sure that the started process is killed in case our process exits in any way.
24+
class WorkerProcess
25+
{
26+
public:
27+
WorkerProcess();
28+
29+
bool startProcess(UnicodeString command);
30+
31+
void update();
32+
33+
bool isRunning() const;
34+
35+
// returns true iff the process exited.
36+
bool isDone() const;
37+
38+
DWORD getExitCode() const;
39+
AsciiString getStdOutput() const;
40+
41+
// Terminate Process if it's running
42+
void kill();
43+
44+
private:
45+
// returns true if all output has been received
46+
// returns false if the worker is still running
47+
bool fetchStdOutput();
48+
49+
private:
50+
HANDLE m_processHandle;
51+
HANDLE m_readHandle;
52+
HANDLE m_jobHandle;
53+
AsciiString m_stdOutput;
54+
DWORD m_exitcode;
55+
bool m_isDone;
56+
};
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
20+
21+
#include "Common/ReplaySimulation.h"
22+
23+
#include "Common/GameEngine.h"
24+
#include "Common/LocalFileSystem.h"
25+
#include "Common/Recorder.h"
26+
#include "Common/WorkerProcess.h"
27+
#include "GameLogic/GameLogic.h"
28+
#include "GameClient/GameClient.h"
29+
30+
31+
Bool ReplaySimulation::s_isRunning = false;
32+
UnsignedInt ReplaySimulation::s_replayIndex = 0;
33+
UnsignedInt ReplaySimulation::s_replayCount = 0;
34+
35+
namespace
36+
{
37+
int countProcessesRunning(const std::vector<WorkerProcess>& processes)
38+
{
39+
int numProcessesRunning = 0;
40+
size_t i = 0;
41+
for (; i < processes.size(); ++i)
42+
{
43+
if (processes[i].isRunning())
44+
++numProcessesRunning;
45+
}
46+
return numProcessesRunning;
47+
}
48+
} // namespace
49+
50+
int ReplaySimulation::simulateReplaysInThisProcess(const std::vector<AsciiString> &filenames)
51+
{
52+
int numErrors = 0;
53+
54+
if (!TheGlobalData->m_headless)
55+
{
56+
s_isRunning = true;
57+
s_replayIndex = 0;
58+
s_replayCount = static_cast<UnsignedInt>(filenames.size());
59+
60+
// If we are not in headless mode, we need to run the replay in the engine.
61+
for (; s_replayIndex < s_replayCount; ++s_replayIndex)
62+
{
63+
TheRecorder->playbackFile(filenames[s_replayIndex]);
64+
TheGameEngine->execute();
65+
if (TheRecorder->sawCRCMismatch())
66+
numErrors++;
67+
if (!s_isRunning)
68+
break;
69+
TheGameEngine->setQuitting(FALSE);
70+
}
71+
s_isRunning = false;
72+
s_replayIndex = 0;
73+
s_replayCount = 0;
74+
return numErrors != 0 ? 1 : 0;
75+
}
76+
// Note that we use printf here because this is run from cmd.
77+
DWORD totalStartTimeMillis = GetTickCount();
78+
for (size_t i = 0; i < filenames.size(); i++)
79+
{
80+
AsciiString filename = filenames[i];
81+
printf("Simulating Replay \"%s\"\n", filename.str());
82+
fflush(stdout);
83+
DWORD startTimeMillis = GetTickCount();
84+
if (TheRecorder->simulateReplay(filename))
85+
{
86+
UnsignedInt totalTimeSec = TheRecorder->getPlaybackFrameCount() / LOGICFRAMES_PER_SECOND;
87+
while (TheRecorder->isPlaybackInProgress())
88+
{
89+
TheGameClient->updateHeadless();
90+
91+
const int progressFrameInterval = 10*60*LOGICFRAMES_PER_SECOND;
92+
if (TheGameLogic->getFrame() != 0 && TheGameLogic->getFrame() % progressFrameInterval == 0)
93+
{
94+
// Print progress report
95+
UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND;
96+
UnsignedInt realTimeSec = (GetTickCount()-startTimeMillis) / 1000;
97+
printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n",
98+
realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60);
99+
fflush(stdout);
100+
}
101+
TheGameLogic->UPDATE();
102+
if (TheRecorder->sawCRCMismatch())
103+
{
104+
numErrors++;
105+
break;
106+
}
107+
}
108+
UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND;
109+
UnsignedInt realTimeSec = (GetTickCount()-startTimeMillis) / 1000;
110+
printf("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d\n",
111+
realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60);
112+
fflush(stdout);
113+
}
114+
else
115+
{
116+
printf("Cannot open replay\n");
117+
numErrors++;
118+
}
119+
}
120+
if (filenames.size() > 1)
121+
{
122+
printf("Simulation of all replays completed. Errors occurred: %d\n", numErrors);
123+
124+
UnsignedInt realTime = (GetTickCount()-totalStartTimeMillis) / 1000;
125+
printf("Total Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60);
126+
fflush(stdout);
127+
}
128+
129+
return numErrors != 0 ? 1 : 0;
130+
}
131+
132+
int ReplaySimulation::simulateReplaysInWorkerProcesses(const std::vector<AsciiString> &filenames, int maxProcesses)
133+
{
134+
DWORD totalStartTimeMillis = GetTickCount();
135+
136+
WideChar exePath[1024];
137+
GetModuleFileNameW(NULL, exePath, ARRAY_SIZE(exePath));
138+
139+
std::vector<WorkerProcess> processes;
140+
int filenamePositionStarted = 0;
141+
int filenamePositionDone = 0;
142+
int numErrors = 0;
143+
144+
while (true)
145+
{
146+
int i;
147+
for (i = 0; i < processes.size(); i++)
148+
processes[i].update();
149+
150+
// Get result of finished processes and print output in order
151+
while (!processes.empty())
152+
{
153+
if (!processes[0].isDone())
154+
break;
155+
AsciiString stdOutput = processes[0].getStdOutput();
156+
printf("%d/%d %s", filenamePositionDone+1, (int)filenames.size(), stdOutput.str());
157+
DWORD exitcode = processes[0].getExitCode();
158+
if (exitcode != 0)
159+
printf("Error!\n");
160+
fflush(stdout);
161+
numErrors += exitcode == 0 ? 0 : 1;
162+
processes.erase(processes.begin());
163+
filenamePositionDone++;
164+
}
165+
166+
int numProcessesRunning = countProcessesRunning(processes);
167+
168+
// Add new processes when we are below the limit and there are replays left
169+
while (numProcessesRunning < maxProcesses && filenamePositionStarted < filenames.size())
170+
{
171+
UnicodeString filenameWide;
172+
filenameWide.translate(filenames[filenamePositionStarted]);
173+
UnicodeString command;
174+
command.format(L"\"%s\"%s%s -replay \"%s\"",
175+
exePath,
176+
TheGlobalData->m_windowed ? L" -win" : L"",
177+
TheGlobalData->m_headless ? L" -headless" : L"",
178+
filenameWide.str());
179+
180+
processes.push_back(WorkerProcess());
181+
processes.back().startProcess(command);
182+
183+
filenamePositionStarted++;
184+
numProcessesRunning++;
185+
}
186+
187+
if (processes.empty())
188+
break;
189+
190+
// Don't waste CPU here, our workers need every bit of CPU time they can get
191+
Sleep(100);
192+
}
193+
194+
DEBUG_ASSERTCRASH(filenamePositionStarted == filenames.size(), ("inconsistent file position 1"));
195+
DEBUG_ASSERTCRASH(filenamePositionDone == filenames.size(), ("inconsistent file position 2"));
196+
197+
printf("Simulation of all replays completed. Errors occurred: %d\n", numErrors);
198+
199+
UnsignedInt realTime = (GetTickCount()-totalStartTimeMillis) / 1000;
200+
printf("Total Wall Time: %d:%02d:%02d\n", realTime/60/60, realTime/60%60, realTime%60);
201+
fflush(stdout);
202+
203+
return numErrors != 0 ? 1 : 0;
204+
}
205+
206+
std::vector<AsciiString> ReplaySimulation::resolveFilenameWildcards(const std::vector<AsciiString> &filenames)
207+
{
208+
// If some filename contains wildcards, search for actual filenames.
209+
// Note that we cannot do this in parseReplay because we require TheLocalFileSystem initialized.
210+
std::vector<AsciiString> filenamesResolved;
211+
for (std::vector<AsciiString>::const_iterator filename = filenames.begin(); filename != filenames.end(); ++filename)
212+
{
213+
if (filename->find('*') || filename->find('?'))
214+
{
215+
AsciiString dir1 = TheRecorder->getReplayDir();
216+
AsciiString dir2 = *filename;
217+
AsciiString wildcard = *filename;
218+
{
219+
int len = dir2.getLength();
220+
while (len)
221+
{
222+
char c = dir2.getCharAt(len-1);
223+
if (c == '/' || c == '\\')
224+
{
225+
wildcard.set(wildcard.str()+dir2.getLength());
226+
break;
227+
}
228+
dir2.removeLastChar();
229+
len--;
230+
}
231+
}
232+
233+
FilenameList files;
234+
TheLocalFileSystem->getFileListInDirectory(dir2.str(), dir1.str(), wildcard, files, FALSE);
235+
for (FilenameList::iterator it = files.begin(); it != files.end(); ++it)
236+
{
237+
AsciiString file;
238+
file.set(it->str() + dir1.getLength());
239+
filenamesResolved.push_back(file);
240+
}
241+
}
242+
else
243+
filenamesResolved.push_back(*filename);
244+
}
245+
return filenamesResolved;
246+
}
247+
248+
int ReplaySimulation::simulateReplays(const std::vector<AsciiString> &filenames, int maxProcesses)
249+
{
250+
std::vector<AsciiString> filenamesResolved = resolveFilenameWildcards(filenames);
251+
if (maxProcesses == SIMULATE_REPLAYS_SEQUENTIAL)
252+
return simulateReplaysInThisProcess(filenamesResolved);
253+
else
254+
return simulateReplaysInWorkerProcesses(filenamesResolved, maxProcesses);
255+
}

0 commit comments

Comments
 (0)