From 020ec2a35dad8754c1a1ef6837c896ae0e53d6bf Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 09:02:56 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20phase=200=20=E2=80=94=20skeleton=20+?= =?UTF-8?q?=20dat=20asset=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brand-new solution targeting .NET 10, using Chorizite.DatReaderWriter 2.1.4 to walk a retail AC dat directory and print how many of each asset type live in client_portal / client_cell_1 / client_highres / client_local_English. Opens the four dats in ~16 ms and counts 887,381 indexed assets across 40+ tracked DBObj types. Cell-database terrain (LandBlock, LandBlockInfo, EnvCell) uses mask-based IDs that DatReaderWriter 2.1.4's GetAllIdsOfType does not support; worked around with a manual b-tree walk in CountCellByLow16. Sanity check: LandBlock count is 65,025 = 255 x 255, exactly the AC world grid. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 24 +++++ AcDream.slnx | 5 + README.md | 22 ++++ src/AcDream.Cli/AcDream.Cli.csproj | 14 +++ src/AcDream.Cli/Program.cs | 162 +++++++++++++++++++++++++++++ 5 files changed, 227 insertions(+) create mode 100644 .gitignore create mode 100644 AcDream.slnx create mode 100644 README.md create mode 100644 src/AcDream.Cli/AcDream.Cli.csproj create mode 100644 src/AcDream.Cli/Program.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d56ee4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Build output +bin/ +obj/ +out/ + +# Rider / VS +.idea/ +.vs/ +*.user +*.suo + +# NuGet +*.nupkg +packages/ + +# OS +.DS_Store +Thumbs.db + +# Reference repos and retail client (large, not our code, separate licenses) +references/ + +# Claude Code session state +.claude/ diff --git a/AcDream.slnx b/AcDream.slnx new file mode 100644 index 0000000..8772319 --- /dev/null +++ b/AcDream.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f2e1a1 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# acdream + +Experimental modern open-source Asheron's Call client in C# / .NET 10. + +**Status:** pre-alpha, not playable. Phase 0 only — dat file asset inventory. + +**Stack:** .NET 10, [Chorizite.DatReaderWriter](https://github.com/Chorizite/DatReaderWriter) for dat parsing. Silk.NET + Avalonia planned for rendering/UI (not yet wired up). + +**Requires:** A retail Asheron's Call install (Turbine/Microsoft property — supply your own). Set `ACDREAM_DAT_DIR` environment variable to the directory containing `client_portal.dat`, `client_cell_1.dat`, `client_highres.dat`, and `client_local_English.dat`, or pass it as the first CLI argument. + +## Layout + +- `src/AcDream.Cli/` — console app that dumps asset counts from a dat directory +- `references/` — local read-only reference material (ACE, ACViewer, WorldBuilder, DatReaderWriter, holtburger, retail AC install). Gitignored. + +## Run + +``` +dotnet run --project src/AcDream.Cli -- "C:\path\to\Asheron's Call" +``` + +Or set `ACDREAM_DAT_DIR` and run without args. diff --git a/src/AcDream.Cli/AcDream.Cli.csproj b/src/AcDream.Cli/AcDream.Cli.csproj new file mode 100644 index 0000000..744845d --- /dev/null +++ b/src/AcDream.Cli/AcDream.Cli.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/src/AcDream.Cli/Program.cs b/src/AcDream.Cli/Program.cs new file mode 100644 index 0000000..a4c290e --- /dev/null +++ b/src/AcDream.Cli/Program.cs @@ -0,0 +1,162 @@ +using System.Diagnostics; +using DatReaderWriter; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Options; +using Env = System.Environment; + +// Phase 0: open the four AC dat files and print how many of each asset type live in them. +// This proves DatReaderWriter works on our retail dats and gives us a baseline inventory +// to compare against what a future renderer needs. + +string? datDir = args.FirstOrDefault() ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); +if (string.IsNullOrWhiteSpace(datDir)) +{ + Console.Error.WriteLine("usage: AcDream.Cli "); + Console.Error.WriteLine(" or: set ACDREAM_DAT_DIR and run with no args"); + return 2; +} + +if (!Directory.Exists(datDir)) +{ + Console.Error.WriteLine($"error: directory not found: {datDir}"); + return 2; +} + +string[] required = ["client_portal.dat", "client_cell_1.dat", "client_highres.dat", "client_local_English.dat"]; +var missing = required.Where(f => !File.Exists(Path.Combine(datDir, f))).ToArray(); +if (missing.Length > 0) +{ + Console.Error.WriteLine($"error: missing dat files in {datDir}:"); + foreach (var f in missing) Console.Error.WriteLine($" - {f}"); + return 2; +} + +Console.WriteLine($"acdream asset dump"); +Console.WriteLine($"dat dir: {datDir}"); +Console.WriteLine(); + +var sw = Stopwatch.StartNew(); +using var dats = new DatCollection(datDir, DatAccessType.Read); +sw.Stop(); +Console.WriteLine($"opened 4 dats in {sw.ElapsedMilliseconds} ms"); +Console.WriteLine(); + +// File sizes, just so we can see they're real +foreach (var f in required) +{ + var path = Path.Combine(datDir, f); + var mb = new FileInfo(path).Length / 1024.0 / 1024.0; + Console.WriteLine($" {f,-30} {mb,8:F1} MB"); +} +Console.WriteLine(); + +// Count assets by type. Grouped by what matters for a renderer / client. +// Uses DatCollection.GetAllIdsOfType() which routes to the right database internally. +var sections = new (string Section, (string Name, Func Count)[] Rows)[] +{ + ("visual — geometry", new (string, Func)[] + { + ("GfxObj", () => dats.GetAllIdsOfType().Count()), + ("GfxObjDegradeInfo", () => dats.GetAllIdsOfType().Count()), + ("Setup", () => dats.GetAllIdsOfType().Count()), + ("Scene", () => dats.GetAllIdsOfType().Count()), + ("Environment", () => dats.GetAllIdsOfType().Count()), + }), + ("visual — texturing / materials", new (string, Func)[] + { + ("Surface", () => dats.GetAllIdsOfType().Count()), + ("SurfaceTexture", () => dats.GetAllIdsOfType().Count()), + ("RenderSurface", () => dats.GetAllIdsOfType().Count()), + ("RenderTexture", () => dats.GetAllIdsOfType().Count()), + ("RenderMaterial", () => dats.GetAllIdsOfType().Count()), + ("MaterialInstance", () => dats.GetAllIdsOfType().Count()), + ("MaterialModifier", () => dats.GetAllIdsOfType().Count()), + ("Palette", () => dats.GetAllIdsOfType().Count()), + ("PalSet", () => dats.GetAllIdsOfType().Count()), + }), + // Note: Cell dat uses mask-based IDs for LandBlock/LandBlockInfo/EnvCell — the low + // 16 bits distinguish the type. DatReaderWriter 2.1.4's GetAllIdsOfType() only + // handles range-based types, so we walk the cell b-tree once manually. + ("terrain / cells", CountCellByLow16(dats)), + ("animation", new (string, Func)[] + { + ("Animation", () => dats.GetAllIdsOfType().Count()), + ("MotionTable", () => dats.GetAllIdsOfType().Count()), + }), + ("particles & physics", new (string, Func)[] + { + ("ParticleEmitter", () => dats.GetAllIdsOfType().Count()), + ("PhysicsScript", () => dats.GetAllIdsOfType().Count()), + ("PhysicsScriptTable", () => dats.GetAllIdsOfType().Count()), + }), + ("audio", new (string, Func)[] + { + ("SoundTable", () => dats.GetAllIdsOfType().Count()), + ("Wave", () => dats.GetAllIdsOfType().Count()), + }), + ("game data tables", new (string, Func)[] + { + ("SpellTable", () => dats.GetAllIdsOfType().Count()), + ("SpellComponentTable", () => dats.GetAllIdsOfType().Count()), + ("SkillTable", () => dats.GetAllIdsOfType().Count()), + ("CombatTable", () => dats.GetAllIdsOfType().Count()), + ("VitalTable", () => dats.GetAllIdsOfType().Count()), + ("ExperienceTable", () => dats.GetAllIdsOfType().Count()), + ("ClothingTable", () => dats.GetAllIdsOfType().Count()), + ("CharGen", () => dats.GetAllIdsOfType().Count()), + ("ChatPoseTable", () => dats.GetAllIdsOfType().Count()), + ("ContractTable", () => dats.GetAllIdsOfType().Count()), + }), + ("ui / strings", new (string, Func)[] + { + ("Font", () => dats.GetAllIdsOfType().Count()), + ("StringTable", () => dats.GetAllIdsOfType().Count()), + ("LanguageString", () => dats.GetAllIdsOfType().Count()), + ("LanguageInfo", () => dats.GetAllIdsOfType().Count()), + ("LayoutDesc", () => dats.GetAllIdsOfType().Count()), + }), +}; + +long grandTotal = 0; +foreach (var (section, rows) in sections) +{ + Console.WriteLine($"[{section}]"); + long sectionTotal = 0; + foreach (var (name, count) in rows) + { + var n = count(); + sectionTotal += n; + Console.WriteLine($" {name,-22} {n,10:N0}"); + } + Console.WriteLine($" {"subtotal",-22} {sectionTotal,10:N0}"); + Console.WriteLine(); + grandTotal += sectionTotal; +} + +Console.WriteLine($"grand total: {grandTotal:N0} indexed assets across tracked types"); +return 0; + +static (string Name, Func Count)[] CountCellByLow16(DatCollection dats) +{ + // Walk the cell b-tree once and bucket by low 16 bits: + // 0xFFFF → LandBlock (terrain heightmap) + // 0xFFFE → LandBlockInfo (static objects on the landblock) + // other → EnvCell (indoor dungeon cell) + int landBlocks = 0, landBlockInfos = 0, envCells = 0, other = 0; + foreach (var file in dats.Cell.Tree) + { + var low = file.Id & 0xFFFFu; + if (low == 0xFFFFu) landBlocks++; + else if (low == 0xFFFEu) landBlockInfos++; + else if (file.Id != 0) envCells++; + else other++; + } + return new (string, Func)[] + { + ("LandBlock", () => landBlocks), + ("LandBlockInfo", () => landBlockInfos), + ("EnvCell", () => envCells), + ("Region", () => dats.GetAllIdsOfType().Count()), + }; +}