Skip to content

Instantly share code, notes, and snippets.

@mbutler
Created September 1, 2025 06:11
Show Gist options
  • Select an option

  • Save mbutler/addcf36ccee0a532f03d44162061f1ea to your computer and use it in GitHub Desktop.

Select an option

Save mbutler/addcf36ccee0a532f03d44162061f1ea to your computer and use it in GitHub Desktop.
How to read a .dnd4e file

Awesome — I’ve ingested all five .dnd4e XMLs and mapped out their structure. To make this concrete, I parsed each file, inventoried every element path, attributes, and common child patterns, then surfaced the results for you to browse:

  • Download the JSON schema summary — machine-readable union of structures across the five files
  • Download the union of XML paths as CSV

I also put three interactive tables in your workspace:

What the files look like (high-level)

Root element (all files): D20Character

Direct children observed under the root (union across files):

  • CharacterSheet – the current state of the character
  • Level (repeats) – per-level audit trail (what rules/items were gained)
  • textstring (lots) – named constants or key/value strings used by the builder
  • D20CampaignSetting (optional) – e.g., Forgotten Realms, Dark Sun, etc.
  • Grabbag (rare) – ad-hoc rules container

Deep dive by major section

/D20Character/CharacterSheet

Think of this as the “denormalized snapshot” for play:

  • Details Leaf nodes with the obvious things:

    • name, Level, Experience, Alignment, Player, Company, Companions, Appearance, Notes, Traits, Gender, Height, Weight, CarriedMoney, StoredMoney, Portrait (a path), Age Values are text; no special attributes.
  • AbilityScores Six elements: Strength, Dexterity, Constitution, Intelligence, Wisdom, Charisma Each has @score (no inner text). There aren’t separate nodes for modifiers here; those appear in StatBlock.

  • StatBlock A large collection of Stat nodes representing derived numbers (AC, defenses, skills, HP, etc.).

    • Stat has @value for the computed total

    • Children:

      • alias @name — human-readable names & synonyms (e.g., AC, Armor Class, Strength modifier, HALF-LEVEL, etc.). I saw ~170 distinct alias names across the five files.

      • statadd — all the contributing parts to the total. Common attributes on statadd:

        • value (numeric), type (e.g., Ability, Feat, item, trained, Armor Penalty, Enhancement, …)
        • Level (the character level at which it applied)
        • charelem (an 8-hex character-element id that ties back to a rules element)
        • statlink (reference to another Stat), abilmod (flag indicating it’s an ability mod), plus conditional flags like wearing, not-wearing, conditional, and occasionally requires or String This is the key place to reconstruct “why does this number equal X?”
  • RulesElementTally A flat list of everything the character “has” (race, class, powers, feats, items, backgrounds, themes, etc.)

    • RulesElement attributes: type, name, url, internal-id (e.g., ID_FMP_SKILL_1), legality, and charelem (the same 8-hex id used by statadd)
    • Child specific @name elements provide name/value metadata (e.g., Power Usage, Action Type, Attack, Prerequisite, Market Price, Short Description, Role, etc.). I saw ~90+ distinct “specific” names used.
  • PowerStats

    • Power @name

      • Weapon (the stat line for a particular implement/weapon usage of the power)

        • Leaves with text (no attributes): AttackBonus, AttackStat, Defense, Damage, DamageType, HitComponents, DamageComponents, CritRange, CritDamage, CritComponents, Conditions
        • Optional nested RulesElementRulesElement (links to the underlying power entry)
      • specific @name children under Power (not Weapon) capture metadata like Power Usage (At-Will/Encounter/Daily) and Action Type.

  • LootTally

    • loot @name @count @equip-count @augment @Weight @ShowPowerCard (each represents an item instance)

      • Nested RulesElement (and sometimes RulesElement/RulesElement) to attach item definitions and specifics
  • Journal, Companions Often empty in these files, but the nodes are present.

/D20Character/Level (repeats many times)

Each level node bundles the things you took or gained at that level:

  • RulesElement (same attributes as above)
  • loot (same shape as under LootTally)
  • Sometimes UserEdit with its own RulesElement children

This gives you a transactional history that you can correlate with statadd @Level to see when a bonus appeared.

