diff --git a/addons/recorder/XEH_preInit.sqf b/addons/recorder/XEH_preInit.sqf index 700d39d..a8cfe7b 100644 --- a/addons/recorder/XEH_preInit.sqf +++ b/addons/recorder/XEH_preInit.sqf @@ -62,6 +62,81 @@ GVAR(allSettings) = [ false // requires restart to apply ], + /* + CBA Setting: OCAP_settings_excludeHeadlessClientsFromAutoStart + Description: + Exclude headless clients when counting players for automatic recording start and restart. If disabled, headless clients alone can meet the threshold, including after an empty-server save. Default: true + + Setting Name: + Exclude Headless Clients + + Value Type: + Boolean + */ + [ + QEGVAR(settings,excludeHeadlessClientsFromAutoStart), + "CHECKBOX", + [ + "Exclude Headless Clients", + "Exclude headless clients when counting players for automatic recording start and restart. If disabled, headless clients alone can meet the threshold, including after an empty-server save. Default: true" + ], + [COMPONENT_NAME, "Auto-start Settings"], + true, + true, + {}, + false + ], + + /* + CBA Setting: OCAP_settings_autoRestartAfterEmpty + Description: + With Auto Start Recording enabled at mission initialization, automatically start a new recording after an empty-server auto-save once the minimum player count is reached again. Default: true + + Setting Name: + Auto-Restart After Empty Server + + Value Type: + Boolean + */ + [ + QEGVAR(settings,autoRestartAfterEmpty), + "CHECKBOX", + [ + "Auto-Restart After Empty Server", + "With Auto Start Recording enabled at mission initialization, automatically start a new recording after an empty-server auto-save once the minimum player count is reached again. Default: true" + ], + [COMPONENT_NAME, "Auto-start Settings"], + true, + true, + {}, + false + ], + + /* + CBA Setting: OCAP_settings_bufferPlayerConnectionEvents + Description: + Include currently connected human players at the first recorded frame. Connections and disconnections before recording are not retained. Default: true + + Setting Name: + Include Connected Players at Recording Start + + Value Type: + Boolean + */ + [ + QEGVAR(settings,bufferPlayerConnectionEvents), + "CHECKBOX", + [ + "Include Connected Players at Recording Start", + "Include currently connected human players at the first recorded frame. Connections and disconnections before recording are not retained. Default: true" + ], + [COMPONENT_NAME, "Auto-start Settings"], + true, + true, + {}, + false + ], + // Section: Core @@ -241,6 +316,31 @@ GVAR(allSettings) = [ // Section: Extra Tracking + /* + CBA Setting: OCAP_settings_includeSteamIdInPlayerConnectionEvents + Description: + Include the player's Steam ID as the fourth value in connected and disconnected events. Default: true + + Setting Name: + Include Steam ID in Connection Events + + Value Type: + Boolean + */ + [ + QEGVAR(settings,includeSteamIdInPlayerConnectionEvents), + "CHECKBOX", + [ + "Include Steam ID in Connection Events", + "Include the player's Steam ID as the fourth value in connected and disconnected events: [frame, action, name, id]. Default: true" + ], + [COMPONENT_NAME, "Extra Tracking"], + true, + true, + {}, + false + ], + /* CBA Setting: OCAP_settings_trackTickets Description: diff --git a/addons/recorder/XEH_prep.sqf b/addons/recorder/XEH_prep.sqf index b9d7e5c..b7417fa 100644 --- a/addons/recorder/XEH_prep.sqf +++ b/addons/recorder/XEH_prep.sqf @@ -6,6 +6,8 @@ PREP(updateTime); PREP(startRecording); PREP(stopRecording); +PREP(autoRestartMonitor); +PREP(getAutoStartPlayerCount); PREP(captureLoop); PREP(isKindOfApc); PREP(getClass); @@ -18,6 +20,8 @@ PREP(addUnitEventHandlers); PREP(eh_connected); PREP(eh_disconnected); +PREP(recordPlayerConnectionEvent); +PREP(flushPlayerConnectionEvents); PREP(eh_onUserAdminStateChanged); PREP(eh_onUserSelectedPlayer); PREP(adminUIcontrol); diff --git a/addons/recorder/fnc_autoRestartMonitor.sqf b/addons/recorder/fnc_autoRestartMonitor.sqf new file mode 100644 index 0000000..93a4cc0 --- /dev/null +++ b/addons/recorder/fnc_autoRestartMonitor.sqf @@ -0,0 +1,29 @@ +/* ---------------------------------------------------------------------------- +FILE: fnc_autoRestartMonitor.sqf + +FUNCTION: OCAP_recorder_fnc_autoRestartMonitor + +Description: + Periodically checks whether a recording auto-saved because the server became + empty and starts a fresh recording after the player threshold is met again. + +Returns: + Nothing + +Public: + No +---------------------------------------------------------------------------- */ + +#include "script_component.hpp" + +if (!GVAR(autoStart) || {!EGVAR(settings,autoRestartAfterEmpty)}) exitWith {}; +if (!GVAR(autoRestartAfterEmptyPending)) exitWith {}; +if (GVAR(recording) || {!isNil QGVAR(startTime)}) exitWith {}; +if (getClientStateNumber <= 9) exitWith {}; + +private _playerCount = call FUNC(getAutoStartPlayerCount); +if (_playerCount < EGVAR(settings,minPlayerCount)) exitWith {}; + +GVAR(autoRestartAfterEmptyPending) = false; +INFO_1("Auto-restarting recording after empty-server save; eligible players: %1",_playerCount); +call FUNC(startRecording); diff --git a/addons/recorder/fnc_eh_connected.sqf b/addons/recorder/fnc_eh_connected.sqf index 9cab6d3..e16f06d 100644 --- a/addons/recorder/fnc_eh_connected.sqf +++ b/addons/recorder/fnc_eh_connected.sqf @@ -29,12 +29,5 @@ params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"]; // skip for server 'connected' message if (_owner isEqualTo 2) exitWith {}; -// log to timeline -[":EVENT:GENERAL:", [ - GVAR(captureFrameNo), - "connected", - _name, - [createHashMapFromArray [ - ["playerUid", _uid] - ]] call CBA_fnc_encodeJSON -]] call EFUNC(extension,sendData); +// Log during an active session. Recording start snapshots current players. +["connected", _name, _uid] call FUNC(recordPlayerConnectionEvent); diff --git a/addons/recorder/fnc_eh_disconnected.sqf b/addons/recorder/fnc_eh_disconnected.sqf index d95b48a..65c6e95 100644 --- a/addons/recorder/fnc_eh_disconnected.sqf +++ b/addons/recorder/fnc_eh_disconnected.sqf @@ -27,14 +27,7 @@ params ["_unit", "_id", "_uid", "_name"]; -[":EVENT:GENERAL:", [ - GVAR(captureFrameNo), - "disconnected", - _name, - [createHashMapFromArray [ - ["playerUid", _uid] - ]] call CBA_fnc_encodeJSON -]] call EFUNC(extension,sendData); +["disconnected", _name, _uid] call FUNC(recordPlayerConnectionEvent); if (_unit getVariable [QGVARMAIN(isInitialized), false]) then { [":SOLDIER:DELETE:", [ @@ -54,6 +47,7 @@ if ( {(call CBA_fnc_players) - [_unit] isEqualTo []} && {(GVAR(frameCaptureDelay) * GVAR(captureFrameNo)) / 60 >= GVAR(minMissionTime)} ) then { + GVAR(autoRestartAfterEmptyPending) = true; [nil, "Recording ended due to server being empty"] call FUNC(exportData); }; diff --git a/addons/recorder/fnc_flushPlayerConnectionEvents.sqf b/addons/recorder/fnc_flushPlayerConnectionEvents.sqf new file mode 100644 index 0000000..a668c0c --- /dev/null +++ b/addons/recorder/fnc_flushPlayerConnectionEvents.sqf @@ -0,0 +1,23 @@ +/* ---------------------------------------------------------------------------- +FILE: fnc_flushPlayerConnectionEvents.sqf + +FUNCTION: OCAP_recorder_fnc_flushPlayerConnectionEvents + +Description: + Records a snapshot of currently connected human players at the first recorded + frame. No connection history is retained between recordings. + +Returns: + Nothing + +Public: + No +---------------------------------------------------------------------------- */ + +#include "script_component.hpp" + +if (!EGVAR(settings,bufferPlayerConnectionEvents)) exitWith {}; + +{ + ["connected", name _x, getPlayerUID _x, 1] call FUNC(recordPlayerConnectionEvent); +} forEach (allPlayers - (entities "HeadlessClient_F")); diff --git a/addons/recorder/fnc_getAutoStartPlayerCount.sqf b/addons/recorder/fnc_getAutoStartPlayerCount.sqf new file mode 100644 index 0000000..a2a18eb --- /dev/null +++ b/addons/recorder/fnc_getAutoStartPlayerCount.sqf @@ -0,0 +1,26 @@ +/* ---------------------------------------------------------------------------- +FILE: fnc_getAutoStartPlayerCount.sqf + +FUNCTION: OCAP_recorder_fnc_getAutoStartPlayerCount + +Description: + Returns the number of players eligible for automatic recording start. The + configured headless-client exclusion is applied here for both initial start + and automatic restart checks. + +Returns: + Eligible player count [Number] + +Public: + No +---------------------------------------------------------------------------- */ + +#include "script_component.hpp" + +private _players = allPlayers; + +if (EGVAR(settings,excludeHeadlessClientsFromAutoStart)) then { + _players = _players - (entities "HeadlessClient_F"); +}; + +count _players diff --git a/addons/recorder/fnc_init.sqf b/addons/recorder/fnc_init.sqf index 30e12bd..1d8c982 100644 --- a/addons/recorder/fnc_init.sqf +++ b/addons/recorder/fnc_init.sqf @@ -54,6 +54,9 @@ publicVariable QGVAR(captureFrameNo); */ GVAR(nextId) = 0; +// Only an empty-server auto-save may arm the automatic restart path. +GVAR(autoRestartAfterEmptyPending) = false; + // save static setting values so changes during a mission don't interrupt timeline @@ -225,13 +228,19 @@ call EFUNC(extension,initSession); Start recording AFTER Briefing screen, so the beginning of the recording matches the start of the actual mission session. */ [ - {(getClientStateNumber > 9 && (count allPlayers) >= EGVAR(settings,minPlayerCount) && GVAR(autoStart)) || !isNil QGVAR(startTime)}, + {(getClientStateNumber > 9 && (call FUNC(getAutoStartPlayerCount)) >= EGVAR(settings,minPlayerCount) && GVAR(autoStart)) || !isNil QGVAR(startTime)}, { call FUNC(startRecording); [QGVARMAIN(customEvent), ["generalEvent", "Mission has started!"]] call CBA_fnc_serverEvent; } ] call CBA_fnc_waitUntilAndExecute; +// Periodically re-arm auto-start after an empty-server auto-save. Unlike the +// initial waiter above, this handler remains active for the whole mission. +[{ + call FUNC(autoRestartMonitor); +}, 10] call CBA_fnc_addPerFrameHandler; + if (isNil QGVAR(entityMonitorsInitialized)) then { call FUNC(entityMonitors); diff --git a/addons/recorder/fnc_recordPlayerConnectionEvent.sqf b/addons/recorder/fnc_recordPlayerConnectionEvent.sqf new file mode 100644 index 0000000..fbdd200 --- /dev/null +++ b/addons/recorder/fnc_recordPlayerConnectionEvent.sqf @@ -0,0 +1,46 @@ +/* ---------------------------------------------------------------------------- +FILE: fnc_recordPlayerConnectionEvent.sqf + +FUNCTION: OCAP_recorder_fnc_recordPlayerConnectionEvent + +Description: + Records a player connection event when a recording session is available. + Events outside a recording are discarded; recording start takes a snapshot + of the players who are still connected instead. + +Parameters: + _eventType - "connected" or "disconnected" [String] + _name - Player name [String] + _uid - Player UID [String] + _frame - Recording frame, defaults to the current capture frame [optional, Number] + +Returns: + Nothing + +Public: + No +---------------------------------------------------------------------------- */ + +#include "script_component.hpp" + +params ["_eventType", "_name", "_uid", ["_frame", GVAR(captureFrameNo), [0]]]; + +if (!EGVAR(extension,sessionReady)) exitWith {}; +// startRecording sets recording before the startup snapshot, while captureLoop +// sets startTime afterwards. A paused recording keeps startTime and its events. +if (isNil QGVAR(startTime) && {!GVAR(recording)}) exitWith {}; + +private _extraData = createHashMap; + +if (EGVAR(settings,includeSteamIdInPlayerConnectionEvents)) then { + _extraData set ["playerUid", _uid]; +}; + +private _eventData = [ + _frame max 1, + _eventType, + _name, + [_extraData] call CBA_fnc_encodeJSON +]; + +[":EVENT:GENERAL:", _eventData] call EFUNC(extension,sendData); diff --git a/addons/recorder/fnc_startRecording.sqf b/addons/recorder/fnc_startRecording.sqf index 975b60f..ea14f33 100644 --- a/addons/recorder/fnc_startRecording.sqf +++ b/addons/recorder/fnc_startRecording.sqf @@ -39,6 +39,9 @@ if (GVAR(recording) && GVAR(captureFrameNo) > 10) exitWith { ] remoteExecCall ["CBA_fnc_notify", [0, -2] select isDedicated]; }; +// Any explicit or automatic start consumes a pending empty-server restart. +GVAR(autoRestartAfterEmptyPending) = false; + GVAR(recording) = true; publicVariable QGVAR(recording); @@ -71,12 +74,14 @@ if (GVAR(captureFrameNo) == 0) then { call EFUNC(extension,newMission); // Wait for extension to confirm new mission before starting capture [{EGVAR(extension,sessionReady)}, { + call FUNC(flushPlayerConnectionEvents); call FUNC(captureLoop); }, [], 30, { ERROR("Timeout waiting for new mission confirmation from extension. Recording will not start."); ["OCAP failed to start recording: extension did not respond", 1, [1, 0, 0, 1]] remoteExecCall ["CBA_fnc_notify", [0, -2] select isDedicated]; }] call CBA_fnc_waitUntilAndExecute; } else { + call FUNC(flushPlayerConnectionEvents); call FUNC(captureLoop); }; }; diff --git a/docs/recording-lifecycle.md b/docs/recording-lifecycle.md new file mode 100644 index 0000000..99c7041 --- /dev/null +++ b/docs/recording-lifecycle.md @@ -0,0 +1,43 @@ +# Automatic recording lifecycle + +After `saveOnEmpty` exports a recording, the recorder checks the player threshold +every 10 seconds. It starts a new recording only if `autoStart` was enabled at +mission initialization and `autoRestartAfterEmpty` is enabled. Manual starts and +paused recordings keep their existing behavior. + +`excludeHeadlessClientsFromAutoStart` still controls the player count for both +initial start and restart. Disabling it allows headless clients alone to meet the +threshold, even after an empty-server save. + +The existing `OCAP_settings_bufferPlayerConnectionEvents` setting now controls +**Include Connected Players at Recording Start**. When enabled, a new recording +includes one `connected` event per currently connected human player at internal +frame 1 (the first recorded frame). Disconnected players and headless clients are +omitted. No connection history is stored while waiting for a recording, so churn +between recordings cannot accumulate or appear in a later recording. Active and +paused recordings still receive live connection events. The Steam ID setting and +`playerUid` payload format are unchanged. + +## Dedicated-server verification + +These scenarios require Arma 3 and the recorder extension; HEMTT checks and builds +do not exercise the server lifecycle. Inspect the resulting recordings as well +as the server notifications. + +1. Enable auto-start, restart and save-on-empty. Reach the player threshold, + record beyond `minMissionTime`, then disconnect everyone. Verify one export; + rejoin below the threshold and verify recording stays stopped; reach the + threshold and verify a fresh recording starts within 10 seconds. Repeat. +2. Disable auto-start before mission initialization, then start manually. Repeat + the empty-server save and rejoin flow. Verify no automatic recording starts, + even when the threshold is reached. Verify manual start still works. +3. Between recordings, connect and disconnect several players. Leave some humans + and a headless client connected, then start. Verify only the humans still + connected have startup `connected` events, at the first frame; no stale + `disconnected` events appear. Repeat before the first recording. +4. Disable the startup-player setting and verify no startup snapshot is emitted. + Check live connect/disconnect events while recording and paused. With Steam + IDs enabled, verify `playerUid`; disable the setting and verify it is omitted. +5. With headless-client exclusion enabled, verify headless clients do not help + reach the start/restart threshold. With it disabled, verify the configured + counting behavior, including the documented headless-only restart case.