chore: phase 0 — skeleton + dat asset inventory

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<T> 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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-10 09:02:56 +02:00
commit 020ec2a35d
5 changed files with 227 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -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/

5
AcDream.slnx Normal file
View file

@ -0,0 +1,5 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/AcDream.Cli/AcDream.Cli.csproj" />
</Folder>
</Solution>

22
README.md Normal file
View file

@ -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.

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chorizite.DatReaderWriter" Version="2.1.4" />
</ItemGroup>
</Project>

162
src/AcDream.Cli/Program.cs Normal file
View file

@ -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 <dat-directory>");
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<T>() which routes to the right database internally.
var sections = new (string Section, (string Name, Func<int> Count)[] Rows)[]
{
("visual — geometry", new (string, Func<int>)[]
{
("GfxObj", () => dats.GetAllIdsOfType<GfxObj>().Count()),
("GfxObjDegradeInfo", () => dats.GetAllIdsOfType<GfxObjDegradeInfo>().Count()),
("Setup", () => dats.GetAllIdsOfType<Setup>().Count()),
("Scene", () => dats.GetAllIdsOfType<Scene>().Count()),
("Environment", () => dats.GetAllIdsOfType<DatReaderWriter.DBObjs.Environment>().Count()),
}),
("visual — texturing / materials", new (string, Func<int>)[]
{
("Surface", () => dats.GetAllIdsOfType<Surface>().Count()),
("SurfaceTexture", () => dats.GetAllIdsOfType<SurfaceTexture>().Count()),
("RenderSurface", () => dats.GetAllIdsOfType<RenderSurface>().Count()),
("RenderTexture", () => dats.GetAllIdsOfType<RenderTexture>().Count()),
("RenderMaterial", () => dats.GetAllIdsOfType<RenderMaterial>().Count()),
("MaterialInstance", () => dats.GetAllIdsOfType<MaterialInstance>().Count()),
("MaterialModifier", () => dats.GetAllIdsOfType<MaterialModifier>().Count()),
("Palette", () => dats.GetAllIdsOfType<Palette>().Count()),
("PalSet", () => dats.GetAllIdsOfType<PalSet>().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<T>() only
// handles range-based types, so we walk the cell b-tree once manually.
("terrain / cells", CountCellByLow16(dats)),
("animation", new (string, Func<int>)[]
{
("Animation", () => dats.GetAllIdsOfType<Animation>().Count()),
("MotionTable", () => dats.GetAllIdsOfType<MotionTable>().Count()),
}),
("particles & physics", new (string, Func<int>)[]
{
("ParticleEmitter", () => dats.GetAllIdsOfType<ParticleEmitter>().Count()),
("PhysicsScript", () => dats.GetAllIdsOfType<PhysicsScript>().Count()),
("PhysicsScriptTable", () => dats.GetAllIdsOfType<PhysicsScriptTable>().Count()),
}),
("audio", new (string, Func<int>)[]
{
("SoundTable", () => dats.GetAllIdsOfType<SoundTable>().Count()),
("Wave", () => dats.GetAllIdsOfType<Wave>().Count()),
}),
("game data tables", new (string, Func<int>)[]
{
("SpellTable", () => dats.GetAllIdsOfType<SpellTable>().Count()),
("SpellComponentTable", () => dats.GetAllIdsOfType<SpellComponentTable>().Count()),
("SkillTable", () => dats.GetAllIdsOfType<SkillTable>().Count()),
("CombatTable", () => dats.GetAllIdsOfType<CombatTable>().Count()),
("VitalTable", () => dats.GetAllIdsOfType<VitalTable>().Count()),
("ExperienceTable", () => dats.GetAllIdsOfType<ExperienceTable>().Count()),
("ClothingTable", () => dats.GetAllIdsOfType<ClothingTable>().Count()),
("CharGen", () => dats.GetAllIdsOfType<CharGen>().Count()),
("ChatPoseTable", () => dats.GetAllIdsOfType<ChatPoseTable>().Count()),
("ContractTable", () => dats.GetAllIdsOfType<ContractTable>().Count()),
}),
("ui / strings", new (string, Func<int>)[]
{
("Font", () => dats.GetAllIdsOfType<Font>().Count()),
("StringTable", () => dats.GetAllIdsOfType<StringTable>().Count()),
("LanguageString", () => dats.GetAllIdsOfType<LanguageString>().Count()),
("LanguageInfo", () => dats.GetAllIdsOfType<LanguageInfo>().Count()),
("LayoutDesc", () => dats.GetAllIdsOfType<LayoutDesc>().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<int> 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<int>)[]
{
("LandBlock", () => landBlocks),
("LandBlockInfo", () => landBlockInfos),
("EnvCell", () => envCells),
("Region", () => dats.GetAllIdsOfType<Region>().Count()),
};
}