Heroes
Heroes are the hardest playable kind because oCDtHeroDefinition has no
registered class UID — it lives as a nested definition inside a parent
SkillProfileDataSettings record. The hero is created and destroyed by the
parent’s deserializer, not by a top-level factory lookup.
The chain
Section titled “The chain”SkillProfileDataSettings (UID 0x186adbdf, size 0xd0, factory FUN_1403122f0) | embeds (typed field) voCDtHeroDefinition (no UID, ctor FUN_1403143b0, >= 0x900 bytes) | named slots resolve through voCTLibrary<oCDtHeroDefinition> (dtor FUN_14032deb0, acquire FUN_14032e960) (singleton address NOT located — see below)SkillProfileDataSettings derives from oISerializable (UID 0x1da16c).
| Class | UID | Size | Ctor | Dtor |
|---|---|---|---|---|
SkillProfileDataSettings |
0x186adbdf |
0xd0 |
registrar FUN_140192330, schema cb FUN_1403122f0 |
— |
oCDtHeroDefinition |
none | ≥ 0x900 |
FUN_1403143b0 |
FUN_140314930 |
oCTLibrary<oCDtHeroDefinition> |
— | — | acquire FUN_14032e960 |
FUN_14032deb0 |
The 29 named slots
Section titled “The 29 named slots”The ctor writes a long sequence of named-slot tuples plus two sub-record ctors.
Each slot is {char* name = &DAT_140eb46d0 (empty sentinel), u32 hash = 0x80000000 (unresolved), u32 flag}, most with an owner pointer naming the
library the name resolves in.
| Group | Slots | Owner | Inferred semantic |
|---|---|---|---|
Pre-ability pairs (+0x2b8, +0x2c8, +0x2f0, +0x300) |
4 | DAT_141447250 |
idle / locomotion animation IDs |
Ability blocks × 4 (+0x328, +0x398, +0x408, +0x478) |
12 | DAT_141446e18 |
4 abilities × {name A, name B, description key} — A likely the power id, B the upgraded variant |
Standalone (+0x4e8) |
1 | (none) | base entity / model ref |
UI spawner pair (inline, +0x500..+0x5d8) |
2 | DAT_141446f38 |
portrait / select-screen UI spawn |
Melody/talent × 4 (+0x5d8, +0x610, +0x648, +0x680) |
8 | DAT_141447250 |
4 melodies (or talent trees) × {name, alt} |
Late slot (+0x7e8) |
2 | DAT_1414470f0 |
voice bank / sound bank |
Total 29. The owner globals are oCMetaClass* records for the respective ID types
(animation, power, sound, UI) — they tell the asset loader which library to look
the name up in. Hash 0x80000000 marks the slot unresolved; the loader replaces
it with the real hash and swaps the pointer for a pooled string once the ID
resolves.
The per-owner mapping is inferred from position and count (4 abilities × 3 names matches the per-hero ability roster), not confirmed from a dumped record.
Other notable offsets: +0x288 installs oIGameProfileDataOwner with a 16-byte
GUID from CoCreateGuid; +0x6b8..+0x7d0 is a fixed vector of 5 × 0x38-byte
entries; +0x7d0 is another {ptr, count, cap} collection of 0x38-byte entries;
+0x828..+0x8a0 is an oCGameLockSettings + event-listener + flag-list complex
(the unlock condition); +0x2a0 is set to 1 at the end of the ctor as a
“fully initialized” marker.
Three parallel arrays
Section titled “Three parallel arrays”The dtor shows the parallel-pointer-array pattern plainly:
A : ptr=+0x8a0, count=+0x8a8, cap_flag=+0x8acB : ptr=+0x8b0, count=+0x8b8, cap_flag=+0x8bcC : ptr=+0x8c0, count=+0x8c8, cap_flag=+0x8ccEach loop body is the standard
for i in 0..count: entry->vftable[8](entry); entry->vftable[0x10](entry, 1) —
every entry is a polymorphic owning pointer with a virtual destructor. The extra
field is the capacity-allocated flag; when set, the buffer itself is released.
Allocation size is therefore at least 0x8d0, almost certainly rounded to 0x900
for the pool.
How the hero gets built
Section titled “How the hero gets built”Register_SkillProfileDataSettings sets up UID 0x186adbdf, size 0xd0, factory
FUN_1403122f0. The oCDtHeroDefinition name string is referenced twice from
data inside the field-schema table the registrar emits — i.e.
SkillProfileDataSettings declares a typed nested field of type
oCDtHeroDefinition. So the parent deserializer:
- Reads the parent’s
0xd0-byte preamble. - Hits the nested field marker → looks up
oCDtHeroDefinitionby name, not by UID. - Calls the hero ctor to construct it inline.
- Streams the named-slot values into the right offsets, replacing the sentinel.
- Streams the variable-length arrays A / B / C.
The acquire helper pulls an entry off the library’s free list at +0x170, runs
the ctor, and links it via the library’s +0x180 critical section.
The missing library singleton
Section titled “The missing library singleton”The oCTLibrary<oCDtHeroDefinition> singleton address is the one piece of
metadata still missing. The dtor is reached only via a scalar-dtor thunk that is
itself referenced exclusively as slot 0 of the library’s own vftable, and the
static-init that does _DAT_<addr> = vftable; InitializeCriticalSectionAndSpinCount(...) is unnamed — likely inlined into a
larger group init.
Until that singleton is located, runtime injection of new heroes through the
library is not possible. The path that does work is the parent record: emit a
cooked asset containing a SkillProfileDataSettings and let the normal level-load
deserializer build the hero.
Insertion recipe (cooked-asset path)
Section titled “Insertion recipe (cooked-asset path)”- Parent settings record —
0xd0bytes, UID0x186adbdfin the cooked header. The factory runs at load time to set the display name. - Embed the hero in the layout above: animation name pairs, 4 ability blocks (two name strings + a description key each), 4 melody/talent name pairs, portrait/voice slot, standalone model ref.
- Variable-length arrays — lengths + entries for A, B, C and the 0x38-stride vector. What A/B/C contain is not confirmed; most likely powers, talents and melodies as full nested records.
- Text bank — one i18n entry per ability description key plus the hero’s
display name, using the
RSMM_<modid>_<heroid>_<slot>key convention. - Hero pool — the level-load orchestrator reads
Map+0x720..0x728(random hero pool) andMap+0x738..0x748(played hero pool) at stage 0. A custom hero must be listed in at least one to be selectable in a run. There is no clean splice for this yet.
Unknown fields fall back to clone-from-base: copy a vanilla hero’s bytes verbatim and patch only the offsets above.
The runtime hero object
Section titled “The runtime hero object”Everything above is the definition — static data used to build a hero. At
runtime the playable hero is a different object: oCDtEntityCpntHeroController
(HeroController_Ctor), the hero controller component of the entity. This is
what the loader captures as the “hero pointer” for R.entity / R.combat.
| Offset | Field |
|---|---|
+0x15c8 |
current HP (f32, plain mirror) |
+0x15cc |
max HP (f32) |
+0x15d0 |
missing-health ratio (f32, default 1.0) |
+0x15d4 |
ratio divisor (f32, default 0.5) |
+0x1d80 |
HUD HP-mirror pointer (local player only) |
+0x1fc..+0x220 |
10-slot ability/input config (int tags {0,1,1,1,2,1,1,1,1,3}) |
Controller size ≈ 0x1e40.
Most hero stats are not plain fields. They are
oe::EntityCpntValueSignal<bool|int|float|oCVec3> sub-components — the ctor builds
dozens. Writing a signal fires a change notification on the gameplay bus, which is
why stat changes surface as named events. Dream shards are the exception (this
field was documented as HP until 2026-09-18): they are additionally
mirrored to the plain f32 at +0x15c8 for the hot path, which is exactly why
R.shards can read and write them directly while energy, cooldowns and speed live
inside signal objects with no fixed plain-float offset. Reaching those is a
per-signal RE job, not a constant lookup — see
Stats & XP for the keyed store that covers most of
them.
See also
Section titled “See also”- Anatomy of an entity — what an entity is, and the three places a number can live.
