Skip to content

Commit dec4d7d

Browse files
authored
ci: Implement Replay Checker (#1366)
1 parent 9f0a55e commit dec4d7d

File tree

8 files changed

+329
-5
lines changed

8 files changed

+329
-5
lines changed

.github/workflows/build-toolchain.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ on:
2828

2929
jobs:
3030
build:
31-
name: Preset ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }}
31+
name: ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }}
3232
runs-on: windows-2022
33-
timeout-minutes: 40
33+
timeout-minutes: 20
3434
steps:
3535
- name: Checkout Code
3636
uses: actions/checkout@v4

.github/workflows/check-replays.yml

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
name: check-replays
2+
3+
permissions:
4+
contents: read
5+
pull-requests: write
6+
7+
on:
8+
workflow_call:
9+
inputs:
10+
game:
11+
required: true
12+
type: string
13+
description: "Game to check (only GeneralsMD for now)"
14+
userdata:
15+
required: true
16+
type: string
17+
description: "Path to folder with replays and maps"
18+
preset:
19+
required: true
20+
type: string
21+
description: "CMake preset"
22+
23+
jobs:
24+
build:
25+
name: ${{ inputs.preset }}
26+
runs-on: windows-latest
27+
timeout-minutes: 15
28+
env:
29+
GAME_PATH: C:\GameData
30+
GENERALS_PATH: C:\GameData\Generals
31+
GENERALSMD_PATH: C:\GameData\GeneralsMD
32+
steps:
33+
- name: Checkout Code
34+
uses: actions/checkout@v4
35+
with:
36+
submodules: true
37+
38+
- name: Download Game Artifact
39+
uses: actions/download-artifact@v4
40+
with:
41+
name: ${{ inputs.game }}-${{ inputs.preset }}
42+
path: build
43+
44+
- name: Cache Game Data
45+
id: cache-gamedata
46+
uses: actions/cache@v4
47+
with:
48+
path: ${{ env.GAME_PATH }}
49+
key: gamedata-permanent-cache-v3
50+
51+
- name: Download Game Data from Cloudflare R2
52+
if: ${{ steps.cache-gamedata.outputs.cache-hit != 'true' }}
53+
env:
54+
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
55+
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
56+
AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}
57+
EXPECTED_HASH_GENERALS: "37A351AA430199D1F05DEB9E404857DCE7B461A6AC272C5D4A0B5652CDB06372"
58+
EXPECTED_HASH_GENERALSMD: "6837FE1E3009A4C239406C39B1598216C0943EE8ED46BB10626767029AC05E21"
59+
shell: pwsh
60+
run: |
61+
# Download trimmed gamedata of both Generals 1.08 and Generals Zero Hour 1.04.
62+
# This data cannot be used for playing because it's
63+
# missing textures, audio and gui files. But it's enough for replay checking.
64+
# It's also encrypted because it's not allowed to distribute these files.
65+
66+
if (-not $env:AWS_ACCESS_KEY_ID -or -not $env:AWS_SECRET_ACCESS_KEY -or -not $env:AWS_ENDPOINT_URL) {
67+
$ok1 = [bool]$env:AWS_ACCESS_KEY_ID
68+
$ok2 = [bool]$env:AWS_SECRET_ACCESS_KEY
69+
$ok3 = [bool]$env:AWS_ENDPOINT_URL
70+
Write-Host "One or more required secrets are not set or are empty. R2_ACCESS_KEY_ID: $ok1, R2_SECRET_ACCESS_KEY: $ok2, R2_ENDPOINT_URL: $ok3"
71+
exit 1
72+
}
73+
74+
# Download Generals Game Files
75+
# The archive contains these files:
76+
# BINKW32.DLL
77+
# English.big
78+
# INI.big
79+
# Maps.big
80+
# mss32.dll
81+
# W3D.big
82+
# Data\Scripts\MultiplayerScripts.scb
83+
# Data\Scripts\SkirmishScripts.scb
84+
85+
Write-Host "Downloading Game Data for Generals" -ForegroundColor Cyan
86+
aws s3 cp s3://github-ci/generals108_gamedata_trimmed.7z generals108_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL
87+
88+
Write-Host "Verifying File Integrity" -ForegroundColor Cyan
89+
$fileHash = (Get-FileHash -Path generals108_gamedata_trimmed.7z -Algorithm SHA256).Hash
90+
Write-Host "Downloaded file SHA256: $fileHash"
91+
Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALS"
92+
if ($fileHash -ne $env:EXPECTED_HASH_GENERALS) {
93+
Write-Error "Hash verification failed! File may be corrupted or tampered with."
94+
exit 1
95+
}
96+
97+
Write-Host "Extracting Archive" -ForegroundColor Cyan
98+
& 7z x generals108_gamedata_trimmed.7z -o$env:GENERALS_PATH
99+
Remove-Item generals108_gamedata_trimmed.7z -Verbose
100+
101+
# Download GeneralsMD (ZH) Game Files
102+
# The archive contains these files:
103+
# BINKW32.DLL
104+
# INIZH.big
105+
# MapsZH.big
106+
# mss32.dll
107+
# W3DZH.big
108+
# Data\Scripts\MultiplayerScripts.scb
109+
# Data\Scripts\Scripts.ini
110+
# Data\Scripts\SkirmishScripts.scb
111+
112+
Write-Host "Downloading Game Data for GeneralsMD" -ForegroundColor Cyan
113+
aws s3 cp s3://github-ci/zerohour104_gamedata_trimmed.7z zerohour104_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL
114+
115+
Write-Host "Verifying File Integrity" -ForegroundColor Cyan
116+
$fileHash = (Get-FileHash -Path zerohour104_gamedata_trimmed.7z -Algorithm SHA256).Hash
117+
Write-Host "Downloaded file SHA256: $fileHash"
118+
Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALSMD"
119+
if ($fileHash -ne $env:EXPECTED_HASH_GENERALSMD) {
120+
Write-Error "Hash verification failed! File may be corrupted or tampered with."
121+
exit 1
122+
}
123+
124+
Write-Host "Extracting Archive" -ForegroundColor Cyan
125+
& 7z x zerohour104_gamedata_trimmed.7z -o$env:GENERALSMD_PATH
126+
Remove-Item zerohour104_gamedata_trimmed.7z -Verbose
127+
128+
- name: Set Up Game Data
129+
shell: pwsh
130+
run: |
131+
$source = "$env:GAME_PATH\${{ inputs.game }}"
132+
$destination = "build"
133+
Copy-Item -Path $source\* -Destination $destination -Recurse -Force
134+
135+
- name: Set Generals InstallPath in Registry
136+
shell: pwsh
137+
run: |
138+
# Zero Hour loads some Generals files and needs this registry key to find the
139+
# Generals data files.
140+
141+
$regPath = "HKCU:\SOFTWARE\Electronic Arts\EA Games\Generals"
142+
$installPath = "$env:GENERALS_PATH\"
143+
144+
# Ensure the key exists
145+
if (-not (Test-Path $regPath)) {
146+
New-Item -Path $regPath -Force | Out-Null
147+
}
148+
149+
# Set the InstallPath value
150+
Set-ItemProperty -Path $regPath -Name InstallPath -Value $installPath -Type String
151+
Write-Host "Registry key set: $regPath -> InstallPath = $installPath"
152+
153+
- name: Move Replays and Maps to User Dir
154+
shell: pwsh
155+
run: |
156+
# These files are expected in the user dir, so we move them here.
157+
158+
$source = "${{ inputs.userdata }}\Replays"
159+
$destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Replays"
160+
Write-Host "Move replays to $destination"
161+
New-Item -ItemType Directory -Path $destination -Force | Out-Null
162+
Move-Item -Path "$source\*" -Destination $destination -Force
163+
164+
$source = "${{ inputs.userdata }}\Maps"
165+
$destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Maps"
166+
Write-Host "Move maps to $destination"
167+
New-Item -ItemType Directory -Path $destination -Force | Out-Null
168+
Move-Item -Path "$source\*" -Destination $destination -Force
169+
170+
- name: Run Replay Compatibility Tests
171+
shell: pwsh
172+
run: |
173+
$exePath = "build/generalszh.exe"
174+
$arguments = "-jobs 4 -headless -replay *.rep"
175+
$timeoutSeconds = 10*60
176+
$stdoutPath = "stdout.log"
177+
$stderrPath = "stderr.log"
178+
179+
if (-not (Test-Path $exePath)) {
180+
Write-Host "ERROR: Executable not found at $exePath"
181+
exit 1
182+
}
183+
184+
# Note that the game is a gui application. That means we need to redirect console output to a file
185+
# in order to retrieve it.
186+
# Clean previous logs
187+
Remove-Item $stdoutPath, $stderrPath -ErrorAction SilentlyContinue
188+
189+
# Start the process
190+
Write-Host "Run $exePath $arguments"
191+
$process = Start-Process -FilePath $exePath `
192+
-ArgumentList $arguments `
193+
-RedirectStandardOutput $stdoutPath `
194+
-RedirectStandardError $stderrPath `
195+
-PassThru
196+
197+
# Wait with timeout
198+
$exited = $process.WaitForExit($timeoutSeconds * 1000)
199+
200+
if (-not $exited) {
201+
Write-Host "ERROR: Process still running after $timeoutSeconds seconds. Killing process..."
202+
Stop-Process -Id $process.Id -Force
203+
}
204+
205+
# Read output
206+
Write-Host "=== STDOUT ==="
207+
Get-Content $stdoutPath
208+
209+
if ((Test-Path $stderrPath) -and (Get-Item $stderrPath).Length -gt 0) {
210+
Write-Host "`n=== STDERR ==="
211+
Get-Content $stderrPath
212+
}
213+
214+
if (-not $exited) {
215+
exit 1
216+
}
217+
218+
# Check exit code
219+
$exitCode = $process.ExitCode
220+
221+
# The above doesn't work on all Windows versions. If not, try this: (see https://stackoverflow.com/a/16018287)
222+
#$process.HasExited | Out-Null # Needs to be called for the command below to work correctly
223+
#$exitCode = $process.GetType().GetField('exitCode', 'NonPublic, Instance').GetValue($process)
224+
#Write-Host "exit code $exitCode"
225+
226+
if ($exitCode -ne 0) {
227+
Write-Host "ERROR: Process failed with exit code $exitCode"
228+
exit $exitCode
229+
}
230+
231+
Write-Host "Success!"
232+
233+
- name: Upload Debug Log
234+
if: always()
235+
uses: actions/upload-artifact@v4
236+
with:
237+
name: Replay-Debug-Log-${{ inputs.preset }}
238+
path: build/DebugLogFile*.txt
239+
retention-days: 30
240+
if-no-files-found: ignore

