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 detailGeometry
// ---- 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 ibfibf_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_ */