1
0
Fork 0
polaris-task-force/docs/arma-bridge/crate-sync.md

7.5 KiB

Crate Sync Contract

Binding contract for supply-crate (box) sync between the Arma 3 bridge mod and the web app. Todos 5 (web) and 6 (mod) implement exactly what is written here; do not redesign it while implementing.

Overview

Supply crates spawned in-game need to appear on the web, and web-side crate definitions need to spawn in-game. This document fixes the two channels that carry that data:

  • Events (mod to web): the box.* event family, delivered through the existing flush_events ingestion (POST /api/arma/v1/events).
  • Commands (web to mod): the crate.* command types, delivered through the existing pull_commands / ack_command loop.

Crates are deliberately OUT of the operation ledger: operation-events validation rejects array payloads for operation.* types, and crates are not operations. The web events route accepts any type string, so the box.* family flows through existing ingestion unchanged, array payloads included.

The bridge extension DLL is closed: only heartbeat, flush_events, pull_commands, ack_command, and fetch_tickets channels exist. Crate sync adds no new extension channels; it rides the event flush and the command poll.

Event family

All payloads are flat arrays. fn_queueEvent.sqf stores queue entries as [_id, _type, _payload, serverTime] and defaults _payload to [], so every event below is an array of positional values.

Type Payload Meaning
box.register [boxId, boxClass, displayName, locationX, locationY, locationZ] A tracked box object exists at a position.
box.sync [boxId, [[itemClass, count], ...]] Full cargo snapshot for one box.
box.deleted [boxId] The box object no longer exists.

box.sync cargo rules:

  • TOP-LEVEL cargo only: itemCargo, magazineCargo, weaponCargo, and backpackCargo merged by class into [itemClass, count] pairs.
  • Nested container contents are a deferred v1 extension. There is NO containerIndex field and no per-container breakdown; anything inside a nested container is invisible to this sync.

Box identity

  • Box id format: box:<sessionId>:<seq>.
  • sessionId is minted in fn_serverInit.sqf as format ["%1-%2", worldName, systemTime joinString ""], unique per mission load. The event sequence and queue are reset in the same place, so seq is a missionNamespace counter that starts at 0 each mission load.
  • The mod NEVER embeds a server identifier in the box id. No server identifier exists mod-side. The WEB resolves the owning server authoritatively from the authenticated ingestion connection, the same pattern as eventId = ${server.serverId}:${event.id} in src/app/api/arma/v1/events/route.ts, which stores server: server.id (the game-servers row) on every raw event.
  • A mission restart mints a new generation (new sessionId). Rows from older sessionIds remain in the database as stale snapshots; they are not deleted. The UI filters by the newest generation per server.

Sync cadence

  • Boxes are scanned and synced every 2nd bridge cycle, roughly every 30 seconds (the bridge cycle loop in fn_serverInit.sqf sleeps 15 seconds).
  • Each sync is a FULL snapshot per box: box.sync replaces the box's cargo wholesale, it is never a delta.
  • Dedup is server-side by eventId (the events route does a batched existence lookup and also survives unique-constraint races), so a retried flush cannot double-apply.
  • Per-cycle candidate cap: 64 boxes per scan cycle. The cap bounds event volume; boxes beyond the cap wait for the next cycle.

Web-side consumption rules

  • Upsert by boxId: create or update the SupplyBoxes row, set lastSyncedAt = now.
  • box.deleted RETAINS the row: it marks the row status: deleted and keeps both the row and the raw arma-sync-events evidence for provenance. It never hard-deletes. A later box.register or box.sync for the same boxId reactivates the row.
  • Null-server guard: if the authenticated ingestion context yields a null server (raw.server null), the event is malformed and is a NO-OP with a logged warning. The SupplyBoxes.server relationship is REQUIRED and never receives null; no SupplyBoxes write happens on the malformed path.
  • Provenance: every SupplyBoxes row links to the raw arma-sync-events rows that produced it (by eventId), so any web-side state can be traced back to the exact mod event.

Command envelope and schemas

  • Envelope: [commandId, commandType, payload]. This is exactly what fn_pollCommands passes and what fn_executeCommand.sqf destructures: _command params ["_id", "_type", ["_payload", []]]. Commands are stored in the arma-commands collection (commandId unique, server required relationship, payload JSON, status queued/delivered/succeeded/failed).
  • Crate arguments live INSIDE payload, also as arrays:
Type Payload arguments Effect
crate.spawn [boxId, boxClass, x, y, z] Create the box object at the position and register it.
crate.apply [boxId, [[itemClass, count], ...]] Replace the box's top-level cargo with the given items.
crate.despawn [boxId] Delete the box object and remove it from the registry.
  • Expected ack results: every command is acked exactly once via ptf_bridge.ack_command with [commandId, success, result, error] (the shape fn_executeCommand.sqf already uses). Successful acks carry success: true and an empty result array. Failed acks carry success: false and a human-readable string in the error slot. The web side records the outcome through acknowledgeCommand (src/lib/arma-bridge/commands.ts), which sets status: succeeded or status: failed with the result and error.

Failure rules

  • Unknown boxId on crate.apply: the mod acks the command as a FAILURE with an error string; the command is marked failed web-side. It is not retried by the mod.
  • Partial sync never blocks other boxes: a failure while processing one box's register/sync/deleted event does not abort the remaining boxes. Events are independent rows; one bad box costs only its own update.
  • Duplicate eventIds are deduped server-side (counted as duplicates, no double-apply), so mod-side retry of a flush is safe.
  • Ack guards (existing behavior in acknowledgeCommand): duplicate acks are no-ops (the first result wins), acking a command that was never delivered is rejected (not-delivered), and a delivered command whose 5-minute lease expires without an ack is re-queued by recoverExpiredCommands.

Security

  • All channels sit behind requireArmaServer (ARMA_BRIDGE_API_KEY plus the x-arma-server-id header). This covers event ingestion and command pull/ack alike.
  • The owning server is always derived from the authenticated connection, never from payload content. Box ids and command payloads carry no server identity and are never trusted to do so.

Mod-side behavior

  • Registry: a missionNamespace variable ptf_boxRegistry holding [boxId, objectHandle] pairs for every tracked box.
  • Deletion detection: ONLY via isNull checks against registry entries. The mod never rescans the world to discover deletions. A registry entry whose handle has become null produces a box.deleted event and is removed from the registry.
  • Replacement objects: an object that replaces a deleted box (new handle) is a NEW boxId with a new sequence number. Box ids are never reused.
  • Container predicate: a locally duplicated fn_isBoxContainer.sqf decides which objects are tracked. It checks ReammoBox_F, WeaponHolder, and Transport* config checks. It has NO dependency on ptf_loot; the predicate is intentionally duplicated so the bridge addon stays independent.