Entity values
What it is
Beyond the few stats with plain fixed offsets on the hero controller (HP at
+0x15c8 — see Combat & damage), most dynamic per-entity
values — combat modifiers, “dream shards on damage”, run meta, scaling
coefficients — live in a generic keyed value store. Each value is addressed
by a 32-bit key (same id space as gameplay-bus event ids). This is the engine’s
oCEntityValue system, and it is the read surface a mod uses to inspect any
named value.
Read path
valueCtx = *(hero + 0x2f8) # the entity's value contextstore = *(valueCtx + 0x4c8) # the value map (0 => no values)EntityValue_Lookup(store, &out, crcKey) # out is a ~0x20-byte unionEntityValue_Get (FUN_1403c71e0) wraps the +0x4c8 deref;
EntityValue_Lookup (FUN_1407481d0) is the core lookup:
- Linear scan of an override array —
store+0xc0, count atstore+0xc8, 0x38-byte entries, key = int atentry+0x0. Holds runtime-modified values. - Fallback hash map at
store+0x80(Fibonacci hash, multiplier0xde5fb9d2630458e9) — the base/default values from the definition. - Miss → the union is initialised to value
0.
oCEntityValueUnion (~0x20 bytes)
vftable 0x140f8fed8 (base oISerializable::vftable 0x140efed08).
+0x00 vftable+0x08 type tag (4 = value stored inline at +0x10)+0x10 value (inline f32 when tag==4; else (tag & ~1) is a pointer to it)+0x18 u16 = 0x000aEngine read pattern (e.g. in Entity_ModifyHealth):
EntityValue_Lookup(store, &out, key);float* p = (float*)(out + 0x10);if (*(uint64_t*)(out + 8) != 4) p = (float*)(*(uint64_t*)(out + 8) & ~1ull);float value = *p;FUN_14082ca50(&out); // destruct the unionKeys are sequential ids, NOT name hashes
crcKey is a 32-bit value id. Observed: 0x173900d4, 0x173900d6,
0x188831a6, 0x12e831f3/0x12e831f4, 0x171c27b5. Two proofs they are
structured ids, not hashes of the value name:
- Adjacency —
0x12e831f3/0x12e831f4differ by 1;0x173900d4/0x173900d6by 2. Independent CRC32 hashes could never land on consecutive integers. - Computed base+index (decisive) —
Hero_GrantMagicalObjectreadsEntityValue_Lookup(store, &out, rarityIdx + 0x1a19e789)whererarityIdxis 0..5. The key is literally a base id plus an index.
So a mod cannot compute a value key by hashing a name — the key must come from the value’s definition (a GUID/id) or be discovered empirically (observe the immediate the engine routine uses).
For contrast, the engine’s string→id hash does exist and is plain CRC32 —
Id_HashString (FUN_14033f7a0) = crc32_pair(0, crc32(name)), poly
0x04C11DB7. But that scheme is for named events / interned name ids, not
these value keys.
Toward a mod-facing API
R.entity.value(name) (read-only) is buildable: resolve EntityValue_Lookup by
pattern (already symbols), take a numeric key, allocate a zeroed ~0x20-byte
scratch for out, call the lookup, read per the union layout, then destruct via
FUN_14082ca50. All reads are safe; the only sharp edge is the out buffer +
destruct. A write path goes through the override array and is a separate,
riskier item — not yet mapped.
See also
- Combat & damage —
Entity_ModifyHealthreads this store; the plain-offset HP mirror vs signal stats.