BotStrikes

Current SDK guide

Build an AI for BotStrikes

Status ·

BotStrikes 0.2.0 · protocol 15 · Agent API 14 · bs_observation: 61 · bs_action: 11.

The SDK and training gym work in the private repository. This guide documents the current interface for people with repository or test-kit access. No public SDK download or public game server has been verified.

Getting started

An AI uses the game's encrypted transport and public inputs. It needs an AI invitation issued by the server, a matching library version and the same map and content. These examples run from the repository root, with build/check already built using tools/check.sh.

The first connection

Start the local server in one terminal. Site boxes enable the bomb objective. In another terminal, run the example with the issued token. Do not reuse a token after disconnecting: request a spare.

build/check/bs_server --listen 127.0.0.1:27149 --players 0 --ai-players 1 \
  --tokens build/tutorial-tokens --seconds 60 --map build/check/content/cinta.bsm \
  --mode teams --rounds 8 \
  --bomb-site-a -36.9,0,12.6,-26.9,3,22.6 --bomb-site-b 23.6,0,11.5,33.6,3,21.5
PYTHONPATH=sdk/python BOTSTRIKES_AGENT_LIB=build/check/libbotstrikes_agent.so \
  python3 sdk/python/examples/first_bot.py --token build/tutorial-tokens/ai-1.token \
  --map build/check/content/cinta.bsm --seconds 10

The expected result is Joined observations greater than zero and successful exit. The example walks and turns; it does not demonstrate navigation or objective play. Agent manages the connection; the policy object returns Action; run receives agent-policy pairs.

"""A small, executable SDK tutorial. Run from the repository with PYTHONPATH=sdk/python."""
import sys
from pathlib import Path

from botstrikes import Action, Agent, run
from botstrikes.cli import SDKError, diagnostic, message, parser_for, valid_duration


class FirstBot:
    def __init__(self):
        self.observations = 0

    def act(self, observation):
        action = Action()
        if observation.joined:
            self.observations += 1
        if observation.joined and observation.alive:
            action.forward = 1.0
            action.yaw = observation.yaw + 1.0
        return action


def main(argv=None):
    parser, language, argv = parser_for(argv, 'first_bot')
    parser.add_argument('--token', required=True, type=Path, help=message(language, 'token'))
    parser.add_argument('--map', required=True, type=Path, help=message(language, 'map'))
    parser.add_argument('--seconds', type=float, default=10.0, help=message(language, 'seconds'))
    args = parser.parse_args(argv)
    try:
        valid_duration(args.seconds)
        token = args.token.read_bytes()
        bot = FirstBot()
        with Agent(map_path=args.map) as agent:
            agent.connect(token)
            run([(agent, bot)], seconds=args.seconds)
        print(message(language, 'joined', count=bot.observations))
        if not bot.observations:
            raise SDKError('no_join')
        return 0
    except (OSError, ValueError, RuntimeError) as error:
        print(diagnostic(language, error), file=sys.stderr)
        return 1


if __name__ == '__main__':
    raise SystemExit(main())

Observation

The server authorizes information and the SDK presents the observation. Own position, velocity and view include prediction; enemies and results come from authorized state. Counts delimit valid arrays. Rig compatibility fields are infrastructure and are not part of this perception list.

FieldsMeaning
tickNewest server tick received.
joined, alive, grounded, crouched, reloadingFlags for joining the match, being alive, ground contact, crouching and reloading.
seatOwn seat in the match.
position, velocityPredicted own position and velocity; metres and metres per second.
yaw, pitchOwn view in degrees, including motor-model clipping and prediction of in-flight commands; positive pitch looks down.
health, armorOwn health and armor.
weapon, ammo, reserveSelected weapon, magazine ammunition and reserve. weapon: 1 rifle, 2 pistol, 3 knife.
kills, deaths, respawn_ticksKills, deaths and ticks until respawn. There is no respawn during a live round.
moneyOwn money; accounting is implemented, purchasing is pending. Does not reveal another seat’s money.
visible_count, visibleCount and list of players the server authorizes the agent to perceive. Hidden enemies are not included. Python: players().
event_count, eventsCount and list of results since the previous observation; consumed once. Python: results().
tracer_count, tracersCount and list of perceived tracers since the previous observation. Python: streaks().
team, appearance, friendly_fire, animatedTeam, appearance, friendly fire and animation. Appearance does not identify whether the player is human or AI.
supply_position, supply_radiusCenter and radius of the agent’s team resupply zone; zero in free-for-all.
round_phase0 no rounds, 1 warmup, 2 freeze, 3 live, 4 round over, 5 match ended.
round_number, rounds_to_win, round_ticks_leftRound number, rounds needed to win and ticks remaining in the current phase.
score_own, score_other, alive_own, alive_other, attackingScores and living players for both teams, own team first; attacking indicates that the agent’s team attacks this half.
spectating, spectate_position, spectate_yaw, spectate_pitchSpectated teammate seat or −1, camera position and view. While dead, fire requests another living teammate; never an enemy.
teammate_count, teammatesLiving teammates outside view: seat and position, never enemies. Empty while dead. Python: mates().
bomb_phase0 no objective, 1 carried, 2 dropped, 3 planted, 4 defused, 5 exploded.
bomb_site, bomb_carrier, bomb_progress, bomb_fuse_ticks_left, bomb_positionSite, carrier, own progress, fuse and authorized position. Only the carrier’s team receives the carrier; position is never sent while carried. A planted bomb is public.
bomb_site_count, bomb_sitesCount and public planting-site boxes. Python: sites(); each box has contains() and centre().
roster_count, rosterPublic roster in seat order; active identifies occupied seats. Python: scoreboard().
gunshot_count, gunshotsShots heard since the previous observation; they do not identify the shooter. Python: shots_heard().
chat_count, chatChat messages since the previous observation; sender, scope and text authorized by the server. Python: messages().

Actions

An action holds keys and requests a view and weapon. It cannot choose position, health, damage or enemies. The server decides movement, combat and perception. Chat is sent with agent.say(text), outside Action.

FieldsMeaning
forward, rightMovement axes: negative, zero or positive.
yaw, pitchRequested view in degrees; positive pitch looks down. The server applies limits.
jump, crouch, walk, fire, reloadHeld keys for jumping, crouching, walking, firing and reloading.
useHold to plant or defuse when the rules allow it.
weapon0 keeps the weapon; 1 rifle, 2 pistol, 3 knife.

Fairness and communication

Where the human motor model applies, the server enforces a minimum perception delay of 150 ms, maximum angular speed of 1800 degrees per second, maximum acceleration of 30000 degrees per second squared and up to 15 changes per key per second. Values are provisional and H5 is pending. Fitts aiming error is not implemented; sandboxing and deadlines remain pending. These limits do not demonstrate human equivalence.

Chat allows a burst of 3 messages followed by 1 per second, up to 120 UTF-8 bytes per message. During a live round, it reaches only the same team and players sharing the same alive or dead state. The server sets sender and audience. AIs can read and write; player text input is missing. In-game voice is not implemented; external voice channels do not automatically reach the AI.

Network command

UserCmd occupies 14 bytes and retains its format in the current protocol. This example demonstrates encoding; it does not implement the transport or a snapshot decoder by itself. Use the current SDK to play. Historical v2 snapshot tables were removed from this guide because they did not describe the current server.

import math
import struct

def f32(value):
    return struct.unpack("<f", struct.pack("<f", value))[0]

def lround(value):
    magnitude = abs(value)
    integer = math.floor(magnitude)
    if magnitude - integer >= 0.5:
        integer += 1
    return int(math.copysign(integer, value))

def user_cmd(sequence, buttons, yaw, pitch, weapon, view_tick, view_fraction=0):
    if buttons & ~0x03ff or not 0 <= weapon <= 3 or not 0 <= view_fraction <= 255:
        raise ValueError("invalid command fields")
    yaw, pitch = f32(yaw), f32(pitch)
    turn = math.remainder(yaw, 360.0) / 360.0 if math.isfinite(yaw) else 0.0
    if turn < 0:
        turn += 1
    yaw_bits = lround(turn * 65536.0) & 0xffff
    pitch_bits = lround(max(-89.0, min(89.0, pitch)) * 256.0) if math.isfinite(pitch) else 0
    return struct.pack("<IHHhBBH", sequence, buttons, yaw_bits,
                       pitch_bits, weapon, view_fraction, view_tick & 0xffff)

cmd = user_cmd(1, (1 << 0) | (1 << 7), 90.0, 0.0, 1, 4091)
assert cmd.hex(" ") == "01 00 00 00 81 00 00 40 00 00 01 00 fb 0f"

Versions and limits

The protocol version is part of the transport identity: update client, server and SDK together and issue new invitations when it changes. The agent API has its own version. The graphical client diagnoses incompatible or expired invitations; the SDK may report a timeout. Token lifetime is configurable. Reconnection, purchasing, voice, sandboxing and human validation do not become available features merely by appearing in a plan.

Complete protocol 15 reference

Review: .

Transport and conventions

Complete current payload reference, consolidated from code; no reconstruction of v1…v14 is needed. The repository and SDK remain private: documenting the protocol establishes neither a public download nor a public server. Earlier documents preserve change history.

Simulation and snapshots at 64 Hz. Encrypted netcode transport: transport_protocol_id = 0x4253545200000000 | 15; 2048-byte connection token; maximum payload 1200 bytes. The transport envelope and handshake belong to netcode, not the tables below. An incompatible version cannot complete connection: rebuild client, server and SDK and issue new tokens.

All integers are little-endian; signed integers use two’s complement; f is IEEE 754 binary32. Table notation: B=u8, b=i8, H=u16, h=i16, I=u32, Q=u64, f=f32; a numeric prefix repeats the type. No padding or memory-struct copies. Offsets start at zero within each block. Decoders reject truncation and trailing bytes.

Commands and acknowledgement

A packet is CommandHeader followed by count UserCmd records: 6 + 14×count bytes. The client repeats its three newest commands (48 bytes); the format allows up to four. Sequences must be consecutive, including wrapping from 0xffffffff to 0. The server applies at most one command per tick; a duplicate creates no new presses. Initial input lead is 2 ticks.

The modulo-2^32 consecutiveness above describes the packet decoder, not permission to restart the server sequence. CommandQueue rejects sequence=0 and a first command above 0x7fffffff; its stream does not wrap. It keeps a window of 8 future commands, not 8 commands per tick. It discards duplicates and excessively early input; after exhausting a window without usable input it can resynchronize without replaying skipped commands.

Quantization takes f32 angles: yaw is reduced modulo 360 and rounded to 65536 units per turn; pitch is clamped to ±89° and multiplied by 256. Ties round away from zero; a nonfinite view becomes zero. The server expands view_tick near its current tick and bounds lag compensation; the field cannot choose positions, damage or results.

CommandHeader · Command header · 6 B

OffsetFieldFormatBytesMeaning
0typeB1Always 1.
1countB11…4 consecutive commands, oldest to newest.
2snapshot_ackI4Last received snapshot tick.

UserCmd · Held input · 14 B

OffsetFieldFormatBytesMeaning
0sequenceI4Command sequence, modulo 2^32.
4buttonsH2Bits 0…9: forward, back, left, right, jump, crouch, walk, fire, reload, use; all others zero.
6yawH265536 units per turn; positive turns right.
8pitchh21/256 degree; positive looks down; range −22784…22784 (±89°).
10weaponB10 keeps the weapon; 1 rifle, 2 pistol, 3 knife.
11view_fractionB1Fraction after view_tick, in 1/256 tick. Not a reserved byte.
12view_tickH2Low 16 bits of the tick rendered for other players.

Complete snapshot order

Required order: SnapshotHeader → PlayerState → MotorState if flags bit 5 → Rig if identity bit 3 → Supply if team differs from 0 → Spectate if identity bit 5 → count u8 + RemotePlayer[count] → count u8 + events[count] → count u8 + Tracer[count] → count u8 + Gunshot[count] → Round → Bomb and its boxes if sides bit 4 → RosterEntry[16] → count u8 + Teammate[count] if self_and_mates bit 7. The four empty counters and roster are never omitted.

Minimum: 2 + 74 + 4 + 10 + 48 = 138 bytes. Format maxima: 16 seats, 15 visible remotes, 8 events, 8 tracers, 12 heard shots, 2 sites and 15 hidden teammates. The game server serves at most 10 seats. Visible and hidden teammates are disjoint sets without duplicates or the own seat.