/D20Character/textstring (many)

  • textstring @name with text content — acts like a string table/constant bank that the builder uses during calculations and rendering (“100 gp”, “+2 Feat bonus – Staff Expertise”, etc.). These are not direct stats but are often the human-readable line items you see in power cards or bonus breakdowns.

/D20Character/D20CampaignSetting

  • Optional, with @name only in these files.

/D20Character/Grabbag

  • Rare; saw RulesElement and sometimes rules/grant @name @type — ad hoc extras.

Cross-references & identifiers (important for parsing)

  • RulesElement/@internal-id — a human-friendly identifier like ID_FMP_FEAT_123
  • RulesElement/@charelem — an 8-hex character-element ID (e.g., 2c7ebef8) that shows up in StatBlock/Stat/statadd/@charelem ➜ Use this to join bonus contributors to the concrete rule that granted them.
  • StatBlock/Stat/statadd/@statlink — soft reference to another stat if the contribution mirrors another value
  • StatBlock/Stat/alias/@name — the canonical string names you’ll want for UI/labeling

Data types & patterns

  • Most numbers are stored in attributes (e.g., Stat/@value, statadd/@value, AbilityScores/*/@score). Many “display” lines store text in elements (e.g., the whole hit/damage components for a power’s weapon entry).
  • specific nodes act as key/value pairs scoped to their parent RulesElement (or Power), with @name as the key and inner text as the value.
  • Collections are easy to recognize because the same child tag repeats many times under a parent (my report flags these).

Suggested parse model (stable & ergonomic)

You’ll likely want to normalize to a few tables/objects. Here’s a clean, loss-less split that balances utility and simplicity:

  1. Character (1 row)
  • From CharacterSheet/Details: name, level, experience, alignment, etc.
  1. Abilities (6 rows)
  • ability, score
  1. Stats (N rows)
  • stat_id (generated), label(s) (from alias), value (from Stat/@value)
  1. StatContribs (M rows)
  • stat_id → (FK to Stats)
  • value, type, level, charelem, statlink, flags (wearing, conditional, etc.)
  • This lets you reconstruct totals and show a “why” breakdown.
  1. RulesElements (K rows, canonical)
  • internal_id, charelem, type, name, url, legality
  • RulesElementSpecifics (child rows): name, value
  1. Powers (P rows)
  • name, usage, action_type, plus any other specific keys you care about
  • PowerWeaponLines (child rows): attack_stat, attack_bonus (string), defense, damage (string), damage_type, crit… & the text blobs for components
  1. Items / Loot (I rows)
  • name, count, equip_count, augment, weight, show_power_card
  • optionally joined to RulesElements by nested RulesElement entries
  1. Levels (L rows)
  • index or level number
  • child collections of RulesElements gained and Loot gained there
  • (Optional) UserEdits
  1. TextStrings (T rows)
  • name, text — useful for UIs/tooltips but not required for core mechanics

Keys & joins

  • Prefer RulesElement.@internal-id as primary key for the rule itself; use charelem to join an instance of that rule (as applied to the character) with stat contributions.

Tradeoffs & alternatives

  • Keep it nested vs normalize relationally

    • Nested (dict/JSON): mirrors the XML; fastest to build; great for 1:1 exports; poorer for queries like “show me all feat bonuses to Reflex”.
    • Relational (tables): ideal for analytics, UI lists, diffing between characters; requires careful joins and a small amount of denormalization.
  • Label resolution for Stats

    • Some Stat entries carry multiple alias values (synonyms). You can pick one canonical label (e.g., prefer simple names like AC, Fortitude, Reflex, Will) and keep the rest as alternates. Or keep a 1-to-many mapping if you need robust search.
  • Attribution of bonuses

    • Use statadd/@charelemRulesElement/@charelem to attach contributions to feats/items/powers where possible.
    • When type is missing or generic, the charelem join still gives you the provenance.

Edge cases you’ll want to handle

  • specific appears in multiple places (RulesElementTally, Power) and has no fixed schema — treat it as arbitrary key/value.
  • Some sections (e.g., Journal, Companions, D20CampaignSetting) may be absent or empty.
  • Item rules sometimes nest as .../RulesElement/RulesElement. Be tolerant of one or two layers of indirection.
  • “Wear state” and other conditionals (wearing, not-wearing, conditional) exist on statadd but may not be consistent across files.
  • A few numeric-looking strings are embedded in text nodes (e.g., heights like 5' 5"). Don’t force numeric parsing there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment