diff --git a/.gitignore b/.gitignore
index bf29764..154bc36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,11 @@ __pycache__/
*.egg-info/
.venv/
+# .NET (engine/) build output
+engine/**/bin/
+engine/**/obj/
+.vs/
+
# Godot / C# (future)
.godot/
*.import
diff --git a/engine/Age.Cli/Age.Cli.csproj b/engine/Age.Cli/Age.Cli.csproj
new file mode 100644
index 0000000..1e7f822
--- /dev/null
+++ b/engine/Age.Cli/Age.Cli.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/engine/Age.Cli/Program.cs b/engine/Age.Cli/Program.cs
new file mode 100644
index 0000000..83fa4f4
--- /dev/null
+++ b/engine/Age.Cli/Program.cs
@@ -0,0 +1,2 @@
+// See https://aka.ms/new-console-template for more information
+Console.WriteLine("Hello, World!");
diff --git a/engine/Age.Engine.Tests/Age.Engine.Tests.csproj b/engine/Age.Engine.Tests/Age.Engine.Tests.csproj
new file mode 100644
index 0000000..2939a28
--- /dev/null
+++ b/engine/Age.Engine.Tests/Age.Engine.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/engine/Age.Engine.Tests/OpcodeTableTests.cs b/engine/Age.Engine.Tests/OpcodeTableTests.cs
new file mode 100644
index 0000000..321ddff
--- /dev/null
+++ b/engine/Age.Engine.Tests/OpcodeTableTests.cs
@@ -0,0 +1,18 @@
+using Age.Engine.Sys4;
+using Xunit;
+
+public class OpcodeTableTests
+{
+ [Fact]
+ public void LoadsAll248FromJson()
+ {
+ var t = OpcodeTableJson.Load(Paths.OpcodesJson);
+ Assert.Equal(248, t.Count);
+ Assert.True(t.TryGet(0x55, out var label, out var argc));
+ Assert.Equal("mov", label);
+ Assert.Equal(2, argc);
+ Assert.Equal("u0041BEB0", t.Label(0x90));
+ Assert.Equal(7, t.Argc(0x90));
+ Assert.Equal(-1, t.Argc(0x9999)); // absent -> -1
+ }
+}
diff --git a/engine/Age.Engine/Age.Engine.csproj b/engine/Age.Engine/Age.Engine.csproj
new file mode 100644
index 0000000..880e720
--- /dev/null
+++ b/engine/Age.Engine/Age.Engine.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/engine/Age.Engine/Model/Instruction.cs b/engine/Age.Engine/Model/Instruction.cs
new file mode 100644
index 0000000..8102072
--- /dev/null
+++ b/engine/Age.Engine/Model/Instruction.cs
@@ -0,0 +1,2 @@
+namespace Age.Engine.Model;
+public sealed record Instruction(int Offset, int Opcode, IReadOnlyList Args);
diff --git a/engine/Age.Engine/Model/OpcodeTable.cs b/engine/Age.Engine/Model/OpcodeTable.cs
new file mode 100644
index 0000000..fa77d01
--- /dev/null
+++ b/engine/Age.Engine/Model/OpcodeTable.cs
@@ -0,0 +1,14 @@
+namespace Age.Engine.Model;
+public sealed class OpcodeTable
+{
+ private readonly IReadOnlyDictionary _t;
+ public OpcodeTable(IReadOnlyDictionary t) => _t = t;
+ public int Count => _t.Count;
+ public bool TryGet(int op, out string label, out int argc)
+ {
+ if (_t.TryGetValue(op, out var e)) { label = e.Label; argc = e.Argc; return true; }
+ label = ""; argc = -1; return false;
+ }
+ public string Label(int op) => _t.TryGetValue(op, out var e) ? e.Label : "";
+ public int Argc(int op) => _t.TryGetValue(op, out var e) ? e.Argc : -1;
+}
diff --git a/engine/Age.Engine/Model/Operand.cs b/engine/Age.Engine/Model/Operand.cs
new file mode 100644
index 0000000..6516d69
--- /dev/null
+++ b/engine/Age.Engine/Model/Operand.cs
@@ -0,0 +1,2 @@
+namespace Age.Engine.Model;
+public readonly record struct Operand(int Type, long Value);
diff --git a/engine/Age.Engine/Model/Script.cs b/engine/Age.Engine/Model/Script.cs
new file mode 100644
index 0000000..870e03d
--- /dev/null
+++ b/engine/Age.Engine/Model/Script.cs
@@ -0,0 +1,9 @@
+namespace Age.Engine.Model;
+public sealed class Script
+{
+ public required ScriptHeader Header { get; init; }
+ public required IReadOnlyList Instructions { get; init; }
+ public required IReadOnlyDictionary IndexByOffset { get; init; }
+ public required IReadOnlyDictionary Strings { get; init; }
+ public string GetString(int offset) => Strings.TryGetValue(offset, out var s) ? s : "";
+}
diff --git a/engine/Age.Engine/Model/ScriptHeader.cs b/engine/Age.Engine/Model/ScriptHeader.cs
new file mode 100644
index 0000000..24236ca
--- /dev/null
+++ b/engine/Age.Engine/Model/ScriptHeader.cs
@@ -0,0 +1,4 @@
+namespace Age.Engine.Model;
+public sealed record ScriptHeader(
+ int LocalInt1, int LocalFloats, int LocalStrings1,
+ int LocalInt2, int Unknown, int LocalStrings2);
diff --git a/engine/Age.Engine/Sys4/OpcodeTableJson.cs b/engine/Age.Engine/Sys4/OpcodeTableJson.cs
new file mode 100644
index 0000000..31aa0af
--- /dev/null
+++ b/engine/Age.Engine/Sys4/OpcodeTableJson.cs
@@ -0,0 +1,19 @@
+using System.Text.Json;
+using Age.Engine.Model;
+namespace Age.Engine.Sys4;
+public static class OpcodeTableJson
+{
+ public static OpcodeTable Load(string path)
+ {
+ using var doc = JsonDocument.Parse(File.ReadAllText(path));
+ var dict = new Dictionary();
+ foreach (var e in doc.RootElement.GetProperty("opcodes").EnumerateArray())
+ {
+ int op = Convert.ToInt32(e.GetProperty("op").GetString(), 16);
+ string label = e.GetProperty("label").GetString() ?? "";
+ int argc = e.GetProperty("argc").GetInt32();
+ dict[op] = (label, argc);
+ }
+ return new OpcodeTable(dict);
+ }
+}
diff --git a/engine/Age.Engine/Sys4/Paths.cs b/engine/Age.Engine/Sys4/Paths.cs
new file mode 100644
index 0000000..930d785
--- /dev/null
+++ b/engine/Age.Engine/Sys4/Paths.cs
@@ -0,0 +1,33 @@
+namespace Age.Engine.Sys4;
+public static class Paths
+{
+ public static string Repo { get; } = FindRepo();
+ public static string Workspace => Directory.GetParent(Repo)!.FullName;
+ public static string Extracted => Path.Combine(Workspace, "extracted");
+ public static string Data1 => Path.Combine(Extracted, "DATA1");
+ public static string GameDir => Path.Combine(Workspace, "姫狩りダンジョンマイスター");
+ public static string Build => Path.Combine(Repo, "build");
+ public static string OpcodesJson => Path.Combine(Build, "opcodes.json");
+
+ 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;
+ }
+
+ /// name(UPPER).BIN -> path; game-dir loose overrides shadow extracted/DATA1 (mirrors paths.scripts()).
+ public static Dictionary Scripts()
+ {
+ var d = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (Directory.Exists(Data1))
+ foreach (var p in Directory.EnumerateFiles(Data1, "*.BIN"))
+ d[Path.GetFileName(p).ToUpperInvariant()] = p;
+ if (Directory.Exists(GameDir))
+ foreach (var p in Directory.EnumerateFiles(GameDir, "*.BIN"))
+ d[Path.GetFileName(p).ToUpperInvariant()] = p;
+ return d;
+ }
+}
diff --git a/engine/AgeEngine.sln b/engine/AgeEngine.sln
new file mode 100644
index 0000000..b8d0065
--- /dev/null
+++ b/engine/AgeEngine.sln
@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine", "Age.Engine\Age.Engine.csproj", "{DD04FB21-F83D-4547-BBEC-97295F30623A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Cli", "Age.Cli\Age.Cli.csproj", "{795573A0-A283-4A28-B26C-606C1AA5E1FE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine.Tests", "Age.Engine.Tests\Age.Engine.Tests.csproj", "{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DD04FB21-F83D-4547-BBEC-97295F30623A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DD04FB21-F83D-4547-BBEC-97295F30623A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DD04FB21-F83D-4547-BBEC-97295F30623A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DD04FB21-F83D-4547-BBEC-97295F30623A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {795573A0-A283-4A28-B26C-606C1AA5E1FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {795573A0-A283-4A28-B26C-606C1AA5E1FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {795573A0-A283-4A28-B26C-606C1AA5E1FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {795573A0-A283-4A28-B26C-606C1AA5E1FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal