Skip to content

feat(recorder): Play a replay file from the command line - #3227

Merged
xezon merged 2 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/feature/loadreplay-cli
Sep 15, 2026
Merged

xezon merged 2 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/feature/loadreplay-cli

Conversation

@bobtista

@bobtista bobtista commented Aug 27, 2026

Copy link
Copy Markdown

-replay already plays visually when used without -headless, but it enters the replay-simulation workflow before the normal shell is shown and terminates the process when that workflow finishes. This is appropriate for batch simulation and synchronization checking, but not for an operating-system file handler whose playback should return to the menus.

-loadreplay <file> instead plays one replay through the normal client lifecycle. loadQueuedReplay runs at the point -loadsave already uses, once the client has initialized the shell, so the menus the playback returns to are on the stack.

Absolute paths are opened in place while relative names still resolve from the Replay directory. RecorderClass::getReplayPathForRead does that, mirroring GameState::getSaveGamePathForRead from #3226. Because this resolution is shared, existing -replay also gains support for absolute paths and no longer requires the .rep extension.

A replay that cannot be read, or whose map is unavailable, is rejected up front with the same message boxes the Replay menu shows, and the game stays on the main menu rather than failing deep in map loading.

Verified with failing files as controls so a pass is distinguishable from "the game started anyway":

case result
control: bogus absolute path "REPLAY CANNOT BE LOADED" on the main menu
control: file that is not a replay "REPLAY CANNOT BE LOADED" on the main menu
control: replay whose map is not installed "MAP NOT FOUND" on the main menu
Replay from an absolute path containing spaces (quoted) loads and plays
Relative Replay filename loads from the managed directory
Replay from a UNC path (\\localhost\C$\...) loads
Restarting a Replay loaded from an absolute path restarts and replays from the beginning
Normal Replay synchronization reporting reports and pauses (InGame:D9C721A5 Replay:D8A198C0 Frame:110)

Todo:

  • Both games build (z_generals and g_generals)
  • Replay paths outside the user data directory
  • Paths containing spaces
  • Windows drive paths and UNC paths
  • Relative Replay filenames still resolve from the managed directory
  • Restarting an externally loaded Replay
  • Normal Replay synchronization reporting is unchanged
  • Replicate to Generals

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add visual replay playback and absolute file loading to CLI

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Adds -loadreplay to launch visual replay playback after client shell initialization.
• Supports absolute replay and save paths while preserving managed-directory resolution for relative
 names.
• Rejects unreadable, malformed, or map-missing replays before entering gameplay.
Diagram

graph TD
  CLI["CLI parser"] --> Queue["Startup request"] --> Client["Client update"] --> Loader["Replay loader"] --> Resolver["Path resolver"] --> Check{"Replay valid?"}
  Check -->|Yes| Playback["Visual playback"]
  Check -->|No| Quit["Quit game"]
Loading
High-Level Assessment

The queued startup approach is appropriate because it reuses the established -loadsave lifecycle point, ensuring the client shell exists before playback and remains available afterward. Starting playback directly during command-line parsing was considered but would run before required client and filesystem state is initialized; centralizing absolute-versus-relative path resolution also preserves existing menu behavior.

Files changed (15) +214 / -18

Enhancement (15) +214 / -18
FileSystem.hExpose platform-aware absolute path detection +1/-0

Expose platform-aware absolute path detection

• Declares a shared helper for distinguishing explicit absolute paths from names resolved within managed directories.

Core/GameEngine/Include/Common/FileSystem.h

CommandLine.cppAdd and validate startup file-loading options +34/-2

Add and validate startup file-loading options

• Adds the '-loadreplay' parser and registration, validates replay and save extensions, and queues startup playback while suppressing intro and shell-map startup. Missing arguments now consume only the option itself.

Core/GameEngine/Source/Common/CommandLine.cpp

FileSystem.cppImplement cross-platform absolute path recognition +25/-0

Implement cross-platform absolute path recognition

• Recognizes Windows drive-rooted, current-drive-rooted, and UNC-style paths, plus POSIX root paths.

Core/GameEngine/Source/Common/System/FileSystem.cpp

GameState.hDeclare save read-path resolution helper +1/-0

Declare save read-path resolution helper

• Adds the Generals API for resolving absolute save paths or managed-directory filenames.

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

GlobalData.hStore queued replay startup requests +1/-0

Store queued replay startup requests

• Adds global startup state for the replay requested through '-loadreplay'.

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

Recorder.hExpose queued replay loading +1/-0

Expose queued replay loading

• Declares the recorder entry point that validates and starts a command-line replay request.

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

Recorder.cppResolve and preflight queued replays +49/-2

Resolve and preflight queued replays

• Reads absolute replay paths in place while retaining Replay-directory lookup for relative names. Validates replay headers, game options, and map availability before playback, quitting cleanly on failure.

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

GameState.cppSupport absolute save paths in Generals +20/-6

Support absolute save paths in Generals

