A Go implementation of an ArmA 3 native extension that records gameplay to PostgreSQL (with SQLite fallback). 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)
↓
├─ [Buffered] → Channel → Goroutine → Handler
└─ [Sync] → Handler directly
↓
Handler (parse args, validate, create model)
↓
├─ Storage Backend (if configured)
└─ Queue (traditional) → batch insert every 2s
↓
DB Writer Loop
↓
PostgreSQL / SQLite
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
├── worker/ Handler registration and DB writer loop
├── handlers/ Business logic for parsing and validation
├── queue/ Thread-safe queues for batch writes
├── cache/ Entity lookup caching (ObjectID → model)
├── model/ GORM database models
├── storage/ Optional alternative storage backend
└── 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
- Graceful fallback: PostgreSQL → SQLite if connection fails
- 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 build -t indifox926/build-a3go:linux-so -f ./Dockerfile .
docker run --rm -v ${PWD}:/app -e GOOS=linux -e GOARCH=amd64 -e CGO_ENABLED=1 -e CC=gcc \
indifox926/build-a3go:linux-so go build -o dist/ocap_recorder_x64.so -linkshared ./cmd/ocap_recorderCopy ocap_recorder.cfg.json.example to ocap_recorder.cfg.json alongside the DLL and edit as needed.
| Command | Buffer | Purpose |
|---|---|---|
:NEW:SOLDIER: |
Sync | Register new unit |
:NEW:VEHICLE: |
Sync | Register new vehicle |
:NEW:SOLDIER:STATE: |
10,000 | Update unit position/state |
:NEW:VEHICLE:STATE: |
10,000 | Update vehicle position/state |
| Command | Buffer | Purpose |
|---|---|---|
:FIRED: |
10,000 | Weapon fire event |
:PROJECTILE: |
5,000 | Projectile tracking |
:HIT: |
2,000 | Damage event |
:KILL: |
2,000 | Kill event |
| Command | Buffer | Purpose |
|---|---|---|
:EVENT: |
1,000 | General gameplay event |
:CHAT: |
1,000 | Chat message |
:RADIO: |
1,000 | Radio transmission |
:FPS: |
1,000 | Server FPS sample |
| Command | Buffer | Purpose |
|---|---|---|
:MARKER:CREATE: |
500 | Create map marker |
:MARKER:MOVE: |
1,000 | Update marker position |
:MARKER:DELETE: |
500 | Delete marker |
| Command | Buffer | Purpose |
|---|---|---|
:ACE3:DEATH: |
1,000 | ACE3 death event |
:ACE3:UNCONSCIOUS: |
1,000 | ACE3 unconscious event |
| Command | Purpose |
|---|---|
:INIT: |
Initialize extension |
:INIT:DB: |
Connect to database |
:NEW:MISSION: |
Start recording mission |
:SAVE: |
End recording, flush data |
:VERSION: |
Get extension version |