.github/workflows/ci.yml

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ jobs:
4141
generalsmd:
4242
- 'GeneralsMD/**'
4343
shared:
44-
- '.github/workflows/build-toolchain.yml'
45-
- '.github/workflows/ci.yml'
44+
- '.github/workflows/**'
4645
- 'CMakeLists.txt'
4746
- 'CMakePresets.json'
4847
- 'cmake/**'
@@ -100,7 +99,9 @@ jobs:
10099
extras: ${{ matrix.extras }}
101100
secrets: inherit
102101

103-
build-generalsmd:
102+
# Note build-generalsmd is split into two jobs for vc6 and win32 because replaycheck-generalsmd
103+
# only requires the vc6 build and compiling vc6 is much faster than win32
104+
build-generalsmd-vc6:
104105
name: Build GeneralsMD${{ matrix.preset && '' }}
105106
needs: detect-changes
106107
if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }}
@@ -116,6 +117,25 @@ jobs:
116117
- preset: "vc6-debug"
117118
tools: true
118119
extras: true
120+
- preset: "vc6-releaselog"
121+
tools: true
122+
extras: true
123+
fail-fast: false
124+
uses: ./.github/workflows/build-toolchain.yml
125+
with:
126+
game: "GeneralsMD"
127+
preset: ${{ matrix.preset }}
128+
tools: ${{ matrix.tools }}
129+
extras: ${{ matrix.extras }}
130+
secrets: inherit
131+
132+
build-generalsmd-win32:
133+
name: Build GeneralsMD${{ matrix.preset && '' }}
134+
needs: detect-changes
135+
if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }}
136+
strategy:
137+
matrix:
138+
include:
119139
- preset: "win32"
120140
tools: true
121141
extras: true
@@ -143,3 +163,20 @@ jobs:
143163
tools: ${{ matrix.tools }}
144164
extras: ${{ matrix.extras }}
145165
secrets: inherit
166+
167+
replaycheck-generalsmd:
168+
name: Replay Check GeneralsMD${{ matrix.preset && '' }}
169+
needs: build-generalsmd-vc6
170+
if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }}
171+
strategy:
172+
matrix:
173+
include:
174+
- preset: "vc6+t+e"
175+
- preset: "vc6-releaselog+t+e" # optimized build with logging and crashing enabled should be compatible, so we test that here.
176+
fail-fast: false
177+
uses: ./.github/workflows/check-replays.yml
178+
with:
179+
game: "GeneralsMD"
180+
userdata: "GeneralsReplays/GeneralsZH/1.04"
181+
preset: ${{ matrix.preset }}
182+
secrets: inherit

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
!.gitignore
44
!.gitattributes
55
!.github
6+
!.gitmodules
67

