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:
Root element (all files): D20Character
Direct children observed under the root (union across files):
CharacterSheet– the current state of the characterLevel(repeats) – per-level audit trail (what rules/items were gained)textstring(lots) – named constants or key/value strings used by the builderD20CampaignSetting(optional) – e.g., Forgotten Realms, Dark Sun, etc.Grabbag(rare) – ad-hoc rules container
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),AgeValues are text; no special attributes.
-
AbilityScores Six elements:
Strength,Dexterity,Constitution,Intelligence,Wisdom,CharismaEach has@score(no inner text). There aren’t separate nodes for modifiers here; those appear inStatBlock. -
StatBlock A large collection of
Statnodes representing derived numbers (AC, defenses, skills, HP, etc.).-
Stathas@valuefor 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 onstatadd: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 likewearing,not-wearing,conditional, and occasionallyrequiresorStringThis 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.)
RulesElementattributes:type,name,url,internal-id(e.g.,ID_FMP_SKILL_1),legality, andcharelem(the same 8-hex id used bystatadd)- Child
specific @nameelements 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
RulesElement→RulesElement(links to the underlying power entry)
- Leaves with text (no attributes):
-
specific @namechildren underPower(notWeapon) capture metadata likePower Usage(At-Will/Encounter/Daily) andAction Type.
-
-
-
LootTally
-
loot @name @count @equip-count @augment @Weight @ShowPowerCard(each represents an item instance)- Nested
RulesElement(and sometimesRulesElement/RulesElement) to attach item definitions and specifics
- Nested
-
-
Journal, Companions Often empty in these files, but the nodes are present.
Each level node bundles the things you took or gained at that level:
RulesElement(same attributes as above)loot(same shape as underLootTally)- Sometimes
UserEditwith its ownRulesElementchildren
This gives you a transactional history that you can correlate with statadd @Level to see when a bonus appeared.
textstring @namewith 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.
- Optional, with
@nameonly in these files.
- Rare; saw
RulesElementand sometimesrules/grant @name @type— ad hoc extras.
RulesElement/@internal-id— a human-friendly identifier likeID_FMP_FEAT_123RulesElement/@charelem— an 8-hex character-element ID (e.g.,2c7ebef8) that shows up inStatBlock/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 valueStatBlock/Stat/alias/@name— the canonical string names you’ll want for UI/labeling
- 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). specificnodes act as key/value pairs scoped to their parentRulesElement(orPower), with@nameas 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).
You’ll likely want to normalize to a few tables/objects. Here’s a clean, loss-less split that balances utility and simplicity:
- Character (1 row)
- From
CharacterSheet/Details: name, level, experience, alignment, etc.
- Abilities (6 rows)
- ability, score
- Stats (N rows)
- stat_id (generated), label(s) (from
alias), value (fromStat/@value)
- 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.
- RulesElements (K rows, canonical)
- internal_id, charelem, type, name, url, legality
- RulesElementSpecifics (child rows): name, value
- Powers (P rows)
- name, usage, action_type, plus any other
specifickeys you care about - PowerWeaponLines (child rows): attack_stat, attack_bonus (string), defense, damage (string), damage_type, crit… & the text blobs for components
- Items / Loot (I rows)
- name, count, equip_count, augment, weight, show_power_card
- optionally joined to RulesElements by nested
RulesElemententries
- Levels (L rows)
- index or level number
- child collections of RulesElements gained and Loot gained there
- (Optional) UserEdits
- TextStrings (T rows)
- name, text — useful for UIs/tooltips but not required for core mechanics
Keys & joins
- Prefer
RulesElement.@internal-idas primary key for the rule itself; usecharelemto join an instance of that rule (as applied to the character) with stat contributions.
-
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
Statentries carry multiplealiasvalues (synonyms). You can pick one canonical label (e.g., prefer simple names likeAC,Fortitude,Reflex,Will) and keep the rest as alternates. Or keep a 1-to-many mapping if you need robust search.
- Some
-
Attribution of bonuses
- Use
statadd/@charelem→RulesElement/@charelemto attach contributions to feats/items/powers where possible. - When
typeis missing or generic, thecharelemjoin still gives you the provenance.
- Use
specificappears 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 onstataddbut 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.