C and C++ API reference

The SDK's headers, section by section, as written: the comments are the documentation. The guide (docs/guide/cpp.md, docs/guide/plugins.md) explains how to use them. The structs they name (ibf_snapshot, ibf_ship, ...) are in ibf_schema.h, described field by field in docs/schema.md.

ibf_client.h: the C API

Every language SDK wraps these calls.

ibf_client: the client side of the IBF bridge (design sections 6 and 10.10).

A bot, a tool or a language SDK connects to a running host (the game with
IBF loaded, or ibf_fakehost.exe), waits for each new snapshot, reads the
host's log and writes its own. Every language SDK wraps this library; the
Python package loads it with ctypes.

    ibf_client* c = NULL;
    ibf_connect_opts opts = {sizeof(opts), "my-bot", 0, IBF_CLIENT_CAPS_TICKS};
    if (ibf_connect("shm://", &opts, &c) != IBF_OK) { puts(ibf_connect_error()); return 1; }
    for (;;) {
        const ibf_snapshot* s = NULL;
        ibf_status st = ibf_wait_tick(c, 100, &s);
        if (st == IBF_TIMEOUT) continue;
        if (st != IBF_OK) break;              // the host left, or dropped this client
        ...                                   // s stays valid until the next wait on c
    }
    ibf_disconnect(c);

A bot plays a pad: ibf_bind_pad(c, 0, 0) once, then ibf_set_pad each tick
(a pad whose input has not changed for the host's input.timeout_ms, 250 by
default, reads neutral). ibf_command runs a console command on the host.

Threads: a handle is for one thread at a time, except ibf_log and
ibf_log_n, which any thread may call; ibf_disconnect frees the handle, so it
must not overlap one of those (the Python SDK's Client holds a lock around
both). Waiting keeps the client alive: the
host frees a client whose heartbeat is older than 1 s, and every call that
waits writes it at least every 100 ms (ibf_heartbeat writes it by hand).

Two transports, one handle: "shm://" (the default) for a host on this
machine, through shared memory; "tcp://host:port" (design 6.6) for one
anywhere, through the host's TCP server ([bridge.tcp]), which needs a token
unless it listens on loopback only. Over TCP a client blocks rather than
spins, and its tick facts are in the host's clock, estimated. A Linux build
of this library speaks TCP only. M4: pads and commands. M5: drawing,
parameters and stats. M7: ibf.hpp, a C++20 wrapper, and ibf_plugin.h for
in-process plugins.

Declarations

#ifndef IBF_CLIENT_H_
#define IBF_CLIENT_H_

#include <stddef.h>
#include <stdint.h>

#include "ibf_schema.h"

#if defined(_WIN32)
#if defined(IBF_CLIENT_BUILD_DLL)
#define IBF_CLIENT_API __declspec(dllexport)
#elif defined(IBF_CLIENT_STATIC)
#define IBF_CLIENT_API
#else
#define IBF_CLIENT_API __declspec(dllimport)
#endif
#define IBF_CALL __cdecl
#else
#define IBF_CLIENT_API __attribute__((visibility("default")))
#define IBF_CALL
#endif

