Inventario del juego: Equipamiento determinista, comercio, almacenamiento y botín
Los jugadores poseen inventarios deterministas, espacios de equipamiento, cuadrículas de almacenamiento y billeteras. Equipar, comerciar, recoger botines, recoger objetos y consolidar pilas se resuelven a través de un pequeño conjunto de primitivas reproducibles.
Si estás leyendo esto sin contexto, este es un recorrido por el inventario de producción: equipamiento determinista, comercio, caídas de botín y operaciones de almacenamiento sin seguimiento del movimiento del jugador. Cada cambio de estado es un compromiso auditable con claves de idempotencia estables, por lo que los flujos de trabajo de servicio en vivo son reproducibles y anti-duplicados por defecto.
Por qué esto es importante
Los sistemas de inventario de servicio en vivo dependen de anti-dup, transacciones atómicas y flujos de trabajo de soporte reproducibles. Asset Core hace que el inventario sea autoritativo al tratar cada equipamiento, intercambio y caída como una transacción determinista.
Modelo del sistema
- Jugadores: modelados como propietarios e identificadores de actores (sin seguimiento de movimiento de jugadores).
- Inventarios: contenedores de cuadrícula para colocación determinista + reglas de colisión.
- Equipamiento: contenedores de ranura para semánticas de equipamiento explícitas.
- Stash: contenedor de cuadrícula para mostrar mecánicas de almacenamiento a largo plazo.
- Monederos: contenedores de saldo para moneda y operaciones atómicas.
- Rejilla de botín de zona: anclas de botín deterministas para encuentros, cofres o caídas.
- Los anclajes de botín representan espacios autoritativos, no física o movimiento.
- Clases y formas: registradas para que las reglas de colocación se mantengan consistentes.
- Unidades: celda (fixed_point_scale=1).
Configuración universal
Dos commits base establecen el dominio de inventario una vez: setup crea contenedores y registra clases; seed acuña los artículos iniciales, saldos y un bloqueador de almacenamiento. Cada caso que sigue asume esta base para poder mantenerse enfocado y autónomo.
Configuración del commit: crear contenedores y registrar las clases de ítems utilizadas por cada caso. Commit de semillas: acuñar ítems iniciales, saldos de billetera, pilas de pociones y el bloqueador de almacenamiento utilizado en el caso de colisión.
Contenedores creados
- zone_loot (id container-32001): Cuadrícula de botín de zona para caídas de encuentros y recompensas de cofres.
- p1_inventory (id container-32002): Rejilla de inventario del jugador 1 (bolsa).
- p1_equipment (id container-32004): Espacios de equipo del Jugador 1 (arma, mano secundaria, armadura).
- p1_stash (id container-32008): Rejilla de almacenamiento a largo plazo del jugador 1.
- p1_wallet (id container-32006): Contenedor del saldo de la billetera del Jugador 1 (moneda).
- p2_inventory (id container-32003): Rejilla de inventario del jugador 2.
- p2_equipment (id container-32005): Espacios de equipo del jugador 2.
- p2_stash (id container-32009): Rejilla de almacenamiento del jugador 2.
- p2_wallet (id container-32007): Contenedor de saldo de la billetera del Jugador 2.
Clases registradas
- sword (id class-42001): Instancia de objeto espada.
- escudo (id class-42002): Instancia de objeto de escudo.
- bow (id class-42003): Instancia de objeto arco.
- armadura (id class-42004): Instancia de objeto de armadura.
- poción (id class-42005): objeto apilable de poción.
- oro (id class-42006): Moneda fungible de oro.
Configuración del compromiso
Registrar clases/formas y crear contenedores.
Por qué este paso es importante: Esta es la única vez que creamos contenedores y registramos clases de elementos. Todo lo que sigue es un movimiento, fusión o cambio de balance en relación con estas definiciones.
Precondiciones:
- Ninguno. Este es el commit de arranque.
Lo que este commit crea:
- zone_loot (id container-32001): Cuadrícula de botín de zona para caídas de encuentros y recompensas de cofres.
- p1_inventory (id container-32002): Rejilla de inventario del jugador 1 (bolsa).
- p1_equipment (id container-32004): Espacios de equipo del Jugador 1 (arma, mano secundaria, armadura).
- p1_stash (id container-32008): Rejilla de almacenamiento a largo plazo del jugador 1.
- p1_wallet (id container-32006): Contenedor del saldo de la billetera del Jugador 1 (moneda).
- p2_inventory (id container-32003): Rejilla de inventario del jugador 2.
- p2_equipment (id container-32005): Espacios de equipo del jugador 2.
- p2_stash (id container-32009): Rejilla de almacenamiento del jugador 2.
- p2_wallet (id container-32007): Contenedor de saldo de la billetera del Jugador 2.
Lo que este commit registra:
- sword (id class-42001): Instancia de objeto espada.
- escudo (id class-42002): Instancia de objeto de escudo.
- bow (id class-42003): Instancia de objeto arco.
- armadura (id class-42004): Instancia de objeto de armadura.
- poción (id class-42005): objeto apilable de poción.
- oro (id class-42006): Moneda fungible de oro.
Qué notar en el código:
- Cada llamada a CreateContainer se mapea 1:1 a un contenedor listado arriba.
- Cada RegisterClass define las reglas de colocación y apilamiento que se utilizan más adelante.
Cambio de estado esperado:
- Los contenedores y las clases están registrados y listos para interacciones de juego.
Operaciones
CreateContainer(x9) - Crea un contenedor (región de memoria estructurada) con el tipo solicitado.RegisterClass(x6) - Registra una definición de clase para que las operaciones futuras puedan hacer referencia a ella.RegisterClassShape(x5) - Registra una huella de forma de cuadrícula para una clase o variante de clase.
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?;Compromiso de semilla
Inventarios de jugadores, bloqueador de almacenamiento y saldos.
Por qué este paso es importante: La siembra es el único momento en que inyectamos elementos y saldos. Después de esto, el estado solo se mueve entre contenedores o decrementa cuentas.
Precondiciones:
- La confirmación de configuración ha creado contenedores y registrado clases.
Qué notar en el código:
- Las instancias de los elementos se acuñan directamente en los inventarios y el almacén de los jugadores.
- Los saldos de la billetera y las pilas de pociones se financian a través de AddFungible.
- Se coloca intencionadamente un bloqueador de almacenamiento para forzar la rama POSITION_OCCUPIED.
Cambio de estado esperado:
- Los elementos iniciales, los saldos y las pilas de pociones existen en sus contenedores de destino.
- El bloqueador de almacenamiento ocupa su ancla para forzar una colisión determinista.
Operaciones
AddInstance(x5) - Crea una nueva instancia y la coloca en una ubicación objetivo.AddFungible(x4) - Añade una cantidad fungible a un saldo o a una celda de cuadrícula explícita.
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?;Casos a simple vista
- Equipar - Equipar un objeto en los espacios del jugador 1.
- Comercio - Comercia un artículo y moneda de manera atómica.
- Soltar - Soltar un objeto en el botín de la zona.
- Recoger - Recoger botín en el inventario del jugador 1.
- Stash Blocked - El stash bloqueado devuelve POSITION_OCCUPIED.
- Stash Grid Free - Encuentra el siguiente ancla de stash disponible.
- Stash - Almacena armadura en un ancla de cuadrícula libre.
- Fusión de Pilas - Fusionar pilas de pociones en el inventario del jugador 1.
- Consumir - Consumir pociones de la pila combinada.
Diseño del Sistema: Lo Que Elegimos y Por Qué
Este escenario se centra en la corrección del inventario de grado MMO: cada interacción es un pequeño compromiso atómico que puede ser reproducido y auditado.
Opción: Jugadores como Propietarios
La Elección: Los jugadores son propietarios; los inventarios y espacios son contenedores de propiedad, no actores en movimiento.
- Por qué: La propiedad aísla el estado, simplifica los permisos y mantiene el sistema enfocado en la autoridad del inventario en lugar del movimiento del jugador.
- Compensación: El movimiento del jugador es gestionado por su motor; usted mapea eventos en compromisos de inventario cuando sea necesario.
- Cuando importa: Juegos de servicio en vivo donde los duplicados y el cruce de jugadores son inaceptables.
Elección: Rejilla de Botín como Anclas Determinísticas
La Elección: El botín vive en un contenedor de cuadrícula con anclajes estables (por encuentro/cofre/zona).
- Por qué: Los anclajes hacen que las entregas y recogidas sean deterministas y auditables sin acoplarse a la física.
- Compensación: El renderizado espacial se realiza del lado del motor; Asset Core proporciona el estado de botín autoritativo.
- Cuando importa: Botín de mundo compartido, recompensas de incursiones e investigaciones de soporte repetibles.
Opción: Compromisos de Comercio Atómico
La Elección: La transferencia de ítems + la transferencia de moneda ocurren en un solo commit.
- Por qué: La atomicidad previene duplicados y transacciones parciales bajo reintentos o desconexiones.
- Compensación: Los clientes deben componer commits de múltiples operaciones (el comercio es una única transacción).
- Cuando importa: Comercio entre jugadores, liquidación de subastas y flujos de custodia.
Opción: Claves de Idempotencia para Seguridad en Reintentos
La elección: Las claves de idempotencia son opcionales, pero, cuando están presentes, deduplican los reintentos basándose en un hash canónico de la carga útil completa de la solicitud.
- Por qué: La misma clave + la misma carga útil devuelve la respuesta en caché; la misma clave + una carga útil diferente es rechazada. Esto previene el doble gasto en reintentos.
- Compensación: El servicio autoritativo debe generar o derivar claves estables por interacción y no debe reenviar claves de cliente no confiables.
- Cuando importa: Servicios de juego impulsados por cola, reconexiones, entrega al menos una vez y conmutación por error entre regiones.
Lo Que No Manejamos (y Por Qué)
- Movimiento del jugador: AssetCore no rastrea el movimiento ni la física. Tu motor es responsable del movimiento; AssetCore posee la autoridad del inventario.
- RNG de combate: Las probabilidades de caída y los lanzamientos de recompensas ocurren en los sistemas de juego; AssetCore registra el estado resultante.
Estos límites mantienen a AssetCore enfocado en garantías de inventario y transacciones deterministas.
Semántica de idempotencia
Las claves de idempotencia son opcionales y solo controlan la deduplicación de reintentos. Cuando una clave está presente, Asset Core hash el payload completo de la solicitud y utiliza (namespace, key, hash) para decidir:
- Misma clave + misma carga útil: devuelve la respuesta en caché (sin reejecución).
- Misma clave + carga útil diferente: rechazar con 409 Conflicto.
- La validación fallida no reserva la clave; la reutilización después de la corrección es válida.
- No key: request executes normally, retries re-run. Sin clave: la solicitud se ejecuta normalmente, los reintentos se vuelven a ejecutar. En flujos autoritativos del servidor, el servicio de juego genera o deriva claves y no debe reenviar claves de cliente no confiables.
Cómo leer los casos
- Los casos asumen que la configuración base y los commits de semilla ya están aplicados.
- Cada caso es una interacción enfocada (compromiso o lectura) que puedes reproducir de forma independiente.
- Las lecturas/streams de validación se enumeran por separado después de los casos.
- Los casos de operación única utilizan ayudantes de acción para mejorar la legibilidad.
- Los casos de múltiples operaciones se mantienen como llamadas de confirmación para preservar la atomicidad.
- Rust HTTP siempre utiliza el endpoint de commit con una o más operaciones.
- Las pestañas C ABI muestran la integración directa en tiempo de ejecución para los equipos de motor.
Artefactos de escenario
- manifest.json - Metadatos y IDs del escenario.
- game_inventory_actions.json - Cargas útiles de recorrido centradas en la acción.
- game_inventory_transcript.json - Registro completo de solicitudes/respuestas.
- http_snapshot.json - Resultados de compromiso condensados.
- game_inventory_steps.json - Índice de pasos determinista.
Instantánea del escenario
{
"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"
}
}
Fallo de rama
POSITION_OCCUPIED -> grid_free -> reintentar con la misma clave de idempotencia
Registro de auditoría (opcional)
La transcripción captura los pares de solicitud y respuesta exactos emitidos durante la ejecución. Aquí hay un extracto compacto que puedes usar para validar el determinismo y la propagación de metadatos:
{
"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
}
}
Nota de pre-vuelo
- Las llamadas a la acción envían confirmaciones por defecto.
- Establezca ActionOptions(preflight=True) o llame a assetcore_commit_preflight para validación.
Configuración del SDK
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";Casos
Caso 1: Equipar
Equipar un objeto en los espacios del jugador 1.
Por qué este paso es importante: Equipar es un movimiento puro de inventario a ranura. No hay un estado oculto, solo un cambio de colocación determinista.
Precondiciones:
- El elemento existe en el inventario del jugador desde el compromiso de semilla.
- El espacio objetivo está vacío.
Qué notar en el código:
- Una operación MoveInstance dirigida a un contenedor de ranuras
- Clave de idempotencia vinculada a la acción de equipar
Cambio de estado esperado:
- El elemento sale de la cuadrícula de inventario y ocupa el espacio objetivo.
Operaciones
- MoveInstance - Mueve una instancia existente a una nueva ubicación.
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?;Caso 2: Comercio
Intercambiar un elemento y una moneda de manera atómica.
Por qué este paso es importante: Las transacciones deben ser atómicas para evitar duplicados y transferencias incompletas.
Precondiciones:
- Un artículo existe en el inventario del vendedor a partir de semillas o casos anteriores.
- Ambos saldos de la billetera existen.
Qué notar en el código:
- MoveInstance + TransferFungible en un solo commit
- Un commit_id para el ítem + liquidación de moneda
Cambio de estado esperado:
- El artículo se mueve al inventario del comprador y la moneda se mueve al monedero del vendedor de manera atómica.
Operaciones
- MoveInstance - Mueve una instancia existente a una nueva ubicación.
TransferFungible- Transfiere una cantidad fungible entre contenedores estructurados.
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?;Caso 3: Caída
Dejar un objeto en la zona de botín.
Por qué este paso es importante: Dejar caer es un movimiento del inventario del jugador a la cuadrícula de botín de la zona. La cuadrícula es la tabla de botín autoritativa para el encuentro.
Precondiciones:
- El objeto existe en el inventario del jugador.
- El ancla de la cuadrícula objetivo está libre.
Qué notar en el código:
- MoveInstance solo especifica el objetivo; la fuente es la ubicación actual de la instancia.
- El ancla de la cuadrícula especifica la colocación determinista del botín
- El objeto sale del inventario del jugador y se convierte en botín de la zona.
Cambio de estado esperado:
- El objeto se elimina del inventario del jugador y se coloca en el botín de la zona.
Operaciones
- MoveInstance - Mueve una instancia existente a una nueva ubicación.
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?;Caso 4: Recogida
Recoger botín en el inventario del jugador 1.
Por qué este paso es importante: Las recogidas son movimientos deterministas del botín de la zona al inventario del jugador: no hay ambigüedad sobre quién es el propietario del objeto.
Precondiciones:
- El artículo existe en la cuadrícula de botín de la zona (del caso de caída).
- El ancla de inventario objetivo está libre.
Qué notar en el código:
- La misma ID de instancia se mueve entre contenedores
- Los metadatos del commit vinculan la acción al jugador
Cambio de estado esperado:
- El objeto sale del botín de la zona y regresa a un espacio de inventario del jugador.
Operaciones
- MoveInstance - Mueve una instancia existente a una nueva ubicación.
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?;Caso 5: Stash Bloqueado (Error esperado)
El stash bloqueado devuelve POSITION_OCCUPIED.
Por qué este paso es importante: Las colisiones de stash se validan antes de la mutación. Sin escrituras parciales, sin lógica de limpieza.
Precondiciones:
- Un bloqueador de stash ocupa el ancla objetivo (commit semilla).
Qué notar en el código:
- Error POSITION_OCCUPIED antes de cualquier cambio de estado
- Retry utiliza grid_free para encontrar un nuevo ancla
Cambio de estado esperado:
- No se aplican cambios de estado; este es un fallo de validación determinista.
Operaciones
- MoveInstance - Mueve una instancia existente a una nueva ubicación.
Error esperado
{
"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?;Caso 6: Stash Grid Gratis
Encuentra el siguiente ancla de almacenamiento disponible.
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?;Caso 7: Stash
Almacena la armadura en un ancla de cuadrícula libre.
Operaciones
- MoveInstance - Mueve una instancia existente a una nueva ubicación.
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?;Caso 8: Fusión de Pilas
Fusionar pilas de pociones en el inventario del jugador 1.
Por qué este paso es importante: La consolidación de pilas mantiene los inventarios compactos mientras preserva la cantidad total.
Precondiciones:
- Existen múltiples pilas de pociones en la cuadrícula de inventario.
Qué notar en el código:
- MergeStacks utiliza identificadores de pila de la proyección de lectura
Cambio de estado esperado:
- Las pilas de origen se consolidan en una única pila con la cantidad total preservada.
Operaciones
MergeStacks- Fusiona un conjunto fungible en otro dentro de un contenedor.
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?;Caso 9: Consumir
Consume pociones de la pila combinada.
Por qué este paso es importante: El consumo es solo un decremento determinista; la auditoría muestra exactamente qué cambió.
Precondiciones:
- Existe una pila de pociones en la cuadrícula de inventario.
Qué notar en el código:
RemoveFungibleapunta a una ubicación de pila de cuadrícula
Cambio de estado esperado:
- La cantidad en la pila disminuye según la cantidad consumida.
Operaciones
RemoveFungible- Elimina la cantidad fungible de un saldo o de una celda de cuadrícula explícita.
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?;Validación
Estas lecturas y flujos confirman el estado final después de los casos sin mutar nada.
Stream 1: Comercio de Flujos
Lee el compromiso comercial de 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?;Leer 2: Leer Pilas de Poción
Resolver los identificadores de pila de pociones para la fusión.
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?;Leer 3: Leer Slots
Verificar los espacios de equipo del jugador 1.
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?;Leer 4: Leer Inventario P1
Verificar la cuadrícula de inventario del jugador 1.
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?;Leer 5: Leer Inventario P2
Verificar la cuadrícula de inventario del jugador 2.
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?;Leer 6: Leer Botín de Zona
Verifique que la cuadrícula de botín de la zona esté vacía.
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?;Leer 7: Leer Stash
Verificar las ubicaciones del almacén del jugador 1.
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?;Leer 8: Leer Carteras
Verificar los saldos de las billeteras de los jugadores después del intercambio.
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?;
Leer 9: Leer Carteras P2
Verificar los saldos de la billetera del jugador 2 después del intercambio.
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?;
Leer 10: Leer Pociones
Verificar el saldo consolidado de pociones en la cuadrícula de inventario.
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?;
Lo que esto demuestra: Inventario de grado MMO sin duplicados
Este escenario muestra cómo un pequeño conjunto de primitivas deterministas puede cubrir equipamiento, comercio, botines, almacenamiento y consumibles.
Punto de Prueba 1: Operaciones Atómicas y Garantías Anti-Dup
La transferencia de elementos y la transferencia de moneda ocurren en un solo commit. Si el commit tiene éxito, ambos cambios de estado se registran; si falla, ninguno ocurre. Esa es la garantía anti-duplicación que requieren los juegos de servicio en vivo.
Punto de Prueba 2: Botín de Zona como Estado Autoritario
Las caídas aterrizan en una cuadrícula de botín de zona (o cuadrícula por encuentro). Las recogidas son movimientos deterministas desde esa cuadrícula hacia el inventario de un jugador. El motor del juego renderiza, AssetCore garantiza la autoridad.
Punto de Prueba 3: Cierre Completo a Través de Inventario, Equipos y Almacenamiento
Cada elemento existe en exactamente un contenedor: cuadrícula de inventario, ranura de equipo, cuadrícula de almacenamiento, botín de zona o destruido. No hay un estado sombra. Cuando un elemento se mueve, sale de un contenedor y entra en otro en el mismo compromiso.
Punto de Prueba 4: Mecánicas de Almacenamiento y Apilamiento con Modos de Fallo Claros
Las colocaciones bloqueadas fallan antes de la mutación, grid_free encuentra el siguiente ancla, y las pilas se consolidan sin perder cantidad. Cada cambio es auditable y reproducible.
Prueba 5: Integración a Nivel de Motor
Cada paso incluye una pestaña C ABI para que los equipos de motor puedan integrarse sin dependencias de HTTP o SDK, mientras se preservan las garantías deterministas.
Próximos Pasos
- Pruébalo localmente: Ejecuta el escenario para llevar a cabo la suite completa de interacciones
- Amplíalo: Añade elaboración, subastas o inventarios estacionales con las mismas primitivas
- Integrarlo: Utiliza la pestaña C ABI para conectar con el tiempo de ejecución de tu motor