From d611c4868d8a76bf98d272d0a12851d8c384fad0 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 3 Aug 2026 13:28:59 -0400 Subject: [PATCH] Make Gitea core validation source-only --- .../workflows/core-validation.yml | 16 +++++++----- docs/PROJECT-STRUCTURE.md | 2 +- docs/platform-portability.md | 4 +-- docs/remake-architecture-and-roadmap.md | 26 ++++++++++++++----- docs/tools-reference.md | 20 +++++++------- .../Age.Engine.Tests/AdvTextHistoryTests.cs | 1 + engine/Age.Engine.Tests/AdvTextOpsTests.cs | 2 ++ engine/Age.Engine.Tests/AgfDecoderTests.cs | 2 ++ .../Age.Engine.Tests/AudioMixerOpcodeTests.cs | 2 ++ .../CallScriptIntegrationTests.cs | 1 + .../CoroutineHostModelTests.cs | 1 + engine/Age.Engine.Tests/CurDecoderTests.cs | 2 ++ engine/Age.Engine.Tests/FrameYieldTests.cs | 1 + engine/Age.Engine.Tests/GameSessionTests.cs | 4 +++ .../Age.Engine.Tests/HistoryDataOpsTests.cs | 1 + .../HistoryInteractionOpsTests.cs | 4 +++ .../HistoryPresentationOpsTests.cs | 1 + engine/Age.Engine.Tests/HotspotInputTests.cs | 5 ++++ engine/Age.Engine.Tests/InputBindingTests.cs | 4 +++ .../Age.Engine.Tests/IntegerQueueOpsTests.cs | 3 +++ engine/Age.Engine.Tests/MovieOpcodeTests.cs | 11 ++++++++ .../NaturalBootIntegrationTests.cs | 1 + .../Age.Engine.Tests/ReadTextDatabaseTests.cs | 1 + engine/Age.Engine.Tests/RecoverTests.cs | 1 + .../RiffWaveSanitizerTests.cs | 2 ++ engine/Age.Engine.Tests/RootReloadTests.cs | 1 + .../SaveUiIntegrationTests.cs | 1 + engine/Age.Engine.Tests/SharedProfileTests.cs | 1 + .../Age.Engine.Tests/Sys4AssetStoreTests.cs | 11 +++++++- engine/Age.Engine.Tests/Sys4LoaderTests.cs | 1 + .../Sys4ScriptProviderTests.cs | 1 + .../Sys4StartupSettingsTests.cs | 3 ++- engine/Age.Engine.Tests/TextureOpsTests.cs | 1 + engine/Age.Engine/Sys4/Paths.cs | 14 +++++++--- tools/opcodes_build.py | 5 ++-- tools/test_opcodes.py | 14 +++++++--- tools/test_validate.py | 8 ++++++ tools/validate.py | 15 +++++++++-- 38 files changed, 154 insertions(+), 40 deletions(-) rename {.github => .gitea}/workflows/core-validation.yml (67%) diff --git a/.github/workflows/core-validation.yml b/.gitea/workflows/core-validation.yml similarity index 67% rename from .github/workflows/core-validation.yml rename to .gitea/workflows/core-validation.yml index cd2fc9c..db15554 100644 --- a/.github/workflows/core-validation.yml +++ b/.gitea/workflows/core-validation.yml @@ -5,19 +5,21 @@ on: branches: - main pull_request: + branches: + - main workflow_dispatch: permissions: contents: read concurrency: - group: core-${{ github.workflow }}-${{ github.ref }} + group: core-${{ gitea.workflow }}-${{ gitea.ref }} cancel-in-progress: true jobs: core: name: Linux core gate - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest timeout-minutes: 15 env: DOTNET_CLI_TELEMETRY_OPTOUT: "1" @@ -26,18 +28,18 @@ jobs: PYTHONUTF8: "1" steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python 3.11 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@v6 with: python-version: "3.11" architecture: x64 - name: Set up .NET SDK - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + uses: actions/setup-dotnet@v4 with: global-json-file: global.json @@ -51,9 +53,9 @@ jobs: - name: Upload failure logs if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: christopherhx/gitea-upload-artifact@v4 with: - name: core-validation-${{ github.run_id }}-${{ github.run_attempt }} + name: core-validation-${{ gitea.run_id }}-${{ gitea.run_attempt }} path: build/validation/** if-no-files-found: ignore retention-days: 7 diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index a9803c9..e4733e2 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -32,7 +32,7 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) ├── 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 - ├── .github/workflows/core-validation.yml planned Ubuntu-hosted core gate; source-only audit pending fix + ├── .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 diff --git a/docs/platform-portability.md b/docs/platform-portability.md index 2a39a54..d414689 100644 --- a/docs/platform-portability.md +++ b/docs/platform-portability.md @@ -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-.jsonl` | Let the future profile launcher pass `--game-root`, and run executable-directory/CWD plus case/permission smoke gates on Linux and macOS | +| 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-.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 | Tracked generated references and the engine solution/project files have canonical LF blobs. A source-only checkout found 501/590 engine cases hermetic, 89 installed-corpus/native-oracle cases still reached by `core`, and one Python bootstrap test coupled to the external corpus. The target Gitea server's known-good baseline is `ubuntu-latest`, checkout v4, setup-dotnet v4, and Gitea-specific artifact upload | Explicitly classify the 89 workspace cases and inject a synthetic opcode corpus fixture, then run the corrected core gate in Gitea. Retain Windows-native GDI parity and private installed-game/runtime checks as separate trusted gates | +| 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 diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index de02f60..058d46c 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -920,8 +920,8 @@ do not mix mechanical moves with semantic changes. 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. - **Linux core workflow (2026-08-03):** item 2 is 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 + **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 @@ -947,6 +947,19 @@ do not mix mechanical moves with semantic changes. 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 @@ -1293,10 +1306,9 @@ layer's rendering diverges from ADV; save layout. --- ## 8. Immediate next step -Continue step 5 of the **codebase consolidation** maintenance slice by making the Linux core gate genuinely -source-only: classify the installed-corpus/native-oracle engine tests as an explicit workspace suite, inject a -synthetic corpus seam into the opcode-bootstrap unit test, and align the workflow with the target Gitea server's -known-good `ubuntu-latest` action baseline. Then make the user-owned project-license and Kelebek-derived-material -decisions required before wider distribution. Do not infer a license choice or publish/configure a remote. +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. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 514e72f..385053c 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -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-/`; 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 ` · `--game-root ` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/.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-/`; 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 ` · `--game-root ` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/.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,14 +35,14 @@ 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 planned hosted wrapper is `.github/workflows/core-validation.yml`. A clean source-only checkout audit on -2026-08-03 found that `--level core` still runs 89 installed-corpus/native-oracle engine cases and that -`test_opcodes.py` bootstraps from the external script corpus. The workflow therefore remains dormant until those -tests are explicitly separated from the hermetic core suite; missing data must not become a successful skip. -The known-good configuration on the target Gitea server uses `ubuntu-latest`, `actions/checkout@v4`, -`actions/setup-dotnet@v4`, and `christopherhx/gitea-upload-artifact@v4`. The future core workflow should match -that runner/action baseline, provision Python 3.11, and keep private corpus, Godot runtime, packaging, -deployment, and secrets outside the job. +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 @@ -68,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 | diff --git a/engine/Age.Engine.Tests/AdvTextHistoryTests.cs b/engine/Age.Engine.Tests/AdvTextHistoryTests.cs index dddefd8..eecf84c 100644 --- a/engine/Age.Engine.Tests/AdvTextHistoryTests.cs +++ b/engine/Age.Engine.Tests/AdvTextHistoryTests.cs @@ -156,6 +156,7 @@ public class AdvTextHistoryTests } [Fact] + [Trait("Category", "Workspace")] public void RealSc0000FirstPagePopulatesTheRetainedBacklogBeforeItsWait() { var scripts = Sys4ScriptProvider.Load(Table); diff --git a/engine/Age.Engine.Tests/AdvTextOpsTests.cs b/engine/Age.Engine.Tests/AdvTextOpsTests.cs index bb3b323..cf0cc3b 100644 --- a/engine/Age.Engine.Tests/AdvTextOpsTests.cs +++ b/engine/Age.Engine.Tests/AdvTextOpsTests.cs @@ -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); diff --git a/engine/Age.Engine.Tests/AgfDecoderTests.cs b/engine/Age.Engine.Tests/AgfDecoderTests.cs index 8346447..66b82f6 100644 --- a/engine/Age.Engine.Tests/AgfDecoderTests.cs +++ b/engine/Age.Engine.Tests/AgfDecoderTests.cs @@ -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(); diff --git a/engine/Age.Engine.Tests/AudioMixerOpcodeTests.cs b/engine/Age.Engine.Tests/AudioMixerOpcodeTests.cs index 7de093c..12de0df 100644 --- a/engine/Age.Engine.Tests/AudioMixerOpcodeTests.cs +++ b/engine/Age.Engine.Tests/AudioMixerOpcodeTests.cs @@ -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); diff --git a/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs index bd37d21..e534c5c 100644 --- a/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs +++ b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs @@ -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 diff --git a/engine/Age.Engine.Tests/CoroutineHostModelTests.cs b/engine/Age.Engine.Tests/CoroutineHostModelTests.cs index b5888f3..81e3148 100644 --- a/engine/Age.Engine.Tests/CoroutineHostModelTests.cs +++ b/engine/Age.Engine.Tests/CoroutineHostModelTests.cs @@ -97,6 +97,7 @@ public class CoroutineHostModelTests } [Fact] + [Trait("Category", "Workspace")] public void Sc0000EntryRunsSetupAndFillsDistinctTextureSlots() { var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table); diff --git a/engine/Age.Engine.Tests/CurDecoderTests.cs b/engine/Age.Engine.Tests/CurDecoderTests.cs index ec0a759..44983bd 100644 --- a/engine/Age.Engine.Tests/CurDecoderTests.cs +++ b/engine/Age.Engine.Tests/CurDecoderTests.cs @@ -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(); diff --git a/engine/Age.Engine.Tests/FrameYieldTests.cs b/engine/Age.Engine.Tests/FrameYieldTests.cs index 4622a15..26b43b3 100644 --- a/engine/Age.Engine.Tests/FrameYieldTests.cs +++ b/engine/Age.Engine.Tests/FrameYieldTests.cs @@ -22,6 +22,7 @@ public class FrameYieldTests } [Fact] + [Trait("Category", "Workspace")] public void FrameYield_CalledOncePerStep() { var table = OpcodeTableJson.Load(Paths.OpcodesJson); diff --git a/engine/Age.Engine.Tests/GameSessionTests.cs b/engine/Age.Engine.Tests/GameSessionTests.cs index f991d51..d8dd35f 100644 --- a/engine/Age.Engine.Tests/GameSessionTests.cs +++ b/engine/Age.Engine.Tests/GameSessionTests.cs @@ -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). diff --git a/engine/Age.Engine.Tests/HistoryDataOpsTests.cs b/engine/Age.Engine.Tests/HistoryDataOpsTests.cs index fb3b6c7..7f7c2e9 100644 --- a/engine/Age.Engine.Tests/HistoryDataOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryDataOpsTests.cs @@ -62,6 +62,7 @@ public class HistoryDataOpsTests } [Fact] + [Trait("Category", "Workspace")] public void LoaderRetainsRealHistoryFooterArraysForRuntimeCopy() { var history = Sys4Loader.Load(Paths.Scripts()["HISTORY.BIN"], Table); diff --git a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs index 9de2226..1a76077 100644 --- a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs @@ -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); diff --git a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs index 8131e8a..d848343 100644 --- a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs @@ -109,6 +109,7 @@ public class HistoryPresentationOpsTests } [Fact] + [Trait("Category", "Workspace")] public void RealHistoryScriptBuildsVisibleRowsFromARealSc0000Page() { var scripts = Sys4ScriptProvider.Load(Table); diff --git a/engine/Age.Engine.Tests/HotspotInputTests.cs b/engine/Age.Engine.Tests/HotspotInputTests.cs index fb84734..e966c18 100644 --- a/engine/Age.Engine.Tests/HotspotInputTests.cs +++ b/engine/Age.Engine.Tests/HotspotInputTests.cs @@ -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); diff --git a/engine/Age.Engine.Tests/InputBindingTests.cs b/engine/Age.Engine.Tests/InputBindingTests.cs index dd5e5a4..b026674 100644 --- a/engine/Age.Engine.Tests/InputBindingTests.cs +++ b/engine/Age.Engine.Tests/InputBindingTests.cs @@ -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[])> diff --git a/engine/Age.Engine.Tests/IntegerQueueOpsTests.cs b/engine/Age.Engine.Tests/IntegerQueueOpsTests.cs index 9ef498e..d27b7e4 100644 --- a/engine/Age.Engine.Tests/IntegerQueueOpsTests.cs +++ b/engine/Age.Engine.Tests/IntegerQueueOpsTests.cs @@ -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"); diff --git a/engine/Age.Engine.Tests/MovieOpcodeTests.cs b/engine/Age.Engine.Tests/MovieOpcodeTests.cs index 8a260a9..381f806 100644 --- a/engine/Age.Engine.Tests/MovieOpcodeTests.cs +++ b/engine/Age.Engine.Tests/MovieOpcodeTests.cs @@ -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) { diff --git a/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs b/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs index 10fd558..abeb46f 100644 --- a/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs +++ b/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs @@ -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 { } diff --git a/engine/Age.Engine.Tests/ReadTextDatabaseTests.cs b/engine/Age.Engine.Tests/ReadTextDatabaseTests.cs index 9448d95..b13fa0a 100644 --- a/engine/Age.Engine.Tests/ReadTextDatabaseTests.cs +++ b/engine/Age.Engine.Tests/ReadTextDatabaseTests.cs @@ -64,6 +64,7 @@ public class ReadTextDatabaseTests } [Fact] + [Trait("Category", "Workspace")] public void InstalledNativeRtDatRoundTripsItsLogicalRecordsWhenPresent() { string path = Path.Combine( diff --git a/engine/Age.Engine.Tests/RecoverTests.cs b/engine/Age.Engine.Tests/RecoverTests.cs index 211239d..ba4d24c 100644 --- a/engine/Age.Engine.Tests/RecoverTests.cs +++ b/engine/Age.Engine.Tests/RecoverTests.cs @@ -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); diff --git a/engine/Age.Engine.Tests/RiffWaveSanitizerTests.cs b/engine/Age.Engine.Tests/RiffWaveSanitizerTests.cs index 487049c..ce0ee9a 100644 --- a/engine/Age.Engine.Tests/RiffWaveSanitizerTests.cs +++ b/engine/Age.Engine.Tests/RiffWaveSanitizerTests.cs @@ -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(); diff --git a/engine/Age.Engine.Tests/RootReloadTests.cs b/engine/Age.Engine.Tests/RootReloadTests.cs index 158e366..623ca67 100644 --- a/engine/Age.Engine.Tests/RootReloadTests.cs +++ b/engine/Age.Engine.Tests/RootReloadTests.cs @@ -85,6 +85,7 @@ public class RootReloadTests } [Fact] + [Trait("Category", "Workspace")] public void HimegariRawScriptZeroIsSystem4() { var table = OpcodeTableJson.Load(Paths.OpcodesJson); diff --git a/engine/Age.Engine.Tests/SaveUiIntegrationTests.cs b/engine/Age.Engine.Tests/SaveUiIntegrationTests.cs index bf41767..277b80f 100644 --- a/engine/Age.Engine.Tests/SaveUiIntegrationTests.cs +++ b/engine/Age.Engine.Tests/SaveUiIntegrationTests.cs @@ -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; diff --git a/engine/Age.Engine.Tests/SharedProfileTests.cs b/engine/Age.Engine.Tests/SharedProfileTests.cs index 1ff83f3..1749135 100644 --- a/engine/Age.Engine.Tests/SharedProfileTests.cs +++ b/engine/Age.Engine.Tests/SharedProfileTests.cs @@ -265,6 +265,7 @@ public class SharedProfileTests } [Fact] + [Trait("Category", "Workspace")] public void InstalledSharedProfileUnlocksKnownCgAndHSceneResourcesWhenPresent() { string root = Path.Combine( diff --git a/engine/Age.Engine.Tests/Sys4AssetStoreTests.cs b/engine/Age.Engine.Tests/Sys4AssetStoreTests.cs index ff1e6a0..f75e0a1 100644 --- a/engine/Age.Engine.Tests/Sys4AssetStoreTests.cs +++ b/engine/Age.Engine.Tests/Sys4AssetStoreTests.cs @@ -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); diff --git a/engine/Age.Engine.Tests/Sys4LoaderTests.cs b/engine/Age.Engine.Tests/Sys4LoaderTests.cs index d65bc02..fa3a147 100644 --- a/engine/Age.Engine.Tests/Sys4LoaderTests.cs +++ b/engine/Age.Engine.Tests/Sys4LoaderTests.cs @@ -4,6 +4,7 @@ using Xunit; public class Sys4LoaderTests { [Fact] + [Trait("Category", "Workspace")] public void ParsesMenuBinLikeSys4load() { var table = OpcodeTableJson.Load(Paths.OpcodesJson); diff --git a/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs b/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs index 3596316..7f79aab 100644 --- a/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs +++ b/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs @@ -2,6 +2,7 @@ using Age.Engine.Sys4; using Age.Engine.Diagnostics; using Xunit; +[Trait("Category", "Workspace")] public class Sys4ScriptProviderTests { [Fact] diff --git a/engine/Age.Engine.Tests/Sys4StartupSettingsTests.cs b/engine/Age.Engine.Tests/Sys4StartupSettingsTests.cs index 57d8480..571a49a 100644 --- a/engine/Age.Engine.Tests/Sys4StartupSettingsTests.cs +++ b/engine/Age.Engine.Tests/Sys4StartupSettingsTests.cs @@ -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( diff --git a/engine/Age.Engine.Tests/TextureOpsTests.cs b/engine/Age.Engine.Tests/TextureOpsTests.cs index 67cbf4f..670b2c9 100644 --- a/engine/Age.Engine.Tests/TextureOpsTests.cs +++ b/engine/Age.Engine.Tests/TextureOpsTests.cs @@ -24,6 +24,7 @@ public class TextureOpsTests } [Fact] + [Trait("Category", "Workspace")] public void SC0000FiresTextureOpsWithAssignedFullScreenSlot() { var table = OpcodeTableJson.Load(Paths.OpcodesJson); diff --git a/engine/Age.Engine/Sys4/Paths.cs b/engine/Age.Engine/Sys4/Paths.cs index 5585578..8b6a40f 100644 --- a/engine/Age.Engine/Sys4/Paths.cs +++ b/engine/Age.Engine/Sys4/Paths.cs @@ -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()). diff --git a/tools/opcodes_build.py b/tools/opcodes_build.py index 9b03363..e7aeb17 100644 --- a/tools/opcodes_build.py +++ b/tools/opcodes_build.py @@ -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): diff --git a/tools/test_opcodes.py b/tools/test_opcodes.py index d8ad201..0ed5f0f 100644 --- a/tools/test_opcodes.py +++ b/tools/test_opcodes.py @@ -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]})") diff --git a/tools/test_validate.py b/tools/test_validate.py index f2a4ef7..23c0f1a 100644 --- a/tools/test_validate.py +++ b/tools/test_validate.py @@ -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"): diff --git a/tools/validate.py b/tools/validate.py index 7863c95..5ea8a3f 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -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")) )