Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions addons/recorder/XEH_preInit.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions addons/recorder/XEH_prep.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ PREP(updateTime);

PREP(startRecording);
PREP(stopRecording);
PREP(autoRestartMonitor);
PREP(getAutoStartPlayerCount);
PREP(captureLoop);
PREP(isKindOfApc);
PREP(getClass);
Expand All @@ -18,6 +20,8 @@ PREP(addUnitEventHandlers);

PREP(eh_connected);
PREP(eh_disconnected);
PREP(recordPlayerConnectionEvent);
PREP(flushPlayerConnectionEvents);
PREP(eh_onUserAdminStateChanged);
PREP(eh_onUserSelectedPlayer);
PREP(adminUIcontrol);
Expand Down
29 changes: 29 additions & 0 deletions addons/recorder/fnc_autoRestartMonitor.sqf
Original file line number Diff line number Diff line change
@@ -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);
11 changes: 2 additions & 9 deletions addons/recorder/fnc_eh_connected.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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);
10 changes: 2 additions & 8 deletions addons/recorder/fnc_eh_disconnected.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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:", [
Expand All @@ -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);
};

Expand Down
23 changes: 23 additions & 0 deletions addons/recorder/fnc_flushPlayerConnectionEvents.sqf
Original file line number Diff line number Diff line change
@@ -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"));
26 changes: 26 additions & 0 deletions addons/recorder/fnc_getAutoStartPlayerCount.sqf
Original file line number Diff line number Diff line change
@@ -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
11 changes: 10 additions & 1 deletion addons/recorder/fnc_init.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
46 changes: 46 additions & 0 deletions addons/recorder/fnc_recordPlayerConnectionEvent.sqf
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 5 additions & 0 deletions addons/recorder/fnc_startRecording.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
};
};
Expand Down
43 changes: 43 additions & 0 deletions docs/recording-lifecycle.md
Original file line number Diff line number Diff line change
@@ -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.
Loading