Documentació d'Asset Core

Documentació del motor d'estat del món determinista i referències de l'API.

Decision Gate docs

Inventari del Joc: Equipament Determinista, Comerç, Magatzem i Botí

Els jugadors tenen inventaris deterministes, espais d’equipament, reixetes d’emmagatzematge i moneders. Equipar, comerciar, obtenir botí, recollides i consolidació de piles es resolen a través d’un petit conjunt de primitives repetibles.

Si estàs llegint això sense context, aquest és un recorregut per l’inventari de producció: equipament determinista, comerç, caigudes de botí i operacions de magatzem sense seguiment del moviment dels jugadors. Cada canvi d’estat és un compromís auditable amb claus d’idempotència estables, així que els fluxos de treball de servei en viu són reproduïbles i anti-duplicats per defecte.

Per què això és important

Els sistemes d’inventari de servei en viu viuen o moren en funció de l’anti-duplicació, les transaccions atòmiques i els fluxos de treball de suport reutilitzables. Asset Core fa que l’inventari sigui autoritari tractant cada equip, intercanvi i caiguda com una transacció determinista.

Model de sistema

  • Jugadors: modelats com a propietaris i identificadors d’actors (sense seguiment de moviment dels jugadors).
  • Inventaris: contenidors de graella per a la col·locació determinista + regles de col·lisió.
  • Equipament: contenidors de ranura per a semàntica d’equip explícita.
  • Stash: contenidor de graella per mostrar mecàniques d’emmagatzematge a llarg termini.
  • Moneders: contenidors de saldo per a moneda i comerços atòmics.
  • Reixeta de botí de zona: ancoratges de botí deterministes per a trobades, cofres o caigudes.
  • Els ancoratges de botí representen espais autoritzats, no física ni moviment.
  • Classes i formes: registrades perquè les regles de col·locació es mantinguin coherents.
  • Unitats: cel (fixed_point_scale=1).

Configuració universal

Dos commits de base estableixen el domini d’inventari una vegada: la configuració crea contenidors i registra classes; la llavor crea els articles inicials, els saldos i un bloquejador d’emmagatzematge. Cada cas que segueix assumeix aquesta base per poder mantenir-se enfocat i autònom.

Setup commit: crear contenidors i registrar classes d’articles utilitzades per cada cas. Seed commit: crear articles inicials, balances de cartera, piles de pocions i el bloquejador d’emmagatzematge utilitzat en el cas de col·lisió.

Contenidors creats

  • zone_loot (id container-32001): Reixeta de botí de zona per a caigudes d’enfrontaments i recompenses de cofres.
  • p1_inventory (id container-32002): Reixeta d’inventari del jugador 1 (bossa).
  • p1_equipment (id container-32004): Espais d’equipament del jugador 1 (arma, mà secundària, armadura).
  • p1_stash (id container-32008): Reixeta d’emmagatzematge a llarg termini del jugador 1.
  • p1_wallet (id container-32006): Contenidor de saldo de la cartera del jugador 1 (moneda).
  • p2_inventory (id container-32003): Reixeta d’inventari del jugador 2.
  • p2_equipment (id container-32005): Espais d’equipament del jugador 2.
  • p2_stash (id container-32009): Reixeta d’emmagatzematge del jugador 2.
  • p2_wallet (id container-32007): Contenidor de saldo de la cartera del jugador 2.

Classes registrades

  • espasa (id class-42001): Instància d’element d’espasa.
  • escut (id class-42002): Instància d’objecte d’escut.
  • arc (id class-42003): Instància d’objecte arc.
  • armadura (id class-42004): Instància d’objecte d’armadura.
  • poció (id class-42005): Article de poció apilable.
  • or (id class-42006): Or moneda fungible.

Configuració del compromís

Registra classes/formes i crea contenidors.

Per què és important aquest pas: Aquesta és l’única vegada que creem contenidors i registrem classes d’articles. Tot el que ve després és un moviment, fusió o canvi d’equilibri en relació amb aquestes definicions.

Condicions prèvies:

  • Cap. Aquest és el commit de bootstrap.