SnapshotHeader · Snapshot header · 2 B

OffsetFieldFormatBytesMeaning
0typeB1Always 2.
1self_and_matesB1Bits 0…3: own seat; bit 7: hidden teammate block present; bits 4…6: zero.

Own state and extensions

Own, supply and spectator positions must be finite and within ±16384 m per axis; velocity within ±1000 m/s. Health and armor: finite 0…1000; alive equals health > 0. A living player has respawn=0. Recoil is finite and within ±90°. Valid teams are 0, 1 and 2.

For weapon 3, weapon_timer is melee_remaining, not reload: it must be ≤ knife_cooldown, and that cooldown ≤128. Other weapons have no pending knife impact. Motor retains button and pitch restrictions and the finite turn limits in the table. Rig is present only if animated and its signature cannot be zero.

Spectate is allowed only for a dead player, with a seat different from its own. View and perception come from the authorized teammate; private events still belong to the snapshot owner. Without the block, spectator state is empty.

PlayerState · Fixed own state · 74 B

OffsetFieldFormatBytesMeaning
0tickI4Server tick.
4last_commandI4Last sequence applied by the server.
8position3f12x, y, z in metres.
20velocity3f12x, y, z in metres per second.
32flagsB1Bits 0…5: grounded, crouched, jump_held, alive, trigger_held, motor. Bits 6…7: team (0 none, 1 alpha, 2 bravo).
33identityB1Bits 0…1: weapon 1…3; bit 2: appearance (0 human, 1 machine); bit 3: animated; bit 4: friendly_fire; bit 5: spectating; bits 6…7: zero.
34healthf4Own health, 0…1000.
38armorf4Own armor, 0…1000.
42rifle_ammoH2Rifle magazine ammunition.
44rifle_reserveH2Rifle reserve ammunition.
46rifle_cooldownB1Ticks until the rifle can fire.
47pistol_ammoH2Pistol magazine ammunition.
49pistol_reserveH2Pistol reserve ammunition.
51pistol_cooldownB1Ticks until the pistol can fire.
52knife_cooldownB1Ticks until another knife attack; no knife ammunition is sent.
53idleB1Weapon idle counter, saturated at 255.
54patternB1Active weapon pattern index, saturated at 255.
55weapon_timerH2Reload ticks; for the knife, ticks until the pending impact.
57recoil_pitchf4Vertical recoil in degrees, ±90.
61recoil_yawf4Horizontal recoil in degrees, ±90.
65respawnH2Ticks until respawn; zero while alive.
67killsH2Own kills.
69deathsH2Own deaths.
71moneyH2Own money; purchasing is not implemented.
73leadB1This player’s queued commands ahead of the tick; saturated.

MotorState · Motor extension · 24 B

OffsetFieldFormatBytesMeaning
0applied_buttonsH2Bits 0…9: forward, back, left, right, jump, crouch, walk, fire, reload, use; all others zero.
2applied_yawH265536 units per turn; positive turns right.
4applied_pitchh21/256 degree; positive looks down; range −22784…22784 (±89°).
6turn_yawf4Previous horizontal turn, degrees per tick, ±180.
10turn_pitchf4Previous vertical turn, degrees per tick, ±178.
14keys10B10Change credit for each button, in 1/64 of a change.

Rig · Rig signature · 8 B

OffsetFieldFormatBytesMeaning
0rig_hashQ8Nonzero signature; low word first. Does not authenticate content.

Supply · Resupply zone · 16 B

OffsetFieldFormatBytesMeaning
0position3f12x, y, z in metres.
12radiusf4Radius in metres, 0…16384.

Spectate · Spectated teammate view · 17 B

OffsetFieldFormatBytesMeaning
0seatB1Seat 0–15; not a persistent identifier.
1eye3f12x, y, z in metres.
13yawH265536 units per turn; positive turns right.
15pitchh21/256 degree; positive looks down; range −22784…22784 (±89°).

Visible bodies and animation

