From b2a43b5a6b90e6e976e28ddea28223627603465a Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Sun, 15 Mar 2026 12:48:24 +0100 Subject: [PATCH 1/2] fix: track static weapon shots bypassing FiredMan (ACE CSW compat) ACE Crew Served Weapons fires static weapons via forceWeaponFire on the vehicle, which may not trigger the unit's FiredMan EH that OCAP relies on. For mortars with allowFireOnLoad, ACE also creates a temporary AI agent as gunner when the seat is empty (solo mortar use), and this agent has no OCAP ID. Add a StaticWeapon Fired class EH as fallback that catches vehicle-level firing events, resolves the gunner, and forwards to the existing tracking handlers. When the gunner is an ACE CSW agent (no OCAP ID), falls back to ace_csw_reloader to attribute the shot to the actual player who loaded the weapon. A dedup guard in eh_fired_client prevents double-tracking when both FiredMan and the vehicle Fired EH fire for the same projectile. --- addons/recorder/fnc_eh_fired_client.sqf | 5 ++++ addons/recorder/fnc_eh_fired_server.sqf | 32 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/addons/recorder/fnc_eh_fired_client.sqf b/addons/recorder/fnc_eh_fired_client.sqf index ccb717b..5f0be4b 100644 --- a/addons/recorder/fnc_eh_fired_client.sqf +++ b/addons/recorder/fnc_eh_fired_client.sqf @@ -15,6 +15,11 @@ if (isNil "_projectile") exitWith { false; }; +// Dedup guard — when both FiredMan (unit) and Fired (vehicle) EHs fire for the +// same shot, only the first one to run should process. The second sees +// projectileData already set and exits. +if (!isNil {_projectile getVariable QGVARMAIN(projectileData)}) exitWith {false}; + // Zeus remote control fix: FiredMan fires on the controller's body, not the // controlled unit. bis_fnc_moduleRemoteControl_unit (local to the controller's // machine) gives us the actual unit doing the firing. diff --git a/addons/recorder/fnc_eh_fired_server.sqf b/addons/recorder/fnc_eh_fired_server.sqf index ef28eb2..54c8ff5 100644 --- a/addons/recorder/fnc_eh_fired_server.sqf +++ b/addons/recorder/fnc_eh_fired_server.sqf @@ -125,6 +125,38 @@ GVAR(trackedPlacedObjects) = createHashMap; // allow inheritance, don't exclude anything, and apply retroactively }, true, [], true] call CBA_fnc_addClassEventHandler; +// Fallback for static weapons whose shots bypass FiredMan on the gunner unit. +// ACE Crew Served Weapons (CSW) fires the vehicle weapon via forceWeaponFire, +// which may not trigger the unit's FiredMan EH. Additionally, for mortars with +// allowFireOnLoad, ACE creates a temporary AI agent as gunner when the seat is +// empty (solo mortar use) — this agent has no OCAP ID. +// The dedup guard in eh_fired_client prevents double-tracking when both fire. +["StaticWeapon", "Fired", { + params ["_vehicle", "_weapon", "_muzzle", "_mode", "_ammo", "_magazine", "_projectile"]; + + if (!local _vehicle) exitWith {}; + + // If FiredMan already handled this projectile, skip + if (!isNil {_projectile getVariable QGVARMAIN(projectileData)}) exitWith {}; + + private _firer = gunner _vehicle; + if (isNull _firer) exitWith {}; + + // ACE CSW agent fallback: when the gunner is a temporary agent (no OCAP ID), + // the actual operator is stored as ace_csw_reloader on the vehicle. + if ((_firer getVariable [QGVARMAIN(id), -1]) isEqualTo -1) then { + private _reloader = _vehicle getVariable ["ace_csw_reloader", objNull]; + if (!isNull _reloader) then { + _firer = _reloader; + }; + }; + + // Forward to existing handlers with FiredMan-compatible params + private _params = [_firer, _weapon, _muzzle, _mode, _ammo, _magazine, _projectile, _vehicle]; + _params call FUNC(eh_fired_client); + _params call FUNC(eh_firedMan); +}, true, [], true] call CBA_fnc_addClassEventHandler; + // Finally, we'll add a CBA Event Handler to take in the pre-processed fired data here on the server and send it to the extension. [QGVARMAIN(handleFiredManData), { From 17e9a04a4ed62e18258bbb44440ace1882829d4b Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Sun, 15 Mar 2026 12:59:40 +0100 Subject: [PATCH 2/2] fix: use Init+Local+remoteExec pattern for StaticWeapon Fired EH Address code review: the previous Fired class EH only ran on the server (CBA class EHs fire where registered), missing player-operated weapons entirely. Also dropped eh_firedMan call since it's server-only and would error on clients. Now follows the same Init + Local + remoteExec pattern used for CAManBase FiredMan: add the Fired EH on the owning machine during init, transfer it on locality changes. Only calls eh_fired_client (distributed to all clients) which already handles weapon attribution via broadcast. ACE CSW handles setShotParents for its own projectiles. --- addons/recorder/fnc_eh_fired_server.sqf | 81 +++++++++++++++++++------ 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/addons/recorder/fnc_eh_fired_server.sqf b/addons/recorder/fnc_eh_fired_server.sqf index 54c8ff5..bde15cc 100644 --- a/addons/recorder/fnc_eh_fired_server.sqf +++ b/addons/recorder/fnc_eh_fired_server.sqf @@ -131,30 +131,75 @@ GVAR(trackedPlacedObjects) = createHashMap; // allowFireOnLoad, ACE creates a temporary AI agent as gunner when the seat is // empty (solo mortar use) — this agent has no OCAP ID. // The dedup guard in eh_fired_client prevents double-tracking when both fire. -["StaticWeapon", "Fired", { - params ["_vehicle", "_weapon", "_muzzle", "_mode", "_ammo", "_magazine", "_projectile"]; +// +// Like FiredMan above, the Fired EH must live on the machine that owns the +// vehicle. We use the same Init + Local + remoteExec pattern: add on init, +// transfer on locality change. Only eh_fired_client is called (not eh_firedMan) +// because eh_firedMan is server-only and the EH may run on a client. +// eh_fired_client already handles weapon attribution (lastFired) via broadcast, +// and ACE CSW handles setShotParents for its own projectiles. +["StaticWeapon", "init", { + params ["_entity"]; - if (!local _vehicle) exitWith {}; + if (local _entity) then { + private _id = _entity addEventHandler ["Fired", { + params ["_vehicle", "_weapon", "_muzzle", "_mode", "_ammo", "_magazine", "_projectile"]; + if (!isNil {_projectile getVariable QGVARMAIN(projectileData)}) exitWith {}; + private _firer = gunner _vehicle; + if (isNull _firer) exitWith {}; + if ((_firer getVariable [QGVARMAIN(id), -1]) isEqualTo -1) then { + private _reloader = _vehicle getVariable ["ace_csw_reloader", objNull]; + if (!isNull _reloader) then { _firer = _reloader; }; + }; + [_firer, _weapon, _muzzle, _mode, _ammo, _magazine, _projectile, _vehicle] call FUNC(eh_fired_client); + }]; + _entity setVariable [QGVARMAIN(staticFiredEHExists), true]; + _entity setVariable [QGVARMAIN(staticFiredEH), _id]; + } else { + [_entity, { + private _id = _this addEventHandler ["Fired", { + params ["_vehicle", "_weapon", "_muzzle", "_mode", "_ammo", "_magazine", "_projectile"]; + if (!isNil {_projectile getVariable QGVARMAIN(projectileData)}) exitWith {}; + private _firer = gunner _vehicle; + if (isNull _firer) exitWith {}; + if ((_firer getVariable [QGVARMAIN(id), -1]) isEqualTo -1) then { + private _reloader = _vehicle getVariable ["ace_csw_reloader", objNull]; + if (!isNull _reloader) then { _firer = _reloader; }; + }; + [_firer, _weapon, _muzzle, _mode, _ammo, _magazine, _projectile, _vehicle] call FUNC(eh_fired_client); + }]; + _this setVariable [QGVARMAIN(staticFiredEHExists), true]; + _this setVariable [QGVARMAIN(staticFiredEH), _id]; + }] remoteExec ["call", owner _entity]; + }; - // If FiredMan already handled this projectile, skip - if (!isNil {_projectile getVariable QGVARMAIN(projectileData)}) exitWith {}; + _entity addEventHandler ["Local", { + params ["_entity", "_isLocal"]; + private _staticFiredEHExists = _entity getVariable [QGVARMAIN(staticFiredEHExists), false]; - private _firer = gunner _vehicle; - if (isNull _firer) exitWith {}; + if (!_isLocal && _staticFiredEHExists) then { + _entity removeEventHandler ["Fired", _entity getVariable QGVARMAIN(staticFiredEH)]; + _entity setVariable [QGVARMAIN(staticFiredEHExists), false]; + _entity setVariable [QGVARMAIN(staticFiredEH), nil]; + }; - // ACE CSW agent fallback: when the gunner is a temporary agent (no OCAP ID), - // the actual operator is stored as ace_csw_reloader on the vehicle. - if ((_firer getVariable [QGVARMAIN(id), -1]) isEqualTo -1) then { - private _reloader = _vehicle getVariable ["ace_csw_reloader", objNull]; - if (!isNull _reloader) then { - _firer = _reloader; + if (_isLocal && !_staticFiredEHExists) then { + private _id = _entity addEventHandler ["Fired", { + params ["_vehicle", "_weapon", "_muzzle", "_mode", "_ammo", "_magazine", "_projectile"]; + if (!isNil {_projectile getVariable QGVARMAIN(projectileData)}) exitWith {}; + private _firer = gunner _vehicle; + if (isNull _firer) exitWith {}; + if ((_firer getVariable [QGVARMAIN(id), -1]) isEqualTo -1) then { + private _reloader = _vehicle getVariable ["ace_csw_reloader", objNull]; + if (!isNull _reloader) then { _firer = _reloader; }; + }; + [_firer, _weapon, _muzzle, _mode, _ammo, _magazine, _projectile, _vehicle] call FUNC(eh_fired_client); + }]; + _entity setVariable [QGVARMAIN(staticFiredEHExists), true]; + _entity setVariable [QGVARMAIN(staticFiredEH), _id]; }; - }; + }]; - // Forward to existing handlers with FiredMan-compatible params - private _params = [_firer, _weapon, _muzzle, _mode, _ammo, _magazine, _projectile, _vehicle]; - _params call FUNC(eh_fired_client); - _params call FUNC(eh_firedMan); }, true, [], true] call CBA_fnc_addClassEventHandler;