El que crea aquest commit:

  • zone_loot (id container-32001): Reixeta de botí de zona per a caigudes d’enfrontaments i recompenses de cofres.
  • p1_inventory (id container-32002): Reixeta d’inventari del jugador 1 (bossa).
  • p1_equipment (id container-32004): Espais d’equipament del jugador 1 (arma, mà secundària, armadura).
  • p1_stash (id container-32008): Reixeta d’emmagatzematge a llarg termini del jugador 1.
  • p1_wallet (id container-32006): Contenidor de saldo de la cartera del jugador 1 (moneda).
  • p2_inventory (id container-32003): Reixeta d’inventari del jugador 2.
  • p2_equipment (id container-32005): Espais d’equipament del jugador 2.
  • p2_stash (id container-32009): Reixeta d’emmagatzematge del jugador 2.
  • p2_wallet (id container-32007): Contenidor de saldo de la cartera del jugador 2.

Què registra aquest commit:

  • espasa (id class-42001): Instància d’element d’espasa.
  • escut (id class-42002): Instància d’objecte d’escut.
  • arc (id class-42003): Instància d’objecte arc.
  • armadura (id class-42004): Instància d’objecte d’armadura.
  • poció (id class-42005): Article de poció apilable.
  • or (id class-42006): Or moneda fungible.

Què cal notar en el codi:

  • Cada trucada a CreateContainer es correspon a 1:1 amb un contenidor enumerat anteriorment.
  • Cada RegisterClass defineix les regles de col·locació i apilament que s’utilitzaran més tard.

Canvi d’estat esperat:

  • Els contenidors i les classes estan registrats i llestos per a les interaccions de joc.

Operacions

  • CreateContainer (x9) - Crea un contenidor (regió de memòria estructurada) amb el tipus sol·licitat.
  • RegisterClass (x6) - Registra una definició de classe perquè les operacions futures la puguin referenciar.
  • RegisterClassShape (x5) - Registra una empremta de forma de graella per a una classe o variant de classe.
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",
    },
)

Compromís de llavor

Inventaris de jugadors de llavors, bloqueig d’emmagatzematge i saldos.

Per què és important aquest pas: La semilla és l’únic moment en què injectem articles i saldos. Després d’això, l’estat només es mou entre contenidors o disminueix els comptes.

Condicions prèvies:

  • La configuració del commit ha creat contenidors i ha registrat classes.

Què cal notar en el codi:

  • Les instàncies d’articles es creen directament als inventaris dels jugadors i al magatzem.
  • Els saldos de cartera i les piles de pocions es financen a través d’AddFungible.
  • Un bloquejador de magatzem es col·loca intencionadament per forçar la branca POSITION_OCCUPIED.

Canvi d’estat esperat:

  • Els articles d’inici, els saldos i les piles de pocions existeixen als seus contenidors objectiu.
  • El bloquejador de stash ocupa el seu ancoratge per forçar una col·lisió determinista.

Operacions

  • AddInstance (x5) - Crea una nova instància i la col·loca en una ubicació objectiu.
  • AddFungible (x4) - Afegeix una quantitat fungible a un saldo o a una cel·la de graella 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",
    },
)

Casos a vista d’ull

  1. Equip - Equipar un article als espais del jugador 1.
  2. Comerç - Comerç d’un article i moneda de manera atòmica.
  3. Deixar - Deixa un article a la zona de botí.
  4. Recollida - Recollir el botí a l’inventari del jugador 1.
  5. Stash Bloquejat - El stash bloquejat retorna POSITION_OCCUPIED.
  6. Stash Grid Free - Troba l’ancoratge de stash disponible més proper.
  7. Amagar - Amaga armadura en un ancoratge de graella lliure.
  8. Fusionar Piles - Fusionar piles de pocions a l’inventari del jugador 1.
  9. Consume - Consume pocions de la pila fusionada.

Disseny del Sistema: Què Vam Escollir i Per Què

Aquest escenari se centra en la correcció de l’inventari de grau MMO: cada interacció és un petit compromís atòmic que es pot reproduir i auditar.

Elecció: Jugadors com a Propietaris

L’Elecció: Els jugadors són propietaris; els inventaris i els espais són contenidors propietaris, no actors en moviment.

  • Per què: La propietat aïlla l’estat, simplifica els permisos i manté el sistema centrat en l’autoritat d’inventari en lloc del moviment del jugador.
  • Compensació: El moviment del jugador és gestionat pel teu motor; mapeixes esdeveniments en compromisos d’inventari quan és necessari.
  • Quan és important: Jocs de servei en viu on els duplicats i el creuament de jugadors són inacceptables.