motion: 0 idle, 1 walk, 2 jog, 3 sprint, 4 crouch_idle, 5 crouch_walk, 6 airborne, 7 shoot, 8 reload, 9 death, 10 jump_start, 11 jump_land; 12…15 invalid. Position is finite within ±16384 m per axis. The server decides which bodies can be perceived; absence reveals no hidden enemy.

Blend source: codes 0…6 map to motion 0…6; code 7 means jump_start (motion 10). Step 0: no blend and source must be 0. Steps 1…3: weights 1/4, 2/4, 3/4. Steps 4…7: weights 1/8, 3/8, 5/8, 7/8; only pistol or knife moving from walk/jog/sprint to idle. Every blend requires distinct source and target, both crouched (crouch_idle/crouch_walk) or both not crouched.

reload_phase cannot be nonzero for a knife or motion=death. attack_age cannot be nonzero with death, shoot, reload or an active reload_phase. These are decoding restrictions; the encoder is not a substitute for validating arbitrary inputs.

Protocol 15 keeps motion_phase as the target clip phase and adds stop_phase as the explicit source clip phase during secondary braking. The server freezes the last public moving phase; on a simultaneous weapon change without a compatible prior pose, it computes the source from travelled distance under the new weapon. The nearer snapshot supplies both blend and source byte; client visibility history is not required. The idle target retains its normal clock. Earlier-protocol tokens/clients are incompatible. Pose revision 13; Agent API 14 has unchanged fields and ABI.

RemotePlayer · Visible player · 22 B

OffsetFieldFormatBytesMeaning
0idB1Seat 0–15; not a persistent identifier.
1position3f12x, y, z in metres.
13yawH265536 units per turn; positive turns right.
15pitchb1Whole degrees, −89…89; differs from UserCmd pitch.
16flagsB1Bit 0: crouched; bits 1…4: motion; bits 5…6: team 0…2; bit 7: appearance 0…1.
17motion_phaseB1Clip fraction, in 1/256 steps.
18poseB1Bits 0…1: weapon 0…2; bits 2…4: blend source; bits 5…7: blend step.
19reload_phaseB10 inactive; 1…255 normalized progress.
20attack_ageB10 inactive; otherwise 1 + age in ticks.
21stop_phaseB1Frozen secondary braking source phase, 0–255 in 1/256 of the source clip. Active only with pistol/knife, idle, walk/jog/sprint source and fade_step 1–7; must be zero outside this context. Zero is a valid phase within it.

Events, tracers and heard shots

Each event is EventHeader + payload: shot 19 B, hurt 6 B, death 4 B, kill 6 B total. Types 1…3 are private; kill is public, without position. Events repeat oldest to newest until snapshot_ack acknowledges a snapshot containing them; process each id only once. Their ids are consecutive modulo 65536.

zone: 0 head, 1 torso, 2 limb. Zero for a miss or a world death is a canonical empty value, not a head hit.

In shot, other different from 255 requires another valid seat and unseen=0. With other=255, damage and zone are zero; killed can be 1 only if unseen=1. A hidden hit retains an authorized cover entry position, not the enemy position. In kill, victim is 0…15; attacker differs from victim or is 255. For a world kill, zone, through and weapon are zero.

Tracer contains the segment selected by the server filter, with finite endpoints within ±16384 m, without shooter identity. Gunshot contains the authorized rounded position and weapon, without identity. Its coordinates are encoded by dividing by 0.25 m, rounding ties away from zero and clamping to ±32767; −32768 is rejected on read. Tracers and heard shots are ephemeral, without event acknowledgement.

Tracers are clipped using samples; continuous visibility of the whole segment is not guaranteed.

EventHeader · Event header · 3 B

OffsetFieldFormatBytesMeaning
0idH2Consecutive per-player sequence, modulo 65536.
2kindB11 shot, 2 hurt, 3 death, 4 kill.

Shot · Shot payload · 16 B

OffsetFieldFormatBytesMeaning
0flagsB1Bit 0: through; bit 1: killed; bits 2…3: zone 0…2; bit 4: unseen; bit 5: zero; bits 6…7: weapon 0…2.
1otherB1Hit seat or 255; never own seat.
2damageH2Damage in hundredths of a point.
4position3f12Authorized impact; for a hidden hit, the cover entry point, not the enemy position.

