Remove packaged runtime repository dependency
This commit is contained in:
@@ -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-<SCENE>.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-<SCENE>.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
|
||||
|
||||
|
||||
@@ -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-<SCENE>.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 |
|
||||
|
||||
@@ -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-<SCENE>.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 <jsonl>` 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-<SCENE>.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 <jsonl>` 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`.
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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<int, (string, int)>();
|
||||
foreach (var e in doc.RootElement.GetProperty("opcodes").EnumerateArray())
|
||||
{
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\engine\Age.Engine\Age.Engine.csproj" />
|
||||
<EmbeddedResource Include="..\build\opcodes.json"
|
||||
LogicalName="Himegari.Runtime.opcodes.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<ProjectReference Include="..\engine\Age.Engine.Text.Windows\Age.Engine.Text.Windows.csproj" />
|
||||
@@ -21,4 +23,8 @@
|
||||
Link="FFmpeg-LICENSE.txt"
|
||||
CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
<Target Name="RequireGeneratedRuntimeMetadata" BeforeTargets="PrepareForBuild"
|
||||
Condition="!Exists('..\build\opcodes.json')">
|
||||
<Error Text="Missing generated runtime metadata '..\build\opcodes.json'. Run 'py -3.11 -X utf8 tools\opcodes_build.py --build' from age-reimpl before building Godot." />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
24
godot/HimegariRuntimeMetadata.cs
Normal file
24
godot/HimegariRuntimeMetadata.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user