Veduta

Veduta game API

A Veduta game is a Go program whose main calls veduta.Run(&game.Game{}). The same binary plays on the console (a linux/arm64 board drawing on its panel's framebuffer and reading a gamepad) and, with -headless, renders, simulates and answers queries for the veduta tool on any machine, including a server with no display. Game logic lives only in the game binary; the tool never contains game code.

package main

import (
	"github.com/riftbane/veduta-engine/v2"
	"mygame/game"
)

func main() { veduta.Run(&game.Game{}) }

Interfaces

type Game interface {
	Init(ctx *veduta.Context) error              // once, after the first scene is loaded
	Update(ctx *veduta.Context, in veduta.Input) // exactly once per tick, before behaviours
	Draw(ctx *veduta.Context, dl *gfx.DrawList)  // once per rendered frame, after the scene
}

type Behaviour interface {
	Update(ctx *veduta.Context, e *scene.Entity, in veduta.Input)
}

func RegisterKind(name string, ctor func(*scene.Entity) veduta.Behaviour)

Context

MemberMeaning
Scene *scene.Scenethe world: Find(name), Get(id), Tagged(tag), Entities() (id order)
Tick uint640 during Init, then 1, 2, … during Update
RNG *sim.RNGseeded xoshiro256**: Float32(), Range(lo, hi), Intn(n), Bool(), Chance(p) — the only allowed randomness
DT float32seconds per tick (1 / tick_rate, 1/20 by default)
Headless booltrue when run by the tool
Project *asset.Projectthe parsed veduta.json
Width, Height inta frame size in pixels, never 0 once Init runs. In Draw: the size of the frame being drawn (the panel's size divided by VEDUTA_SCALE in the player, --width×--height in render, the screenshot tile in simulate). In Init, Update and behaviours: the project's resolution (the frame the game is designed for) in every mode (player, render, simulate, snapshot), so logic that reads them gives the same trace everywhere and HUD layout can be computed during Update; in the player it can differ from the frame Draw receives.
Font, FontTexturebuilt-in 8×8 font for HUD text
Trace(event, fields)emit a game event into the current tick's trace
Invariant(name, pred)register a predicate checked after every tick when listed
RegisterState(codec)save/restore the game's own state in snapshots
Spawn(tmpl) *scene.Entityadd an entity (next id); its behaviour starts next tick
SpawnPrefab(name, origin, rotation, prefix)add a prefab's entities with the min corner of its footprint at origin, turned by 0, 90, 180 or 270 degrees about +Y as a world places it, named <prefix>_<entity> (the prefab's name when prefix is empty) and parented as the prefab says; returns them in prefab order
Despawn(e)remove e and its children at the end of the tick
LoadScene(name)replace the scene (ids restart at 1), e.g. to reset a level
SetModel(name, m) erroradd or replace a model the game builds at runtime (a voxel chunk's mesh): entities name it like an asset model, with culling, LODs and DrawDistance; uploaded at the next frame. The name contains : and does not start with world:; pass a new *asset.Model to change it (the engine re-uploads when the pointer changes). Not saved in snapshots and kept across LoadScene
RemoveModel(name), Model(name)forget a runtime model; look one up
Map() *tilemap.Mapthe scene's tile map (docs/map.md), nil without one: Get(layer, x, y), Set(layer, x, y, terrain) error (a map_set event), Has(layer, x, y, tag) (layer −1: any), CellAt(x, y), Center(x, y), Layer(name), Size(), Source() (its objects)
LoadMap(name) errorreplace the scene's tile map with a map as its file describes it ("" removes it); the entities stay; a map_load event
Clip(e, name) *asset.Clip, Clips(e)a clip of the textures e draws with, and their names; play one with e.SetAnim(name) (unless it plays) or e.Play(name) (from the start): the engine sets e.Frame every tick
Overlapping(e)live entities whose AABB overlaps e's (last tick's bounds)
Texture(name)a texture asset's handle and size in texels, for Batch.Image and Batch.NineSlice in Draw (ok false for an unknown texture)
ReadSave(name), WriteSave(name, data), RemoveSave(name), SaveNames()the game's saves: JSON objects or arrays of at most 1 MiB under valid names. The player keeps them as files in VEDUTA_SAVE_DIR (default out/saves of the project; the console sets it to the card); headless runs keep them in memory, starting from the scenario's saves. Writes and removals are trace events save_write and save_remove
HUD(dl) *sprite.Batchstart a HUD batch covering the frame; call End()
Text(b, x, y, scale, s, color)draw text with the built-in font
type StateCodec interface {
	SaveState(enc *gob.Encoder) error
	LoadState(dec *gob.Decoder) error
}

Input

veduta.Input is a value type, identical whether it comes from the console or a script.

Field / methodMeaning
Pressed, Held, Releasedbutton sets (sim.Buttons)
Down(b), JustPressed(b), JustReleased(b)button queries, b one of the constants below
DPad() gmath.Vec2the D-pad as a direction: X −1 (left), 0 or +1 (right), Y −1 (down), 0 or +1 (up); opposite directions cancel; a diagonal is (±1, ±1)

A button pressed and released within one tick appears in Pressed and Released but not in Held.

The console's controls

The console has a D-pad, A, B, Select, Cancel and Home. A game sees eight buttons; Home returns to the console's home, and no game can see or swallow it.

ButtonConstantScenario nameMeant forKeyboardGamepad
D-padveduta.ButtonUp, ButtonDown, ButtonLeft, ButtonRightup, down, left, rightmoving, choosingarrows, W A S DD-pad, hat, or a stick where the D-pad is not four buttons
Aveduta.ButtonAathe main action, confirmSpace, ZA (BTN_SOUTH)
Bveduta.ButtonBbthe second actionX, ShiftB (BTN_EAST)
Selectveduta.ButtonSelectselectthe game's menuEnter, TabStart
Cancelveduta.ButtonCancelcancelback, close a menuEscape, BackspaceKEY_BACK (the handheld's Cancel)
Homenonenoneleave the gameCtrl+QHome (BTN_MODE, KEY_HOMEPAGE), or the pad's Select

A USB pad has no Home button, so its Select leaves the game and its Start is the game's menu. It has no Cancel either: a menu has to close with Select (Start on the pad) or B as well. A pad's other buttons (X, Y, shoulders) and sticks beside a four-button D-pad are ignored.

Cameras

ctx.Scene.Camera is the camera the player and render --camera scene draw with; a game may replace it during Update.

Entities

scene.Entity fields: ID, Name, Kind, Transform (Position, Rotation quaternion, Scale, relative to Parent), Model, Material, Tags, AABB (world bounds of the hitbox, else of the model, recomputed after every tick), Visible, State, Parent, Hitbox *gmath.AABB (the scene file's hitbox: a local-space box that replaces the model bounds as the source of AABB, so collisions, Overlapping, no_overlap and the trace's aabb use it; nil for none; Spawn copies it), Layer int (the scene file's layer: the first key of the draw order, lower layers drawn first; within a layer opaque parts in id order, then blended parts back to front by depth along the view axis; opaque and cutout parts write depth, so among them the nearest is in front whatever the layer, while blended parts write none, so an opaque part on a higher layer is drawn over a blended part on a lower one whatever their depth). Useful methods: WorldPosition(), World(), HasTag(t), Alive(). Use gmath for math: its Sin/Cos/Atan2 are deterministic on every platform; never use math.Sin in game logic.

Coordinates: right-handed, Y up, −Z forward, meters. Angles are degrees in files and radians in code. rotation_deg: [x, y, z] means R = Ry·Rx·Rz.

Tick timeline

  1. Tick 0: the scene is loaded (ids 1…n in scene-file order), Init runs, then the engine records tick 0 (events scene_load and any spawns from Init).
  2. Each tick t = 1, 2, …: Game.Update(ctx, in), then every live entity's behaviour in id order (entities spawned during the tick start next tick), then despawns are applied, transforms and AABBs are recomputed, collisions and invariants are checked, and tick t is recorded.

Input events of a script at tick t are part of the Input of tick t; events at tick 0 apply before the first update and appear in tick 1. The simulation runs at tick_rate (20 Hz by default); there is no interpolation and the player renders once per tick.

Determinism rules

Trace

One canonical JSON object per tick in trace.jsonl:

{"entities":[{"aabb":{"max":[0.5,1,-0.75],"min":[-0.5,0,-1.75]},"id":2,"kind":"player","material":"hero","model":"hero","name":"player","parent":"","position":[0,0,-1.25],"rotation_deg":[0,180,0],"scale":[1,1,1],"state":{"score":1},"tags":["player"],"visible":true}],"events":[{"event":"gem_collected","gem":"gem_1","score":1}],"tick":75}

Keys are sorted, no whitespace, floats use the shortest form that round-trips (float32 values as float32), non-finite numbers are the strings "NaN", "+Inf", "-Inf". Entity summaries hold: id, name, kind, position (world), rotation_deg (local Euler angles, R = Ry·Rx·Rz, pitch in [-90, 90]), scale (local), visible, tags, model, material, parent (name, "" for none), aabb ({min, max}, world; only entities with a model or a hitbox, and the hitbox's bounds when one is set) and state (only when set). Scenario expectation paths address these keys (position.z, aabb.max.y, state.score). The trace hash is the SHA-256 of the file's bytes.

Built-in events: scene_load (scene, entities), spawn and despawn (id, name, kind), collision (a, b, a_id, b_id: two AABBs started overlapping this tick; touching does not count; two static entities never collide), invariant_violation (name, detail: an invariant started failing this tick). Game events come from ctx.Trace.

Invariants

Checked after every tick (including tick 0). A scenario's invariants list is used when present, else the project's. Built-ins: finite_positions, within_bounds (project bounds), entity_count_max:N, no_overlap:tagA,tagB. Any other name must be registered by the game with ctx.Invariant(name, pred). A violation is reported when an invariant starts failing, not again while it keeps failing.

Headless subcommands

Every game binary accepts -project DIR (default .), -version, and:

game -headless render   --scene S --tick T --seed N --camera P --mode M --width W --height H --out F [--bundle] [--input F]
game -headless simulate --scenario F | --scene S --ticks N --seed N [--input F] [--screenshots 0,60] [--invariants a,b] --out DIR
game -headless query    --frame F --at x,y | --coverage
game -headless snapshot --scene S --tick T --out F [--input F]
game -headless snapshot --restore F --ticks N [--input F] [--out trace.jsonl]
game -headless describe
game -headless bench    --scenario F | --scene S --ticks N --seed N [--input F] [--width W --height H] [--cpus N]