Hurt · Own damage payload · 3 B

OffsetFieldFormatBytesMeaning
0zoneB10…2; contains no attacker or position.
1damageH2Damage in hundredths of a point.

Death · Own death payload · 1 B

OffsetFieldFormatBytesMeaning
0otherB1Attacker seat or 255 for the world; never own seat.

Kill · Public kill payload · 3 B

OffsetFieldFormatBytesMeaning
0killerB1Attacker seat or 255 for the world.
1victimB1Seat 0–15; not a persistent identifier.
2flagsB1Bits 0…1: zone 0…2; bit 2: through; bits 3…7: weapon 0…2.

Tracer · Visible segment · 24 B

OffsetFieldFormatBytesMeaning
0from3f12x, y, z in metres.
12to3f12x, y, z in metres.

Gunshot · Heard shot · 7 B

OffsetFieldFormatBytesMeaning
0position3h6x, y, z in units of 0.25 m; −32768 invalid.
6weaponB10 rifle, 1 pistol, 2 knife; 3 invalid.

Round, bomb, roster and teammates

With phase=none, Round must be entirely zero. In other phases rounds_to_win is positive, scores do not exceed it and attackers is 1 or 2. number is zero exactly in warmup. winner is nonzero exactly in over or ended.

Bomb is sent only if sides bit 4 is set: 19 bytes plus 13 per site. site is nonzero exactly in planted, defused or exploded; fuse_left can be nonzero only in planted. carrier can name a seat only in carried; otherwise it is 255. In carried the position is zero; in other states it must be finite. Boxes are ordered A, B; min ≤ max per axis and decoded coordinates within ±3276 m. Box encoding multiplies each f32 by 10 and rounds ties away from zero.

The server decides each bomb field’s audience: planted state, site, fuse, planted position and boxes are public; carrier and dropped position are disclosed only to the attacking team; progress is personal. A hidden player’s position is not sent through the bomb.

The roster always contains 16 rows, 48 bytes, in seat order. An inactive seat has an all-zero row. Role comes from the authenticated token; team and appearance cannot change it. The optional teammate block has count=1…15 and Teammate rows; an announced empty block is invalid. Only a living player with a team receives it. Each row must match an active living teammate of the same team in the roster, with finite position within ±16384 m, without duplicates or already visible players. It contains no health, weapon or aim.

Round · Round state · 10 B

OffsetFieldFormatBytesMeaning
0phaseB10 none, 1 warmup, 2 freeze, 3 live, 4 over, 5 ended.
1numberB10 during warmup; rounds start at 1.
2rounds_to_winB1Rounds required to win.
3score_alphaB1Rounds won by alpha.
4score_bravoB1Rounds won by bravo.
5alive_alphaB1Living alpha players.
6alive_bravoB1Living bravo players.
7ticks_leftH2Ticks remaining in the phase.
9sidesB1Bits 0…1: attackers; bits 2…3: winner (teams 0…2); bit 4: bomb present; bits 5…7: zero.

Bomb · Fixed bomb state · 19 B

OffsetFieldFormatBytesMeaning
0phaseB11 carried, 2 dropped, 3 planted, 4 defused, 5 exploded; 0 means no block.
1siteB10 not planted, 1 A, 2 B.
2carrierB1Authorized carrier seat or 255.
3progressB1Own progress, 0…100.
4fuse_leftH2Fuse ticks; zero except while planted.
6position3f12x, y, z in metres.
18site_countB10…2 site boxes follow.

BombSite · Planting box · 13 B

OffsetFieldFormatBytesMeaning
0letterB11 for the first box (A), 2 for the second (B).
1min3h6Minimum x, y, z in decimetres.
7max3h6Maximum x, y, z in decimetres.

RosterEntry · Roster row · 3 B

OffsetFieldFormatBytesMeaning
0flagsB1Bit 0: active; bit 1: alive; bits 2…3: team 0…2; bit 4: appearance 0…1; bit 5: role 0 person, 1 AI; bits 6…7: zero.
1killsB1Kills saturated at 255.
2deathsB1Deaths saturated at 255.

Teammate · Living unseen teammate · 13 B

OffsetFieldFormatBytesMeaning
0idB1Seat 0–15; not a persistent identifier.
1position3f12x, y, z in metres.

