Asset Core Docs

Deterministic world-state engine documentation and API references.

Decision Gate docs

Game Inventory: Deterministic Equip, Trade, Stash, and Loot

Players own deterministic inventories, equipment slots, stash grids, and wallets. Equip, trade, loot drops, pickups, and stack consolidation all resolve through a small, replayable set of primitives.

If you are reading this cold, this is a production inventory walkthrough: deterministic equip, trade, loot drops, and stash operations without tracking player movement. Every state change is an auditable commit with stable idempotency keys, so live-service workflows are replayable and anti-dup by default.

Why this matters

Live-service inventory systems live or die on anti-dup, atomic trades, and replayable support workflows. Asset Core makes inventory authoritative by treating every equip, trade, and drop as a deterministic transaction.

System model

  • Players: modeled as owners and actor IDs (no player movement tracking).
  • Inventories: grid containers for deterministic placement + collision rules.
  • Equipment: slot containers for explicit equip semantics.
  • Stash: grid container to show long-term storage mechanics.
  • Wallets: balance containers for currency and atomic trades.
  • Zone loot grid: deterministic loot anchors for encounters, chests, or drops.
  • Loot anchors represent authoritative slots, not physics or movement.
  • Classes and shapes: registered so placement rules stay consistent.
  • Units: cell (fixed_point_scale=1).

Universal setup

Two baseline commits establish the inventory domain once: setup creates containers and registers classes; seed mints the starter items, balances, and a stash blocker. Every case that follows assumes this baseline so it can stay focused and self-contained.

Setup commit: create containers and register item classes used by every case. Seed commit: mint starter items, wallet balances, potion stacks, and the stash blocker used in the collision case.

Containers created

  • zone_loot (id container-32001): Zone loot grid for encounter drops and chest rewards.
  • p1_inventory (id container-32002): Player 1 inventory grid (bag).
  • p1_equipment (id container-32004): Player 1 equipment slots (weapon, offhand, armor).
  • p1_stash (id container-32008): Player 1 stash grid for long-term storage.
  • p1_wallet (id container-32006): Player 1 wallet balance container (currency).
  • p2_inventory (id container-32003): Player 2 inventory grid.
  • p2_equipment (id container-32005): Player 2 equipment slots.
  • p2_stash (id container-32009): Player 2 stash grid.
  • p2_wallet (id container-32007): Player 2 wallet balance container.

Classes registered

  • sword (id class-42001): Sword item instance.
  • shield (id class-42002): Shield item instance.
  • bow (id class-42003): Bow item instance.
  • armor (id class-42004): Armor item instance.
  • potion (id class-42005): Potion stackable item.
  • gold (id class-42006): Gold fungible currency.

Setup commit

Register classes/shapes and create containers.

Why this step matters: This is the only time we create containers and register item classes. Everything after this is a move, merge, or balance change against these definitions.

Preconditions:

  • None. This is the bootstrap commit.

What this commit creates:

  • zone_loot (id container-32001): Zone loot grid for encounter drops and chest rewards.
  • p1_inventory (id container-32002): Player 1 inventory grid (bag).
  • p1_equipment (id container-32004): Player 1 equipment slots (weapon, offhand, armor).
  • p1_stash (id container-32008): Player 1 stash grid for long-term storage.
  • p1_wallet (id container-32006): Player 1 wallet balance container (currency).
  • p2_inventory (id container-32003): Player 2 inventory grid.
  • p2_equipment (id container-32005): Player 2 equipment slots.
  • p2_stash (id container-32009): Player 2 stash grid.
  • p2_wallet (id container-32007): Player 2 wallet balance container.

What this commit registers:

  • sword (id class-42001): Sword item instance.
  • shield (id class-42002): Shield item instance.
  • bow (id class-42003): Bow item instance.
  • armor (id class-42004): Armor item instance.
  • potion (id class-42005): Potion stackable item.
  • gold (id class-42006): Gold fungible currency.

What to notice in the code:

  • Each CreateContainer call maps 1:1 to a container listed above.
  • Each RegisterClass defines the placement and stacking rules used later.

Expected state change:

  • Containers and classes are registered and ready for gameplay interactions.