Elecció: Reixeta de Botí com a Ancoratges Deterministes

L’Elecció: El botí viu en un contenidor de graella amb ancoratges estables (per trobada/caixa/zona).

  • Per què: Els ancoratges fan que les caigudes i recollides siguin deterministes i auditable sense acoblar-se a la física.
  • Compensació: El renderitzat espacial és del costat del motor; Asset Core proporciona un estat de botí autoritzat.
  • Quan és important: Loot de món compartit, recompenses de raid i investigacions de suport repetibles.

Elecció: Compromisos de Comerç Atòmic

L’Elecció: La transferència d’articles + la transferència de moneda es realitzen en un únic compromís.

  • Per què: L’atomicitat evita duplicats i operacions parcials sota reintents o desconnexions.
  • Compensació: Els clients han de compondre compromisos multi-op (el comerç és una única transacció).
  • Quan és important: Comerç entre jugadors, liquidació d’ofertes i fluxos d’escrow.

Elecció: Clau d’idempotència per a la seguretat de reintents

La tria: Les claus d’idempotència són opcionals però, quan estan presents, dedupliquen les reintents basant-se en un hash canònic de la càrrega útil completa de la sol·licitud.

  • Per què: La mateixa clau + la mateixa càrrega útil retorna la resposta emmagatzemada; la mateixa clau + càrrega útil diferent és rebutjada. Això evita dobles despeses en els reintents.
  • Compensació: El servei autoritzat ha de generar o derivar claus estables per interacció i no ha de reenvair claus de clients no fiables.
  • Quan és important: Serveis de jocs basats en cues, reconexions, entrega com a mínim una vegada, i fallades entre regions.

Què No Gestionem (i Per Què)

  • Moviment del jugador: AssetCore no rastreja el moviment ni la física. El teu motor és responsable del moviment; AssetCore és responsable de l’autoritat de l’inventari.
  • Combat RNG: Les probabilitats de caiguda i els llançaments de recompenses es produeixen en sistemes de joc; AssetCore registra l’estat resultant.

Aquests límits mantenen AssetCore centrat en garanties d’inventari i transaccions deterministes.

Semàntica d’idempotència

Les claus d’idempotència són opcionals i només controlen la desduplicació de reintents. Quan hi ha una clau present, Asset Core fa un hash de la càrrega completa de la sol·licitud i utilitza (namespace, key, hash) per decidir:

  • Mateix clau + mateix càrrec: retorna la resposta emmagatzemada (sense re-execució).
  • Mateix clau + càrrega útil diferent: rebutjar amb 409 Conflict.
  • La validació fallida no reserva la clau; la reutilització després de la correcció és vàlida.
  • No key: request executes normally, retries re-run. Sense clau: la sol·licitud s’executa normalment, els reintents es tornen a executar. En fluxos autoritzats pel servidor, el servei de joc genera o deriva claus i no hauria de reenviar claus de clients no fiables.

Com llegir els casos

  • Els casos assumeixen que la configuració bàsica i els compromisos de llavor ja s’han aplicat.
  • Cada cas és una interacció enfocada (commit o lectura) que podeu reproduir de manera independent.
  • Les lectures/flux de validació es llisten per separat després dels casos.
  • Els casos d’una sola operació utilitzen helpers d’acció per a la llegibilitat.
  • Els casos de múltiples operacions es mantenen com a trucades de compromís per preservar l’atomicitat.
  • Rust HTTP sempre utilitza el punt final de commit amb una o més operacions.
  • Les pestanyes C ABI mostren una integració directa en temps d’execució per a equips de motor.

Artifacts de l’escenari

Instantània de l’escenari

{
  "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 -> graella lliure -> torna a provar amb la mateixa clau d’idempotència

Audit trail (opcional)

La transcripció captura les parelles de sol·licitud i resposta exactes emeses durant l’execució. Aquí hi ha un extracte compacte que podeu utilitzar per validar el determinisme i la propagació de metadades:

{
  "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 prevol

  • Per defecte, les accions criden a enviar compromisos.
  • Estableix ActionOptions(preflight=True) o crida assetcore_commit_preflight per a la validació.

SDK configuració

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",
)

Casos

Cas 1: Equip

Equipar un article als espais del jugador 1.

Per què aquest pas és important: L’equipament és un moviment pur de l’inventari a la ranura. No hi ha cap estat ocult - només un canvi de col·locació determinista.

Condicions prèvies:

  • L’element existeix a l’inventari del jugador des del compromís de llavor.
  • El slot objectiu està buit.

Què cal notar en el codi:

  • Una operació MoveInstance que té com a objectiu un contenidor de slots
  • Clau d’idempotència vinculada a l’acció d’equip

Canvi d’estat esperat:

  • L’element surt de la graella d’inventari i ocupa la slot objectiu.

Operacions

  • MoveInstance - Mou una instància existent a una nova ubicació.
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",
        },
    ),
)