#if defined(__cplusplus)
extern "C" {
#endif

typedef enum ibf_status {
    IBF_OK = 0,
    IBF_TIMEOUT = 1,            /* nothing new within the timeout */
    IBF_NO_HOST = -1,           /* no running host, or not the one asked for */
    IBF_ABI_MISMATCH = -2,      /* the host's bridge ABI is another major version */
    IBF_NO_SLOT = -3,           /* every client slot is taken */
    IBF_HOST_GONE = -4,         /* the host exited or is exiting */
    IBF_DISCONNECTED = -5,      /* the host freed this client's slot (no heartbeat for 1 s) */
    IBF_INVALID_ARGUMENT = -6,
    IBF_BAD_BRIDGE = -7,        /* the bridge's layout or contents make no sense */
    IBF_SYSTEM_ERROR = -8,
    IBF_UNSUPPORTED = -9,       /* a host too old for the call, or shm:// where there is no shared memory */
    IBF_BUSY = -10,             /* the snapshot changed under every retry, or the log queue was full */
    IBF_PAD_TAKEN = -11,        /* another client owns the pad (bind with force to take it) */
    IBF_NOT_BOT_PAD = -12,      /* the slot is not a bot pad (the host's input.bot_pads) */
    IBF_NOT_BOUND = -13,        /* this client owns no pad: never bound, released, or taken by another */
    IBF_COMMAND_FAILED = -14,   /* the command ran and failed, or there is no such command: the text says why */
    IBF_COMMAND_REFUSED = -15,  /* the host refused the request: it takes no commands, or the line was malformed */
    IBF_REFUSED = -16,          /* tcp://: the host refused the connection (a wrong or spent token) */
} ibf_status;

typedef struct ibf_client ibf_client;

/* ibf_connect_opts.spin_us (design 6.3). A spinning client wakes just before
 * each tick is due and spins until it comes: the lowest latency, and no
 * wake-up for the game thread to pay, for about 6% of a core per client. */
#define IBF_SPIN_DEFAULT 0u           /* a bot spins IBF_SPIN_BOT_US; a tool (IBF_CLIENT_CAPS_TOOL) blocks */
#define IBF_SPIN_OFF 0xFFFFFFFFu      /* block until each tick */
#define IBF_SPIN_BOT_US 1000u         /* the default window around each tick */

typedef struct ibf_connect_opts {
    uint32_t size;               /* sizeof(ibf_connect_opts) */
    const char* name;            /* UTF-8, up to 31 bytes; shown in-game and in logs. NULL: "client" */
    uint32_t instance_pid;       /* the host's process ID; 0: the newest running host */
    uint32_t caps;               /* IBF_CLIENT_CAPS_* */
    uint32_t spin_us;            /* the spin window around each tick in us, IBF_SPIN_DEFAULT or IBF_SPIN_OFF */
    uint32_t connect_timeout_ms; /* wait this long for a host to appear; 0: fail at once */
    /* tcp://: the token the host's TCP server asks for ([bridge.tcp] token,
     * or one the referee pinned to a pad); NULL for none. Ignored by shm://. */
    const char* token;
} ibf_connect_opts;

/* What came with a snapshot. publish-to-wake = wake_ns - publish_ns. */
typedef struct ibf_tick_info {
    uint64_t tick;
    uint64_t publish_seq; /* which publication: 1, 2, ... */
    uint64_t publish_ns;  /* host clock when it was published */
    uint64_t wake_ns;     /* host clock when this client saw it (woke, or found it waiting) */
    uint32_t size;        /* bytes copied: the snapshot, then its extras */
    uint32_t skipped;     /* publications this client missed before this one ("latest wins") */
} ibf_tick_info;

typedef struct ibf_host_info {
    uint32_t pid;
    uint32_t generation;
    uint16_t abi_major;
    uint16_t abi_minor;
    uint32_t client_index; /* this client's row */
    uint32_t host_state;   /* IBF_HOST_STATE_* */
    char version[IBF_VERSION_LEN];
    uint64_t clock_mono_ns; /* unix_ns = clock_unix_ns + (t - clock_mono_ns) */
    int64_t clock_unix_ns;
    uint64_t published;     /* snapshots published so far */
} ibf_host_info;

/* One active row of the client table. */
typedef struct ibf_client_row {
    uint32_t index;
    uint32_t pid;
    uint32_t caps;
    uint32_t generation;
    uint64_t connected_ns;
    uint64_t heartbeat_ns;
    uint8_t pad;
    char name[IBF_CLIENT_NAME_LEN];
} ibf_client_row;

/* One running host from the directory. */
typedef struct ibf_instance {
    uint32_t pid;
    uint16_t abi_major;
    uint16_t abi_minor;
    uint64_t process_time; /* FILETIME of the process's creation: newer is larger */
    char version[IBF_VERSION_LEN];
} ibf_instance;

/* One host log record; source and text are NUL-terminated UTF-8. */
typedef struct ibf_log_record {
    uint64_t seq;
    uint64_t time_ns;
    uint64_t tick;
    uint32_t thread;
    uint32_t level; /* IBF_LOG_LEVEL_* */
    uint32_t source_len;
    uint32_t text_len;
    char source[IBF_LOG_SOURCE_MAX + 1];
    char text[IBF_LOG_TEXT_MAX + 1];
} ibf_log_record;

/* Connects to a host: "shm://" (or NULL) for shared memory, "tcp://host[:port]"
 * (port 47800 when left out) for the host's TCP server. opts may be NULL
 * (name "client", the newest host, ticks, spinning). On failure *out stays NULL and
 * ibf_connect_error() says why, naming both versions on an ABI mismatch.
 * Over TCP, connect_timeout_ms keeps trying a host that is not listening yet,
 * and a token pinned to a pad (IBF_REFUSED otherwise) connects with that pad
 * bound: such a client cannot run commands, bind another pad or read the
 * host's log. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_connect(const char* url, const ibf_connect_opts* opts, ibf_client** out);
/* Says goodbye (the host frees the slot at once) and frees the handle. */
IBF_CLIENT_API void IBF_CALL ibf_disconnect(ibf_client* c);

/* Waits up to timeout_ms (UINT32_MAX: forever) for a snapshot newer than the
 * last one this client got; the newest wins and the ones in between count as
 * skipped. *out points into the handle and stays valid until the next wait
 * or latest call. IBF_TIMEOUT when nothing new came. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_wait_tick(ibf_client* c, uint32_t timeout_ms, const ibf_snapshot** out);
/* The same, copying into the caller's buffer (one copy, for language SDKs).
 * capacity must hold the payload: IBF_RING_SLOT_SIZE is always enough. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_wait_tick_copy(ibf_client* c, uint32_t timeout_ms, void* out,
                                                      uint32_t capacity, ibf_tick_info* info);
/* The newest snapshot now, without waiting; IBF_TIMEOUT before the first.
 * Does not change what counts as new for the waits. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_latest(ibf_client* c, const ibf_snapshot** out);
IBF_CLIENT_API ibf_status IBF_CALL ibf_latest_copy(ibf_client* c, void* out, uint32_t capacity, ibf_tick_info* info);
/* The facts of the last snapshot a wait or latest call returned. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_last_tick(const ibf_client* c, ibf_tick_info* out);

/* Writes the heartbeat and checks that the host and this client's slot are
 * still there. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_heartbeat(ibf_client* c);

/* Logs text into the host's log as bot:<name> (design 9.2), split into
 * pieces of IBF_LOG_TEXT_MAX bytes. level is IBF_LOG_LEVEL_*. IBF_BUSY when
 * the queue was full and the text was dropped (and counted). Any thread. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_log(ibf_client* c, int level, const char* text);
IBF_CLIENT_API ibf_status IBF_CALL ibf_log_n(ibf_client* c, int level, const char* text, size_t length);

/* The host's log ring. ibf_logs_open places the reader at the oldest record
 * still held (from_oldest) or at the next one; ibf_logs_read gives one
 * record (IBF_OK) or IBF_TIMEOUT when caught up. ibf_logs_lost counts the
 * records overwritten before this reader got to them. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_logs_open(ibf_client* c, int from_oldest);
IBF_CLIENT_API ibf_status IBF_CALL ibf_logs_read(ibf_client* c, ibf_log_record* out);
IBF_CLIENT_API uint64_t IBF_CALL ibf_logs_lost(const ibf_client* c);

IBF_CLIENT_API ibf_status IBF_CALL ibf_info(const ibf_client* c, ibf_host_info* out);
/* The active rows of the client table, this client's included. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_clients(const ibf_client* c, ibf_client_row* out, uint32_t capacity,
                                               uint32_t* count);
/* The running hosts on this machine, newest first. Needs no connection.
 * Shared memory only: a build without it finds none. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_instances(ibf_instance* out, uint32_t capacity, uint32_t* count);

Pads (design 6.4, 6.5, 7.1)

/* ---- pads (design 6.4, 6.5, 7.1) ----
 * A bot pad is a player slot the host made virtual from the game's start-up
 * (input.bot_pads), so the game counts it as a player. A client owns at most
 * one; the host frees it, and it reads neutral, when the client leaves. */

/* Takes pad (0-3). With force, takes it from the client that owns it. Binding
 * another pad gives the one this client had back. IBF_NOT_BOT_PAD,
 * IBF_PAD_TAKEN; IBF_UNSUPPORTED for a host without pads. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_bind_pad(ibf_client* c, int pad, int force);
/* Gives the pad back: it reads neutral. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_release_pad(ibf_client* c);
/* The pad this client owns now, or -1 (never bound, released, or taken). */
IBF_CLIENT_API int IBF_CALL ibf_pad(const ibf_client* c);
/* The pad's state from now on (in->seq is ignored; the flag
 * IBF_PAD_INPUT_FLAGS_CONNECTED is set for you). in->for_tick is the tick of
 * the snapshot this input answers; 0 means the last one a wait returned.
 * Write every tick: a state unchanged for input.timeout_ms reads neutral.
 * IBF_NOT_BOUND when the pad is not this client's (any more). */
IBF_CLIENT_API ibf_status IBF_CALL ibf_set_pad(ibf_client* c, const ibf_pad_input* in);
/* The four pad_owner bytes: a client row, IBF_NO_PAD (a free bot pad) or
 * IBF_PAD_NOT_BOT, for each pad. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_pad_owners(const ibf_client* c, uint8_t out[4]);
/* The pad input each game action maps to (design 7.7, the host's
 * [controls]): the default mapping when the host predates it. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_get_controls(const ibf_client* c, ibf_controls* out);

Run modes (design 4.5; ABI 0.7)

/* ---- run modes (design 4.5; ABI 0.7) ----
 * In lockstep the host waits after each tick until every bot pad answers it
 * (ibf_set_pad with that tick, or a later one), up to its lockstep timeout;
 * ibf_set_pad wakes the wait. While the simulation is paused (sim.pause) no
 * ticks come, except the one that shows it. */

/* IBF_RUN_MODE_REALTIME or IBF_RUN_MODE_LOCKSTEP; -1 from a host before ABI 0.7. */
IBF_CLIENT_API int IBF_CALL ibf_get_run_mode(const ibf_client* c);
/* IBF_SIM_STATE_RUNNING or IBF_SIM_STATE_PAUSED; -1 from a host before ABI 0.7. */
IBF_CLIENT_API int IBF_CALL ibf_get_sim_state(const ibf_client* c);

Commands (design 9.3)

/* ---- commands (design 9.3) ----
 * Runs one console command line on the host (ibf_command: up to 30 s) and
 * copies its result, NUL-terminated, into out. IBF_OK, IBF_COMMAND_FAILED
 * (the result says why), IBF_COMMAND_REFUSED, IBF_TIMEOUT. A line is at most
 * IBF_COMMAND_LINE_MAX - 1 bytes. A result longer than about 35 KB arrives
 * cut, with a note; *total (when not NULL) is the whole result's length. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_command(ibf_client* c, const char* line, char* out, size_t capacity);
IBF_CLIENT_API ibf_status IBF_CALL ibf_command_ex(ibf_client* c, const char* line, uint32_t timeout_ms, char* out,
                                                  size_t capacity, size_t* length, size_t* total);

Drawing (design 8.4)

/* ---- drawing (design 8.4) ----
 * A bot draws over the game with a list of commands it publishes whole: the
 * overlay shows the newest list until the next replaces it or its time to
 * live runs out (0 means 500 ms). The list is built in shared memory, so
 * building and publishing copy nothing more:
 *
 *     ibf_draw* d = ibf_draw_begin(c);
 *     ibf_draw_line(d, a, b, IBF_RGBA(255, 51, 85, 255), 2.0f);  // world units: tiles, +Y up
 *     ibf_draw_publish(c, d, 0);
 *
 * A list holds IBF_DRAW_MAX_COMMANDS commands and IBF_DRAW_PAYLOAD_MAX bytes
 * of payload; what does not fit is dropped and the list marked overflow. */
typedef struct ibf_draw ibf_draw;
#define IBF_RGBA(r, g, b, a) \
    ((uint32_t)(r) | ((uint32_t)(g) << 8) | ((uint32_t)(b) << 16) | ((uint32_t)(a) << 24))

/* Starts a new list, empty; NULL when the host has no draw lists (it predates
 * ABI 0.5) or the client is gone. The list stays this handle's until published. */
IBF_CLIENT_API ibf_draw* IBF_CALL ibf_draw_begin(ibf_client* c);
/* Appends one command; its payload (payload_len bytes, may be NULL when 0)
 * goes into the list's payload area and the command's payload_off and
 * payload_len point at it. IBF_BUSY when it did not fit (it is counted). */
IBF_CLIENT_API ibf_status IBF_CALL ibf_draw_add(ibf_draw* d, const ibf_draw_cmd* cmd, const void* payload,
                                                uint32_t payload_len);
/* Shorthands in world space (tiles, origin at the board's centre, +Y up). */
IBF_CLIENT_API void IBF_CALL ibf_draw_line(ibf_draw* d, ibf_vec2 a, ibf_vec2 b, uint32_t rgba, float width);
IBF_CLIENT_API void IBF_CALL ibf_draw_rect(ibf_draw* d, ibf_vec2 a, ibf_vec2 b, uint32_t rgba, float width, int fill);
IBF_CLIENT_API void IBF_CALL ibf_draw_circle(ibf_draw* d, ibf_vec2 centre, float radius, uint32_t rgba, float width,
                                             int fill);
/* At most 4,096 bytes of the text, cut between UTF-8 characters; empty text
 * counts as dropped (the host draws none). */
IBF_CLIENT_API void IBF_CALL ibf_draw_text(ibf_draw* d, ibf_vec2 at, const char* text, uint32_t rgba, float size);
/* A board tile (x, y), counted from the lower left. */
IBF_CLIENT_API void IBF_CALL ibf_draw_tile(ibf_draw* d, int x, int y, uint32_t rgba, int fill);
/* width x height cells from tile (x, y), the lower left, one byte each, row
 * by row from the bottom; palette is an IBF_HEATMAP_PALETTE_*, and rgba's
 * alpha the opacity (the tint palette uses its colour too). At most 256 cells
 * a side, and the cells and their 8-byte header fit the list's payload
 * (IBF_DRAW_PAYLOAD_MAX): past either it counts as dropped. */
IBF_CLIENT_API void IBF_CALL ibf_draw_heatmap(ibf_draw* d, int x, int y, int width, int height, const uint8_t* cells,
                                              int palette, uint32_t rgba);
/* Publishes the list (ttl_ms 0: 500 ms); the handle is spent. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_draw_publish(ibf_client* c, ibf_draw* d, uint32_t ttl_ms);
/* A whole list at once, for language SDKs: count commands whose payload_off
 * index payload (payload_len bytes). Too many commands or too much payload
 * are dropped and the list marked overflow; dropped counts the commands the
 * caller dropped already (they mark it too). */
IBF_CLIENT_API ibf_status IBF_CALL ibf_draw_submit(ibf_client* c, const ibf_draw_cmd* cmds, uint32_t count,
                                                   const void* payload, uint32_t payload_len, uint32_t ttl_ms,
                                                   uint32_t dropped);

Parameters (design 8.6)

/* ---- parameters (design 8.6) ----
 * A bot declares parameters; the overlay's Bots panel shows each as a control,
 * and the console's param.set changes them. The host keeps the values. */

/* Declares the bot's parameters (at most IBF_MAX_PARAMS), replacing any
 * declared before; a parameter declared again with the same name and kind
 * keeps its value. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_params_define(ibf_client* c, const ibf_param_def* defs, uint32_t count);
/* Adds one parameter to those declared. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_param_define(ibf_client* c, const ibf_param_def* def);
/* The values, in the order declared, when the host changed any since change
 * count `since` (0 at first): IBF_OK with *count values and the new *changes,
 * or IBF_TIMEOUT when nothing changed. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_param_values(ibf_client* c, uint32_t since, ibf_param_value* out,
                                                    uint32_t capacity, uint32_t* count, uint32_t* changes);

Stats (design 8.6)

/* ---- stats (design 8.6) ----
 * Named numbers or short texts the Bots panel shows; a number flagged
 * IBF_STAT_FLAGS_PLOT gets a sparkline. The client also reports its ticks,
 * skipped ticks and publish-to-wake there by itself. */
IBF_CLIENT_API ibf_status IBF_CALL ibf_stats_publish(ibf_client* c, const ibf_stat_entry* stats, uint32_t count);

/* This machine's monotonic clock in nanoseconds. On Windows it is
 * QueryPerformanceCounter, the same numbers as a local host's snapshot
 * host_time_ns; a TCP client's tick info is in the host's clock instead,
 * converted with an offset it measures. */
IBF_CLIENT_API uint64_t IBF_CALL ibf_now_ns(void);
/* (IBF_ABI_MAJOR << 16) | IBF_ABI_MINOR, as this library was built. */
IBF_CLIENT_API uint32_t IBF_CALL ibf_client_abi(void);
IBF_CLIENT_API const char* IBF_CALL ibf_status_text(ibf_status status);
/* Why the last ibf_connect on this thread failed. */
IBF_CLIENT_API const char* IBF_CALL ibf_connect_error(void);
/* Why the last call on this handle failed. */
IBF_CLIENT_API const char* IBF_CALL ibf_last_error(const ibf_client* c);

#if defined(__cplusplus)
}
#endif