Operations

  • CreateContainer (x9) - Creates a container (structured memory region) with the requested kind.
  • RegisterClass (x6) - Registers a class definition so future operations can reference it.
  • RegisterClassShape (x5) - Registers a grid shape footprint for a class or class variant.
await write_client.commit_operations(
    [
        CreateContainer(
            kind={"capacity": 25, "grid_width": 5, "type": "grid"},
            policies=None,
            external_id="container-32001",
            owner_external_id="owner-73",
        ),
        CreateContainer(
            kind={"capacity": 25, "grid_width": 5, "type": "grid"},
            policies=None,
            external_id="container-32002",
            owner_external_id="owner-71",
        ),
        CreateContainer(
            kind={"capacity": 25, "grid_width": 5, "type": "grid"},
            policies=None,
            external_id="container-32003",
            owner_external_id="owner-72",
        ),
        CreateContainer(
            kind={"count": 4, "type": "slots"},
            policies=None,
            external_id="container-32004",
            owner_external_id="owner-71",
        ),
        CreateContainer(
            kind={"count": 4, "type": "slots"},
            policies=None,
            external_id="container-32005",
            owner_external_id="owner-72",
        ),
        CreateContainer(
            kind={"quantization_inv": 1, "type": "balance"},
            policies=None,
            external_id="container-32006",
            owner_external_id="owner-71",
        ),
        CreateContainer(
            kind={"quantization_inv": 1, "type": "balance"},
            policies=None,
            external_id="container-32007",
            owner_external_id="owner-72",
        ),
        CreateContainer(
            kind={"capacity": 25, "grid_width": 5, "type": "grid"},
            policies=None,
            external_id="container-32008",
            owner_external_id="owner-71",
        ),
        CreateContainer(
            kind={"capacity": 25, "grid_width": 5, "type": "grid"},
            policies=None,
            external_id="container-32009",
            owner_external_id="owner-72",
        ),
        RegisterClass(
            request={
                "behavior": {"balance_scale": 1},
                "class_id": "class-42001",
                "flags": 0,
                "name": "item-sword",
            },
        ),
        RegisterClass(
            request={
                "behavior": {"balance_scale": 1},
                "class_id": "class-42002",
                "flags": 0,
                "name": "item-shield",
            },
        ),
        RegisterClass(
            request={
                "behavior": {"balance_scale": 1},
                "class_id": "class-42003",
                "flags": 0,
                "name": "item-bow",
            },
        ),
        RegisterClass(
            request={
                "behavior": {"balance_scale": 1},
                "class_id": "class-42004",
                "flags": 0,
                "name": "item-armor",
            },
        ),
        RegisterClass(
            request={
                "behavior": {"balance_scale": 1},
                "class_id": "class-42005",
                "flags": 0,
                "name": "item-potion",
            },
        ),
        RegisterClass(
            request={
                "behavior": {"balance_scale": 1},
                "class_id": "class-42006",
                "flags": 0,
                "name": "currency-gold",
            },
        ),
        RegisterClassShape(
            request={"class_id": "class-42001", "shape": {"height": 1, "width": 1}},
        ),
        RegisterClassShape(
            request={"class_id": "class-42002", "shape": {"height": 2, "width": 2}},
        ),
        RegisterClassShape(
            request={"class_id": "class-42003", "shape": {"height": 1, "width": 1}},
        ),
        RegisterClassShape(
            request={"class_id": "class-42004", "shape": {"height": 2, "width": 2}},
        ),
        RegisterClassShape(
            request={"class_id": "class-42005", "shape": {"height": 1, "width": 1}},
        ),
    ],
    namespace_id=1,
    idempotency_key="game-inventory-setup",
    actor_id="game-setup",
    metadata={
        "job_id": "job-inventory-001",
        "step": "setup",
        "trace_id": "trace-inventory-001",
    },
)

Seed commit

Seed player inventories, stash blocker, and balances.

Why this step matters: Seeding is the only time we inject items and balances. After this, state only moves between containers or decrements counts.

Preconditions:

  • Setup commit has created containers and registered classes.

What to notice in the code:

  • Item instances are minted directly into player inventories and stash.
  • Wallet balances and potion stacks are funded via AddFungible.
  • A stash blocker is intentionally placed to force the POSITION_OCCUPIED branch.

Expected state change:

  • Starter items, balances, and potion stacks exist in their target containers.
  • The stash blocker occupies its anchor to force a deterministic collision.

Operations

  • AddInstance (x5) - Mints a new instance and places it at a target location.
  • AddFungible (x4) - Adds fungible quantity to a balance or an explicit grid cell.
await write_client.commit_operations(
    [
        AddInstance(
            class_id="class-42001",
            client_tag="p1-sword",
            key=None,
            location={
                "container_id": "container-32002",
                "kind": "grid_cell",
                "position": 3,
                "rotation": None,
            },
        ),
        AddInstance(
            class_id="class-42003",
            client_tag="p1-bow",
            key=None,
            location={
                "container_id": "container-32002",
                "kind": "grid_cell",
                "position": 5,
                "rotation": None,
            },
        ),
        AddInstance(
            class_id="class-42004",
            client_tag="p1-armor",
            key=None,
            location={
                "container_id": "container-32002",
                "kind": "grid_cell",
                "position": 7,
                "rotation": None,
            },
        ),
        AddInstance(
            class_id="class-42002",
            client_tag="p2-shield",
            key=None,
            location={
                "container_id": "container-32003",
                "kind": "grid_cell",
                "position": 8,
                "rotation": None,
            },
        ),
        AddInstance(
            class_id="class-42004",
            client_tag="stash-blocker",
            key=None,
            location={
                "container_id": "container-32008",
                "kind": "grid_cell",
                "position": 1,
                "rotation": None,
            },
        ),
        AddFungible(
            class_id="class-42006",
            key=None,
            location={"container_id": "container-32006", "kind": "balance"},
            quantity="1000",
        ),
        AddFungible(
            class_id="class-42006",
            key=None,
            location={"container_id": "container-32007", "kind": "balance"},
            quantity="800",
        ),
        AddFungible(
            class_id="class-42005",
            key=None,
            location={
                "container_id": "container-32002",
                "kind": "grid_cell",
                "position": 11,
                "rotation": None,
            },
            quantity="5",
        ),
        AddFungible(
            class_id="class-42005",
            key=None,
            location={
                "container_id": "container-32002",
                "kind": "grid_cell",
                "position": 14,
                "rotation": None,
            },
            quantity="3",
        ),
    ],
    namespace_id=1,
    idempotency_key="game-inventory-seed",
    actor_id="world-seed",
    metadata={
        "job_id": "job-inventory-001",
        "step": "seed",
        "trace_id": "trace-inventory-001",
    },
)

Cases at a glance

  1. Equip - Equip an item into player 1 slots.
  2. Trade - Trade an item and currency atomically.
  3. Drop - Drop an item into zone loot.
  4. Pickup - Pick up loot into player 1 inventory.
  5. Stash Blocked - Blocked stash returns POSITION_OCCUPIED.
  6. Stash Grid Free - Find the next available stash anchor.
  7. Stash - Stash armor into a free grid anchor.
  8. Stack Merge - Merge potion stacks in player 1 inventory.
  9. Consume - Consume potions from the merged stack.

System Design: What We Chose and Why

This scenario focuses on MMO-grade inventory correctness: each interaction is a small, atomic commit that can be replayed and audited.

Choice: Players as Owners

The Choice: Players are owners; inventories and slots are owned containers, not moving actors.

  • Why: Ownership isolates state, simplifies permissions, and keeps the system focused on inventory authority rather than player movement.
  • Trade-off: Player movement is handled by your engine; you map events into inventory commits when needed.
  • When it matters: Live-service games where dupes and cross-player bleed are unacceptable.

Choice: Zone Loot Grid as Deterministic Anchors

The Choice: Loot lives in a grid container with stable anchors (per encounter/chest/zone).

  • Why: Anchors make drops and pickups deterministic and auditable without coupling to physics.
  • Trade-off: Spatial rendering is engine-side; Asset Core provides authoritative loot state.
  • When it matters: Shared-world loot, raid rewards, and replayable support investigations.

Choice: Atomic Trade Commits

The Choice: Item transfer + currency transfer happen in one commit.

  • Why: Atomicity prevents dupes and partial trades under retries or disconnects.
  • Trade-off: Clients must compose multi-op commits (trade is a single transaction).
  • When it matters: Player-to-player trading, auction settlement, and escrow flows.

