From 3eb1cd89051d053c6558d11de787c89b1aefd943 Mon Sep 17 00:00:00 2001 From: Yethe Samartaka Date: Sat, 22 Aug 2026 15:30:24 +0200 Subject: [PATCH 1/2] feat(recorder): improve automatic recording lifecycle Added: - Auto-restart and connection event buffering - New CBA settings to configure everything these changes are touching Changed: - Player counting and Steam ID handling --- addons/recorder/XEH_preInit.sqf | 105 ++++++++++++++++++ addons/recorder/XEH_prep.sqf | 4 + addons/recorder/fnc_autoRestartMonitor.sqf | 29 +++++ addons/recorder/fnc_eh_connected.sqf | 12 +- addons/recorder/fnc_eh_disconnected.sqf | 10 +- .../fnc_flushPlayerConnectionEvents.sqf | 38 +++++++ .../recorder/fnc_getAutoStartPlayerCount.sqf | 26 +++++ addons/recorder/fnc_init.sqf | 17 ++- .../fnc_recordPlayerConnectionEvent.sqf | 45 ++++++++ addons/recorder/fnc_startRecording.sqf | 5 + 10 files changed, 273 insertions(+), 18 deletions(-) create mode 100644 addons/recorder/fnc_autoRestartMonitor.sqf create mode 100644 addons/recorder/fnc_flushPlayerConnectionEvents.sqf create mode 100644 addons/recorder/fnc_getAutoStartPlayerCount.sqf create mode 100644 addons/recorder/fnc_recordPlayerConnectionEvent.sqf diff --git a/addons/recorder/XEH_preInit.sqf b/addons/recorder/XEH_preInit.sqf index 4f6616d..fe3f516 100644 --- a/addons/recorder/XEH_preInit.sqf +++ b/addons/recorder/XEH_preInit.sqf @@ -62,6 +62,86 @@ 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. 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. Default: true" + ], + [COMPONENT_NAME, "Auto-start Settings"], + true, + true, + {}, + false + ], + + /* + CBA Setting: OCAP_settings_autoRestartAfterEmpty + Description: + 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", + "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: + Buffer player connect and disconnect events before a recording starts, then replay them at frame 0. Default: true + + Setting Name: + Buffer Player Connection Events + + Value Type: + Boolean + */ + [ + QEGVAR(settings,bufferPlayerConnectionEvents), + "CHECKBOX", + [ + "Buffer Player Connection Events", + "Buffer player connect and disconnect events before a recording starts, then replay them at frame 0. Default: true" + ], + [COMPONENT_NAME, "Auto-start Settings"], + true, + true, + { + params ["_value"]; + if (!_value && {!isNil QGVAR(connectedPlayerNamesBuffer)}) then { + GVAR(connectedPlayerNamesBuffer) = []; + }; + }, + false + ], + // Section: Core @@ -216,6 +296,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 a967ea6..4b56d9b 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(adminUIcontrol); diff --git a/addons/recorder/fnc_autoRestartMonitor.sqf b/addons/recorder/fnc_autoRestartMonitor.sqf new file mode 100644 index 0000000..162ea5d --- /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 (!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 a0e8210..6669b91 100644 --- a/addons/recorder/fnc_eh_connected.sqf +++ b/addons/recorder/fnc_eh_connected.sqf @@ -31,15 +31,9 @@ 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 immediately during an active session, otherwise preserve the event for +// frame 0 of the next recording. +["connected", _name, _uid] call FUNC(recordPlayerConnectionEvent); // trigger admin control check for all connecting players [_idstr, "connect"] call FUNC(adminUIcontrol); 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..7ed0ddc --- /dev/null +++ b/addons/recorder/fnc_flushPlayerConnectionEvents.sqf @@ -0,0 +1,38 @@ +/* ---------------------------------------------------------------------------- +FILE: fnc_flushPlayerConnectionEvents.sqf + +FUNCTION: OCAP_recorder_fnc_flushPlayerConnectionEvents + +Description: + Replays buffered player connection events into the newly registered recording + at frame 0, then clears the buffer. + +Returns: + Nothing + +Public: + No +---------------------------------------------------------------------------- */ + +#include "script_component.hpp" + +{ + _x params ["_eventType", "_name", "_uid"]; + + private _extraData = createHashMap; + + if (EGVAR(settings,includeSteamIdInPlayerConnectionEvents)) then { + _extraData set ["playerUid", _uid]; + }; + + private _eventData = [ + 0, + _eventType, + _name, + [_extraData] call CBA_fnc_encodeJSON + ]; + + [":EVENT:GENERAL:", _eventData] call EFUNC(extension,sendData); +} forEach GVAR(connectedPlayerNamesBuffer); + +GVAR(connectedPlayerNamesBuffer) = []; 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 44457f8..dc4081e 100644 --- a/addons/recorder/fnc_init.sqf +++ b/addons/recorder/fnc_init.sqf @@ -54,6 +54,15 @@ publicVariable QGVAR(captureFrameNo); */ GVAR(nextId) = 0; +/* + Connection events which occur before the extension is ready to receive a + recording are replayed at frame 0 when that recording starts. +*/ +GVAR(connectedPlayerNamesBuffer) = []; + +// 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 @@ -218,13 +227,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..de8ba71 --- /dev/null +++ b/addons/recorder/fnc_recordPlayerConnectionEvent.sqf @@ -0,0 +1,45 @@ +/* ---------------------------------------------------------------------------- +FILE: fnc_recordPlayerConnectionEvent.sqf + +FUNCTION: OCAP_recorder_fnc_recordPlayerConnectionEvent + +Description: + Records a player connection event immediately when a recording session is + available, or buffers it for frame 0 of the next recording. + +Parameters: + _eventType - "connected" or "disconnected" [String] + _name - Player name [String] + _uid - Player UID [String] + +Returns: + Nothing + +Public: + No +---------------------------------------------------------------------------- */ + +#include "script_component.hpp" + +params ["_eventType", "_name", "_uid"]; + +if (isNil QGVAR(startTime) || {!EGVAR(extension,sessionReady)}) exitWith { + if (EGVAR(settings,bufferPlayerConnectionEvents)) then { + GVAR(connectedPlayerNamesBuffer) pushBack [_eventType, _name, _uid]; + }; +}; + +private _extraData = createHashMap; + +if (EGVAR(settings,includeSteamIdInPlayerConnectionEvents)) then { + _extraData set ["playerUid", _uid]; +}; + +private _eventData = [ + GVAR(captureFrameNo), + _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); }; }; From fdb88445f1e96c60d2f6eab7cdf369ce06aa4877 Mon Sep 17 00:00:00 2001 From: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:58:14 +0200 Subject: [PATCH 2/2] Update addons/recorder/fnc_eh_connected.sqf Co-authored-by: thegamecracks <61257169+thegamecracks@users.noreply.github.com> --- addons/recorder/fnc_eh_connected.sqf | 3 --- 1 file changed, 3 deletions(-) diff --git a/addons/recorder/fnc_eh_connected.sqf b/addons/recorder/fnc_eh_connected.sqf index 0a074ac..d3f0d5d 100644 --- a/addons/recorder/fnc_eh_connected.sqf +++ b/addons/recorder/fnc_eh_connected.sqf @@ -32,6 +32,3 @@ if (_owner isEqualTo 2) exitWith {}; // Log immediately during an active session, otherwise preserve the event for // frame 0 of the next recording. ["connected", _name, _uid] call FUNC(recordPlayerConnectionEvent); - -// Trigger admin control check for all connecting players -[_idstr, "connect"] call FUNC(adminUIcontrol);