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",
},
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Grid;
kind.capacity = 25;
kind.grid_width = 5;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32001, kind, 73, NULL, &out_container_id),
"asset_tx_create_container[0]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Grid;
kind.capacity = 25;
kind.grid_width = 5;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32002, kind, 71, NULL, &out_container_id),
"asset_tx_create_container[1]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Grid;
kind.capacity = 25;
kind.grid_width = 5;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32003, kind, 72, NULL, &out_container_id),
"asset_tx_create_container[2]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Slots;
kind.slots = 4;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32004, kind, 71, NULL, &out_container_id),
"asset_tx_create_container[3]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Slots;
kind.slots = 4;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32005, kind, 72, NULL, &out_container_id),
"asset_tx_create_container[4]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Balance;
kind.quantization_inv = 1;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32006, kind, 71, NULL, &out_container_id),
"asset_tx_create_container[5]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Balance;
kind.quantization_inv = 1;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32007, kind, 72, NULL, &out_container_id),
"asset_tx_create_container[6]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Grid;
kind.capacity = 25;
kind.grid_width = 5;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32008, kind, 71, NULL, &out_container_id),
"asset_tx_create_container[7]");
}
{
AssetContainerKind kind = {0};
kind.kind = AssetContainerKindTag_Grid;
kind.capacity = 25;
kind.grid_width = 5;
uint32_t out_container_id = 0;
check_ok(
asset_tx_create_container(runtime, tx, 32009, kind, 72, NULL, &out_container_id),
"asset_tx_create_container[8]");
}
{
AssetClassBehavior behavior = {0};
behavior.has_balance_scale = 1;
behavior.balance_scale = 1;
uint32_t out_class_id = 0;
check_ok(asset_tx_register_class(runtime,
tx,
42001,
0,
&behavior,
(const uint8_t*)"item-sword",
10,
&out_class_id),
"asset_tx_register_class[9]");
}
{
AssetClassBehavior behavior = {0};
behavior.has_balance_scale = 1;
behavior.balance_scale = 1;
uint32_t out_class_id = 0;
check_ok(asset_tx_register_class(runtime,
tx,
42002,
0,
&behavior,
(const uint8_t*)"item-shield",
11,
&out_class_id),
"asset_tx_register_class[10]");
}
{
AssetClassBehavior behavior = {0};
behavior.has_balance_scale = 1;
behavior.balance_scale = 1;
uint32_t out_class_id = 0;
check_ok(asset_tx_register_class(runtime,
tx,
42003,
0,
&behavior,
(const uint8_t*)"item-bow",
8,
&out_class_id),
"asset_tx_register_class[11]");
}
{
AssetClassBehavior behavior = {0};
behavior.has_balance_scale = 1;
behavior.balance_scale = 1;
uint32_t out_class_id = 0;
check_ok(asset_tx_register_class(runtime,
tx,
42004,
0,
&behavior,
(const uint8_t*)"item-armor",
10,
&out_class_id),
"asset_tx_register_class[12]");
}
{
AssetClassBehavior behavior = {0};
behavior.has_balance_scale = 1;
behavior.balance_scale = 1;
uint32_t out_class_id = 0;
check_ok(asset_tx_register_class(runtime,
tx,
42005,
0,
&behavior,
(const uint8_t*)"item-potion",
11,
&out_class_id),
"asset_tx_register_class[13]");
}
{
AssetClassBehavior behavior = {0};
behavior.has_balance_scale = 1;
behavior.balance_scale = 1;
uint32_t out_class_id = 0;
check_ok(asset_tx_register_class(runtime,
tx,
42006,
0,
&behavior,
(const uint8_t*)"currency-gold",
13,
&out_class_id),
"asset_tx_register_class[14]");
}
{
AssetItemShape shape = {.width = 1, .height = 1};
uint32_t inserted = 0;
check_ok(asset_tx_register_class_shape(runtime, tx, 42001, 0, shape, &inserted),
"asset_tx_register_class_shape[15]");
}
{
AssetItemShape shape = {.width = 2, .height = 2};
uint32_t inserted = 0;
check_ok(asset_tx_register_class_shape(runtime, tx, 42002, 0, shape, &inserted),
"asset_tx_register_class_shape[16]");
}
{
AssetItemShape shape = {.width = 1, .height = 1};
uint32_t inserted = 0;
check_ok(asset_tx_register_class_shape(runtime, tx, 42003, 0, shape, &inserted),
"asset_tx_register_class_shape[17]");
}
{
AssetItemShape shape = {.width = 2, .height = 2};
uint32_t inserted = 0;
check_ok(asset_tx_register_class_shape(runtime, tx, 42004, 0, shape, &inserted),
"asset_tx_register_class_shape[18]");
}
{
AssetItemShape shape = {.width = 1, .height = 1};
uint32_t inserted = 0;
check_ok(asset_tx_register_class_shape(runtime, tx, 42005, 0, shape, &inserted),
"asset_tx_register_class_shape[19]");
}
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_commit",
"params": {
"actor_id": "game-setup",
"idempotency_key": "game-inventory-setup",
"metadata": {
"job_id": "job-inventory-001",
"step": "setup",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"external_id": "container-32001",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-73",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32002",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32003",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32004",
"kind": {
"count": 4,
"type": "slots"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32005",
"kind": {
"count": 4,
"type": "slots"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32006",
"kind": {
"quantization_inv": 1,
"type": "balance"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32007",
"kind": {
"quantization_inv": 1,
"type": "balance"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32008",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32009",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42001",
"flags": 0,
"name": "item-sword"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42002",
"flags": 0,
"name": "item-shield"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42003",
"flags": 0,
"name": "item-bow"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42004",
"flags": 0,
"name": "item-armor"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42005",
"flags": 0,
"name": "item-potion"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42006",
"flags": 0,
"name": "currency-gold"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"class_id": "class-42001",
"shape": {
"height": 1,
"width": 1
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42002",
"shape": {
"height": 2,
"width": 2
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42003",
"shape": {
"height": 1,
"width": 1
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42004",
"shape": {
"height": 2,
"width": 2
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42005",
"shape": {
"height": 1,
"width": 1
}
}
},
"op": "RegisterClassShape"
}
]
}
}let payload = json!({
"actor_id": "game-setup",
"idempotency_key": "game-inventory-setup",
"metadata": {
"job_id": "job-inventory-001",
"step": "setup",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"external_id": "container-32001",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-73",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32002",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32003",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32004",
"kind": {
"count": 4,
"type": "slots"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32005",
"kind": {
"count": 4,
"type": "slots"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32006",
"kind": {
"quantization_inv": 1,
"type": "balance"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32007",
"kind": {
"quantization_inv": 1,
"type": "balance"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32008",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-71",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"external_id": "container-32009",
"kind": {
"capacity": 25,
"grid_width": 5,
"type": "grid"
},
"owner_external_id": "owner-72",
"policies": null
},
"op": "CreateContainer"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42001",
"flags": 0,
"name": "item-sword"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42002",
"flags": 0,
"name": "item-shield"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42003",
"flags": 0,
"name": "item-bow"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42004",
"flags": 0,
"name": "item-armor"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42005",
"flags": 0,
"name": "item-potion"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"behavior": {
"balance_scale": 1
},
"class_id": "class-42006",
"flags": 0,
"name": "currency-gold"
}
},
"op": "RegisterClass"
},
{
"args": {
"request": {
"class_id": "class-42001",
"shape": {
"height": 1,
"width": 1
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42002",
"shape": {
"height": 2,
"width": 2
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42003",
"shape": {
"height": 1,
"width": 1
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42004",
"shape": {
"height": 2,
"width": 2
}
}
},
"op": "RegisterClassShape"
},
{
"args": {
"request": {
"class_id": "class-42005",
"shape": {
"height": 1,
"width": 1
}
}
},
"op": "RegisterClassShape"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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",
},
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetInstanceLocation location_0 = {0};
location_0.kind = AssetInstanceLocationKind_GridCell;
location_0.container = 32002;
location_0.position = 3;
location_0.rotation = AssetRotation_None;
{
uint64_t instance_id = 0;
check_ok(
asset_tx_add_instance_semantic(runtime, tx, 42001, 0, location_0, &instance_id),
"asset_tx_add_instance_semantic[0]");
}
AssetInstanceLocation location_1 = {0};
location_1.kind = AssetInstanceLocationKind_GridCell;
location_1.container = 32002;
location_1.position = 5;
location_1.rotation = AssetRotation_None;
{
uint64_t instance_id = 0;
check_ok(
asset_tx_add_instance_semantic(runtime, tx, 42003, 0, location_1, &instance_id),
"asset_tx_add_instance_semantic[1]");
}
AssetInstanceLocation location_2 = {0};
location_2.kind = AssetInstanceLocationKind_GridCell;
location_2.container = 32002;
location_2.position = 7;
location_2.rotation = AssetRotation_None;
{
uint64_t instance_id = 0;
check_ok(
asset_tx_add_instance_semantic(runtime, tx, 42004, 0, location_2, &instance_id),
"asset_tx_add_instance_semantic[2]");
}
AssetInstanceLocation location_3 = {0};
location_3.kind = AssetInstanceLocationKind_GridCell;
location_3.container = 32003;
location_3.position = 8;
location_3.rotation = AssetRotation_None;
{
uint64_t instance_id = 0;
check_ok(
asset_tx_add_instance_semantic(runtime, tx, 42002, 0, location_3, &instance_id),
"asset_tx_add_instance_semantic[3]");
}
AssetInstanceLocation location_4 = {0};
location_4.kind = AssetInstanceLocationKind_GridCell;
location_4.container = 32008;
location_4.position = 1;
location_4.rotation = AssetRotation_None;
{
uint64_t instance_id = 0;
check_ok(
asset_tx_add_instance_semantic(runtime, tx, 42004, 0, location_4, &instance_id),
"asset_tx_add_instance_semantic[4]");
}
AssetFungibleLocation location_5 = {0};
location_5.kind = AssetFungibleLocationKind_Balance;
location_5.container = 32006;
check_ok(asset_tx_add_fungible_semantic(runtime, tx, 42006, 0, 1000, location_5),
"asset_tx_add_fungible_semantic[5]");
AssetFungibleLocation location_6 = {0};
location_6.kind = AssetFungibleLocationKind_Balance;
location_6.container = 32007;
check_ok(asset_tx_add_fungible_semantic(runtime, tx, 42006, 0, 800, location_6),
"asset_tx_add_fungible_semantic[6]");
AssetFungibleLocation location_7 = {0};
location_7.kind = AssetFungibleLocationKind_GridCell;
location_7.container = 32002;
location_7.position = 11;
location_7.rotation = AssetRotation_None;
check_ok(asset_tx_add_fungible_semantic(runtime, tx, 42005, 0, 5, location_7),
"asset_tx_add_fungible_semantic[7]");
AssetFungibleLocation location_8 = {0};
location_8.kind = AssetFungibleLocationKind_GridCell;
location_8.container = 32002;
location_8.position = 14;
location_8.rotation = AssetRotation_None;
check_ok(asset_tx_add_fungible_semantic(runtime, tx, 42005, 0, 3, location_8),
"asset_tx_add_fungible_semantic[8]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_commit",
"params": {
"actor_id": "world-seed",
"idempotency_key": "game-inventory-seed",
"metadata": {
"job_id": "job-inventory-001",
"step": "seed",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"class_id": "class-42001",
"client_tag": "p1-sword",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 3,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42003",
"client_tag": "p1-bow",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 5,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42004",
"client_tag": "p1-armor",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 7,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42002",
"client_tag": "p2-shield",
"key": null,
"location": {
"container_id": "container-32003",
"kind": "grid_cell",
"position": 8,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42004",
"client_tag": "stash-blocker",
"key": null,
"location": {
"container_id": "container-32008",
"kind": "grid_cell",
"position": 1,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42006",
"key": null,
"location": {
"container_id": "container-32006",
"kind": "balance"
},
"quantity": "1000"
},
"op": "AddFungible"
},
{
"args": {
"class_id": "class-42006",
"key": null,
"location": {
"container_id": "container-32007",
"kind": "balance"
},
"quantity": "800"
},
"op": "AddFungible"
},
{
"args": {
"class_id": "class-42005",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 11,
"rotation": null
},
"quantity": "5"
},
"op": "AddFungible"
},
{
"args": {
"class_id": "class-42005",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 14,
"rotation": null
},
"quantity": "3"
},
"op": "AddFungible"
}
]
}
}let payload = json!({
"actor_id": "world-seed",
"idempotency_key": "game-inventory-seed",
"metadata": {
"job_id": "job-inventory-001",
"step": "seed",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"class_id": "class-42001",
"client_tag": "p1-sword",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 3,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42003",
"client_tag": "p1-bow",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 5,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42004",
"client_tag": "p1-armor",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 7,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42002",
"client_tag": "p2-shield",
"key": null,
"location": {
"container_id": "container-32003",
"kind": "grid_cell",
"position": 8,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42004",
"client_tag": "stash-blocker",
"key": null,
"location": {
"container_id": "container-32008",
"kind": "grid_cell",
"position": 1,
"rotation": null
}
},
"op": "AddInstance"
},
{
"args": {
"class_id": "class-42006",
"key": null,
"location": {
"container_id": "container-32006",
"kind": "balance"
},
"quantity": "1000"
},
"op": "AddFungible"
},
{
"args": {
"class_id": "class-42006",
"key": null,
"location": {
"container_id": "container-32007",
"kind": "balance"
},
"quantity": "800"
},
"op": "AddFungible"
},
{
"args": {
"class_id": "class-42005",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 11,
"rotation": null
},
"quantity": "5"
},
"op": "AddFungible"
},
{
"args": {
"class_id": "class-42005",
"key": null,
"location": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 14,
"rotation": null
},
"quantity": "3"
},
"op": "AddFungible"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;Cases at a glance
- Equip - Equip an item into player 1 slots.
- Trade - Trade an item and currency atomically.
- Drop - Drop an item into zone loot.
- Pickup - Pick up loot into player 1 inventory.
- Stash Blocked - Blocked stash returns POSITION_OCCUPIED.
- Stash Grid Free - Find the next available stash anchor.
- Stash - Stash armor into a free grid anchor.
- Stack Merge - Merge potion stacks in player 1 inventory.
- 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
- manifest.json - Scenario metadata and IDs.
- game_inventory_actions.json - Action-first walkthrough payloads.
- game_inventory_transcript.json - Full request/response log.
- http_snapshot.json - Condensed commit outcomes.
- game_inventory_steps.json - Deterministic step index.
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",
)#include "asset_core.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
static void check_ok(int32_t code, const char* label) {
if (code == ERROR_SUCCESS) {
return;
}
fprintf(stderr, "%s failed: %d\n", label, code);
abort();
}
OpaqueRuntime* runtime = NULL;
AssetRuntimeConfig config = {0};
config.struct_size = sizeof(AssetRuntimeConfig);
config.clock_type = AssetClockType_System;
check_ok(asset_runtime_create(&config, &runtime), "asset_runtime_create");use reqwest::Client;
use serde_json::json;
let client = Client::new();
let write_base_url = "http://localhost:8080";
let read_base_url = "http://localhost:8081";
let write_api_key = "WRITE_API_KEY";
let read_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",
},
),
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetInstanceLocation location_0 = {0};
location_0.kind = AssetInstanceLocationKind_Slot;
location_0.container = 32004;
location_0.slot = 1;
check_ok(asset_tx_move_instance_semantic(runtime, tx, 1, location_0),
"asset_tx_move_instance_semantic[0]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_move_instance",
"params": {
"actor_id": "player-1",
"idempotency_key": "game-inventory-equip",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "equip",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"instance": "inst-1",
"to": {
"container_id": "container-32004",
"kind": "slot",
"slot_index": 1
}
},
"op": "MoveInstance"
}
]
}
}let payload = json!({
"actor_id": "player-1",
"idempotency_key": "game-inventory-equip",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "equip",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"instance": "inst-1",
"to": {
"container_id": "container-32004",
"kind": "slot",
"slot_index": 1
}
},
"op": "MoveInstance"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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",
},
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetInstanceLocation location_0 = {0};
location_0.kind = AssetInstanceLocationKind_GridCell;
location_0.container = 32003;
location_0.position = 2;
location_0.rotation = AssetRotation_None;
check_ok(asset_tx_move_instance_semantic(runtime, tx, 2, location_0),
"asset_tx_move_instance_semantic[0]");
check_ok(asset_tx_transfer_fungible(runtime, tx, 32007, 32006, 42006, 0, 50),
"asset_tx_transfer_fungible[1]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_commit",
"params": {
"actor_id": "trade-service",
"idempotency_key": "game-inventory-trade",
"metadata": {
"from_player": "player-1",
"job_id": "job-inventory-001",
"step": "trade",
"to_player": "player-2",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"instance": "inst-2",
"to": {
"container_id": "container-32003",
"kind": "grid_cell",
"position": 2,
"rotation": null
}
},
"op": "MoveInstance"
},
{
"args": {
"class_id": "class-42006",
"from_container": "container-32007",
"key": null,
"quantity": "50",
"to_container": "container-32006"
},
"op": "TransferFungible"
}
]
}
}let payload = json!({
"actor_id": "trade-service",
"idempotency_key": "game-inventory-trade",
"metadata": {
"from_player": "player-1",
"job_id": "job-inventory-001",
"step": "trade",
"to_player": "player-2",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"instance": "inst-2",
"to": {
"container_id": "container-32003",
"kind": "grid_cell",
"position": 2,
"rotation": null
}
},
"op": "MoveInstance"
},
{
"args": {
"class_id": "class-42006",
"from_container": "container-32007",
"key": null,
"quantity": "50",
"to_container": "container-32006"
},
"op": "TransferFungible"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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",
},
),
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetInstanceLocation location_0 = {0};
location_0.kind = AssetInstanceLocationKind_GridCell;
location_0.container = 32001;
location_0.position = 6;
location_0.rotation = AssetRotation_None;
check_ok(asset_tx_move_instance_semantic(runtime, tx, 4, location_0),
"asset_tx_move_instance_semantic[0]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_move_instance",
"params": {
"actor_id": "player-2",
"idempotency_key": "game-inventory-drop",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-2",
"step": "drop",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"instance": "inst-4",
"to": {
"container_id": "container-32001",
"kind": "grid_cell",
"position": 6,
"rotation": null
}
},
"op": "MoveInstance"
}
]
}
}let payload = json!({
"actor_id": "player-2",
"idempotency_key": "game-inventory-drop",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-2",
"step": "drop",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"instance": "inst-4",
"to": {
"container_id": "container-32001",
"kind": "grid_cell",
"position": 6,
"rotation": null
}
},
"op": "MoveInstance"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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",
},
),
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetInstanceLocation location_0 = {0};
location_0.kind = AssetInstanceLocationKind_GridCell;
location_0.container = 32002;
location_0.position = 19;
location_0.rotation = AssetRotation_None;
check_ok(asset_tx_move_instance_semantic(runtime, tx, 4, location_0),
"asset_tx_move_instance_semantic[0]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_move_instance",
"params": {
"actor_id": "player-1",
"idempotency_key": "game-inventory-pickup",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "pickup",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"instance": "inst-4",
"to": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 19,
"rotation": null
}
},
"op": "MoveInstance"
}
]
}
}let payload = json!({
"actor_id": "player-1",
"idempotency_key": "game-inventory-pickup",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "pickup",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"instance": "inst-4",
"to": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 19,
"rotation": null
}
},
"op": "MoveInstance"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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",
},
),
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetInstanceLocation location_0 = {0};
location_0.kind = AssetInstanceLocationKind_GridCell;
location_0.container = 32008;
location_0.position = 1;
location_0.rotation = AssetRotation_None;
check_ok(asset_tx_move_instance_semantic(runtime, tx, 3, location_0),
"asset_tx_move_instance_semantic[0]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_move_instance",
"params": {
"actor_id": "player-1",
"idempotency_key": "game-inventory-stash",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "stash-attempt",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"instance": "inst-3",
"to": {
"container_id": "container-32008",
"kind": "grid_cell",
"position": 1,
"rotation": null
}
},
"op": "MoveInstance"
}
]
}
}let payload = json!({
"actor_id": "player-1",
"idempotency_key": "game-inventory-stash",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "stash-attempt",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"instance": "inst-3",
"to": {
"container_id": "container-32008",
"kind": "grid_cell",
"position": 1,
"rotation": null
}
},
"op": "MoveInstance"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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,
){
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_find_grid_free",
"params": {
"container_id": "container-32008",
"height": 2,
"namespace_id": 1,
"width": 2
}
}let request = client
.get(format!("{}/v1/read/namespaces/1/containers/container-32008/grid/free", read_base_url))
.query(&[("height", 2), ("width", 2)])
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;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",
},
),
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetInstanceLocation location_0 = {0};
location_0.kind = AssetInstanceLocationKind_GridCell;
location_0.container = 32008;
location_0.position = 3;
location_0.rotation = AssetRotation_None;
check_ok(asset_tx_move_instance_semantic(runtime, tx, 3, location_0),
"asset_tx_move_instance_semantic[0]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_move_instance",
"params": {
"actor_id": "player-1",
"idempotency_key": "game-inventory-stash",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "stash",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"instance": "inst-3",
"to": {
"container_id": "container-32008",
"kind": "grid_cell",
"position": 3,
"rotation": null
}
},
"op": "MoveInstance"
}
]
}
}let payload = json!({
"actor_id": "player-1",
"idempotency_key": "game-inventory-stash",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "stash",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"instance": "inst-3",
"to": {
"container_id": "container-32008",
"kind": "grid_cell",
"position": 3,
"rotation": null
}
},
"op": "MoveInstance"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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",
},
),
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
check_ok(asset_tx_merge_stacks(runtime, tx, stack - 1, stack - 2),
"asset_tx_merge_stacks[0]");
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_commit",
"params": {
"actor_id": "player-1",
"idempotency_key": "game-inventory-stack-merge",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "stack-merge",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"dst_stack": "stack-1",
"src_stack": "stack-2"
},
"op": "MergeStacks"
}
]
}
}let payload = json!({
"actor_id": "player-1",
"idempotency_key": "game-inventory-stack-merge",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "stack-merge",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"dst_stack": "stack-1",
"src_stack": "stack-2"
},
"op": "MergeStacks"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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",
},
),
)OpaqueTransaction* tx = NULL;
check_ok(asset_tx_begin(runtime, 1, &tx), "asset_tx_begin");
AssetFungibleLocation location_0 = {0};
location_0.kind = AssetFungibleLocationKind_GridCell;
location_0.container = 32002;
location_0.position = 11;
location_0.rotation = AssetRotation_None;
{
int64_t removed = 0;
check_ok(
asset_tx_remove_fungible_semantic(runtime, tx, 42005, 0, 2, location_0, &removed),
"asset_tx_remove_fungible_semantic[0]");
}
AssetTxMeta meta = {0};
OpaqueEventBatch* batch = NULL;
check_ok(asset_tx_commit(runtime, tx, &meta, &batch), "asset_tx_commit");
asset_event_batch_destroy(runtime, batch);{
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_remove_fungible",
"params": {
"actor_id": "player-1",
"idempotency_key": "game-inventory-consume",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "consume",
"trace_id": "trace-inventory-001"
},
"namespace_id": 1,
"operations": [
{
"args": {
"class_id": "class-42005",
"from": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 11,
"rotation": null
},
"key": null,
"quantity": "2"
},
"op": "RemoveFungible"
}
]
}
}let payload = json!({
"actor_id": "player-1",
"idempotency_key": "game-inventory-consume",
"metadata": {
"job_id": "job-inventory-001",
"player_id": "player-1",
"step": "consume",
"trace_id": "trace-inventory-001"
},
"operations": [
{
"args": {
"class_id": "class-42005",
"from": {
"container_id": "container-32002",
"kind": "grid_cell",
"position": 11,
"rotation": null
},
"key": null,
"quantity": "2"
},
"op": "RemoveFungible"
}
]
});
let response = client
.post(format!("{}/v1/write/namespaces/1/commit", write_base_url))
.bearer_auth(write_api_key)
.json(&payload)
.send()
.await?;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
}
}let request = client
.get(format!("{}/v1/read/namespaces/1/stream", read_base_url))
.query(&[("from_world_seq", 36), ("limit", 1)])
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;Read 2: Read Potion Stacks
Resolve potion stack ids for merge.
await read_client.get_container_grid(
container_id="container-32002",
namespace_id=1,
){
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_get_container_grid",
"params": {
"container_id": "container-32002",
"namespace_id": 1
}
}let request = client
.get(format!("{}/v1/read/namespaces/1/containers/container-32002/grid/cells", read_base_url))
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;Read 3: Read Slots
Verify player 1 equipment slots.
await read_client.get_container_slots(
container_id="container-32004",
namespace_id=1,
)let request = client
.get(format!("{}/v1/read/namespaces/1/containers/container-32004/slots", read_base_url))
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;Read 4: Read P1 Inventory
Verify player 1 inventory grid.
await read_client.get_container_grid(
container_id="container-32002",
namespace_id=1,
){
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_get_container_grid",
"params": {
"container_id": "container-32002",
"namespace_id": 1
}
}let request = client
.get(format!("{}/v1/read/namespaces/1/containers/container-32002/grid/cells", read_base_url))
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;Read 5: Read P2 Inventory
Verify player 2 inventory grid.
await read_client.get_container_grid(
container_id="container-32003",
namespace_id=1,
){
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_get_container_grid",
"params": {
"container_id": "container-32003",
"namespace_id": 1
}
}let request = client
.get(format!("{}/v1/read/namespaces/1/containers/container-32003/grid/cells", read_base_url))
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;Read 6: Read Zone Loot
Verify zone loot grid is empty.
await read_client.get_container_grid(
container_id="container-32001",
namespace_id=1,
){
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_get_container_grid",
"params": {
"container_id": "container-32001",
"namespace_id": 1
}
}let request = client
.get(format!("{}/v1/read/namespaces/1/containers/container-32001/grid/cells", read_base_url))
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;Read 7: Read Stash
Verify player 1 stash placements.
await read_client.get_container_grid(
container_id="container-32008",
namespace_id=1,
){
"id": 1,
"jsonrpc": "2.0",
"method": "assetcore_get_container_grid",
"params": {
"container_id": "container-32008",
"namespace_id": 1
}
}let request = client
.get(format!("{}/v1/read/namespaces/1/containers/container-32008/grid/cells", read_base_url))
.bearer_auth(read_api_key)
.send()
.await?;
let body = request.text().await?;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