<!-- Generated from sdk/wasm/README.md and protocol/v2/api.json by protocol/v2/codegen/docs.js; do not edit. Run `make api`. -->
# Clunkyard WebAssembly ABI 4

A WebAssembly robot runs inside Wasmtime under `build/clunkyard-wasm-host`,
which links the robot core (`sdk/core`) natively and speaks protocol 2 for
the robot. The robot calls the core through imports from the `clunkyard`
module, one for each API entry, and the core reads each hello and state and
writes the ready and each act. There is no WASI context and no other import.

[`abi4/clunkyard.h`](../../sdk/wasm/abi4/clunkyard.h) is the C SDK. It is generated from
`protocol/v2/api.json` (`make api`); do not edit it. It declares every
import at the name `libclunkyard` has, without the handle (an instance is
one robot), with the plain names (`cy_sensors_nearest`) aliasing the latest
version, and the records and constants the core uses.

```c
#include "clunkyard.h"

static void turn(void) {
  cy_contact target;
  if (cy_sensors_nearest(&target) == CY_OK) {
    cy_gun_aim_heading(target.bearing);
    cy_gun_fire();
  }
}

CY_ROBOT(NULL, turn)
```

The same surface, generated beside it, for the other languages:

| Language | File | Exports through |
| --- | --- | --- |
| C | `abi4/clunkyard.h` | `CY_ROBOT(start, turn)` |
| Rust | `clunkyard.rs` (`mod clunkyard;` in a `#![no_std]` crate, which it gives a trapping panic handler) | `cy_robot!(start, turn);` |
| Zig | `clunkyard.zig` (`@import("clunkyard.zig")`; build with `-rdynamic`) | `comptime { cy.exportRobot(start, turn); }` |
| AssemblyScript | `clunkyard.ts` (`--runtime stub --use abort=clunkyard/cyAbort`) | `cy.robot(start, turn)` and `export { clunkyard_start, clunkyard_turn } from "./clunkyard"` |
| WAT | `clunkyard.wat`, pasted first in the module, before any other definition | the module's own `clunkyard_start` and `clunkyard_turn` |

Each declares the imports at their C names in Rust (`cy_sensors_nearest`)
and at their import names elsewhere (`sensors_nearest`), with the records
(a view over the record's bytes in AssemblyScript, field offsets as globals
in WAT) and the constants. Only the C header has the DSL words import.

## The module

A module exports:

```text
memory
clunkyard_start() -> i32    once, after the hello; 0 to play on
clunkyard_turn() -> i32     once a state; 0 to send the act
```

`CY_ROBOT(start, turn)` writes both; `start` may be `NULL`. Its imports must
all be `clunkyard.*` functions with exactly the generated signatures. An
unknown name, a wrong signature or any other import fails at link, before
the hello: that is the version negotiation. A module without
`clunkyard_start`, such as one built for ABI 1 to 3, is refused.

Every import returns a status (`CY_OK` 0, `CY_NONE` 1 for absent, `CY_SHORT`
2 for a short buffer, negative for an error) and writes its result through
pointers into guest memory. The host bounds-checks every pointer and length
against memory as it is at the call, so a module may grow its memory.

`cy_robot_seed(&seed)` is the host's own import, not the core's: the seed
for the robot's own randomness, from `CLUNKYARD_ROBOT_SEED`, which the
referee derives from the match seed and the robot's id. It defaults to 1.

## Metering

