From 45bd350c1f8c54845dfe358a60f031680dcc1d1a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 31 Jul 2026 08:50:26 -0400 Subject: [PATCH] Remove packaged runtime repository dependency --- docs/PROJECT-STRUCTURE.md | 13 ++++++----- docs/platform-portability.md | 5 ++++- docs/tools-reference.md | 10 +++++---- engine/Age.Engine.Tests/OpcodeTableTests.cs | 15 +++++++++++++ engine/Age.Engine/Sys4/OpcodeTableJson.cs | 9 +++++++- godot/Himegari.csproj | 6 ++++++ godot/HimegariRuntimeMetadata.cs | 24 +++++++++++++++++++++ godot/Main.cs | 20 ++++++++++++++--- 8 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 godot/HimegariRuntimeMetadata.cs diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index af39a84..bd2e6c3 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -117,13 +117,16 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) │ and bootstrap/build scripts (outputs stay under disposable build/) ├── tools/frida/ runtime-capture + engine-dump scripts (see tools/frida/README.md) └── godot/ DELIVERABLE — the Godot/C# ADV front-end (references Age.Engine - plus the optional exact Windows text adapter), including the - portable TextServer glyph adapter, per-profile font-substitution - configuration, and TITLE-only F4 debug scene launcher + plus the optional exact Windows text adapter). Its assembly embeds + generated build/opcodes.json through HimegariRuntimeMetadata, so + exports do not need repository discovery. Also contains the portable + TextServer glyph adapter, per-profile font-substitution configuration, + and TITLE-only F4 debug scene launcher ``` -The disposable `build/page-map-.jsonl` files are produced by normal Godot runs and map runtime ADV -page ordinals to their authoritative script offsets for `tools/locate_page.py`. +The disposable `build/page-map-.jsonl` files are produced by editor/development Godot runs and map +runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports +have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead. ## Conventions diff --git a/docs/platform-portability.md b/docs/platform-portability.md index 447ba88..c8781ff 100644 --- a/docs/platform-portability.md +++ b/docs/platform-portability.md @@ -18,6 +18,9 @@ The VM and content pipeline are already mostly platform-neutral: - Native-compatible persistence uses managed streams behind `INativeDatStore`; Godot redirects AGE's related save/settings profile to `user://`, yielding `user://SAVE` and `user://SYS4REG.INI` while retaining native filenames and formats. +- The Himegari Godot assembly embeds generated `build/opcodes.json` as profile runtime metadata. Exported + builds never search for an `age-reimpl` repository ancestor; their automatic page-map diagnostics live + under `user://diagnostics/page-maps`, while editor/development runs retain the workspace `build/` handoff. The sole movie path now uses the project-owned FFmpeg C ABI rather than a Windows multimedia API, but only a Windows-x64 native bundle is built and staged today. The accepted DirectShow fallback was deleted after the @@ -36,7 +39,7 @@ or replaced before claiming portable exports. | ADV font discovery/raster fidelity | `Age.Engine.Text` remains OS-neutral. On compatible ACP-932 Windows, `auto` selects the separate exact `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. 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 | 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 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. `Paths.cs` remains the development/test locator for generated opcode data, diagnostics, and CLI conveniences | Installed game data no longer depends on the workspace sibling layout, enabling an executable beside `AGE.EXE`, a terminal launch from the install, or a launcher-supplied absolute profile root. A packaged export still needs its generated runtime metadata bundled independently of repository discovery | Add export packaging for the opcode/profile artifacts, 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`. `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. An executable can sit beside the game, inherit a terminal-launched install directory, or receive a launcher-supplied absolute profile root. Exported automatic page maps use `user://diagnostics/page-maps`; editor/development runs preserve `build/page-map-.jsonl` | Add the export preset/artifact pipeline, 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 | diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 9c4e6a5..39b99e5 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -431,10 +431,12 @@ with the coordinator globals queued on the VM thread, then lets SYSTEM4 perform call. F4 outside TITLE prints an unavailable reason and changes no state. Cancel or Escape closes the panel. The launcher does not seed story/profile state, jump to byte offsets, or force-switch an active child scene. -**Godot page locator:** every normal run recreates `build/page-map-.jsonl`, adding one record per -`wait-for-input` with the run-relative page, page-start location, canonical wait script/offset, last -show-text instruction and string offsets, text, and nested call stack. Use `--page-map ` to override -the output. `--locator-hud` shows `SC0000 P014 · wait SC0000@0x… · text SC0000@0x…` at launch; **F2** +**Godot page locator:** every editor/development run recreates `build/page-map-.jsonl`; a packaged +export writes the same automatic map below `user://diagnostics/page-maps`. Each `wait-for-input` adds the +run-relative page, page-start location, canonical wait script/offset, last show-text instruction and string +offsets, text, and nested call stack. Use `--page-map ` to override the output; pass that explicit path +to `locate_page.py --map` when resolving a map copied from an export profile. `--locator-hud` shows +`SC0000 P014 · wait SC0000@0x… · text SC0000@0x…` at launch; **F2** toggles it and **F3** copies the current locator to the clipboard. The offset remains authoritative because branching/state can shift page ordinals between runs. Resolve a reported page with `py -3.11 -X utf8 tools/locate_page.py SC0000 14`. diff --git a/engine/Age.Engine.Tests/OpcodeTableTests.cs b/engine/Age.Engine.Tests/OpcodeTableTests.cs index 9007940..2b75a2b 100644 --- a/engine/Age.Engine.Tests/OpcodeTableTests.cs +++ b/engine/Age.Engine.Tests/OpcodeTableTests.cs @@ -4,6 +4,21 @@ using Xunit; public class OpcodeTableTests { + [Fact] + public void LoadsFromCallerOwnedStreamWithoutClosingIt() + { + using var source = new MemoryStream(System.Text.Encoding.UTF8.GetBytes( + """ + {"opcodes":[{"op":"0x55","label":"mov","argc":2}]} + """)); + + OpcodeTable table = OpcodeTableJson.Load(source); + + Assert.True(source.CanRead); + Assert.Equal("mov", table.Label(0x55)); + Assert.Equal(2, table.Argc(0x55)); + } + [Fact] public void LoadsCompleteAgeCatalog() { diff --git a/engine/Age.Engine/Sys4/OpcodeTableJson.cs b/engine/Age.Engine/Sys4/OpcodeTableJson.cs index 31aa0af..08780a7 100644 --- a/engine/Age.Engine/Sys4/OpcodeTableJson.cs +++ b/engine/Age.Engine/Sys4/OpcodeTableJson.cs @@ -5,7 +5,14 @@ public static class OpcodeTableJson { public static OpcodeTable Load(string path) { - using var doc = JsonDocument.Parse(File.ReadAllText(path)); + using FileStream stream = File.OpenRead(path); + return Load(stream); + } + + public static OpcodeTable Load(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + using var doc = JsonDocument.Parse(stream); var dict = new Dictionary(); foreach (var e in doc.RootElement.GetProperty("opcodes").EnumerateArray()) { diff --git a/godot/Himegari.csproj b/godot/Himegari.csproj index e303620..3510f5f 100644 --- a/godot/Himegari.csproj +++ b/godot/Himegari.csproj @@ -9,6 +9,8 @@ + @@ -21,4 +23,8 @@ Link="FFmpeg-LICENSE.txt" CopyToOutputDirectory="PreserveNewest" /> + + + diff --git a/godot/HimegariRuntimeMetadata.cs b/godot/HimegariRuntimeMetadata.cs new file mode 100644 index 0000000..1ba4a34 --- /dev/null +++ b/godot/HimegariRuntimeMetadata.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using System.Reflection; +using Age.Engine.Model; +using Age.Engine.Sys4; + +/// +/// Himegari profile metadata that must travel with every Godot build. Repository discovery belongs to +/// development tooling; exported runtimes read the generator output embedded into this assembly. +/// +internal static class HimegariRuntimeMetadata +{ + internal const string OpcodeTableResourceName = "Himegari.Runtime.opcodes.json"; + + internal static OpcodeTable LoadOpcodeTable() + { + Assembly assembly = typeof(HimegariRuntimeMetadata).Assembly; + using Stream stream = assembly.GetManifestResourceStream(OpcodeTableResourceName) + ?? throw new InvalidDataException( + $"Embedded runtime metadata '{OpcodeTableResourceName}' is missing from " + + $"{assembly.GetName().Name}."); + return OpcodeTableJson.Load(stream); + } +} diff --git a/godot/Main.cs b/godot/Main.cs index 71d6a39..713dfb4 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -275,7 +275,7 @@ public partial class Main : Godot.Control _clock.Speed = System.Math.Clamp(speed, 0.05, 8.0); GD.Print($"[renderer] retained backend={(_useGpuBackend ? "gpu" : "software")}"); - var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var table = HimegariRuntimeMetadata.LoadOpcodeTable(); // Persistence retains AGE's native filenames and formats, but the port owns one profile-root // interception point. Himegari's SYS4INI makes SAVEPATH the SAVE child of REGFILEPATH, so both // save payloads and SYS4REG.INI remain isolated together under Godot's user directory. @@ -333,7 +333,7 @@ public partial class Main : Godot.Control && !scene.Equals("SYSTEM4", System.StringComparison.OrdinalIgnoreCase); if (_timelineLogPath != null) _timeline = new GodotTimelineLog(_timelineLogPath); if (!_selftest && pageMapPath == null) - pageMapPath = System.IO.Path.Combine(Paths.Build, $"page-map-{scene.ToUpperInvariant()}.jsonl"); + pageMapPath = DefaultPageMapPath(scene); _locator = new PageLocatorState(scene, _selftest ? null : pageMapPath); _locatorHud.Visible = _locatorHudVisible; var resources = scripts != null @@ -2043,7 +2043,7 @@ public partial class Main : Godot.Control private void RunSelfTest() { - var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var table = HimegariRuntimeMetadata.LoadOpcodeTable(); var (script, provider) = BuildSelfTestScene(table); var headless = new VirtualMachine(script, table, new CaptureHost(), null, provider); headless.Run(); @@ -2381,6 +2381,20 @@ public partial class Main : Godot.Control GetTree().Quit(ok ? 0 : 1); } + private static string DefaultPageMapPath(string scene) + { + string fileName = $"page-map-{scene.ToUpperInvariant()}.jsonl"; +#if TOOLS + // Preserve the established workspace handoff for editor/development runs. + return System.IO.Path.Combine(Paths.Build, fileName); +#else + // Exports have no repository and may be installed read-only. Keep diagnostics with the + // profile-owned Godot data instead of probing for an age-reimpl ancestor. + return System.IO.Path.Combine( + ProjectSettings.GlobalizePath("user://"), "diagnostics", "page-maps", fileName); +#endif + } + // A deterministic synthesized scene: show-text, wait-for-input (exercises the suspend plumbing), a // nested call-script into a synthetic subroutine (exercises call-script handling), shared globals. private static (Script, IScriptProvider) BuildSelfTestScene(OpcodeTable table)