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