Compare commits

19 changed files with 732 additions and 9 deletions

No files matched your search

@@ -0,0 +1,76 @@
name: Upload frozen preview to existing draft
on:
workflow_dispatch:
permissions: {}
concurrency:
group: frozen-preview-v0.1.0-preview.20261020
cancel-in-progress: false
jobs:
upload-draft:
if: github.repository == 'Quintine/RustRaptor'
runs-on: ubuntu-latest
timeout-minutes: 180
permissions:
contents: write
env:
PYTHONDONTWRITEBYTECODE: '1'
steps:
- name: Make room before any source checkout
shell: bash
run: |
set -euo pipefail
minimum=25000000000
available() { df --output=avail -B1 "$GITHUB_WORKSPACE" | tail -1 | tr -d ' '; }
for directory in /usr/local/lib/android /usr/share/dotnet /opt/ghc; do
if (( $(available) >= minimum )); then break; fi
sudo rm -rf -- "$directory"
done
(( $(available) >= minimum )) || { echo 'Need at least 25GB free before source checkout.' >&2; exit 1; }
df -h "$GITHUB_WORKSPACE"
- name: Fetch only the publishing controller from this maintenance commit
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
path: controller
filter: blob:none
sparse-checkout-cone-mode: false
sparse-checkout: |
/tools/upload_draft_release.py
/tools/test_upload_draft_release.py
persist-credentials: false
- name: Fetch the immutable tested game checkpoint
uses: actions/checkout@v4
with:
ref: 7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555
path: frozen
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Test the existing exporter and publishing guards
shell: bash
run: |
set -euo pipefail
python -B -m unittest discover -s frozen/tools -p test_export_release.py -v
python -B -m unittest discover -s controller/tools -p test_upload_draft_release.py -v
- name: Export and verify every asset on the existing draft
env:
GH_TOKEN: ${{ github.token }}
run: >-
python -B controller/tools/upload_draft_release.py
--package "$GITHUB_WORKSPACE/frozen"
--state "$RUNNER_TEMP/RustRaptor-draft-export-state"
--temporary "$RUNNER_TEMP/RustRaptor-single-export"
- name: Retain frozen plan, local receipts, remote proofs and logs
if: always()
uses: actions/upload-artifact@v4
with:
name: frozen-preview-draft-export-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/RustRaptor-draft-export-state
if-no-files-found: warn
retention-days: 30
+112
View File
@@ -0,0 +1,112 @@
name: Upload frozen release to existing draft
on:
workflow_dispatch:
inputs:
commit:
description: Exact tested 40-character game commit
required: true
type: string
tag:
description: Exact existing game tag
required: true
type: string
release_id:
description: Exact existing empty draft release ID
required: true
type: string
prerelease:
description: Expected prerelease flag on the existing draft
required: true
default: false
type: boolean
permissions: {}
concurrency:
group: frozen-draft-release-${{ inputs.release_id }}
cancel-in-progress: false
jobs:
upload-draft:
if: github.repository == 'Quintine/RustRaptor'
runs-on: ubuntu-latest
timeout-minutes: 180
permissions:
contents: write
env:
PYTHONDONTWRITEBYTECODE: '1'
FROZEN_COMMIT: ${{ inputs.commit }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_ID: ${{ inputs.release_id }}
EXPECT_PRERELEASE: ${{ inputs.prerelease }}
steps:
- name: Validate exact release inputs before any checkout
shell: bash
run: |
python3 - <<'PYTHON'
import os, re
assert re.fullmatch('[0-9a-f]{40}', os.environ['FROZEN_COMMIT']), 'Exact commit required'
assert re.fullmatch(r'v[0-9A-Za-z._-]+', os.environ['RELEASE_TAG']), 'Safe exact tag required'
assert re.fullmatch('[1-9][0-9]*', os.environ['RELEASE_ID']), 'Positive release ID required'
assert os.environ['EXPECT_PRERELEASE'] in ('true', 'false'), 'Boolean prerelease expectation required'
PYTHON
- name: Make room before any source checkout
shell: bash
run: |
set -euo pipefail
minimum=25000000000
available() { df --output=avail -B1 "$GITHUB_WORKSPACE" | tail -1 | tr -d ' '; }
for directory in /usr/local/lib/android /usr/share/dotnet /opt/ghc; do
if (( $(available) >= minimum )); then break; fi
sudo rm -rf -- "$directory"
done
(( $(available) >= minimum )) || { echo 'Need at least 25GB free before source checkout.' >&2; exit 1; }
df -h "$GITHUB_WORKSPACE"
- name: Fetch only the publishing controller from this maintenance commit
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
path: controller
filter: blob:none
sparse-checkout-cone-mode: false
sparse-checkout: |
/tools/upload_draft_release.py
/tools/test_upload_draft_release.py
persist-credentials: false
- name: Fetch the immutable tested game checkpoint
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit }}
path: frozen
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Test the existing exporter and publishing guards
shell: bash
run: |
set -euo pipefail
python -B -m unittest discover -s frozen/tools -p test_export_release.py -v
python -B -m unittest discover -s controller/tools -p test_upload_draft_release.py -v
- name: Export and verify every asset on the existing draft
env:
GH_TOKEN: ${{ github.token }}
run: >-
python -B controller/tools/upload_draft_release.py
--package "$GITHUB_WORKSPACE/frozen"
--state "$RUNNER_TEMP/RustRaptor-draft-export-state"
--temporary "$RUNNER_TEMP/RustRaptor-single-export"
--commit "$FROZEN_COMMIT"
--tag "$RELEASE_TAG"
--release-id "$RELEASE_ID"
--prerelease "$EXPECT_PRERELEASE"
- name: Retain frozen plan, local receipts, remote proofs and logs
if: always()
uses: actions/upload-artifact@v4
with:
name: frozen-release-draft-export-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/RustRaptor-draft-export-state
if-no-files-found: warn
retention-days: 30
Generated
+1 -1
View File
@@ -449,7 +449,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "raptor-replica" name = "raptor-replica"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "raptor-replica" name = "raptor-replica"
version = "0.1.0" version = "1.0.0"
edition = "2024" edition = "2024"
rust-version = "1.88" rust-version = "1.88"
license = "GPL-2.0-or-later" license = "GPL-2.0-or-later"
+2
View File
@@ -1,5 +1,7 @@
# RustRaptor # RustRaptor
**v1.0.0 artwork note:** HD coverage is **5,743 of 5,753 original pictures**. Ten pictures retain their original graphics; HD coverage is not complete. All original game data and the 45 bundled mods remain included.
The independent Linux and Windows Rust game and its complete local mod packages live here. The independent Linux and Windows Rust game and its complete local mod packages live here.
SDL2 handles platform services; gameplay, asset decoding, rendering, audio, SDL2 handles platform services; gameplay, asset decoding, rendering, audio,
saving and mod scripting run in Rust. saving and mod scripting run in Rust.
BIN
View File
Binary file not shown.
Binary file not shown.
+18 -1
View File
@@ -1,5 +1,9 @@
# Downloadable releases # Downloadable releases
Current release: [v1.0.0 — Linux and Windows](https://github.com/Quintine/RustRaptor/releases/tag/v1.0.0).
**Artwork note:** 5,743 of 5,753 original pictures have approved HD remasters. Ten retain original-art fallback; v1.0.0 does not claim complete HD coverage.
The GitHub release provides playable Linux x86_64 and Windows x86_64 packages. The GitHub release provides playable Linux x86_64 and Windows x86_64 packages.
Cargo, Python, the old engine and a Git checkout are unnecessary for play. Cargo, Python, the old engine and a Git checkout are unnecessary for play.
@@ -77,10 +81,23 @@ export copy before producing the next. Keep receipts. After all assets exist:
```sh ```sh
python3 tools/export_release.py checksums --plan /tmp/release-plan.json \ python3 tools/export_release.py checksums --plan /tmp/release-plan.json \
--receipts /path/to/receipts --output /tmp/SHA256SUMS \ --receipts /path/to/receipts --output /tmp/SHA256SUMS \
--manifest-output /tmp/release-manifest.json --tag v0.1.0-preview.20261020 --manifest-output /tmp/release-manifest.json --tag v1.0.0
``` ```
Upload the versioned release manifest, checksum list and installers, check complete remote membership, and Upload the versioned release manifest, checksum list and installers, check complete remote membership, and
publish the draft only after all checks pass. Native Windows CI and local publish the draft only after all checks pass. Native Windows CI and local
representative rendering/audio evidence retain their actual commit and scope; representative rendering/audio evidence retain their actual commit and scope;
archive integrity checks do not imply a fresh full playthrough. archive integrity checks do not imply a fresh full playthrough.
## Hosted export
The manually dispatched **Upload frozen release to existing draft** workflow
accepts the exact tested 40-character commit, existing version tag, empty draft
release ID and expected prerelease flag. For v1.0.0 select `false` for prerelease.
The publishing controller is checked out separately from the immutable game
checkpoint. Every source and remote release identity is checked, all archives
are verified, and the frozen plan and receipts are retained as an Actions
artifact. The job leaves the draft unpublished for final review.
The older preview workflow retains its fixed 20 October checkpoint and cannot
modify its already-published release. Neither workflow publishes automatically.
+84
View File
@@ -4122,5 +4122,89 @@
"new_World_intrinsicQA_imagegen_game_build_calls_during_assembly": 0, "new_World_intrinsicQA_imagegen_game_build_calls_during_assembly": 0,
"native_Windows_scope": "Dated native5654 SUCCESS39229967668 for126d28ba; no native 5743 claim.", "native_Windows_scope": "Dated native5654 SUCCESS39229967668 for126d28ba; no native 5743 claim.",
"scope": "Exact50 Terrain pictures:32 Source-guided derived, nine untouched genuine-atlas references, three references to a partly derived shared atlas and six lossless genuine-atlas crops.304fb alone is patched;304fa/304fc cell pixels are unchanged inside that derived file.211 retained Original cells and all Source construction get zero generated detail credit.2043f/20458 are unused catalogue-only, not runtime-placed. Literal original measurements and separate joint35 H5654+89 overlay remain distinct; the overlay is not a full5743 measured bank. Different peer checked all 560 saved native roles; Root all 35 native4. Static finite controls do not claim every caller, World, complete missions, audio or performance. Ten provider-refused originals remain unapproved." "scope": "Exact50 Terrain pictures:32 Source-guided derived, nine untouched genuine-atlas references, three references to a partly derived shared atlas and six lossless genuine-atlas crops.304fb alone is patched;304fa/304fc cell pixels are unchanged inside that derived file.211 retained Original cells and all Source construction get zero generated detail credit.2043f/20458 are unused catalogue-only, not runtime-placed. Literal original measurements and separate joint35 H5654+89 overlay remain distinct; the overlay is not a full5743 measured bank. Different peer checked all 560 saved native roles; Root all 35 native4. Static finite controls do not claim every caller, World, complete missions, audio or performance. Ten provider-refused originals remain unapproved."
},
"v1_0_0_distribution": {
"status": "v1_0_0_fresh_binaries_local_CLI_and_SDL_pass_native_Windows_and_export_pending",
"version": "1.0.0",
"approved_hd_frames": 5743,
"source_picture_targets": 5753,
"remaining_original_pictures": 10,
"complete_remaster": false,
"remaining_ids": [
"1035b",
"1035c",
"1035d",
"1035e",
"300a1",
"301ae",
"40374",
"404d2",
"4050b",
"40512"
],
"engine_scope": "Same18Rustsourcefiles as SourceG2; Cargo package version only changed. No new artwork, simulation, mod or data changes. Earlier receipts retain their actual binaries and scope.",
"linux_binary_sha256": "14d85a8377078cf6abba44d72bc4eb8ef2760c6a3c810dd51e2d55d4ff91a5af",
"windows_binary_sha256": "187a927767c7042c6c54f0307ac30c08504b1bd46c3b742e34617afa798d93f0",
"data_files": 32373,
"data_bytes": 10376039515,
"data_manifest_map_unchanged": true,
"installed_cli": [
{
"args": [
"--verify-assets"
],
"exit": 0,
"stdout": "Verified 6449 assets; 27 missions, 528 enemies, 16 weapons, 24 items\n",
"stderr": "",
"elapsed_seconds": 0.5691442220122553
},
{
"args": [
"--validate-hd"
],
"exit": 0,
"stdout": "HD pack: 5743 registered, 5743 validated, 0 rejected supplied frames\n",
"stderr": "",
"elapsed_seconds": 160.12859854800627
},
{
"args": [
"--mods",
"/home/quintine/Projects/RaptorReplica/RustRaptor/mods",
"--profile-root",
"/home/quintine/Projects/RaptorReplica/RustRaptor/.reference/v1-release-prep-20261021/root-state/validation-profile",
"--validate-mods",
"aurora,ion_lance,iron_coast_boss,iron_coast_boss_2,iron_coast_boss_3,iron_coast_boss_4,iron_coast_boss_5,iron_coast_boss_6,iron_coast_campaign,iron_coast_depth_charge,iron_coast_mission_1,iron_coast_mission_2,iron_coast_mission_3,iron_coast_mission_4,iron_coast_mission_5,iron_coast_mission_6,iron_coast_music,iron_coast_railburst,iron_coast_scout,iron_coast_stage2_boss_1,iron_coast_stage2_boss_2,iron_coast_stage2_boss_3,iron_coast_stage2_boss_4,iron_coast_stage2_boss_5,iron_coast_stage2_boss_6,iron_coast_stage2_boss_7,iron_coast_stage2_boss_8,iron_coast_stage2_campaign,iron_coast_stage2_interceptor,iron_coast_stage2_mission_1,iron_coast_stage2_mission_2,iron_coast_stage2_mission_3,iron_coast_stage2_mission_4,iron_coast_stage2_mission_5,iron_coast_stage2_mission_6,iron_coast_stage2_mission_7,iron_coast_stage2_mission_8,iron_coast_stage2_music,iron_coast_stage2_siege,iron_coast_stage2_skimmer,iron_coast_turret,lightning_cannon,lightning_shield,pulse_cannon,seeker_swarm"
],
"exit": 0,
"stdout": "Validated 45 mods; 1 override(s)\n",
"stderr": "",
"elapsed_seconds": 8.680746864993125
}
],
"linux_SDL_smoke": {
"frames": 240,
"resolution": [
1920,
1080
],
"HD_mode": true,
"mission": "base:sector_1_wave_1",
"audio": {
"frames": 132300,
"rate": 44100,
"channels": 2,
"nonzero_samples": 260239
},
"screenshot_sha256": "65a65351583a1f4602268ca67280e8f7e6f2e2904d5e09fd0db508a24da00ab0",
"audio_sha256": "fe037b25fc2f0267b3ed151ed20a1502070433765a1ae173855402010c3bdefe",
"scope": "240 render frames of base sector1wave1, software SDL/dummy audio capture; not hardware audio or complete mission playthrough"
},
"packaging_tests": {
"exporter": 22,
"publisher_guards": 10
},
"native_windows_execution": "pending new1.0.0 CI; prior7ec result is historical",
"distribution_note": "Ten pictures retain original graphics. v1.0.0 does not claim complete HD coverage."
} }
} }
+8
View File
@@ -1,5 +1,13 @@
# Current approved-artwork checkpoint — 20 October 2026 # Current approved-artwork checkpoint — 20 October 2026
v1.0.0 preparation — 21 October 2026: the user approved release with a prominent **5,743/5,753 HD; ten original-art fallbacks** note. Both executables were rebuilt with Cargo package version 1.0.0 using the existing Rust 1.99 toolchains. Engine source, mods and all 32,373 data-file hashes are unchanged. Fresh installed CLI passed catalog, all HD frames and 45 mods; one 240-frame 1080p HD SDL capture has non-silent stereo audio. Exporter 22 and publisher 10 checks pass. Native Windows CI, frozen-tag export and public v1.0.0 publication remain pending at this source checkpoint. The tracked release note is `release/notes-v1.0.0.md`.
Published release checkpoint — 20 October 2026: [RustRaptor 0.1.0 Preview](https://github.com/Quintine/RustRaptor/releases/tag/v0.1.0-preview.20261020) is public, with all 13 assets verified: seven complete shared-data ZIPs, Linux and Windows platform ZIPs, two installers, the versioned manifest and checksums. The helpers assemble one self-contained folder with original data, 45 mods and their artwork/audio.
The release tag is fixed at `7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555`. [Native Windows game validation](https://github.com/Quintine/RustRaptor/actions/runs/39410204613) passed at that exact commit. [Hosted export](https://github.com/Quintine/RustRaptor/actions/runs/39411973689) checked every archive and all remote hashes; final independent review matched all 48,209 archive payload rows and 13 remote assets. The later maintenance commit `991b471f` adds the hosted publishing controller without moving the release tag or changing game data.
Current HD coverage is **5,743 / 5,753 original pictures**. Ten remain unapproved and retain original-art fallback. The reviewed 50-picture append was published in `443d7785`; no further artwork was generated for the release.
Current artwork checkpoint — 20 October 2026: **5,743 reviewed pictures / 3,830 complete sequences**, including **4,530 Terrain pictures**. **Ten originals remain unapproved**: five Terrain, four enemies/ground objects and one UI/other picture. Generation was declined for those ten; they are not counted as remastered. Current artwork checkpoint — 20 October 2026: **5,743 reviewed pictures / 3,830 complete sequences**, including **4,530 Terrain pictures**. **Ten originals remain unapproved**: five Terrain, four enemies/ground objects and one UI/other picture. Generation was declined for those ten; they are not counted as remastered.
This 50-picture Terrain append covers dark/night material, coastlines, industrial structures and the remaining first/late-campaign terrain. Its PNG provenance is 32 Source-guided derived pictures, nine selections from untouched genuine atlases, three pictures sharing one partially derived atlas, and six lossless genuine-atlas crops. Only 304fb is patched in the shared atlas; 304fa/304fc cells remain unchanged genuine pixels. All 211 retained Original owners, Source geometry and tonal backing earn no generated-HD-detail credit. Stepped joins, dark material and native-scale detail limits remain documented. Two unused assets, 2043f/20458, have catalogue/intrinsic approval without an invented runtime placement. This 50-picture Terrain append covers dark/night material, coastlines, industrial structures and the remaining first/late-campaign terrain. Its PNG provenance is 32 Source-guided derived pictures, nine selections from untouched genuine atlases, three pictures sharing one partially derived atlas, and six lossless genuine-atlas crops. Only 304fb is patched in the shared atlas; 304fa/304fc cells remain unchanged genuine pixels. All 211 retained Original owners, Source geometry and tonal backing earn no generated-HD-detail credit. Stepped joins, dark material and native-scale detail limits remain documented. Two unused assets, 2043f/20458, have catalogue/intrinsic approval without an invented runtime placement.
+12 -1
View File
@@ -57,7 +57,18 @@ The [Windows workflow](../.github/workflows/windows.yml) runs simulation,
modding and save tests on Windows, validates packaged assets and all local modding and save tests on Windows, validates packaged assets and all local
mods, captures SDL gameplay and publishes the zip as a workflow artifact. mods, captures SDL gameplay and publishes the zip as a workflow artifact.
## Current runtime and validation ## Current v1.0.0 runtime
The included version 1.0.0 GNU executable has SHA-256
`187a927767c7042c6c54f0307ac30c08504b1bd46c3b742e34617afa798d93f0`.
It shares **5,743 approved HD frames**, **32,373 data files / 10,376,039,515 bytes**
and **45 mods** with Linux. Ten original pictures retain original-art fallback.
The release does not claim complete HD coverage. PE imports and SDL dependencies
are unchanged from SourceG2; the runtime manifests contain the exact current hashes.
Fresh native Windows execution for this rebuilt version is pending its CI run.
Earlier CI and exported-folder observations below retain their actual snapshots.
## Historical runtime and validation
The included GNU executable uses the reviewed **2,935-frame HD snapshot**; The included GNU executable uses the reviewed **2,935-frame HD snapshot**;
the shared manifest binds **7,816 data files** and the package contains the shared manifest binds **7,816 data files** and the package contains
+1 -1
View File
@@ -16,7 +16,7 @@ import tempfile
import urllib.request import urllib.request
import zipfile import zipfile
TAG = 'v0.1.0-preview.20261020' TAG = 'v1.0.0'
BASE = 'https://github.com/Quintine/RustRaptor/releases/download/' + TAG BASE = 'https://github.com/Quintine/RustRaptor/releases/download/' + TAG
+1 -1
View File
@@ -1,6 +1,6 @@
param([string]$Destination = (Get-Location).Path) param([string]$Destination = (Get-Location).Path)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$tag = 'v0.1.0-preview.20261020' $tag = 'v1.0.0'
$base = "https://github.com/Quintine/RustRaptor/releases/download/$tag" $base = "https://github.com/Quintine/RustRaptor/releases/download/$tag"
if (-not [Environment]::Is64BitOperatingSystem) { throw 'RustRaptor requires 64-bit Windows.' } if (-not [Environment]::Is64BitOperatingSystem) { throw 'RustRaptor requires 64-bit Windows.' }
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+1 -1
View File
@@ -34,7 +34,7 @@ try {
$longMember = 'data/' + (('long-proof-component/' * 20)) + 'accepted.json' $longMember = 'data/' + (('long-proof-component/' * 20)) + 'accepted.json'
$data = New-FixtureArchive 'RustRaptor-data-01-of-01.zip' 'shared-data' $longMember $data = New-FixtureArchive 'RustRaptor-data-01-of-01.zip' 'shared-data' $longMember
$core = New-FixtureArchive 'RustRaptor-windows-x86_64.zip' 'windows' 'run.cmd' $core = New-FixtureArchive 'RustRaptor-windows-x86_64.zip' 'windows' 'run.cmd'
$manifest = @{format_version=1; tag='v0.1.0-preview.20261020'; assets=@($data, $core)} $manifest = @{format_version=1; tag='v1.0.0'; assets=@($data, $core)}
$manifestPath = Join-Path $global:RaptorInstallerFixtureDownloads 'release-manifest.json' $manifestPath = Join-Path $global:RaptorInstallerFixtureDownloads 'release-manifest.json'
$manifest | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $manifestPath -Encoding UTF8 $manifest | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
$destination = Join-Path $temp 'portable folder' $destination = Join-Path $temp 'portable folder'
+22
View File
@@ -0,0 +1,22 @@
# RustRaptor v1.0.0
Self-contained Linux x86_64 and Windows x86_64 packages, with original game data, the classic intro and sound, the in-game mod manager and all 45 bundled mods. Includes both Iron Coast stages, their 14 missions, bosses, weapons and original music, plus SD/HD mission briefings.
**HD artwork note: 5,743 of 5,753 original pictures have approved HD remasters. Ten pictures retain their original graphics. This release does not claim complete HD coverage.** The remaining IDs are `1035b`, `1035c`, `1035d`, `1035e`, `300a1`, `301ae`, `40374`, `404d2`, `4050b` and `40512`. Original and crisp scaling modes remain available; HD art does not alter collision dimensions or simulation.
This release keeps the gameplay, mod content and artwork from the tested 20 October preview, with freshly rebuilt version 1.0.0 Linux and Windows executables. No new artwork was generated for this release.
## Download and install
Download `Install-Windows.ps1` or `Install-Linux.sh` from this release and run it in a directory without an existing `RustRaptor` folder:
- Windows: `powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-Windows.ps1`
- Linux: `bash ./Install-Linux.sh` (the optional download helper needs Bash and Python 3.8+; the game does not require Python).
The helper downloads your platform ZIP and all seven shared-data ZIPs, checks archive and payload SHA-256 hashes, and assembles one portable folder. Start `RustRaptor/run.cmd` or `RustRaptor/run.sh`. Saves and settings stay in the separate `RaptorReplica` user profile directories.
For manual installation, download one platform ZIP plus every `RustRaptor-data-NN-of-07.zip` from this release. Compare with `SHA256SUMS`, then extract all eight archives into the same parent directory. Every ZIP starts with `RustRaptor/`. Use the Windows helper for reliable long-path extraction of retained artwork-review files.
Allow roughly 13 GB free for the installed folder and one temporary download. The complete shared data contains 32,373 files / 10,376,039,515 bytes. Original assets, accepted HD artwork and bundled mod graphics, sound and music are included. No compiler or old C++ engine is required.
Broader playtesting remains useful; the included validation covers its documented scenarios and is not a new full playthrough of every mission.
+1 -1
View File
@@ -3,7 +3,7 @@
"platform": "Linux x86_64", "platform": "Linux x86_64",
"catalog_entries": 6449, "catalog_entries": 6449,
"approved_hd_frames": 5743, "approved_hd_frames": 5743,
"binary_sha256": "e9cbc9f987bb60e9533a8e2d7ab216f6ecccc0848249354ded22abd513a3f255", "binary_sha256": "14d85a8377078cf6abba44d72bc4eb8ef2760c6a3c810dd51e2d55d4ff91a5af",
"data_files": 32373, "data_files": 32373,
"data_bytes": 10376039515, "data_bytes": 10376039515,
"files": { "files": {
+1 -1
View File
@@ -37,7 +37,7 @@
] ]
}, },
"files": { "files": {
"bin/raptor-rust.exe": "a5a82c7942ac07e11ef00219218c3f7eadfa9eff51ee22c44d16f7f54ebaec53", "bin/raptor-rust.exe": "187a927767c7042c6c54f0307ac30c08504b1bd46c3b742e34617afa798d93f0",
"bin/SDL2.dll": "53e8fa7e9ed43c30bd650c39396f0d9d2067d2fff0b926c9433b75202592e4aa", "bin/SDL2.dll": "53e8fa7e9ed43c30bd650c39396f0d9d2067d2fff0b926c9433b75202592e4aa",
"bin/SDL2-WINDOWS-LICENSE.txt": "97f35b302b361680ec1e891e95d2d52097bb95abff361434916d99dc1305f127", "bin/SDL2-WINDOWS-LICENSE.txt": "97f35b302b361680ec1e891e95d2d52097bb95abff361434916d99dc1305f127",
"run.cmd": "b007382d9ed3560191856514a9ebd4e429801fc25428840e304bb297f3553269" "run.cmd": "b007382d9ed3560191856514a9ebd4e429801fc25428840e304bb297f3553269"
+152
View File
@@ -0,0 +1,152 @@
"""Synthetic guards for this one fixed preview upload; no GitHub mutations."""
from copy import deepcopy
from pathlib import Path
import json
import tempfile
import unittest
from unittest.mock import patch
import upload_draft_release as upload
class FrozenDraftUploadTests(unittest.TestCase):
def test_release_guard_rejects_any_changed_identity_or_published_state(self):
good = {'id': upload.RELEASE_ID, 'tag_name': upload.TAG,
'target_commitish': upload.COMMIT, 'draft': True, 'prerelease': True}
self.assertEqual(upload.checked_release(good), good)
for field, value in (('id', 1), ('tag_name', 'wrong'), ('target_commitish', 'main'), ('draft', False), ('prerelease', False)):
bad = dict(good)
bad[field] = value
with self.subTest(field=field), self.assertRaises(ValueError):
upload.checked_release(bad)
def test_direct_and_annotated_tag_must_peel_to_exact_commit(self):
direct = {'ref': 'refs/tags/' + upload.TAG, 'object': {'type': 'commit', 'sha': upload.COMMIT}}
upload.checked_tag(direct)
reference = {'ref': direct['ref'], 'object': {'type': 'tag', 'sha': 'a' * 40}}
annotation = {'sha': 'a' * 40, 'tag': upload.TAG, 'object': direct['object']}
upload.checked_tag(reference, annotation)
for bad in (None, dict(annotation, sha='b' * 40), dict(annotation, object={'type': 'commit', 'sha': 'b' * 40})):
with self.assertRaises(ValueError):
upload.checked_tag(reference, bad)
def test_remote_asset_requires_exact_hash_size_name_and_uploaded_state(self):
good = {'name': 'one.zip', 'size': 42, 'state': 'uploaded', 'digest': 'sha256:' + 'a' * 64}
upload.checked_remote(good, 'one.zip', 42, 'a' * 64)
for field, value in (('name', 'two.zip'), ('size', 41), ('state', 'starter'), ('digest', None)):
with self.subTest(field=field), self.assertRaises(ValueError):
upload.checked_remote(dict(good, **{field: value}), 'one.zip', 42, 'a' * 64)
def test_receipt_must_bind_same_plan_checkpoint_kind_and_every_payload(self):
plan = {'snapshot': {'commit': upload.COMMIT}}
asset = {'name': 'one.zip', 'kind': 'shared-data', 'files': [{'name': 'data/pic', 'sha256': 'b' * 64}]}
good = {'status': 'ZIP_CRC_AND_COMPLETE_PAYLOAD_SHA256_PASS', 'asset': 'one.zip', 'kind': 'shared-data',
'checkpoint': plan['snapshot'], 'plan_sha256': 'c' * 64, 'payload_files': 1,
'payload_sha256': {'data/pic': 'b' * 64}, 'bytes': 42, 'sha256': 'd' * 64}
upload.checked_receipt(plan, 'c' * 64, asset, good)
for field, value in (('checkpoint', {}), ('plan_sha256', 'a' * 64), ('kind', 'linux'),
('payload_files', 0), ('payload_sha256', {}), ('bytes', 2 * 1024**3), ('sha256', 'x')):
with self.subTest(field=field), self.assertRaises(ValueError):
upload.checked_receipt(plan, 'c' * 64, asset, dict(good, **{field: value}))
def test_remote_members_rejects_unplanned_missing_or_duplicate_assets(self):
controller = object.__new__(upload.DraftExporter)
controller.verified = {'one.zip': {'bytes': 42, 'sha256': 'a' * 64}}
asset = {'name': 'one.zip', 'size': 42, 'state': 'uploaded', 'digest': 'sha256:' + 'a' * 64}
controller.remote_members({'assets': [asset]})
for assets in ([], [asset, asset], [dict(asset, name='extra.zip')]):
with self.assertRaises(ValueError):
controller.remote_members({'assets': assets})
def test_upload_failure_or_remote_digest_mismatch_never_deletes_local_zip(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
package = root / 'frozen'
package.mkdir()
controller = upload.DraftExporter(package, root / 'state', root / 'single')
archive = root / 'single/one.zip'
archive.write_bytes(b'complete ZIP fixture')
expected = upload.digest(archive)
before = {'assets': []}
wrong = {'id': 123, 'name': 'one.zip', 'size': archive.stat().st_size,
'state': 'uploaded', 'digest': 'sha256:' + '0' * 64}
with patch.object(controller, 'snapshot'), patch.object(controller, 'release', return_value=before), \
patch.object(controller, 'log'), patch.object(upload.subprocess, 'run') as run:
run.return_value.returncode = 0
run.return_value.stdout = json.dumps(wrong)
run.return_value.stderr = ''
with self.assertRaises(ValueError):
controller.upload(archive, 'one.zip', expected, archive.stat().st_size)
self.assertTrue(archive.exists())
self.assertEqual(controller.verified, {})
run.return_value.returncode = 1
with self.assertRaises(RuntimeError):
controller.upload(archive, 'one.zip', expected, archive.stat().st_size)
self.assertTrue(archive.exists())
def test_output_directories_must_be_new_and_outside_frozen_tree(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
package = root / 'frozen'
package.mkdir()
with self.assertRaises(ValueError):
upload.DraftExporter(package, package / 'state', root / 'single')
state = root / 'already-existing-state'
state.mkdir()
with self.assertRaises(FileExistsError):
upload.DraftExporter(package, state, root / 'single')
class ConfigurableReleaseTargetTests(unittest.TestCase):
def test_stable_and_preview_guards_require_the_explicit_expected_state(self):
stable = upload.ReleaseTarget('c' * 40, 'v1.0.0', 12345, False)
release = {'id': 12345, 'tag_name': 'v1.0.0', 'target_commitish': 'c' * 40,
'draft': True, 'prerelease': False}
upload.checked_release(release, stable)
with self.assertRaises(ValueError):
upload.checked_release(dict(release, prerelease=True), stable)
with self.assertRaises(ValueError):
upload.checked_release(release) # Legacy preview defaults cannot match stable.
reference = {'ref': 'refs/tags/v1.0.0', 'object': {'type': 'commit', 'sha': 'c' * 40}}
upload.checked_tag(reference, target=stable)
with self.assertRaises(ValueError):
upload.checked_tag(dict(reference, object={'type': 'commit', 'sha': upload.COMMIT}), target=stable)
def test_invalid_exact_release_inputs_fail_before_any_directory_or_upload(self):
for values in (("main", 'v1.0.0', 123, False), ('A' * 40, 'v1.0.0', 123, False),
('c' * 40, '../v1', 123, False), ('c' * 40, 'v1/other', 123, False),
('c' * 40, 'v1.0.0', 0, False), ('c' * 40, 'v1.0.0', True, False),
('c' * 40, 'v1.0.0', 123, 'false')):
with self.subTest(values=values), self.assertRaises(ValueError):
upload.ReleaseTarget(*values)
def test_explicit_release_routes_api_and_upload_to_its_exact_id_and_tag(self):
target = upload.ReleaseTarget('c' * 40, 'v1.0.0', 12345, False)
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
package = root / 'frozen'
package.mkdir()
controller = upload.DraftExporter(package, root / 'state', root / 'single', target)
reference = {'ref': 'refs/tags/v1.0.0', 'object': {'type': 'commit', 'sha': 'c' * 40}}
release = {'id': 12345, 'tag_name': 'v1.0.0', 'target_commitish': 'c' * 40,
'draft': True, 'prerelease': False, 'assets': []}
with patch.object(controller, 'api', side_effect=[reference, release]) as api:
self.assertEqual(controller.release(), release)
self.assertEqual(api.call_args_list[0].args[0], 'repos/Quintine/RustRaptor/git/ref/tags/v1.0.0')
self.assertEqual(api.call_args_list[1].args[0], 'repos/Quintine/RustRaptor/releases/12345')
archive = root / 'single/one.zip'
archive.write_bytes(b'fixture')
wrong = {'name': 'one.zip', 'size': archive.stat().st_size, 'state': 'uploaded', 'digest': 'sha256:' + '0' * 64}
with patch.object(controller, 'snapshot'), patch.object(controller, 'release', return_value=release), \
patch.object(controller, 'log'), patch.object(upload.subprocess, 'run') as run:
run.return_value.returncode = 0
run.return_value.stdout = json.dumps(wrong)
run.return_value.stderr = ''
with self.assertRaises(ValueError):
controller.upload(archive, 'one.zip', upload.digest(archive), archive.stat().st_size)
self.assertIn('/releases/12345/assets?name=one.zip', run.call_args.args[0][4])
self.assertTrue(archive.exists())
if __name__ == '__main__':
unittest.main()
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Export an exact frozen release to its existing empty draft; never publish it."""
import argparse
import hashlib
import json
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
from dataclasses import dataclass
from urllib.parse import quote
REPO = 'Quintine/RustRaptor'
TAG = 'v0.1.0-preview.20261020'
COMMIT = '7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555'
RELEASE_ID = 418286902
FLOOR = 1_800_000_000
@dataclass(frozen=True)
class ReleaseTarget:
commit: str = COMMIT
tag: str = TAG
release_id: int = RELEASE_ID
prerelease: bool = True
def __post_init__(self):
if not isinstance(self.commit, str) or not re.fullmatch('[0-9a-f]{40}', self.commit):
raise ValueError('release commit must be an exact lowercase 40-character SHA')
if not isinstance(self.tag, str) or not re.fullmatch(r'v[0-9A-Za-z._-]+', self.tag):
raise ValueError('release tag must be a safe exact version tag')
if type(self.release_id) is not int or self.release_id <= 0:
raise ValueError('release ID must be a positive integer')
if type(self.prerelease) is not bool:
raise ValueError('prerelease expectation must be a boolean')
PREVIEW_TARGET = ReleaseTarget()
def digest(path):
with Path(path).open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def write_json(path, value):
with Path(path).open('x', encoding='utf-8') as stream:
json.dump(value, stream, indent=2)
stream.write('\n')
def checked_release(release, target=PREVIEW_TARGET):
if (release.get('id') != target.release_id or release.get('tag_name') != target.tag
or release.get('target_commitish') != target.commit or release.get('draft') is not True
or release.get('prerelease') is not target.prerelease):
raise ValueError('existing release ID/tag/target/draft state differs')
return release
def checked_tag(reference, annotated=None, target=PREVIEW_TARGET):
if reference.get('ref') != 'refs/tags/' + target.tag:
raise ValueError('wrong frozen tag reference')
obj = reference.get('object', {})
if obj.get('type') == 'tag':
if (not isinstance(annotated, dict) or annotated.get('sha') != obj.get('sha')
or annotated.get('tag') != target.tag):
raise ValueError('annotated tag identity differs')
obj = annotated.get('object', {})
if obj.get('type') != 'commit' or obj.get('sha') != target.commit:
raise ValueError('frozen tag no longer points to the tested commit')
def checked_remote(remote, name, size, sha256):
if (remote.get('name') != name or remote.get('size') != size
or remote.get('state') != 'uploaded' or remote.get('digest') != 'sha256:' + sha256):
raise ValueError('remote asset name/size/state/SHA-256 differs; retain local export')
return remote
def checked_receipt(plan, plan_sha, asset, receipt):
if (receipt.get('status') != 'ZIP_CRC_AND_COMPLETE_PAYLOAD_SHA256_PASS'
or receipt.get('asset') != asset['name'] or receipt.get('kind') != asset['kind']
or receipt.get('checkpoint') != plan['snapshot'] or receipt.get('plan_sha256') != plan_sha
or receipt.get('payload_files') != len(asset['files'])
or receipt.get('payload_sha256') != {r['name']: r['sha256'] for r in asset['files']}
or not re.fullmatch('[0-9a-f]{64}', receipt.get('sha256', ''))
or not 0 < receipt.get('bytes', 0) < 2 * 1024**3):
raise ValueError('local receipt differs from the one frozen runner plan')
class DraftExporter:
def __init__(self, package, state, temporary, target=PREVIEW_TARGET):
self.target = target
self.package = package.resolve(strict=True)
self.state = state.absolute()
self.temporary = temporary.absolute()
if any(directory.resolve() != directory for directory in (self.state, self.temporary)):
raise ValueError('state and temporary paths must be literal paths without symlink ancestors')
for directory in (self.state, self.temporary):
if directory.is_relative_to(self.package):
raise ValueError('release outputs must remain outside the frozen source tree')
directory.mkdir(parents=True, exist_ok=False)
self.receipts = self.state / 'receipts'
self.remote = self.state / 'remote-receipts'
self.receipts.mkdir()
self.remote.mkdir()
self.plan_path = self.state / 'release-plan.json'
self.exporter = self.package / 'tools/export_release.py'
self.verified = {}
def log(self, event, **details):
row = {'time_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'event': event, **details}
print(json.dumps(row), flush=True)
with (self.state / 'progress.jsonl').open('a') as stream:
stream.write(json.dumps(row) + '\n')
def api(self, endpoint):
return json.loads(subprocess.check_output(['gh', 'api', endpoint], cwd=self.package))
def release(self):
reference = self.api(f'repos/{REPO}/git/ref/tags/{self.target.tag}')
obj = reference.get('object', {})
annotated = None
if obj.get('type') == 'tag':
sha = obj.get('sha', '')
if not re.fullmatch('[0-9a-f]{40}', sha):
raise ValueError('unsafe annotated tag SHA')
annotated = self.api(f'repos/{REPO}/git/tags/{sha}')
checked_tag(reference, annotated, self.target)
return checked_release(self.api(f'repos/{REPO}/releases/{self.target.release_id}'), self.target)
def snapshot(self):
actual = {'commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=self.package, text=True).strip(),
'runtime_manifest_sha256': digest(self.package / 'runtime-manifest.json'),
'windows_manifest_sha256': digest(self.package / 'runtime-windows-manifest.json')}
if actual['commit'] != self.target.commit or subprocess.check_output(
['git', 'status', '--porcelain', '--untracked-files=all'], cwd=self.package):
raise ValueError('frozen source commit or cleanliness differs')
if hasattr(self, 'plan') and actual != self.plan['snapshot']:
raise ValueError('frozen runtime manifests changed')
if shutil.disk_usage(self.package).free < FLOOR:
raise ValueError('runner workspace crossed the 1.8GB free-space floor')
def remote_members(self, release):
assets = release.get('assets', [])
names = [asset.get('name') for asset in assets]
if len(names) != len(set(names)) or set(names) != set(self.verified):
raise ValueError('unexpected, missing or duplicate remote assets; no clobber allowed')
for asset in assets:
receipt = self.verified[asset['name']]
checked_remote(asset, asset['name'], receipt['bytes'], receipt['sha256'])
return assets
def upload(self, path, name, expected_sha, expected_size):
self.snapshot()
self.remote_members(self.release())
if (path.is_symlink() or not path.is_file() or path.stat().st_size != expected_size
or digest(path) != expected_sha):
raise ValueError('local upload no longer matches authenticated receipt')
self.log('uploading', asset=name, bytes=expected_size, sha256=expected_sha)
result = subprocess.run(['gh', 'api', '--method', 'POST', f'https://uploads.github.com/repos/{REPO}/releases/{self.target.release_id}/assets?name=' + quote(name, safe=''),
'--input', str(path), '-H', 'Content-Type: application/octet-stream'],
cwd=self.package, capture_output=True, text=True)
(self.state / (name + '.upload.stdout')).write_text(result.stdout)
(self.state / (name + '.upload.stderr')).write_text(result.stderr)
if result.returncode:
raise RuntimeError('upload returned nonzero; retain local file and inspect remote draft before retry')
uploaded = checked_remote(json.loads(result.stdout), name, expected_size, expected_sha)
release = self.release()
matches = [asset for asset in release['assets'] if asset.get('name') == name]
if len(matches) != 1 or matches[0].get('id') != uploaded.get('id'):
raise ValueError('uploaded asset is not uniquely attached to the guarded draft')
checked_remote(matches[0], name, expected_size, expected_sha)
self.verified[name] = {'bytes': expected_size, 'sha256': expected_sha}
self.remote_members(release)
write_json(self.remote / (name + '.json'), matches[0])
self.log('remote_sha256_verified', asset=name, bytes=expected_size, sha256=expected_sha)
def run(self):
self.snapshot()
initial = self.release()
if initial.get('assets'):
raise ValueError('draft must be completely empty before this one-pass export')
write_json(self.state / 'initial-empty-draft.json', initial)
subprocess.run([sys.executable, '-B', str(self.exporter), 'plan', '--package', str(self.package),
'--expected-commit', self.target.commit, '--output', str(self.plan_path)], check=True, cwd=self.package)
self.plan = json.loads(self.plan_path.read_text())
plan_sha = digest(self.plan_path)
if (len(self.plan['assets']) != 9 or self.plan['snapshot']['commit'] != self.target.commit
or sorted(asset['kind'] for asset in self.plan['assets']) != ['linux'] + ['shared-data'] * 7 + ['windows']):
raise ValueError('frozen release no longer has exactly the expected nine ZIPs')
for asset in self.plan['assets']:
self.snapshot()
self.remote_members(self.release())
name = asset['name']
path = self.temporary / name
receipt_path = self.receipts / (name + '.json')
self.log('building', asset=name, uncompressed_bytes=asset['uncompressed_bytes'])
subprocess.run([sys.executable, '-B', str(self.exporter), 'archive', '--plan', str(self.plan_path),
'--asset', name, '--output', str(path), '--receipt', str(receipt_path)],
check=True, cwd=self.package)
receipt = json.loads(receipt_path.read_text())
checked_receipt(self.plan, plan_sha, asset, receipt)
self.upload(path, name, receipt['sha256'], receipt['bytes'])
path.unlink() # Only this just-authenticated disposable archive, after remote proof.
self.log('temporary_export_removed', asset=name)
sums = self.state / 'SHA256SUMS'
manifest = self.state / 'release-manifest.json'
subprocess.run([sys.executable, '-B', str(self.exporter), 'checksums', '--plan', str(self.plan_path),
'--receipts', str(self.receipts), '--output', str(sums),
'--manifest-output', str(manifest), '--tag', self.target.tag], check=True, cwd=self.package)
for path in (sums, manifest, self.package / 'release/Install-Linux.sh', self.package / 'release/Install-Windows.ps1'):
self.upload(path, path.name, digest(path), path.stat().st_size)
final = self.release()
self.remote_members(final)
if len(final['assets']) != 13:
raise ValueError('complete release must contain nine ZIPs plus four companion assets')
write_json(self.state / 'final-13-assets-draft-still-unpublished.json', final)
self.log('COMPLETE_THIRTEEN_REMOTE_ASSETS_VERIFIED_DRAFT_NOT_PUBLISHED', checkpoint=self.target.commit)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--package', type=Path, required=True)
parser.add_argument('--state', type=Path, required=True)
parser.add_argument('--temporary', type=Path, required=True)
parser.add_argument('--commit', default=COMMIT)
parser.add_argument('--tag', default=TAG)
parser.add_argument('--release-id', type=int, default=RELEASE_ID)
parser.add_argument('--prerelease', choices=('true', 'false'), default='true')
args = parser.parse_args()
target = ReleaseTarget(args.commit, args.tag, args.release_id, args.prerelease == 'true')
DraftExporter(args.package, args.state, args.temporary, target).run()
if __name__ == '__main__':
main()