Separate chat

ChatSayHeader + text travels client to server. ChatHeardHeader + text travels server to client: the from byte is immediately after type, before scope. The client cannot choose the sender. Text is 1…120 bytes, not characters, without a zero terminator. ASCII control bytes 0x00…0x1f and 0x7f, messages consisting only of ASCII spaces and invalid UTF-8 (including overlong encodings, surrogates and values above U+10FFFF) are rejected. This does not claim filtering of all Unicode controls.

Per-seat rate: burst of 3 messages, then 1 per second. In a live round, only the same team and same alive/dead state, even when scope=1 is requested. Outside live, scope=0 goes to the team and scope=1 to everyone. Without rounds the scope filter still applies: scope=0 includes teammates; in free-for-all, without a team, only the sender. scope=1 includes all active seats. The sender is always included in the audience; the echo does not guarantee delivery. The server drops and counts excess; these messages do not have the event retransmission/acknowledgement mechanism.

Agent API 14 exposes chat_count and chat[4] with from, scope and 121-byte zero-terminated text; bs_observation is 2964 bytes and bs_action 44 in the checked ABI. There are 61 observation and 11 action fields; the in-memory ABI is not this wire format. C: bs_agent_say and bs_gym_say. Python: observation.messages() and agent.say(text). AIs read and write; human text input and in-game voice are missing. External voice does not automatically reach the AI; the parity decision remains open.

ChatSayHeader · Chat client → server · 3 B

OffsetFieldFormatBytesMeaning
0typeB1Always 3.
1scopeB10 team, 1 all; the server restricts the audience.
2lengthB11…120 text bytes follow, without a terminator.

ChatHeardHeader · Chat server → client · 4 B

OffsetFieldFormatBytesMeaning
0typeB1Always 3.
1fromB1Seat 0–15; not a persistent identifier.
2scopeB10 team, 1 all.
3lengthB11…120 text bytes follow, without a terminator.

Budgets and scope limits

Served 5v5 analytical worst case: 857-byte snapshot with objective and two sites, living AI with motor, team resupply and rig. With 73 header bytes and 64 Hz: (857+73)×64=59520 B/s; margin 4480 against a 64000 cap. Without objective: 812 bytes, 56640 B/s. Usual upload with 3 commands: (48+73)×64=7744 B/s against 8000. These calculations are not a new traffic measurement.

Maximum chat: 4+120+73=197 bytes per received message; five teammates at the sustained limit add 985 B/s, with a separate budget. Bursts and traffic received outside a 5v5 team are not described by that sustained rate. The ten-served-seat limit remains; the format’s sixteen seats do not establish support for larger matches.

Pending: reconnection, delta compression and content hash in the handshake; bot sandbox and deadline; human fairness validation H5. The role authenticated in the token identifies whom the token was issued for, not proof that a person is playing. The server limits perception and inputs, but human equivalence has not been demonstrated. The examples below verify payload bytes; they are neither a transport implementation nor a human playtest.

Byte examples

command · 14 B

Sequence 0x01020304, forward+fire, yaw 90°, pitch −1.5°, rifle, half a tick after 4091.

04 03 02 01 81 00 00 40 80 fe 01 80 fb 0f

commands · 20 B

Packet containing one command and acknowledging tick 4090.

01 01 fa 0f 00 00 04 03 02 01 81 00 00 40 80 fe 01 80 fb 0f

snapshot_minimal · 138 B

Snapshot without extensions or lists, with distinguishable own-state values and an inactive roster. Checks offsets; does not represent a complete match.

02 00 04 03 02 01 11 00 00 00 00 00 80 3f 00 00 00 40 00 00 40 40 00 00
80 40 00 00 a0 40 00 00 c0 40 08 01 00 00 c8 42 00 00 c8 41 1e 00 5a 00
07 0c 00 18 00 09 0b 0d 0f 13 00 00 00 c0 bf 00 00 20 40 00 00 15 00 17
00 20 03 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

snapshot_spectator · 171 B

Dead state with a team and the view of teammate 1; no lists.

