Skip to content

Commit 07be406

Browse files
authored
feat(recorder): Implement replay archive feature (#1703)
Introduces a feature to automatically archive replays. Can be enabled by adding ArchiveReplays=yes to Options.ini Whenever the Last Replay (or 00000000.rep) is saved, a copy is placed within an archive directory located at Command and Conquer Generals Zero Hour Data\ArchivedReplays. This ensures that replays can never be accidentally lost. Replays are saved in the format YYYYMMDD_HHMMSS.rep for simplicity and to avoid conflicts.
1 parent 8a9db21 commit 07be406

File tree

8 files changed

+150
-10
lines changed

8 files changed

+150
-10
lines changed

Generals/Code/GameEngine/Include/Common/Recorder.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,10 @@ class RecorderClass : public SubsystemInterface {
117117
Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; }
118118
void initControls(); ///< Show or Hide the Replay controls
119119

120-
AsciiString getReplayDir(); ///< Returns the directory that holds the replay files.
121-
static AsciiString getReplayExtention(); ///< Returns the file extention for replay files.
122-
AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay.
120+
static AsciiString getReplayDir(); ///< Returns the directory that holds the replay files.
121+
static AsciiString getReplayArchiveDir(); ///< Returns the directory that holds the archived replay files.
122+
static AsciiString getReplayExtention(); ///< Returns the file extention for replay files.
123+
static AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay.
123124

124125
GameInfo *getGameInfo( void ) { return &m_gameInfo; } ///< Returns the slot list for playback game start
125126

@@ -132,10 +133,12 @@ class RecorderClass : public SubsystemInterface {
132133
Bool sawCRCMismatch() const;
133134
void cleanUpReplayFile( void ); ///< after a crash, send replay/debug info to a central repository
134135

136+
void setArchiveEnabled(Bool enable) { m_archiveReplays = enable; } ///< Enable or disable replay archiving.
135137
void stopRecording(); ///< Stop recording and close m_file.
136138
protected:
137139
void startRecording(GameDifficulty diff, Int originalGameMode, Int rankPoints, Int maxFPS); ///< Start recording to m_file.
138140
void writeToFile(GameMessage *msg); ///< Write this GameMessage to m_file.
141+
void archiveReplay(AsciiString fileName); ///< Move the specified replay file to the archive directory.
139142

140143
void logGameStart(AsciiString options);
141144
void logGameEnd( void );
@@ -166,6 +169,7 @@ class RecorderClass : public SubsystemInterface {
166169
Bool m_wasDesync;
167170

168171
Bool m_doingAnalysis;
172+
Bool m_archiveReplays; ///< if true, each replay is archived to the replay archive folder after recording
169173

170174
Int m_originalGameMode; // valid in replays
171175

Generals/Code/GameEngine/Include/Common/UserPreferences.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class OptionPreferences : public UserPreferences
8888
void setOnlineIPAddress(AsciiString IP); // convenience function
8989
void setLANIPAddress(UnsignedInt IP); // convenience function
9090
void setOnlineIPAddress(UnsignedInt IP); // convenience function
91+
Bool getArchiveReplaysEnabled() const; // convenience function
9192
Bool getAlternateMouseModeEnabled(void); // convenience function
9293
Real getScrollFactor(void); // convenience function
9394
Bool getDrawScrollAnchor(void);

Generals/Code/GameEngine/Source/Common/Recorder.cpp

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "GameLogic/GameLogic.h"
4646
#include "Common/RandomValue.h"
4747
#include "Common/CRCDebug.h"
48+
#include "Common/UserPreferences.h"
4849
#include "Common/version.h"
4950

5051
constexpr const char s_genrep[] = "GENREP";
@@ -370,6 +371,7 @@ RecorderClass::RecorderClass()
370371
//Added By Sadullah Nader
371372
//Initializtion(s) inserted
372373
m_doingAnalysis = FALSE;
374+
m_archiveReplays = FALSE;
373375
m_nextFrame = 0;
374376
m_wasDesync = FALSE;
375377
//
@@ -406,6 +408,9 @@ void RecorderClass::init() {
406408
m_wasDesync = FALSE;
407409
m_doingAnalysis = FALSE;
408410
m_playbackFrameCount = 0;
411+
412+
OptionPreferences optionPref;
413+
m_archiveReplays = optionPref.getArchiveReplaysEnabled();
409414
}
410415

411416
/**
@@ -726,10 +731,42 @@ void RecorderClass::stopRecording() {
726731
if (m_file != NULL) {
727732
m_file->close();
728733
m_file = NULL;
734+
735+
if (m_archiveReplays)
736+
archiveReplay(m_fileName);
729737
}
730738
m_fileName.clear();
731739
}
732740

741+
/**
742+
* TheSuperHackers @feature Stubbjax 17/10/2025 Copy the replay file to the archive directory and rename it using the current timestamp.
743+
*/
744+
void RecorderClass::archiveReplay(AsciiString fileName)
745+
{
746+
SYSTEMTIME st;
747+
GetLocalTime(&st);
748+
749+
AsciiString archiveFileName;
750+
// Use a standard YYYYMMDD_HHMMSS format for simplicity and to avoid conflicts.
751+
archiveFileName.format("%04d%02d%02d_%02d%02d%02d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
752+
753+
AsciiString extension = getReplayExtention();
754+
AsciiString sourcePath = getReplayDir();
755+
sourcePath.concat(fileName);
756+
757+
if (!sourcePath.endsWith(extension))
758+
sourcePath.concat(extension);
759+
760+
AsciiString destPath = getReplayArchiveDir();
761+
TheFileSystem->createDirectory(destPath.str());
762+
763+
destPath.concat(archiveFileName);
764+
destPath.concat(extension);
765+
766+
if (!CopyFile(sourcePath.str(), destPath.str(), FALSE))
767+
DEBUG_LOG(("RecorderClass::archiveReplay: Failed to copy %s to %s", sourcePath.str(), destPath.str()));
768+
}
769+
733770
/**
734771
* Write this game message to the record file. This also writes the game message's execution frame.
735772
*/
@@ -1595,10 +1632,18 @@ RecorderClass::CullBadCommandsResult RecorderClass::cullBadCommands() {
15951632
*/
15961633
AsciiString RecorderClass::getReplayDir()
15971634
{
1598-
const char* replayDir = "Replays\\";
1635+
AsciiString tmp = TheGlobalData->getPath_UserData();
1636+
tmp.concat("Replays\\");
1637+
return tmp;
1638+
}
15991639

1640+
/**
1641+
* returns the directory that holds the archived replay files.
1642+
*/
1643+
AsciiString RecorderClass::getReplayArchiveDir()
1644+
{
16001645
AsciiString tmp = TheGlobalData->getPath_UserData();
1601-
tmp.concat(replayDir);
1646+
tmp.concat("ArchivedReplays\\");
16021647
return tmp;
16031648
}
16041649

Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include "Common/GameEngine.h"
3939
#include "Common/UserPreferences.h"
4040
#include "Common/GameLOD.h"
41+
#include "Common/Recorder.h"
4142
#include "Common/Registry.h"
4243
#include "Common/version.h"
4344

@@ -310,6 +311,18 @@ void OptionPreferences::setOnlineIPAddress( UnsignedInt IP )
310311
(*this)["GameSpyIPAddress"] = tmp;
311312
}
312313

314+
Bool OptionPreferences::getArchiveReplaysEnabled() const
315+
{
316+
OptionPreferences::const_iterator it = find("ArchiveReplays");
317+
if (it == end())
318+
return FALSE;
319+
320+
if (stricmp(it->second.str(), "yes") == 0) {
321+
return TRUE;
322+
}
323+
return FALSE;
324+
}
325+
313326
Bool OptionPreferences::getAlternateMouseModeEnabled(void)
314327
{
315328
OptionPreferences::const_iterator it = find("UseAlternateMouse");
@@ -1296,6 +1309,13 @@ static void saveOptions( void )
12961309
TheWritableGlobalData->m_enablePlayerObserver = enabled;
12971310
}
12981311

1312+
// TheSuperHackers @todo Add checkbox ?
1313+
{
1314+
Bool enabled = pref->getArchiveReplaysEnabled();
1315+
(*pref)["ArchiveReplays"] = enabled ? "yes" : "no";
1316+
TheRecorder->setArchiveEnabled(enabled);
1317+
}
1318+
12991319
//-------------------------------------------------------------------------------------------------
13001320
// scroll speed val
13011321
val = GadgetSliderGetPosition(sliderScrollSpeed);

GeneralsMD/Code/GameEngine/Include/Common/Recorder.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,10 @@ class RecorderClass : public SubsystemInterface {
117117
Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; }
118118
void initControls(); ///< Show or Hide the Replay controls
119119

120-
AsciiString getReplayDir(); ///< Returns the directory that holds the replay files.
121-
static AsciiString getReplayExtention(); ///< Returns the file extention for replay files.
122-
AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay.
120+
static AsciiString getReplayDir(); ///< Returns the directory that holds the replay files.
121+
static AsciiString getReplayArchiveDir(); ///< Returns the directory that holds the archived replay files.
122+
static AsciiString getReplayExtention(); ///< Returns the file extention for replay files.
123+
static AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay.
123124

124125
GameInfo *getGameInfo( void ) { return &m_gameInfo; } ///< Returns the slot list for playback game start
125126

@@ -132,10 +133,12 @@ class RecorderClass : public SubsystemInterface {
132133
Bool sawCRCMismatch() const;
133134
void cleanUpReplayFile( void ); ///< after a crash, send replay/debug info to a central repository
134135

136+
void setArchiveEnabled(Bool enable) { m_archiveReplays = enable; } ///< Enable or disable replay archiving.
135137
void stopRecording(); ///< Stop recording and close m_file.
136138
protected:
137139
void startRecording(GameDifficulty diff, Int originalGameMode, Int rankPoints, Int maxFPS); ///< Start recording to m_file.
138140
void writeToFile(GameMessage *msg); ///< Write this GameMessage to m_file.
141+
void archiveReplay(AsciiString fileName); ///< Move the specified replay file to the archive directory.
139142

140143
void logGameStart(AsciiString options);
141144
void logGameEnd( void );
@@ -166,6 +169,7 @@ class RecorderClass : public SubsystemInterface {
166169
Bool m_wasDesync;
167170

168171
Bool m_doingAnalysis;
172+
Bool m_archiveReplays; ///< if true, each replay is archived to the replay archive folder after recording
169173

170174
Int m_originalGameMode; // valid in replays
171175

GeneralsMD/Code/GameEngine/Include/Common/UserPreferences.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class OptionPreferences : public UserPreferences
8989
void setOnlineIPAddress(AsciiString IP); // convenience function
9090
void setLANIPAddress(UnsignedInt IP); // convenience function
9191
void setOnlineIPAddress(UnsignedInt IP); // convenience function
92+
Bool getArchiveReplaysEnabled() const; // convenience function
9293
Bool getAlternateMouseModeEnabled(void); // convenience function
9394
Bool getRetaliationModeEnabled(); // convenience function
9495
Bool getDoubleClickAttackMoveEnabled(void); // convenience function

GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "GameLogic/GameLogic.h"
4646
#include "Common/RandomValue.h"
4747
#include "Common/CRCDebug.h"
48+
#include "Common/UserPreferences.h"
4849
#include "Common/version.h"
4950

5051
constexpr const char s_genrep[] = "GENREP";
@@ -370,6 +371,7 @@ RecorderClass::RecorderClass()
370371
//Added By Sadullah Nader
371372
//Initializtion(s) inserted
372373
m_doingAnalysis = FALSE;
374+
m_archiveReplays = FALSE;
373375
m_nextFrame = 0;
374376
m_wasDesync = FALSE;
375377
//
@@ -406,6 +408,9 @@ void RecorderClass::init() {
406408
m_wasDesync = FALSE;
407409
m_doingAnalysis = FALSE;
408410
m_playbackFrameCount = 0;
411+
412+
OptionPreferences optionPref;
413+
m_archiveReplays = optionPref.getArchiveReplaysEnabled();
409414
}
410415

411416
/**
@@ -728,10 +733,42 @@ void RecorderClass::stopRecording() {
728733
if (m_file != NULL) {
729734
m_file->close();
730735
m_file = NULL;
736+
737+
if (m_archiveReplays)
738+
archiveReplay(m_fileName);
731739
}
732740
m_fileName.clear();
733741
}
734742

743+
/**
744+
* TheSuperHackers @feature Stubbjax 17/10/2025 Copy the replay file to the archive directory and rename it using the current timestamp.
745+
*/
746+
void RecorderClass::archiveReplay(AsciiString fileName)
747+
{
748+
SYSTEMTIME st;
749+
GetLocalTime(&st);
750+
751+
AsciiString archiveFileName;
752+
// Use a standard YYYYMMDD_HHMMSS format for simplicity and to avoid conflicts.
753+
archiveFileName.format("%04d%02d%02d_%02d%02d%02d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
754+
755+
AsciiString extension = getReplayExtention();
756+
AsciiString sourcePath = getReplayDir();
757+
sourcePath.concat(fileName);
758+
759+
if (!sourcePath.endsWith(extension))
760+
sourcePath.concat(extension);
761+
762+
AsciiString destPath = getReplayArchiveDir();
763+
TheFileSystem->createDirectory(destPath.str());
764+
765+
destPath.concat(archiveFileName);
766+
destPath.concat(extension);
767+
768+
if (!CopyFile(sourcePath.str(), destPath.str(), FALSE))
769+
DEBUG_LOG(("RecorderClass::archiveReplay: Failed to copy %s to %s", sourcePath.str(), destPath.str()));
770+
}
771+
735772
/**
736773
* Write this game message to the record file. This also writes the game message's execution frame.
737774
*/
@@ -1598,10 +1635,18 @@ RecorderClass::CullBadCommandsResult RecorderClass::cullBadCommands() {
15981635
*/
15991636
AsciiString RecorderClass::getReplayDir()
16001637
{
1601-
const char* replayDir = "Replays\\";
1638+
AsciiString tmp = TheGlobalData->getPath_UserData();
1639+
tmp.concat("Replays\\");
1640+
return tmp;
1641+
}
16021642

1643+
/**
1644+
* returns the directory that holds the archived replay files.
1645+
*/
1646+
AsciiString RecorderClass::getReplayArchiveDir()
1647+
{
16031648
AsciiString tmp = TheGlobalData->getPath_UserData();
1604-
tmp.concat(replayDir);
1649+
tmp.concat("ArchivedReplays\\");
16051650
return tmp;
16061651
}
16071652

GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include "Common/GameEngine.h"
3939
#include "Common/UserPreferences.h"
4040
#include "Common/GameLOD.h"
41+
#include "Common/Recorder.h"
4142
#include "Common/Registry.h"
4243
#include "Common/version.h"
4344

@@ -319,6 +320,18 @@ void OptionPreferences::setOnlineIPAddress( UnsignedInt IP )
319320
(*this)["GameSpyIPAddress"] = tmp;
320321
}
321322

323+
Bool OptionPreferences::getArchiveReplaysEnabled() const
324+
{
325+
OptionPreferences::const_iterator it = find("ArchiveReplays");
326+
if (it == end())
327+
return FALSE;
328+
329+
if (stricmp(it->second.str(), "yes") == 0) {
330+
return TRUE;
331+
}
332+
return FALSE;
333+
}
334+
322335
Bool OptionPreferences::getAlternateMouseModeEnabled(void)
323336
{
324337
OptionPreferences::const_iterator it = find("UseAlternateMouse");
@@ -1356,6 +1369,13 @@ static void saveOptions( void )
13561369
TheWritableGlobalData->m_enablePlayerObserver = enabled;
13571370
}
13581371

1372+
// TheSuperHackers @todo Add checkbox ?
1373+
{
1374+
Bool enabled = pref->getArchiveReplaysEnabled();
1375+
(*pref)["ArchiveReplays"] = enabled ? "yes" : "no";
1376+
TheRecorder->setArchiveEnabled(enabled);
1377+
}
1378+
13591379
//-------------------------------------------------------------------------------------------------
13601380
// scroll speed val
13611381
val = GadgetSliderGetPosition(sliderScrollSpeed);

0 commit comments

Comments
 (0)