Keep manifest-required derived artwork in portable game archives

This commit is contained in:
raptorreplica committed 2026-10-18 12:16:25 +11:00
1 parent 00304e25c1
commit 6b1a2629a0
5 files changed
+103 -4

No files matched your search

+3
View File
@@ -18,6 +18,7 @@ on:
- "runtime-windows-manifest.json"
- "tools/assemble_windows.py"
- "tools/assemble_runtime.py"
- "tools/test_runtime_archives.py"
- ".github/workflows/windows.yml"
permissions:
@@ -71,6 +72,8 @@ jobs:
$env:SDL_AUDIODRIVER = "dummy"
& "$package\run.cmd" --profile-root "$env:RUNNER_TEMP\bundled-profile" --smoke-frames 32 --preview-mission base:sector_1_wave_1
if ($LASTEXITCODE -ne 0) { throw "Included Windows SDL gameplay failed" }
- name: Test runtime archive payload selection
run: python -m unittest discover -s tools -p test_runtime_archives.py
- name: Build and package Windows desktop
run: python tools/assemble_windows.py --target x86_64-pc-windows-msvc --sdl2-dir "$env:RUNNER_TEMP\sdl2\SDL2-2.32.10" --zip dist/RustRaptor-windows-x86_64.zip
- name: Check displayed boss phases and SD HD shop artwork
+2
View File
@@ -14,6 +14,8 @@ SourceF, both binaries, Mission 2 version 1.0.3 and the nine palette opt-ins rem
The assembled data contains **9,504 files / 8,407,923,443 bytes**. The 307 native AGX cinematic/font records remain preserved separately. Iron Coast retains its published retune, Mission 2 version 1.0.3 and 65-test campaign evidence; see [Iron Coast retune evidence](iron-coast-retune.md). This artwork append changes no mission, weapon or shop behavior.
Windows package follow-up: the exact H5500 run at `00304e25` passed 401 tests (26 ignored, zero failed), then rejected the 15 new picture routes because the ZIP filter skipped directories named `derived`. Mod, PCM and Stage II checks after that HD guard did not run. Both ZIP builders now require every runtime-manifest data file to survive archive selection and fail if any required route is missing. A new synthetic ZIP/relocation check covers the failure; a fresh native Windows result remains pending. [Failed native run](https://github.com/Quintine/RustRaptor/actions/runs/39083332442).
# Earlier 5,339-frame checkpoint — retained historical observation
# HD continuation checkpoint at 5,339 frames — 17 October 2026
+14 -3
View File
@@ -28,9 +28,16 @@ def checked_file(root, relative):
return resolved
def iter_archive_files(package, archive, skipped, excluded_files=()):
"""Yield the same sorted archive files without walking excluded subtrees."""
def iter_archive_files(package, archive, skipped, excluded_files=(), required_files=()):
"""Keep manifest payloads even when a parent has a build-cache name."""
required = {Path(name) for name in required_files}
required_directories = {parent for path in required for parent in path.parents}
for relative in required:
checked_file(package, relative)
def excluded(relative):
if relative in required or relative in required_directories:
return False
return (skipped.intersection(relative.parts)
or any(part.startswith(".runtime-data-") for part in relative.parts)
or (len(relative.parts) >= 4 and relative.parts[0] == "mods"
@@ -47,6 +54,9 @@ def iter_archive_files(package, archive, skipped, excluded_files=()):
if (not excluded(relative) and relative not in excluded_files and path.is_file()
and path.resolve() != archive):
files.append(path)
missing = required - {path.relative_to(package) for path in files}
if missing:
raise ValueError(f"runtime archive is missing required files: {sorted(map(str, missing))}")
yield from sorted(files)
@@ -165,7 +175,8 @@ def main():
archive.parent.mkdir(parents=True, exist_ok=True)
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git"}
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
for path in iter_archive_files(package, archive, skipped):
for path in iter_archive_files(package, archive, skipped,
required_files=report["files"]):
relative = path.relative_to(package)
output.write(path, str(Path("RustRaptor") / relative))
print(json.dumps({key: value for key, value in report.items() if key not in {"files", "system_dependencies"}}, indent=2))
+2 -1
View File
@@ -174,7 +174,8 @@ def main():
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git", "lib"}
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
for path in iter_archive_files(package, archive, skipped,
excluded_files=(Path("bin/raptor-rust"),)):
excluded_files=(Path("bin/raptor-rust"),),
required_files=data_manifest["files"]):
relative = path.relative_to(package)
output.write(path, str(Path("RustRaptor") / relative))
print(json.dumps(report, indent=2))
+82
View File
@@ -0,0 +1,82 @@
"""Small archive fixtures; no game build or production data copy required."""
import hashlib
from pathlib import Path
import tempfile
import unittest
import zipfile
from assemble_runtime import iter_archive_files
class RuntimeArchiveTests(unittest.TestCase):
def test_manifest_derived_payload_survives_cache_filter_and_relocation(self):
with tempfile.TemporaryDirectory() as directory:
package = Path(directory) / "package"
payload = "data/hd/jobs/reviewed-family/derived/200a2-derived.png"
required = ["data/INDEX", "data/hd/hd.json", payload]
fixtures = {
"data/INDEX": b"catalog",
"data/hd/hd.json": b"manifest",
payload: b"accepted derived PNG bytes",
"data/hd/jobs/reviewed-family/derived/rejected-preview.png": b"not admitted",
"derived/preview.png": b"private cache",
"target/build-cache": b"build",
".reference/private": b"private",
"mods/demo/art/source/parent.png": b"authoring reference",
"mods/demo/art/runtime.png": b"mod runtime",
"bin/raptor-rust": b"Linux only",
"bin/raptor-rust.exe": b"Windows",
}
for name, data in fixtures.items():
path = package / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
archive = Path(directory) / "windows.zip"
skipped = {"target", "dist", ".reference", "derived", "lib"}
selected = list(iter_archive_files(
package, archive, skipped, excluded_files=(Path("bin/raptor-rust"),),
required_files=required))
names = {path.relative_to(package).as_posix() for path in selected}
self.assertTrue(set(required) <= names)
self.assertEqual(names - set(required), {"mods/demo/art/runtime.png", "bin/raptor-rust.exe"})
with zipfile.ZipFile(archive, "w") as output:
for path in selected:
output.write(path, "RustRaptor/" + path.relative_to(package).as_posix())
relocated = Path(directory) / "Portable Windows Ω folder"
with zipfile.ZipFile(archive) as packed:
packed.extractall(relocated)
for name in required:
original = package / name
copied = relocated / "RustRaptor" / name
self.assertEqual(hashlib.sha256(copied.read_bytes()).digest(),
hashlib.sha256(original.read_bytes()).digest())
def test_missing_manifest_payload_fails_before_yielding_files(self):
with tempfile.TemporaryDirectory() as directory:
package = Path(directory)
with self.assertRaises(FileNotFoundError):
next(iter_archive_files(package, package / "out.zip", {"derived"},
required_files=["data/hd/missing.png"]))
def test_explicit_exclusion_cannot_silently_drop_manifest_payload(self):
with tempfile.TemporaryDirectory() as directory:
package = Path(directory)
path = package / "data/required"
path.parent.mkdir()
path.write_bytes(b"required")
with self.assertRaisesRegex(ValueError, "missing required files"):
list(iter_archive_files(package, package / "out.zip", set(),
excluded_files=(Path("data/required"),),
required_files=["data/required"]))
def test_manifest_path_cannot_escape_package(self):
with tempfile.TemporaryDirectory() as directory:
package = Path(directory)
for name in ("../outside", str(package / "absolute")):
with self.subTest(name=name), self.assertRaises(ValueError):
list(iter_archive_files(package, package / "out.zip", set(),
required_files=[name]))
if __name__ == "__main__":
unittest.main()