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:
commit
020ec2a35d
5 changed files with 227 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
5
AcDream.slnx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<Solution>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/AcDream.Cli/AcDream.Cli.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
22
README.md
Normal file
22
README.md
Normal 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.
|
||||||
14
src/AcDream.Cli/AcDream.Cli.csproj
Normal file
14
src/AcDream.Cli/AcDream.Cli.csproj
Normal 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
162
src/AcDream.Cli/Program.cs
Normal 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()),
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue