From 987a0a948cbd9196c6552864ca30f4c0bcae5034 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Mon, 19 Oct 2026 22:46:17 +0200 Subject: [PATCH] feat(recorder): exclude markers by type New CBA setting OCAP_settings_excludeMarkerTypeFromRecord (stringified array of marker type classnames, default []) so missions can filter out marker types that are used as internal bookkeeping, e.g. zeus players placing Empty markers as readable location notes. Addresses OCAP2/OCAP#99. - MarkerCreated/MarkerUpdated handlers and the initial marker collection skip markers whose markerType is in the list, client-side, exactly like the existing name-prefix exclusion (no network traffic). - Server-side CREATED case applies the same check as defense in depth. - Marker deletions carry no type information and are intentionally not filtered: the server already drops deletions of markers it never tracked, so this keeps markers recorded before the exclusion was enabled deletable. - tests/recorder_regressions.sqf gains a section covering an excluded type, a kept type, the tracked-marker state and both deletions. --- addons/recorder/XEH_preInit.sqf | 28 +++++++++++++++++++ addons/recorder/fnc_handleMarkers.sqf | 30 +++++++++++++++++++- addons/recorder/fnc_init.sqf | 6 ++++ tests/recorder_regressions.sqf | 40 +++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/addons/recorder/XEH_preInit.sqf b/addons/recorder/XEH_preInit.sqf index 59c1334..6105bd3 100644 --- a/addons/recorder/XEH_preInit.sqf +++ b/addons/recorder/XEH_preInit.sqf @@ -286,6 +286,34 @@ GVAR(allSettings) = [ false // requires restart to apply ], + /* + CBA Setting: OCAP_settings_excludeMarkerTypeFromRecord + Description: + Array of marker type classnames. Any marker whose type matches one of these is excluded from recording. Use single quotes! Default: [] + + Setting Name: + Marker Types To Exclude + + Value Type: + Stringified Array + + Example: + > "['Empty']" + */ + [ + QEGVAR(settings,excludeMarkerTypeFromRecord), + "EDITBOX", // setting type + [ + "Marker Types to Exclude", // Pretty name shown inside the ingame settings menu. Can be stringtable entry. + "Array of marker type classnames. Any marker with one of these types is excluded from recording. Use single quotes! Default: []" + ], + [COMPONENT_NAME, "Exclusions"], // Pretty name of the category where the setting can be found. Can be stringtable entry. + "[]", // default string value + true, // "_isGlobal" flag. Set this to true to always have this setting synchronized between all clients in multiplayer + {}, // function that will be executed once on mission start and every time the setting is changed. + false // requires restart to apply + ], + /* CBA Setting: OCAP_settings_excludeVarNameFromRecord Description: diff --git a/addons/recorder/fnc_handleMarkers.sqf b/addons/recorder/fnc_handleMarkers.sqf index f7af013..31e5151 100644 --- a/addons/recorder/fnc_handleMarkers.sqf +++ b/addons/recorder/fnc_handleMarkers.sqf @@ -12,7 +12,7 @@ Description: Due to the nature of locality and single-view playback, markers of the same name which exist in different states on different clients may display odd behavior during playback. - Marker exclusions as configured in are handled client-side to avoid unnecessary network traffic. + Marker exclusions as configured in and are handled client-side to avoid unnecessary network traffic. This will also wait until the mission proceeds past the briefing screen, then gather all existing markers and send them to the server for entry onto the Editor/Briefing Markers layer during playback. @@ -41,6 +41,14 @@ Author: GVAR(trackedMarkers) = []; // Markers which we save into replay GVAR(trackedMarkerStates) = createHashMap; +// VARIABLE: OCAP_recorder_excludeMarkerTypeList +// Parsed list of marker type classnames excluded from recording, as configured in . +GVAR(excludeMarkerTypeList) = if (!isNil QEGVAR(settings,excludeMarkerTypeFromRecord)) then { + parseSimpleArray EGVAR(settings,excludeMarkerTypeFromRecord) +} else { + [] +}; + // VARIABLE: OCAP_listener_markers // Contains handle for CBA event handler. @@ -66,6 +74,14 @@ EGVAR(listener,markers) = [QGVARMAIN(handleMarker), { }; }; + // A marker whose type is excluded (for example "Empty" zeus location markers) + // is not recorded, so no delete event for it needs to be sent either. + if (_type in GVAR(excludeMarkerTypeList)) exitWith { + if (GVARMAIN(isDebug)) then { + OCAPEXTLOG(ARR4("MARKER:CREATE: Marker",_mrk_name,"excluded by type",_type)); + }; + }; + if (GVARMAIN(isDebug)) then { format["CREATE:MARKER: Valid CREATED process of %1, sending to extension", _mrk_name] SYSCHAT; OCAPEXTLOG(ARR3("CREATE:MARKER: Valid CREATED process of",_mrk_name,", sending to extension")); @@ -204,6 +220,10 @@ EGVAR(listener,markers) = [QGVARMAIN(handleMarker), { _isExcluded = true; }; } forEach GVAR(excludeMarkerList); + // check for an excluded marker type, e.g. the "Empty" type of zeus location markers + if ((markerType _marker) in GVAR(excludeMarkerTypeList)) then { + _isExcluded = true; + }; if (_isExcluded) exitWith {}; private _event = _this; @@ -255,6 +275,10 @@ EGVAR(listener,markers) = [QGVARMAIN(handleMarker), { _isExcluded = true; }; } forEach GVAR(excludeMarkerList); + // check for an excluded marker type, e.g. the "Empty" type of zeus location markers + if ((markerType _marker) in GVAR(excludeMarkerTypeList)) then { + _isExcluded = true; + }; if (_isExcluded) exitWith {}; private _pos = ATLToASL (markerPos [_marker, true]); @@ -327,6 +351,10 @@ EGVAR(listener,markers) = [QGVARMAIN(handleMarker), { _isExcluded = true; }; } forEach GVAR(excludeMarkerList); + // check for an excluded marker type, e.g. the "Empty" type of zeus location markers + if ((markerType _marker) in GVAR(excludeMarkerTypeList)) then { + _isExcluded = true; + }; if (_isExcluded) then {continue}; diff --git a/addons/recorder/fnc_init.sqf b/addons/recorder/fnc_init.sqf index 3106be4..bf6f417 100644 --- a/addons/recorder/fnc_init.sqf +++ b/addons/recorder/fnc_init.sqf @@ -100,6 +100,12 @@ GVAR(excludeMarkerList) = if (!isNil QEGVAR(settings,excludeMarkerFromRecord)) t [] }; +GVAR(excludeMarkerTypeList) = if (!isNil QEGVAR(settings,excludeMarkerTypeFromRecord)) then { + parseSimpleArray EGVAR(settings,excludeMarkerTypeFromRecord) +} else { + [] +}; + // Lowercased once here so lookups in the capture loop can be case-insensitive, // matching how class/kind exclusions behave. GVAR(excludeVarNameList) = if (!isNil QEGVAR(settings,excludeVarNameFromRecord)) then { diff --git a/tests/recorder_regressions.sqf b/tests/recorder_regressions.sqf index 0aab700..26488f5 100644 --- a/tests/recorder_regressions.sqf +++ b/tests/recorder_regressions.sqf @@ -290,6 +290,46 @@ try { (OCAP_test_wait select 2) call (OCAP_test_wait select 1); (OCAP_test_wait select 2) call (OCAP_test_wait select 4); [OCAP_test_commands isEqualTo [], "export then start with reused ids emits no fictitious servicing, disassembly or queued assembly"] call _assert; + + // Marker type exclusion (OCAP2/OCAP#99): a marker of an excluded type is + // never recorded; a marker of another type still is. The type list is read + // at event time, so assigning it directly is enough. + OCAP_recorder_excludeMarkerTypeList = ["Empty"]; + OCAP_test_marker_commands = []; + private _origMarkerSend = OCAP_extension_fnc_sendData; + OCAP_extension_fnc_sendData = { + params ["_command", ["_args", []]]; + private _result = call _origMarkerSend; + if (_command in [":MARKER:CREATE:", ":MARKER:DELETE:"]) then { + OCAP_test_marker_commands pushBack +_this; + }; + _result + }; + createMarker ["Empty", [500, 500, 0], "mk_type_excluded"]; + createMarker ["mil_dot", [510, 510, 0], "mk_type_kept"]; + // Marker creations are forwarded after a two second delay. + private _markerDeadline = diag_tickTime + 15; + waitUntil { + sleep 0.1; + ({"mk_type_kept" in str _x} count OCAP_test_marker_commands) > 0 || {diag_tickTime > _markerDeadline}; + }; + [({"mk_type_kept" in str _x} count OCAP_test_marker_commands) == 1, "marker of a non-excluded type is recorded"] call _assert; + [({"mk_type_excluded" in str _x} count OCAP_test_marker_commands) == 0, "marker of an excluded type is not recorded"] call _assert; + [("mk_type_kept" in OCAP_recorder_trackedMarkers) && {!("mk_type_excluded" in OCAP_recorder_trackedMarkers)}, "tracked marker list reflects the type exclusion"] call _assert; + // Deletions carry no type information; the server drops deletions of markers + // it never tracked, so both deletions are accepted as no-ops where needed. + deleteMarker "mk_type_kept"; + deleteMarker "mk_type_excluded"; + _markerDeadline = diag_tickTime + 5; + waitUntil { + sleep 0.05; + ({"mk_type_kept" in str _x} count OCAP_test_marker_commands) > 1 || {diag_tickTime > _markerDeadline}; + }; + [({"mk_type_kept" in str _x} count OCAP_test_marker_commands) == 2, "deletion of a tracked marker is recorded"] call _assert; + [!("mk_type_excluded" in OCAP_recorder_trackedMarkers), "deleted excluded marker stays untracked"] call _assert; + OCAP_extension_fnc_sendData = _origMarkerSend; + OCAP_recorder_excludeMarkerTypeList = []; + OCAP_test_marker_commands = []; } catch { _error = _exception; };