Skip to content

Parser Architecture

JeanxPereira edited this page May 28, 2026 · 1 revision

Parser Architecture

How the C# port is structured. The wire-level behaviour it implements is on Binary Format; the client design it mirrors is on Asset System. This page is about the code.

Two layers

The parser core is pure POCO (plain objects, no UI concerns). Observable bindings live in a separate editor adapter. The boundary is deliberate — the original C# conflated them, shipping INotifyPropertyChanged and ObservableCollection inside the parser's output, which forced every consumer (CLI, wiki, a server) to drag editor machinery along.

flowchart TB
    subgraph L2 ["L2 — Editor (Avalonia, MVVM)"]
        EN["EditorNode hierarchy<br/>INotifyPropertyChanged + ObservableCollection"]
        ETB["EditorTreeBuilder.Build(AssetValue)"]
        ETB --> EN
    end
    subgraph L1 ["L1 — Core (lean, immutable)"]
        Dbpf["DbpfReader"]
        Parser["AssetParser"]
        Registry["TypeRegistry"]
        Value["AssetValue tree (POCO)"]
        Catalog["Catalog/ — fluent stubs"]
        Dbpf --> Parser
        Catalog -.->|registers| Registry
        Parser -->|uses| Registry
        Parser --> Value
    end
    Value -.->|consumed by| ETB
Loading
Layer Project Carries
L1 — Core AssetData.Parser (library) DbpfReader, AssetParser, TypeRegistry, the AssetValue tree, the catalog stubs. Depends on nothing but the BCL.
L2 — Editor AssetData.Parser.Editor (Avalonia) EditorNode adapters, view-models, undo/redo, XML round-trip.

The CLI and the wiki generator consume L1 directly — they never touch L2.

Solution topology

graph TD
    subgraph libs["Libraries"]
        Core["AssetData.Parser<br/>Core engine"]
        CommonUI["ReCap.CommonUI<br/>Avalonia controls/theme"]
    end
    subgraph apps["Executables"]
        CLI["AssetData.Parser.CLI<br/>batch parse / export"]
        Editor["AssetData.Parser.Editor<br/>Avalonia desktop"]
        Wiki["AssetData.Parser.Wiki<br/>doc generator (this wiki)"]
    end
    CLI --> Core
    Wiki --> Core
    Editor --> Core
    Editor --> CommonUI
    Core -.->|reads| Pkg[("AssetData_Binary.package")]
    Core -.->|embedded| Reg["reg_type.txt / reg_file.txt"]
Loading

Dependency direction is clean: every consumer depends on Core; Core depends on nobody.

The Core engine

Type registry and the catalog DSL

A fluent DSL describes each format. DataType is an enum whose values are the FNV-1a hashes of the client's canonical type names (sentinels + value types — see Binary Format). At AssetParser construction, every AssetCatalog subclass is discovered by reflection, its Build() runs, and its struct/enum definitions merge into the global TypeRegistry.

sequenceDiagram
    participant P as AssetParser()
    participant A as Assembly
    participant C as AssetCatalog subclass
    participant R as TypeRegistry
    P->>A: find all AssetCatalog subclasses
    loop each catalog
        P->>C: instantiate -> ctor runs Build()
        C-->>R: register structs / enums
    end
    P->>R: ResolveEnumReferences()
Loading

TypeRegistry.FindTypeByHash(uint) is the heart of dispatch — the direct analogue of the client's AssetTypeRegistry::FindTypeByHash. Element-type hashing is centralized in one place (WireHash); the original had three separate FNV implementations.

The parse pipeline

AssetParser.Parse(bytes, rootStruct, headerSize) walks the struct's fields in declaration order, reading scalars from the header and reserving variable data from the blob via a BlobReader cursor. Each field resolves through FindTypeByHash: registered struct -> recurse; sentinel -> handle the wire shape; value type -> read inline. This is the C# analogue of DeserializeObject (0x009cd2c0). Errors are wrapped with field + struct + offset context for diagnostics.

The output: AssetValue tree

The parser produces an immutable POCO tree:

AssetValue
├── StructValue        (TypeName, children)
├── ArrayValue         (element values)
├── StringValue        (Kind: Asset | Key | CharPtr | Char)
├── NumberValue        (OriginalType, value)
├── BoolValue
├── EnumValue
├── VectorValue        (Vector2/3/4, Orientation)
├── LocalizedStringValue
└── NullValue

No INotifyPropertyChanged, no ObservableCollection — just data. Walk it with a switch on the node type.

The editor adapter (L2)

The editor wraps each L1 node in an EditorNode that adds INotifyPropertyChanged for two-way binding:

var value      = await Task.Run(() => assetService.LoadFile(path)); // L1 (Core)
var editorRoot = EditorTreeBuilder.Build(value);                    // L2 (Editor)
ViewModel.SetRoot(editorRoot);

XML export round-trips back through L1 (EditorToValue.Convert -> AssetSerializer.ToXml), so user edits are preserved. The editor uses Microsoft.Extensions.DependencyInjection and consumes ReCap.CommonUI for controls and theming.

End-to-end data flow

sequenceDiagram
    actor U as Consumer (Editor / CLI)
    participant DR as DbpfReader
    participant AP as AssetParser
    participant T as AssetValue tree
    U->>DR: GetAsset("ZelemBoss.phase")
    DR->>DR: resolve name -> ResourceKey, read, RefPack decompress
    DR-->>U: byte[] payload
    U->>AP: Parse(bytes, "phase", headerSize)
    loop each field
        AP->>AP: read header / reserve blob
    end
    AP-->>U: root AssetValue
    U->>T: navigate, export, or wrap in EditorNode
Loading

Stack

.NET 10, C# 14. Central package management via Directory.Packages.props; shared TFM/LangVersion/Nullable via Directory.Build.props. Avalonia 11.3 and CommunityToolkit.Mvvm power the editor; the Core library has zero third-party dependencies.

See also

Darkspore AssetData

Base

Guides

Catalog Assets

Structures
Enums

Clone this wiki locally