#endif /* IBF_CLIENT_H_ */

ibf.hpp: C++20 over the C API

Header-only; link ibf_client.

ibf.hpp: the C++20 wrapper over ibf_client.h (design 10.10, M7 task 4).

Header-only; link ibf_client (the DLL's import library, or the static
library with IBF_CLIENT_STATIC defined). A bot:

    #include "ibf.hpp"

    int main() {
        const ibf::RunEnv env = ibf::RunEnv::read(0);          // what `ibf run` / `ibf match` set
        ibf::Client client = ibf::Client::connect({.name = env.name_or("walker"), .instance = env.instance,
                                                   .connect_timeout_ms = env.wait_ms > 0 ? env.wait_ms : 10000,
                                                   .url = env.url, .token = env.token});
        if (env.pad >= 0 && client.pad() < 0) client.bind_pad(env.pad, env.force);  // IBF_PAD=none: -1
        const ibf_controls controls = client.controls();
        for (;;) {
            const std::optional<ibf::Snapshot> s = client.wait(std::chrono::milliseconds(100));
            if (!s || client.pad() < 0) continue;             // no tick within 100 ms, or no pad
            client.set_pad(ibf::Pad::action(ibf::Dir::right, ibf::Dir::none, controls));
        }
    }

Errors are exceptions (ibf::Error, with the ibf_status); a host that leaves
ends wait() with ibf::Error whose status() is IBF_HOST_GONE or
IBF_DISCONNECTED. A Snapshot is a view into the client: valid until its next
wait() or latest(). A Client is for one thread at a time, like the handle it
wraps (log() excepted).

