diff --git a/.github/workflows/build-toolchain.yml b/.github/workflows/build-toolchain.yml index 86a2877018..216d8d82c6 100644 --- a/.github/workflows/build-toolchain.yml +++ b/.github/workflows/build-toolchain.yml @@ -28,9 +28,9 @@ on: jobs: build: - name: Preset ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} + name: ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} runs-on: windows-2022 - timeout-minutes: 40 + timeout-minutes: 20 steps: - name: Checkout Code uses: actions/checkout@v4 diff --git a/.github/workflows/check-replays.yml b/.github/workflows/check-replays.yml new file mode 100644 index 0000000000..7dca0fba61 --- /dev/null +++ b/.github/workflows/check-replays.yml @@ -0,0 +1,240 @@ +name: check-replays + +permissions: + contents: read + pull-requests: write + +on: + workflow_call: + inputs: + game: + required: true + type: string + description: "Game to check (only GeneralsMD for now)" + userdata: + required: true + type: string + description: "Path to folder with replays and maps" + preset: + required: true + type: string + description: "CMake preset" + +jobs: + build: + name: ${{ inputs.preset }} + runs-on: windows-latest + timeout-minutes: 15 + env: + GAME_PATH: C:\GameData + GENERALS_PATH: C:\GameData\Generals + GENERALSMD_PATH: C:\GameData\GeneralsMD + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Download Game Artifact + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.game }}-${{ inputs.preset }} + path: build + + - name: Cache Game Data + id: cache-gamedata + uses: actions/cache@v4 + with: + path: ${{ env.GAME_PATH }} + key: gamedata-permanent-cache-v3 + + - name: Download Game Data from Cloudflare R2 + if: ${{ steps.cache-gamedata.outputs.cache-hit != 'true' }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }} + EXPECTED_HASH_GENERALS: "37A351AA430199D1F05DEB9E404857DCE7B461A6AC272C5D4A0B5652CDB06372" + EXPECTED_HASH_GENERALSMD: "6837FE1E3009A4C239406C39B1598216C0943EE8ED46BB10626767029AC05E21" + shell: pwsh + run: | + # Download trimmed gamedata of both Generals 1.08 and Generals Zero Hour 1.04. + # This data cannot be used for playing because it's + # missing textures, audio and gui files. But it's enough for replay checking. + # It's also encrypted because it's not allowed to distribute these files. + + if (-not $env:AWS_ACCESS_KEY_ID -or -not $env:AWS_SECRET_ACCESS_KEY -or -not $env:AWS_ENDPOINT_URL) { + $ok1 = [bool]$env:AWS_ACCESS_KEY_ID + $ok2 = [bool]$env:AWS_SECRET_ACCESS_KEY + $ok3 = [bool]$env:AWS_ENDPOINT_URL + 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" + exit 1 + } + + # Download Generals Game Files + # The archive contains these files: + # BINKW32.DLL + # English.big + # INI.big + # Maps.big + # mss32.dll + # W3D.big + # Data\Scripts\MultiplayerScripts.scb + # Data\Scripts\SkirmishScripts.scb + + Write-Host "Downloading Game Data for Generals" -ForegroundColor Cyan + aws s3 cp s3://github-ci/generals108_gamedata_trimmed.7z generals108_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL + + Write-Host "Verifying File Integrity" -ForegroundColor Cyan + $fileHash = (Get-FileHash -Path generals108_gamedata_trimmed.7z -Algorithm SHA256).Hash + Write-Host "Downloaded file SHA256: $fileHash" + Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALS" + if ($fileHash -ne $env:EXPECTED_HASH_GENERALS) { + Write-Error "Hash verification failed! File may be corrupted or tampered with." + exit 1 + } + + Write-Host "Extracting Archive" -ForegroundColor Cyan + & 7z x generals108_gamedata_trimmed.7z -o$env:GENERALS_PATH + Remove-Item generals108_gamedata_trimmed.7z -Verbose + + # Download GeneralsMD (ZH) Game Files + # The archive contains these files: + # BINKW32.DLL + # INIZH.big + # MapsZH.big + # mss32.dll + # W3DZH.big + # Data\Scripts\MultiplayerScripts.scb + # Data\Scripts\Scripts.ini + # Data\Scripts\SkirmishScripts.scb + + Write-Host "Downloading Game Data for GeneralsMD" -ForegroundColor Cyan + aws s3 cp s3://github-ci/zerohour104_gamedata_trimmed.7z zerohour104_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL + + Write-Host "Verifying File Integrity" -ForegroundColor Cyan + $fileHash = (Get-FileHash -Path zerohour104_gamedata_trimmed.7z -Algorithm SHA256).Hash + Write-Host "Downloaded file SHA256: $fileHash" + Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALSMD" + if ($fileHash -ne $env:EXPECTED_HASH_GENERALSMD) { + Write-Error "Hash verification failed! File may be corrupted or tampered with." + exit 1 + } + + Write-Host "Extracting Archive" -ForegroundColor Cyan + & 7z x zerohour104_gamedata_trimmed.7z -o$env:GENERALSMD_PATH + Remove-Item zerohour104_gamedata_trimmed.7z -Verbose + + - name: Set Up Game Data + shell: pwsh + run: | + $source = "$env:GAME_PATH\${{ inputs.game }}" + $destination = "build" + Copy-Item -Path $source\* -Destination $destination -Recurse -Force + + - name: Set Generals InstallPath in Registry + shell: pwsh + run: | + # Zero Hour loads some Generals files and needs this registry key to find the + # Generals data files. + + $regPath = "HKCU:\SOFTWARE\Electronic Arts\EA Games\Generals" + $installPath = "$env:GENERALS_PATH\" + + # Ensure the key exists + if (-not (Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + + # Set the InstallPath value + Set-ItemProperty -Path $regPath -Name InstallPath -Value $installPath -Type String + Write-Host "Registry key set: $regPath -> InstallPath = $installPath" + + - name: Move Replays and Maps to User Dir + shell: pwsh + run: | + # These files are expected in the user dir, so we move them here. + + $source = "${{ inputs.userdata }}\Replays" + $destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Replays" + Write-Host "Move replays to $destination" + New-Item -ItemType Directory -Path $destination -Force | Out-Null + Move-Item -Path "$source\*" -Destination $destination -Force + + $source = "${{ inputs.userdata }}\Maps" + $destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Maps" + Write-Host "Move maps to $destination" + New-Item -ItemType Directory -Path $destination -Force | Out-Null + Move-Item -Path "$source\*" -Destination $destination -Force + + - name: Run Replay Compatibility Tests + shell: pwsh + run: | + $exePath = "build/generalszh.exe" + $arguments = "-jobs 4 -headless -replay *.rep" + $timeoutSeconds = 10*60 + $stdoutPath = "stdout.log" + $stderrPath = "stderr.log" + + if (-not (Test-Path $exePath)) { + Write-Host "ERROR: Executable not found at $exePath" + exit 1 + } + + # Note that the game is a gui application. That means we need to redirect console output to a file + # in order to retrieve it. + # Clean previous logs + Remove-Item $stdoutPath, $stderrPath -ErrorAction SilentlyContinue + + # Start the process + Write-Host "Run $exePath $arguments" + $process = Start-Process -FilePath $exePath ` + -ArgumentList $arguments ` + -RedirectStandardOutput $stdoutPath ` + -RedirectStandardError $stderrPath ` + -PassThru + + # Wait with timeout + $exited = $process.WaitForExit($timeoutSeconds * 1000) + + if (-not $exited) { + Write-Host "ERROR: Process still running after $timeoutSeconds seconds. Killing process..." + Stop-Process -Id $process.Id -Force + } + + # Read output + Write-Host "=== STDOUT ===" + Get-Content $stdoutPath + + if ((Test-Path $stderrPath) -and (Get-Item $stderrPath).Length -gt 0) { + Write-Host "`n=== STDERR ===" + Get-Content $stderrPath + } + + if (-not $exited) { + exit 1 + } + + # Check exit code + $exitCode = $process.ExitCode + + # The above doesn't work on all Windows versions. If not, try this: (see https://stackoverflow.com/a/16018287) + #$process.HasExited | Out-Null # Needs to be called for the command below to work correctly + #$exitCode = $process.GetType().GetField('exitCode', 'NonPublic, Instance').GetValue($process) + #Write-Host "exit code $exitCode" + + if ($exitCode -ne 0) { + Write-Host "ERROR: Process failed with exit code $exitCode" + exit $exitCode + } + + Write-Host "Success!" + + - name: Upload Debug Log + if: always() + uses: actions/upload-artifact@v4 + with: + name: Replay-Debug-Log-${{ inputs.preset }} + path: build/DebugLogFile*.txt + retention-days: 30 + if-no-files-found: ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed86c978b3..2d0a8efd65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,7 @@ jobs: generalsmd: - 'GeneralsMD/**' shared: - - '.github/workflows/build-toolchain.yml' - - '.github/workflows/ci.yml' + - '.github/workflows/**' - 'CMakeLists.txt' - 'CMakePresets.json' - 'cmake/**' @@ -100,7 +99,9 @@ jobs: extras: ${{ matrix.extras }} secrets: inherit - build-generalsmd: + # Note build-generalsmd is split into two jobs for vc6 and win32 because replaycheck-generalsmd + # only requires the vc6 build and compiling vc6 is much faster than win32 + build-generalsmd-vc6: name: Build GeneralsMD${{ matrix.preset && '' }} needs: detect-changes if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} @@ -116,6 +117,25 @@ jobs: - preset: "vc6-debug" tools: true extras: true + - preset: "vc6-releaselog" + tools: true + extras: true + fail-fast: false + uses: ./.github/workflows/build-toolchain.yml + with: + game: "GeneralsMD" + preset: ${{ matrix.preset }} + tools: ${{ matrix.tools }} + extras: ${{ matrix.extras }} + secrets: inherit + + build-generalsmd-win32: + name: Build GeneralsMD${{ matrix.preset && '' }} + needs: detect-changes + if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} + strategy: + matrix: + include: - preset: "win32" tools: true extras: true @@ -143,3 +163,20 @@ jobs: tools: ${{ matrix.tools }} extras: ${{ matrix.extras }} secrets: inherit + + replaycheck-generalsmd: + name: Replay Check GeneralsMD${{ matrix.preset && '' }} + needs: build-generalsmd-vc6 + if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} + strategy: + matrix: + include: + - preset: "vc6+t+e" + - preset: "vc6-releaselog+t+e" # optimized build with logging and crashing enabled should be compatible, so we test that here. + fail-fast: false + uses: ./.github/workflows/check-replays.yml + with: + game: "GeneralsMD" + userdata: "GeneralsReplays/GeneralsZH/1.04" + preset: ${{ matrix.preset }} + secrets: inherit diff --git a/.gitignore b/.gitignore index e5e550b48d..9ae9ce4288 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ !.gitignore !.gitattributes !.github +!.gitmodules *.user *.ncb diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..09d4419ccc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "GeneralsReplays"] + path = GeneralsReplays + url = https://github.com/TheSuperHackers/GeneralsReplays diff --git a/CMakePresets.json b/CMakePresets.json index 51610bf95c..6dd63816a8 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -44,6 +44,15 @@ "RTS_BUILD_OPTION_DEBUG": "ON" } }, + { + "name": "vc6-releaselog", + "displayName": "Windows 32bit VC6 Release Logging", + "inherits": "vc6", + "cacheVariables": { + "RTS_DEBUG_LOGGING": "ON", + "RTS_DEBUG_CRASHING": "ON" + } + }, { "name": "default", "displayName": "Default Config (don't use directly!)", @@ -163,6 +172,12 @@ "displayName": "Build Windows 32bit VC6 Debug", "description": "Build Windows 32bit VC6 Debug" }, + { + "name": "vc6-releaselog", + "configurePreset": "vc6-releaselog", + "displayName": "Build Windows 32bit VC6 Release Logging", + "description": "Build Windows 32bit VC6 Release Logging" + }, { "name": "win32", "configurePreset": "win32", @@ -253,6 +268,19 @@ } ] }, + { + "name": "vc6-releaselog", + "steps": [ + { + "type": "configure", + "name": "vc6-releaselog" + }, + { + "type": "build", + "name": "vc6-releaselog" + } + ] + }, { "name": "win32", "steps": [ diff --git a/GeneralsReplays b/GeneralsReplays new file mode 160000 index 0000000000..af0c61ccdc --- /dev/null +++ b/GeneralsReplays @@ -0,0 +1 @@ +Subproject commit af0c61ccdccbf06750b0ecd2de99fe65842bfc1a diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000000..bcfed39142 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,14 @@ +# Test Replays + +The GeneralsReplays folder contains replays and the required maps that are tested in CI to ensure that the game is retail compatible. + +You can also test with these replays locally: +- Copy the replays into a subfolder in your `%USERPROFILE%/Documents/Command and Conquer Generals Zero Hour Data/Replays` folder. +- Copy the maps into `%USERPROFILE%/Documents/Command and Conquer Generals Zero Hour Data/Maps` +- Start the test with this: (copy into a .bat file next to your executable) +``` +START /B /W generalszh.exe -jobs 4 -headless -replay subfolder/*.rep > replay_check.log +echo %errorlevel% +PAUSE +``` +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. \ No newline at end of file