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