How every piece of game data in this repo was extracted, which tools were used, every pitfall we hit, and how to redo it when the game updates. Written July 2026 against Steam build 20244830 (Chaos Update era), Unity 2022.3.10f1, Mono scripting backend.


1. The Three Data Sources (and which to trust)

SourceWhat it isTrust
resources.assets runtime dumpRead the game's own asset files directly with UnityPyGround truth. Always prefer this
AssetRipper YAML exportExportedProject/Assets/… YAML per assetGood for most assets, but see §3 failure modes
Decompiled C#ExportedProject/Assets/Scripts/Assembly-CSharp/Logic/formulas: reliable. Field initializer VALUES: DO NOT TRUST

The compile-time-default trap (the #1 lesson of this project)

public float CritDamagePerDex = 4f; in decompiled code is a default, not the value the game uses. Unity serializes the real value in the asset/scene that instantiates the class, overriding the initializer. We shipped months of wrong formulas by reading initializers. Values that burned us: AbilityPwrPerMight (2→1), CritDamagePerDex (4→1.5), WeaponDamagePerLevel (0.20→0.23), HealthBase (30→100), maxArmorReductionPercent (80→90), PartyCountRollModifiers ([0,1,2,2,3,3][-2,-2,-1,-1,0,0]), and a dozen more.

Rule: any constant you cite must come from a serialized asset/scene instance, never from a .cs initializer. ScriptableObject values live in .asset files (e.g. Resources/global settings/GS 1.asset); MonoBehaviour values live in scene files (e.g. DiceRollingManager in level1).


2. Toolchain

ToolVersion usedPurpose
AssetRipper2026 buildBulk YAML export + decompiled C# + icons
Python 3.14 + UnityPy 1.25.2pipDirect reads of resources.assets / level* files
TypeTreeGeneratorAPIpipGenerates type trees from the game DLLs for UnityPy
PyYAMLpipParsing exported Unity YAML (with caveats, §3)
.NET SDK (builds net48)10.xscripts/odt/ — Odin blob decoder using the game's own DLLs

Game paths (adjust per machine):

> Scripts in scripts/ contain absolute paths from the original workstation — fix the > path constants at the top of each script before running.


3. AssetRipper export — failure modes we hit

  1. Classes with [SerializeReference] fields export as empty stubs. 668/669 actions/*.asset and 340/340 enemies/*.asset were 14-line husks (m_Script: {fileID: 0}, no data) because ActionInfo.AITargetChoices and CharacterInfo.SkillsAndAI[].AIOverrideInfo use [SerializeReference]. Detection: an exported MonoBehaviour asset with ≤15 lines is a stub. Fix: read those classes from resources.assets with UnityPy (§4).
  2. Some arrays are garbled in scene YAML. PartyCountRollModifiers exported as feffff/ffeffff/… (invalid hex with slashes). Read scene MonoBehaviours with UnityPy instead (§6).
  3. Filename sanitization. [Dusk King] Brain Freeze_Dusk King_ Brain Freeze, Ymir Rune, CourageYmir Rune_ Courage. When matching exported filenames to runtime m_Names, normalize [/]/,_.
  4. PyYAML octal trap (our bug, but export-format-induced): Unity writes enum-array scalars as packed hex (itemTypes: 04000000). YAML 1.1 parses leading-zero numbers as octal ints, destroying them. Quote them first: re.sub(r'^(\s+\w+:) ([0-9a-fA-F]{8,})\s*$', r"\1 '\2'", text, flags=re.M) then decode as little-endian uint32s per 8 hex chars.

4. Reading resources.assets directly (UnityPy pipeline)

scripts/extract_runtime_assets.py is the workhorse. Flow:

  1. UnityPy.load(<game>\resources.assets)
  2. TypeTreeGenerator(unity_version).load_local_dll_folder(<game>\Managed)
  3. Fast header scan of every MonoBehaviour: first bytes are m_GameObject PPtr (12B) | m_Enabled (u8+3 pad) | m_Script PPtr (12B) | m_Name (len-prefixed) — group objects by script, resolve MonoScript → class name once per script.
  4. Per target class: gen.get_nodes_up("Assembly-CSharp.dll", fullName) → patch the tree (below) → obj.read_typetree(nodes, check_read=False) per object.

Required tree patches (both bugs, or parses fail/corrupt)

Odin-serialized fields

Classes extending SerializedScriptableObject keep Unity-unserializable fields (interface arrays like IEffectInfo[] Effects, ITargetInfo[] Targets) in serializationData.SerializedBytes (binary Odin blob) with external refs in ReferencedUnityObjects. Everything else is normal typetree data. Notes:

Decoding blobs — scripts/odt/ (C#)

odt --batch <dump.json> <_names.json> <TypeFullName> <fields> <out.json> deserializes every record's blob using the game's own DLLs. Hard-won requirements:

PitfallFix
.NET 5+ lacks AssemblyBuilder.DefineDynamicModule(string,bool) (Odin's emitter)target net48
Uninitialized UnityEngine.Objects read as "fake null" (m_CachedPtr == 0), Odin throws ArgumentNullreflection-set m_CachedPtr to any nonzero value
Odin logs via UnityEngine.Debug → native icall → SecurityException masks real errorsreplace Debug.unityLogger.logHandler with a managed ILogHandler
"FieldInfo must be a runtime FieldInfo" emit errorharmless; Odin falls back to reflection formatters
External refscreate uninitialized ScriptableObject stubs per ReferencedUnityObjects entry, map index→name via _names.json

All 714 action + 397 status blobs decoded with zero failures. Every Effects entry in the game is a GeneralEffect { string Action } expression, e.g. TargetStored["FireDamage"] = Source.SpellPower("Fire") * 1.1f.


5. Pipeline run order (full refresh after a game update)

# 0. AssetRipper: export the game to full/ExportedProject (YAML + decompiled C# + icons)
# 1. runtime dumps (resources.assets → md/odin_extracted/raw/)
python extract_runtime_assets.py            # ActionInfo, CharacterInfo by default
python extract_runtime_assets.py PartyEvent Burst2Flame.ActionStatusInfo ItemInfo WeaponInfo EventStatus
python scan_names.py                        # path_id -> name map
# 2. plain-YAML settings (from the export)
python extract_globalsettings.py            # GS 1.asset -> global_settings.json
python extract_settings_assets.py           # difficulty/endgame/misc settings
python extract_shops.py                     # shopkeepers -> shops.json
python extract_crafting.py                  # crafting recipes
# 3. Odin blob decode (C#; build once with: dotnet build scripts/odt)
odt --batch raw/ActionInfo.json raw/_names.json Burst2Flame.ActionInfo "Targets,Effects,UseConditions" raw/ActionInfo_odin.json
odt --batch raw/ActionStatusInfo.json raw/_names.json Burst2Flame.ActionStatusInfo "Effects,TickTargets,EffectOverrides" raw/ActionStatusInfo_odin.json
# 4. merge/derive website data
python build_action_data.py                 # action_data.json + skill_actions.json
python build_loot_tables.py                 # personal + group loot
python build_enemies.py                     # enemies.json + Enemies.md
python build_status_data.py                 # enrich status_effects.json
python build_skills_enriched.py             # merge AP/range/damage into skills.json
python build_skill_statuses_md.py           # human-readable skill->status doc
python fix_items_meta.py                    # items.json scaling constants (runtime values)
# 5. legacy YAML extractors still valid for their domains:
#    extract_fortunes / extract_skills(+resolve_deps/rebuild_skills_md) / extract_items
#    extract_statuses / extract_enemy_mods / map_bosses / roll_checks

MonoBehaviour scene values (not in resources.assets): load level files — UnityPy.load(level0, level1, level2, globalgamemanagers.assets, resources.assets) so m_Script PPtrs resolve across files, then read the class as usual (that's how DiceRollingManager.PartyCountRollModifiers was recovered from level1).


6. Validation anchors (rerun these after any refresh)

CheckExpected
Medusa PersonalLootcontains Medusa Staff at 10%
Bottled Anger status attributeEffectsDamageMod +20 / DamageReduction −10 (matches its description text)
Fireball actionCost 2 AP, Cooldown 3, range 7, blast 2, SpellPower("Fire") * 1.1
Crit chance curverating 1→4%, 50→30%, 300→100% (piecewise linear, GetMultipler in GlobalExtensions.cs)
items.json vs runtime ItemInfo/WeaponInfo0 field mismatches (script the comparison; we did rarity/stats/ratios/levels)
Documented event roll limits vs PartyEvent.jsonall match (ignore RollLimit 1 follow-up branches — always-pass catch-alls)

7. Selected findings that took real digging (don't re-derive)

Full changelog of corrections: docs/CHANGES.md. Working notes: docs/DEV_NOTES.md.