Compare commits

10 Commits

Author SHA1 Message Date
gamer147
71ec6e55ed Make Gitea core validation source-only 2026-08-03 13:28:59 -04:00
gamer147
4db2f416cf Record verified binary history purge 2026-08-03 13:12:19 -04:00
gamer147
a4f029c0f8 Remove unverified binary tools from tracking 2026-08-03 13:06:45 -04:00
gamer147
17f31dc728 Add Linux core validation workflow 2026-08-03 12:28:46 -04:00
gamer147
5f6e647114 Normalize tracked text for Linux CI 2026-08-03 12:07:13 -04:00
gamer147
21c1317b05 Add repository toolchain policy 2026-08-03 11:54:18 -04:00
gamer147
067a11440e Document repository policy audit 2026-08-03 11:45:08 -04:00
gamer147
9e59e98efa Move corpus gate logic to frontend 2026-08-03 11:37:06 -04:00
gamer147
728399cb8b Move FFmpeg runtime to frontend 2026-08-03 11:33:38 -04:00
gamer147
81d063da37 Move movie runtime support to frontend 2026-08-03 11:29:47 -04:00
63 changed files with 8090 additions and 7676 deletions

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{c,cpp,cs,h,py,ps1,sh}]
indent_style = space
indent_size = 4
[*.{csproj,json,jsonl,props,targets,toml,tscn,tres,xml,yaml,yml}]
indent_style = space
indent_size = 2
[*.{bat,cmd}]
end_of_line = crlf

69
.gitattributes vendored
View File

@@ -1,5 +1,72 @@
* text=auto
.editorconfig text eol=lf
.gitattributes text eol=lf
.gdignore text eol=lf
.gitignore text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.c text eol=lf
*.cfg text eol=lf
*.cs text eol=lf
*.csproj text eol=lf
*.config text eol=lf
*.cpp text eol=lf
*.gd text eol=lf
*.godot text eol=lf
*.h text eol=lf
*.json text eol=lf
*.jsonl text eol=lf
*.md text eol=lf
*.props text eol=lf
*.targets text eol=lf
*.ps1 text eol=lf
*.py text eol=lf
*.sh text eol=lf
*.sln text eol=lf
*.toml text eol=lf
*.tscn text eol=lf
*.tres text eol=lf
*.uid text eol=lf
*.xml text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.7z binary
*.a binary
*.agf binary
*.avi binary
*.bin binary
*.bmp binary
*.cur binary
*.dll binary
*.dylib binary
*.exe binary
*.gif binary
*.ico binary
*.jpeg binary
*.jpg binary
*.lib binary
*.mp3 binary
*.mp4 binary
*.mpeg binary
*.mpg binary
*.ogg binary
*.otf binary
*.pdb binary
*.png binary
*.rar binary
*.so binary
*.ttf binary
*.wav binary
*.woff binary
*.woff2 binary
*.zip binary
# Generated references are ordinary canonical-LF text. Their whitespace
# exceptions preserve generator-owned formatting without disabling EOL policy.
docs/opcode-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
docs/global-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
docs/engine-ctx-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
tools/age_opcodes_himegari.py whitespace=trailing-space,space-before-tab,cr-at-eol
*.sh text eol=lf

View File

