diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a7f675ad3..d40072ea4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,8 +10,8 @@ body: - **`docs/quickstart.md` §8** has one troubleshooting entry per known failure mode, keyed to the exact message each one prints. Version mismatches, busy ports, `UNREACHABLE` databases, "Unable to connect", and wrong-build clients are all in there. - - **Security problems do not go here.** Use the "Report a vulnerability" link in this repo's - Security tab (private advisory) — do not open a public issue. + - **Security problems do not go here.** Send a private message to a maintainer on the + [LyraCore Discord](https://discord.gg/wxWHS3Tbj). Do not open a public issue. Public/VPS hosting, Windows/WSL, non-5875 clients, and requests for game data are out of scope for this alpha and will be closed as such (`SUPPORT.md` has the full list). diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d027eb5c4..07c0589c4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -7,5 +7,5 @@ contact_links: url: https://github.com/LyraCoreProject/LyraCore/blob/main/docs/quickstart.md#8-troubleshooting about: One entry per known failure mode, keyed to the exact message it prints. Check here first. - name: Report a security vulnerability - url: https://github.com/LyraCoreProject/LyraCore/security/advisories/new - about: Private advisory thread. Do not report security problems in a public issue — several weaknesses are already known and accepted for this alpha's local-development-only posture, so check the advisory isn't a duplicate first. + url: https://discord.gg/wxWHS3Tbj + about: Send a private message to a maintainer on the LyraCore Discord. Do not report security problems in a public issue. This alpha supports local development only, and some weaknesses are known and accepted for it. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bdad7b0d0..6211bfd9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,9 +123,9 @@ when** replace **Fix direction**. ## Reporting a bug Open an issue with the bug template: what you did, what you saw, and your `./lyracore doctor` -output. If you know what 1.12.1 does instead, say so. Report a security problem privately through -[GitHub's vulnerability reporting](https://github.com/LyraCoreProject/LyraCore/security/advisories/new), -never in a public issue. [`SUPPORT.md`](SUPPORT.md) says what is in scope. +output. If you know what 1.12.1 does instead, say so. Report a security problem in a private +message to a maintainer on the [LyraCore Discord](https://discord.gg/wxWHS3Tbj), never in a public +issue. [`SUPPORT.md`](SUPPORT.md) says what is in scope. ## Working with an AI agent diff --git a/SUPPORT.md b/SUPPORT.md index 2a378ec3d..69caee373 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,74 +1,68 @@ # Getting help -LyraCore is an alpha, maintained by a very small number of people in their own time. There is no -support contract, no SLA, and no hosted service. What there is: documentation that tries to be -specific, and a tracker that gets read. +LyraCore is an alpha, maintained by a few people in their spare time. There is no support contract +and no hosted service. -## The answer is often already written down +## Look here first | Your situation | Where to look | |---|---| -| "I cloned it, now what?" | [`docs/quickstart.md`](./docs/quickstart.md) — prerequisites, `./lyracore dev up`, account creation, `realmlist.wtf`, LAN play. | -| The stack will not start, a port is busy, `doctor` complains, the client will not connect | [`docs/quickstart.md` §8](./docs/quickstart.md#8-troubleshooting) — one entry per failure mode, with the exact message each one prints. | -| What does this CLI command do / is it safe | The [CLI command reference](https://github.com/LyraCoreProject/lyracore-cli/blob/main/docs/commands.md) in the `lyracore-cli` repository. | -| How does the thing actually work | [`docs/architecture.md`](./docs/architecture.md), whose §9 indexes the rest of the documentation. | -| I want to change something | [`CONTRIBUTING.md`](./CONTRIBUTING.md): the three documents to read first, what gets in, and how a pull request is judged. | -| Is it safe to expose this | No — see the warning banner in [`README.md`](./README.md). | -| Where does world content come from | [`docs/data-ingestion.md`](./docs/data-ingestion.md). | +| You cloned it and want a running realm | [`docs/quickstart.md`](./docs/quickstart.md): prerequisites, `./lyracore dev up`, accounts, `realmlist.wtf` and LAN play. | +| The stack does not start, a port is busy, `doctor` complains, or the client does not connect | [`docs/quickstart.md` §8](./docs/quickstart.md#8-troubleshooting), one entry per failure with the message it prints. | +| What a CLI command does, and whether it is safe | The [CLI command reference](https://github.com/LyraCoreProject/lyracore-cli/blob/main/docs/commands.md) in the `lyracore-cli` repository. | +| How the server works | [`docs/architecture.md`](./docs/architecture.md). | +| You want to change something | [`CONTRIBUTING.md`](./CONTRIBUTING.md). | +| Where world content comes from | [`docs/data-ingestion.md`](./docs/data-ingestion.md). | -Before reporting anything, run: +Before you report a problem, run: ```bash ./lyracore doctor ./lyracore dev status ``` -Between them they distinguish most of the common failures — a wrong toolchain version, a port held -by someone else's process, a healthy pair of processes pointed at a node where nothing was ever -published. +`doctor` finds a wrong toolchain version or a port held by another process. `dev status` finds a +running stack whose node has no published Module. ## Where to ask -**Bugs, questions and concrete proposals → [GitHub Issues](https://github.com/LyraCoreProject/LyraCore/issues).** -For a bug, include the quickstart step you were on, your `./lyracore doctor` output, and the log the -failure appeared in. Those three answers turn a report into a fix. A question is welcome there too: -say what you expected and what you saw. +Use [GitHub Issues](https://github.com/LyraCoreProject/LyraCore/issues) for bugs, questions and +proposals. For a bug, give the quickstart step you were on, your `./lyracore doctor` output and the +log that shows the failure. For a question, say what you expected and what you saw. -**Security reports → not here.** Use GitHub's private vulnerability reporting on this repository; do -not open a public issue. +Report a security problem in a private message to a maintainer on the +[LyraCore Discord](https://discord.gg/wxWHS3Tbj). Do not post it in a channel or a public issue. +Report harassment or abuse the same way. -There is no chat server, no forum, no mailing list, and no social account. If you find something -claiming to be an official LyraCore channel, it is not one. +This repository and that Discord server are the only official channels. ## What is in scope -Supported, and worth reporting: +We support: -- The **local development fixture** on Linux and macOS: `./lyracore` and everything +- The local development fixture on Linux and macOS: `./lyracore` and everything [`docs/quickstart.md`](./docs/quickstart.md) describes. -- A stock, unmodified **1.12.1 build 5875** client connecting over loopback or a private LAN. -- The **seeded** world — the fixture written by the module's `init` reducer, which needs no - client-data import. -- Anything in this repository being wrong: a broken command, a stale document, a dead link. - -Out of scope, and closed as such: - -- **Public or VPS hosting.** Not supported in this alpha, at all. See the warning banner in - [`README.md`](./README.md). -- **Windows and WSL.** Not covered and not tested. -- **Any client that is not build 5875**, and any modified or launcher-patched client. -- **Requests for game data** — a client, MPQ/DBC files, or a world-database dump. This project - distributes none of those and will not point you at somewhere that does. -- **Problems that only appear after your own content import.** Say so in the report; it is still - worth filing, but it is a different world from the seeded fixture the test suites run against. -- Conduct problems — harassment, abuse, or bad-faith participation. Report those privately to the maintainers rather than in a public issue. - -## Response expectations - -Best effort, by people with day jobs. `P1` (blocks play, or a live regression) gets looked at first; -everything else is queued by priority label (`P1`-`P4`) on [GitHub Issues](https://github.com/LyraCoreProject/LyraCore/issues). -A quiet issue has not been rejected. An issue with **How to check** waits on a person at a real game -client, which is scarcer here than engineering time. - -The fastest way to get something fixed is a pull request. [`CONTRIBUTING.md`](./CONTRIBUTING.md) -says where to start and how a pull request is judged. +- An unmodified 1.12.1 build 5875 client over loopback or a private LAN. +- The seeded world that the Module's `init` reducer writes. It needs no client data import. +- Errors in this repository, such as a broken command, a stale document or a dead link. + +We close these as out of scope: + +- Public or VPS hosting. The [`README.md`](./README.md) says why. +- Windows and WSL. Nobody tests them. +- A client that is not build 5875, or a modified or launcher-patched client. +- Requests for game data: a client, MPQ or DBC files, or a world database dump. This project + distributes none of them and does not say where to find them. + +A problem that appears only after your own content import is still worth a report. Say so in it, +because the test suites run against the seeded world, not yours. + +## Response times + +We work through [issues](https://github.com/LyraCoreProject/LyraCore/issues) by priority label, `P1` +(blocks play, or a live regression) first and `P4` last. A quiet issue is not a rejected one. An +issue with **How to check** waits for a person with a real game client, and we have fewer of those +than engineers. + +A pull request is the fastest way to get a fix. [`CONTRIBUTING.md`](./CONTRIBUTING.md) says how we +judge one. diff --git a/docs/benilla-compatibility.md b/docs/benilla-compatibility.md index 47785ecc3..772130aad 100644 --- a/docs/benilla-compatibility.md +++ b/docs/benilla-compatibility.md @@ -5,8 +5,8 @@ The Gateway's development dependencies pin `benilla-protocol` and `benilla-srp` to `c2fc3cf304075a0fa1c9accffaf5efb99be9edfe`. Tests use that revision from Cargo, so changes in a neighboring Benilla checkout cannot change the results. -All 277 requests in the pinned Benilla writer now have a typed dispatch pattern -or a reviewed raw route. The inventory test flags requests with neither source +Every request in the pinned Benilla writer has a typed dispatch pattern or a +reviewed raw route. The inventory test flags requests with neither source classification. This accounts for requests; it does not certify every gameplay path or every outgoing packet. Unsupported operations return a Refusal where the protocol defines one, or a System Message naming the unavailable feature. @@ -115,10 +115,6 @@ The Headless Client scenarios cover these exchanges through the same WorldStore packet's quest id, money and XP. An incomplete quest must stay on the progress screen and grant nothing when its reward screen is requested. -The reward scenario exposed `CMSG_QUESTGIVER_REQUEST_REWARD`, which now uses the -same durable completion evaluation as `CMSG_QUESTGIVER_COMPLETE_QUEST`. Choosing -a reward still crosses the Store Seam and leaves the grant rules in the Module. - ## Durable scenarios The ignored `benilla_durable_` tests start private SpacetimeDB instances on loopback @@ -149,25 +145,6 @@ runs them in CI. They cover: - Refusing a distant, dead or wrong-kind innkeeper, then binding at the selected innkeeper and receiving the new hearth point during the same World Session. -Module inventory regressions also check that split, destroy, use, move, merge, -vendor sale, disenchanting and reagent consumption cannot change offered trade -items. Quest item exchanges use the same inventory Gate. Scheduled crafting checks -all reagents before consuming any or charging power. Automatic grants that grow -an offered stack clear both acceptances and refresh both trade windows. Failed -crafts also refresh the restored quantities. A completed trade delivers the newly -accepted stack count and copper. Equipment moves refuse a two-handed weapon -while an offhand item is equipped, and refuse the reverse combination. The -Character must remove the conflicting item first. - -The XP scenario found a codec mismatch. Vanilla kill XP includes base XP and the -group multiplier; other XP awards have no extra fields. The Gateway now writes -that layout directly. `XpEvent.rested_bonus` is an appended field with a zero -default so the kill packet can distinguish base XP from rested XP. - -The bag scenario found missing container pointers at world entry. Those pointers -now follow the item and Character creation packets, using the same slot mapping -as incremental inventory updates. - ## Additional protocol corrections The Gateway owns these build-5875 layouts where the protocol library differs: @@ -188,6 +165,11 @@ The Gateway owns these build-5875 layouts where the protocol library differs: Ticket requests receive Refusals after framing checks. The unavailable service does not decompress the optional transcript or allocate from its size headers. +Kill XP carries base XP and the group multiplier; other XP awards have no extra +fields, and the Gateway writes that layout directly. At world entry, container +pointers follow the item and Character creation packets, with the same slot +mapping as incremental inventory updates. + At War changes reach the existing Module operation. Bind and talent-reset confirmations also reach existing Module operations. Selecting the gossip option opens the confirmation dialog first. Talent quotes use the same price calculation @@ -234,21 +216,6 @@ System Message. Possession and remote movement authority remain unavailable. The scenarios establish the behavior listed above, not every branch of the older handlers. Further gameplay verification includes targeted item spells, channel refresh and reconnect, pet spell execution, multi-shard instance flows and the -complete outgoing opcode inventory. The source report contains 346 outgoing -opcode rows; decoder presence alone does not require a realm to emit each one. - -## Verification record - -The workspace run passed 4,376 tests with `lyracore-package-delta` excluded, -using the Module's `debug_reducers` feature. All eight durable Benilla scenarios -passed against disposable SpacetimeDB instances. Formatting, workspace Clippy with -all targets and features, and the check of all 1,182 Gateway binding files passed. -Plain publish from `main` at `f09dafbd` to this Module migrated a private instance -without clearing it. Its Character rows survived, and `XpEvent.rested_bonus` was -readable after the update. - -An earlier full workspace run found two unchanged package-delta fixture failures: -`a_package_may_insert_a_quest_inside_the_package_range` and -`an_incomplete_quest_insert_is_refused`. Their shared `WHOLE_QUEST_ROW` lacks the -`quest_type` field required by the existing package-delta schema. Those files are -outside this change. +complete outgoing opcode inventory. Decoder presence alone does not require a +realm to emit an outgoing message. + diff --git a/docs/design/item-guids.md b/docs/design/item-guids.md deleted file mode 100644 index 4f504906a..000000000 --- a/docs/design/item-guids.md +++ /dev/null @@ -1,68 +0,0 @@ -# Item GUID allocation - -Characters and items consume the same durable high-water mark in a Shard's GUID Range. -Realm-core assigns disjoint billion-value ranges. `install_guid_range` refuses reassignment; -deleting a Character, deleting an item, Transfer and reinstalling the same range never rewind -the mark. A batch must fit below the range's exclusive end before the allocator advances. -The range now budgets Character creation and item creation together. Its base is the allocator -floor, leaving 999,999,999 issued identities per range. - -New items retain `HIGHGUID_ITEM = 0x4000` in bits 48 through 63. Bit 47 marks this allocation -format. Bits 0 through 46 hold the issued value, so 140,737,488,355,327 is the largest usable -item allocation. Requests above that limit return `ITEM_GUID_EXHAUSTED` before advancing the -mark. Missing ranges return `NO_GUID_RANGE`; a batch crossing a range end returns -`GUID_RANGE_EXHAUSTED`. Items do not truncate or pack their Character owner's GUID. - -Grants reserve complete batches before updating partial stacks or deleting Trading items. -Issued identities are never reused, including items consumed by a quest or craft earlier in -the same request. If a craft cannot grant all products, it restores the original carried -inventory with the original item GUIDs. Profession skill gains and automatic learning wait -until all direct product grants succeed. This restores carried inventory on a refused craft -without minting refund items from an exhausted range. Other effects retain their authored order. -This is not whole-cast rollback: a mixed recipe with triggered casts or other side effects needs -separate acceptance coverage. The committed conjuring fixture has one direct CreateItem effect; -it does not establish that every imported recipe has that shape. - -An item keeps its GUID while moving between inventory slots and during Character Transfer. -Trading creates new recipient items. Mail, Auctions and Buyback store item snapshots and -create new items when the recipient takes them. Preserved identities now rely on disjoint -GUID Ranges and a durable issuer mark, rather than a packed Character namespace. - -Transfer does not move or replace a Shard's allocator. With a range installed, an arriving -Character raises the local mark only when its GUID belongs to the destination's own range. Incoming items need no floor -update because the issuing Shard retains their high-water mark after they leave. - -## Compatibility and rollout - -No table, column, reducer argument or Transfer row format changes. Existing item rows and saved -Transfer Escrow keep their exact GUIDs. There is no automatic renumbering or data sweep. -Destination import checks arriving item ownership, duplicate GUIDs and local collisions before -inserting item rows. A conflict returns `ITEM_GUID_CONFLICT`; the reducer transaction preserves -the destination and source Escrow. It never deletes another Character's item to make room. - -The old allocator normally used only the low 32 bits, but unchecked increments could escape -that range. No bit below the item prefix is provably unreachable by the old code. Before -deployment, an authorized Operator must inventory item GUIDs and saved Transfer Escrow across -every Shard. Record duplicate GUIDs with different owners, malformed item prefixes, legacy IDs -with bit 47 set, missing or overlapping GUID Ranges, and allocator marks at or beyond their -range end. Resolve those cases under separate human approval. Local collision checks do not -replace this realm-wide audit. An already escaped Character GUID also needs explicit review. - -Without an installed range, Character cleanup retains the legacy allocator floor. Gateway -startup can continue after a failed range claim, so deleting a Character in that state must not -allow its GUID to be reused when provisioning recovers. Item creation still returns -`NO_GUID_RANGE`. Resolve missing ranges before deployment; the update never invents an assignment -or resets a mark. - -Pause World Sessions and all item-granting or ownership-changing activity during the update. -Record outstanding Transfer Escrow. Verify each Shard's permanent range assignment, publish the -reviewed Module to every Shard, and confirm that all Shards run the same build before resuming -activity. An old Module would mint from a new item's maximum GUID without respecting the shared -allocator. Rolling back requires the same pause and a reviewed plan for new-format rows. -Never reset `game_guid_allocator`, reinstall a different range or delete the range registry. - -Before deployment, obtain human review of this allocation and rollout plan. On an authorized -isolated realm, verify existing and new items through grant, split, crafting, Trading, Mail, -Buyback and round-trip Transfer, including a conflicting legacy arrival. Check actual 5875 -client item rendering and slot moves. Unit tests and private reducer tests do not establish -real-client acceptance or prove a live realm's existing GUIDs are conflict-free. diff --git a/docs/design/loot-roll-promotion.md b/docs/design/loot-roll-promotion.md deleted file mode 100644 index 27d74b1d9..000000000 --- a/docs/design/loot-roll-promotion.md +++ /dev/null @@ -1,33 +0,0 @@ -# Loot Roll promotion identity - -A promotion names the source Module identity and the source LootRoll row id. The Module stamps `promotion_source` with `ctx.database_identity()` when it stages a local roll. This is the identity of the source Module, not the Operator identity shared by Gateway connections. The Gateway copies both values from the staging row on every retry. - -Realm-core keeps one promotion receipt per source identity, corpse guid and slot. The receipt stores the greatest accepted source row id. A START with that id or an older id succeeds without creating rows. The Gateway can then clear exactly that staging row by its id. A larger id can start after the active roll on the corpse and slot has resolved. If that roll is still active, the request leaves all durable state unchanged and the Gateway keeps the staging row for retry. - -The source table's auto-increment sequence orders generations within one Module identity. Ordinary publish and restart preserve the table and its sequence. Content imports must not replace this gameplay table or insert explicit LootRoll ids. Restoring an older copy under the same identity can reuse ids and is outside this guarantee. Such a restore needs a separate reviewed recovery procedure. - -## Cleanup - -Resolution and staging cleanup remove the active LootRoll and its votes. They do not remove promotion receipts. A later accepted generation updates the existing receipt, so it also continues to cover every earlier generation. Receipts for unrelated slots or source identities have independent order. - -There is no time-based expiry. Storage grows with distinct source identities, corpse guids and slots, including dynamic creature guids. Retiring a source does not make its delayed requests invalid, so retirement alone does not permit receipt deletion. Deletion would need a durable protocol that refuses every request from the retired identity before removing its receipts. This implementation has no such operation. - -## Upgrade review - -This change adds a defaulted `Identity` column to LootRoll, a private receipt table, and two reducer arguments. The default identity is ZERO. START refuses ZERO and source row id zero, so an old staging row cannot silently enter a new identity namespace. - -Human review under `docs/danger-zones.md` section 1 is required before shipping. The reviewer must approve the additive schema, generated bindings, retained receipt storage and the following upgrade boundary: - -1. Prevent new gameplay and Operator requests from creating Loot Rolls, including bot combat. Keep the old Gateways running while existing rolls resolve so their relays can deliver winner settlement to the World Shards. -2. On every World Shard, Instance Pool and Realm-core, require zero rows in `game_loot_roll` and `game_loot_roll_vote`, and zero rows in `game_corpse_loot WHERE withheld = true`. An empty active-roll table alone does not prove that the winner received the item. Check the state rather than assuming the 60-second roll deadline completed settlement. Diagnose any remaining rows before continuing. -3. Once settlement has drained, stop every old Gateway and other Operator client that can promote or settle a roll. Keep gameplay producers quiescent. Recheck every intended destination before the first publish. -4. Publish the new Module to every destination with the ordinary non-destructive publish procedure. Do not reset tables or their auto-increment sequences. -5. Start only the Gateway build that sends the new promotion identity arguments. Old Gateway processes and their retained requests must remain stopped. - -The empty-roll checkpoint prevents an old Realm-core roll from resolving without a corresponding receipt while an old staging copy is still eligible for promotion. The withheld-loot check also prevents publishing between resolution and winner settlement. A source-column backfill alone would not establish the missing Realm-core receipt. - -The pinned `lyracore publish` checks every intended destination before its first publish. For a destination without the receipt table, it refuses pending rolls, votes, withheld loot, unreadable state or malformed query results. `--skip-preflight` does not bypass this check. The command does not stop gameplay producers or lock the destinations together. The Operator must complete the quiescence procedure above and keep it in force through the entire publish batch. - -A controlled check published approved main `8a40989` to two disposable Shards, created a source staging roll and a Realm-core roll with property 117, then cast the winning votes. Both roll tables became empty while the source still held withheld loot. Delivering the old `settle_loot_roll` request granted exactly one item and cleared the final pending row. Publishing this change then preserved that item, property 117 and the source sequence, refused the old START arguments and produced a nonzero identity on new staging rows. This checks the Module transition; the publish command's separate all-destination check enforces the checkpoint. - -No production upgrade was performed while developing this change. The regression suite uses owned disposable Standalone fixtures. diff --git a/docs/testing.md b/docs/testing.md index 3d5955a5d..c3892f1d7 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -86,6 +86,9 @@ CI also runs Gateway tests that need a node. The workflow names individual tests cargo test -p lyracore-gateway --bin lyracore-gateway stdb::subscriptions::party_command_durable_tests -- --ignored --test-threads=1 ``` +The Headless Client scenarios, their Benilla pin and the opcode inventory are in +[`benilla-compatibility.md`](./benilla-compatibility.md). + ## Gateway bindings Before Gateway tests, checks, or Clippy on a fresh checkout, prepare its bindings: diff --git a/module/src/items/guids.rs b/module/src/items/guids.rs index 4ce17f836..0c1c0fc72 100644 --- a/module/src/items/guids.rs +++ b/module/src/items/guids.rs @@ -1,4 +1,9 @@ //! Item identities issued from the Shard's durable GUID Range. +//! +//! Characters and items share the range's high-water mark. An item GUID keeps `HIGHGUID_ITEM` in +//! bits 48–63, sets bit 47 to mark this format, and holds the issued value in bits 0–46. It does not +//! pack its owner's GUID. An item keeps its GUID across slot moves and Character Transfer; Trading, +//! Mail, Auctions and Buyback create new items for the recipient. use spacetimedb::ReducerContext; diff --git a/module/src/loot/rolls.rs b/module/src/loot/rolls.rs index f5692f372..d2ac4a146 100644 --- a/module/src/loot/rolls.rs +++ b/module/src/loot/rolls.rs @@ -51,6 +51,11 @@ pub struct LootRoll { /// One Loot Roll Promotion Receipt per source Module identity, corpse and slot. Resolution keeps /// this high-water mark; replacing it with a later source id also covers every older replay. +/// +/// The source `game_loot_roll` auto-increment sequence orders generations, so content imports must +/// not replace that table or insert explicit roll ids. Restoring an older copy under the same +/// identity can reuse ids and needs its own recovery procedure. Receipts never expire: a retired +/// source can still have delayed requests in flight, and nothing refuses them yet. #[table(accessor = game_loot_roll_promotion_receipt, index(accessor = by_source_slot, btree(columns = [promotion_source, corpse_guid, slot])))] pub struct LootRollPromotionReceipt { #[primary_key]