Rewards
Loader RewardDef_RegisterAssetLoader = FUN_140323b60 (label “Reward
definition”). Codec cooked_schemas.definitions.RewardDefinitionHandler
(rewarddef.json) — byte-stable round-trip over all 9 retail files
(test_rewarddef_roundtrip). Rows fully editable: items, entity refs, tier bands,
flag filters, min/max counts.
What this system is — and isn’t
Reward_GenerateAndDistribute runs at level load (“Level load - Generate
rewards”) and decides which reward ENTITY spawns at each reward spawn point —
chests, astrolabs, dream crystals (Objects_*\*.entity.ot).
It does not pick the talent/item cards offered when a chest opens or the
player levels up. That draw comes from the magical-object pool (the LiveOps
versiondef MO vector — see Custom items). So a
per-item/talent card ban lives in the MO pool; this system is the lever for
reward-object placement.
How the roll works
_InitAllRewards → Distribute → “Fill remaining rewards on random slot”.
Seeded (“Seed : {0} ; Base seed {1}”) and host-authoritative deterministic
(Multiplayer) — a per-peer runtime filter
desyncs MP, so edits must be data-level. After the roll, level load posts a
GENERATE_REWARDS oCGameNamedEvent via NamedEvent_Dispatch and drains the
context queue (EventQueue_Drain, ctx+0x1c0).
Candidates are level entities carrying
oCDtRewardEntitySelectorToSpawnEntityCpnt (“reward spawners”). Per spawner a
value f32 is lazily rolled and cached at spawner+0x68. A spawner matches an
oCItem row iff:
oCItem.min <= value <= oCItem.max(runtimeoCItem+0xb0/+0xb4)CustomFlagList_ContainsAll(settings+0x100, oCItem+0xc0)— required flags!CustomFlagList_ContainsAny(settings+0x100, oCItem+0xd8)— excluded flags
Flag lists are string sets: {char* name, u32 len} entries, 0x18 stride, data at
+0x8, count at +0x10; compared by length + memcmp. Every shipping rewarddef
has all flag lists empty — the mechanism exists but retail data doesn’t use it,
so a mod can claim flag names freely.
Cooked grammar
Recovered from the actual deserializers and byte-verified against the
Camp_Rewards_Avalon and Refugees files. Three structural facts apply to all
cooked defs:
AABB1111/AABB2222are section separators thatcooked.parsealready strips — not node open/close inside a payload. Nested sub-objects each get their own section; references between them are u32 sub-object ids (order of sub-object sections).- Section 0 is a sub-object class directory:
{u32 count, count × u32 class-table-index}, one entry per sub-object section, naming its class. - Field presence is version-gated per class via the header class table
(
Serializer_GetClassVersionby class hash — the codec parses these asclasses[].version_*). A typed codec must gate optional fields the same way.
oCDtRewardDefinition (hash 0x176f164e, LAST section): u32 res? + u8 base_a + u8 base_b # oCDtDefinition base (v1+ bools @+0x285/+0x284) u32 count, count × u32 sub-object id # -> the oCType rows (runtime vector @def+0x288) v1+: u8 # unpinned bool
RewardDefInternal::oCType (0x176f4fdc): u32 count, count × u32 sub-object id # -> oCItem rows ({ptrs @+0x8, count @+0x10}) v1+: u8 use_float; if 0: u32 n (min=max=n) else f32 legacy_weight (1.0) v2+: u32 min_count (+0x18), u32 max_count (+0x1c)
RewardDefInternal::oCItem (0x176f5023): resource-ref { lstr "EntitySettings", lstr entity path } # @+0x8 f32 value_min (+0xb0), f32 value_max (+0xb4) # tier band over 0..1 roll v1+: inline oCCustomFlagFilter (+0xb8) # required @+0xc0, excluded @+0xd8 v2+: resource-ref (+0x40) # JSON _ref_b — LOCKED-variant entity on chest rows v4+: resource-ref (+0x78) # JSON _ref_c — empty in retail v5+: u32 enum <7 # JSON _kind — 1=chest, 3=astrolab, 4=dream crystal
oCCustomFlagFilter (0x15a9d9bf): inline oCCustomFlagList required (+0x8), excluded (+0x20)oCCustomFlagList (0x15a9d9be): u32 count, count × lstr
candidate side — oCDtRewardEntitySelectorToSpawnEntityCpntSettings (0x1740e3e9),cooked in the reward object's *.entity.ot settings: v<3: legacy discarded resource-ref v1+: inline oCCustomFlagList @+0x100 # spawner flags, matched vs the oCItem filter v2+: u32 enum <4 @+0xf8, v3+: u32 enum <2 @+0xfc # unpinned (likely value-source mode)The “Reward type {}” debug index is the position in the oCType row list; a
“category” is an oCType row’s oCItem set.
Ban levers (data-level, MP-safe)
- Ban a reward object — remove its index from every
oCTyperow that lists it, or setvalue_min > value_max. - Ban a category — remove/empty an
oCTyperow, or zero its min/max counts. - Flag-based suppression — add a flag string to an
oCItemexcluded list; only spawners whose settings carry that flag are suppressed. The spawner side must be authored too, so this is for mod content, not for banning retail objects. - Per-item/talent card ban — not this system: remove the entry from the
LiveOps
versiondefMO vector. That de-registers it from the offer pool but also hides it from the compendium.
SDK kind
reward (sdk/kinds/rewards.py) takes base + ban (entity substring → drop
from categories; a locked-variant _ref_b match clears the ref, so chests spawn
unlocked only) + counts ({category: [min, max]}; [0,0] bans the category).
It emits an override at the retail decoded path, not an additive def.
Why the type-count ban did NOT work in-game
A playtest shipped a reward mod emptying the basic-chest reward type
(min=max=0, items=[]) across all 6 Camp_Rewards defs. The output was
byte-correct and installed — chests still spawned. Decompiling the roll
handler explains it: there are two independent placement blocks with two def
sources.
- Block 1 — reward_types-driven, count-gated. Walks a def’s reward_types
vector at
def+0x288/+0x290. Per type:min_count @type+0x18,max_count @type+0x1c; candidate items fromtype+0x08/+0x10; each item has an exclude byte atitem+0x30(nonzero ⇒ skipped) and a tag match atitem+0xc0/+0xd8vsspawner+0x100. Emptying a type does yield 0 spawns here. But the def list it walks is built from globals — a runtime reward registry, not theCamp_Rewardsfile pointer directly. - Block 2 — guaranteed/forced, count-BYPASSING. Uses a different def
(scene-context
param_1[0x15]= game-context+0xa8) with its own collections at+0xe0/+0xf0/+0x110and count loops at+0x70/+0x7c. It never reads reward_types min/max, so a “guaranteed” chest ignores the type-count edit.
Open
- Runtime loader dump to close this: hook the roll handler, log the Block-1 def
list and the context
+0xa8def, dump each one’s+0x288reward_types and whether the basic-chest type is present or guaranteed. That pins whether the failure is a def-identity miss (Block 1) or a Block-2 placement. - Prototype versiondef MO-vector entry removal for the card-level ban, and check whether the compendium side effect is acceptable.