Declarations

#pragma once

#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "ibf_client.h"

namespace ibf {

Errors

// ---- errors -------------------------------------------------------------------------

class Error : public std::runtime_error {
public:
    Error(ibf_status status, const std::string& what) : std::runtime_error(what), status_(status) {}
    ibf_status status() const noexcept { return status_; }

private:
    ibf_status status_;
};

// A console command that failed or was refused; text() is the host's answer.
class CommandError : public Error {
public:
    CommandError(ibf_status status, std::string text) : Error(status, text), text_(std::move(text)) {}
    const std::string& text() const noexcept { return text_; }

private:
    std::string text_;
};

namespace detail {
inline void check(ibf_status status, const ibf_client* c, std::string_view what) {
    if (status == IBF_OK) return;
    const char* why = c != nullptr ? ibf_last_error(c) : ibf_connect_error();
    std::string text(what);
    text += ": ";
    text += (why != nullptr && why[0] != '\0') ? why : ibf_status_text(status);
    throw Error(status, text);
}

template <std::size_t N>
void copy_text(char (&out)[N], std::string_view text) {
    const std::size_t n = std::min(text.size(), N - 1);
    std::memcpy(out, text.data(), n);
    out[n] = '\0';
}
}  // namespace detail

Geometry

// ---- geometry -------------------------------------------------------------------------

struct Vec2 {
    float x = 0;
    float y = 0;

    constexpr Vec2() = default;
    constexpr Vec2(float x_, float y_) : x(x_), y(y_) {}
    constexpr Vec2(ibf_vec2 v) : x(v.x), y(v.y) {}  // NOLINT: from the schema's vector
    constexpr operator ibf_vec2() const { return {x, y}; }
    constexpr Vec2 operator+(Vec2 o) const { return {x + o.x, y + o.y}; }
    constexpr Vec2 operator-(Vec2 o) const { return {x - o.x, y - o.y}; }
    constexpr Vec2 operator*(float k) const { return {x * k, y * k}; }
    float length() const { return std::sqrt(x * x + y * y); }
    float distance(Vec2 o) const { return (*this - o).length(); }
};

struct Tile {
    int x = 0;
    int y = 0;
    constexpr bool operator==(const Tile&) const = default;
};

// A direction: up, down, left, right and none for shots; the diagonals for moves too.
enum class Dir { none, up, down, left, right, up_left, up_right, down_left, down_right };

constexpr Tile dir_step(Dir d) {
    switch (d) {
        case Dir::up: return {0, 1};
        case Dir::down: return {0, -1};
        case Dir::left: return {-1, 0};
        case Dir::right: return {1, 0};
        case Dir::up_left: return {-1, 1};
        case Dir::up_right: return {1, 1};
        case Dir::down_left: return {-1, -1};
        case Dir::down_right: return {1, -1};
        default: return {0, 0};
    }
}

The game state

// ---- the game state -------------------------------------------------------------------

// The board: tiles row by row from the bottom, the world's origin at its centre.
// The helpers do not wrap (a wrapping map: Snapshot::wraps()).
class Board {
public:
    explicit Board(const ibf_board& b) : b_(&b) {}
    int width() const { return b_->width; }
    int height() const { return b_->height; }
    bool contains(Tile t) const { return t.x >= 0 && t.y >= 0 && t.x < width() && t.y < height(); }
    // IBF_TILE_WHITE, IBF_TILE_BLACK, or IBF_TILE_NONE off the board.
    ibf_tile tile(Tile t) const { return contains(t) ? b_->tiles[t.y * width() + t.x] : IBF_TILE_NONE; }
    Tile to_tile(Vec2 pos) const {
        return {static_cast<int>(std::floor(pos.x + static_cast<float>(width()) / 2.0f)),
                static_cast<int>(std::floor(pos.y + static_cast<float>(height()) / 2.0f))};
    }
    Vec2 center(Tile t) const {
        return {static_cast<float>(t.x) + 0.5f - static_cast<float>(width()) / 2.0f,
                static_cast<float>(t.y) + 0.5f - static_cast<float>(height()) / 2.0f};
    }
    // A ship walks on the other colour: a white ship on black tiles, a black one on white.
    bool walkable(Tile t, ibf_color ship) const {
        const ibf_tile v = tile(t);
        return v != IBF_TILE_NONE && (ship == IBF_COLOR_WHITE || ship == IBF_COLOR_BLACK) && v != ship;
    }
    const ibf_board& raw() const { return *b_; }

private:
    const ibf_board* b_;
};

// One snapshot, read in place: valid until the client's next wait() or latest().
class Snapshot {
public:
    Snapshot(const ibf_snapshot* s, std::uint32_t size) : s_(s), size_(size) {}

    const ibf_snapshot& raw() const { return *s_; }
    std::uint32_t size() const { return size_; }
    std::uint64_t tick() const { return s_->header.tick; }
    std::uint64_t frame() const { return s_->header.frame; }
    float time() const { return s_->header.game_time; }
    bool has(ibf_snapshot_flags flag) const { return (s_->header.flags & flag) != 0; }
    bool valid() const { return has(IBF_SNAPSHOT_FLAGS_VALID); }
    bool in_match() const { return has(IBF_SNAPSHOT_FLAGS_IN_MATCH); }
    bool lockstep() const { return has(IBF_SNAPSHOT_FLAGS_LOCKSTEP); }
    bool sim_paused() const { return has(IBF_SNAPSHOT_FLAGS_SIM_PAUSED); }
    ibf_phase phase() const { return s_->game.phase; }
    const ibf_game& game() const { return s_->game; }
    Board board() const { return Board(s_->board); }

    std::span<const ibf_ship> ships() const { return {s_->ships, std::min<std::size_t>(s_->ship_count, IBF_MAX_SHIPS)}; }
    std::span<const ibf_projectile> projectiles() const {
        return {s_->projectiles, std::min<std::size_t>(s_->projectile_count, IBF_MAX_PROJECTILES)};
    }
    std::span<const ibf_pickup> pickups() const {
        return {s_->pickups, std::min<std::size_t>(s_->pickup_count, IBF_MAX_PICKUPS)};
    }
    // The ship player `pad` plays (pad n is player n), or null.
    const ibf_ship* player(int pad) const {
        if (pad < 0 || pad >= static_cast<int>(IBF_MAX_PLAYERS)) return nullptr;
        const std::uint8_t i = s_->players[pad];
        return i < s_->ship_count && i < IBF_MAX_SHIPS ? &s_->ships[i] : nullptr;
    }
    // ABI 0.6's extensions, from a host that has them.
    bool has_ext() const { return s_->header.schema_minor >= 6; }
    const ibf_match* match() const { return has_ext() ? &s_->match : nullptr; }
    const ibf_ship_ext* ship_ext(const ibf_ship& ship) const {
        const std::ptrdiff_t i = &ship - s_->ships;
        return has_ext() && i >= 0 && i < static_cast<std::ptrdiff_t>(IBF_MAX_SHIPS) ? &s_->ships_ext[i] : nullptr;
    }
    bool wraps() const { return has_ext() && s_->match.wrap != 0; }

    // The living ships of the other colour than `me`'s, nearest first.
    std::vector<const ibf_ship*> enemies(const ibf_ship& me) const {
        std::vector<const ibf_ship*> out;
        for (const ibf_ship& s : ships()) {
            if (&s == &me || !s.alive) continue;
            if (me.color != IBF_COLOR_UNKNOWN && s.color == me.color) continue;
            out.push_back(&s);
        }
        std::sort(out.begin(), out.end(), [&](const ibf_ship* a, const ibf_ship* b) {
            return Vec2(a->pos).distance(me.pos) < Vec2(b->pos).distance(me.pos);
        });
        return out;
    }

    // The events detected since `seen` (a count of events, 0 at first), oldest
    // first; `seen` moves past them. Older ones the ring no longer holds are lost.
    std::vector<ibf_event> events_since(std::uint64_t& seen) const {
        std::vector<ibf_event> out;
        const std::uint64_t total = s_->event_total;
        std::uint64_t first = seen;
        if (total > IBF_MAX_EVENTS && first < total - IBF_MAX_EVENTS) first = total - IBF_MAX_EVENTS;
        for (std::uint64_t i = first; i < total; ++i) out.push_back(s_->events[i % IBF_MAX_EVENTS]);
        seen = total;
        return out;
    }

private:
    const ibf_snapshot* s_;
    std::uint32_t size_;
};

Input

// ---- input ----------------------------------------------------------------------------

// One pad state. Actions map through the host's [controls] (design 7.7), as
// the Python SDK's Action does.
struct Pad {
    ibf_pad_input in{};

    static Pad neutral() { return {}; }
    // Movement toward (x, y), each -1..1, the way `controls` moves: a stick, or
    // the D-pad's nearest of eight directions.
    static Pad stick(float x, float y, const ibf_controls& controls) {
        Pad p;
        const float length = std::sqrt(x * x + y * y);
        if (length > 1.0f) {
            x /= length;
            y /= length;
        }
        if (controls.move == IBF_CONTROL_STICK_DPAD) {
            if (length > 0.3f) {
                const int sector = static_cast<int>(std::lround(std::atan2(y, x) / (3.14159265f / 4.0f)) + 8) % 8;
                std::uint16_t b = 0;
                if (sector == 0 || sector == 1 || sector == 7) b |= IBF_PAD_BUTTONS_DPAD_RIGHT;
                if (sector == 3 || sector == 4 || sector == 5) b |= IBF_PAD_BUTTONS_DPAD_LEFT;
                if (sector == 1 || sector == 2 || sector == 3) b |= IBF_PAD_BUTTONS_DPAD_UP;
                if (sector == 5 || sector == 6 || sector == 7) b |= IBF_PAD_BUTTONS_DPAD_DOWN;
                p.in.buttons = static_cast<ibf_pad_buttons>(b);
            }
            return p;
        }
        const std::int16_t ax = axis(x);
        const std::int16_t ay = axis(y);
        if (controls.move == IBF_CONTROL_STICK_RIGHT_STICK) {
            p.in.rx = ax;
            p.in.ry = ay;
        } else {
            p.in.lx = ax;
            p.in.ly = ay;
        }
        return p;
    }
    // A move in one of eight directions (diagonals at 0.707), and a held shot
    // up, down, left or right (holding charges it).
    static Pad action(Dir move, Dir shoot, const ibf_controls& controls) {
        const Tile m = dir_step(move);
        const float k = (m.x != 0 && m.y != 0) ? 0.70710678f : 1.0f;
        Pad p = stick(static_cast<float>(m.x) * k, static_cast<float>(m.y) * k, controls);
        return p.press(shoot_buttons(shoot, controls));
    }
    static std::uint16_t shoot_buttons(Dir shoot, const ibf_controls& controls) {
        switch (shoot) {
            case Dir::up: return controls.shoot_up;
            case Dir::down: return controls.shoot_down;
            case Dir::left: return controls.shoot_left;
            case Dir::right: return controls.shoot_right;
            default: return 0;
        }
    }
    Pad& press(std::uint16_t buttons) {
        in.buttons = static_cast<ibf_pad_buttons>(in.buttons | buttons);
        return *this;
    }
    static std::int16_t axis(float v) {
        return static_cast<std::int16_t>(std::lround(std::clamp(v, -1.0f, 1.0f) * 32767.0f));
    }
};

// What `ibf run` and `ibf match` tell a bot they start (design 10.4): its pad,
// the host, its name, whether to take the pad from another client; and for a
// host over TCP (design 6.6), its address and the token (IBF_URL, IBF_TOKEN:
// the arena's bot containers).
struct RunEnv {
    int pad = 0;  // -1: watch only
    std::uint32_t instance = 0;
    std::string name;
    bool force = false;
    std::string url;    // "tcp://host:port"; empty: shared memory
    std::string token;
    std::uint32_t wait_ms = 0;  // IBF_WAIT (seconds): how long to keep trying to connect; 0: the bot's own choice

    static RunEnv read(int default_pad = 0) {
        RunEnv env;
        env.pad = default_pad;
        if (const auto p = variable("IBF_PAD"); p && !p->empty()) env.pad = *p == "none" ? -1 : std::atoi(p->c_str());
        if (const auto i = variable("IBF_INSTANCE")) {
            env.instance = static_cast<std::uint32_t>(std::strtoul(i->c_str(), nullptr, 10));
        }
        if (const auto n = variable("IBF_BOT_NAME")) env.name = *n;
        if (const auto f = variable("IBF_FORCE_PAD")) env.force = *f == "1";
        if (const auto u = variable("IBF_URL")) env.url = *u;
        if (const auto t = variable("IBF_TOKEN")) env.token = *t;
        if (const auto w = variable("IBF_WAIT"); w && !w->empty()) {
            env.wait_ms = static_cast<std::uint32_t>(std::max(0.0, std::atof(w->c_str())) * 1000.0);
        }
        return env;
    }
    // An environment variable's value, or nullopt when it is not set.
    static std::optional<std::string> variable(const char* name) {
#if defined(_MSC_VER)
        char* value = nullptr;
        std::size_t length = 0;
        if (_dupenv_s(&value, &length, name) != 0 || value == nullptr) return std::nullopt;
        std::string out(value);
        std::free(value);
        return out;
#else
        const char* value = std::getenv(name);
        return value != nullptr ? std::optional<std::string>(value) : std::nullopt;
#endif
    }
    std::string name_or(std::string_view fallback) const { return name.empty() ? std::string(fallback) : name; }
};

Parameters and stats (design 8.6)

// ---- parameters and stats (design 8.6) -------------------------------------------------

inline ibf_param_def param(ibf_param_kind kind, std::string_view name, double value, double min = 0, double max = 0,
                           std::string_view help = {}) {
    ibf_param_def d{};
    detail::copy_text(d.name, name);
    d.kind = kind;
    d.default_value = value;
    d.min = min;
    d.max = max;
    detail::copy_text(d.help, help);
    return d;
}
inline ibf_param_def param_bool(std::string_view name, bool value, std::string_view help = {}) {
    return param(IBF_PARAM_KIND_BOOL, name, value ? 1.0 : 0.0, 0, 1, help);
}
inline ibf_param_def param_float(std::string_view name, double value, double min, double max,
                                 std::string_view help = {}) {
    return param(IBF_PARAM_KIND_FLOAT, name, value, min, max, help);
}
inline ibf_param_def param_int(std::string_view name, int value, int min, int max, std::string_view help = {}) {
    return param(IBF_PARAM_KIND_INT, name, value, min, max, help);
}

inline ibf_stat_entry stat(std::string_view name, double value, bool plot = false) {
    ibf_stat_entry e{};
    detail::copy_text(e.name, name);
    e.kind = IBF_STAT_KIND_NUMBER;
    e.flags = plot ? IBF_STAT_FLAGS_PLOT : static_cast<ibf_stat_flags>(0);
    e.value = value;
    return e;
}
inline ibf_stat_entry stat(std::string_view name, std::string_view text) {
    ibf_stat_entry e{};
    detail::copy_text(e.name, name);
    e.kind = IBF_STAT_KIND_TEXT;
    detail::copy_text(e.text, text);
    return e;
}

Drawing (design 8.4)

// ---- drawing (design 8.4) ---------------------------------------------------------------

// A draw list being built in shared memory: add to it, then publish(). A client
// builds one list at a time: the next Client::draw() drops this one unpublished,
// and from then on adding to this Draw does nothing and publishing it throws.
class Draw {
public:
    Draw(ibf_client* c, ibf_draw* d) : c_(c), d_(d) {}
    // Client::draw()'s: newest counts the client's lists; this one is the newest now.
    Draw(ibf_client* c, ibf_draw* d, std::shared_ptr<const std::uint64_t> newest)
        : c_(c), d_(d), newest_(std::move(newest)), number_(newest_ != nullptr ? *newest_ : 0) {}
    explicit operator bool() const { return building(); }
    Draw& line(Vec2 a, Vec2 b, std::uint32_t rgba, float width = 1.0f) {
        if (building()) ibf_draw_line(d_, a, b, rgba, width);
        return *this;
    }
    Draw& rect(Vec2 a, Vec2 b, std::uint32_t rgba, float width = 1.0f, bool fill = false) {
        if (building()) ibf_draw_rect(d_, a, b, rgba, width, fill ? 1 : 0);
        return *this;
    }
    Draw& circle(Vec2 centre, float radius, std::uint32_t rgba, float width = 1.0f, bool fill = false) {
        if (building()) ibf_draw_circle(d_, centre, radius, rgba, width, fill ? 1 : 0);
        return *this;
    }
    Draw& text(Vec2 at, const std::string& text, std::uint32_t rgba, float size = 0.0f) {
        if (building()) ibf_draw_text(d_, at, text.c_str(), rgba, size);
        return *this;
    }
    Draw& tile(Tile t, std::uint32_t rgba, bool fill = true) {
        if (building()) ibf_draw_tile(d_, t.x, t.y, rgba, fill ? 1 : 0);
        return *this;
    }
    Draw& heatmap(Tile from, int width, int height, std::span<const std::uint8_t> cells, int palette,
                  std::uint32_t rgba) {
        if (building() && cells.size() >= static_cast<std::size_t>(width) * static_cast<std::size_t>(height)) {
            ibf_draw_heatmap(d_, from.x, from.y, width, height, cells.data(), palette, rgba);
        }
        return *this;
    }
    // Shows the list (ttl_ms 0: 500 ms); this Draw is spent. Throws when a later
    // Client::draw() dropped it.
    void publish(std::uint32_t ttl_ms = 0) {
        if (d_ == nullptr) return;
        ibf_draw* d = std::exchange(d_, nullptr);
        if (!newest()) throw Error(IBF_INVALID_ARGUMENT, "draw: a later Client::draw() dropped this list");
        detail::check(ibf_draw_publish(c_, d, ttl_ms), c_, "draw");
    }

private:
    bool newest() const noexcept { return newest_ == nullptr || *newest_ == number_; }
    bool building() const noexcept { return d_ != nullptr && newest(); }

    ibf_client* c_;
    ibf_draw* d_;
    std::shared_ptr<const std::uint64_t> newest_;
    std::uint64_t number_ = 0;
};

The client

// ---- the client -------------------------------------------------------------------------

struct ConnectOptions {
    std::string name = "client";
    std::uint32_t instance = 0;  // the host's process ID; 0: the newest
    bool tool = false;           // a tool, not a bot (it blocks rather than spins, and is not counted)
    std::uint32_t spin_us = IBF_SPIN_DEFAULT;
    std::uint32_t connect_timeout_ms = 0;  // wait this long for a host to appear
    std::string url;    // "tcp://host:port" for a host over TCP (design 6.6); empty: shared memory
    std::string token;  // the TCP server's token, or one pinned to a pad
};

class Client {
public:
    static Client connect(const ConnectOptions& o = {}) {
        ibf_connect_opts opts{};
        opts.size = sizeof(opts);
        opts.name = o.name.c_str();
        opts.instance_pid = o.instance;
        opts.caps = IBF_CLIENT_CAPS_TICKS | (o.tool ? IBF_CLIENT_CAPS_TOOL : 0u);
        opts.spin_us = o.spin_us;
        opts.connect_timeout_ms = o.connect_timeout_ms;
        opts.token = o.token.empty() ? nullptr : o.token.c_str();
        ibf_client* c = nullptr;
        detail::check(ibf_connect(o.url.empty() ? "shm://" : o.url.c_str(), &opts, &c), nullptr, "connect");
        return Client(c);
    }

    Client(Client&& other) noexcept
        : c_(std::exchange(other.c_, nullptr)), since_(other.since_), draws_(other.draws_) {}
    Client& operator=(Client&& other) noexcept {
        if (this != &other) {
            close();
            c_ = std::exchange(other.c_, nullptr);
            since_ = other.since_;
            draws_ = other.draws_;
        }
        return *this;
    }
    Client(const Client&) = delete;
    Client& operator=(const Client&) = delete;
    ~Client() { close(); }

    // Says goodbye now (the destructor does it too).
    void close() noexcept {
        if (c_ != nullptr) ibf_disconnect(std::exchange(c_, nullptr));
    }
    ibf_client* get() const noexcept { return c_; }

    // The next snapshot newer than the last one this client got; nullopt when
    // none came within the timeout. Throws when the host has gone.
    std::optional<Snapshot> wait(std::chrono::milliseconds timeout) {
        const ibf_snapshot* s = nullptr;
        const ibf_status st = ibf_wait_tick(c_, static_cast<std::uint32_t>(timeout.count()), &s);
        if (st == IBF_TIMEOUT) return std::nullopt;
        detail::check(st, c_, "wait");
        return Snapshot(s, last_tick().size);
    }
    // The newest snapshot now; nullopt before the first, or when the host was
    // writing it through every retry (IBF_BUSY: ask again).
    std::optional<Snapshot> latest() {
        const ibf_snapshot* s = nullptr;
        const ibf_status st = ibf_latest(c_, &s);
        if (st == IBF_TIMEOUT || st == IBF_BUSY) return std::nullopt;
        detail::check(st, c_, "latest");
        return Snapshot(s, last_tick().size);
    }
    ibf_tick_info last_tick() const {
        ibf_tick_info info{};
        (void)ibf_last_tick(c_, &info);
        return info;
    }
    void heartbeat() { detail::check(ibf_heartbeat(c_), c_, "heartbeat"); }
    // Into the host's log as bot:<name>. Any thread.
    void log(ibf_log_level level, std::string_view text) { (void)ibf_log_n(c_, level, text.data(), text.size()); }
    ibf_host_info info() const {
        ibf_host_info out{};
        detail::check(ibf_info(c_, &out), c_, "info");
        return out;
    }

    // Pads (design 6.4): bind a bot pad, then set it every tick.
    void bind_pad(int pad, bool force = false) { detail::check(ibf_bind_pad(c_, pad, force ? 1 : 0), c_, "bind pad"); }
    void release_pad() { detail::check(ibf_release_pad(c_), c_, "release pad"); }
    int pad() const { return ibf_pad(c_); }
    // for_tick 0: the last tick a wait returned.
    void set_pad(const Pad& pad, std::uint64_t for_tick = 0) {
        ibf_pad_input in = pad.in;
        in.for_tick = for_tick;
        detail::check(ibf_set_pad(c_, &in), c_, "set pad");
    }
    ibf_controls controls() const {
        ibf_controls out{};
        (void)ibf_get_controls(c_, &out);
        return out;
    }
    bool lockstep() const { return ibf_get_run_mode(c_) == IBF_RUN_MODE_LOCKSTEP; }

    // A console command (design 9.3); its result, or CommandError.
    std::string command(std::string_view line, std::chrono::milliseconds timeout = std::chrono::seconds(30)) {
        const std::string text(line);
        std::string out(36 * 1024, '\0');
        std::size_t length = 0;
        const ibf_status st = ibf_command_ex(c_, text.c_str(), static_cast<std::uint32_t>(timeout.count()), out.data(),
                                             out.size(), &length, nullptr);
        out.resize(std::min(length, out.size()));
        if (st == IBF_COMMAND_FAILED || st == IBF_COMMAND_REFUSED) throw CommandError(st, out);
        detail::check(st, c_, "command");
        return out;
    }

    // A new draw list; an earlier Draw not yet published is dropped.
    Draw draw() {
        ++*draws_;
        return Draw(c_, ibf_draw_begin(c_), draws_);
    }

    void define_params(std::span<const ibf_param_def> defs) {
        detail::check(ibf_params_define(c_, defs.data(), static_cast<std::uint32_t>(defs.size())), c_, "parameters");
    }
    // The parameters' values in the order declared, when the host changed any
    // since the last call; nullopt when none changed (or the host was writing
    // them through every retry: the change comes on a later call).
    std::optional<std::vector<ibf_param_value>> param_values() {
        std::vector<ibf_param_value> out(IBF_MAX_PARAMS);
        std::uint32_t count = 0;
        std::uint32_t changes = 0;
        const ibf_status st = ibf_param_values(c_, since_, out.data(), IBF_MAX_PARAMS, &count, &changes);
        if (st == IBF_TIMEOUT || st == IBF_BUSY) return std::nullopt;
        detail::check(st, c_, "parameters");
        since_ = changes;
        out.resize(count);
        return out;
    }
    void publish_stats(std::span<const ibf_stat_entry> stats) {
        detail::check(ibf_stats_publish(c_, stats.data(), static_cast<std::uint32_t>(stats.size())), c_, "stats");
    }

private:
    explicit Client(ibf_client* c) : c_(c) {}
    ibf_client* c_ = nullptr;
    std::uint32_t since_ = 0;
    std::shared_ptr<std::uint64_t> draws_ = std::make_shared<std::uint64_t>(0);  // its Draws, counted
};

}  // namespace ibf

ibf_plugin.h: in-process plugins

A 32-bit DLL the host loads (design 11.2).

ibf_plugin: in-process plugins for the IBF host (design section 11.2).

A plugin is a 32-bit DLL, <game>\ibf\plugins\<name>.dll, that the host loads
when ibf.toml names it:

    [plugins]
    enabled = ["hello"]

    [plugins.hello]          # the plugin's own settings (config_get)
    greeting = "hi"

It exports one function, which the host calls once, on its service thread,
after the bridge has opened:

    IBF_PLUGIN_EXPORT int IBF_PLUGIN_CALL ibf_plugin_entry(const ibf_host_api* api, uint32_t api_version,
                                                           ibf_plugin_info* info);

Inside it the plugin registers what it wants (commands, a tick callback, a
render callback, probes) and fills `info`; returning anything but 0 declines
to load (the host logs the number). Registration works only during that
call. `api` stays valid for the life of the process: keep it.

What a plugin can do:
  - log into the host's log as plugin:<name>;
  - commands named "<name>.<command>", run from the console, the CLI and bots;
  - on_tick: each published snapshot, on the tick thread, read-only, before
    lockstep waits for the bots (so a plugin bot answers within its tick);
  - on_render: inside the overlay's Dear ImGui frame, with the context;
  - a bridge client of its own (IBF_PLUGIN_FLAG_CLIENT): play a bot pad,
    publish a draw list, declare parameters and stats, like any bot;
  - probes (design 5.6), as [[probe]] TOML: their values go to every bot.

Rules:
  - Every call into the plugin is guarded. A fault or an exception that
    escapes a callback disables the plugin, and only it: no more callbacks,
    and its commands answer that it is disabled. Plugins are never unloaded.
  - on_tick runs on the game's tick thread: no blocking, no waiting, keep it
    short (its time is the game's).
  - The client calls (pads, drawing, parameters, stats) may come from any
    thread: the host takes a plugin's calls one at a time. Their order across
    threads is the plugin's to keep (a set_pad on the tick thread racing a
    release_pad from a command lands before or after it).
  - ImGui: build against the host's Dear ImGui version (on_render refuses
    another) and call ibf_imgui_begin(render) first in the callback: it sets
    the host's context and allocator, so ImGui allocates from the host's
    heap, as it must.

The table is append-only: a later host adds members at its end and raises
api->size. A plugin that uses a member added after version 1 checks
IBF_PLUGIN_HAS(api, member) first.

Declarations

#ifndef IBF_PLUGIN_H_
#define IBF_PLUGIN_H_

#include <stddef.h>
#include <stdint.h>

#include "ibf_client.h"
#include "ibf_schema.h"

#define IBF_PLUGIN_API_VERSION 1u

#if defined(_WIN32)
#define IBF_PLUGIN_EXPORT __declspec(dllexport)
#define IBF_PLUGIN_CALL __cdecl
#else
#define IBF_PLUGIN_EXPORT __attribute__((visibility("default")))
#define IBF_PLUGIN_CALL
#endif

#if defined(__cplusplus)
extern "C" {
#endif

/* The host's record of one plugin: the first argument of every call. */
typedef struct ibf_plugin ibf_plugin;

/* ibf_plugin_info.flags */
#define IBF_PLUGIN_FLAG_CLIENT 0x1u        /* connect a bridge client for the plugin: pads, drawing, parameters, stats */
#define IBF_PLUGIN_FLAG_RENDER_HIDDEN 0x2u /* call on_render while the overlay is hidden too (its cost is the frame's) */

/* What the plugin says about itself; the host zeroes it and sets size first. */
typedef struct ibf_plugin_info {
    uint32_t size;          /* sizeof(ibf_plugin_info) as the host knows it */
    uint32_t flags;         /* IBF_PLUGIN_FLAG_* */
    char version[32];       /* the plugin's own version, for `plugins` */
    char description[96];
    /* The game is closing (its window is going away): on the game thread. May be NULL. */
    void(IBF_PLUGIN_CALL* shutdown)(void* user);
    void* user;
} ibf_plugin_info;

/* One published snapshot, for on_tick: valid during the call only. */
typedef struct ibf_plugin_tick {
    uint32_t size;        /* sizeof(ibf_plugin_tick) */
    uint32_t extras_size; /* bytes at extras: an ibf_extras_header and its blob */
    uint64_t tick;
    const ibf_snapshot* snapshot;
    const void* extras; /* NULL when there are none */
} ibf_plugin_tick;

/* The Dear ImGui a plugin was built with; on_render refuses a plugin whose
 * version or data layout differs from the host's. With imgui.h included
 * before this header, IBF_IMGUI_ABI fills it. */
typedef struct ibf_imgui_abi {
    uint32_t size; /* sizeof(ibf_imgui_abi) */
    uint32_t version_num;
    uint32_t sizeof_io;
    uint32_t sizeof_style;
    uint32_t sizeof_vec2;
    uint32_t sizeof_vec4;
    uint32_t sizeof_draw_vert;
    uint32_t sizeof_draw_idx;
} ibf_imgui_abi;

#if defined(IMGUI_VERSION_NUM)
#define IBF_IMGUI_ABI                                                                                                \
    {(uint32_t)sizeof(ibf_imgui_abi), (uint32_t)IMGUI_VERSION_NUM,  (uint32_t)sizeof(ImGuiIO),                      \
     (uint32_t)sizeof(ImGuiStyle),    (uint32_t)sizeof(ImVec2),     (uint32_t)sizeof(ImVec4),                       \
     (uint32_t)sizeof(ImDrawVert),    (uint32_t)sizeof(ImDrawIdx)}
#endif

/* ibf_plugin_render.flags */
#define IBF_RENDER_VISIBLE 0x1u /* the overlay is visible (not hidden) */
#define IBF_RENDER_PANELS 0x2u  /* the IBF panels are shown: draw windows only then */

/* The overlay's frame, for on_render: valid during the call only. */
typedef struct ibf_plugin_render {
    uint32_t size;  /* sizeof(ibf_plugin_render) */
    uint32_t flags; /* IBF_RENDER_* */
    void* imgui_context; /* ImGuiContext* */
    void*(IBF_PLUGIN_CALL* imgui_alloc)(size_t size, void* user); /* ImGui's allocator, the host's heap */
    void(IBF_PLUGIN_CALL* imgui_free)(void* ptr, void* user);
    void* imgui_alloc_user;
    void* background; /* ImDrawList*: under the IBF panels, over the game */
    void* foreground; /* ImDrawList*: over everything */
    uint32_t back_width;  /* the back buffer, in pixels */
    uint32_t back_height;
    uint64_t now_ns;
    const void* view; /* the host's: for world_to_screen */
} ibf_plugin_render;

/* A plugin command: argv are its words after the name ("key=value" words as
 * written). Writes its result, NUL-terminated, into out (capacity bytes);
 * returns 0 for success, anything else for failure. */
typedef int(IBF_PLUGIN_CALL* ibf_plugin_command_fn)(void* user, int argc, const char* const* argv, char* out,
                                                    uint32_t capacity);
typedef void(IBF_PLUGIN_CALL* ibf_plugin_tick_fn)(void* user, const ibf_plugin_tick* tick);
typedef void(IBF_PLUGIN_CALL* ibf_plugin_render_fn)(void* user, const ibf_plugin_render* render);

/* register_command flags */
#define IBF_PLUGIN_COMMAND_GAME 0x1u    /* run on the game thread, at its next frame (default: the service thread) */
#define IBF_PLUGIN_COMMAND_GUARDED 0x2u /* refused to other clients while a bot holds the match lock (design 10.8) */

/* The host's side. Every call takes the plugin's own `self` first. */
typedef struct ibf_host_api {
    uint32_t size;            /* sizeof(ibf_host_api) as the host built it */
    uint32_t version;         /* the host's IBF_PLUGIN_API_VERSION */
    ibf_plugin* self;         /* this plugin */
    const char* name;         /* this plugin's name: its file name, without .dll */
    const char* host_version; /* e.g. "0.7.0+3f2a9c1" */
    const char* ibf_dir;      /* <game>\ibf, UTF-8 */

    /* ---- any thread, any time ---- */

    /* Logs as plugin:<name>; level is IBF_LOG_LEVEL_*. */
    void(IBF_PLUGIN_CALL* log)(ibf_plugin* self, int level, const char* text);
    /* The host's clock (the snapshot's host_time_ns), and the newest tick's number. */
    uint64_t(IBF_PLUGIN_CALL* now_ns)(void);
    uint64_t(IBF_PLUGIN_CALL* tick_now)(void);
    /* Copies the newest snapshot and its extras (IBF_RING_SLOT_SIZE bytes is
     * always enough); IBF_TIMEOUT before the first. */
    ibf_status(IBF_PLUGIN_CALL* get_snapshot)(ibf_plugin* self, void* out, uint32_t capacity, uint32_t* size);
    /* A setting from [plugins.<name>], as text: a string without its quotes,
     * anything else as TOML writes it (cut to capacity). IBF_INVALID_ARGUMENT
     * when it is not there, and out keeps what it held: put the default in it
     * first. Edits to ibf.toml show at once. */
    ibf_status(IBF_PLUGIN_CALL* config_get)(ibf_plugin* self, const char* key, char* out, uint32_t capacity);

    /* ---- inside ibf_plugin_entry only (IBF_BUSY afterwards) ---- */

    /* name must start with "<plugin name>.". */
    ibf_status(IBF_PLUGIN_CALL* register_command)(ibf_plugin* self, const char* name, const char* usage,
                                                  const char* help, uint32_t flags, ibf_plugin_command_fn fn,
                                                  void* user);
    ibf_status(IBF_PLUGIN_CALL* on_tick)(ibf_plugin* self, ibf_plugin_tick_fn fn, void* user);
    /* IBF_UNSUPPORTED when the plugin's ImGui differs from the host's, or the overlay is off. */
    ibf_status(IBF_PLUGIN_CALL* on_render)(ibf_plugin* self, const ibf_imgui_abi* imgui, ibf_plugin_render_fn fn,
                                           void* user);
    /* [[probe]] tables, as in ibf\probes.toml (design 5.6). Loaded with the
     * profile; a probe that does not load is logged. */
    ibf_status(IBF_PLUGIN_CALL* register_probe)(ibf_plugin* self, const char* toml);

    /* ---- the plugin's bridge client (IBF_PLUGIN_FLAG_CLIENT), any thread; IBF_UNSUPPORTED without one ---- */

    ibf_status(IBF_PLUGIN_CALL* bind_pad)(ibf_plugin* self, int pad, int force);
    ibf_status(IBF_PLUGIN_CALL* release_pad)(ibf_plugin* self);
    /* in->for_tick 0 means the newest tick. Write every tick, as a bot does. */
    ibf_status(IBF_PLUGIN_CALL* set_pad)(ibf_plugin* self, const ibf_pad_input* in);
    ibf_status(IBF_PLUGIN_CALL* draw_submit)(ibf_plugin* self, const ibf_draw_cmd* cmds, uint32_t count,
                                             const void* payload, uint32_t payload_len, uint32_t ttl_ms);
    ibf_status(IBF_PLUGIN_CALL* params_define)(ibf_plugin* self, const ibf_param_def* defs, uint32_t count);
    ibf_status(IBF_PLUGIN_CALL* param_values)(ibf_plugin* self, uint32_t since, ibf_param_value* out,
                                              uint32_t capacity, uint32_t* count, uint32_t* changes);
    ibf_status(IBF_PLUGIN_CALL* stats_publish)(ibf_plugin* self, const ibf_stat_entry* stats, uint32_t count);

    /* ---- inside on_render only ---- */

    /* Where world point (x, y) (tiles, the board's centre, +Y up) lands on
     * screen this frame, in pixels; 0 when no board is placed. */
    int(IBF_PLUGIN_CALL* world_to_screen)(const ibf_plugin_render* render, float x, float y, float* sx, float* sy);
} ibf_host_api;

/* True when the host's table has `member` (a member added after version 1). */
#define IBF_PLUGIN_HAS(api, member) \
    ((api)->size >= offsetof(ibf_host_api, member) + sizeof(((ibf_host_api*)0)->member))

typedef int(IBF_PLUGIN_CALL* ibf_plugin_entry_fn)(const ibf_host_api* api, uint32_t api_version,
                                                  ibf_plugin_info* info);

#if defined(__cplusplus)
}
#endif

#if defined(__cplusplus) && defined(IMGUI_VERSION_NUM)
/* The first call in an on_render callback: the host's context and allocator. */
inline void ibf_imgui_begin(const ibf_plugin_render* render) {
    ImGui::SetAllocatorFunctions(reinterpret_cast<ImGuiMemAllocFunc>(render->imgui_alloc),
                                 reinterpret_cast<ImGuiMemFreeFunc>(render->imgui_free), render->imgui_alloc_user);
    ImGui::SetCurrentContext(static_cast<ImGuiContext*>(render->imgui_context));
}
#endif

#endif /* IBF_PLUGIN_H_ */