Workflows
Worked AI sessions: a map from nothing, a fighting NPC with define_boss, an item with an auto-rendered icon, a test build on a real server, and the pitfalls.
11 min read
How to use these examples#
Each workflow below is a sequence of tool calls in the order the server expects them, with the arguments that matter and what to look for in the results. You do not type these calls yourself — you describe what you want and your assistant makes them — but knowing the shape of a good session lets you spot a bad one: an assistant that skips list_assets, invents an id, or places an NPC without reading the map first is about to break something.
Every example assumes the server is connected and a project is selected, as described in Setup. Full argument lists are in the Tools reference.
Bootstrap a map without the editor#
The from-scratch path: a game folder with no maps, an assistant, and no editor running. It produces a map the editor opens normally.
- Orient.
get_project_summaryreports the maps that exist and the starting map.list_mapsconfirms there is nothing yet. - Scaffold the map.
create_map({ map: "home" })writes the six files a map is —regions.jsonwith no placements,terrain_data.json,water.json, the height-layer and walkability binaries, andhome_region.tscn— plus an emptydata/folder for the terrain surface. The defaultsize_tilesof 128 gives the standard playable area, tiles -64..63 on both axes, and the result reports thosetile_bounds. - Shape the ground.
generate_terrain({ map: "home", ops: [...] })builds the working heightfield from an ordered op-stack — typically anoisepreset, thenstampfeatures,carve_riverorcarve_path, anerosionpass and abiomeorpaint_terrain_materialop. The result is a slope report withmax_slope_degandwalkable_fraction; iterate withsculpt_terrainandread_terrainuntil the relief matches and stays walkable where players must walk. - Preview the bake.
bake_terrain({ map: "home", dryRun: true })returns the diff and a validation report without writing: blocked tiles added and removed, andflagged_spawns— placements that would be stranded on a tile steeper than the walkable ceiling (45 degrees by default) or outside the baked surface. - Bake.
bake_terrain({ map: "home" })rewrites the server's walkability file and exportsheightmap.r16,control.rawandbake_meta.jsonunderterrain_work/bake/home/. The Terrain3D surface the player sees is then imported one of three ways, and the result'splane_a_importsays which: a live editor on the bridge imports it immediately; with no editor, the server runs the editor's own importer under a headless Godot binary (VASTOPIA_GODOTorGODOT_PATH) so the surface exists right away; with neither, the editor imports the pending bake automatically the next time the map is opened. There is no manual import step in any case. - Make it the entry point.
set_starting_map({ map: "home" })writes the map into bothproject.vastopiaandgame_config.json. - Place content.
get_map_layout({ map: "home", walkability: true })shows an ASCII grid where.means walkable rather than merely unoccupied. Thenplace_npc_spawner,place_object_on_map,define_locationandplace_propsas needed — see Regions and elements for what each element type does in game. - Look at it. Open the project in the editor: the map lists like any other, and
show_in_editor({ map: "home", tile: [0, 0] })points the camera at the origin once the bridge is up.
The generate-terrain prompt bundles steps 3 to 5 — it asks the assistant to translate a landscape description into an op-stack, check slopes, dry-run the bake, then commit. For the editor side of the same features see Terrain.
Author an NPC that fights#
An NPC cannot attack until a fight is bound to it. Combat stats on the definition are the numbers; define_boss is what makes them swing, and it is the standard tool for any attacking NPC, not only bosses. This is the sequence the bundled new-enemy prompt follows.
- Find a body.
list_assets({ query: "wolf", type: "MODEL_3D" })is the only way to discover a validmodel_asset_id. If nothing fits, the assistant canimport_asseta model you have on disk (FBX and OBJ are converted with Blender when one is installed),pull_cloud_assetone from the cloud library, orrequest_assetto record that art is still needed and stop. - Create the definition.
create_npcwith the model, a right-click option set and acombatblock:
{
"name": "Dire Wolf",
"model_asset_id": "01K...",
"tiles": 1,
"options": ["Attack", "Examine"],
"combat": {
"hitpoints": 40,
"max_hit": 6,
"combat_level": 18,
"attack_speed": 4,
"attack_distance": 1,
"aggressive": true,
"respawn_ticks": 50
}
}
The server allocates the npc_id and writes both halves of the dual export. Unspecified combat fields get defaults (10 hitpoints, max hit 1, attack speed 4 ticks, attack distance 1 tile, not aggressive, attackable, 50 respawn ticks); options defaults to ["Talk-to", "Examine"] when omitted, so pass "Attack" explicitly for an enemy.
- Bind a fight.
define_bosswith the new id. A simple attacker is one melee attack using the definition's damage and accuracy, and no phases:
{
"fight_id": "dire_wolf",
"name": "Dire Wolf",
"npc_ids": [12],
"attacks": [{ "attack_id": "bite", "style": "melee" }]
}
Phase 0 is created for you and is always active from spawn. For a real boss, add more attacks and phases with trigger_type: "hp_below" and a trigger_value; an attack with custom_script delegates one swing to a C# NpcCombatScript, and a fight-level custom_script hands over the whole fight. Either script must already exist in server/scripts/combat/ — write_server_script({ kind: "combat", name: "..." }) creates it from the engine's template — or the tool refuses. The fight appears in the editor's Combat Scripts tool under the Tools tab, in the folder you give it.
- Give it loot.
set_drop_table({ npc_id: 12, drops: [{ "item_id": 7, "amount_min": 5, "amount_max": 20, "chance": 100 }] }). The default system isindependent(each entry rolls its own 0–100 chance);tableandwheelare the other two. Everyitem_idmust come fromlist_items. Details are in Loot tables. - Place it.
get_map_layoutfirst, thenplace_npc_spawner({ map: "home", npc_id: 12, tileX: 14, tileZ: -9, behavior: "wander", spawn_count: 3, spawn_radius: 4 }). The tool writes the spawner intoregions.json, re-derives the server'snpc_spawnsexport, and refreshes a live editor showing that map. - Test. With the editor open,
play_localruns the same pipeline as the Play button and returns the editor's diagnostic report — the phase a failure died in, timings, and the tail of the server log. Without an editor, the test-build workflow below is the alternative.
To make an NPC talk instead, bind an option to a dialogue at creation: options: [{ "label": "Talk-to", "dialogue_id": "elder_intro" }]. The dialogue must already exist from define_dialogue, and a dialogue no option points at is unreachable in game. More on the definition fields in NPCs.
Create an item with an auto-rendered icon#
Items with a 3D model get their inventory icon rendered from it — the same pipeline as the Item editor's Take Snapshot button — so nobody draws one by hand.
list_assets({ query: "sword", type: "MODEL_3D" })for the model id.create_item:
{
"name": "Iron Sword",
"description": "A plain but serviceable blade.",
"model_asset_id": "01K...",
"is_equippable": true,
"slot": "right_hand",
"interact_options": ["Equip", "Drop"],
"value": 120
}
Because a model_asset_id was given and no icon_path, the tool renders the icon right after creating the item and stamps the paths onto it. This automatic render runs in a throwaway Godot process rather than through your editor, so it needs a Godot 4.6 binary (VASTOPIA_GODOT, GODOT_PATH, or godot on PATH) and a machine that can open a window. The result carries an icon block — rendered: true, mode: "headless_render" and the icon_path — and the files land at assets/icons/64/<item_id>.png, assets/icons/48/<item_id>.png and assets/icons/32/<item_id>.png.
- If the render failed, the item still exists — the
iconblock saysrendered: falsewith the reason (no Godot binary, a cloud-reference model whose store copy is missing, and so on). Fix the cause, or simply callrender_item_icon({ item_id: 19, model_asset_id: "01K..." })with the editor open: that tool renders through the live editor when one is on the bridge (mode: "editor") and only falls back to the headless process otherwise. It also takesyaw(default 35),pitch(default 30),zoom,outlineandlight_anglewhen you want to re-frame an icon. - Items with no model — a potion, a key, a scroll — use
generate_item_icon({ item_id, shapes: [...] }), which draws flat fills and facets in a 128x128 space and finishes them with an outline, occlusion, light ramp, rim and contact shadow before writing the same three sizes. Passauto_icon: falsetocreate_itemonly when you intend to supply an icon another way.
Pass stackable: true for coins and ammunition, and tradeable / droppable (both default true) to lock an item down. The rest of the item fields — equipment stats, scripts, the equip transform — are covered in Items.
Publish a test build#
This is what the editor's top-bar Publish button does, as tool calls, ending on a running test server. Publishing tools need a credential — enable AI with Allow publishing checked, as in Setup — or the assistant gets a clear failure at step 3.
- Check the project.
validate_projectreports one-sided dual exports, placements of deleted entities, broken script bindings, drops of missing items and more, each with the tool that fixes it. Resolve what it finds before spending a build. - Link the game.
get_project_summaryshows the linkedgame_id. If there is none,register_gamecreates or links a platform game and stores the id inproject.vastopia; it is safe to repeat. - Allocate the build.
create_build({ build_name: "0.3.0" })returns thebuild_id, the build number and four presigned upload URLs (pck,serverPackage,gameConfig,manifest). With an editor on the bridge it also exports the PCK in the same call and reportspck_exported: true. - Produce the artifacts. With no editor,
export_pckbuilds the client PCK under headless Godot (roughly half a minute) topacks/<game>.pck.build_server_packagezipsserver/data,server/modules,server/scriptsandserver/schemasinto a deterministic_server_package.zip.game_config.jsonis already on disk. - Upload.
upload_build_artifactswith one entry per artifact —{ kind, url, path }— PUTs each file to its presigned URL with a SHA-256 checksum and returns the sizes and hashes. Useupload_pck_multipart({ build_id, path })instead for a large PCK.dryRun: truehashes everything without uploading. - Finalize.
finalize_build({ build_id, artifacts: [{ kind, size, sha256 }, ...] })submits the rows from step 5 so the platform verifies the uploads. The PCK's size and hash are mandatory here; the build then shows as Ready inlist_buildsand on the dashboard's Builds page. - Run it.
deploy_test_build({ build_id })boots the build on a test server and returnstest_server_hostandtest_server_port. A null host means the container is still starting — ask again in a few seconds.read_server_logs({ mode: "test", lines: 200 })reads its output, withcontains,excludeandmin_levelfilters for the noise. - Stop it.
stop_test_server({ build_id })when you are done. A test server keeps running, and costs, until stopped.
Going live is a separate, confirmed step — promote_build with confirm: true, release notes and a type of update or patch — described in Push to Test and Builds and versions.
Common pitfalls#
| Symptom | Cause | What to do |
|---|---|---|
| The NPC stands there and never attacks | Combat stats were set but no fight is bound. | define_boss for the npc_id — even one melee attack with definition damage. Avoid binding a raw combat_script_path on the NPC; fights registered in the tool override it. |
| A dialogue was authored but never opens | No NPC option points at it. | update_npc with options: [{ "label": "Talk-to", "dialogue_id": "..." }]. |
| A trigger zone does nothing in game | define_trigger_zone writes regions.json only; there is no server export. | For area events the server runs, use a named define_location plus define_task with a location_enter trigger, or a location script. See Tasks and quests. |
| New players spawn somewhere other than the map you configured | The server hardcodes home as the spawn region. | Name the starting map home, or move the player with a server script on login. validate_project flags this. |
| An NPC or object sits inside a cliff or off the map | Placement tools do not refuse blocked or out-of-range tiles. | Call get_map_layout with walkability: true before placing; keep everything within tiles -64..63 on a standard map. bake_terrain lists stranded placements in flagged_spawns. |
bake_terrain wrote files when a preview was wanted | An older convention used preview: true. | Both dryRun: true and preview: true are accepted and behave identically; use either. |
| The editor did not show what was just written | The map has unsaved changes, so refresh_editor refused. | Save in the editor, then refresh. Only force: true overrides, and it discards the unsaved work. |
| A script was rejected before it was written | The structural check found an unbalanced bracket or an unterminated string. | Fix the source. allow_lint_errors: true exists only for a false positive. |
<, > or & appear literally in a .cs file | The body was XML-escaped before being passed; write_server_script writes it verbatim. | Pass raw C#. |
| A helper from one script is "not found" in another | Each script file compiles on its own. | Put shared logic in a module and reach it with ModuleLoader.Module<FooModule>(); generate_script_project then compile-checks everything before launch. See IDE setup. |
| An item's icon is a hand-drawn shape although it has a model | generate_item_icon was used on a modelled item. | render_item_icon for any item with a mesh; procedural icons are for model-less items only. |
| A shop exists but nothing opens it | Shops are opened by a server script or a dialogue action, not by the Trade option alone. | Bind an NPC script (write_server_script({ kind: "npc" })) or a dialogue action node that opens the shop_id. |
| Recipes grant no XP | skill_rewards was omitted; it is the only form the runtime reads. | define_recipe with skill_rewards: [{ skill_id, experience }]. |
| Another chat started writing to a different game | select_project re-points the whole server process. | Check get_active_project before writing when more than one conversation shares the server. |
| Writes suddenly refused with a rate-limit error | More than 120 mutating calls in a minute from one actor. | Wait, or raise VASTOPIA_RATE_MAX. Read-only tools are never limited. |
| Content vanished from players' inventories after an update | An item was deleted from a live game; saves hold bare ids. | Prefer making content unobtainable (out of drop tables and shops). check_references before any delete, and mention deletions in the release notes. |
For failures that show up in the game client rather than in a tool result — an NPC that is invisible, a magenta model, white terrain — see Troubleshooting.
Where to go next#
- Tools reference — every argument for the tools used above.
- Overview — the guardrails that make these sessions safe to run against a real project.
- Coordinate system — tiles, world units and the footprint anchor the placement tools assume.
- Test your game — Play Local, Push to Test and beta builds from the editor's side.