Cas 2: Comerç

Intercanvia un article i moneda de manera atòmica.

Per què és important aquest pas: Les operacions han de ser atòmiques per evitar duplicats i transferències a mitges.

Condicions prèvies:

  • Un article existeix a l’inventari del venedor a partir de llavors o casos anteriors.
  • Ambdós saldos de cartera existeixen.

Què cal notar en el codi:

  • MoveInstance + TransferFungible en un únic commit
  • Un commit_id per a l’element + liquidació de moneda

Canvi d’estat esperat:

  • L’element es mou a l’inventari del comprador i la moneda es mou al moneder del venedor de manera atòmica.

Operacions

  • MoveInstance - Mou una instància existent a una nova ubicació.
  • TransferFungible - Transfereix una quantitat fungible entre contenidors estructurats.
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",
    },
)

Cas 3: Caiguda

Deixa un article a la zona de botí.

Per què aquest pas és important: Deixar caure és un moviment de l’inventari del jugador a la graella de botí de la zona. La graella és la taula de botí autoritzada per l’encontre.

Condicions prèvies:

  • L’element existeix a l’inventari del jugador.
  • L’ancora de la graella objectiu està lliure.

Què cal notar en el codi:

  • MoveInstance només especifica l’objectiu; la font és la ubicació actual de la instància.
  • L’ancoratge de la graella especifica la col·locació de botí determinista
  • L’objecte surt de la inventari del jugador i es converteix en botí propietat de la zona.

Canvi d’estat esperat:

  • L’objecte es treu de l’inventari del jugador i es col·loca en el botí de la zona.

Operacions

  • MoveInstance - Mou una instància existent a una nova ubicació.
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",
        },
    ),
)

Cas 4: Recollida

Recollir el botí a l’inventari del jugador 1.

Per què és important aquest pas: Les recollides són moviments deterministes del botí de la zona a l’inventari del jugador - no hi ha ambigüitat sobre qui és el propietari de l’objecte.

Condicions prèvies:

  • L’element existeix a la graella de botí de la zona (del cas de caiguda).
  • L’ancora d’inventari objectiu està lliure.

Què cal notar en el codi:

  • La mateixa ID d’instància es mou entre contenidors
  • La metadada del compromís vincula l’acció al jugador

Canvi d’estat esperat:

  • L’objecte deixa el botí de la zona i torna a una ranura d’inventari del jugador.

Operacions

  • MoveInstance - Mou una instància existent a una nova ubicació.
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",
        },
    ),
)

Cas 5: Stash Bloquejat (Error esperat)

Bloqueig de l’emmagatzematge retorna POSITION_OCCUPIED.

Per què aquest pas és important: Les col·lisions de stash es validen abans de la mutació. Sense escriptures parcials, sense lògica de neteja.

Condicions prèvies:

  • Un bloquejador de stash ocupa l’ancora objectiu (commit de llavor).

Què cal notar en el codi:

  • Error POSITION_OCCUPIED abans de qualsevol canvi d’estat
  • Retry utilitza grid_free per trobar un nou ancoratge

Canvi d’estat esperat:

  • No s’apliquen canvis d’estat; aquesta és una fallada de validació determinista.

Operacions

  • MoveInstance - Mou una instància existent a una nova ubicació.

Error esperat

{
  "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",
        },
    ),
)

Cas 6: Stash Grid Lliure

Troba l’ancora de stash disponible més propera.

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

Cas 7: Stash

Emmagatzemar armadura en un ancoratge de gra lliure.

Operacions

  • MoveInstance - Mou una instància existent a una nova ubicació.
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",
        },
    ),
)

Cas 8: Fusió de Pila

Fusiona les piles de pocions a l’inventari del jugador 1.

Per què és important aquest pas: La consolidació de les piles manté els inventaris compactes mentre preserva la quantitat total.

