Documentation
¶
Overview ¶
Package wire IS the shellcade game ABI as code: the version handshake, the export and host-function names, and the packed little-endian payload encodings, expressed over neutral types with zero dependencies.
This package is the single source of truth both sides compile against: the gamekit guest SDK maps wire types to its authoring types, and the private host adapter maps them to its engine types. Non-Go guests implement the same layouts from ABI.md, which documents exactly what this package encodes.
Index ¶
- Constants
- func ApplyFrameDelta(prev, delta []byte) error
- func BuildFrameDelta(base, next, dst []byte, epoch uint32) int
- func BuildKeyframe(next, dst []byte, epoch uint32) int
- func CheckFrame(b []byte) error
- func CheckFrameDelta(b []byte) error
- func DeltaEpoch(b []byte) uint32
- func EncodeCtx(w *Buf, c Ctx)
- func EncodeCtxEpoch(w *Buf, c Ctx, epoch uint32, full bool)
- func EncodeCtxEpochFeat(w *Buf, c Ctx, epoch uint32, full bool, features uint32)
- func EncodeCtxFeat(w *Buf, c Ctx, features uint32)
- func EncodeMeta(m Meta) []byte
- func EncodeResult(res Result) []byte
- func IsKeyframe(b []byte) bool
- func PutCell(buf []byte, i int, c Cell)
- func ValidateConfigSpecs(specs []ConfigSpec) error
- func ValidateControls(decls []ControlDecl) error
- func ValidateLifecycle(lifecycle uint8, minPlayers uint16) error
- func ValidateMetaTrailer(ctxFeatures uint32, heartbeatMS uint16) error
- type Buf
- type Cell
- type Character
- type ConfigSpec
- type ControlDecl
- type Ctx
- type Meta
- type Player
- type Ranking
- type Rd
- type Result
Constants ¶
const ( // DeltaHeaderBytes is the fixed container header length. DeltaHeaderBytes = 9 // RunHeaderBytes is the per-run prefix (u16 startIndex + u16 runLen). RunHeaderBytes = 4 // KeyframeBytes is the keyframe form's exact size and the worst-case bound: // 9-byte header (bit0 set) + one run {start=0, len=FrameCells} + the full // 46080-byte packed grid = 46093. The SDK budget rule ships the keyframe // when an encoded delta would meet or exceed this (inclusive). KeyframeBytes = DeltaHeaderBytes + RunHeaderBytes + FrameBytes // 46093 // FlagKeyframe is header flags bit0: the payload is a self-contained keyframe. FlagKeyframe = 0x01 )
The v2 frame-delta container (normative, ABI.md §4.5). Every send/identical carries this variable-length, little-endian, index-addressed run-list:
Header (9 bytes): u8 flags bit0 = keyframe (1 = full-frame keyframe); all other bits MUST be zero u32 epoch the host-issued epoch this delta is computed against (0 on a fresh instance) u16 runCount number of runs (keyframe: exactly 1; no-change: 0) u8 rows grid geometry; MUST be 24 in v2 u8 cols grid geometry; MUST be 80 in v2 then runCount runs, each: u16 startIndex first cell index (0..FrameCells-1, == row*Cols+col) u16 runLen 1..FrameCells, consecutive changed cells runLen × CellBytes packed canonical-zero cells (PutCell output)
These encoders/validators are index-addressed (mirroring PutCell/GetCell), never the appending Buf, so they are allocation-free over caller-owned scratch — the SDK's leaking-GC steady-state requirement.
const ( ExpABI = "shellcade_abi" ExpMeta = "meta" ExpStart = "start" ExpJoin = "join" ExpLeave = "leave" ExpInput = "input" ExpWake = "wake" ExpClose = "close" )
Guest export names.
const ( FnSend = "send" FnIdentical = "identical" FnSetInputContext = "set_input_context" FnEnd = "end" FnPost = "post" FnLog = "log" FnKVGet = "kv_get" FnKVSet = "kv_set" FnKVDelete = "kv_delete" FnConfigGet = "config_get" )
Host function names.
const ( Rows = 24 Cols = 80 CellBytes = 24 FrameCells = Rows * Cols // 1920 FrameBytes = FrameCells * CellBytes // 46080 RowBytes = Cols * CellBytes // 1920 )
Frame geometry: 80x24 cells, 24 bytes per v2 grapheme cell.
const ( KindGuest uint8 = 0 KindMember uint8 = 1 )
Player kind codes.
const ( ModeQuick uint8 = 0 ModePrivate uint8 = 1 ModeSolo uint8 = 2 )
Mode codes.
const ( StatusFinished uint8 = 0 StatusDNF uint8 = 1 StatusFlagged uint8 = 2 )
Status codes.
const ( InputRune uint8 = 0 InputKey uint8 = 1 )
Input kind codes.
const ( CtxRosterUnchanged uint16 = 0xFFFF CtxRosterFull uint16 = 0xFFFE CtxRosterMaxCount uint16 = 0xFFFD )
Ctx member-section sentinels (roster-epoch mode). Real rosters are capped far below these values, so the count u16 disambiguates the three forms: 0..CtxRosterMaxCount = legacy full roster (no epoch), CtxRosterFull = u32 epoch + u16 real count + members, CtxRosterUnchanged = u32 epoch only.
const ( // CtxFeatRosterEpoch opts the guest into the ctx member-section sentinel // forms: full roster only on change (with an epoch), 6-byte unchanged // sections otherwise. CtxFeatRosterEpoch uint32 = 1 << 0 // CtxFeatCharacter opts the guest into per-member character sections // (ABI.md §4.1): str glyph + ink RGB + bg RGB + ascii fallback, appended // after each member's Kind byte in both member-bearing forms. CtxFeatCharacter uint32 = 1 << 1 // KnownCtxFeatures is the mask of bits this wire revision defines. KnownCtxFeatures uint32 = CtxFeatRosterEpoch | CtxFeatCharacter )
CtxFeatures bits a game may declare in its meta trailer. The host ignores bits it does not implement; the SDKs reject bits they do not define.
const ( LifecycleResumable uint8 = 0 LifecycleEphemeral uint8 = 1 LifecycleResident uint8 = 2 )
Lifecycle values for the meta trailer.
const ( ConfigText uint8 = 0 ConfigNumber uint8 = 1 ConfigBool uint8 = 2 ConfigJSON uint8 = 3 )
Config value type codes (how the admin surface renders/validates a value).
const ( KeyCodeEnter uint8 = 1 KeyCodeBackspace uint8 = 2 KeyCodeEsc uint8 = 3 KeyCodeTab uint8 = 4 KeyCodeUp uint8 = 5 KeyCodeDown uint8 = 6 KeyCodeLeft uint8 = 7 KeyCodeRight uint8 = 8 KeyCodeCtrlC uint8 = 9 )
Named key codes carried in input payloads and ControlDecl (the InputKey value space; match the SDK Key enum).
const ( HeartbeatMinMS uint16 = 20 HeartbeatMaxMS uint16 = 1000 )
Heartbeat declaration envelope (mirrors the host's clamp range).
const ControlLabelMaxRunes = 16
ControlLabelMaxRunes caps a declared control's display label.
const HostKeyPrefix = "host."
HostKeyPrefix is the reserved config-key namespace interpreted by the host (e.g. host.heartbeat_ms). Games MUST NOT declare specs under it — the platform declares those knobs itself.
const HostNamespace = "extism:host/user"
HostNamespace is the wasm import namespace for shellcade host functions.
const MaxControls = 32
MaxControls caps the declared-controls list: a control surface for the handful of inputs beyond the canonical vocabulary, not a keymap dump.
const MaxDeltaBytes = KeyframeBytes
MaxDeltaBytes is a scratch-buffer cap big enough for any delta this package emits: the run-list worst case never exceeds the keyframe form (the SDK caps it there), so KeyframeBytes is a sufficient and exact ceiling.
const Revision uint16 = 6
Revision is the wire revision: a monotonic counter of the wire-visible minor additions within ABI major Version, bumped in the same change that lands each addition (ABI.md §5). SDK encoders stamp it automatically into the meta payload's trailing wireRevision field, giving hosts a per-artifact declaration of the newest wire feature the artifact may assume — the mechanical anchor for the deploy-order rule (a host warns on or refuses artifacts declaring a revision above its own compiled-in Revision).
Ledger (the revision that INTRODUCED each wire-visible minor):
0 — unknown: the meta predates the wireRevision field (kit ≤ v2.7.x)
1 — config-spec meta section (kit v2.3.0)
2 — large-room meta section + ctx roster-epoch sentinels (kit v2.6.0)
3 — lifecycle meta byte (kit v2.7.0)
4 — the wireRevision meta field itself
5 — per-member character section behind CtxFeatCharacter (str glyph ·
u8 ink RGB · u8 bg RGB · u8 asciiFallback after kind, both
member-bearing forms)
6 — declared-controls meta section (u16 count of {u8 kind ·
u32 rune | u8 key · str label} after wireRevision)
Revisions 1–3 predate the field, so artifacts of those eras decode as 0; only 0 or values ≥ 4 are ever observed on the wire. Any future change that adds a wire-visible encoding (a new trailing meta section, a ctx-feature bit, a delta flag bit, a new host function the guest calls, …) MUST append a ledger entry and bump this constant in the same change.
Like RosterCap, this is a protocol constant mirrored by the Rust guest SDK (rust/src/wire.rs WIRE_REVISION, asserted equal to this constant by TestRustWireRevisionMatchesWire in this package) — the two must change in lockstep.
const RosterCap = 1024
RosterCap is the contract-wide roster ceiling for per-index frame baselines: an SDK sizes its baseline table to RosterCap slots (plus the broadcast slot, conventionally at index RosterCap) and silently drops Send for a roster index >= RosterCap, and the host bounds-checks the send index and sizes its per-slot cache the same way (ABI.md §3, §4.6). 1024 since the large-room scale work (kit v2.5.0 Go / v2.7.0 Rust).
This is a protocol invariant shared by every implementation — the Go guest SDK (internal/game), the Rust guest SDK (rust/src/broadcast.rs ROSTER_CAP, asserted equal to this constant by TestRustRosterCapMatchesWire in this package), and the host adapter — so changing it is ABI-affecting and must land in all of them in lockstep.
const Version uint32 = 2
Version is the ABI major version.
Variables ¶
This section is empty.
Functions ¶
func ApplyFrameDelta ¶
ApplyFrameDelta applies a (validated) delta in place to prev, a FrameBytes baseline buffer: each run's cells are copied into prev at startIndex*24, so a keyframe (one full-cover run) overwrites all 1920 cells. It re-validates structurally (so a caller may apply untrusted bytes safely) and returns errMalformedDelta on any malformation, never panicking, never reading OOB, and never partially mutating prev on a malformed container (it validates fully first). It allocates nothing.
func BuildFrameDelta ¶
BuildFrameDelta is the reference delta encoder: it diffs the packed frames base vs next into the caller-provided scratch dst (sized to at least MaxDeltaBytes) and returns the number of payload bytes. It coalesces changed cells into MAXIMAL runs of consecutive changed cells, greedy left-to-right (gap = 0): a single unchanged cell between two changed spans forces two runs. That determinism is what makes cross-implementation golden vectors byte-identical. epoch is stamped into the header (the slot's host-issued epoch). It allocates nothing.
A runCount==0 result (the 9-byte header alone) is the canonical no-change delta. The keyframe form is NOT produced here — callers apply the budget rule (>= KeyframeBytes ⇒ BuildKeyframe) on the returned length.
func BuildKeyframe ¶
BuildKeyframe writes the keyframe form into dst: a 9-byte header (flags bit0 set), one run {startIndex=0, runLen=FrameCells} and the full packed grid next. Returns KeyframeBytes (46093). It allocates nothing.
func CheckFrame ¶
CheckFrame validates a full-frame payload length (the bare packed grid; used by the host-side baseline buffers, not the wire send path, which carries the delta container instead — see CheckFrameDelta).
func CheckFrameDelta ¶
CheckFrameDelta validates a frame-delta container structurally without ever panicking or reading out of bounds (the drop-not-fatal contract). It checks:
- len >= 9 (header present)
- geometry bytes == (Rows, Cols) == (24, 80)
- no unknown flag bits set (only bit0 is assigned in v2 — rejected, not ignored)
- runCount consistent with body length: 9 + Σ(4 + runLen*24) == len(b)
- every run in-bounds: startIndex+runLen <= FrameCells
- runs strictly ascending and non-overlapping: start[i] >= start[i-1]+len[i-1]
A short read degrades to errMalformedDelta. It does NOT require runs to be minimal, greedy, or true diffs — host acceptance is the envelope, so a hand-rolled guest's structurally valid container passes.
func DeltaEpoch ¶
DeltaEpoch reads the epoch field from a container header without validating the rest. Callers use it to decide acceptance; it returns 0 on a short read.
func EncodeCtx ¶
EncodeCtx appends the packed CallContext to w in the LEGACY form (full roster, no epoch) — the only form pre-roster-epoch guests understand, and byte-identical to all prior wire revisions. Character fields on Player are silently ignored. This is a frozen features=0 wrapper around EncodeCtxFeat.
func EncodeCtxEpoch ¶
EncodeCtxEpoch appends the packed CallContext in roster-epoch mode (only for guests whose meta declares CtxFeatRosterEpoch). full=true emits the CtxRosterFull sentinel (epoch + real count + members); full=false emits the CtxRosterUnchanged sentinel (epoch only — the member section is 6 bytes regardless of roster size, and c.Members is not read). Character fields on Player are silently ignored. This is a frozen features=0 wrapper around EncodeCtxEpochFeat.
func EncodeCtxEpochFeat ¶ added in v2.9.0
EncodeCtxEpochFeat appends the packed CallContext in roster-epoch mode, encoding per-member character sections after each Kind byte when features&CtxFeatCharacter != 0. For guests that do not declare CtxFeatCharacter pass features=0 (or use EncodeCtxEpoch).
func EncodeCtxFeat ¶ added in v2.9.0
EncodeCtxFeat appends the packed CallContext to w in the legacy full-roster form, encoding per-member character sections after each Kind byte when features&CtxFeatCharacter != 0. For guests that do not declare CtxFeatCharacter pass features=0 (or use EncodeCtx).
func IsKeyframe ¶
IsKeyframe reports whether the container's flags carry the keyframe bit. It returns false on a short read.
func PutCell ¶
PutCell writes one cell at index i (0..FrameCells-1) into a FrameBytes buffer using the v2 24-byte anchor layout:
rune@0 cp2@4 cp3@8 fg@12..15 bg@16..19 attr@20 cont@21 pad@22..23
PutCell is the normative CANONICAL-ZERO enforcer: it always writes pad = 0 and writes whatever cp2/cp3 the cell carries (0 = unused), so even a hand-built Cell with garbage left in a slot it should not use serializes canonically — cell equality is then exactly a 24-byte memcmp, which is load-bearing for delta determinism and hibernation byte-identity.
func ValidateConfigSpecs ¶
func ValidateConfigSpecs(specs []ConfigSpec) error
ValidateConfigSpecs enforces the authoring rules for declared config specs, shared by guest SDK encoders and host/CLI decoders: keys non-empty and unique, no reserved host. prefix, a known type code, and Schema only on JSON-typed keys where it must itself parse as JSON. The JSON check is a well-formedness scan (json.Valid) — schema COMPILATION is a host concern, keeping this package dependency-free.
func ValidateControls ¶ added in v2.10.0
func ValidateControls(decls []ControlDecl) error
ValidateControls enforces the authoring rules for declared controls, shared by guest SDK encoders and host/CLI decoders (the same fail-fast posture as ValidateConfigSpecs): a known input kind; a printable rune or an assigned named-key code; a non-empty label of at most ControlLabelMaxRunes runes; no duplicate inputs; at most MaxControls declarations.
func ValidateLifecycle ¶
ValidateLifecycle is the shared authoring rule set for the lifecycle declaration, enforced at meta() encode time by both SDKs: the value must be a defined lifecycle, and resident cannot be combined with minPlayers > 1 (a resident room runs with zero members).
func ValidateMetaTrailer ¶
ValidateMetaTrailer is the shared authoring rule set for the large-room meta section, enforced at meta() encode time by both SDKs (the same fail-fast posture as ValidateConfigSpecs): no undefined ctx-feature bits; heartbeat 0 (no declaration) or within [HeartbeatMinMS, HeartbeatMaxMS].
Types ¶
type Cell ¶
type Cell struct {
Rune rune
Cp2 rune
Cp3 rune
FGSet bool
FGR, FGG, FGB uint8
BGSet bool
BGR, BGG, BGB uint8
Attr uint8
Cont bool
}
Cell is one drawable cell of a frame. In v2 it carries up to three code points of a grapheme cluster: Rune is the base, Cp2/Cp3 the extra code points (0 = unused). The packed form is exactly 24 bytes (CellBytes), with the canonical-zero rule (unused cp slots and pad are zero) enforced by PutCell so cell equality is a 24-byte memcmp.
type Character ¶ added in v2.9.0
type Character struct {
Glyph string // single width-1 glyph (unicode)
InkR uint8
InkG uint8
InkB uint8
BgR uint8
BgG uint8
BgB uint8
Fallback uint8 // ASCII fallback codepoint
}
Character is the per-player resolved character, encoded after the Kind byte in every member-bearing Ctx section iff the guest's meta declares CtxFeatCharacter. The zero value means "not declared / not encoded".
type ConfigSpec ¶
type ConfigSpec struct {
Key string // the config_get key the game reads
Title string // short admin-facing label
Description string // one-or-two-sentence admin help
Type uint8 // ConfigText..ConfigJSON
Default string // value the game uses when unset ("" = not declared)
Schema string // JSON Schema document (json type only; "" = none)
}
ConfigSpec is one declared admin-settable config key in the meta payload.
type ControlDecl ¶ added in v2.10.0
type ControlDecl struct {
Kind uint8 // InputRune or InputKey
Rune rune // the printable rune (Kind == InputRune)
Key uint8 // the named key code (Kind == InputKey; KeyCodeEnter..KeyCodeCtrlC)
Label string // short display label, 1..16 runes
}
ControlDecl is one declared extra control in the meta payload: the exact input it sends — a printable rune or a named key, the same value space as input events — plus a short display label (e.g. "RESIGN").
type Ctx ¶
type Ctx struct {
NowUnixNanos int64
Seed int64
SeedSet bool
Mode uint8
Capacity uint16
MinPlayers uint16
Members []Player
Settled bool
// Roster-epoch mode (spec minor addition; emitted only to guests whose
// meta declares CtxFeatRosterEpoch). RosterEpochSet marks a sentinel-form
// member section: RosterUnchanged means the section carried only the
// epoch (Members is nil — the guest reuses its cached roster); otherwise
// Members is the full roster at RosterEpoch.
RosterEpoch uint32
RosterEpochSet bool
RosterUnchanged bool
}
Ctx is the CallContext every host→guest callback carries.
func DecodeCtx ¶
DecodeCtx reads a CallContext, leaving r positioned at the event extras. It recognises all three member-section forms; on the unchanged sentinel Members is nil and RosterUnchanged is true (the caller supplies its cached roster). Character fields are never populated. This is a frozen features=0 wrapper around DecodeCtxFeat.
func DecodeCtxFeat ¶ added in v2.9.0
DecodeCtxFeat reads a CallContext encoded with the given features bitset, populating per-member Character fields when features&CtxFeatCharacter != 0. For payloads encoded without CtxFeatCharacter pass features=0 (or use DecodeCtx). Passing features=CtxFeatCharacter against a features-0 payload will trigger a short-read and set r.Bad — this is intentional (host-fault contract: features must match the payload). Unlike roster-epoch, whose forms are self-describing via the count u16's spare sentinel space, per-member trailing bytes carry no in-band discriminator — hence the out-of-band features parameter.
type Meta ¶
type Meta struct {
Slug string
Name string
ShortDescription string
MinPlayers uint16
MaxPlayers uint16
Tags []string
QuickModeLabel string
SoloModeLabel string
PrivateInviteLine string
HasLeaderboard bool
MetricLabel string
Direction uint8
Aggregation uint8
Format uint8
// ConfigSpecs is the trailing config-spec section (spec minor addition):
// the game's declared admin-settable config keys. Encoders always write
// the section (count 0 when empty); decoders treat a payload ending after
// the leaderboard block as a valid pre-config meta with no specs.
ConfigSpecs []ConfigSpec
// CtxFeatures + HeartbeatMS form the trailing large-room section (spec
// minor addition) after the config-spec section. CtxFeatures is the
// negotiated-encoding bitset (CtxFeat*); HeartbeatMS is the game's
// declared wake cadence (0 = no declaration; host precedence: admin
// config > declaration > platform default, clamped to the platform
// envelope). Encoders always write the section; decoders treat a payload
// ending after the config-spec section as a valid older meta with zero
// values.
CtxFeatures uint32
HeartbeatMS uint16
// Lifecycle is the room end-of-life declaration (spec minor addition;
// trailing byte after HeartbeatMS): 0 resumable (hibernate on abandon —
// today's behavior and the zero-value default), 1 ephemeral (end +
// dispose on abandon; no snapshot, no Resume entry), 2 resident (one
// long-lived granted room per slug). Hosts treat values they do not
// implement as resumable.
Lifecycle uint8
// WireRevision is the trailing wire-revision declaration (spec minor
// addition; u16 after the lifecycle byte): the wire.Revision of the kit
// the artifact was built against. 0 = unknown — the artifact predates
// the field. SDK encode paths stamp wire.Revision automatically (it is
// not author-settable); hosts use it to warn on or refuse artifacts
// declaring a revision above their own (ABI.md §4.2, §5).
WireRevision uint16
// Controls is the trailing declared-controls section (spec minor
// addition; u16 count after WireRevision): the game's extra controls —
// inputs beyond the canonical vocabulary, each paired with a short
// display label so a front end on a device without the corresponding
// physical key (touch) can surface a tappable affordance that sends
// exactly the declared input. Presentation metadata only: a declaration
// changes no input interpretation. Encoders always write the section
// (count 0 when empty); decoders treat a payload ending after the
// wire-revision field as a valid older meta with no declarations.
Controls []ControlDecl
}
Meta is the packed GameMeta.
type Ranking ¶
type Ranking struct {
PlayerIdx uint32 // roster index in the callback's Ctx
Metric int64
Rank uint16
Status uint8
}
Ranking is one player's outcome in an end/post payload.
type Rd ¶
Rd is a bounds-checked little-endian decoder.