78
*.user
89
*.ncb

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "GeneralsReplays"]
2+
path = GeneralsReplays
3+
url = https://github.com/TheSuperHackers/GeneralsReplays

CMakePresets.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@
4444
"RTS_BUILD_OPTION_DEBUG": "ON"
4545
}
4646
},
47+
{
48+
"name": "vc6-releaselog",
49+
"displayName": "Windows 32bit VC6 Release Logging",
50+
"inherits": "vc6",
51+
"cacheVariables": {
52+
"RTS_DEBUG_LOGGING": "ON",
53+
"RTS_DEBUG_CRASHING": "ON"
54+
}
55+
},
4756
{
4857
"name": "default",
4958
"displayName": "Default Config (don't use directly!)",
@@ -163,6 +172,12 @@
163172
"displayName": "Build Windows 32bit VC6 Debug",
164173
"description": "Build Windows 32bit VC6 Debug"
165174
},
175+
{
176+
"name": "vc6-releaselog",
177+
"configurePreset": "vc6-releaselog",
178+
"displayName": "Build Windows 32bit VC6 Release Logging",
179+
"description": "Build Windows 32bit VC6 Release Logging"
180+
},
166181
{
167182
"name": "win32",
168183
"configurePreset": "win32",
@@ -253,6 +268,19 @@
253268
}
254269
]
255270
},
271+
{
272+
"name": "vc6-releaselog",
273+
"steps": [
274+
{
275+
"type": "configure",
276+
"name": "vc6-releaselog"
277+
},
278+
{
279+
"type": "build",
280+
"name": "vc6-releaselog"
281+
}
282+
]
283+
},
256284
{
257285
"name": "win32",
258286
"steps": [

GeneralsReplays

Submodule GeneralsReplays added at af0c61c

TESTING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Test Replays
2+
3+
The GeneralsReplays folder contains replays and the required maps that are tested in CI to ensure that the game is retail compatible.
4+
5+
You can also test with these replays locally:
6+
- Copy the replays into a subfolder in your `%USERPROFILE%/Documents/Command and Conquer Generals Zero Hour Data/Replays` folder.
7+
- Copy the maps into `%USERPROFILE%/Documents/Command and Conquer Generals Zero Hour Data/Maps`
8+
- Start the test with this: (copy into a .bat file next to your executable)
9+
```
10+
START /B /W generalszh.exe -jobs 4 -headless -replay subfolder/*.rep > replay_check.log
11+
echo %errorlevel%
12+
PAUSE
13+
```
14+
It will run the game in the background and check that each replay is compatible. You need to use a VC6 build with optimizations and RTS_BUILD_OPTION_DEBUG = OFF, otherwise the game won't be compatible.

0 commit comments

Comments
 (0)