Condicions prèvies:

  • Existeixen múltiples piles de pocions a la graella d’inventari.

Què cal notar en el codi:

  • MergeStacks utilitza identificadors de pila de la projecció de lectura

Canvi d’estat esperat:

  • Les piles d’origen es consoliden en una única pila amb la quantitat total preservada.

Operacions

  • MergeStacks - Fusiona una pila fungible dins d’una altra dins d’un contenidor.
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",
        },
    ),
)

Cas 9: Consum

Consumeix pocions de la pila fusionada.

Per què és important aquest pas: El consum és només un decrement determinista; la pista d’auditoria mostra exactament què ha canviat.

Condicions prèvies:

  • Existeix una pila de pocions a la graella d’inventari.

Què cal notar en el codi:

  • RemoveFungible apunta a una ubicació de pila de graella

Canvi d’estat esperat:

  • La quantitat de la pila disminueix per l’import consumit.

Operacions

  • RemoveFungible - Elimina la quantitat fungible d’un saldo o d’una cel·la de la graella 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",
        },
    ),
)

Validació

Aquests lectures i fluxos confirmen l’estat final després dels casos sense mutar res.

Stream 1: Stream Trade

Llegeix el compromís comercial de SSE.

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

Llegir 2: Llegir les piles de pocions

Resolució dels identificadors de pila de pocions per a la fusió.

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

Llegir 3: Llegir Slots

Verifiqueu els espais d’equipament del jugador 1.

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

Llegir 4: Llegir l’inventari de P1

Verifiqueu la graella d’inventari del jugador 1.

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

Llegir 5: Llegir l’inventari de P2

Verifiqueu la graella d’inventari del jugador 2.

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

Llegir 6: Llegir el Botí de la Zona

Verifiqueu que la graella de botí de la zona estigui buida.

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

Llegir 7: Llegir Stash

Verifiqueu les ubicacions de l’estoc del jugador 1.

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

Llegir 8: Llegir Moneders

Verifiqueu els saldos de les carteres dels jugadors després del comerç.

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?;

Llegir 9: Llegir Moneders P2

Verifiqueu els saldos de la cartera del jugador 2 després del comerç.

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?;

Llegir 10: Llegir Pocions

Verifiqueu el saldo consolidat de pocions a la graella d’inventari.

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?;

Què Prova Això: Inventari de Grau MMO Sense Duplicats

Aquest escenari mostra com un petit conjunt primitiu determinista pot cobrir equip, comerç, caigudes de botí, emmagatzematge i consumibles.

Punt de Prova 1: Comerços Atòmics i Garanties Anti-Dup

La transferència d’articles i la transferència de moneda es realitzen en un únic compromís. Si el compromís té èxit, ambdues modificacions d’estat es registren; si falla, cap de les dues es realitza. Aquesta és la garantia anti-duplicació que requereixen els jocs de servei en viu.

Punt de prova 2: Zona de loot com a Estat Autoritari

Les caigudes aterren en una graella de botí de zona (o graella per trobada). Les recollides són moviments deterministes d’aquesta graella cap a l’inventari d’un jugador. El motor del joc renderitza, AssetCore garanteix l’autoritat.

Punt de prova 3: Tancament complet a través d’inventari, equipament i emmagatzematge

Cada element existeix en exactament un contenidor: graella d’inventari, espai d’equipament, graella de magatzem, botí de zona o destruït. No hi ha estat d’ombra. Quan un element es mou, deixa un contenidor i entra en un altre en el mateix compromís.

Punt de prova 4: Mecàniques de Stash i Stack amb Modes de Fallada Clars

Les ubicacions bloquejades fallen abans de la mutació, grid_free troba l’ancora següent, i les piles es consoliden sense perdre quantitat. Cada canvi és auditable i reproduïble.

Punt de prova 5: Integració a nivell de motor

Cada pas inclou una pestanya C ABI perquè els equips de motor puguin integrar-se sense dependències d’HTTP o SDK mentre es mantenen garanties deterministes.

Properes passes

  • Prova-ho localment: Executa l’escenari per executar el conjunt complet d’interaccions
  • Amplia-ho: Afegeix creació, subhastes o inventaris estacionals amb les mateixes primitives
  • Integra-ho: Utilitza la pestanya C ABI per connectar-te al teu temps d’execució del motor