02 00 04 03 02 01 11 00 00 00 00 00 80 3f 00 00 00 40 00 00 40 40 00 00
80 40 00 00 a0 40 00 00 c0 40 40 21 00 00 00 00 00 00 c8 41 1e 00 5a 00
07 0c 00 18 00 09 0b 0d 0f 13 00 00 00 c0 bf 00 00 20 40 07 00 15 00 17
00 20 03 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 40
41 00 00 40 40 00 00 80 40 00 80 00 02 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00

snapshot_extended · 333 B

Synthetic vector with motor, rig, supply, one remote, all four event types, tracer, heard shot, bomb with two boxes, roster and one hidden teammate. Tests block order, not maximum bandwidth or a played session.

02 80 04 03 02 01 11 00 00 00 00 00 80 3f 00 00 00 40 00 00 40 40 00 00
80 40 00 00 a0 40 00 00 c0 40 79 1d 00 00 c8 42 00 00 c8 41 1e 00 5a 00
07 0c 00 18 00 09 0b 0d 0f 13 00 00 00 c0 bf 00 00 20 40 00 00 15 00 17
00 20 03 02 81 00 00 40 80 fd 00 00 a0 3f 00 00 20 c0 0a 14 1e 28 32 3c
46 50 5a 64 08 07 06 05 04 03 02 01 00 00 80 3f 00 00 00 40 00 00 40 40
00 00 80 40 01 01 00 00 e0 40 00 00 00 41 00 00 10 41 00 80 e2 4b 80 31
21 00 00 04 01 00 01 49 01 d2 04 00 00 80 3f 00 00 00 40 00 00 40 40 02
00 02 01 ae 08 03 00 03 01 04 00 04 01 00 15 01 00 00 80 3f 00 00 00 40
00 00 40 40 00 00 80 40 00 00 a0 40 00 00 c0 40 01 05 00 f6 ff 0f 00 02
03 02 0d 01 00 05 04 d2 04 11 03 02 ff 25 ae 08 00 00 20 41 00 00 00 00
00 00 a0 41 02 01 0a 00 00 00 14 00 1e 00 28 00 32 00 02 3c 00 00 00 46
00 50 00 28 00 5a 00 37 15 17 0b 02 03 07 04 05 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 01 02 00 00 20 41 00 00 30 41 00 00 40 41

chat_say · 5 B

Team request: UTF-8 text “ñ”, two bytes.

03 00 02 c3 b1

chat_heard · 6 B

The same line received from seat 3.

03 03 00 02 c3 b1

snapshot_secondary_stop · 333 B

The same extended vector with pistol braking: idle target, walk source, first 1/8 weight and source phase 66/256. Zero in other contexts does not replace this active example.

02 80 04 03 02 01 11 00 00 00 00 00 80 3f 00 00 00 40 00 00 40 40 00 00
80 40 00 00 a0 40 00 00 c0 40 79 1d 00 00 c8 42 00 00 c8 41 1e 00 5a 00
07 0c 00 18 00 09 0b 0d 0f 13 00 00 00 c0 bf 00 00 20 40 00 00 15 00 17
00 20 03 02 81 00 00 40 80 fd 00 00 a0 3f 00 00 20 c0 0a 14 1e 28 32 3c
46 50 5a 64 08 07 06 05 04 03 02 01 00 00 80 3f 00 00 00 40 00 00 40 40
00 00 80 40 01 01 00 00 e0 40 00 00 00 41 00 00 10 41 00 80 e2 40 50 85
00 00 42 04 01 00 01 49 01 d2 04 00 00 80 3f 00 00 00 40 00 00 40 40 02
00 02 01 ae 08 03 00 03 01 04 00 04 01 00 15 01 00 00 80 3f 00 00 00 40
00 00 40 40 00 00 80 40 00 00 a0 40 00 00 c0 40 01 05 00 f6 ff 0f 00 02
03 02 0d 01 00 05 04 d2 04 11 03 02 ff 25 ae 08 00 00 20 41 00 00 00 00
00 00 a0 41 02 01 0a 00 00 00 14 00 1e 00 28 00 32 00 02 3c 00 00 00 46
00 50 00 28 00 5a 00 37 15 17 0b 02 03 07 04 05 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 01 02 00 00 20 41 00 00 30 41 00 00 40 41