From 1994d60007d4106009b646c947c60f7fbea364cd Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Sun, 22 Feb 2026 19:23:47 +0100 Subject: [PATCH 1/4] feat: replace mine projectile events with placed object lifecycle Mines and explosives now get dedicated :NEW:PLACED: and :PLACED:EVENT: commands instead of being sent as :PROJECTILE: events. Placed objects receive unique OCAP IDs and track Explode/Deleted events for detonation and removal, avoiding the double-send issue with the projectile path. --- addons/recorder/fnc_eh_fired_client.sqf | 86 ++++++++++++++++++++----- addons/recorder/fnc_eh_fired_server.sqf | 14 ++++ 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/addons/recorder/fnc_eh_fired_client.sqf b/addons/recorder/fnc_eh_fired_client.sqf index f4faa75..55cd71a 100644 --- a/addons/recorder/fnc_eh_fired_client.sqf +++ b/addons/recorder/fnc_eh_fired_client.sqf @@ -109,25 +109,79 @@ private _data = [ _projectile setVariable [QGVARMAIN(projectileData), _data]; -// carryover variables to submunitions -if ((_data select 17) isEqualTo "shotSubmunitions") then { - _projectile addEventHandler ["SubmunitionCreated", { - params ["_projectile", "_submunitionProjectile"]; - private _data = +(_projectile getVariable QGVARMAIN(projectileData)); - _data set [17, getText(configOf _submunitionProjectile >> "simulation")]; // actual sim type - _data set [18, true]; // isSub = true - (_data select 14) pushBack [ - diag_tickTime, - EGVAR(recorder,captureFrameNo), - (getPosASL _submunitionProjectile) joinString "," +// Handle placed objects (mines, explosives) — separate lifecycle from projectiles +if (_weapon == "put") then { + // Assign unique OCAP ID to placed object (same counter as soldiers/vehicles) + private _placedId = GVAR(nextId); + GVAR(nextId) = GVAR(nextId) + 1; + _projectile setVariable [QGVARMAIN(placedId), _placedId]; + _projectile setVariable [QGVARMAIN(detonated), false]; + + // Build :NEW:PLACED: data + private _placedData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + _placedId, // 1: placedId + typeOf _projectile, // 2: className + _data select 11, // 3: displayName (magazineDisplay) + (getPosASL _projectile) joinString ",", // 4: position + _firerOcapId, // 5: firerOcapId + str (side group _firer), // 6: side + _weapon, // 7: weapon + _data select 19 // 8: magazineIcon + ]; + + [QGVARMAIN(handlePlacedData), [_placedData]] call CBA_fnc_serverEvent; + + // Attach simplified EHs for placed object lifecycle + _projectile addEventHandler ["Explode", { + params ["_projectile", "_pos", "_velocity"]; + if (_projectile getVariable [QGVARMAIN(detonated), true]) exitWith {}; + _projectile setVariable [QGVARMAIN(detonated), true]; + private _placedId = _projectile getVariable [QGVARMAIN(placedId), -1]; + private _eventData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + _placedId, // 1: placedId + "detonated", // 2: eventType + _pos joinString "," // 3: position ]; - _submunitionProjectile setVariable [QGVARMAIN(projectileData), _data]; - // add the rest of EHs to submunition - [_submunitionProjectile] call FUNC(eh_fired_clientBullet); + [QGVARMAIN(handlePlacedEvent), [_eventData]] call CBA_fnc_serverEvent; + }]; + + _projectile addEventHandler ["Deleted", { + params ["_projectile"]; + // Only send "deleted" if not already detonated (avoid double-send) + if (_projectile getVariable [QGVARMAIN(detonated), true]) exitWith {}; + _projectile setVariable [QGVARMAIN(detonated), true]; + private _placedId = _projectile getVariable [QGVARMAIN(placedId), -1]; + private _eventData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + _placedId, // 1: placedId + "deleted", // 2: eventType + (getPosASL _projectile) joinString "," // 3: position + ]; + [QGVARMAIN(handlePlacedEvent), [_eventData]] call CBA_fnc_serverEvent; }]; } else { - // add the rest of EHs to projectile - [_projectile] call FUNC(eh_fired_clientBullet); + // carryover variables to submunitions + if ((_data select 17) isEqualTo "shotSubmunitions") then { + _projectile addEventHandler ["SubmunitionCreated", { + params ["_projectile", "_submunitionProjectile"]; + private _data = +(_projectile getVariable QGVARMAIN(projectileData)); + _data set [17, getText(configOf _submunitionProjectile >> "simulation")]; // actual sim type + _data set [18, true]; // isSub = true + (_data select 14) pushBack [ + diag_tickTime, + EGVAR(recorder,captureFrameNo), + (getPosASL _submunitionProjectile) joinString "," + ]; + _submunitionProjectile setVariable [QGVARMAIN(projectileData), _data]; + // add the rest of EHs to submunition + [_submunitionProjectile] call FUNC(eh_fired_clientBullet); + }]; + } else { + // add the rest of EHs to projectile + [_projectile] call FUNC(eh_fired_clientBullet); + }; }; true; diff --git a/addons/recorder/fnc_eh_fired_server.sqf b/addons/recorder/fnc_eh_fired_server.sqf index c19505b..eee6538 100644 --- a/addons/recorder/fnc_eh_fired_server.sqf +++ b/addons/recorder/fnc_eh_fired_server.sqf @@ -130,3 +130,17 @@ [":PROJECTILE:", _this] call EFUNC(extension,sendData); }; }] call CBA_fnc_addEventHandler; + +// Handle placed object creation events (mines, explosives) +[QGVARMAIN(handlePlacedData), { + params ["_data"]; + TRACE_1("Sending placed object data to extension",_data); + [":NEW:PLACED:", _data] call EFUNC(extension,sendData); +}] call CBA_fnc_addEventHandler; + +// Handle placed object lifecycle events (detonation, deletion) +[QGVARMAIN(handlePlacedEvent), { + params ["_data"]; + TRACE_1("Sending placed event data to extension",_data); + [":PLACED:EVENT:", _data] call EFUNC(extension,sendData); +}] call CBA_fnc_addEventHandler; From 83220d5e436b9c04482387d1a8d6af06663e8a18 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Sun, 22 Feb 2026 20:25:45 +0100 Subject: [PATCH 2/4] fix: assign placed object IDs on server where nextId exists The fired handler runs on the projectile owner (client/HC/server), but GVAR(nextId) is only initialized on the server. On clients it was undefined, producing "any" for placedId and cascading to -1 in Explode/Deleted event handlers. Move ID assignment to the server-side handlePlacedData CBA handler and broadcast the assigned ID back onto the projectile object. --- addons/recorder/fnc_eh_fired_client.sqf | 10 +++------- addons/recorder/fnc_eh_fired_server.sqf | 9 +++++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/addons/recorder/fnc_eh_fired_client.sqf b/addons/recorder/fnc_eh_fired_client.sqf index 55cd71a..9cf7098 100644 --- a/addons/recorder/fnc_eh_fired_client.sqf +++ b/addons/recorder/fnc_eh_fired_client.sqf @@ -111,16 +111,12 @@ _projectile setVariable [QGVARMAIN(projectileData), _data]; // Handle placed objects (mines, explosives) — separate lifecycle from projectiles if (_weapon == "put") then { - // Assign unique OCAP ID to placed object (same counter as soldiers/vehicles) - private _placedId = GVAR(nextId); - GVAR(nextId) = GVAR(nextId) + 1; - _projectile setVariable [QGVARMAIN(placedId), _placedId]; _projectile setVariable [QGVARMAIN(detonated), false]; - // Build :NEW:PLACED: data + // Build :NEW:PLACED: data — placedId assigned server-side (GVAR(nextId) only exists there) private _placedData = [ EGVAR(recorder,captureFrameNo), // 0: captureFrameNo - _placedId, // 1: placedId + -1, // 1: placedId (assigned by server) typeOf _projectile, // 2: className _data select 11, // 3: displayName (magazineDisplay) (getPosASL _projectile) joinString ",", // 4: position @@ -130,7 +126,7 @@ if (_weapon == "put") then { _data select 19 // 8: magazineIcon ]; - [QGVARMAIN(handlePlacedData), [_placedData]] call CBA_fnc_serverEvent; + [QGVARMAIN(handlePlacedData), [_placedData, _projectile]] call CBA_fnc_serverEvent; // Attach simplified EHs for placed object lifecycle _projectile addEventHandler ["Explode", { diff --git a/addons/recorder/fnc_eh_fired_server.sqf b/addons/recorder/fnc_eh_fired_server.sqf index eee6538..b45727c 100644 --- a/addons/recorder/fnc_eh_fired_server.sqf +++ b/addons/recorder/fnc_eh_fired_server.sqf @@ -132,9 +132,14 @@ }] call CBA_fnc_addEventHandler; // Handle placed object creation events (mines, explosives) +// ID assignment happens here because GVAR(nextId) only exists on the server [QGVARMAIN(handlePlacedData), { - params ["_data"]; - TRACE_1("Sending placed object data to extension",_data); + params ["_data", "_projectile"]; + private _placedId = GVAR(nextId); + GVAR(nextId) = GVAR(nextId) + 1; + _data set [1, _placedId]; + _projectile setVariable [QGVARMAIN(placedId), _placedId, true]; + TRACE_2("Sending placed object data to extension",_placedId,_data); [":NEW:PLACED:", _data] call EFUNC(extension,sendData); }] call CBA_fnc_addEventHandler; From 503710aba2bcfc26fd38e9638806cd4de21236b0 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Sun, 22 Feb 2026 22:44:10 +0100 Subject: [PATCH 3/4] feat: add HitExplosion tracking to placed objects Record which entities are hit by mine/explosive detonations. Sends a "hit" placed event per victim, filtered by _hitThings to avoid false positives from near-misses. --- addons/recorder/fnc_eh_fired_client.sqf | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/addons/recorder/fnc_eh_fired_client.sqf b/addons/recorder/fnc_eh_fired_client.sqf index 9cf7098..46fee20 100644 --- a/addons/recorder/fnc_eh_fired_client.sqf +++ b/addons/recorder/fnc_eh_fired_client.sqf @@ -129,6 +129,23 @@ if (_weapon == "put") then { [QGVARMAIN(handlePlacedData), [_placedData, _projectile]] call CBA_fnc_serverEvent; // Attach simplified EHs for placed object lifecycle + _projectile addEventHandler ["HitExplosion", { + params ["_projectile", "_hitEntity", "_projectileOwner", "_hitThings"]; + if (isNull _hitEntity) exitWith {}; + if (count _hitThings isEqualTo 0) exitWith {}; + private _hitOcapId = _hitEntity getVariable [QGVARMAIN(id), -1]; + if (_hitOcapId isEqualTo -1) exitWith {}; + private _placedId = _projectile getVariable [QGVARMAIN(placedId), -1]; + private _eventData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + _placedId, // 1: placedId + "hit", // 2: eventType + (getPosASL _hitEntity) joinString ",", // 3: position (victim pos) + _hitOcapId // 4: hitEntityOcapId + ]; + [QGVARMAIN(handlePlacedEvent), [_eventData]] call CBA_fnc_serverEvent; + }]; + _projectile addEventHandler ["Explode", { params ["_projectile", "_pos", "_velocity"]; if (_projectile getVariable [QGVARMAIN(detonated), true]) exitWith {}; From 34c15975f6c67d52133abd16b876288cc592ed43 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Sun, 22 Feb 2026 23:14:58 +0100 Subject: [PATCH 4/4] refactor: unify ACE3 explosives into placed object pipeline Replace marker-based tracking with the same :NEW:PLACED: / :PLACED:EVENT: pipeline used by vanilla mines, giving ACE3 explosives proper entity tracking, HitExplosion casualty recording, and web playback hit events. --- addons/recorder/fnc_aceExplosives.sqf | 130 ++++++++++++++------------ 1 file changed, 71 insertions(+), 59 deletions(-) diff --git a/addons/recorder/fnc_aceExplosives.sqf b/addons/recorder/fnc_aceExplosives.sqf index 7e37045..465cb2a 100644 --- a/addons/recorder/fnc_aceExplosives.sqf +++ b/addons/recorder/fnc_aceExplosives.sqf @@ -4,13 +4,17 @@ FILE: fnc_aceExplosives.sqf FUNCTION: OCAP_recorder_fnc_aceExplosives Description: - Adds marker on the mine's position to the recording timeline. - Uses Explode event handler to detect detonation and indicates it with a 10-frame long red X before removing the marker. + Integrates ACE3-placed explosives into the placed object pipeline. + Sends :NEW:PLACED: data and attaches lifecycle EHs (HitExplosion, Explode, + Deleted) identical to vanilla mines in fnc_eh_fired_client.sqf. Called by CBA listener. Parameters: - None + _explosive - Object: the placed explosive + _dir - Number: direction + _pitch - Number: pitch + _unit - Object: the unit that placed the explosive Returns: Nothing @@ -34,68 +38,76 @@ if (!SHOULDSAVEEVENTS) exitWith {}; params ["_explosive", "_dir", "_pitch", "_unit"]; -private _int = random(2000); - +// Resolve explosive metadata from config private _explType = typeOf _explosive; private _explosiveMag = getText(configFile >> "CfgAmmo" >> _explType >> "defaultMagazine"); private _explosiveDisp = getText(configFile >> "CfgMagazines" >> _explosiveMag >> "displayName"); private _explosivePic = getText(configFile >> "CfgMagazines" >> _explosiveMag >> "picture"); -private _placedPos = getPosASL _explosive; -_unit addOwnedMine _explosive; - -private _markTextLocal = format["%1", _explosiveDisp]; -private _markName = format["%1#%2/%3", QGVARMAIN(mine), _int, _placedPos]; - -// Signals creation of a Minefield (triangle) marker on the timeline at the location the explosive was armed. -[QGVARMAIN(handleMarker), [ - "CREATED", _markName, _unit, _placedPos, "Minefield", "ICON", [1,1], 0, "Solid", "ColorRed", 1, _markTextLocal, true -]] call CBA_fnc_localEvent; - -if (GVARMAIN(isDebug)) then { - private _debugArr = [_explosive, _explosivePic, format["%1 %2 - %3", str side group _unit, name _unit, _markTextLocal], [side group _unit] call BIS_fnc_sideColor]; - GVAR(liveDebugMagIcons) pushBack _debugArr; - publicVariable QGVAR(liveDebugMagIcons); -}; +// Get placer's OCAP ID +private _unitOcapId = _unit getVariable [QGVARMAIN(id), -1]; +if (_unitOcapId isEqualTo -1) exitWith {}; + +_explosive setVariable [QGVARMAIN(detonated), false]; + +// Build :NEW:PLACED: data — same format as vanilla mines in fnc_eh_fired_client.sqf +private _placedData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + -1, // 1: placedId (assigned by server) + _explType, // 2: className + _explosiveDisp, // 3: displayName + (getPosASL _explosive) joinString ",", // 4: position + _unitOcapId, // 5: firerOcapId + str (side group _unit), // 6: side + "put", // 7: weapon + _explosivePic // 8: magazineIcon +]; + +[QGVARMAIN(handlePlacedData), [_placedData, _explosive]] call CBA_fnc_serverEvent; + +// Attach lifecycle EHs — identical to vanilla path in fnc_eh_fired_client.sqf +_explosive addEventHandler ["HitExplosion", { + params ["_explosive", "_hitEntity", "_explosiveOwner", "_hitThings"]; + if (isNull _hitEntity) exitWith {}; + if (count _hitThings isEqualTo 0) exitWith {}; + private _hitOcapId = _hitEntity getVariable [QGVARMAIN(id), -1]; + if (_hitOcapId isEqualTo -1) exitWith {}; + private _placedId = _explosive getVariable [QGVARMAIN(placedId), -1]; + private _eventData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + _placedId, // 1: placedId + "hit", // 2: eventType + (getPosASL _hitEntity) joinString ",", // 3: position (victim pos) + _hitOcapId // 4: hitEntityOcapId + ]; + [QGVARMAIN(handlePlacedEvent), [_eventData]] call CBA_fnc_serverEvent; +}]; -// Use Explode event handler instead of polling for null _explosive addEventHandler ["Explode", { - params ["_explosive", "_damage", "_source", "_instigator"]; - - private _data = _explosive getVariable [QGVAR(explosiveData), []]; - if (_data isEqualTo []) exitWith {}; - - _data params ["_explosiveDisp", "_unit", "_placedPos", "_markName", "_int"]; - - if (GVARMAIN(isDebug)) then { - format["Removed explosive placed marker, %1, %2", _markName, _explosiveDisp] SYSCHAT; - OCAPEXTLOG(ARR3("Removed explosive placed marker",_markName,_explosiveDisp)); - }; - - // Signals removal of the Minefield (triangle) marker when the explosive detonates - [QGVARMAIN(handleMarker), ["DELETED", _markName]] call CBA_fnc_localEvent; - - private _detonationMarkName = format["Detonation#%1", _int]; - - if (GVARMAIN(isDebug)) then { - format["Created explosive explosion marker, %1, %2", _detonationMarkName, _explosiveDisp] SYSCHAT; - OCAPEXTLOG(ARR3("Created explosive explosion marker",_detonationMarkName,_explosiveDisp)); - }; - - // Signals creation of a Waypoint (X) marker on the timeline at the location the explosive detonated - [QGVARMAIN(handleMarker), [ - "CREATED", _detonationMarkName, _unit, _placedPos, "waypoint", "ICON", [1,1], 0, "Solid", "ColorRed", 1, format["%1", _explosiveDisp], true - ]] call CBA_fnc_localEvent; - - [{ - params ["_markName", "_explosiveDisp"]; - if (GVARMAIN(isDebug)) then { - format["Removed explosive explosion marker, %1, %2", _markName, _explosiveDisp] SYSCHAT; - OCAPEXTLOG(ARR3("Removed explosive explosion marker",_markName,_explosiveDisp)); - }; - [QGVARMAIN(handleMarker), ["DELETED", _markName]] call CBA_fnc_localEvent; - }, [_detonationMarkName, _explosiveDisp], GVAR(captureFrameNo) * 10] call CBA_fnc_waitAndExecute; + params ["_explosive", "_pos", "_velocity"]; + if (_explosive getVariable [QGVARMAIN(detonated), true]) exitWith {}; + _explosive setVariable [QGVARMAIN(detonated), true]; + private _placedId = _explosive getVariable [QGVARMAIN(placedId), -1]; + private _eventData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + _placedId, // 1: placedId + "detonated", // 2: eventType + _pos joinString "," // 3: position + ]; + [QGVARMAIN(handlePlacedEvent), [_eventData]] call CBA_fnc_serverEvent; }]; -// Store data on the explosive for retrieval in the event handler -_explosive setVariable [QGVAR(explosiveData), [_explosiveDisp, _unit, _placedPos, _markName, _int]]; +_explosive addEventHandler ["Deleted", { + params ["_explosive"]; + // Only send "deleted" if not already detonated (avoid double-send) + if (_explosive getVariable [QGVARMAIN(detonated), true]) exitWith {}; + _explosive setVariable [QGVARMAIN(detonated), true]; + private _placedId = _explosive getVariable [QGVARMAIN(placedId), -1]; + private _eventData = [ + EGVAR(recorder,captureFrameNo), // 0: captureFrameNo + _placedId, // 1: placedId + "deleted", // 2: eventType + (getPosASL _explosive) joinString "," // 3: position + ]; + [QGVARMAIN(handlePlacedEvent), [_eventData]] call CBA_fnc_serverEvent; +}];