• Centralizes save read-path resolution so command-line absolute paths open in place and relative menu names remain under the Save directory. Applies the helper to metadata, existence, and full-load paths.

Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp

GameClient.cppStart queued replays after client initialization +5/-0

Start queued replays after client initialization

• Invokes replay loading from the established queued-load lifecycle point after shell setup, while retaining save-load priority.

Generals/Code/GameEngine/Source/GameClient/GameClient.cpp

GameState.hDeclare Zero Hour save path resolution +1/-0

Declare Zero Hour save path resolution

• Adds the Zero Hour API for resolving absolute save paths or managed-directory filenames.

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

GlobalData.hStore Zero Hour replay startup requests +1/-0

Store Zero Hour replay startup requests

• Adds global startup state for the replay requested through '-loadreplay'.

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

Recorder.hExpose Zero Hour queued replay loading +1/-0

Expose Zero Hour queued replay loading

• Declares the recorder entry point that validates and starts a command-line replay request.

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

Recorder.cppResolve and preflight Zero Hour replays +49/-2

Resolve and preflight Zero Hour replays

• Mirrors absolute and relative replay path handling for Zero Hour. Validates replay headers, game options, and map availability before playback, quitting cleanly on failure.

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

GameState.cppSupport absolute save paths in Zero Hour +20/-6

Support absolute save paths in Zero Hour

• Mirrors centralized save read-path resolution so absolute command-line paths open in place and relative names remain under the Save directory.

GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp

GameClient.cppStart Zero Hour replays after initialization +5/-0

Start Zero Hour replays after initialization

• Invokes queued replay loading after shell setup in Zero Hour while retaining queued save-load priority.

GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp

@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds normal-lifecycle replay playback through -loadreplay, including absolute-path resolution and early replay/map validation.

  • Stores the requested replay in shared startup state and launches it after the shell initializes.
  • Centralizes relative and absolute replay path resolution for menu, simulation, and playback callers.
  • Applies the replay behavior consistently to Generals and Zero Hour.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
Core/GameEngine/Source/Common/CommandLine.cpp Adds -loadreplay parsing and relaxes the existing simulation option's extension restriction.
Generals/Code/GameEngine/Source/Common/Recorder.cpp Adds replay path resolution, queued-playback validation, and the normal client-lifecycle launch flow for Generals.
GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp Mirrors replay path resolution and queued-playback behavior for Zero Hour.
Generals/Code/GameEngine/Source/GameClient/GameClient.cpp Starts a queued replay after shell initialization, using the same startup point as queued save loading.
GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp Mirrors the post-shell queued replay handoff in the Zero Hour client.
Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp Updates menu header reads to use the centralized filename and playback-mode contract.
GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp Applies the centralized replay-header API to the Zero Hour replay menu.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    CLI[-loadreplay path] --> Global[Store queued replay]
    Global --> Init[Initialize client and shell]
    Init --> Validate[Read replay header and validate map]
    Validate -->|Invalid replay| ReplayError[Show replay-load error and remain in menus]
    Validate -->|Missing map| MapError[Show map error and remain in menus]
    Validate -->|Valid| Playback[Open replay for playback]
    Playback --> Queue[Queue MSG_NEW_GAME]
    Queue --> Game[Start replay]
    Game --> Menus[Return to menus after playback]
Loading

Reviews (7): Last reviewed commit: "feat(cli): Play a replay file from the c..." | Re-trigger Greptile

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 27, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. -ignoreReplaySyncErrors is unregistered 📎 Requirement gap ≡ Correctness
Description
The PR registers -loadreplay but does not register the required -ignoreReplaySyncErrors option,
so invoking the mandated suppression flag cannot set TheDebugIgnoreSyncErrors. Only the
differently named legacy -ignoresync option reaches parseSync.
Code

Core/GameEngine/Source/Common/CommandLine.cpp[1208]

+	{ "-loadreplay", parseLoadReplay },
Evidence
PR Compliance ID 6 explicitly requires suppression when -ignoreReplaySyncErrors is supplied. The
PR extends the startup command table with -loadreplay at line 1208, while the branch contains no
registration for -ignoreReplaySyncErrors; parseSync at lines 810-814 provides the required
behavior but is registered only under -ignoresync at line 1322.

Honor explicit replay synchronization error suppression
Core/GameEngine/Source/Common/CommandLine.cpp[810-814]
Core/GameEngine/Source/Common/CommandLine.cpp[1205-1208]
Core/GameEngine/Source/Common/CommandLine.cpp[1322-1322]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`-loadreplay` must support the explicit `-ignoreReplaySyncErrors` command-line option, but that name is not registered.
## Issue Context
The existing `parseSync` handler already enables `TheDebugIgnoreSyncErrors`, and the legacy `-ignoresync` registration should remain compatible. Register the required option name as an alias to the same handler.
## Fix Focus Areas
- Core/GameEngine/Source/Common/CommandLine.cpp[1205-1208]
- Core/GameEngine/Source/Common/CommandLine.cpp[1319-1323]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Playerless replay crashes startup 🐞 Bug ≡ Correctness
Description
loadQueuedReplay() passes headers with localPlayerIndex == -1 into playbackFile(), which
dereferences getSlot(-1) and crashes instead of playing or cleanly rejecting the replay. The
recorder itself can write -1 for non-network single-player recordings, so -loadreplay can hit
this with a generated replay file.
Code

