A Go implementation of an ArmA 3 native extension that records gameplay to PostgreSQL, SQLite, or in-memory JSON. Captures unit positions, combat events, markers, and more for mission replay and analytics.
ArmA 3 Game
↓ callExtension("ocap_recorder", [":COMMAND:", args])
RVExtensionArgs() [CGo boundary]
↓
Dispatcher.Dispatch(Event)
↓
├─ [Sync] → Handler directly
└─ [Buffered] → Channel → Goroutine → Handler
↓
Parser (args → core types)
↓
EntityCache (validate/enrich)
↓
Storage Backend
├─ Memory → in-memory append → JSON export on save
├─ Postgres → Queue → DB writer (batch insert every 2s) → PostgreSQL
└─ SQLite → Queue → DB writer (batch insert every 2s) → SQLite
Buffered handlers are gated on :STORAGE:INIT: — events queue in channels until the storage backend is ready.
The extension exposes CGo-exported functions that ArmA 3 calls:
| Function | Purpose |
|---|---|
RVExtensionArgs() |
Main entry point; receives command and arguments |
RVExtension() |
Legacy simple command handler |
RVExtensionVersion() |
Returns version string |
The dispatcher routes commands to handlers with optional buffering:
- Sync handlers: Execute immediately (entity creation)
- Buffered handlers: Queue events in channels for async processing (high-volume state updates)
- Metrics: OpenTelemetry integration for queue sizes, events processed/dropped
cmd/ocap_recorder/main.go Entry point, initialization, lifecycle commands
pkg/a3interface/ CGo exports (RVExtension*)
internal/
├── dispatcher/ Event routing with async buffering
├── parser/ Command parsing (args → core types)
├── worker/ Handler registration and DB writer loop
├── queue/ Thread-safe queues for batch writes
├── cache/ Entity lookup caching (ObjectID → model)
├── model/ Database models + converters
├── storage/ Storage backends (memory, postgres, sqlite)
└── geo/ Coordinate/geometry utilities
- Low latency: Async buffered handlers don't block ArmA's game loop
- High throughput: Batch writes every 2 seconds instead of per-event
- Entity caching: Sync entity creation → cache → async state updates use cached FK
- Pluggable storage: Memory (JSON export), PostgreSQL, or SQLite (in-memory with periodic disk dump)
- Observability: OpenTelemetry metrics and structured logging (slog)
Requires Docker with Linux containers.
docker pull x1unix/go-mingw:1.24
docker run --rm -v ${PWD}:/go/work -w /go/work x1unix/go-mingw:1.24 \
go build -buildvcs=false -o dist/ocap_recorder_x64.dll -buildmode=c-shared ./cmd/ocap_recorderdocker run --rm -v ${PWD}:/go/work -w /go/work golang:1.24-bullseye \
go build -buildvcs=false -o dist/ocap_recorder_x64.so -buildmode=c-shared ./cmd/ocap_recorderUses Debian Bullseye (glibc 2.31) for broad compatibility with Linux game servers.
Copy ocap_recorder.cfg.json.example to ocap_recorder.cfg.json alongside the DLL and edit as needed.
All commands follow a :RESOURCE:ACTION: naming convention. New commands must use this pattern — resource noun first, then verb/qualifier (e.g., :SOLDIER:CREATE:, :EVENT:KILL:, :SYS:INIT:).
| Command | Buffer | Purpose |
|---|---|---|
:SOLDIER:CREATE: |
Sync | Register new unit |
:SOLDIER:STATE: |
10,000 | Update unit position/state |
:SOLDIER:DELETE: |
500 | Mark unit as removed (disconnect, respawn corpse) |
:VEHICLE:CREATE: |
Sync | Register new vehicle |
:VEHICLE:STATE: |
10,000 | Update vehicle position/state |
:VEHICLE:DELETE: |
500 | Mark vehicle as removed |
| Command | Buffer | Purpose |
|---|---|---|
:EVENT:PROJECTILE: |
5,000 | Projectile tracking (positions + hits) |
:EVENT:KILL: |
2,000 | Kill event |
| Command | Buffer | Purpose |
|---|---|---|
:EVENT:GENERAL: |
1,000 | General gameplay event |
:EVENT:CHAT: |
1,000 | Chat message |
:EVENT:RADIO: |
1,000 | Radio transmission |
:TELEMETRY:FRAME: |
100 | Server telemetry (FPS, entity counts, weather, player network stats) |
:TIME:STATE: |
100 | Mission time/date tracking |
| Command | Buffer | Purpose |
|---|---|---|
:MARKER:CREATE: |
Sync | Create map marker (needs immediate DB ID) |
:MARKER:STATE: |
1,000 | Update marker position/appearance (pos, dir, alpha, text, color, size, type, brush, shape) |
:MARKER:DELETE: |
500 | Delete marker |
| Command | Buffer | Purpose |
|---|---|---|
:PLACED:CREATE: |
Sync | Register placed object (mine, explosive) |
:PLACED:EVENT: |
1,000 | Placed object lifecycle event (detonated/deleted) |
| Command | Buffer | Purpose |
|---|---|---|
:ACE3:DEATH: |
1,000 | ACE3 death event |
:ACE3:UNCONSCIOUS: |
1,000 | ACE3 unconscious event |
| Command | Purpose |
|---|---|
:SYS:INIT: |
Initialize extension, send :SYS:READY: callback |
:STORAGE:INIT: |
Initialize storage backend, ungate buffered handlers |
:MISSION:START: |
Start recording mission |
:MISSION:SAVE: |
End recording, flush data, upload if configured |
:SYS:VERSION: |
Get extension version |