Choice: Idempotency Keys for Retry Safety

The Choice: Idempotency keys are optional but, when present, deduplicate retries based on a canonical hash of the full request payload.

  • Why: Same key + same payload returns the cached response; same key + different payload is rejected. This prevents double-spends under retries.
  • Trade-off: The authoritative service must generate or derive stable keys per interaction and must not forward untrusted client keys.
  • When it matters: Queue-driven game services, reconnects, at-least-once delivery, and cross-region failover.

What We Don’t Handle (and Why)

  • Player movement: AssetCore does not track movement or physics. Your engine owns movement; AssetCore owns inventory authority.
  • Combat RNG: Drop chances and reward rolls happen in gameplay systems; AssetCore records the resulting state.

These boundaries keep AssetCore focused on deterministic inventory and transaction guarantees.

Idempotency semantics

Idempotency keys are optional and only control retry de-duplication. When a key is present, Asset Core hashes the full request payload and uses (namespace, key, hash) to decide:

  • Same key + same payload: return the cached response (no re-execution).
  • Same key + different payload: reject with 409 Conflict.
  • Failed validation does not reserve the key; reuse after correction is valid.
  • No key: request executes normally, retries re-run. In server-authoritative flows, the game service generates or derives keys and should not forward untrusted client keys.

How to read the cases

  • Cases assume the baseline setup and seed commits are already applied.
  • Each case is a focused interaction (commit or read) you can replay independently.
  • Validation reads/streams are listed separately after the cases.
  • Single-operation cases use action helpers for readability.
  • Multi-operation cases stay as commit calls to preserve atomicity.
  • Rust HTTP always uses the commit endpoint with one or more operations.
  • C ABI tabs show direct runtime integration for engine teams.

Scenario artifacts

Scenario snapshot

{
  "scenario_id": "game_inventory",
  "version": "0.1.0",
  "namespace_id": 1,
  "containers": {
    "p1_equipment": "container-32004",
    "p1_inventory": "container-32002",
    "p1_stash": "container-32008",
    "p1_wallet": "container-32006",
    "p2_equipment": "container-32005",
    "p2_inventory": "container-32003",
    "p2_stash": "container-32009",
    "p2_wallet": "container-32007",
    "zone_loot": "container-32001"
  },
  "classes": {
    "armor": "class-42004",
    "bow": "class-42003",
    "gold": "class-42006",
    "potion": "class-42005",
    "shield": "class-42002",
    "sword": "class-42001"
  },
  "units": {
    "fixed_point_scale": 1,
    "length_unit": "cell",
    "time_unit": "ms"
  }
}

Failure branch

POSITION_OCCUPIED -> grid_free -> retry with same idempotency key

Audit trail (optional)

The transcript captures the exact request and response pairs emitted during the run. Here is a compact excerpt you can use to validate determinism and metadata propagation:

{
  "kind": "commit",
  "name": "setup",
  "request": {
    "path": "/v1/write/namespaces/1/commit",
    "idempotency_key": "game-inventory-setup",
    "actor_id": "game-setup",
    "metadata": {
      "job_id": "job-inventory-001",
      "step": "setup",
      "trace_id": "trace-inventory-001"
    },
    "operations": [
      "CreateContainer",
      "CreateContainer",
      "CreateContainer",
      "CreateContainer",
      "CreateContainer",
      "CreateContainer",
      "CreateContainer",
      "CreateContainer",
      "CreateContainer",
      "RegisterClass",
      "RegisterClass",
      "RegisterClass",
      "RegisterClass",
      "RegisterClass",
      "RegisterClass",
      "RegisterClassShape",
      "RegisterClassShape",
      "RegisterClassShape",
      "RegisterClassShape",
      "RegisterClassShape"
    ]
  },
  "response": {
    "commit_id": "00000000000000000000000000000007",
    "world_seq_start": 1,
    "world_seq_end": 20,
    "event_count": 20
  }
}

Preflight note

  • Action calls submit commits by default.
  • Set ActionOptions(preflight=True) or call assetcore_commit_preflight for validation.

SDK setup

from assetcore_sdk import AssetCoreClient
from assetcore_sdk.actions import ActionOptions
from assetcore_sdk.operations import (
    AddFungible,
    AddInstance,
    CreateContainer,
    MergeStacks,
    MoveInstance,
    RegisterClass,
    RegisterClassShape,
    RemoveFungible,
    TransferFungible,
)

# Snippets assume an async context (e.g., inside async def main()).
write_client = AssetCoreClient(
    base_url="http://localhost:8080",
    api_key="WRITE_API_KEY",
)
read_client = AssetCoreClient(
    base_url="http://localhost:8081",
    api_key="READ_API_KEY",
)

Cases

Case 1: Equip

Equip an item into player 1 slots.

Why this step matters: Equipping is a pure move from inventory to slot. There is no hidden state - just a deterministic placement change.

Preconditions:

  • The item exists in the player inventory from the seed commit.
  • The target slot is empty.

What to notice in the code:

  • One MoveInstance operation targeting a slot container
  • Idempotency key tied to the equip action

Expected state change:

  • The item leaves the inventory grid and occupies the target slot.

Operations

  • MoveInstance - Moves an existing instance to a new location.
await write_client.actions.move_instance(
    instance="inst-1",
    to={"container_id": "container-32004", "kind": "slot", "slot_index": 1},
    options=ActionOptions(
        namespace_id=1,
        idempotency_key="game-inventory-equip",
        actor_id="player-1",
        metadata={
            "job_id": "job-inventory-001",
            "player_id": "player-1",
            "step": "equip",
            "trace_id": "trace-inventory-001",
        },
    ),
)

Case 2: Trade

Trade an item and currency atomically.

Why this step matters: Trades must be atomic to prevent dupes and half-completed transfers.

Preconditions:

  • An item exists in the seller inventory from seed or prior cases.
  • Both wallet balances exist.

What to notice in the code:

  • MoveInstance + TransferFungible in a single commit
  • One commit_id for item + currency settlement

Expected state change:

  • The item moves to the buyer inventory and currency moves to the seller wallet atomically.

Operations

  • MoveInstance - Moves an existing instance to a new location.
  • TransferFungible - Transfers fungible quantity between structured containers.
await write_client.commit_operations(
    [
        MoveInstance(
            instance="inst-2",
            to={
                "container_id": "container-32003",
                "kind": "grid_cell",
                "position": 2,
                "rotation": None,
            },
        ),
        TransferFungible(
            class_id="class-42006",
            from_container="container-32007",
            key=None,
            quantity="50",
            to_container="container-32006",
        ),
    ],
    namespace_id=1,
    idempotency_key="game-inventory-trade",
    actor_id="trade-service",
    metadata={
        "from_player": "player-1",
        "job_id": "job-inventory-001",
        "step": "trade",
        "to_player": "player-2",
        "trace_id": "trace-inventory-001",
    },
)

Case 3: Drop

Drop an item into zone loot.

Why this step matters: Dropping is a move from the player inventory into the zone loot grid. The grid is the authoritative loot table for the encounter.

Preconditions:

  • The item exists in the player inventory.
  • The target grid anchor is free.

What to notice in the code:

  • MoveInstance only specifies the target; the source is the instance’s current location.
  • Grid anchor specifies deterministic loot placement
  • The item leaves player inventory and becomes zone-owned loot

Expected state change:

  • The item is removed from the player inventory and placed in zone loot.

Operations

  • MoveInstance - Moves an existing instance to a new location.
await write_client.actions.move_instance(
    instance="inst-4",
    to={
        "container_id": "container-32001",
        "kind": "grid_cell",
        "position": 6,
        "rotation": None,
    },
    options=ActionOptions(
        namespace_id=1,
        idempotency_key="game-inventory-drop",
        actor_id="player-2",
        metadata={
            "job_id": "job-inventory-001",
            "player_id": "player-2",
            "step": "drop",
            "trace_id": "trace-inventory-001",
        },
    ),
)

Case 4: Pickup

Pick up loot into player 1 inventory.

Why this step matters: Pickups are deterministic moves from zone loot into player inventory - no ambiguity about who owns the item.

Preconditions:

  • The item exists in the zone loot grid (from the drop case).
  • The target inventory anchor is free.

What to notice in the code:

  • Same instance ID moves across containers
  • Commit metadata ties the action to the player

Expected state change:

  • The item leaves zone loot and returns to a player inventory slot.

Operations

  • MoveInstance - Moves an existing instance to a new location.
await write_client.actions.move_instance(
    instance="inst-4",
    to={
        "container_id": "container-32002",
        "kind": "grid_cell",
        "position": 19,
        "rotation": None,
    },
    options=ActionOptions(
        namespace_id=1,
        idempotency_key="game-inventory-pickup",
        actor_id="player-1",
        metadata={
            "job_id": "job-inventory-001",
            "player_id": "player-1",
            "step": "pickup",
            "trace_id": "trace-inventory-001",
        },
    ),
)

Case 5: Stash Blocked (Expected error)

Blocked stash returns POSITION_OCCUPIED.

Why this step matters: Stash collisions are validated before mutation. No partial writes, no cleanup logic.

Preconditions:

  • A stash blocker occupies the target anchor (seed commit).

What to notice in the code:

  • POSITION_OCCUPIED error before any state change
  • Retry uses grid_free to find a new anchor

Expected state change:

  • No state changes are applied; this is a deterministic validation failure.

Operations

  • MoveInstance - Moves an existing instance to a new location.

Expected error

{
  "code": "POSITION_OCCUPIED",
  "detail": "Position 1 in container 8 is occupied by item at anchor 1",
  "hint": "Select an unoccupied position or move the existing item first.",
  "retryable": false,
  "status": 409,
  "title": "ConflictError",
  "type": "urn:assetcore:error:POSITION_OCCUPIED"
}
await write_client.actions.move_instance(
    instance="inst-3",
    to={
        "container_id": "container-32008",
        "kind": "grid_cell",
        "position": 1,
        "rotation": None,
    },
    options=ActionOptions(
        namespace_id=1,
        idempotency_key="game-inventory-stash",
        actor_id="player-1",
        metadata={
            "job_id": "job-inventory-001",
            "player_id": "player-1",
            "step": "stash-attempt",
            "trace_id": "trace-inventory-001",
        },
    ),
)

Case 6: Stash Grid Free

Find the next available stash anchor.

await read_client.get_container_grid_free(
    container_id="container-32008",
    namespace_id=1,
    height=2,
    width=2,
)

Case 7: Stash

Stash armor into a free grid anchor.

Operations

  • MoveInstance - Moves an existing instance to a new location.
await write_client.actions.move_instance(
    instance="inst-3",
    to={
        "container_id": "container-32008",
        "kind": "grid_cell",
        "position": 3,
        "rotation": None,
    },
    options=ActionOptions(
        namespace_id=1,
        idempotency_key="game-inventory-stash",
        actor_id="player-1",
        metadata={
            "job_id": "job-inventory-001",
            "player_id": "player-1",
            "step": "stash",
            "trace_id": "trace-inventory-001",
        },
    ),
)

Case 8: Stack Merge

Merge potion stacks in player 1 inventory.

Why this step matters: Stack consolidation keeps inventories compact while preserving total quantity.

Preconditions:

  • Multiple potion stacks exist in the inventory grid.

What to notice in the code:

  • MergeStacks uses stack IDs from the read projection

Expected state change:

  • Source stacks are consolidated into a single stack with total quantity preserved.

Operations

  • MergeStacks - Merges one fungible stack into another within a container.
await write_client.actions.merge_stacks(
    dst_stack="stack-1",
    src_stack="stack-2",
    options=ActionOptions(
        namespace_id=1,
        idempotency_key="game-inventory-stack-merge",
        actor_id="player-1",
        metadata={
            "job_id": "job-inventory-001",
            "player_id": "player-1",
            "step": "stack-merge",
            "trace_id": "trace-inventory-001",
        },
    ),
)

Case 9: Consume

Consume potions from the merged stack.

Why this step matters: Consumption is just a deterministic decrement; the audit trail shows exactly what changed.

Preconditions:

  • A potion stack exists in the inventory grid.

What to notice in the code:

  • RemoveFungible targets a grid stack location

Expected state change:

  • The stack quantity decreases by the consumed amount.

