-
Notifications
You must be signed in to change notification settings - Fork 2
Asset System
How the retail Darkspore client (Darkspore.exe) loads its assets, reverse-engineered from the binary. AssetData.Parser is a faithful C# mirror of this system, so understanding the original is the best way to understand the parser. The companion pages Binary Format and Parser Architecture cover the wire format and the C# implementation respectively.
Source: a Ghidra decompilation of the
Asset*namespaces inDarkspore.exe— 337 functions across 10 namespaces. Of those, only 17 are infrastructure; the other 320 are auto-generated reflection code, two stubs per format.
The client ships a single global asset runtime backed by:
-
one generic recursive parser (
AssetParser::DeserializeObject), and -
one global catalog (
AssetCatalog, a singleton).
Every asset format — Noun, Level, Markerset, ability, and ~140 others — is a pair of auto-generated reflection stubs (AssetType::Foo + AssetData::Foo) that, at startup, calls AssetTypeRegistry::Register to describe its fields. No format gets its own loader. The parser dispatches on a small, fixed set of FNV-1a-hashed wire-shape sentinels.
The practical consequence — for both the client and this port — is that adding a format is data, not code: you describe its field table once and the generic parser handles the rest.
| Namespace | Funcs | Role | Mirrored in C# by |
|---|---|---|---|
AssetCatalog |
47 | Master registry / database singleton (g_GlobalManager). |
(server-side concern; the parser returns trees) |
AssetTypeRegistry |
4 | Linked list of every registered type; binary-search index. | TypeRegistry |
AssetType |
140 | Per-format reflection stubs (one per format). |
AssetCatalog subclasses under Catalog/
|
AssetData |
137 | Twin stubs that emit the field-descriptor table. | the Build() override of each stub |
AssetLoader |
4 | Path build + file read + deserialize entry point. |
AssetService / DbpfReader
|
AssetParser |
1 |
DeserializeObject — the generic recursive deserializer. |
AssetParser.Parse |
AssetObject |
1 |
CreateInstance — allocator + live-list insertion. |
(not modeled — C# returns POCO trees) |
AssetCache |
1 |
FindCachedAsset — hash-keyed live-object lookup. |
(not modeled — re-parses on demand) |
AssetDestructor |
1 |
DestroyObjectRecursive — symmetric tear-down. |
n/a (GC) |
Everything is keyed by FNV-1a 32-bit, case-insensitive (the client lowercases input before hashing):
uint FNV1a_Hash(byte* data, int length, uint seed /* = 0x811c9dc5 */) {
for (byte* p = data; p < data + length; ++p)
seed = seed * 0x1000193 ^ *p;
return seed;
}Type names, field names, and asset names all run through this. Wire-shape sentinels are themselves just FNV-1a hashes of canonical type names ("array", "Nullable", "char*", …) — they collide with no real struct hash by construction. The full sentinel table is on Binary Format.
flowchart TD
A[C runtime startup<br/>static initializers] --> B["AssetData::Foo + AssetType::Foo<br/>per format"]
B --> C[AssetTypeRegistry::Register<br/>name, hash, fields, count, size]
C --> D[push onto g_AssetTypeRegistryHead]
D -->|all types registered| E[InitializeAll]
E --> F[Pass 1 — IndexType<br/>build field hash buckets, sum flattened size]
F --> G[Pass 2 — BuildTypeMetadata<br/>recursive FNV structural fingerprint]
G --> H[AssetCatalog::Create + Initialize]
H --> I[LoadCatalogFile<br/>catalog_LANG.bin]
I --> J[ProcessCatalogItem per entry]
Each registered type is a fixed-layout descriptor record carrying its name, type hash, an array of field descriptors, the instance size, and a recursive structural fingerprint (BuildTypeMetadata) — a hash that flips whenever any field name, type, offset, order, or nested layout changes. The client uses it as a schema-version / disk-cache-invalidation key. C# encodes the same field tables via the catalog DSL (see Adding a Format).
flowchart TD
A[GetAsset name] --> B[FindCachedAsset<br/>FNV1a name -> bucket]
B -->|hit| Z[return cached object]
B -->|miss| C[CreateInstance]
C --> D[strip extension, FNV1a remainder = nameHash]
D --> E[FindTypeByHash extHash]
E -->|none| X[unknown format]
E -->|ok| J[LoadBinaryFile]
J -->|"mode 0"| K[fopen + fread raw bytes]
J -->|"mode 1"| L[stream from DBPF package]
K & L --> M[LoadAndParseAsset]
M --> O[DeserializeObject<br/>buffer, type, base]
O --> Z2[status = loaded]
The client picks one of two byte sources based on a global flag:
-
mode 0 — direct
fopen/freadof a loose file on disk (e.g._Generated/foo.bin). -
mode 1 — stream the bytes out of a DBPF package (
AssetData_Binary.package).
AssetData.Parser covers both: DbpfReader handles the DBPF/DBBF wrapper (including RefPack/EA-QFS decompression), and AssetParser handles the per-entry payload. See Binary Format for the DBPF and catalog-file layouts.
DeserializeObject walks the field descriptors of a type. For each field it calls FindTypeByHash(field.typeHash):
- registered struct -> recurse into it on the same byte stream;
- sentinel hash -> handle that wire shape inline (string in the blob, nullable indicator, array of N elements, …);
- value-type hash -> copy the fixed number of raw bytes.
flowchart TD
A[DeserializeObject] --> B[for each field]
B --> D[FindTypeByHash field.typeHash]
D -->|registered struct| E[recurse]
D -->|Asset| F[resolve name string -> live object]
D -->|Nullable| G[indicator; if set, recurse child in blob]
D -->|Key| H[indicator; if set, read key string]
D -->|CharPtr / Char| J[read string from blob]
D -->|LocalizedAssetString| I[two indicators -> up to two strings]
D -->|Array| K[count x stride; dispatch per element type]
E & F & G & H & I & J & K --> L{more fields?}
L -->|yes| B
L -->|no| M[done]
A key performance detail the port preserves: strings, blobs, and sub-structs are pointers into the original buffer, not copies. The client's destructor (DestroyObjectRecursive) relies on this — it only frees pointers that fall outside the asset buffer. C# sidesteps all of this with the GC, but the parse logic is otherwise 1:1.
Two sentinels both reference other assets, but with different timing:
-
Asset— "resolve now": the parser immediately loads (or instantiates) the referenced object and stores a live pointer. -
Key— "store the hash, resolve later": the field holds a deferred reference string, resolved on demand.
This is why the client tolerates an O(n²)-ish cross-reference walk at warm-up, while a lazy consumer can treat everything as a Key and resolve on access.
An alternative server port (dalkon's C++ NounDatabase) hand-rolls eight per-format loaders. That name is a historical misnomer — Noun is just 1 of 143 formats; the database was named after the first one tackled, then seven more were bolted on. AssetData.Parser instead mirrors the client's design: one generic parser, formats as data. The client itself runs exactly one parser (DeserializeObject, ~183 lines). Replicating per-format loaders would invent complexity the original never carried.
- Binary Format — the full sentinel/value-type tables, array dispatch, type-descriptor layout, DBPF and catalog-file formats.
- Parser Architecture — how this maps onto the C# classes.
- Catalog Reference — every format currently described, generated from the schema.
Structures
- ability
- affix
- AffixTuning
- AIDefinition
- AudioTriggerDef
- cAffixDifficultyTuning
- cAgentBlackboard
- cAICondition
- cAIDirector
- cAINode
- cAnimatedData
- cAnimatorData
- cAssetProperty
- cAssetPropertyList
- cAssetQueryString
- Catalog
- CatalogEntry
- cAttributeData
- cAudioEventData
- cCameraComponent
- cCameraComponentData
- cCinematicView
- cCombatantData
- cControllerState
- cDecalData
- cDoorDef
- cEffectEventData
- cEliteAffix
- cGambitDefinition
- cGameObjectGfxStateData
- cGameObjectGfxStates
- cGfxComponentDef
- cGraphicsData
- cGrassData
- ChainLevel
- ChainLevels
- CharacterAnimation
- CharacterType
- cHardpointInfo
- Cinematic
- cInteractableData
- cKeyAsset
- cLabsMarker
- ClassAttributes
- cLayerPrefs
- cLineLightData
- cLobParams
- cLocomotionData
- cLongDescription
- cLootData
- cMapCameraData
- cNewGfxState
- cOccluderData
- CollisionVolumeDef
- CombatantDef
- CombatEvent
- Condition
- cParallelLightData
- cPointLightData
- cPressureSwitchDef
- cProjectileParams
- CrystalDef
- CrystalDropDef
- CrystalLevel
- CrystalTuning
- cSpaceshipCameraTuning
- cSPBoundingBox
- cSplineCameraData
- cSplineCameraNodeBaseData
- cSpotLightData
- cSwitchDef
- cThumbnailCaptureParameters
- cToolPos
- cVolumeDef
- cWaterData
- cWaterSimData
- DifficultyTuning
- DirectorBucket
- DirectorClass
- DirectorTuning
- EditorPrefs
- EliteNPCGlobals
- EventListenerData
- EventListenerDef
- ExtentsCategory
- GameObjectGfxStateTuning
- Gfx
- GravityForce
- InteractableDef
- labsCharacter
- labsCrystal
- labsPlayer
- Level
- LevelCameraSettings
- LevelConfig
- LevelKey
- LevelMarkerset
- LevelObjectives
- LocomotionTuning
- LootData
- LootPreferences
- LootPrefix
- LootRigblock
- LootSuffix
- MagicNumbers
- Markerset
- NavMeshLayer
- NavPowerTuning
- NonPlayerClass
- Noun
- NPCAffix
- ObjectExtents
- objective
- OrbitDef
- Phase
- PlayerClass
- PopupTip
- ProjectileDef
- SectionConfig
- ServerEvent
- ServerEventDef
- SharedComponentData
- SpaceshipSpawnPointDef
- SpaceshipTuning
- SpawnPointDef
- SpawnTriggerDef
- sporelabsObject
- TeleporterDef
- TestAsset
- TriggerVolumeComponentDef
- TriggerVolumeDef
- TriggerVolumeEvents
- UnlockDef
- UnlocksTuning
- WeaponDef
- WeaponTuning
Enums
- cGfxComponentDef.gfxType
- cHardpointInfo.bodyCap
- cHarpointInfo.type
- cLabsMarker.navMeshSetting
- cLabsMarker.shadowed
- cLabsMarker.type
- CollisionShape
- CrystalDef.rarity
- CrystalDef.type
- cVolumeDefShape
- EditorPrefs.cameraMode
- EditorPrefs.transformSpace
- gfxPickMethod
- LevelType
- LocomotionType
- Markerset.condition
- NonPlayerClass.creatureType
- NonPlayerClass.dropType
- NonPlayerClass.mNPCType
- NounType
- phaseType
- PhysicsType
- PlayerClass.creatureClass
- PlayerClass.creatureType
- PlayerClass.descriptionTag
- PlayerClass.homeworld
- PlayerClass.primaryAttribute
- presetExtents
- ProjectileDef.targetType
- SpawnPointDef.sectionType
- spawnTeamId
- targetType
- triggerActivationType
- TriggerShape
- type
- UnlockDef.unlockFunction
- UnlockDef.unlockType