Skip to content

random item properties — vanilla's weighted per-instance stat suffixes, end-to-end #31

Closed

Description

@Mathih13

Vanilla's "of the Bear" system IS weighted per-instance randomized stats, unimported. All wire
plumbing verified present: gtker exposes set_item_random_properties_id (+ property_seed +
enchantment) in the vanilla update mask, the item-query response carries random_property
(the client's <Random enchantment> preview line), and our loot body already has the
random_property_id u32 hardcoded 0 (codec/loot.rs:37).

Build: (1) IMPORT — ItemRandomProperties.dbc + SpellItemEnchantment.dbc → server mirror
tables (game_item_random_property / game_item_enchantment — id, suffix name, up to 3
enchant refs → stat kind/amount rows), and the dump's item_enchantment_template →
game_item_property_weight (the WEIGHTED roll table keyed by the template's RandomProperty
group); item_template.RandomProperty joins the imported columns (the import audit — old
work-item 213, private archive — lists it dropped). (2) ROLL — one weighted-pick fn (the
group_pick idiom) called at BOTH creation sites: the creature/chest loot roll and
E_CREATE_ITEM (crafting); stamp random_property_id END-appended on ItemInstance
(default 0 = plain). (3) FOLD — equipped_stat_bonus adds the property's stat rows
(mirror-table lookup; the enchant-overlay pattern). (4) WIRE — emit the id in: the
loot-response item block, the item CREATE object (set_item_random_properties_id),
trade/inspect passthrough, and keep template query random_property so craft previews show
<Random enchantment> natively — the blacksmith's mystery preview resolving at creation, for
free. Every touched ITEM_FIELD arm gets the OBJECT_FIELD_TYPE crash-class values pin
(danger-zones §1 — hence not agent-safe).

Done when: an imported suffix item (e.g. a Defias-dropped "of the ..." world drop) rolls
different suffixes across kills (sql distribution over N debug rolls matches the weight
table), the live client tooltip shows the suffix name + stats matching sql, the crafted
fixture previews <Random enchantment> and resolves on craft, and the stat fold moves vitals
(recompute assert).


RE-SCOPED IN (operator decision 2026-07-06, superseding the same-day out-of-scope call):
this IS a vanilla feature, and — clarifying the concern that prompted the earlier call —
vanilla does NOT implement it by seeding item variations. There is ONE template row per
suffix item; the suffix is a per-INSTANCE random_property_id resolved at read time by both
sides. That is precisely the no-duplication, DB-as-truth shape this codebase wants:
one imported template + one END-appended instance column + mirror tables for the stat fold +
a weighted roll at the two creation sites. Nothing in this item creates template copies; if
any implementation attempt reaches for per-variation template rows, that attempt is wrong —
the instance column is the identity. (For custom BEYOND-vanilla pools, see the rejected
custom-pool proposal — old work-item 219, private archive — and its synthetic-entry
correction; different mechanism, still out of scope.)


Migrated 2026-08-11 from the private tracker (spacetime-core#161; originally work-item 218).

Activity

  1. Mathih13 commented on Aug 22, 2026

    @Mathih13
    ContributorAuthor

    Spec

    Settled in a design interview on 2026-08-22. This replaces the build plan in the issue body. The differences are listed at the end.

    Model

    One Item Template per suffix item. No template copies, ever. The suffix is a per-instance value, random_property_id, that both sides resolve at read time. 0 means a plain item.

    An implementation that adds a template row per variation is wrong.

    Data

    Three new tables and one new template column. The importer fills all of them. All are plain SQL rows that a person can read and write.

    1. game_item_random_property. One row per property: property_id, its enchantment references (up to 3, per the DBC row), and suffix text. From ItemRandomProperties.dbc.
    2. game_item_enchantment. One row per enchantment effect: enchant_id, kind, amount, and a spell reference where the effect needs one. From SpellItemEnchantment.dbc.
    3. game_item_property_weight. The weighted pick table: pool id, property id, weight. From the dump's item_enchantment_template.
    4. game_item_template.random_property. END-appended u32, #[default(0)]. The pool id. The importer drops this column today (source column 114, per its own comment at importer/src/main.rs:395).

    Two END-appended instance columns, both u32 and #[default(0)]:

    • ItemInstance.random_property_id in module/src/items/tables.rs.
    • CorpseLoot.random_property_id in module/src/loot/mod.rs.

    rules::ENCHANTS goes away. enchant_stat reads game_item_enchantment instead, so the property fold and the enchant fold share one mechanism and one import. The hand-authored const says in its own comment that it stands in for this import. The import audit must assert that enchant 7745 and 7748 keep their current stat and amount, so equipped enchanted items do not move.

    Three traps, checked against docs/danger-zones.md:

    • The suffix string is safe because game_item_random_property is a new table. Danger zone 6 blocks a defaulted String on an END-append only, and a new table has no rows to backfill.
    • Danger zone 2's "what does the default mean as a value" test passes. Nothing indexes or filters on the new id columns, and 0 reads correctly for every existing row.
    • New tables need a gateway binding regeneration with --include-private, plus the post-regen hand-patch listed in danger zone 2.

    Stat kinds

    kind is our own vocabulary, not the DBC encoding. Stable integers, append-only, never reordered, documented in CONTEXT.md. The importer translates the DBC into it, so a person writing a modded row needs no DBC knowledge.

    The vocabulary covers every suffix family: the five primary stats, health, mana, the six resistance schools, armor, weapon damage, spell power, healing power, mana per 5, health per 5, crit, hit, defense, dodge, parry, block.

    The importer never drops a row it cannot map. An unmapped effect imports as kind 0, unknown, and folds as nothing. A mapping fix is then an importer change plus a reimport, with no schema change.

    The roll

    One pure weighted pick over the pool's rows, in the shape of group_pick (module/src/loot/mod.rs:160). The pure function takes the roll. The call site supplies ctx.random::<u32>() % 10000, matching the loot roll idiom.

    The roll runs in store_item (module/src/items/ops.rs:180). Every item birth already goes through it: grant, buy, loot, craft, buyback, starter kit. store_item takes an optional pre-rolled id. If the caller supplies one, it uses it. If not, and the template names a pool, it rolls. That is one roll site, not two.

    Loot rolls early because the Loot Window must show the suffix before pickup. roll_corpse_loot (module/src/combat/death.rs:309) stamps the id on the CorpseLoot row, and apply_take_loot passes that id into store_item. GameObject loot writes CorpseLoot rows through the same path, so chests need no separate work.

    The merge branch of store_item must not top up a stack whose property id differs. Suffix items are never stackable, so this is a guard, not a behaviour.

    The fold

    The property's rows add on top of the template stat and the enchant, in the overlay shape equipped_stat_bonus already uses.

    Full family support is in scope, in two Tickets.

    Tracer: the import, the roll, the two instance columns, the wire, and the fold for the kinds EquipStat covers today (strength, agility, stamina, intellect, spirit, crit, hit, armor).

    Second Ticket: the other families. Each one has a fold that exists and needs a gear term added. None of them is a new system.

    • Resistance. resistance_bonus (module/src/spell/math.rs:719) sums auras only. A gear term also makes the six ItemTemplate resist columns live, which the importer fills and nothing reads.
    • Weapon damage. weapon_profile_in_slot (module/src/combat/folds.rs:343) reads the template only.
    • Dodge, parry, block, defense. effective_dodge_bp and its siblings (module/src/combat/folds.rs:36) are aura-only, and the note at line 23 says so. Copy the crit and hit pattern at line 266, which already sums gear.
    • Mana per 5 and health per 5. regen_power and regen_health (module/src/combat/tables.rs:484) scale on spirit and level. Add a flat term.
    • Spell power and healing. spell_power (module/src/spell/math.rs:661) sums an intellect ramp and auras. Add a gear term.

    The wire

    Send the id everywhere an item instance goes:

    • gateway/src/codec/loot.rs:38, the loot body random_property_id, and the hardcoded zeros at lines 89, 128 and 148.
    • build_item_push_result, gateway/src/codec/item.rs:408.
    • gateway/src/codec/mail.rs:42.
    • gateway/src/codec/auction.rs:33.
    • build_item_create_object, gateway/src/codec/item.rs:329. Add .set_item_random_properties_id(..) to both the UpdateItem and UpdateContainer builders. The setter exists in the dependency.
    • build_item_query_response, gateway/src/codec/item.rs:107. Set random_property from the template's pool id. The field is on the wire struct and falls out of ..Default::default() as 0 today. This is what makes the client print <Random enchantment> on a craft preview.
    • Trade and inspect read the instance. Confirm their views carry the column.

    The partial VALUES crash class in danger zone 3 does not apply here. build_item_create_object builds a full CREATE mask, and items have no partial VALUES path. The property id never changes after creation, so they still need none. A later change that adds one must route through dirty_reset, and gateway/src/world/packet_lint.rs catches the mistake.

    Acceptance

    • An exact band test on the pure pick: every roll value in 0..10000 selects the member the weights say. This is stronger than counting samples, and it ships no debug reducer.
    • The import audit asserts enchant 7745 and 7748 are unchanged.
    • One live kill of a creature that drops a suffix item. The item rolls a suffix, the stat fold moves the character's vitals, and SQL agrees with the recompute. Commands go on the PR. No Verification document.
    • The client tooltip is a secondary check, flagged for a human to look at, per danger zone 5.

    Out of scope

    Custom variants beyond vanilla. A content package owns those: SQL rows in the tables above, plus a client addon or XML extension for display. The engine needs no change to support them. Worth stating plainly, because the split is deliberate. A modded property id works server-side and shows no suffix text, because the 5875 client reads suffix names from its own DBC and the protocol has no per-instance name field. The addon bridge (module/src/bridge.rs, gateway/src/codec/addon.rs) and the suffix column are the seams a package would build on.

    The realm update. No suffix drops until a realm reimports. Normal operator process, after merge.

    Glossary

    Add to CONTEXT.md in the Tracer, under a new "Random properties" heading.

    Random Property: The suffix rolled onto one item instance at creation, held as random_property_id. 0 is a plain item.
    Avoid: random enchant, item variant, roll

    Property Pool: The weighted set of Random Properties a template can roll, named by item_template.random_property.
    Avoid: random property group, suffix group

    Suffix: The name a Random Property adds to the item, such as "of the Bear". The server keeps a copy for diagnosis; the client renders its own.
    Avoid: affix, postfix

    Stat Kind: Our own stable code for what an enchantment effect grants. Append-only, and not the DBC encoding.
    Avoid: stat type, mod type, effect type

    Differences from the issue body

    1. The roll runs at one site, store_item, instead of at the loot and E_CREATE_ITEM sites separately. Every creation path then behaves the same, and a path added later gets suffixes with no code change.
    2. Every suffix family folds, not only the stat families, split across two Tickets. The pipeline checks showed each family already has a fold to extend.
    3. game_item_enchantment replaces rules::ENCHANTS rather than sitting beside it. One overlay mechanism, and a documented deferral closes.
    4. kind uses our own vocabulary rather than the DBC encoding, so SQL is the authoring surface for a modder.
    5. The ITEM_FIELD crash-class note does not apply as written. Item sends are full CREATEs.

    Specified with Claude Code. Fable 5 ran the design interview, Opus 5 wrote this up.

  2. Mathih13 commented on Sep 28, 2026

    @Mathih13
    ContributorAuthor

    Live acceptance on Argus, 2026-09-28

    Realm: Argus at Core eb4ffe4d, Packages 9df067d2b. Client: stock 1.12.1 (5875). Character: Pbguide (guid 2117, level 20 Mage).

    Roll. 37 debug kills of Lord Condar (14268) in front of Pbguide. Every pooled drop got a nonzero random_property_id on its game_corpse_loot row. Properties seen: 96, 1179, 760, 17, 1879, 839, 840, 1013, 18, 757, 585, 22, 1010, and more.

    Client display. The loot window and loot tooltips show the Suffix and the stat lines. Each one matches game_item_random_property and game_item_enchantment:

    Item Property SQL effects Client tooltip
    Birchwood Maul of Stamina 96 Stamina +4 "of Stamina"
    Buccaneer's Bracers of the Owl 757 Intellect +2, Spirit +2 +2 Intellect, +2 Spirit
    Willow Boots of Stamina 22 Stamina +3 +3 Stamina
    Ritual Bands of the Whale 1010 Spirit +2, Stamina +1 +2 Spirit, +1 Stamina

    Fold. Willow Boots of Stamina replaced Apprentice's Boots (no stats, no armor). Server max health went from 327 to 357. The character sheet shows Stamina 29 (was 26) and Armor 72 (48 base + 3 robe + 2 pants + 19 boots). The instance kept property 22 through loot, bag and equip.

    Gap found and fixed. Owned items showed the Suffix name but no stat lines. The loot tooltip had them, because the client reads an owned item's property stat lines from enchantment slots 3 to 5. #583 sends the property's enchant ids there. After the update, the equipped Willow Boots and a newly taken Ritual Bands of the Whale show their stat lines. That covers the world entry and live insert paths.

    <Random enchantment> preview. No vanilla craft, vendor item or quest reward uses a Property Pool, and 1.12 link tooltips do not render the line. The item query value stays pinned by the Gateway test.

    Commands (on Argus, -s local, database lyracore):

    spacetime call -s local lyracore debug_spawn_at_feet 2117 14268 4.0
    spacetime call -s local lyracore debug_apply_damage "<corpse guid>" 100 2117
    spacetime call -s local lyracore debug_kill_nearest 2117 14268
    spacetime sql -s local lyracore "SELECT slot, item_entry, count, random_property_id FROM game_corpse_loot WHERE corpse_guid = <corpse guid>"
    spacetime call -s local lyracore debug_take_loot 2117 "<corpse guid>" <slot>
    spacetime sql -s local lyracore "SELECT max_health, stamina, armor FROM game_world_entity WHERE guid = 2117"
    # after the corpse decays, so the debug spawn does not respawn:
    spacetime sql -s local lyracore "DELETE FROM game_creature_spawn WHERE guid = <corpse guid>"

    Every debug spawn row was deleted after its corpse decayed. Screenshots and recordings are with the maintainer.

    Pickup from the client (LootSlot or a click) did not reach gw_take_loot on either Gateway build, so the item moved through debug_take_loot. That calls the same apply_take_loot. This is tracked separately.

    Posted by Claude Opus 5.5 via Claude Code.

  3. Mathih13 commented on Sep 28, 2026

    @Mathih13
    ContributorAuthor

    Screenshots from the live acceptance on Argus, taken with a stock 1.12.1 client as Pbguide.

    Random Property evidence on Argus

    Top row, left to right:

    • The loot window shows "Outrunner's Cloak of the Bear".
    • Loot tooltips show "Buccaneer's Bracers of the Owl" (+2 Intellect, +2 Spirit) and "Willow Boots of Stamina" (+3 Stamina).
    • The owned boots before fix(items): owned suffix items show their stat lines #583: Suffix name, but no stat line.

    Bottom row, left to right:

    The commands and SQL results are in the comment above. #462, #466 and #583 ship the spec, and this check verifies it live. The follow-ups are #584 (client loot pickup) and #585 (permanent enchants on the client).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2normalenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions