How this site’s data was extracted from the game — tools, pitfalls, and replication steps for future game updates.
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.
| Source | What it is | Trust |
|---|---|---|
resources.assets runtime dump | Read the game's own asset files directly with UnityPy | Ground truth. Always prefer this |
| AssetRipper YAML export | ExportedProject/Assets/… YAML per asset | Good for most assets, but see §3 failure modes |
| Decompiled C# | ExportedProject/Assets/Scripts/Assembly-CSharp/ | Logic/formulas: reliable. Field initializer VALUES: DO NOT TRUST |
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).
| Tool | Version used | Purpose |
|---|---|---|
| AssetRipper | 2026 build | Bulk YAML export + decompiled C# + icons |
| Python 3.14 + UnityPy 1.25.2 | pip | Direct reads of resources.assets / level* files |
| TypeTreeGeneratorAPI | pip | Generates type trees from the game DLLs for UnityPy |
| PyYAML | pip | Parsing exported Unity YAML (with caveats, §3) |
| .NET SDK (builds net48) | 10.x | scripts/odt/ — Odin blob decoder using the game's own DLLs |
Game paths (adjust per machine):
C:\Program Files (x86)\Steam\steamapps\common\Stolen Realm\Stolen Realm_Data…\Stolen Realm_Data\Managed (needs Assembly-CSharp.dll, Sirenix.Serialization.dll, UnityEngine.CoreModule.dll, …)> Scripts in scripts/ contain absolute paths from the original workstation — fix the > path constants at the top of each script before running.
[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).PartyCountRollModifiers exported as feffff/ffeffff/… (invalid hex with slashes). Read scene MonoBehaviours with UnityPy instead (§6).[Dusk King] Brain Freeze → _Dusk King_ Brain Freeze, Ymir Rune, Courage → Ymir Rune_ Courage. When matching exported filenames to runtime m_Names, normalize [/]/, → _.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.scripts/extract_runtime_assets.py is the workhorse. Flow:
UnityPy.load(<game>\resources.assets)TypeTreeGenerator(unity_version).load_local_dll_folder(<game>\Managed)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.gen.get_nodes_up("Assembly-CSharp.dll", fullName) → patch the tree (below) → obj.read_typetree(nodes, check_read=False) per object.managedReference nodes: rename m_Type to anything neutral. On disk each managed reference is just an SInt64 rid; the reader special-cases the type name and errors ("Failed to get ref type node"). Drop the trailing references (ManagedReferencesRegistry) top-level field — it's always last.string[] / List<string> emitted as single string: desyncs the parse wherever the array is non-empty, and the misparse fabricates giant lengths (multi-GB allocations, looks like a hang). Scan the decompiled sources for public string[] X / public List<string> X declarations per class and wrap those string nodes into proper vector → Array → (size, string data) subtrees. See string_array_fields() + patch_string_arrays() in the script.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:
Sirenix.Serialization.dll — the public GitHub format docs apply.PartyEvent/Shopkeeper/CraftingRecipe blobs contain only the object's Guid — no logic. Don't assume a blob hides secrets.ActionInfo (Targets + Effects with damage expressions), ActionStatusInfo (Effects for ground/DoT statuses).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:
| Pitfall | Fix |
|---|---|
.NET 5+ lacks AssemblyBuilder.DefineDynamicModule(string,bool) (Odin's emitter) | target net48 |
Uninitialized UnityEngine.Objects read as "fake null" (m_CachedPtr == 0), Odin throws ArgumentNull | reflection-set m_CachedPtr to any nonzero value |
Odin logs via UnityEngine.Debug → native icall → SecurityException masks real errors | replace Debug.unityLogger.logHandler with a managed ILogHandler |
| "FieldInfo must be a runtime FieldInfo" emit error | harmless; Odin falls back to reflection formatters |
| External refs | create 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.
# 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).
| Check | Expected |
|---|---|
Medusa PersonalLoot | contains Medusa Staff at 10% |
Bottled Anger status attributeEffects | DamageMod +20 / DamageReduction −10 (matches its description text) |
| Fireball action | Cost 2 AP, Cooldown 3, range 7, blast 2, SpellPower("Fire") * 1.1 |
| Crit chance curve | rating 1→4%, 50→30%, 300→100% (piecewise linear, GetMultipler in GlobalExtensions.cs) |
| items.json vs runtime ItemInfo/WeaponInfo | 0 field mismatches (script the comparison; we did rarity/stats/ratios/levels) |
Documented event roll limits vs PartyEvent.json | all match (ignore RollLimit 1 follow-up branches — always-pass catch-alls) |
DeckRandomizer.cs); profile save (GlobalSaveData.LastVisitedEvents, rolling 100) pre-suppresses across runs.battleOverrideInfo.VictoryEvent chains Strength→Honor→Courage), finale forced before the island boss by the quest status's ForcedEventBeforeBoss. Event blobs hold no conditions — event gating is all plain YAML (requiredStatusesToSpawn, option requiredStatuses, EventStatus.Forced*).Character.GetLootDrop); gambling is the only random-drop Epic source (25/50/15/8/2 rarity %).CharacterInfo.Overrides = per-skill cooldown/mana/damage multipliers; merged into enemies.json.CraftingManager), Mythic rarity, 3 fortunes (Avenging Ancestors, Everlasting Sacrifice, Master of the Blade), The Spell Council Returns event, Ymir Honor/Strength "- Victory" events.MinLevel gates enemy spawns; MaxLevel is ignored (IsInLevelRange).Full changelog of corrections: docs/CHANGES.md. Working notes: docs/DEV_NOTES.md.