@@ -0,0 +1,61 @@
name: Core validation
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: core-${{ gitea.workflow }}-${{ gitea.ref }}
cancel-in-progress: true
jobs:
core:
name: Linux core gate
runs-on: ubuntu-latest
timeout-minutes: 15
env:
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_NOLOGO: "true"
NUGET_XMLDOC_MODE: skip
PYTHONUTF8: "1"
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v6
with:
python-version: "3.11"
architecture: x64
- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Report toolchain
run: |
python --version
dotnet --version
- name: Run asset-independent validation
run: python -X utf8 tools/validate.py --level core
- name: Upload failure logs
if: failure()
uses: christopherhx/gitea-upload-artifact@v4
with:
name: core-validation-${{ gitea.run_id }}-${{ gitea.run_attempt }}
path: build/validation/**
if-no-files-found: ignore
retention-days: 7

3
bin/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!README.md

15
bin/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Local third-party tools
This directory is reserved for optional, machine-local binaries. Its contents are ignored by Git;
only this README and `.gitignore` are repository content.
The installed-data archive parity test can use `BinExtractALF.exe` here together with its matching
`LzssCpp.dll`. These files are not required by the runtime or the asset-independent test suite, and
their provenance and redistribution terms have not been verified. Do not commit them. The historical
local copies audited on 2026-08-03 had these SHA-256 values:
- `BinExtractALF.exe`: `46167CDF3DA1F02733CE1E7F0DA99D155E934EDE0987CA79C8927F48FEB25FE2`
- `LzssCpp.dll`: `596B9DDB07CA5C29CEE609E846F1512654EDEC56310A90D6C2A6951BE1E39FE1`
`pe-sieve32.exe` is no longer used. The failed IAT reconstruction experiment is retained in
`docs/engine-re.md`; the Frida import-map workflow replaced it.

View File

@@ -22,13 +22,17 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ └── *.exe (uninstallers), SAS0099.OGG … other shipped files.
├── extracted/ ← DERIVED (game-side) — extracted ALF contents,
│ │ ~3.9 GB, regenerable via age-reimpl/bin/BinExtractALF.
│ │ ~3.9 GB, regenerable with a local archive extractor.
│ └── DATA1/ … DATA5/ DATA1 = 481 .BIN scripts (the corpus we parse)
│ + AGF/BMP/WAV in the others.
└── age-reimpl/ ← OUR WORK (everything we made lives here)
├── README.md navigation-only repository front door; canonical facts stay in docs/
├── global.json pins the validated .NET 8 SDK feature band
├── .editorconfig / .gitattributes UTF-8, indentation, text/EOL, and binary-file policy;
│ tracked source and generated references use canonical LF
├── .gitea/workflows/core-validation.yml read-only Ubuntu source-only core gate for the private Gitea remote
├── run-godot.ps1 / run-godot.cmd tracked development launcher + Windows wrapper;
│ resolves Godot/game-root from parameters, environment,
│ PATH, and the conventional sibling install
@@ -56,10 +60,9 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ ├── movie-corpus-gate/ C# full-corpus FFmpeg decode/lifecycle acceptance tool
│ └── probe_*.py format reverse-engineering probes (historical)
├── bin/ 3rd-party binaries we use (not ours, not the game's)
│ ├── BinExtractALF.exe ALF archive extractor → produces extracted/
── LzssCpp.dll its LZSS codec dependency
│ └── pe-sieve32.exe (hollowsprocess) unpacked-PE dumper — engine-RE escalation (see docs/engine-re.md)
├── bin/ ignored home for optional machine-local tools
│ ├── README.md / .gitignore tracked policy; binaries never enter Git
── BinExtractALF.exe + LzssCpp.dll optional local archive oracle (unverified provenance)
├── vm-map/ VM / reverse-engineering reference artifacts
│ ├── opcodes.toml ★ CANONICAL opcode reference (hand-edited: ABI + semantics
@@ -118,9 +121,9 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ ├── Age.Engine/Persistence/ native S3SD/S4SD + S3RT codecs, shared payload/ReadTextDB,
│ layout-3 numbered state/history/gfx, DAT/STH pair + BMP codec,
│ and profile-owned state
│ ├── Age.Engine.Frontend/ platform-neutral frontend diagnostics/options project: page
│ locator, timeline/trace/step-limit diagnostics, VM/window launch
options, and performance-frame logging
│ ├── Age.Engine.Frontend/ platform-neutral frontend support: diagnostics/options, page
│ location, performance logging, movie runtime/FFmpeg decoding,
audio alignment, surface registry, and WAV sanitization
│ └── Age.Engine.Text.Windows/ optional exact Windows GDI gray-4 reference backend; kept
│ outside the platform-neutral VM/runtime core
├── native/ authored native runtime boundaries
@@ -154,8 +157,13 @@ full-width text entry.
Both `engine/AgeEngine.sln` and `godot/Himegari.sln` include the project; Godot and `Age.Engine.Tests` consume it
through project references rather than compiling duplicate linked sources. The first owned cluster contains
`PageLocatorState`, `GodotTimelineLog`, `GodotTraceSink`, `StepLimitDiagnosticFormatter`, `GodotVmOptions`,
`PerformanceFrameLog`, and `WindowLaunchOptions`. Movie runtime/decoder helpers remain under `godot/` until their
separate ownership slices.
`PerformanceFrameLog`, and `WindowLaunchOptions`. It also owns the internal movie decoder boundary/runtime,
audio-timeline alignment, movie-surface registry, and RIFF/WAVE sanitizer; only the `Himegari` and
`Age.Engine.Tests` assemblies receive explicit friend access to that internal API. FFmpeg native interop and the
asynchronous/paced decoder also live here; the `Age.MovieCorpusGate` tool receives friend access to the native
frame-source seam and references this project rather than compiling source links. Reusable movie-corpus discovery,
decode-gate, and report contracts live here as well; `tools/movie-corpus-gate/Program.cs` remains only the
command-line entry point and output writer.
`godot/GodotAdvHost.cs` retains cross-domain host coordination. Its partial-class companion
`godot/GodotAdvHost.AdvText.cs` owns live/retained ADV text, surface glyph rasterization and caching,
@@ -276,7 +284,8 @@ have no repository output tree and write their automatic maps below `user://diag
and under `extracted/DATA1/`, the game-folder copy (patch v1.03) wins. `paths.scripts()` resolves
this automatically (overrides win).
- **`build/` and `extracted/` are disposable.** `build/` regenerates via `tools/extract_phase2.py`
(or `sys4load.py`); `extracted/` regenerates via `bin/BinExtractALF.exe` on the `.ALF` files.
(or `sys4load.py`); `extracted/` regenerates from the `.ALF` files with a separately obtained local
extractor. `bin/README.md` records the optional local convention; no extractor binary is tracked.
Safe to delete and rebuild; do not hand-edit.
- **Opcode knowledge is edited ONLY in `vm-map/opcodes.toml`** (ABI + semantics + provenance +
`depends_on`). Run `tools/opcodes_build.py --build` to regenerate the shim (`tools/age_opcodes_himegari.py`),

View File

@@ -275,7 +275,7 @@ scene-local numeric addressing mode.
LZSS-or-raw header section, 4/8/truecolor source pixels, LZSS-or-raw pixel section, bottom-up row/stride
conversion, and optional `ACIF` LZSS alpha plane. Port only the algorithm and attribution into
platform-neutral .NET code; do not carry GARbro's WPF/GameRes dependencies. Kelebek's extractor and the
on-disk `BinExtractALF.exe` are validation references; the Kelebek repository exposes no clear license,
separately obtained local `BinExtractALF.exe` is a validation reference; the Kelebek repository exposes no clear license,
so its code should not be copied without clarification. The focused `LzssDecoder` is shared with
`Sys4AssetCatalog`; raw and compressed information/pixel/ACIF sections use the same bounded primitive.
4. **Runtime consumers (packed-id correction complete 2026-07-21).** Script loading, textures, voice,
@@ -461,7 +461,8 @@ VFS-B passes its bounded gates in `Sys4AssetStoreTests`: the installed AAI expan
`0x118` (expanded size at `0x110`, packed size at `0x114`) to one `APPEND01.ALF` archive and 81 80-byte
records. All records carry selector 1 and literal `$1$` names. The full directory has stable SHA-256
`23F0C104A45C099CEFB7D333362716EDE6F20B9EC53E4C3705A8E3A87063708E` over its ordered record fields.
The integration gate runs `BinExtractALF.exe` into a disposable directory, compares all 81 names, validates
The private installed-data integration gate can run a machine-local `bin/BinExtractALF.exe` into a disposable
directory, compares all 81 names, validates
every range and size, and byte-compares all 81 payloads. `Sys4ScriptProvider` resolves a real append script
through `0x01xxxxxx`; direct base-name lookup deliberately does not see append records.

View File

@@ -34,7 +34,7 @@ Related: `docs/scjump-progression.md` (the SCJUMP decoder that hit this wall), `
→ `run_analysis`.
- **Load sanity check (AGF-decoder landmark):** at VA `0x474f23`, `CMP word ptr [ESI + 0x4], 0x4d42`
(the `BM`/BMP-magic check) confirms the image is correctly based + decoded.
- IAT reconstruction — **tried, DOESN'T WORK on this binary (2026-07-09):** `bin/pe-sieve32.exe /pid
- IAT reconstruction — **tried, DOESN'T WORK on this binary (2026-07-09):** a local PE-sieve 0.4.1.1 `/pid
<PID> /imp 3 /dmode 3 /dir build/pe-sieve` (run from **PowerShell**, not Git Bash — it mangles
`/flags`) ran fine but the game is packed with a **zeroed IAT** resolved via `GetProcAddress` at load,
so there is no conventional import table to rebuild. Of 363 "imports" it emitted, only ~17 are genuine
@@ -46,7 +46,8 @@ Related: `docs/scjump-progression.md` (the SCJUMP decoder that hit this wall), `
30×, `in_main:0`, non-terminated). ⇒ **do not graft pe-sieve output** — grafting the noise would inject
wrong import names. The game's hot APIs (`ReadFile`/`CreateFileA`/`timeGetTime`/d3d9 device methods) are
`GetProcAddress`-resolved into private pointer tables, invisible to a static IAT scan. Report/dump left
at `build/pe-sieve/process_<pid>/` (disposable).
at `build/pe-sieve/process_<pid>/` (disposable). The executable was removed from the repository and purged
from reachable history on 2026-08-03; it is obsolete and is not needed to reproduce the accepted workflow.
**→ The Frida import-map approach — ✅ DONE 2026-07-09 (replaced pe-sieve).** Named the
dynamically-resolved APIs at their call sites via the LIVE process. `tools/frida/map_imports.py`

View File

@@ -52,10 +52,10 @@ claiming portable exports.
| ADV font discovery/raster fidelity | `Age.Engine.Text` remains OS-neutral. On compatible ACP-932 Windows, `auto` selects the separate exact-to-request `Age.Engine.Text.Windows` GDI gray-4 backend. Otherwise Godot TextServer supplies Unicode masks and metrics from ordered system-font substitutions in `godot/config/himegari-text-rendering.json`; this policy is explicitly not GDI pixel-exact. Immediate `0x204`/`0x205`, live ADV, retained History, and the wait atlas all enter ordinary RGBA surfaces/retained handles | Three Mincho/Gothic regular/bold samples byte-match an independent Unicode GDI oracle. A live `SC0000@0x869` trace proves that the `AGE Patch.exe`/`jprun.dll` wrapper overrides authored Mincho to MS Gothic: 13 captured masks match Gothic byte-for-byte and Mincho 0/13. This is wrapper behavior, not the true-native default. Forced exact and forced portable self-tests cover immediate, live, and History rasterization, bounded caching, complete-line materialization, retained ordering, alpha/tint/fade, transforms, clipping, capture, HIDEWIN suspension, partial erase/republication, reset, and teardown. Gameplay `Label` pools and overlay projection are gone | Optionally expose realized-face overrides through future launch profiles when wrapper compatibility is desired; independently produce and smoke-test Linux/macOS exports, confirm installed family availability/fallback selection there, and keep the GDI project out of non-Windows deliverables |
| Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules |
| Save/profile/settings storage | `Sys4PersistencePaths` models AGE's independent `SAVEPATH` and `REGFILEPATH` resolutions. Godot replaces Himegari's related profile directory with `user://`, yielding `user://SAVE` for native S3SD/S4SD/S3RT files and thumbnails plus `user://SYS4REG.INI` for the BOM-less CP932 options file; the preserving writer changes only its nine `[sound]` keys | Save payloads and engine options are isolated together without changing either native format. Native/drop-in resolution remains available through `USEAPPDATAFOLDER` plus both SYS4INI paths. A single-root override is rejected when `SAVEPATH` is not beneath `REGFILEPATH`, preventing cross-profile guesses | Expose explicit profile/native selection through the future launcher and allow independent overrides for profiles whose two native paths are unrelated. Validate CP932 availability, replace/flush, case, permissions, and interrupted-write behavior on each export target |
| Game-install, runtime metadata, and repository discovery | `GameRootSelection` accepts `--game-root`, then probes the executable directory and current working directory for `SYS4INI.BIN`; on Unix the frontend prefers inherited shell `PWD` because Godot may change the process directory during project startup. Godot injects the selected root into its catalog and loose-first ALF store. `Himegari.csproj` embeds generated `build/opcodes.json`, and the frontend loads that assembly resource rather than `Paths.OpcodesJson`. `Paths.cs` remains a development/test locator; its page-map use is compiled only for `TOOLS` builds | Installed game data and packaged VM metadata no longer depend on the workspace sibling layout. The Linux export passed an explicit absolute game root outside the repository and read the installed archives. Exported automatic page maps use `user://diagnostics/page-maps`; editor/development runs preserve `build/page-map-<SCENE>.jsonl` | Let the future profile launcher pass `--game-root`, and run executable-directory/CWD plus case/permission smoke gates on Linux and macOS |
| Archive parity oracle | One integration test launches `bin/BinExtractALF.exe` | Windows-only test helper, not a shipped runtime dependency | Skip/replace on non-Windows CI; runtime ALF/AAI readers do not depend on it |
| Native RE tools | Frida/Ghidra helpers target the original `AGE.EXE`; supporting utilities include Windows executables and Windows command conventions | Development/research only | Keep separate from export requirements; document platform prerequisites per tool |
| Python workflow | Operating guide uses Windows `py -3.11` invocation | Developer workflow only | Add equivalent `python3` instructions if non-Windows development becomes active |
| Game-install, runtime metadata, and repository discovery | `GameRootSelection` accepts `--game-root`, then probes the executable directory and current working directory for `SYS4INI.BIN`; on Unix the frontend prefers inherited shell `PWD` because Godot may change the process directory during project startup. Godot injects the selected root into its catalog and loose-first ALF store. `Himegari.csproj` embeds generated `build/opcodes.json`, and the frontend loads that assembly resource rather than `Paths.OpcodesJson`. Development/test `Paths.cs` finds the repository by tracked `global.json`, solution, and opcode-map markers rather than a checkout-directory name | Installed game data and packaged VM metadata no longer depend on the workspace sibling layout. The Linux export passed an explicit absolute game root outside the repository and read the installed archives; a source-only core proof also passes from an arbitrary repository directory name. Exported automatic page maps use `user://diagnostics/page-maps`; editor/development runs preserve `build/page-map-<SCENE>.jsonl` | Let the future profile launcher pass `--game-root`, and run executable-directory/CWD plus case/permission smoke gates on Linux and macOS |
| Archive parity oracle | One integration test can launch a machine-local `bin/BinExtractALF.exe`; the binary and matching DLL are ignored and have unverified redistribution terms | Windows-only installed-data helper, not a shipped runtime dependency | Keep it in an explicit private workspace gate; runtime ALF/AAI readers do not depend on it |
| Native RE tools | Frida/Ghidra helpers target the original `AGE.EXE`; PE-sieve was tried and replaced by the Frida import-map workflow | Development/research only; no native RE executable is tracked | Keep separate from export requirements; document platform prerequisites per tool |
| Python workflow and core CI | The operating guide uses Windows `py -3.11`; `validate.py` launches child tools through `sys.executable` and has POSIX process-group cleanup plus `ps`-based leak auditing | The 590 engine cases are explicitly split into 502 source-only core cases and 88 `Workspace` installed-data/native-oracle cases; both selections pass with zero skips. The opcode-bootstrap unit injects synthetic observations while production bootstrap retains real-corpus scanning. `.gitea/workflows/core-validation.yml` uses the target server's demonstrated `ubuntu-latest`, checkout/setup-dotnet v4, setup-python v6, and Gitea artifact action baseline. A standalone arbitrary-name, corpus-free repository passes the complete core driver | Confirm the first real Linux run after the rewritten history is pushed. Retain Windows-native GDI parity and private installed-game/runtime checks as separate trusted gates |
No authored runtime code currently calls native DirectSound, Direct3D, Vulkan, Metal, or OpenGL. Mentions of
those APIs in `docs/engine-re.md` describe the original AGE implementation. Both the software-oracle display

View File

@@ -834,6 +834,27 @@ do not mix mechanical moves with semantic changes.
test source links without changing public types, namespaces, behavior, or export configuration. Both solution
builds and runtime validation remain green.
The second bounded build-graph slice moved the unchanged internal movie decoder boundary/runtime, audio
alignment policy, movie-surface registry, and RIFF/WAVE sanitizer from `godot/` into `Age.Engine.Frontend`.
Five more direct test source links are gone. Explicit friend access is limited to `Himegari` and
`Age.Engine.Tests`, so the move preserves the intentionally internal API instead of widening it; FFmpeg remains
in Godot while implementing the moved boundary through that friend access. Both solution builds and runtime
validation remain green.
The third bounded build-graph slice moved the unchanged FFmpeg native interop and asynchronous/paced decoder
from `godot/` into `Age.Engine.Frontend`. The two remaining movie test source links and the corpus tool's native
source link were replaced by project references. Friend access now also names only the `Age.MovieCorpusGate`
assembly required by the native frame-source seam; native ABI resolution, decoder pacing, seek/audio behavior,
failure handling, and teardown remain unchanged. Engine, Godot, and corpus-gate builds plus runtime validation
remain green.
The fourth bounded build-graph slice moved the unchanged reusable movie-corpus discovery, decode-gate, and
report implementation from `tools/movie-corpus-gate/` into `Age.Engine.Frontend`. The tool retains only its
command-line entry point and consumes the internal gate through its existing friend/project boundary; tests use
the same production implementation through their project reference. The final cross-project production `.cs`
source link is gone, completing the planned build-graph ownership cleanup. Engine, Godot, and corpus-gate
builds plus runtime validation remain green.
5. **Close repository-policy and storage gaps.** Add an explicit project license, third-party notices and
checksums/provenance for committed binaries, consistent editor/build policy, a pinned .NET SDK, and a
CI entry point built on the same validation driver. Remove the empty workspace-root `.git` directory and
@@ -841,6 +862,104 @@ do not mix mechanical moves with semantic changes.
only after active work is committed. Configure an off-machine remote or equivalent backup before relying
on local history as the sole recovery path.
**Read-only audit (2026-08-03):**
- **License/provenance:** the repository has no project `LICENSE`, `NOTICE`, third-party notice, or artifact
manifest. The three committed binaries are unsigned and have no dedicated artifact records; audit hashes are
`BinExtractALF.exe` 0.8
(`46167cdf3da1f02733ce1e7f0da99d155e934ede0987ca79c8927f48feb25fe2`), metadata-free `LzssCpp.dll`
(`596b9ddb07ca5c29cee609e846f1512654edec56310a90d6c2a6951be1e39fe1`), and PE-sieve 0.4.1.1
(`ee684b34b37af24d1c1e9ca80ceeee6878cc80bb24c0c3793840c9acf905bd59`). PE-sieve's upstream is
[BSD-2-Clause](https://github.com/hasherezade/pe-sieve/blob/master/LICENSE), but that license is not carried
here; the extractor/DLL provenance and redistribution terms remain unverified. The two committed Kelebek
source snapshots and transcribed opcode data come from the
[upstream repository](https://github.com/Kelebek1/Eushully-Decompiler), where no license file was found, so
their redistribution status also remains unresolved. FFmpeg is the good model: Windows/Linux manifests pin
provider, release, URL, version, variant, and SHA-256, while build/export scripts stage its LGPL license.
NuGet/Godot dependencies are versioned in project files but have no consolidated notice inventory.
- **Editor/build/CI policy:** there is no `.editorconfig`, `global.json`, `Directory.Build.*`, package lock,
Python environment manifest, or CI configuration. `.gitattributes` only fixes LF for shell scripts and
whitespace exceptions for generated references. Projects target `net8.0`; the Godot SDK is pinned to 4.7.0
and NuGet versions are explicit. The locally validated tools are .NET SDK 8.0.408 and Python 3.11.4, but only
the Python 3.11 invocation is documented. `tools/validate.py --level core` is already the asset-independent
CI entry point; workspace/runtime/full levels deliberately require the private game corpus and/or Godot.
- **Repository/storage:** `age-reimpl/` is the sole real worktree. The workspace parent contains an empty
`.git` directory, and the repository has an empty `.godot/logs` nest plus an accidental ignored
`godot/godot/build` nest (3.7 MB). All inspected ignored outputs total 6.43 GiB across 12,190 files: 4.38 GB
under `build/`, 1.29 GB under `godot/.godot`, 1.12 GB under historical `godot/build`, and the remainder in
.NET/tool caches. The real `.git` holds 6,100 loose objects (71 MiB), no packs and no reported garbage; defer
compaction until backup and cleanup decisions are complete.
- **Recovery:** `main` is the only ref and has no remote, upstream, additional worktree, or repository/parent
bundle. An unrelated off-machine backup cannot be disproved from repository state, but none is configured or
evidenced here. Do not publish, delete ignored data, remove the empty parent `.git`, or run Git compaction
until the user chooses the corresponding legal, retention, and backup policy.
**Bounded execution order:** (1) add tracked SDK/editor/line-ending policy without reformatting existing files;
(2) add an asset-independent CI workflow that invokes the existing core validation driver; (3) obtain the
user's project-license choice and resolve, replace, or exclude every unverified third-party artifact before
adding the project license/notice/manifest set; (4) configure a user-selected private/off-machine backup or
remote; (5) after explicit retention approval, remove only the verified empty/obsolete ignored nests and any
selected regenerable caches; (6) compact the real repository only after a clean committed state and verified
backup. Public distribution remains blocked on item 3; destructive cleanup and compaction remain blocked on
item 4 plus explicit target approval.
**Policy baseline (2026-08-03):** item 1 is complete. `global.json` pins the validated .NET 8.0.408 feature
band to stable patch releases; `.editorconfig` establishes UTF-8, final-newline, trailing-whitespace, and
indentation defaults; and `.gitattributes` classifies repository text, Windows command wrappers, and binary
assets. Generator-owned references that already commit CRLF are explicit byte-preserving exceptions with
textual diffs, so rebuilding them does not create whole-file normalization churn. No existing source or
project file was reformatted or renormalized. Both solution builds and the asset-independent core validation
gate remain green under the tracked policy. Item 2, a CI workflow calling that same core gate, is the next
safe slice.
**Linux CI preparation (2026-08-03):** the eight legacy text blobs that prevented a clean Linux checkout or
generated-reference comparison are now deliberately normalized to canonical LF: the engine solution, three
engine project files, three generated Markdown references, and the generated Himegari opcode module. The
generated files are once again ordinary text under the repository EOL policy; their existing whitespace
exceptions do not disable normalization. Regeneration through all three owning tools is clean, Windows core
validation remains green, and a Linux Git comparison reports no unstaged EOL drift. The first hosted CI job
can therefore target Linux rather than carrying forward a Windows-runner dependency.
**Initial Linux core workflow (2026-08-03, superseded below):** item 2 was first implemented as
`.github/workflows/core-validation.yml`. One read-only `ubuntu-24.04` job provisions Python 3.11 and the SDK selected by `global.json`, reports both resolved
versions, and invokes the existing asset-independent core driver. Pull requests, `main` pushes, and manual
dispatch share the job; branch-local concurrency cancels superseded runs, and seven-day validation-log upload
occurs only after failure. The workflow has no cache, secrets, private corpus, Godot runtime, packaging, or
deployment access. All four official actions are pinned to full immutable release SHAs. `actionlint` accepts
the workflow and the underlying Python 3.11 core gate remains green locally. Because no remote is configured,
the tracked workflow is dormant and its first hosted Linux execution remains a publication-time confirmation,
not a condition hidden by this commit.
**Private-repository preflight and binary purge (2026-08-03):** a clean source-only checkout disproved the
earlier asset-independent claim: after opcode generation, 501/590 engine cases pass while 89 installed-corpus
or native-oracle cases fail, and `test_opcodes.py` still scans the external script corpus. The target Gitea
server demonstrates a working `ubuntu-latest` runner with checkout/setup-dotnet v4 and Gitea-specific artifact
upload. CI must explicitly separate those workspace cases and inject a synthetic opcode fixture before Actions
is enabled; missing private data will not be converted into green skips.
The unverified `bin/BinExtractALF.exe`, `bin/LzssCpp.dll`, and obsolete `bin/pe-sieve32.exe` are removed from
tracking and purged from every reachable commit before the first remote is added. The rewritten tip is
`a4f029c`; `git fsck --full --strict` is clean, the three paths have no reachable object or path history, and
compaction leaves one 3.00 MiB pack with no loose or garbage objects. The complete pre-rewrite recovery bundle
is `../age-reimpl-pre-binary-purge-20260803.bundle` (8,824,649 bytes; SHA-256
`E157D7D2404DD2FC6E19FB4B65D08FB9784F415DAB1A2CFF6850121A5F305E5E`) and was verified again after the rewrite.
`bin/` now tracks policy only and ignores optional machine-local extractor files; PE-sieve remains documented
solely as a failed historical experiment. This resolves the committed-binary portion of item 3, but
Kelebek-derived source/data and the user-owned project license choice remain unresolved for wider distribution.
**Hermetic Gitea core workflow (2026-08-03):** the correction is complete. The 590 engine cases are now
explicitly partitioned into 502 source-only cases and 88 `Workspace` installed-data/native-oracle cases;
both selections pass with zero skips in the populated workspace. The one accidentally coupled synthetic
asset-store test now builds its catalog fixture instead of being excluded. `test_opcodes.py` injects 248
synthetic observations derived from the canonical observed-opcode set, while production `--bootstrap` still
scans the real corpus by default. Development/test repository discovery uses tracked marker files rather than
the checkout directory name.
The workflow moved to `.gitea/workflows/core-validation.yml` and matches the same-server baseline:
`ubuntu-latest`, checkout/setup-dotnet v4, setup-python v6, and Gitea-specific artifact upload. A standalone
source-only repository under an arbitrary name passes the complete core driver without sibling game/extracted
data. The first actual Linux runner result remains pending until the user adds and pushes the selected remote.
**Not cleanup targets:** generated `build/` output, the two intentional solution files, historical
`docs/superpowers/` plans/specifications, and fidelity-specific complexity that is directly covered by the
native ABI. Reorganization is successful when ownership and reproduction become clearer, not when the raw
@@ -1187,10 +1306,9 @@ layer's rendering diverges from ADV; save layout.
---
## 8. Immediate next step
Begin step 4 of the **codebase consolidation** maintenance slice: make the build graph express source ownership
without changing behavior. Move the platform-neutral movie abstractions and runtime support—decoder interfaces,
movie/audio runtime state, movie-surface registry, and RIFF sanitization—from `godot/` into
`Age.Engine.Frontend` next. Replace their test source links while preserving the intentionally internal API through
explicit friend-assembly access; leave FFmpeg interop/decoder ownership for the following slice.
Continue step 5 of the **codebase consolidation** maintenance slice by adding and pushing the user-selected
private Gitea remote, then confirm the first hosted Linux core result. After that, make the user-owned project-
license and Kelebek-derived-material decisions required before wider distribution. Do not infer a license choice
or configure/publish a remote without the user's explicit direction.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -26,7 +26,7 @@ whenever a tool's inputs/outputs change.**
| Tool | Purpose | Run | Reads → Writes |
|---|---|---|---|
| `validate.py` | Layered project validation front door. `core` regenerates/runtime-checks opcode metadata, lints canonical registries, runs pure Python tooling tests and all .NET engine tests, checks generated opcode references, and runs `git diff --check`. `workspace` adds corpus-derived global generation, the real-data Python suites, full SYS4 decode, and Python RECOVER. `runtime` adds the Godot C# build and forced-portable threaded self-test. `full` combines all phases and adds the booted faithful-wait C# scene sweep. Selected prerequisites are strict: an unavailable game/corpus/Godot requirement fails before execution instead of becoming a green skip. Each gate has a timeout and UTF-8 log under `build/validation/validate-<timestamp>/`; Godot receives an isolated validation-owned user-data/log root there so it cannot read or modify the developer's saves/settings. The final table reports results/durations and a before/after Godot-process leak audit. | `validate.py` (defaults to `--level full`) · `--level core|workspace|runtime|full` · `--godot <console>` · `--game-root <install>` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/<gate>.log` |
| `validate.py` | Layered project validation front door. `core` regenerates/runtime-checks opcode metadata, lints canonical registries, runs pure Python tooling tests and the 502 .NET cases without the `Workspace` trait, checks generated opcode references, and runs `git diff --check`. `workspace` adds the 88 explicitly traited installed-data/native-oracle cases, corpus-derived global generation, real-data Python suites, full SYS4 decode, and Python RECOVER. `runtime` adds the Godot C# build and forced-portable threaded self-test. `full` combines all phases and adds the booted faithful-wait C# scene sweep. Selected prerequisites are strict: an unavailable game/corpus/Godot requirement fails before execution instead of becoming a green skip. Each gate has a timeout and UTF-8 log under `build/validation/validate-<timestamp>/`; Godot receives an isolated validation-owned user-data/log root there so it cannot read or modify the developer's saves/settings. The final table reports results/durations and a before/after Godot-process leak audit. | `validate.py` (defaults to `--level full`) · `--level core|workspace|runtime|full` · `--godot <console>` · `--game-root <install>` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/<gate>.log` |
| `test_validate.py` | Pure tests for launcher-equivalent explicit/environment/PATH/conventional resolution precedence, invalid-explicit hard failure, level composition, and final gate ordering. | `test_validate.py` | temporary files only |
Levels are cumulative around `core`: `workspace` means core+workspace-corpus, `runtime` means core+Godot,
@@ -35,6 +35,23 @@ by a failed preflight; rebuild each through its owning tool in this reference. R
`--godot`, then `AGE_GODOT_CONSOLE`, then `godot4`/`godot`/`godot-mono` on `PATH`. Game-root resolution uses
`--game-root`, then `AGE_GAME_ROOT`, then the conventional sibling install and always requires `SYS4INI.BIN`.
The hosted wrapper is `.gitea/workflows/core-validation.yml`. It matches the target server's demonstrated
`ubuntu-latest`, `actions/checkout@v4`, `actions/setup-dotnet@v4`, and
`christopherhx/gitea-upload-artifact@v4` baseline, adds Python 3.11, and runs only `--level core`. A standalone
source-only repository under an arbitrary directory name passes that exact driver without sibling game or
extracted data. Repository discovery uses tracked marker files rather than requiring the checkout directory to
be named `age-reimpl`. The workflow has no secrets, private corpus, Godot runtime, cache, packaging, or deployment
access; failure logs are retained for seven days. Its first actual Linux/Gitea execution remains pending until
the rewritten repository is pushed.
## Optional local binary tools
`bin/` is an ignored machine-local tool directory, not a source or artifact distribution directory. A local
`BinExtractALF.exe` plus its matching `LzssCpp.dll` may be used by the installed-data archive parity test and to
regenerate `../extracted/`; neither is a runtime dependency. Their known local hashes and unverified provenance
are recorded in `bin/README.md`. PE-sieve is obsolete and is not retained; its historical experiment remains in
`docs/engine-re.md`.
## Container parse / disassemble
| Tool | Purpose | Run | Reads → Writes |
@@ -51,7 +68,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
|---|---|---|---|
| `opcodes_build.py` | Generator + linter for the opcode reference. `--bootstrap` appends entries observed in the Himegari corpus; `--bootstrap-age` appends every missing opcode from the pristine 548-entry AGE catalog as an unobserved compatibility stub. Catalog stubs retain Kelebek's ABI label/argument count so other AGE scripts decode past them, but remain `noop_headless=false`: the VM currently traces/skips them, while coverage continues to report them as unresolved rather than semantically safe. | `--build` · `--lint` · `--bootstrap` · `--bootstrap-age` | `vm-map/opcodes.toml` → ⚙ `tools/age_opcodes_himegari.py`, ⚙ `build/opcodes.json`, ⚙ `docs/opcode-reference.md`, ⚙ `build/opcode-coverage.md` |
| `opcodes_model.py` | In-memory model + loader + linter (dangling-ref / confidence-ceiling / vocabulary / dependents). | *Imported by `opcodes_build.py`.* | `vm-map/opcodes.toml` → — |
| `test_opcodes.py` | Unit tests for the opcode tooling. | `test_opcodes.py` | — |
| `test_opcodes.py` | Unit tests for the opcode tooling. Bootstrap coverage injects 248 synthetic observations derived from the canonical observed-opcode set; it does not read the private script corpus. Production `--bootstrap` still scans the real corpus by default. | `test_opcodes.py` | `vm-map/opcodes.toml` → temporary files only |
| `opcode_context.py` | Read-only evidence gatherer for classifying unnamed opcodes (frequency, argc, operand-type signature, neighbours, disasm snippets, Kelebek comment). | `--top 20` · `opcode_context.py 0x1f4 0x71 …` | corpus → stdout |
| `validate_opcode_table.py` | Definitive decode-coverage validator (replicates Kelebek's `data_array_end` code/data split). | `validate_opcode_table.py` | corpus → stdout |
| `validate_opcode_table_naive.py` | Naïve variant of the above (baseline comparison). | `validate_opcode_table_naive.py` | corpus → stdout |

View File

@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Age.Engine.Tests")]
[assembly: InternalsVisibleTo("Age.MovieCorpusGate")]
[assembly: InternalsVisibleTo("Himegari")]

View File

@@ -156,6 +156,7 @@ public class AdvTextHistoryTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealSc0000FirstPagePopulatesTheRetainedBacklogBeforeItsWait()
{
var scripts = Sys4ScriptProvider.Load(Table);

View File

@@ -259,6 +259,7 @@ public class AdvTextOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealMamesResearchDescriptionUsesImmediateRetainedTextPath()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -310,6 +311,7 @@ public class AdvTextOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void System4BootstrapReplaysAllResetCursorAndBoundsConfigurations()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -24,14 +24,6 @@
<ProjectReference Include="..\Age.Engine\Age.Engine.csproj" />
<ProjectReference Include="..\Age.Engine.Frontend\Age.Engine.Frontend.csproj" />
<ProjectReference Include="..\Age.Engine.Text.Windows\Age.Engine.Text.Windows.csproj" />
<Compile Include="..\..\godot\IMovieDecoder.cs" Link="IMovieDecoder.cs" />
<Compile Include="..\..\godot\MovieRuntime.cs" Link="MovieRuntime.cs" />
<Compile Include="..\..\godot\MovieAudioTimeline.cs" Link="MovieAudioTimeline.cs" />
<Compile Include="..\..\godot\FfmpegMovieNative.cs" Link="FfmpegMovieNative.cs" />
<Compile Include="..\..\godot\FfmpegMovieDecoder.cs" Link="FfmpegMovieDecoder.cs" />
<Compile Include="..\..\godot\MovieSurfaceRegistry.cs" Link="MovieSurfaceRegistry.cs" />
<Compile Include="..\..\godot\RiffWaveSanitizer.cs" Link="RiffWaveSanitizer.cs" />
<Compile Include="..\..\tools\movie-corpus-gate\MovieCorpusGate.cs" Link="MovieCorpusGate.cs" />
</ItemGroup>
</Project>

View File

@@ -71,6 +71,7 @@ public class AgfDecoderTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledSo001HasExpectedAlphaBearingDimensions()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -87,6 +88,7 @@ public class AgfDecoderTests
[InlineData(0x32db, "SO007.AGF")]
[InlineData(0x32dc, "SO008A.AGF")]
[InlineData(0x32dd, "SO007A.AGF")]
[Trait("Category", "Workspace")]
public void InstalledFieldMapSheetsResolveAndDecodeByRawCatalogIndex(int rawId, string name)
{
var resources = ResourceMap.Load();

View File

@@ -160,6 +160,7 @@ public class AudioMixerOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void NativePersistencePathsComeFromIndependentSys4IniProfileValues()
{
Sys4AssetCatalog catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -177,6 +178,7 @@ public class AudioMixerOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void ProfileOverrideRedirectsSaveAndSettingsAsOneNativeLayout()
{
Sys4AssetCatalog catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);

View File

@@ -3,6 +3,7 @@ using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
[Trait("Category", "Workspace")]
public class CallScriptIntegrationTests
{
private sealed class NullHost : IHost

View File

@@ -97,6 +97,7 @@ public class CoroutineHostModelTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000EntryRunsSetupAndFillsDistinctTextureSlots()
{
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table);

View File

@@ -6,6 +6,7 @@ using Xunit;
public class CurDecoderTests
{
[Fact]
[Trait("Category", "Workspace")]
public void HimegariCursor_DecodesPixelsAndHotspot()
{
var resources = ResourceMap.Load();
@@ -24,6 +25,7 @@ public class CurDecoderTests
}
[Fact]
[Trait("Category", "Workspace")]
public void HimegariPanCursor_DecodesFourBitPixelsAndHotspot()
{
var resources = ResourceMap.Load();

View File

@@ -22,6 +22,7 @@ public class FrameYieldTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FrameYield_CalledOncePerStep()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -61,6 +61,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void SC0000ViaSessionMatchesSingleRun()
{
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table);
@@ -77,6 +78,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void BootingSkinitPopulatesDataTableGlobals()
{
// Running the SKINIT data script through the session populates the real skill table into the
@@ -103,6 +105,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void BootedStateSurvivesSnapshot()
{
var s = new GameSession();
@@ -115,6 +118,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void SeedingFormFlagChangesBehavior()
{
// Lily's lines are gated on form flags G[0xa57/8/9]; unseeded => all skipped (0 voices on her lines).

View File

@@ -62,6 +62,7 @@ public class HistoryDataOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void LoaderRetainsRealHistoryFooterArraysForRuntimeCopy()
{
var history = Sys4Loader.Load(Paths.Scripts()["HISTORY.BIN"], Table);

View File

@@ -179,6 +179,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryWheelUpNavigatesToOlderRetainedRows()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -196,6 +197,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryRendersMultipleRowsAfterSeveralSc0000Messages()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -282,6 +284,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealSc0000HistoryButtonRendersAndClosesWithoutAdvancingThePageWait()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -323,6 +326,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryVoicedRowDispatchesItsRetainedVoicePair()
{
var scripts = Sys4ScriptProvider.Load(Table);

View File

@@ -109,6 +109,7 @@ public class HistoryPresentationOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryScriptBuildsVisibleRowsFromARealSc0000Page()
{
var scripts = Sys4ScriptProvider.Load(Table);

View File

@@ -255,6 +255,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000AdvChromeBootstrap_VisitsAllFiveVisibleButtonRegistrations()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -274,6 +275,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000FirstWait_DispatchesRealHistoryHoverCallback()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -436,6 +438,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000MessageSkipButton_EnablesPersistentServiceState()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -471,6 +474,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000HideWindowButton_RunsRealHidewinAndReturnsToAdvWait()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -497,6 +501,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000ActionSeven_EntersRealMenuAndRestoresParentAdvHotspots()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -48,6 +48,7 @@ public class InputBindingTests
}
[Fact]
[Trait("Category", "Workspace")]
public void System4BootstrapReplaysAllSixteenInputConfigurationCalls()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -72,6 +73,7 @@ public class InputBindingTests
[InlineData(0x08)] // retained native Backspace default
[InlineData(0x11)] // SYSTEM4 LeftCtrl binding
[InlineData(0x43)] // SYSTEM4 C binding
[Trait("Category", "Workspace")]
public void LogicalActionSixDrivesHeldAdvFastForward(int virtualKey)
{
var script = ScriptAssembler.Assemble(Table, "ADV_FAST_FORWARD", new List<(int, Operand[])>
@@ -99,6 +101,7 @@ public class InputBindingTests
}
[Fact]
[Trait("Category", "Workspace")]
public void HeldLogicalActionSixSurvivesPersistentSkipLifecycleSuspension()
{
var script = ScriptAssembler.Assemble(Table, "HELD_SKIP_LIFECYCLE", new List<(int, Operand[])>
@@ -122,6 +125,7 @@ public class InputBindingTests
}
[Fact]
[Trait("Category", "Workspace")]
public void ReleasingPhysicalFastForwardDoesNotClearPersistentSkip()
{
var script = ScriptAssembler.Assemble(Table, "ADV_PERSISTENT_AND_HELD_SKIP", new List<(int, Operand[])>

View File

@@ -87,6 +87,7 @@ public class IntegerQueueOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RegisteredRealMvseek_ExpandsMovementCostsThroughTheSystem4ServiceAbi()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -111,6 +112,7 @@ public class IntegerQueueOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealMvseek_ExpandsMovementCostsBeyondTheOrigin()
{
var vm = RealSearchVm("MVSEEK.BIN");
@@ -126,6 +128,7 @@ public class IntegerQueueOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealAtseek_ExpandsAttackDistancesBeyondTheOrigin()
{
var vm = RealSearchVm("ATSEEK.BIN");

View File

@@ -554,6 +554,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000ResumesImmediatelyAfterMovieOpcodeAtBytecodeOffset13d1()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -756,6 +757,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000MoviePayloadReadsFromArchiveVfsAndIsMpegProgramStream()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -771,6 +773,7 @@ public class MovieOpcodeTests
[Theory]
[InlineData(0x335f, "LOGO.AGF")]
[InlineData(0x3364, "OP.AGF")]
[Trait("Category", "Workspace")]
public void ModalMoviePayloadResolvesFromUniversalPackedCatalog(int resourceId, string expectedName)
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -783,6 +786,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void DebugTestMovieDecodesToExactGreenMaskDimensions()
{
if (!OperatingSystem.IsWindows()) return;
@@ -807,6 +811,7 @@ public class MovieOpcodeTests
[Theory]
[InlineData(0x335f, "LOGO.AGF")]
[InlineData(0x3364, "OP.AGF")]
[Trait("Category", "Workspace")]
public void ModalMovieOpeningFramesAreOpaqueAndHaveContinuousCadence(
int resourceId, string expectedName)
{
@@ -841,6 +846,7 @@ public class MovieOpcodeTests
[InlineData(0x2b94, "MVB238.AGF", 280, 352, 866)]
[InlineData(0x2bc2, "MVB908.AGF", 400, 400, 333)]
[InlineData(0x33, "CHAPTER.AGF", 800, 600, 12016)]
[Trait("Category", "Workspace")]
public void FfmpegShimDecodesRepresentativeVfsMovie(int resourceId, string expectedName,
int expectedWidth, int expectedHeight,
long expectedStopTimeMs)
@@ -879,6 +885,7 @@ public class MovieOpcodeTests
[InlineData(0x33, "CHAPTER.AGF")]
[InlineData(0x2bf1, "MVS001.AGF")]
[InlineData(0x335f, "LOGO.AGF")]
[Trait("Category", "Workspace")]
public void FfmpegShimDecodesRepresentativeMpegAudio(int resourceId, string expectedName)
{
if (!OperatingSystem.IsWindows()) return;
@@ -912,6 +919,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FfmpegShimSeeksBothVideoAndAudioNearRequestedPosition()
{
if (!OperatingSystem.IsWindows()) return;
@@ -943,6 +951,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FfmpegShimRejectsTruncatedMovieWithBoundedDiagnostic()
{
if (!OperatingSystem.IsWindows()) return;
@@ -956,6 +965,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FfmpegShimSupportsRepeatedOpenAndClose()
{
if (!OperatingSystem.IsWindows()) return;
@@ -974,6 +984,7 @@ public class MovieOpcodeTests
[Theory]
[InlineData(0x2be3, 280, 500, 450)]
[InlineData(0x2bc2, 400, 333, 300)]
[Trait("Category", "Workspace")]
public void FfmpegPacedDecoderKeepsRealMovieAliveThroughItsStopTime(
int resourceId, int expectedWidth, long expectedStopTimeMs, long minimumElapsedMs)
{

View File

@@ -4,6 +4,7 @@ using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
[Trait("Category", "Workspace")]
public class NaturalBootIntegrationTests
{
private sealed class ReachedSc0000Exception : Exception { }

View File

@@ -64,6 +64,7 @@ public class ReadTextDatabaseTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledNativeRtDatRoundTripsItsLogicalRecordsWhenPresent()
{
string path = Path.Combine(

View File

@@ -9,6 +9,7 @@ public class RecoverTests
private static long G(VirtualMachine vm, int k) => vm.Globals.TryGetValue(k, out var v) ? v : 0;
[Fact]
[Trait("Category", "Workspace")]
public void RecoverUnitTestPasses()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -48,6 +48,7 @@ public class RiffWaveSanitizerTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledSc0000GlowSoundDropsCp932InfoBlockWithoutChangingPcmData()
{
var resources = ResourceMap.Load();
@@ -63,6 +64,7 @@ public class RiffWaveSanitizerTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledFirstBossSoundStopsAtFirstDeclaredRiff()
{
var resources = ResourceMap.Load();

View File

@@ -85,6 +85,7 @@ public class RootReloadTests
}
[Fact]
[Trait("Category", "Workspace")]
public void HimegariRawScriptZeroIsSystem4()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -5,6 +5,7 @@ using Age.Engine.Persistence;
using Age.Engine.Sys4;
using Age.Engine.Vm;
[Trait("Category", "Workspace")]
public class SaveUiIntegrationTests
{
private sealed class MenuReadyException : Exception;

View File

@@ -265,6 +265,7 @@ public class SharedProfileTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledSharedProfileUnlocksKnownCgAndHSceneResourcesWhenPresent()
{
string root = Path.Combine(

View File

@@ -8,6 +8,7 @@ using Xunit;
public class Sys4AssetStoreTests
{
[Fact]
[Trait("Category", "Workspace")]
public void InstalledAppendCatalogHasNativePackSelectionAndStableDirectory()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -40,6 +41,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void CompletePackedAssetEnumerationPreservesBaseThenAppendOrder()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -57,6 +59,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void CompleteAppendDirectoryAndPayloadsMatchBinExtractAlf()
{
string temp = Path.Combine(Path.GetTempPath(), "age-vfsb-oracle-" + Guid.NewGuid().ToString("N"));
@@ -105,6 +108,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RuntimeCatalogMatchesDiagnosticCatalogAndSceneViews()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -154,6 +158,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void EveryCatalogRangeFitsAndRepresentativePayloadsMatchExtractedData()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -186,6 +191,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000RoomAndTitleAudioPayloadsReadDirectlyFromTheirNativeAddressSpaces()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -222,6 +228,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0010LowOperandsAreAlreadyUniversalPackedIds()
{
var resources = ResourceMap.Load();
@@ -236,6 +243,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void TypedResourceResolutionPreservesAppendPackSelector()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -249,6 +257,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void AllInstalledLooseScriptOverridesShadowArchiveCopies()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -281,7 +290,7 @@ public class Sys4AssetStoreTests
try
{
File.WriteAllBytes(Path.Combine(archives, "DATA1.ALF"), new byte[] { 9, 8, 1, 2, 3, 7 });
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
var catalog = Sys4AssetCatalog.Parse(Sys4StartupSettingsTests.BuildCatalog(includeTrailer: true));
var entry = new AssetEntry("TEST.BIN", "DATA1.ALF", 2, 3);
var store = new Sys4AssetStore(catalog, archives, loose);

View File

@@ -4,6 +4,7 @@ using Xunit;
public class Sys4LoaderTests
{
[Fact]
[Trait("Category", "Workspace")]
public void ParsesMenuBinLikeSys4load()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -2,6 +2,7 @@ using Age.Engine.Sys4;
using Age.Engine.Diagnostics;
using Xunit;
[Trait("Category", "Workspace")]
public class Sys4ScriptProviderTests
{
[Fact]

View File

@@ -5,6 +5,7 @@ using Age.Engine.Sys4;
public class Sys4StartupSettingsTests
{
[Fact]
[Trait("Category", "Workspace")]
public void InstalledCatalogExposesOrderedStartupSettingsAndLogicalCanvas()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -100,7 +101,7 @@ public class Sys4StartupSettingsTests
params (string Key, string Value)[] pairs)
=> WrapCatalog(BuildExpanded(pairs));
private static byte[] BuildCatalog(bool includeTrailer)
internal static byte[] BuildCatalog(bool includeTrailer)
=> WrapCatalog(BuildExpanded(Array.Empty<(string, string)>(), includeTrailer));
private static byte[] BuildExpanded(

View File

@@ -24,6 +24,7 @@ public class TextureOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void SC0000FiresTextureOpsWithAssignedFullScreenSlot()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -19,10 +19,16 @@ public static class Paths
private static string FindRepo()
{
var d = new DirectoryInfo(AppContext.BaseDirectory);
while (d != null && d.Name != "age-reimpl") d = d.Parent;
if (d == null) throw new DirectoryNotFoundException(
"age-reimpl root not found above " + AppContext.BaseDirectory);
return d.FullName;
while (d != null)
{
if (File.Exists(Path.Combine(d.FullName, "global.json"))
&& File.Exists(Path.Combine(d.FullName, "engine", "AgeEngine.sln"))
&& File.Exists(Path.Combine(d.FullName, "vm-map", "opcodes.toml")))
return d.FullName;
d = d.Parent;
}
throw new DirectoryNotFoundException(
"repository root not found above " + AppContext.BaseDirectory);
}
/// name(UPPER).BIN -> path; game-dir loose overrides shadow extracted/DATA1 (mirrors paths.scripts()).

7
global.json Normal file
View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.408",
"rollForward": "latestPatch",
"allowPrerelease": false
}
}

View File

@@ -8,6 +8,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\engine\Age.Engine\Age.Engine.csproj" />
<Compile Include="..\..\godot\FfmpegMovieNative.cs" Link="FfmpegMovieNative.cs" />
<ProjectReference Include="..\..\engine\Age.Engine.Frontend\Age.Engine.Frontend.csproj" />
</ItemGroup>
</Project>

View File

@@ -100,8 +100,9 @@ def skeleton_toml(op: int, label: str, argc: int, argtypes_for_op: dict,
f"observed_types = [{obs}]"]
return "\n".join(lines) + "\n"
def bootstrap(toml_path: Path) -> None:
used, argtypes = scan_corpus()
def bootstrap(toml_path: Path, *, corpus_scan=None) -> None:
"""Append observed skeletons from a real scan or an injected synthetic scan fixture."""
used, argtypes = corpus_scan if corpus_scan is not None else scan_corpus()
present = set(M.load(toml_path).opcodes) if toml_path.exists() else set()
blocks = []
for op in sorted(used):

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Standalone tests for the opcode reference tooling. Run: py -3.11 -X utf8 tools/test_opcodes.py"""
import os, sys, tempfile
from collections import Counter
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import opcodes_model as M
@@ -120,14 +121,21 @@ def test_bootstrap():
import opcodes_build as B
from age_opcodes import OPCODES
from pathlib import Path
canonical = M.load(Path(__file__).resolve().parents[1] / "vm-map" / "opcodes.toml")
observed = sorted(op for op, entry in canonical.opcodes.items() if entry.observed_in_himegari)
synthetic_scan = (
Counter({op: 1 for op in observed}),
{op: {i: {0} for i in range(OPCODES[op][1])} for op in observed},
)
fd, p = tempfile.mkstemp(suffix=".toml"); os.close(fd); os.remove(p)
tp = Path(p)
B.bootstrap(tp) # first run: meta + all skeletons
B.bootstrap(tp, corpus_scan=synthetic_scan) # first run: meta + synthetic observed skeletons
m = M.load(tp)
check(len(m.opcodes) >= 240, f"bootstrap seeded ~248 opcodes (got {len(m.opcodes)})")
check(len(m.opcodes) == len(observed),
f"bootstrap seeded all {len(observed)} synthetic observations (got {len(m.opcodes)})")
check(0x90 in m.opcodes and m.opcodes[0x90].argc == 7, "0x90 seeded with argc 7")
n1 = len(m.opcodes)
B.bootstrap(tp) # idempotent: appends nothing new
B.bootstrap(tp, corpus_scan=synthetic_scan) # idempotent: appends nothing new
check(len(M.load(tp).opcodes) == n1, "second bootstrap adds no duplicates")
e, w = M.lint(m)
check(e == [], f"bootstrapped file lints clean (errors: {e[:3]})")

View File

@@ -75,13 +75,21 @@ class GatePlanTests(unittest.TestCase):
self.assertNotIn("sys4-corpus-validate", keys)
self.assertNotIn("godot-selftest", keys)
self.assertNotIn("age-cli-sweep", keys)
engine = next(gate for gate in validate.build_gate_plan("core") if gate.key == "engine-tests")
self.assertIn("Category!=Workspace", engine.command)
def test_workspace_extends_core(self) -> None:
core = {gate.key for gate in validate.build_gate_plan("core")}
workspace = {gate.key for gate in validate.build_gate_plan("workspace")}
self.assertLess(core, workspace)
self.assertIn("sys4-corpus-validate", workspace)
self.assertIn("engine-workspace-tests", workspace)
self.assertNotIn("godot-selftest", workspace)
installed = next(
gate for gate in validate.build_gate_plan("workspace")
if gate.key == "engine-workspace-tests"
)
self.assertIn("Category=Workspace", installed.command)
def test_runtime_requires_resolved_paths(self) -> None:
with self.assertRaisesRegex(ValueError, "resolved Godot"):

View File

@@ -152,16 +152,27 @@ def build_gate_plan(
gates.append(
Gate(
"engine-tests",
".NET engine tests",
".NET engine core tests",
(
"dotnet", "test", "engine/AgeEngine.sln", "--nologo", "--verbosity", "minimal",
"-p:UseSharedCompilation=false",
"--filter", "Category!=Workspace", "-p:UseSharedCompilation=false",
),
300,
)
)
if "workspace" in phases:
gates.append(
Gate(
"engine-workspace-tests",
".NET installed-data and native-oracle tests",
(
"dotnet", "test", "engine/AgeEngine.sln", "--nologo", "--verbosity", "minimal",
"--filter", "Category=Workspace", "-p:UseSharedCompilation=false",
),
300,
)
)
gates.append(
Gate("globals-build", "Global registry build", _python_tool("globals_build.py", "--build"))
)