Fuel counts the guest's own operators. Each import also takes a nominal fee
from the turn's fuel, at once: 32 a call, whatever the core does for it, and
1 a byte for the data it moves (text in, a list's records, text out). The
fees are frozen per season in `rates/s3.json` (`reflex.imports`); `make
import-costs` checks that they keep the core's real work inside the CPU
cap. An import that would overdraw takes the rest of the fuel, is not
answered, and the turn forfeits `WASM_TURN_FUEL`.

Work the core does natively sits outside fuel, so the host also holds a turn
to 256 import calls and 200 ms of its thread's CPU; over either, the turn
forfeits `WASM_CPU`. The season's limits are `reflex.wasmFuelPerTurn` and
`reflex.wasmFuelPerMatch` in `rates/s3.json`: a floor of the calls a turn
makes to act plus the thinking the interpreted DSL allows.

## Building

```sh
scripts/build-wasm.sh
```

builds `build/clunkyard-wasm-host` and the example robot,
`build/wasm-sentry-c.wasm`, from [`bots/c/sentry.c`](../../bots/c/sentry.c).
It needs `cc`, Clang with the wasm32 target, the Wasmtime C API
(`WASMTIME_PREFIX`, default `/usr/local`) and the robot core: the image's
`/opt/clunkyard/sdk/lib/libclunkyard.a`, or `cargo` to build one from
`sdk/core`. A robot is freestanding C:

```sh
clang --target=wasm32 -mbulk-memory -std=c11 -O2 -nostdlib -Isdk/wasm/abi4 robot.c \
  -Wl,--no-entry -Wl,--export-memory -o robot.wasm
```

With no libc, a struct copy or a zeroed array needs `memcpy` or `memset`;
`-mbulk-memory` lowers them to `memory.copy` and `memory.fill`, which the
host runs. Recent Clang turns it on by default; Clang 18, the image's,
does not.

The Rust starter in `src/public/starters/rust-wasm` uses the Rust surface. The DSL `-wasm` builds (`scripts/build-dsl-wasm.sh`) lower
Vane, Cadence and Glyph to the same ABI.

## Running the host by hand

```sh
build/clunkyard-wasm-host robot.wasm \
  --init-fuel 250000 --turn-fuel 5807 --match-fuel 10452600 \
  --memory-bytes 4194304 --table-elements 256 --wasm-stack-bytes 262144 \
  --cpu-ms 200 --import-calls 256 < protocol/v2/fixtures/2d-classic.ndjson
```

The environment sets the same limits (`CLUNKYARD_WASM_INIT_FUEL`,
`CLUNKYARD_WASM_TURN_FUEL`, `CLUNKYARD_WASM_MATCH_FUEL`,
`CLUNKYARD_WASM_MEMORY_BYTES`, `CLUNKYARD_WASM_TABLE_ELEMENTS`,
`CLUNKYARD_WASM_STACK_BYTES`); the command line takes precedence. Each act
carries the turn's fuel (`wasmFuel`), the match's (`wasmFuelTotal`), memory
and the import calls (`wasmImports`). A budget exhausted, a trap or memory
growth refused ends the robot with a structured failure.

## Names

Every entry of the robot API 2.0.0 in WebAssembly (C).
[names.md](names.md) has every language side by side.

| Canonical | Form | WebAssembly (C) |
| --- | --- | --- |
| `match.mode` | property | `cy_match_mode(&out)` |
| `match.ruleset` | property | `cy_match_ruleset(&out)` |
| `match.control` | property | `cy_match_control(&out)` |
| `match.robots` | property | `cy_match_robots(&out)` |
| `match.is_3d` | property | `cy_match_is_3d(&out)` |
| `match.tick_rate` | property | `cy_match_tick_rate(&out)` |
| `match.max_ticks` | property | `cy_match_max_ticks(&out)` |
| `match.robot_id` | call | `cy_robot_id(slot, &buf, &length)` |
| `clock.tick` | property | `cy_clock_tick(&out)` |
| `clock.seconds` | property | `cy_clock_seconds(&out)` |
| `clock.remaining_ticks` | property | `cy_clock_remaining_ticks(&out)` |
| `arena.size` | property | `cy_arena_size(&out)` |
| `arena.gravity` | property | `cy_arena_gravity(&out)` |
| `arena.terrain_kind` | property | `cy_arena_terrain_kind(&out)` |
| `arena.ground_height` | call | `cy_arena_ground_height(x, y, &out)` |
| `me.health` | property | `cy_me_health(&out)` |
| `me.position` | property | `cy_me_position(&out)` |
| `me.velocity` | property | `cy_me_velocity(&out)` |
| `me.heading` | property | `cy_me_heading(&out)` |
| `me.pitch` | property | `cy_me_pitch(&out)` |
| `me.roll` | property | `cy_me_roll(&out)` |
| `me.angular_velocity` | property | `cy_me_angular_velocity(&out)` |
| `me.attitude` | property | `cy_me_attitude(&out)` |
| `me.grounded` | property | `cy_me_grounded(&out)` |
| `me.altitude` | property | `cy_me_altitude(&out)` |
| `me.applied` | property | `cy_me_applied(&out)` |
| `battery.charge` | property | `cy_battery_charge(&out)` |
| `battery.capacity` | property | `cy_battery_capacity(&out)` |
| `battery.regen` | property | `cy_battery_regen(&out)` |
| `battery.sustainable_drive` | property | `cy_battery_sustainable_drive(&out)` |
| `battery.compute` | property | `cy_battery_compute(&out)` |
| `battery.drive_cost` | call | `cy_battery_drive_cost(fraction, &out)` |
| `drive.hover_lift` | property | `cy_drive_hover_lift(&out)` |
| `drive.set` | control | `cy_drive_throttle(value)`, `cy_drive_turn(value)`, `cy_drive_pitch_turn(value)` |
| `drive.forces` | control | `cy_drive_thrust(value)`, `cy_drive_strafe(value)`, `cy_drive_lift(value)`, `cy_drive_yaw_torque(value)`, `cy_drive_pitch_torque(value)`, `cy_drive_roll_torque(value)`, `cy_drive_brake(value)` |
| `drive.get` | call | `cy_drive_get_throttle(&out)`, `cy_drive_get_turn(&out)`, `cy_drive_get_pitch_turn(&out)`, `cy_drive_get_thrust(&out)`, `cy_drive_get_strafe(&out)`, `cy_drive_get_lift(&out)`, `cy_drive_get_yaw_torque(&out)`, `cy_drive_get_pitch_torque(&out)`, `cy_drive_get_roll_torque(&out)`, `cy_drive_get_brake(&out)` |
| `gun.aim_now` | property | `cy_gun_aim_now(&out)` |
| `gun.cooldown_ticks` | property | `cy_gun_cooldown_ticks(&out)` |
| `gun.spread` | property | `cy_gun_spread(&out)` |
| `gun.max_power` | property | `cy_gun_max_power(&out)` |
| `gun.damage_scale` | property | `cy_gun_damage_scale(&out)` |
| `gun.can_fire` | call | `cy_gun_can_fire(power, reserve, &out)` |
| `gun.shot_speed` | call | `cy_gun_shot_speed(power, &out)` |
| `gun.shot_cost` | call | `cy_gun_shot_cost(power, &out)` |
| `gun.reload_ticks` | call | `cy_gun_reload_ticks(power, &out)` |
| `gun.shot_damage` | call | `cy_gun_shot_damage(power, &out)` |
| `gun.power_for_speed` | call | `cy_gun_power_for_speed(speed, &out)` |
| `gun.power_for_flight_time` | call | `cy_gun_power_for_flight_time(range, seconds, &out)` |
| `gun.lead` | call | `cy_gun_lead(contact, power, &out)` |
| `gun.lead_point` | call | `cy_gun_lead_point(position, velocity, power, &out)` |
| `gun.aim` | control | `cy_gun_aim_heading(value)`, `cy_gun_aim_pitch(value)` |
| `gun.fire` | control | `cy_gun_fire()`, `cy_gun_power(value)` |
| `gun.get` | call | `cy_gun_get_aim_heading(&out)`, `cy_gun_get_aim_pitch(&out)`, `cy_gun_get_fire(&out)`, `cy_gun_get_power(&out)` |
| `scanner.last` | property | `cy_scanner_last(&out)` |
| `scanner.proximity_range` | property | `cy_scanner_proximity_range(&out)` |
| `scanner.reach` | call | `cy_scanner_reach(arc, &out)` |
| `scanner.arc_for_reach` | call | `cy_scanner_arc_for_reach(range, &out)` |
| `scanner.set` | control | `cy_scanner_heading(value)`, `cy_scanner_arc(value)`, `cy_scanner_pitch(value)` |
| `scanner.get` | call | `cy_scanner_get_heading(&out)`, `cy_scanner_get_arc(&out)`, `cy_scanner_get_pitch(&out)` |
| `sensors.contacts` | property | `cy_sensors_contacts(&out, &count)` |
| `sensors.nearest` | property | `cy_sensors_nearest(&out)` |
| `sensors.cues` | property | `cy_sensors_cues(&out, &count)` |
| `sensors.contact` | call | `cy_sensors_contact(slot, &out)` |
| `sensors.position_of` | call | `cy_sensors_position_of(contact, &out)` |
| `tracker.cue` | property | `cy_tracker_cue(&out)` |
| `tracker.nearest_bearing_rate` | property | `cy_tracker_nearest_bearing_rate(&out)` |
| `tracker.velocity` | call | `cy_tracker_velocity(slot, &out)` |
| `tracker.position` | call | `cy_tracker_position(slot, &out)` |
| `tracker.last_seen` | call | `cy_tracker_last_seen(slot, &out)` |
| `tracker.configure` | call | `cy_tracker_configure(smoothing, forget_ticks, cue_ticks)` |
| `nav.zone` | property | `cy_nav_zone(&out)` |
| `nav.zone.margin` | call | `cy_nav_zone_margin(point, &out)` |
| `nav.zone.contains` | call | `cy_nav_zone_contains(point, margin, &out)` |
| `nav.zone.distance_from` | call | `cy_nav_zone_distance_from(point, &out)` |
| `nav.storm` | property | `cy_nav_storm(&out)` |
| `nav.storm.margin` | call | `cy_nav_storm_margin(point, &out)` |
| `nav.storm.contains` | call | `cy_nav_storm_contains(point, margin, &out)` |
| `nav.storm.distance_from` | call | `cy_nav_storm_distance_from(point, &out)` |
| `nav.hill` | property | `cy_nav_hill(&out)` |
| `nav.hill.margin` | call | `cy_nav_hill_margin(point, &out)` |
| `nav.hill.contains` | call | `cy_nav_hill_contains(point, margin, &out)` |
| `nav.hill.distance_from` | call | `cy_nav_hill_distance_from(point, &out)` |
| `nav.pickups` | property | `cy_nav_pickups(&out, &count)` |
| `nav.nearest_pickup` | call | `cy_nav_nearest_pickup(kind, &out)` |
| `nav.field_distance` | call | `cy_nav_field_distance(a, b, &out)` |
| `score.me` | property | `cy_score_me(&out)` |
| `score.best` | property | `cy_score_best(&out)` |
| `score.armour` | property | `cy_score_armour(&out)` |
| `angles.normalize` | call | `cy_angles_normalize(degrees, &out)` |
| `angles.angle_diff` | call | `cy_angles_angle_diff(target, current, &out)` |
| `angles.bearing_to` | call | `cy_angles_bearing_to(from, to, &out)` |
| `angles.elevation_to` | call | `cy_angles_elevation_to(from, to, &out)` |
| `angles.direction` | call | `cy_angles_direction(heading, pitch, &out)` |
| `angles.distance` | call | `cy_angles_distance(a, b, &out)` |
| `angles.clamp` | call | `cy_angles_clamp(value, low, high, &out)` |
| `status.set` | control | `cy_status_text(text)`, `cy_status_value(value)` |
| `status.get` | call | `cy_status_get_text(&buf, &length)`, `cy_status_get_value(&out)` |
| `raw.number` | call | `cy_raw_number(path, &out)` |
| `raw.text` | call | `cy_raw_text(path, &buf, &length)` |
| `raw.has` | call | `cy_raw_has(path, &out)` |
| `raw.flag` | call | `cy_raw_flag(path, &out)` |
| `raw.count` | call | `cy_raw_count(path, &out)` |
| `raw.line` | call | `cy_raw_line(&buf, &length)` |
| `raw.extra` | call | `cy_raw_extra(json)` |
| `session.log` | call | `cy_session_log(line)` |