Generals/Code/GameEngine/Source/Common/Recorder.cpp[1117]

+	if (!playbackFile(filename))
Evidence
The replay writer initializes the recorded local index to -1 and leaves it unchanged for
non-network, non-skirmish single-player recording. The reader considers -1 valid, while the newly
invoked playback path passes it to GameInfo::getSlot, which returns null for negative indexes
before the caller dereferences it; GeneralsMD mirrors the same path.

Generals/Code/GameEngine/Source/Common/Recorder.cpp[590-640]
Generals/Code/GameEngine/Source/Common/Recorder.cpp[923-938]
Generals/Code/GameEngine/Source/Common/Recorder.cpp[1117-1120]
Generals/Code/GameEngine/Source/Common/Recorder.cpp[1191-1200]
Core/GameEngine/Source/GameNetwork/GameInfo.cpp[445-452]
GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp[1120-1123]
GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp[1194-1203]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Queued playback can receive a valid replay header with `localPlayerIndex == -1`, but `playbackFile()` unconditionally dereferences that slot and crashes. Handle the no-local-player case without calling `getSlot(-1)`, and apply the equivalent fix to both game variants.
## Issue Context
`readReplayHeader()` explicitly accepts `-1`, and `startRecording()` can serialize `-1` for non-network single-player recordings. The multiplayer flag should only inspect a slot when the index is nonnegative; otherwise use the appropriate non-multiplayer default.
## Fix Focus Areas
- Generals/Code/GameEngine/Source/Common/Recorder.cpp[1117-1120]
- Generals/Code/GameEngine/Source/Common/Recorder.cpp[1191-1200]
- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp[1120-1123]
- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp[1194-1203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread Core/GameEngine/Source/Common/CommandLine.cpp
Comment thread Generals/Code/GameEngine/Source/Common/Recorder.cpp
@tintinhamans

Copy link
Copy Markdown

There is no way to play a replay visually from the command line. -replay simulates headlessly, so an externally supplied .rep cannot be launched from an operating-system file handler and watched.

This is not entirely the case. -replay works fine without -headless.

@bobtista

Copy link
Copy Markdown
Author

There is no way to play a replay visually from the command line. -replay simulates headlessly, so an externally supplied .rep cannot be launched from an operating-system file handler and watched.

This is not entirely the case. -replay works fine without -headless.

Yeah that's true, replay without -headless already plays visually. Fixed the description.
The difference is lifecycle:
-replay enters the replay-simulation workflow before the shell is shown and terminates the process after the replay workflow finishes.
-loadreplay queues a single replay after normal shell initialization, so playback returns to the menus afterward.
The absolute-path handling is shared, so this PR also allows -replay to open absolute paths. The separate option is specifically for the normal client lifecycle.

@bobtista
bobtista force-pushed the bobtista/feature/loadreplay-cli branch from a82128f to cbeeaa5 Compare September 11, 2026 17:32
@bobtista

Copy link
Copy Markdown
Author

Both are split into follow ups: -ignoreReplaySyncErrors is implemented in #3152, and the playerless replay crash is fixed in #3240.

Comment thread Core/GameEngine/Source/Common/CommandLine.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/Common/Recorder.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/Common/Recorder.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/Common/Recorder.cpp
Comment thread Generals/Code/GameEngine/Source/Common/Recorder.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/Common/Recorder.cpp Outdated
@bobtista
bobtista force-pushed the bobtista/feature/loadreplay-cli branch from cbeeaa5 to d35d0ff Compare September 12, 2026 15:38
Comment thread GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp
Comment thread GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp Outdated
@bobtista
bobtista force-pushed the bobtista/feature/loadreplay-cli branch from d35d0ff to 00e5bc8 Compare September 12, 2026 19:07
Comment thread Core/GameEngine/Source/Common/CommandLine.cpp Outdated
@bobtista
bobtista force-pushed the bobtista/feature/loadreplay-cli branch 2 times, most recently from 1d058e2 to dc0d164 Compare September 14, 2026 15:48
@xezon xezon added Enhancement Is new feature or request Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour labels Sep 14, 2026

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable.

@xezon xezon changed the title feat(cli): Play a replay file from the command line feat(recorder): Play a replay file from the command line Sep 14, 2026
@bobtista
bobtista force-pushed the bobtista/feature/loadreplay-cli branch from dc0d164 to 1b5c5be Compare September 14, 2026 21:08
@xezon
xezon merged commit d28c7a5 into TheSuperHackers:main Sep 15, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Is new feature or request Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow -loadsave and -loadreplay to load files from any directory

3 participants