Manter módulo que usa o SDK original host.call
DTOs concretos de ator/token, revisões opacas e os demais contratos multipart/locais.

O projeto que você vai criar
Crie módulo do SDK original com botões reais para atores e tokens. Use campanha de desenvolvimento, cena ativa e sistema externo original. Essas interfaces coexistem com a API de domínio, mas usam assinaturas e formatos de revisão diferentes.
Host alvo: Gravewright 0.3.0. As ações rodam somente quando você clica Run action; leituras e escritas usam a conta conectada. O painel de demonstração está em inglês. Cada receita abaixo informa os dados de entrada e o resultado concreto.
1. Crie os arquivos
Crie uma pasta sdk-compat. Salve os arquivos mostrados abaixo em UTF-8, ou baixe o projeto completo. manifest.json fica na raiz. Não há dependências npm nem build de JavaScript.
sdk-compat/
manifest.json
main.mjs
recipes.mjs
styles.css
LICENSE.txtAs outras operações originais estão em arquivos (asset.list/upload/download, actor.image.upload, card.asset.upload) e tradução (locale.apply). Juntos, os guias cobrem todas as operações de registry.json. Para módulo novo, prefira API de domínio, usando SDK multipart quando necessário.
2. Empacote, instale e ative
Partindo da pasta do projeto, execute no PowerShell:
python -m json.tool manifest.json
python -m zipfile -c ../sdk-compat-1.0.0.zip manifest.json main.mjs recipes.mjs styles.css LICENSE.txt
python -m zipfile -l ../sdk-compat-1.0.0.zipNo Linux/macOS, substitua python por python3 no mesmo comando. A listagem precisa mostrar manifest.json na raiz. Como proprietário, abra Inside → Módulos → Instalar módulo → Instalar de ZIP, selecione o ZIP e clique Instalar. Como GM, abra a campanha, use o botão Gravewright da mesa → Extensions e clique Activate na versão 1.0.0 de Original SDK workshop. Feche o menu e clique no botão Original SDK workshop. Se precisar de detalhes de cada tela, veja seu primeiro módulo.
O projeto fica inativo até a seleção na mesa. Teste uma extensão de demonstração por vez, pois seus botões usam a mesma posição. Para novos bytes, mude version para 1.0.1, gere outro ZIP, instale e ative essa versão; não edite o pacote instalado.
3. Execute cada receita
Ler todas as páginas autorizadas de atores
Esta é lista de DTOs do SDK original, diferente de api.actors.list. Usa paginação e revisões opacas.
async ({host,signal}) => {
const items=[];let cursor=null;
do {
const page=await host.call('actor.list',{limit:50,cursor},{signal});
items.push(...page.items);cursor=page.nextCursor;
} while(cursor&&!signal.aborted);
return items;
}Resultado esperado: Todos os atores legíveis desta campanha, reautorizados em cada página.
Como conferir: Compare IDs com o diretório e teste filtragem como jogador.
Ler DTO do ator e revisão dos dados
Cole UUID de ator legível. Metadados e dados da ficha têm domínios de revisão diferentes.
async ({host,signal},v) => ({
actor:await host.call('actor.read',{id:v.id},{signal}),
sheet:await host.call('actor.data.read',{id:v.id},{signal})
})Resultado esperado: actor.revision e sheet.revision, cada uma mantida com seu próprio retorno.
Como conferir: Não decodifique nem troque os tokens de revisão entre operações.
Criar ator descartável por host.call
API de domínio descobre o tipo do sistema selecionado. Schema antigo de criação aceita name/type, não payload arbitrário de dados.
async ({api,host,signal},v) => {
const type=(await api.actors.state({}, {signal})).actorTypes[0]?.id;
if(!type)throw new Error('Choose a system with an actor type');
return host.call('actor.create',{name:v.name,type},{signal});
}Resultado esperado: DTO do ator criado e sua revisão opaca.
Como conferir: Encontre o mesmo ID em Actors; guarde para as próximas receitas.
Renomear com revisão de metadados do SDK
Use ator descartável. changes contém apenas metadados suportados.
async ({host,signal},v) => {
const before=await host.call('actor.read',{id:v.id},{signal});
return host.call('actor.update',{id:v.id,expectedRevision:before.revision,changes:{name:v.name}},{signal});
}Resultado esperado: Mesmo ator com novo nome e revisão de metadados.
Como conferir: expectedRevision desatualizada deve gerar conflict; recarregue sem forçar.
Preservar JSON completo do ator ao mudar campo próprio
Use sistema externo de exemplo que aceite JSON original opaco, como Example Expedition. workshop pertence ao exemplo; não sobrescreva campo de outro pacote.
async ({host,signal},v) => {
const before=await host.call('actor.data.read',{id:v.id},{signal});
const data={...before.data,workshop:{note:v.note}};
return host.call('actor.data.update',{id:v.id,expectedRevision:before.revision,data},{signal});
}Resultado esperado: Dados atualizados com todos os demais campos superiores preservados.
Como conferir: Releia e compare outras chaves. Validador próprio de sistema pode recusar campos sem suporte.
Ler centro do token e os dados da própria ficha
Ative uma cena existente e use token dessa cena. O pacote monta scene.controls para ter escopo de cena.
async ({host,signal},v) => ({
token:await host.call('token.read',{id:v.id},{signal}),
sheet:await host.call('token.data.read',{id:v.id},{signal})
})Resultado esperado: position é centro em pixels da cena, diferente de gridX/gridY. Dados da ficha resolvem vínculo ou armazenamento independente.
Como conferir: Compare token vinculado e snapshot; não substitua snapshot por dados do ator.
Mover token usando sua revisão opaca
Use token controlável e centro de destino dentro da cena. O servidor converte pixels para sua geometria de grade.
async ({host,signal},v) => {
const token=await host.call('token.read',{id:v.id},{signal});
return host.call('token.move',{id:v.id,expectedRevision:token.revision,position:{x:v.x,y:v.y}},{signal});
}Resultado esperado: Posição atualizada; validação de movimento e permissões continuam valendo.
Como conferir: Tente token de outra campanha e revisão antiga; ambos devem falhar sem movimento.
Alterar dados resolvidos do token preservando outros campos
Vinculado altera dados do ator; independente altera snapshot. Use apenas sistema original de exemplo com JSON opaco.
async ({host,signal},v) => {
const before=await host.call('token.data.read',{id:v.id},{signal});
return host.call('token.data.update',{id:v.id,expectedRevision:before.revision,data:{...before.data,workshop:{note:v.note}}},{signal});
}Resultado esperado: Nova revisão de dados no domínio correto.
Como conferir: Leia ator e ambos tokens; alteração de snapshot não pode alterar o ator de origem.
Excluir ator descartável pelo SDK antigo
Remova só ator sem tokens criado pelo tutorial. Integrações novas devem preferir API de domínio, cuja exclusão nativa inclui limpeza de tokens/automações.
async ({host,signal},v) => {
if(!v.confirm)throw new Error('Confirm the disposable actor');
const actor=await host.call('actor.read',{id:v.id},{signal});
return host.call('actor.delete',{id:v.id,expectedRevision:actor.revision},{signal});
}Resultado esperado: null do contrato original de exclusão.
Como conferir: Leia novamente e espere not_found. Não substitua limpeza do ciclo nativo por esse caminho.
4. Entenda e adapte os arquivos completos
main.mjs desenha apenas o lançador e o formulário. recipes.mjs contém as ações: para adaptar o comportamento, altere a função run da receita desejada, mantendo sua assinatura. O formulário passa tipos reais (number, boolean e File), bloqueia requisições simultâneas e desenha resultados com textContent. Não há avaliação de código fornecido pelo usuário. api, host, assets e storage pertencem à montagem; signal cancela trabalho ao desativar. O modal não fecha durante uma operação pendente e é liberado ao abortar a montagem. A função de negócio continua responsável por revisões, permissões e sequência transacional.
manifest.json
{
"id": "tutorial.sdk-compat",
"name": "Original SDK workshop",
"version": "1.0.0",
"sdk": {
"requires": ">=1.0.0 <2.0.0",
"tested": "1.0.0"
},
"entry": "main.mjs",
"description": "Concrete actor/token DTOs, opaque revisions and the remaining multipart/local operations.",
"author": "Gravewright documentation",
"license": "MIT"
}main.mjs
import {recipes} from './recipes.mjs';
export default {
start({styles}) { styles.use('styles.css'); },
register({register}) {
register("scene.controls", ({root,api,host,assets,storage,context,events,viewport,signal,onDispose}) => {
const doc=root.ownerDocument;
const button=doc.createElement('button');
button.type='button';button.className='tutorial-launcher';
button.textContent="Original SDK workshop";
root.append(button);
for(const type of ['pointerdown','mousedown'])button.addEventListener(type,event=>event.stopPropagation(),{signal});
let modal,pending=false;
button.addEventListener('click',event=>{
event.stopPropagation();
if(modal?.isConnected) { modal.focus();return; }
const panel=doc.createElement('section');
panel.className="tutorial-workbench-sdk-compat";
panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');
panel.setAttribute('aria-label',button.textContent);panel.tabIndex=-1;
const title=doc.createElement('h2');title.textContent=button.textContent;panel.append(title);
const choose=doc.createElement('select');choose.setAttribute('aria-label','Action');
for(const recipe of recipes) {
const option=doc.createElement('option');option.value=recipe.id;option.textContent=recipe.label;choose.append(option);
}
const form=doc.createElement('form'),fields=doc.createElement('div');
const description=doc.createElement('p');
const run=doc.createElement('button');run.type='submit';run.textContent='Run action';
const close=doc.createElement('button');close.type='button';close.textContent='Close';
const output=doc.createElement('pre');output.setAttribute('role','status');output.setAttribute('aria-live','polite');
form.append(description,fields,run,close,output);panel.append(choose,form);
function renderFields(){
const recipe=recipes.find(item=>item.id===choose.value);fields.replaceChildren();
description.textContent=recipe.description;
for(const field of recipe.fields||[]) {
const label=doc.createElement('label');label.textContent=field.label;
const input=doc.createElement('input');input.name=field.name;input.type=field.type||'text';
if(field.type==='number')input.step=String(field.step??'any');
if(field.type==='checkbox')input.checked=!!field.value;
else if(field.type!=='file')input.value=String(field.value??'');
input.required=!!field.required;
if(field.accept)input.accept=field.accept;
label.append(input);fields.append(label);
}
output.textContent='';
}
choose.addEventListener('change',renderFields,{signal});renderFields();
form.addEventListener('submit',async event=>{
event.preventDefault();if(pending)return;
const recipe=recipes.find(item=>item.id===choose.value),values={};
for(const input of fields.querySelectorAll('input')) {
values[input.name]=input.type==='file'?input.files[0]:input.type==='checkbox'?input.checked:input.type==='number'?Number(input.value):input.value;
}
pending=true;choose.disabled=true;run.disabled=true;close.disabled=true;
for(const input of fields.querySelectorAll('input'))input.disabled=true;
output.textContent='Running…';
try {
const value=await recipe.run({api,host,assets,storage,context,events,viewport,signal,onDispose},values);
if(!signal.aborted)output.textContent=typeof value==='string'?value:JSON.stringify(value??null,null,2);
} catch(error) {
if(!signal.aborted)output.textContent=String(error.code||error.message||'unavailable');
} finally {
pending=false;
if(!signal.aborted){choose.disabled=false;run.disabled=false;close.disabled=false;for(const input of fields.querySelectorAll('input'))input.disabled=false;}
}
},{signal});
close.addEventListener('click',()=>void modal.close(),{signal});
modal=api.ui.openModal(panel,{canClose:()=>!pending||signal.aborted});
},{signal});
onDispose(()=>{button.remove();if(modal)void modal.close();});
});
},
stop() {}
};recipes.mjs
export const recipes=[
{
id:"pages",
label:"Read every authorized actor page",
description:"This is the original SDK DTO list, not api.actors.list. It paginates and includes opaque revision strings.",
fields:[],
run:async ({host,signal}) => {
const items=[];let cursor=null;
do {
const page=await host.call('actor.list',{limit:50,cursor},{signal});
items.push(...page.items);cursor=page.nextCursor;
} while(cursor&&!signal.aborted);
return items;
}
},
{
id:"read",
label:"Read one actor DTO and its data revision",
description:"Paste a readable actor UUID. Actor metadata and sheet data have different revision domains.",
fields:[
{
"name": "id",
"label": "Actor ID",
"value": "",
"type": "text",
"required": true
}
],
run:async ({host,signal},v) => ({
actor:await host.call('actor.read',{id:v.id},{signal}),
sheet:await host.call('actor.data.read',{id:v.id},{signal})
})
},
{
id:"create",
label:"Create a disposable actor through host.call",
description:"The domain API discovers the selected system type. The legacy create schema accepts name/type, not an arbitrary actor data payload.",
fields:[
{
"name": "name",
"label": "Actor name",
"value": "SDK workshop scout",
"type": "text",
"required": true
}
],
run:async ({api,host,signal},v) => {
const type=(await api.actors.state({}, {signal})).actorTypes[0]?.id;
if(!type)throw new Error('Choose a system with an actor type');
return host.call('actor.create',{name:v.name,type},{signal});
}
},
{
id:"rename",
label:"Rename with the legacy metadata revision",
description:"Use the disposable actor. changes contains only supported metadata changes.",
fields:[
{
"name": "id",
"label": "Actor ID",
"value": "",
"type": "text",
"required": true
},
{
"name": "name",
"label": "New name",
"value": "SDK navigator",
"type": "text",
"required": true
}
],
run:async ({host,signal},v) => {
const before=await host.call('actor.read',{id:v.id},{signal});
return host.call('actor.update',{id:v.id,expectedRevision:before.revision,changes:{name:v.name}},{signal});
}
},
{
id:"data",
label:"Preserve a complete actor JSON while changing one owned field",
description:"Use an external example system that accepts opaque original JSON, such as Example Expedition. workshop is owned by this example; do not overwrite fields belonging to another package.",
fields:[
{
"name": "id",
"label": "Actor ID",
"value": "",
"type": "text",
"required": true
},
{
"name": "note",
"label": "Original workshop note",
"value": "Original test note",
"type": "text",
"required": true
}
],
run:async ({host,signal},v) => {
const before=await host.call('actor.data.read',{id:v.id},{signal});
const data={...before.data,workshop:{note:v.note}};
return host.call('actor.data.update',{id:v.id,expectedRevision:before.revision,data},{signal});
}
},
{
id:"token-read",
label:"Read token center coordinates and its own sheet data",
description:"Activate an existing scene and use a token from that scene. This package mounts scene.controls so the legacy scene scope is present.",
fields:[
{
"name": "id",
"label": "Token ID",
"value": "",
"type": "text",
"required": true
}
],
run:async ({host,signal},v) => ({
token:await host.call('token.read',{id:v.id},{signal}),
sheet:await host.call('token.data.read',{id:v.id},{signal})
})
},
{
id:"token-move",
label:"Move a token using its own opaque revision",
description:"Use a token you control and an in-bounds target center. The server converts scene pixels into its grid geometry.",
fields:[
{
"name": "id",
"label": "Token ID",
"value": "",
"type": "text",
"required": true
},
{
"name": "x",
"label": "Center X in scene pixels",
"value": 160,
"type": "number",
"required": true
},
{
"name": "y",
"label": "Center Y in scene pixels",
"value": 160,
"type": "number",
"required": true
}
],
run:async ({host,signal},v) => {
const token=await host.call('token.read',{id:v.id},{signal});
return host.call('token.move',{id:v.id,expectedRevision:token.revision,position:{x:v.x,y:v.y}},{signal});
}
},
{
id:"token-data",
label:"Update the token’s resolved data without replacing unrelated fields",
description:"For a linked token this edits actor data; for an independent token it edits its snapshot. Use only the original opaque-JSON example system.",
fields:[
{
"name": "id",
"label": "Token ID",
"value": "",
"type": "text",
"required": true
},
{
"name": "note",
"label": "Original workshop note",
"value": "Snapshot-only note",
"type": "text",
"required": true
}
],
run:async ({host,signal},v) => {
const before=await host.call('token.data.read',{id:v.id},{signal});
return host.call('token.data.update',{id:v.id,expectedRevision:before.revision,data:{...before.data,workshop:{note:v.note}}},{signal});
}
},
{
id:"delete",
label:"Delete the disposable legacy actor",
description:"Remove only an unused actor created by this tutorial, with no scene tokens. New integrations should prefer the domain API, whose native deletion includes token/automation cleanup.",
fields:[
{
"name": "id",
"label": "Disposable actor ID",
"value": "",
"type": "text",
"required": true
},
{
"name": "confirm",
"label": "I selected the disposable actor",
"value": false,
"type": "checkbox",
"required": false
}
],
run:async ({host,signal},v) => {
if(!v.confirm)throw new Error('Confirm the disposable actor');
const actor=await host.call('actor.read',{id:v.id},{signal});
return host.call('actor.delete',{id:v.id,expectedRevision:actor.revision},{signal});
}
}
];styles.css
.gw-module-root[data-module-id="tutorial.sdk-compat"] .tutorial-launcher { position: fixed; left: 16px; bottom: 112px; z-index: 10; padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); color: var(--text); font-size: 14px; pointer-events: auto; max-width: min(440px, calc(100vw - 32px)); white-space: normal; }
.tutorial-workbench-sdk-compat { width: min(640px, calc(100vw - 48px)); padding: var(--space-5); font-size: 14px; }
.tutorial-workbench-sdk-compat label { display: block; margin: var(--space-3) 0; }
.tutorial-workbench-sdk-compat input, .tutorial-workbench-sdk-compat select { display: block; width: 100%; margin-top: var(--space-2); }
.tutorial-workbench-sdk-compat pre { max-height: 360px; max-width: 100%; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
.tutorial-workbench-sdk-compat button { margin-right: var(--space-3); }LICENSE.txt
MIT License
Copyright (c) 2026 Gravewright contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.5. Permissões, falhas e teste final
Teste acesso só de leitura, outra cena/campanha, revisões opacas antigas e cancelamento. Opções de host.call não fornecem o mesmo contrato de recibos dos comandos de domínio: requestId não torna toda operação SDK idempotente. Não troque revisão de metadados, dados, posição e snapshot. IDs precisam vir do estado autorizado da campanha.
Como usar a IA para estender este projeto
Migre este módulo host.call completo para a API de domínio do Gravewright 0.3.0. Compare DTO e assinatura de cada ação, preserve snapshot/autorização e explique as mudanças de revisão e recibos. Entregue arquivos completos e testes, sem apenas renomear métodos.