Operations

  • RemoveFungible - Removes fungible quantity from a balance or an explicit grid cell.
await write_client.actions.remove_fungible(
    class_id="class-42005",
    from_={
        "container_id": "container-32002",
        "kind": "grid_cell",
        "position": 11,
        "rotation": None,
    },
    key=None,
    quantity="2",
    options=ActionOptions(
        namespace_id=1,
        idempotency_key="game-inventory-consume",
        actor_id="player-1",
        metadata={
            "job_id": "job-inventory-001",
            "player_id": "player-1",
            "step": "consume",
            "trace_id": "trace-inventory-001",
        },
    ),
)

Validation

These reads and streams confirm the final state after the cases without mutating anything.

Stream 1: Stream Trade

Read the trade commit from SSE.

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "assetcore_read_stream",
  "params": {
    "from_world_seq": 36,
    "limit": 1,
    "namespace_id": 1
  }
}

Read 2: Read Potion Stacks

Resolve potion stack ids for merge.

await read_client.get_container_grid(
    container_id="container-32002",
    namespace_id=1,
)

Read 3: Read Slots

Verify player 1 equipment slots.

await read_client.get_container_slots(
    container_id="container-32004",
    namespace_id=1,
)

Read 4: Read P1 Inventory

Verify player 1 inventory grid.

await read_client.get_container_grid(
    container_id="container-32002",
    namespace_id=1,
)

Read 5: Read P2 Inventory

Verify player 2 inventory grid.

await read_client.get_container_grid(
    container_id="container-32003",
    namespace_id=1,
)

Read 6: Read Zone Loot

Verify zone loot grid is empty.

await read_client.get_container_grid(
    container_id="container-32001",
    namespace_id=1,
)

Read 7: Read Stash

Verify player 1 stash placements.

await read_client.get_container_grid(
    container_id="container-32008",
    namespace_id=1,
)

Read 8: Read Wallets

Verify player wallet balances after trade.

let request = client
    .get(format!("{}/v1/read/namespaces/1/containers/container-32006/balances", read_base_url))
    .bearer_auth(read_api_key)
    .send()
    .await?;
let body = request.text().await?;

Read 9: Read Wallets P2

Verify player 2 wallet balances after trade.

let request = client
    .get(format!("{}/v1/read/namespaces/1/containers/container-32007/balances", read_base_url))
    .bearer_auth(read_api_key)
    .send()
    .await?;
let body = request.text().await?;

Read 10: Read Potions

Verify consolidated potion balance in inventory grid.

let request = client
    .get(format!("{}/v1/read/namespaces/1/containers/container-32002/balances", read_base_url))
    .bearer_auth(read_api_key)
    .send()
    .await?;
let body = request.text().await?;

What This Proves: MMO-Grade Inventory Without Dupes

This scenario shows how a small, deterministic primitive set can cover equip, trade, loot drops, stash, and consumables.

Proof Point 1: Atomic Trades and Anti-Dup Guarantees

Item transfer and currency transfer happen in one commit. If the commit succeeds, both state changes are recorded; if it fails, neither happens. That is the anti-dup guarantee live-service games require.

Proof Point 2: Zone Loot as Authoritative State

Drops land in a zone loot grid (or per-encounter grid). Pickups are deterministic moves from that grid into a player inventory. The game engine renders, AssetCore guarantees authority.

Proof Point 3: Full Closure Across Inventory, Equipment, and Storage

Every item exists in exactly one container: inventory grid, equipment slot, stash grid, zone loot, or destroyed. There is no shadow state. When an item moves, it leaves one container and enters another in the same commit.

Proof Point 4: Stash and Stack Mechanics with Clear Failure Modes

Blocked placements fail before mutation, grid_free finds the next anchor, and stacks consolidate without losing quantity. Every change is auditable and replayable.

Proof Point 5: Engine-Level Integration

Every step includes a C ABI tab so engine teams can integrate without HTTP or SDK dependencies while still preserving deterministic guarantees.

Next Steps

  • Try it locally: Run the scenario to execute the full interaction suite
  • Extend it: Add crafting, auctions, or seasonal inventories with the same primitives
  • Integrate it: Use the C ABI tab to wire into your engine runtime