From 34ab385a2baca3d887b71e053a5cf8c722be10c9 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Fri, 6 Mar 2026 19:28:03 +0100 Subject: [PATCH 1/4] docs: add script-side focus range design --- .../2026-03-06-script-side-focus-design.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/plans/2026-03-06-script-side-focus-design.md diff --git a/docs/plans/2026-03-06-script-side-focus-design.md b/docs/plans/2026-03-06-script-side-focus-design.md new file mode 100644 index 0000000..b9b0d11 --- /dev/null +++ b/docs/plans/2026-03-06-script-side-focus-design.md @@ -0,0 +1,60 @@ +# Script-Side Focus Range + +## Problem + +OCAP2 records entire missions including prep phases, briefings, and downtime. The web UI already supports a focus range (focusStart/focusEnd in frames) to highlight the interesting portion, but it can only be set after upload via the admin interface. Mission makers want to define this range from SQF during the mission. + +## Design + +### Addon (SQF) + +Two new CBA server events: + +- `OCAP_setFocusStart` — optional param `[frameNumber]`, defaults to current `GVAR(captureFrameNo)` +- `OCAP_setFocusEnd` — same pattern + +Each event sends a command to the extension immediately: +- `:MISSION:FOCUS_START:` +- `:MISSION:FOCUS_END:` + +No SQF-side storage — the extension is the source of truth. + +### Extension (Go) + +Two new commands stored on mission state. At save time (`MISSION:SAVE`), the extension resolves incomplete pairs: + +| focusStart | focusEnd | Result | +|------------|----------|--------| +| set | set | Pass both to web upload | +| set | not set | Fill end with total frame count | +| not set | set | Fill start with 0 | +| not set | not set | Pass nothing | + +The resolved pair is sent as `focusStart`/`focusEnd` form fields in the existing web upload request. + +### Web + +No changes. The upload endpoint already accepts and validates `focusStart`/`focusEnd`. + +## Data Flow + +``` +Mission script → Addon → Extension → Web + | | | + OCAP_setFocusStart :MISSION:FOCUS_START: upload(focusStart, focusEnd) + OCAP_setFocusEnd :MISSION:FOCUS_END: +``` + +## Usage + +```sqf +// Trim prep phase (end auto-filled at save time) +["OCAP_setFocusStart"] call CBA_fnc_serverEvent; + +// Explicit frame numbers +["OCAP_setFocusStart", [120]] call CBA_fnc_serverEvent; +["OCAP_setFocusEnd", [850]] call CBA_fnc_serverEvent; + +// Mark end of action (start defaults to 0) +["OCAP_setFocusEnd"] call CBA_fnc_serverEvent; +``` From 3af20500cea70ccffddd24096e2a7610b45cdfdf Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Fri, 6 Mar 2026 19:33:07 +0100 Subject: [PATCH 2/4] docs: add script-side focus implementation plan --- .../2026-03-06-script-side-focus-plan.md | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 docs/plans/2026-03-06-script-side-focus-plan.md diff --git a/docs/plans/2026-03-06-script-side-focus-plan.md b/docs/plans/2026-03-06-script-side-focus-plan.md new file mode 100644 index 0000000..8b8e820 --- /dev/null +++ b/docs/plans/2026-03-06-script-side-focus-plan.md @@ -0,0 +1,458 @@ +# Script-Side Focus Range — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Allow mission makers to set a focus range (start/end frames) via SQF script, which flows through the extension to the web upload. + +**Architecture:** Two CBA server events in the addon send frame numbers to the extension via new `:MISSION:FOCUS_START:` / `:MISSION:FOCUS_END:` commands. The extension stores them as lifecycle state, resolves incomplete pairs at save time, and includes them in the web upload. No storage interface changes — focus is a lifecycle/upload concern handled in main.go. + +**Tech Stack:** SQF (addon), Go (extension), existing web upload API (no web changes) + +--- + +### Task 1: Extension — Add focus fields to UploadMetadata + +**Files:** +- Modify: `/home/fank/repo/ocap2-extension/pkg/core/types.go:54-60` + +**Step 1: Add fields to UploadMetadata** + +```go +// UploadMetadata contains mission information needed for upload. +type UploadMetadata struct { + WorldName string + MissionName string + MissionDuration float64 + Tag string + EndFrame Frame + FocusStart *Frame + FocusEnd *Frame +} +``` + +`EndFrame` is needed so the MISSION:SAVE handler can fill in a missing `FocusEnd` with the last frame. `FocusStart`/`FocusEnd` are pointer types (nil = not set). + +**Step 2: Commit** + +```bash +cd /home/fank/repo/ocap2-extension +git add pkg/core/types.go +git commit -m "feat: add focus and EndFrame fields to UploadMetadata" +``` + +--- + +### Task 2: Extension — Populate EndFrame in computeExportMetadata + +**Files:** +- Modify: `/home/fank/repo/ocap2-extension/internal/storage/memory/memory.go:409-444` + +**Step 1: Set EndFrame on the returned metadata** + +In `computeExportMetadata()`, after computing `endFrame` (line 414-435) and before the return statement (line 439), set `EndFrame` on the result: + +```go +return core.UploadMetadata{ + WorldName: b.world.WorldName, + MissionName: b.mission.MissionName, + MissionDuration: duration, + Tag: b.mission.Tag, + EndFrame: endFrame, +} +``` + +**Step 2: Verify existing tests still pass** + +```bash +cd /home/fank/repo/ocap2-extension +go test ./internal/storage/memory/... -v -run TestGetExportMetadata +``` + +**Step 3: Commit** + +```bash +git add internal/storage/memory/memory.go +git commit -m "feat: populate EndFrame in memory backend export metadata" +``` + +--- + +### Task 3: Extension — Register focus lifecycle handlers in main.go + +**Files:** +- Modify: `/home/fank/repo/ocap2-extension/cmd/ocap_recorder/main.go` + +**Step 1: Add package-level focus state variables** + +After line 85 (`addonVersion string = "unknown"`), add: + +```go +// Focus range: set by :MISSION:FOCUS_START: / :MISSION:FOCUS_END:, cleared on new mission +focusStart *core.Frame +focusEnd *core.Frame +``` + +**Step 2: Reset focus in handleNewMission** + +In `handleNewMission` (line 289), after `MarkerCache.Reset()` / `EntityCache.Reset()` (lines 303-304), add: + +```go +// Reset focus range for new mission +focusStart = nil +focusEnd = nil +``` + +**Step 3: Register focus handlers in registerLifecycleHandlers** + +After the `:MISSION:START:` registration (line 375), add: + +```go +d.Register(":MISSION:FOCUS_START:", func(e dispatcher.Event) (any, error) { + if len(e.Args) < 1 { + return nil, fmt.Errorf("MISSION:FOCUS_START requires 1 arg (frame)") + } + v, err := strconv.ParseUint(strings.TrimSpace(util.TrimQuotes(e.Args[0])), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid focus start frame: %w", err) + } + f := core.Frame(v) + focusStart = &f + Logger.Info("Focus start set", "frame", f) + return nil, nil +}) + +d.Register(":MISSION:FOCUS_END:", func(e dispatcher.Event) (any, error) { + if len(e.Args) < 1 { + return nil, fmt.Errorf("MISSION:FOCUS_END requires 1 arg (frame)") + } + v, err := strconv.ParseUint(strings.TrimSpace(util.TrimQuotes(e.Args[0])), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid focus end frame: %w", err) + } + f := core.Frame(v) + focusEnd = &f + Logger.Info("Focus end set", "frame", f) + return nil, nil +}) +``` + +Add `"strconv"` and `"strings"` to imports if not already present. + +**Step 4: Resolve focus pair and set on metadata in MISSION:SAVE handler** + +In the `MISSION:SAVE` handler (line 377-408), after `meta := u.GetExportMetadata()` (line 389), add focus resolution: + +```go +// Resolve focus range +if focusStart != nil || focusEnd != nil { + if focusStart != nil { + meta.FocusStart = focusStart + } else { + f := core.Frame(0) + meta.FocusStart = &f + } + if focusEnd != nil { + meta.FocusEnd = focusEnd + } else { + meta.FocusEnd = &meta.EndFrame + } +} +``` + +**Step 5: Verify it compiles** + +```bash +cd /home/fank/repo/ocap2-extension +go build ./cmd/ocap_recorder/... +``` + +**Step 6: Commit** + +```bash +git add cmd/ocap_recorder/main.go +git commit -m "feat: register MISSION:FOCUS_START/END lifecycle handlers" +``` + +--- + +### Task 4: Extension — Send focus fields in API upload + +**Files:** +- Modify: `/home/fank/repo/ocap2-extension/internal/api/client.go:68-74` + +**Step 1: Add focus form fields to Upload** + +After the existing form fields (line 74, `_ = writer.WriteField("tag", meta.Tag)`), add: + +```go +if meta.FocusStart != nil { + _ = writer.WriteField("focusStart", strconv.FormatUint(uint64(*meta.FocusStart), 10)) +} +if meta.FocusEnd != nil { + _ = writer.WriteField("focusEnd", strconv.FormatUint(uint64(*meta.FocusEnd), 10)) +} +``` + +Add `"strconv"` to imports. + +**Step 2: Verify it compiles** + +```bash +cd /home/fank/repo/ocap2-extension +go build ./... +``` + +**Step 3: Commit** + +```bash +git add internal/api/client.go +git commit -m "feat: include focus range in web upload form fields" +``` + +--- + +### Task 5: Extension — Update README + +**Files:** +- Modify: `/home/fank/repo/ocap2-extension/README.md:157-165` + +**Step 1: Add focus commands to Lifecycle Commands table** + +After `:MISSION:SAVE:` row (line 164), add: + +```markdown +| `:MISSION:FOCUS_START:` | Set playback focus start frame | +| `:MISSION:FOCUS_END:` | Set playback focus end frame | +``` + +**Step 2: Commit** + +```bash +git add README.md +git commit -m "docs: add MISSION:FOCUS_START/END to supported commands" +``` + +--- + +### Task 6: Extension — Run all tests + +**Step 1: Run full test suite** + +```bash +cd /home/fank/repo/ocap2-extension +go test ./... -v +``` + +Expected: all existing tests pass. No new tests needed — focus handlers are simple lifecycle commands with inline parsing (same pattern as `:SYS:ADDON_VERSION:`), and the UploadMetadata changes are additive (new fields default to nil/zero). + +**Step 2: Fix any failures, commit if needed** + +--- + +### Task 7: Addon — Create fnc_setFocusStart.sqf + +**Files:** +- Create: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_setFocusStart.sqf` + +**Step 1: Write the function** + +```sqf +/* ---------------------------------------------------------------------------- +FILE: fnc_setFocusStart.sqf + +FUNCTION: OCAP_recorder_fnc_setFocusStart + +Description: + Sets the playback focus start frame. If no frame number is provided, + uses the current capture frame. Sends :MISSION:FOCUS_START: to extension. + +Parameters: + _frameNumber - (optional) explicit frame number [Number] + +Returns: + Nothing + +Examples: + > ["OCAP_setFocusStart"] call CBA_fnc_serverEvent; + > ["OCAP_setFocusStart", [120]] call CBA_fnc_serverEvent; + +Public: + No + +Author: + Fank +---------------------------------------------------------------------------- */ +#include "script_component.hpp" + +if (!SHOULDSAVEEVENTS) exitWith {}; + +params [["_frameNumber", GVAR(captureFrameNo), [0]]]; + +[":MISSION:FOCUS_START:", [_frameNumber]] call EFUNC(extension,sendData); +``` + +**Step 2: Commit** + +```bash +cd /home/fank/repo/ocap2-addon +git add addons/recorder/fnc_setFocusStart.sqf +git commit -m "feat: add fnc_setFocusStart handler" +``` + +--- + +### Task 8: Addon — Create fnc_setFocusEnd.sqf + +**Files:** +- Create: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_setFocusEnd.sqf` + +**Step 1: Write the function** + +```sqf +/* ---------------------------------------------------------------------------- +FILE: fnc_setFocusEnd.sqf + +FUNCTION: OCAP_recorder_fnc_setFocusEnd + +Description: + Sets the playback focus end frame. If no frame number is provided, + uses the current capture frame. Sends :MISSION:FOCUS_END: to extension. + +Parameters: + _frameNumber - (optional) explicit frame number [Number] + +Returns: + Nothing + +Examples: + > ["OCAP_setFocusEnd"] call CBA_fnc_serverEvent; + > ["OCAP_setFocusEnd", [850]] call CBA_fnc_serverEvent; + +Public: + No + +Author: + Fank +---------------------------------------------------------------------------- */ +#include "script_component.hpp" + +if (!SHOULDSAVEEVENTS) exitWith {}; + +params [["_frameNumber", GVAR(captureFrameNo), [0]]]; + +[":MISSION:FOCUS_END:", [_frameNumber]] call EFUNC(extension,sendData); +``` + +**Step 2: Commit** + +```bash +cd /home/fank/repo/ocap2-addon +git add addons/recorder/fnc_setFocusEnd.sqf +git commit -m "feat: add fnc_setFocusEnd handler" +``` + +--- + +### Task 9: Addon — Register functions and CBA events + +**Files:** +- Modify: `/home/fank/repo/ocap2-addon/addons/recorder/XEH_prep.sqf:45` +- Modify: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_addEventMission.sqf:302-308` + +**Step 1: Add PREP calls to XEH_prep.sqf** + +After `PREP(exportData);` (line 45), add: + +```sqf +PREP(setFocusStart); +PREP(setFocusEnd); +``` + +**Step 2: Register CBA events in fnc_addEventMission.sqf** + +After the `OCAP_exportData` listener block (after line 308), add: + +```sqf + +/* + CBA Event: OCAP_setFocusStart + Description: + Sets the playback focus start frame. Uses current capture frame if no + frame number is provided. Calls . + + Parameters: + 0 - Event name [String] + 1 - Event data [Array] + 1.0 - (optional) Frame number [Number] + + Example: + > ["OCAP_setFocusStart"] call CBA_fnc_serverEvent; + > ["OCAP_setFocusStart", [120]] call CBA_fnc_serverEvent; +*/ +if (isNil QEGVAR(listener,setFocusStart)) then { + EGVAR(listener,setFocusStart) = [QGVARMAIN(setFocusStart), { + _this call FUNC(setFocusStart); + }] call CBA_fnc_addEventHandler; + OCAPEXTLOG(["Initialized setFocusStart listener"]); +}; + +/* + CBA Event: OCAP_setFocusEnd + Description: + Sets the playback focus end frame. Uses current capture frame if no + frame number is provided. Calls . + + Parameters: + 0 - Event name [String] + 1 - Event data [Array] + 1.0 - (optional) Frame number [Number] + + Example: + > ["OCAP_setFocusEnd"] call CBA_fnc_serverEvent; + > ["OCAP_setFocusEnd", [850]] call CBA_fnc_serverEvent; +*/ +if (isNil QEGVAR(listener,setFocusEnd)) then { + EGVAR(listener,setFocusEnd) = [QGVARMAIN(setFocusEnd), { + _this call FUNC(setFocusEnd); + }] call CBA_fnc_addEventHandler; + OCAPEXTLOG(["Initialized setFocusEnd listener"]); +}; +``` + +**Step 3: Build addon to verify** + +```bash +cd /home/fank/repo/ocap2-addon +hemtt build +``` + +**Step 4: Commit** + +```bash +git add addons/recorder/XEH_prep.sqf addons/recorder/fnc_addEventMission.sqf +git commit -m "feat: register OCAP_setFocusStart/End CBA events" +``` + +--- + +### Task 10: Addon — Update listener handle documentation + +**Files:** +- Modify: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_addEventMission.sqf:127-139` + +**Step 1: Add focus listener handles to the Variables doc block** + +In the variables documentation block (lines 127-139), add before the closing `*/`: + +```sqf + OCAP_listener_setFocusStart - Handle for listener. + OCAP_listener_setFocusEnd - Handle for listener. +``` + +**Step 2: Commit** + +```bash +cd /home/fank/repo/ocap2-addon +git add addons/recorder/fnc_addEventMission.sqf +git commit -m "docs: add focus listener handles to variable documentation" +``` From f783f87e5e7eeb735de6c98104d0435b353ba29d Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Fri, 6 Mar 2026 21:54:29 +0100 Subject: [PATCH 3/4] fix: add authorization checks to admin-level CBA server events CBA server events can be triggered by any client via CBA_fnc_serverEvent. This adds access control to prevent unauthorized players from controlling recording (record, pause, exportData) or setting focus ranges (setFocusStart, setFocusEnd). Authorization allows: - Server-local execution (remoteExecutedOwner == 0) for mission scripts - Clients with OCAP admin controls (Arma server admins or players in OCAP_administratorList) Data recording events (customEvent, counterEvent) remain unrestricted as they are designed to be called from any mission script. --- addons/recorder/fnc_addEventMission.sqf | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/addons/recorder/fnc_addEventMission.sqf b/addons/recorder/fnc_addEventMission.sqf index 9cd3257..cbad80f 100644 --- a/addons/recorder/fnc_addEventMission.sqf +++ b/addons/recorder/fnc_addEventMission.sqf @@ -140,6 +140,23 @@ call FUNC(trackSectors); OCAP_listener_setFocusEnd - Handle for listener. */ +/* + Function: OCAP_recorder_isAuthorizedEventCaller + Description: + Authorization check for admin-level CBA server events (record, pause, + exportData, setFocusStart, setFocusEnd). Allows server-local execution + (mission scripts) and clients with OCAP admin controls (Arma server + admins or players in OCAP_administratorList). + + Returns: + true if caller is authorized, false otherwise [Boolean] +*/ +GVAR(isAuthorizedEventCaller) = { + if (remoteExecutedOwner isEqualTo 0) exitWith {true}; + private _callerUnit = (allPlayers select {owner _x isEqualTo remoteExecutedOwner}) param [0, objNull]; + _callerUnit getVariable [QGVARMAIN(hasAdminControls), false] +}; + if (isClass (configFile >> "CfgPatches" >> "ace_advanced_throwing")) then { if (isNil QEGVAR(listener,aceThrowing)) then { @@ -267,6 +284,9 @@ if (isNil QEGVAR(listener,counterEvent)) then { */ if (isNil QEGVAR(listener,record)) then { EGVAR(listener,record) = [QGVARMAIN(record), { + if !(call GVAR(isAuthorizedEventCaller)) exitWith { + WARNING("Unauthorized OCAP_record call blocked"); + }; call FUNC(startRecording); }] call CBA_fnc_addEventHandler; OCAPEXTLOG(["Initialized record listener"]); @@ -282,6 +302,9 @@ if (isNil QEGVAR(listener,record)) then { */ if (isNil QEGVAR(listener,pause)) then { EGVAR(listener,pause) = [QGVARMAIN(pause), { + if !(call GVAR(isAuthorizedEventCaller)) exitWith { + WARNING("Unauthorized OCAP_pause call blocked"); + }; call FUNC(stopRecording); }] call CBA_fnc_addEventHandler; OCAPEXTLOG(["Initialized pause listener"]); @@ -303,6 +326,9 @@ if (isNil QEGVAR(listener,pause)) then { */ if (isNil QEGVAR(listener,exportData)) then { EGVAR(listener,exportData) = [QGVARMAIN(exportData), { + if !(call GVAR(isAuthorizedEventCaller)) exitWith { + WARNING("Unauthorized OCAP_exportData call blocked"); + }; _this set [3, true]; _this call FUNC(exportData); }] call CBA_fnc_addEventHandler; @@ -326,6 +352,9 @@ if (isNil QEGVAR(listener,exportData)) then { */ if (isNil QEGVAR(listener,setFocusStart)) then { EGVAR(listener,setFocusStart) = [QGVARMAIN(setFocusStart), { + if !(call GVAR(isAuthorizedEventCaller)) exitWith { + WARNING("Unauthorized OCAP_setFocusStart call blocked"); + }; _this call FUNC(setFocusStart); }] call CBA_fnc_addEventHandler; OCAPEXTLOG(["Initialized setFocusStart listener"]); @@ -348,6 +377,9 @@ if (isNil QEGVAR(listener,setFocusStart)) then { */ if (isNil QEGVAR(listener,setFocusEnd)) then { EGVAR(listener,setFocusEnd) = [QGVARMAIN(setFocusEnd), { + if !(call GVAR(isAuthorizedEventCaller)) exitWith { + WARNING("Unauthorized OCAP_setFocusEnd call blocked"); + }; _this call FUNC(setFocusEnd); }] call CBA_fnc_addEventHandler; OCAPEXTLOG(["Initialized setFocusEnd listener"]); From 0914b6fbaafb45496149c45458ca4ba36f07b392 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Fri, 6 Mar 2026 22:02:43 +0100 Subject: [PATCH 4/4] chore: remove focus implementation plan files --- .../2026-03-06-script-side-focus-design.md | 60 --- .../2026-03-06-script-side-focus-plan.md | 458 ------------------ 2 files changed, 518 deletions(-) delete mode 100644 docs/plans/2026-03-06-script-side-focus-design.md delete mode 100644 docs/plans/2026-03-06-script-side-focus-plan.md diff --git a/docs/plans/2026-03-06-script-side-focus-design.md b/docs/plans/2026-03-06-script-side-focus-design.md deleted file mode 100644 index b9b0d11..0000000 --- a/docs/plans/2026-03-06-script-side-focus-design.md +++ /dev/null @@ -1,60 +0,0 @@ -# Script-Side Focus Range - -## Problem - -OCAP2 records entire missions including prep phases, briefings, and downtime. The web UI already supports a focus range (focusStart/focusEnd in frames) to highlight the interesting portion, but it can only be set after upload via the admin interface. Mission makers want to define this range from SQF during the mission. - -## Design - -### Addon (SQF) - -Two new CBA server events: - -- `OCAP_setFocusStart` — optional param `[frameNumber]`, defaults to current `GVAR(captureFrameNo)` -- `OCAP_setFocusEnd` — same pattern - -Each event sends a command to the extension immediately: -- `:MISSION:FOCUS_START:` -- `:MISSION:FOCUS_END:` - -No SQF-side storage — the extension is the source of truth. - -### Extension (Go) - -Two new commands stored on mission state. At save time (`MISSION:SAVE`), the extension resolves incomplete pairs: - -| focusStart | focusEnd | Result | -|------------|----------|--------| -| set | set | Pass both to web upload | -| set | not set | Fill end with total frame count | -| not set | set | Fill start with 0 | -| not set | not set | Pass nothing | - -The resolved pair is sent as `focusStart`/`focusEnd` form fields in the existing web upload request. - -### Web - -No changes. The upload endpoint already accepts and validates `focusStart`/`focusEnd`. - -## Data Flow - -``` -Mission script → Addon → Extension → Web - | | | - OCAP_setFocusStart :MISSION:FOCUS_START: upload(focusStart, focusEnd) - OCAP_setFocusEnd :MISSION:FOCUS_END: -``` - -## Usage - -```sqf -// Trim prep phase (end auto-filled at save time) -["OCAP_setFocusStart"] call CBA_fnc_serverEvent; - -// Explicit frame numbers -["OCAP_setFocusStart", [120]] call CBA_fnc_serverEvent; -["OCAP_setFocusEnd", [850]] call CBA_fnc_serverEvent; - -// Mark end of action (start defaults to 0) -["OCAP_setFocusEnd"] call CBA_fnc_serverEvent; -``` diff --git a/docs/plans/2026-03-06-script-side-focus-plan.md b/docs/plans/2026-03-06-script-side-focus-plan.md deleted file mode 100644 index 8b8e820..0000000 --- a/docs/plans/2026-03-06-script-side-focus-plan.md +++ /dev/null @@ -1,458 +0,0 @@ -# Script-Side Focus Range — Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Allow mission makers to set a focus range (start/end frames) via SQF script, which flows through the extension to the web upload. - -**Architecture:** Two CBA server events in the addon send frame numbers to the extension via new `:MISSION:FOCUS_START:` / `:MISSION:FOCUS_END:` commands. The extension stores them as lifecycle state, resolves incomplete pairs at save time, and includes them in the web upload. No storage interface changes — focus is a lifecycle/upload concern handled in main.go. - -**Tech Stack:** SQF (addon), Go (extension), existing web upload API (no web changes) - ---- - -### Task 1: Extension — Add focus fields to UploadMetadata - -**Files:** -- Modify: `/home/fank/repo/ocap2-extension/pkg/core/types.go:54-60` - -**Step 1: Add fields to UploadMetadata** - -```go -// UploadMetadata contains mission information needed for upload. -type UploadMetadata struct { - WorldName string - MissionName string - MissionDuration float64 - Tag string - EndFrame Frame - FocusStart *Frame - FocusEnd *Frame -} -``` - -`EndFrame` is needed so the MISSION:SAVE handler can fill in a missing `FocusEnd` with the last frame. `FocusStart`/`FocusEnd` are pointer types (nil = not set). - -**Step 2: Commit** - -```bash -cd /home/fank/repo/ocap2-extension -git add pkg/core/types.go -git commit -m "feat: add focus and EndFrame fields to UploadMetadata" -``` - ---- - -### Task 2: Extension — Populate EndFrame in computeExportMetadata - -**Files:** -- Modify: `/home/fank/repo/ocap2-extension/internal/storage/memory/memory.go:409-444` - -**Step 1: Set EndFrame on the returned metadata** - -In `computeExportMetadata()`, after computing `endFrame` (line 414-435) and before the return statement (line 439), set `EndFrame` on the result: - -```go -return core.UploadMetadata{ - WorldName: b.world.WorldName, - MissionName: b.mission.MissionName, - MissionDuration: duration, - Tag: b.mission.Tag, - EndFrame: endFrame, -} -``` - -**Step 2: Verify existing tests still pass** - -```bash -cd /home/fank/repo/ocap2-extension -go test ./internal/storage/memory/... -v -run TestGetExportMetadata -``` - -**Step 3: Commit** - -```bash -git add internal/storage/memory/memory.go -git commit -m "feat: populate EndFrame in memory backend export metadata" -``` - ---- - -### Task 3: Extension — Register focus lifecycle handlers in main.go - -**Files:** -- Modify: `/home/fank/repo/ocap2-extension/cmd/ocap_recorder/main.go` - -**Step 1: Add package-level focus state variables** - -After line 85 (`addonVersion string = "unknown"`), add: - -```go -// Focus range: set by :MISSION:FOCUS_START: / :MISSION:FOCUS_END:, cleared on new mission -focusStart *core.Frame -focusEnd *core.Frame -``` - -**Step 2: Reset focus in handleNewMission** - -In `handleNewMission` (line 289), after `MarkerCache.Reset()` / `EntityCache.Reset()` (lines 303-304), add: - -```go -// Reset focus range for new mission -focusStart = nil -focusEnd = nil -``` - -**Step 3: Register focus handlers in registerLifecycleHandlers** - -After the `:MISSION:START:` registration (line 375), add: - -```go -d.Register(":MISSION:FOCUS_START:", func(e dispatcher.Event) (any, error) { - if len(e.Args) < 1 { - return nil, fmt.Errorf("MISSION:FOCUS_START requires 1 arg (frame)") - } - v, err := strconv.ParseUint(strings.TrimSpace(util.TrimQuotes(e.Args[0])), 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid focus start frame: %w", err) - } - f := core.Frame(v) - focusStart = &f - Logger.Info("Focus start set", "frame", f) - return nil, nil -}) - -d.Register(":MISSION:FOCUS_END:", func(e dispatcher.Event) (any, error) { - if len(e.Args) < 1 { - return nil, fmt.Errorf("MISSION:FOCUS_END requires 1 arg (frame)") - } - v, err := strconv.ParseUint(strings.TrimSpace(util.TrimQuotes(e.Args[0])), 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid focus end frame: %w", err) - } - f := core.Frame(v) - focusEnd = &f - Logger.Info("Focus end set", "frame", f) - return nil, nil -}) -``` - -Add `"strconv"` and `"strings"` to imports if not already present. - -**Step 4: Resolve focus pair and set on metadata in MISSION:SAVE handler** - -In the `MISSION:SAVE` handler (line 377-408), after `meta := u.GetExportMetadata()` (line 389), add focus resolution: - -```go -// Resolve focus range -if focusStart != nil || focusEnd != nil { - if focusStart != nil { - meta.FocusStart = focusStart - } else { - f := core.Frame(0) - meta.FocusStart = &f - } - if focusEnd != nil { - meta.FocusEnd = focusEnd - } else { - meta.FocusEnd = &meta.EndFrame - } -} -``` - -**Step 5: Verify it compiles** - -```bash -cd /home/fank/repo/ocap2-extension -go build ./cmd/ocap_recorder/... -``` - -**Step 6: Commit** - -```bash -git add cmd/ocap_recorder/main.go -git commit -m "feat: register MISSION:FOCUS_START/END lifecycle handlers" -``` - ---- - -### Task 4: Extension — Send focus fields in API upload - -**Files:** -- Modify: `/home/fank/repo/ocap2-extension/internal/api/client.go:68-74` - -**Step 1: Add focus form fields to Upload** - -After the existing form fields (line 74, `_ = writer.WriteField("tag", meta.Tag)`), add: - -```go -if meta.FocusStart != nil { - _ = writer.WriteField("focusStart", strconv.FormatUint(uint64(*meta.FocusStart), 10)) -} -if meta.FocusEnd != nil { - _ = writer.WriteField("focusEnd", strconv.FormatUint(uint64(*meta.FocusEnd), 10)) -} -``` - -Add `"strconv"` to imports. - -**Step 2: Verify it compiles** - -```bash -cd /home/fank/repo/ocap2-extension -go build ./... -``` - -**Step 3: Commit** - -```bash -git add internal/api/client.go -git commit -m "feat: include focus range in web upload form fields" -``` - ---- - -### Task 5: Extension — Update README - -**Files:** -- Modify: `/home/fank/repo/ocap2-extension/README.md:157-165` - -**Step 1: Add focus commands to Lifecycle Commands table** - -After `:MISSION:SAVE:` row (line 164), add: - -```markdown -| `:MISSION:FOCUS_START:` | Set playback focus start frame | -| `:MISSION:FOCUS_END:` | Set playback focus end frame | -``` - -**Step 2: Commit** - -```bash -git add README.md -git commit -m "docs: add MISSION:FOCUS_START/END to supported commands" -``` - ---- - -### Task 6: Extension — Run all tests - -**Step 1: Run full test suite** - -```bash -cd /home/fank/repo/ocap2-extension -go test ./... -v -``` - -Expected: all existing tests pass. No new tests needed — focus handlers are simple lifecycle commands with inline parsing (same pattern as `:SYS:ADDON_VERSION:`), and the UploadMetadata changes are additive (new fields default to nil/zero). - -**Step 2: Fix any failures, commit if needed** - ---- - -### Task 7: Addon — Create fnc_setFocusStart.sqf - -**Files:** -- Create: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_setFocusStart.sqf` - -**Step 1: Write the function** - -```sqf -/* ---------------------------------------------------------------------------- -FILE: fnc_setFocusStart.sqf - -FUNCTION: OCAP_recorder_fnc_setFocusStart - -Description: - Sets the playback focus start frame. If no frame number is provided, - uses the current capture frame. Sends :MISSION:FOCUS_START: to extension. - -Parameters: - _frameNumber - (optional) explicit frame number [Number] - -Returns: - Nothing - -Examples: - > ["OCAP_setFocusStart"] call CBA_fnc_serverEvent; - > ["OCAP_setFocusStart", [120]] call CBA_fnc_serverEvent; - -Public: - No - -Author: - Fank ----------------------------------------------------------------------------- */ -#include "script_component.hpp" - -if (!SHOULDSAVEEVENTS) exitWith {}; - -params [["_frameNumber", GVAR(captureFrameNo), [0]]]; - -[":MISSION:FOCUS_START:", [_frameNumber]] call EFUNC(extension,sendData); -``` - -**Step 2: Commit** - -```bash -cd /home/fank/repo/ocap2-addon -git add addons/recorder/fnc_setFocusStart.sqf -git commit -m "feat: add fnc_setFocusStart handler" -``` - ---- - -### Task 8: Addon — Create fnc_setFocusEnd.sqf - -**Files:** -- Create: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_setFocusEnd.sqf` - -**Step 1: Write the function** - -```sqf -/* ---------------------------------------------------------------------------- -FILE: fnc_setFocusEnd.sqf - -FUNCTION: OCAP_recorder_fnc_setFocusEnd - -Description: - Sets the playback focus end frame. If no frame number is provided, - uses the current capture frame. Sends :MISSION:FOCUS_END: to extension. - -Parameters: - _frameNumber - (optional) explicit frame number [Number] - -Returns: - Nothing - -Examples: - > ["OCAP_setFocusEnd"] call CBA_fnc_serverEvent; - > ["OCAP_setFocusEnd", [850]] call CBA_fnc_serverEvent; - -Public: - No - -Author: - Fank ----------------------------------------------------------------------------- */ -#include "script_component.hpp" - -if (!SHOULDSAVEEVENTS) exitWith {}; - -params [["_frameNumber", GVAR(captureFrameNo), [0]]]; - -[":MISSION:FOCUS_END:", [_frameNumber]] call EFUNC(extension,sendData); -``` - -**Step 2: Commit** - -```bash -cd /home/fank/repo/ocap2-addon -git add addons/recorder/fnc_setFocusEnd.sqf -git commit -m "feat: add fnc_setFocusEnd handler" -``` - ---- - -### Task 9: Addon — Register functions and CBA events - -**Files:** -- Modify: `/home/fank/repo/ocap2-addon/addons/recorder/XEH_prep.sqf:45` -- Modify: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_addEventMission.sqf:302-308` - -**Step 1: Add PREP calls to XEH_prep.sqf** - -After `PREP(exportData);` (line 45), add: - -```sqf -PREP(setFocusStart); -PREP(setFocusEnd); -``` - -**Step 2: Register CBA events in fnc_addEventMission.sqf** - -After the `OCAP_exportData` listener block (after line 308), add: - -```sqf - -/* - CBA Event: OCAP_setFocusStart - Description: - Sets the playback focus start frame. Uses current capture frame if no - frame number is provided. Calls . - - Parameters: - 0 - Event name [String] - 1 - Event data [Array] - 1.0 - (optional) Frame number [Number] - - Example: - > ["OCAP_setFocusStart"] call CBA_fnc_serverEvent; - > ["OCAP_setFocusStart", [120]] call CBA_fnc_serverEvent; -*/ -if (isNil QEGVAR(listener,setFocusStart)) then { - EGVAR(listener,setFocusStart) = [QGVARMAIN(setFocusStart), { - _this call FUNC(setFocusStart); - }] call CBA_fnc_addEventHandler; - OCAPEXTLOG(["Initialized setFocusStart listener"]); -}; - -/* - CBA Event: OCAP_setFocusEnd - Description: - Sets the playback focus end frame. Uses current capture frame if no - frame number is provided. Calls . - - Parameters: - 0 - Event name [String] - 1 - Event data [Array] - 1.0 - (optional) Frame number [Number] - - Example: - > ["OCAP_setFocusEnd"] call CBA_fnc_serverEvent; - > ["OCAP_setFocusEnd", [850]] call CBA_fnc_serverEvent; -*/ -if (isNil QEGVAR(listener,setFocusEnd)) then { - EGVAR(listener,setFocusEnd) = [QGVARMAIN(setFocusEnd), { - _this call FUNC(setFocusEnd); - }] call CBA_fnc_addEventHandler; - OCAPEXTLOG(["Initialized setFocusEnd listener"]); -}; -``` - -**Step 3: Build addon to verify** - -```bash -cd /home/fank/repo/ocap2-addon -hemtt build -``` - -**Step 4: Commit** - -```bash -git add addons/recorder/XEH_prep.sqf addons/recorder/fnc_addEventMission.sqf -git commit -m "feat: register OCAP_setFocusStart/End CBA events" -``` - ---- - -### Task 10: Addon — Update listener handle documentation - -**Files:** -- Modify: `/home/fank/repo/ocap2-addon/addons/recorder/fnc_addEventMission.sqf:127-139` - -**Step 1: Add focus listener handles to the Variables doc block** - -In the variables documentation block (lines 127-139), add before the closing `*/`: - -```sqf - OCAP_listener_setFocusStart - Handle for listener. - OCAP_listener_setFocusEnd - Handle for listener. -``` - -**Step 2: Commit** - -```bash -cd /home/fank/repo/ocap2-addon -git add addons/recorder/fnc_addEventMission.sqf -git commit -m "docs: add focus listener handles to variable documentation" -```