From 794f2ac26ac6c5492c8e7a4551305b1b56a862d3 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:59:19 -0300 Subject: [PATCH 1/9] feat: add CSW magazine source and compatibility lookups Adds three helpers the reload rework needs: - getNearbySources: nearSupplies within DISTANCE_SEARCH_RADIUS, filtered to friendly non-player owners, with men expanded into their containers and then dropped. magazineCargo does not read a man's inventory, so a gunner's own magazines were invisible to every caller that passed him straight in. Cached per source object via common_fnc_cachedCall; the cache uid covers every argument, since different flags give different results. - getSourceCompatibleMagazines: the magazines in one source that a given CSW can actually take, grouped so the fullest of each type comes first. - compatibleMagazines: a CSW's compatible carry magazines, including weapons added by script. compatibleMagazines also owns the two config caches, filled lazily on first lookup. Populating them on weapon swap would leave every CSW without a proxy weapon with an empty cache, and so with no load actions at all. --- addons/csw/XEH_PREP.hpp | 3 + addons/csw/XEH_postInit.sqf | 1 - addons/csw/XEH_preInit.sqf | 6 ++ .../csw/functions/fnc_compatibleMagazines.sqf | 58 +++++++++++++ addons/csw/functions/fnc_getNearbySources.sqf | 81 +++++++++++++++++++ .../fnc_getSourceCompatibleMagazines.sqf | 38 +++++++++ addons/csw/script_component.hpp | 5 ++ 7 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 addons/csw/functions/fnc_compatibleMagazines.sqf create mode 100644 addons/csw/functions/fnc_getNearbySources.sqf create mode 100644 addons/csw/functions/fnc_getSourceCompatibleMagazines.sqf diff --git a/addons/csw/XEH_PREP.hpp b/addons/csw/XEH_PREP.hpp index 68eb7f202d3..801704d450f 100644 --- a/addons/csw/XEH_PREP.hpp +++ b/addons/csw/XEH_PREP.hpp @@ -29,7 +29,10 @@ PREP(assemble_startDeployTripod); PREP(canGetIn); PREP(deployCancel); PREP(deployConfirm); +PREP(compatibleMagazines); PREP(getCarryMagazine); +PREP(getNearbySources); +PREP(getSourceCompatibleMagazines); PREP(handleScrollWheel); PREP(proxyWeapon); diff --git a/addons/csw/XEH_postInit.sqf b/addons/csw/XEH_postInit.sqf index a24b7390a4b..f018d911ff3 100644 --- a/addons/csw/XEH_postInit.sqf +++ b/addons/csw/XEH_postInit.sqf @@ -1,6 +1,5 @@ #include "script_component.hpp" -GVAR(vehicleMagCache) = createHashMap; GVAR(deployPFH) = -1; ["CBA_settingsInitialized", { diff --git a/addons/csw/XEH_preInit.sqf b/addons/csw/XEH_preInit.sqf index 2cef0dfd2cb..2d44b0c66c4 100644 --- a/addons/csw/XEH_preInit.sqf +++ b/addons/csw/XEH_preInit.sqf @@ -10,5 +10,11 @@ PREP_RECOMPILE_END; GVAR(initializedStaticTypes) = []; +// Config-derived, so they hold for the whole session. Filled lazily by FUNC(compatibleMagazines) and +// FUNC(getCarryMagazine) rather than on weapon swap, a CSW without a proxy weapon needs them too +GVAR(vehicleMagCache) = createHashMap; +GVAR(compatibleCarryMagsCache) = createHashMap; +GVAR(compatibleVehicleMagsCache) = createHashMap; + ADDON = true; diff --git a/addons/csw/functions/fnc_compatibleMagazines.sqf b/addons/csw/functions/fnc_compatibleMagazines.sqf new file mode 100644 index 00000000000..e581d53586a --- /dev/null +++ b/addons/csw/functions/fnc_compatibleMagazines.sqf @@ -0,0 +1,58 @@ +#include "..\script_component.hpp" +/* + * Author: LinkIsGrim + * Gets all carry magazines that can be loaded into a CSW, includes weapons added by script. + * + * Arguments: + * 0: CSW + * + * Return Value: + * Compatible Carry Magazines + * Magazine classname + * true + * + * Example: + * [cursorObject] call ace_csw_fnc_compatibleMagazines + * + * Public: Yes + */ + +params [["_csw", objNull, [objNull]]]; + +if !((typeOf _csw) in GVAR(initializedStaticTypes)) exitWith {createHashMap}; + +// Caches are filled here rather than on weapon swap, a CSW without a proxy weapon needs them too +private _fnc_cacheWeapon = { + private _weapon = _this; + + GVAR(compatibleCarryMagsCache) getOrDefaultCall [_weapon, { + // Engine command, not this function + private _vehicleMags = compatibleMagazines _weapon; + GVAR(compatibleVehicleMagsCache) set [_weapon, _vehicleMags]; + + // Vehicle magazines without a carry equivalent come back as "", they can't be loaded by hand + private _carryMags = (_vehicleMags apply {_x call FUNC(getCarryMagazine)}) select {_x != ""}; + + _carryMags createHashMapFromArray (_carryMags apply {true}) + }, true] +}; + +private _weapons = []; +{ + private _turret = _x; + { + _weapons pushBackUnique _x; + } forEach (_csw weaponsTurret _turret); +} forEach (allTurrets _csw); + +if (_weapons isEqualTo []) exitWith {createHashMap}; + +// Copies, so callers can't edit the cache. Single weapon is by far the most common CSW, skip the merge +if (count _weapons isEqualTo 1) exitWith {+((_weapons select 0) call _fnc_cacheWeapon)}; + +private _carryMagazines = createHashMap; +{ + _carryMagazines merge [_x call _fnc_cacheWeapon, true]; +} forEach _weapons; + +_carryMagazines // return diff --git a/addons/csw/functions/fnc_getNearbySources.sqf b/addons/csw/functions/fnc_getNearbySources.sqf new file mode 100644 index 00000000000..2c289aed5f6 --- /dev/null +++ b/addons/csw/functions/fnc_getNearbySources.sqf @@ -0,0 +1,81 @@ +#include "..\script_component.hpp" +/* + * Author: LinkIsGrim + * Gets available ammo sources for loading a CSW. Units are replaced by their containers, since + * magazineCargo does not read a unit's inventory. + * + * Arguments: + * 0: Unit or vehicle attempting to load + * 1: Skip vehicle sources (default: false) + * 2: Include crew of the source (default: false) + * + * Return Value: + * Ammo sources + * + * Example: + * [player] call ace_csw_fnc_getNearbySources + * + * Public: No + */ + +params ["_unit", ["_skipVehicles", false], ["_includeCrew", false]]; + +// Normalised so an omitted default and an explicit one hit the same cache entry +private _params = [_unit, _skipVehicles, _includeCrew]; + +[ + _params, + { + params ["_unit", "_skipVehicles", "_includeCrew"]; + + // group is grpNull on a vehicle, so a CSW passed straight in has to be asked directly + private _side = if (_unit isKindOf "CAManBase") then {side group _unit} else {side _unit}; + + // group is also grpNull on crates and weapon holders, which is what lets them through + private _nearSupplies = (_unit nearSupplies DISTANCE_SEARCH_RADIUS) select { + isNull (group _x) || + {!([_x] call EFUNC(common,isPlayer)) && {[_side, side group _x] call BIS_fnc_sideIsFriendly}} + }; + + if (_includeCrew) then { + _nearSupplies append (crew _unit); + }; + + if (_skipVehicles) then { + _nearSupplies = _nearSupplies select { + private _source = _x; + (["Ship", "Car", "Air", "Tank"] findIf {_source isKindOf _x}) == -1 + }; + }; + + _nearSupplies pushBackUnique _unit; + + // Built into a second array rather than appended in place, forEach over an array that is + // growing underneath it has no defined behaviour + private _sources = []; + { + // magazineCargo does not read a unit's inventory, only their containers do + if (_x isKindOf "CAManBase") then { + { + _sources pushBackUnique _x; + } forEach [uniformContainer _x, vestContainer _x, backpackContainer _x]; + continue; + }; + + _sources pushBackUnique _x; + + { + _x params ["", "_container"]; + _sources pushBackUnique _container; + } forEach (everyContainer _x); + } forEach _nearSupplies; + + // A unit with no vest or backpack gives objNull back + _sources select {!isNull _x} // return + }, + _unit, + // The uid has to cover every argument, cachedCall hands it to setVariable so it must be a string + format [QGVAR(nearbySourcesCache_%1), hashValue _params], + NEARBY_SOURCES_CACHE_EXPIRY, + QGVAR(clearNearbySourcesCache) +] call EFUNC(common,cachedCall) diff --git a/addons/csw/functions/fnc_getSourceCompatibleMagazines.sqf b/addons/csw/functions/fnc_getSourceCompatibleMagazines.sqf new file mode 100644 index 00000000000..05475213838 --- /dev/null +++ b/addons/csw/functions/fnc_getSourceCompatibleMagazines.sqf @@ -0,0 +1,38 @@ +#include "..\script_component.hpp" +/* + * Author: LinkIsGrim + * Gets compatible magazines to load a CSW from a magazine source. + * + * Arguments: + * 0: Magazine Source (default: objNull) + * 1: CSW (default: objNull) + * + * Return Value: + * Magazines, grouped by classname, fullest of each type first + * Magazine classname + * Magazine ammo + * + * Example: + * [backpackContainer player, cursorObject] call ace_csw_fnc_getSourceCompatibleMagazines + * + * Public: Yes + */ + +params [["_source", objNull, [objNull]], ["_csw", objNull, [objNull]]]; + +if (isNull _source || {!alive _csw}) exitWith {[]}; + +private _magazines = magazinesAmmoCargo _source; +if (_magazines isEqualTo []) exitWith {[]}; + +private _compatibleMagazines = [_csw] call FUNC(compatibleMagazines); +if (_compatibleMagazines isEqualTo createHashMap) exitWith {[]}; + +private _return = _magazines select {(_x select 0) in _compatibleMagazines}; + +// Sorting an array of [classname, ammo] compares the classname first, so this groups by type and +// puts the fullest magazine of each type at the front of its group. Callers dedupe on classname and +// take the first hit, which is why they never have to scan a source twice +_return sort false; + +_return // return diff --git a/addons/csw/script_component.hpp b/addons/csw/script_component.hpp index c43fa20b494..9f7a5ecbe07 100644 --- a/addons/csw/script_component.hpp +++ b/addons/csw/script_component.hpp @@ -17,6 +17,11 @@ #include "\z\ace\addons\main\script_macros.hpp" +#define GET_NUMBER(config,default) (if (isNumber (config)) then {getNumber (config)} else {default}) + +// How far a magazine source can be from a CSW and still be loadable from +#define DISTANCE_SEARCH_RADIUS 5 +#define NEARBY_SOURCES_CACHE_EXPIRY 5 #define DISTANCE_FROM_GUN 1.5 #define RELATIVE_DIRECTION(direction) [DISTANCE_FROM_GUN, direction] From d391de8ad3b5dac810be34a237c9faebb8526b87 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:02:42 -0300 Subject: [PATCH 2/9] refactor: source CSW loadable magazines from the new lookups reload_getLoadableMagazines now takes its sources from getNearbySources and its magazines from getSourceCompatibleMagazines instead of walking nearSupplies and the magazine groups config itself. It gains an AI flag that restricts the scan to the unit's own turret, and returns the chosen magazine's ammo count so callers stop recomputing it. The unit was also being returned as element 4 despite being an argument the caller already had; that slot now holds the ammo count. Whether a source is usable moves out of reload_canLoadMagazine into reload_canUseSource, leaving the former to answer only whether a magazine fits a turret. A null source passes, which is all reload_getLoadableMagazines needs once it has picked one. That check also drops from 10m to DISTANCE_SEARCH_RADIUS and falls back to objectParent, so a container nested inside another container is measured by the position of whatever is actually on the ground. It replaces a blanket skip for Bag_Base and ContainerSupply that also skipped verifying the source held the magazine at all. reload_getVehicleMagazine reads the cached compatible magazines rather than querying the engine per weapon per call. getCarryMagazine keys its cache on toLowerANSI, since config lookups are case-insensitive but hashmap keys are not. --- addons/csw/XEH_PREP.hpp | 1 + addons/csw/functions/fnc_getCarryMagazine.sqf | 16 ++-- .../functions/fnc_reload_canLoadMagazine.sqf | 13 +-- .../csw/functions/fnc_reload_canUseSource.sqf | 41 +++++++++ .../fnc_reload_getLoadableMagazines.sqf | 88 +++++++++---------- .../fnc_reload_getVehicleMagazine.sqf | 9 +- 6 files changed, 102 insertions(+), 66 deletions(-) create mode 100644 addons/csw/functions/fnc_reload_canUseSource.sqf diff --git a/addons/csw/XEH_PREP.hpp b/addons/csw/XEH_PREP.hpp index 801704d450f..a664b6fb61e 100644 --- a/addons/csw/XEH_PREP.hpp +++ b/addons/csw/XEH_PREP.hpp @@ -40,6 +40,7 @@ PREP(getLoadActions); PREP(getUnloadActions); PREP(reload_canLoadMagazine); PREP(reload_canUnloadMagazine); +PREP(reload_canUseSource); PREP(reload_getLoadableMagazines); PREP(reload_getVehicleMagazine); PREP(reload_handleAddTurretMag); diff --git a/addons/csw/functions/fnc_getCarryMagazine.sqf b/addons/csw/functions/fnc_getCarryMagazine.sqf index 3d94ca2fe1c..5f50eaece4f 100644 --- a/addons/csw/functions/fnc_getCarryMagazine.sqf +++ b/addons/csw/functions/fnc_getCarryMagazine.sqf @@ -15,14 +15,16 @@ * Public: No */ -params ["_vehicleMag"]; +params [["_vehicleMag", "", [""]]]; -private _carryMag = GVAR(vehicleMagCache) get _vehicleMag; -if (isNil "_carryMag") then { +if (_vehicleMag == "") exitWith {""}; + +// Config lookups are case-insensitive but hashmap keys are not, so normalise before caching. +// A magazine with no carry equivalent caches "" on purpose, so the miss costs nothing to look up again +GVAR(vehicleMagCache) getOrDefaultCall [toLowerANSI _vehicleMag, { private _groups = "getNumber (_x >> _vehicleMag) == 1 && {isClass (configFile >> 'CfgMagazines' >> configName _x)}" configClasses (configFile >> QGVAR(groups)); - _carryMag = configName (_groups param [0, configNull]); - GVAR(vehicleMagCache) set [_vehicleMag, _carryMag]; + private _carryMag = configName (_groups param [0, configNull]); TRACE_2("setting cache",_vehicleMag,_carryMag); -}; -_carryMag + _carryMag +}, true] // return diff --git a/addons/csw/functions/fnc_reload_canLoadMagazine.sqf b/addons/csw/functions/fnc_reload_canLoadMagazine.sqf index d4049706bdb..9b9d7def5f5 100644 --- a/addons/csw/functions/fnc_reload_canLoadMagazine.sqf +++ b/addons/csw/functions/fnc_reload_canLoadMagazine.sqf @@ -19,21 +19,14 @@ */ params ["_vehicle", "_turret", "_carryMag", ["_magSource", objNull]]; -// TRACE_4("reload_canLoadMagazine",_vehicle,_turret,_carryMag,_magSource); +TRACE_4("reload_canLoadMagazine",_vehicle,_turret,_carryMag,_magSource); private _return = [false, "", -2, false]; // Handle disassembled or deleted if (!alive _vehicle) exitWith { _return }; -// Verify holder has carry magazine -if ( - (!isNull _magSource) && - {!((_magSource isKindOf "Bag_Base") || {_magSource isKindOf "ContainerSupply"})} && // Hacky workaround for magazines within dropped backpacks - { - ((_vehicle distance _magSource) > 10) || - {((magazineCargo _magSource) findIf {_x == _carryMag}) == -1} - } -) exitWith { _return }; + +if !([_vehicle, _carryMag, _magSource] call FUNC(reload_canUseSource)) exitWith { _return }; // solve config lookups private _cfgMagazines = configFile >> "CfgMagazines"; diff --git a/addons/csw/functions/fnc_reload_canUseSource.sqf b/addons/csw/functions/fnc_reload_canUseSource.sqf new file mode 100644 index 00000000000..8143ad533c0 --- /dev/null +++ b/addons/csw/functions/fnc_reload_canUseSource.sqf @@ -0,0 +1,41 @@ +#include "..\script_component.hpp" +/* + * Author: LinkIsGrim + * Tests whether a magazine source is close enough to a CSW and still holds the magazine. + * + * A null source passes. Callers that picked their source from FUNC(getNearbySources) already know + * both, and only want to know whether the magazine fits the turret. + * + * Arguments: + * 0: CSW + * 1: Carryable Magazine + * 2: Magazine source (default: objNull) + * + * Return Value: + * Source is usable + * + * Example: + * [cursorObject, "ACE_csw_100Rnd_127x99_mag_red", backpackContainer player] call ace_csw_fnc_reload_canUseSource + * + * Public: No + */ + +params ["_vehicle", "_carryMag", ["_magSource", objNull]]; + +if (isNull _magSource) exitWith {true}; + +if !(_carryMag in (magazineCargo _magSource)) exitWith { + TRACE_2("source does not have carry mag",_magSource,_carryMag); + false +}; + +// objectParent covers a container inside another container, whose own position is not meaningful +if ( + ((_vehicle distance _magSource) > DISTANCE_SEARCH_RADIUS) && + {(_vehicle distance (objectParent _magSource)) > DISTANCE_SEARCH_RADIUS} +) exitWith { + TRACE_2("source too far",_vehicle,_magSource); + false +}; + +true diff --git a/addons/csw/functions/fnc_reload_getLoadableMagazines.sqf b/addons/csw/functions/fnc_reload_getLoadableMagazines.sqf index aa49d0284b5..41caffed3fb 100644 --- a/addons/csw/functions/fnc_reload_getLoadableMagazines.sqf +++ b/addons/csw/functions/fnc_reload_getLoadableMagazines.sqf @@ -1,15 +1,16 @@ #include "..\script_component.hpp" /* - * Author: PabstMirror + * Author: PabstMirror, LinkIsGrim * Gets nearby magazines that can be loaded into the CSW. * * Arguments: * 0: CSW * 1: Unit + * 2: AI reloading, only check the unit's own turret (default: false) * * Return Value: * Mags - * [Carry Magazine , Turret Path , Load Info , Magazine Source ] + * [Carry Magazine , Turret Path , Load Info , Magazine Source , Magazine Ammo ] * * Example: * [cursorObject, player] call ace_csw_fnc_reload_getLoadableMagazines @@ -17,62 +18,57 @@ * Public: No */ -params ["_vehicle", "_player"]; +params ["_vehicle", "_unit", ["_aiReload", false]]; -private _magGroupsConfig = configFile >> QGVAR(groups); // so we don't solve in loop every time -private _availableMagazines = createHashMap; // slower than array, still needed for setting source of magazine +// Hashmap rather than an array, the source each magazine came from has to be carried along +private _availableMagazines = createHashMap; -// filter enemy & player units while allowing pulling from friendly AI, crates, etc -private _nearSupplies = ((_vehicle nearSupplies 10) select { - isNull (group _x) || - {!([_x] call EFUNC(common,isPlayer)) && {[side group _player, side group _x] call BIS_fnc_sideIsFriendly}} -}); - -// backpacks/uniforms/etc need to be added manually. -// array can't be modified while iterating, use copy { + private _xSource = _x; + private _handledSourceMags = []; + { - _x params ["_classname", "_container"]; - _nearSupplies pushBack _container; - } forEach (everyContainer _x); -} forEach ((+_nearSupplies) select {(everyContainer _x) isNotEqualTo []}); + _x params ["_classname", "_ammo"]; -// add caller to list of sources -_nearSupplies = [_player] + _nearSupplies; + // Sources come back grouped by classname with the fullest first, so only the first counts + if (_classname in _handledSourceMags) then {continue}; + _handledSourceMags pushBack _classname; -{ - private _xSource = _x; - private _mags = magazineCargo _xSource; + // Across sources, take whichever holds the fullest magazine of this type + if (_ammo > ((_availableMagazines getOrDefault [_classname, [objNull, 0]]) select 1)) then { + _availableMagazines set [_classname, [_xSource, _ammo]]; + }; + } forEach ([_xSource, _vehicle] call FUNC(getSourceCompatibleMagazines)); +} forEach ([_unit] call FUNC(getNearbySources)); - { - _availableMagazines set [_x, _xSource]; - } forEach (_mags select {isClass (_magGroupsConfig >> _x)}); -} forEach _nearSupplies; +if (_availableMagazines isEqualTo createHashMap) exitWith {[]}; // fast exit if no available mags + +// AI only ever reloads the turret it is sitting in, no reason to walk the rest. +// unitTurret gives [] when nobody is in a turret and [-1] when the gunner is also the driver +private _allTurrets = allTurrets _vehicle; +private _turrets = _allTurrets; -if (_availableMagazines isEqualTo createHashMap) exitWith { [] }; // fast exit if no available mags +if (_aiReload) then { + private _turretPath = _vehicle unitTurret _unit; + _turrets = [[0], _turretPath] select (_turretPath in _allTurrets); + _turrets = [_turrets]; +}; -private _loadInfo = []; private _return = []; -// Go through turrets and find weapons that we could reload + { private _turretPath = _x; { - private _weapon = _x; - { - private _carryMag = _x; - private _magSource = _y; - private _carryGroup = _magGroupsConfig >> _carryMag; - { - if ( - ((getNumber (_carryGroup >> _x)) == 1) && - {_loadInfo = [_vehicle, _turretPath, _carryMag, _magSource] call FUNC(reload_canLoadMagazine); _loadInfo select 0} - ) exitWith { - _return pushBack [_carryMag, _turretPath, _loadInfo, _magSource]; - }; - } forEach (compatibleMagazines _weapon); - } forEach _availableMagazines; - } forEach (_vehicle weaponsTurret _turretPath); -} forEach (allTurrets _vehicle); -// Note: these nested forEach's looks terrible, but most only have one element + //IGNORE_PRIVATE_WARNING ["_x", "_y"]; + private _carryMag = _x; + _y params ["_magSource", "_ammo"]; + + // No source passed, the source was picked above and is known to hold the magazine + private _loadInfo = [_vehicle, _turretPath, _carryMag] call FUNC(reload_canLoadMagazine); + if (_loadInfo select 0) then { + _return pushBack [_carryMag, _turretPath, _loadInfo, _magSource, _ammo]; + }; + } forEach _availableMagazines; +} forEach _turrets; _return diff --git a/addons/csw/functions/fnc_reload_getVehicleMagazine.sqf b/addons/csw/functions/fnc_reload_getVehicleMagazine.sqf index 24d21534ee2..3c3ec9ac08d 100644 --- a/addons/csw/functions/fnc_reload_getVehicleMagazine.sqf +++ b/addons/csw/functions/fnc_reload_getVehicleMagazine.sqf @@ -26,18 +26,21 @@ if (_desiredAmmo == 0) then { _desiredAmmo = 100; }; private _bestMag = "#"; private _bestMagCount = -1; +private _cfgMagazines = configFile >> "CfgMagazines"; + +// Warms GVAR(compatibleVehicleMagsCache) for every weapon on the CSW, read per turret below +[_vehicle] call FUNC(compatibleMagazines); { - private _weapon = _x; { if ((getNumber (_carryGroupCfg >> _x)) == 1) then { - private _xAmmo = getNumber (configFile >> "CfgMagazines" >> _x >> "ammo"); + private _xAmmo = getNumber (_cfgMagazines >> _x >> "ammo"); if (((_xAmmo >= _bestMagCount) && {_bestMagCount < _desiredAmmo}) || {(_xAmmo >= _desiredAmmo) && {_xAmmo < _bestMagCount}}) then { _bestMag = _x; _bestMagCount = _xAmmo; }; }; - } forEach (compatibleMagazines _weapon); + } forEach (GVAR(compatibleVehicleMagsCache) getOrDefault [_x, []]); } forEach (_vehicle weaponsTurret _turret); TRACE_3("best fit",_desiredAmmo,_bestMag,_bestMagCount); From d0ee7276d73abf7e14890efdca32a6fb89bfb7d9 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:04:32 -0300 Subject: [PATCH 3/9] fix: make AI CSW reloading work without a selected weapon ai_handleGetIn passed currentWeapon of the CSW into ai_reload, which drove every lookup in it. currentWeapon reports a selection, and on GetIn there is none: nothing has been fired yet, and proxyWeapon has just removed and re-added the turret's weapon. The lookup came back empty, ai_reload exited on a TRACE, and AI sat in an empty static forever. AI that had already fired reloaded fine, because the Fired EH supplies a real weapon classname. ai_reload now derives the turret from the gunner and asks reload_getLoadableMagazines what fits, so neither caller needs a weapon at all. Both event handlers drop the argument. Fixes AI never reloading a CSW they mounted while it was empty. --- addons/csw/functions/fnc_ai_handleFired.sqf | 6 +- addons/csw/functions/fnc_ai_handleGetIn.sqf | 2 +- addons/csw/functions/fnc_ai_reload.sqf | 100 ++++++-------------- 3 files changed, 34 insertions(+), 74 deletions(-) diff --git a/addons/csw/functions/fnc_ai_handleFired.sqf b/addons/csw/functions/fnc_ai_handleFired.sqf index 4a6615f4043..4576339fb73 100644 --- a/addons/csw/functions/fnc_ai_handleFired.sqf +++ b/addons/csw/functions/fnc_ai_handleFired.sqf @@ -14,12 +14,12 @@ if (GVAR(ammoHandling) != 2) exitWith {}; -params ["_vehicle", "_weapon", "", "", "", "_magazine", "", "_gunner"]; -TRACE_4("firedEH:",_vehicle,_weapon,_magazine,_gunner); +params ["_vehicle", "", "", "", "", "", "", "_gunner"]; +TRACE_2("firedEH:",_vehicle,_gunner); if (someAmmo _vehicle) exitWith {}; if ((!local _gunner) || {[_gunner] call EFUNC(common,isPlayer)}) exitWith {}; TRACE_1("need ammo",magazinesAllTurrets _vehicle); -[_vehicle, _gunner, _weapon, _magazine] call FUNC(ai_reload); +[_vehicle, _gunner] call FUNC(ai_reload); diff --git a/addons/csw/functions/fnc_ai_handleGetIn.sqf b/addons/csw/functions/fnc_ai_handleGetIn.sqf index bbbddaf537f..2dc358d7fd3 100644 --- a/addons/csw/functions/fnc_ai_handleGetIn.sqf +++ b/addons/csw/functions/fnc_ai_handleGetIn.sqf @@ -22,4 +22,4 @@ if ((!local _gunner) || {[_gunner] call EFUNC(common,isPlayer)}) exitWith {}; TRACE_1("need ammo",magazinesAllTurrets _vehicle); -[_vehicle, _gunner, currentWeapon _vehicle] call FUNC(ai_reload); +[_vehicle, _gunner] call FUNC(ai_reload); diff --git a/addons/csw/functions/fnc_ai_reload.sqf b/addons/csw/functions/fnc_ai_reload.sqf index 95a4e9cc7df..bf2f75daf1a 100644 --- a/addons/csw/functions/fnc_ai_reload.sqf +++ b/addons/csw/functions/fnc_ai_reload.sqf @@ -6,95 +6,55 @@ * Arguments: * 0: CSW * 1: Gunner - * 2: Weapon - * 3: Magazine (default: "") * * Return Value: * None * + * Example: + * [cursorObject, gunner cursorObject] call ace_csw_fnc_ai_reload + * * Public: No */ -params ["_vehicle", "_gunner", "_weapon", ["_magazine", ""]]; - -private _isProxy = _gunner getVariable [QGVAR(autofire_isProxy), false]; -TRACE_2("checking for proxy",_gunner,_isProxy); -if (_isProxy) exitWith {}; +params ["_vehicle", "_gunner"]; +TRACE_2("AI reload",_vehicle,_gunner); -private _turretPath = [_gunner] call EFUNC(common,getTurretIndex); -private _reloadSource = objNull; -private _reloadMag = ""; -private _reloadNeededAmmo = -1; +// Proxy gunners are scripted, they do not carry or load anything +if (_gunner getVariable [QGVAR(autofire_isProxy), false]) exitWith {TRACE_1("proxy gunner",_gunner)}; -private _cfgMagGroups = configFile >> QGVAR(groups); +// The turret is derived from the gunner rather than from a weapon argument. What weapon the CSW has +// selected is not an answerable question on GetIn: nothing has been fired yet, and FUNC(proxyWeapon) +// has just swapped the turret's weapon out from under it +private _loadableMagazines = [_vehicle, _gunner, true] call FUNC(reload_getLoadableMagazines); +if (_loadableMagazines isEqualTo []) exitWith {TRACE_1("no loadable mag",_vehicle)}; -private _nearSupplies = [_gunner] + ((_vehicle nearSupplies 10) select { - isNull (group _x) || - {!([_x] call EFUNC(common,isPlayer)) && {[side group _gunner, side group _x] call BIS_fnc_sideIsFriendly}} -}); +private _bestAmmo = -1; +private _magazineInfo = []; -// Find if there is anything we can reload with { - scopeName "findSource"; - private _xSource = _x; - - private _cswMagazines = []; - { - _cswMagazines pushBackUnique _x; - } forEach ((magazineCargo _xSource) select {isClass (_cfgMagGroups >> _x)}); - TRACE_2("",_xSource,_cswMagazines); - - private _compatibleMags = compatibleMagazines _weapon; - if (_magazine != "") then { - _compatibleMags insert [0, [_magazine]]; + if ((_x select 4) > _bestAmmo) then { + _bestAmmo = _x select 4; + _magazineInfo = _x; }; +} forEach _loadableMagazines; - { - private _xWeaponMag = _x; - { - if ((getNumber (_cfgMagGroups >> _x >> _xWeaponMag)) == 1) then { - private _loadInfo = [_vehicle, _turretPath, _x, _xSource] call FUNC(reload_canLoadMagazine); - if (_loadInfo select 0) then { - _reloadMag = _x; - _reloadSource = _xSource; - _reloadNeededAmmo = _loadInfo select 2; - TRACE_3("found mag",_reloadMag,_reloadSource,_x); - breakOut "findSource"; - }; - }; - } forEach _cswMagazines; - } forEach _compatibleMags; -} forEach _nearSupplies; -if (_reloadMag == "") exitWith {TRACE_1("could not find mag",_reloadMag);}; - -// Figure out what we can add from the magazines we have -private _bestAmmoToSend = -1; -{ - _x params ["_xMag", "_xAmmo"]; - TRACE_2("",_xMag,_xAmmo); - if (_xMag == _reloadMag) then { - if ((_bestAmmoToSend == -1) || {(_xAmmo > _bestAmmoToSend) && {_xAmmo <= _reloadNeededAmmo}}) then { - _bestAmmoToSend = _xAmmo; - }; - }; -} forEach (if (_reloadSource isKindOf "CAManBase") then {magazinesAmmo _reloadSource} else {magazinesAmmoCargo _reloadSource}); -TRACE_4("",_reloadSource,_reloadMag,_reloadNeededAmmo,_bestAmmoToSend); -if (_bestAmmoToSend == -1) exitWith {ERROR("No ammo");}; +_magazineInfo params ["_carryMag", "_turretPath", "", "_magSource", "_ammo"]; // Remove the mag from the source -[_reloadSource, _reloadMag, _bestAmmoToSend] call EFUNC(common,removeSpecificMagazine); +[_magSource, _carryMag, _ammo] call EFUNC(common,removeSpecificMagazine); -private _timeToLoad = 1; -if (!isNull(configOf _vehicle >> QUOTE(ADDON) >> "ammoLoadTime")) then { - _timeToLoad = getNumber(configOf _vehicle >> QUOTE(ADDON) >> "ammoLoadTime"); -}; +// No source: the magazine is already gone by the time the event fires, so there is nothing left to +// check it against. Leftover ammo goes to the gunner, passed explicitly since it can't default to +// the source any more +private _eventParams = [_vehicle, _turretPath, objNull, _carryMag, _ammo, _gunner]; +private _timeToLoad = GET_NUMBER(configOf _vehicle >> QUOTE(ADDON) >> "ammoLoadTime",1); TRACE_1("Reloading in progress",_timeToLoad); + [{ - params ["_vehicle", "_turretPath", "_gunner", "_reloadMag", "_bestAmmoToSend"]; - if ((!alive _vehicle) || {!alive _gunner} || {(_vehicle distance _gunner) > 10}) exitWith {TRACE_1("invalid state",_this);}; + params ["_vehicle", "", "", "", "", "_gunner"]; + if !(alive _vehicle && {alive _gunner}) exitWith {TRACE_2("invalid state",alive _vehicle,alive _gunner)}; - // Reload the static weapon - TRACE_5("calling addTurretMag event",_vehicle,_turretPath,_gunner,_reloadMag,_bestAmmoToSend); + TRACE_1("calling addTurretMag event",_this); [QGVAR(addTurretMag), _this] call CBA_fnc_globalEvent; -}, [_vehicle, _turretPath, _gunner, _reloadMag, _bestAmmoToSend], _timeToLoad] call CBA_fnc_waitAndExecute; +}, _eventParams, _timeToLoad] call CBA_fnc_waitAndExecute; From cddf9fd2356bba257ba423881ff90cd95750a422 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:07:02 -0300 Subject: [PATCH 4/9] fix: pass the loading unit into the CSW load progress bar reload_loadMagazine's onFinish reads _unit, but the unit was never in the argument array the progress bar hands back, so it was nil throughout. Two things silently did nothing as a result: the fallback that returns the magazine to the unit when emptying a weapon holder deletes it, and QGVAR(reloader), which autofire_fire reads to attribute a scripted shot. Setting a variable to nil deletes it, so the reloader was never recorded. Also clears the nearby-source cache after the magazine is removed, since emptying a weapon holder deletes it and the cached list would otherwise hold a deleted object until it expires. --- addons/csw/functions/fnc_reload_loadMagazine.sqf | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/addons/csw/functions/fnc_reload_loadMagazine.sqf b/addons/csw/functions/fnc_reload_loadMagazine.sqf index 23c55cc1187..352935be4c2 100644 --- a/addons/csw/functions/fnc_reload_loadMagazine.sqf +++ b/addons/csw/functions/fnc_reload_loadMagazine.sqf @@ -22,11 +22,7 @@ params ["_vehicle", "_turret", "_carryMag", "_magSource", "_unit"]; TRACE_5("loadMagazine",_vehicle,_turret,_carryMag,_magSource,_unit); -private _timeToLoad = 1; -private _config = configOf _vehicle >> QUOTE(ADDON) >> "ammoLoadTime"; -if (!isNull _config) then { - _timeToLoad = getNumber _config; -}; +private _timeToLoad = GET_NUMBER(configOf _vehicle >> QUOTE(ADDON) >> "ammoLoadTime",1); private _displayName = format [LLSTRING(loadX), getText (configFile >> "CfgMagazines" >> _carryMag >> "displayName")]; @@ -52,6 +48,9 @@ private _onFinish = { [_magSource, _carryMag, _bestAmmoToSend] call EFUNC(common,removeSpecificMagazine); if (_bestAmmoToSend == 0) exitWith {}; + // Emptying a weapon holder deletes it, so the cached source list can hold a deleted object + [QGVAR(clearNearbySourcesCache), []] call CBA_fnc_localEvent; + // Workaround for removeSpecificMagazine and WeaponHolders being deleted when empty, give back to the unit if the weapon holder was deleted // TODO: Pass type and position of deleted object to create a new one private _args = [_vehicle, _turret, _magSource, _carryMag, _bestAmmoToSend]; @@ -69,7 +68,7 @@ private _onFinish = { [ TIME_PROGRESSBAR(_timeToLoad), - [_vehicle, _turret, _carryMag, _magSource], + [_vehicle, _turret, _carryMag, _magSource, _unit], _onFinish, {TRACE_1("load progressBar fail",_this);}, _displayName, From c5ba0b91f7c7351e50fdb85887bfc3d5ed9f1dd1 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:08:00 -0300 Subject: [PATCH 5/9] fix: reselect the CSW weapon for AI after a proxy weapon swap proxyWeapon takes the turret's weapon away and adds a different one. An AI already sitting in that turret is left with a selection that no longer exists and stops firing. reload_handleAddTurretMag calls proxyWeapon mid-reload, so this happens to any AI reloading a static that uses one. proxyWeapon now raises QGVAR(proxyWeaponChanged) globally with the turret and both weapon classnames. proxyWeapon itself only runs where the turret is local, but weapon selection follows the gunner, who can be local elsewhere, so the listener has to run everywhere and check for itself. Players are skipped, they pick their own weapon. --- addons/csw/XEH_PREP.hpp | 1 + addons/csw/XEH_postInit.sqf | 1 + .../fnc_handleProxyWeaponChanged.sqf | 37 +++++++++++++++++++ addons/csw/functions/fnc_proxyWeapon.sqf | 4 ++ 4 files changed, 43 insertions(+) create mode 100644 addons/csw/functions/fnc_handleProxyWeaponChanged.sqf diff --git a/addons/csw/XEH_PREP.hpp b/addons/csw/XEH_PREP.hpp index a664b6fb61e..fbaa73885f3 100644 --- a/addons/csw/XEH_PREP.hpp +++ b/addons/csw/XEH_PREP.hpp @@ -33,6 +33,7 @@ PREP(compatibleMagazines); PREP(getCarryMagazine); PREP(getNearbySources); PREP(getSourceCompatibleMagazines); +PREP(handleProxyWeaponChanged); PREP(handleScrollWheel); PREP(proxyWeapon); diff --git a/addons/csw/XEH_postInit.sqf b/addons/csw/XEH_postInit.sqf index f018d911ff3..6cac6001ec3 100644 --- a/addons/csw/XEH_postInit.sqf +++ b/addons/csw/XEH_postInit.sqf @@ -40,6 +40,7 @@ GVAR(deployPFH) = -1; [QGVAR(addTurretMag), LINKFUNC(reload_handleAddTurretMag)] call CBA_fnc_addEventHandler; [QGVAR(removeTurretMag), LINKFUNC(reload_handleRemoveTurretMag)] call CBA_fnc_addEventHandler; [QGVAR(returnAmmo), LINKFUNC(reload_handleReturnAmmo)] call CBA_fnc_addEventHandler; +[QGVAR(proxyWeaponChanged), LINKFUNC(handleProxyWeaponChanged)] call CBA_fnc_addEventHandler; [QGVAR(autofire_fire), LINKFUNC(autofire_fire)] call CBA_fnc_addEventHandler; // Cancel placement if interact menu open diff --git a/addons/csw/functions/fnc_handleProxyWeaponChanged.sqf b/addons/csw/functions/fnc_handleProxyWeaponChanged.sqf new file mode 100644 index 00000000000..7bc08b9223c --- /dev/null +++ b/addons/csw/functions/fnc_handleProxyWeaponChanged.sqf @@ -0,0 +1,37 @@ +#include "..\script_component.hpp" +/* + * Author: LinkIsGrim + * Points an AI gunner at the proxy weapon that just replaced the one they had selected. + * Called from a global event, only does anything where the gunner is local. + * + * Arguments: + * 0: CSW + * 1: Turret Path + * 2: Weapon that was removed + * 3: Proxy weapon that replaced it + * + * Return Value: + * None + * + * Example: + * [cursorObject, [0], "mortar_82mm", "ace_mortar_82mm"] call ace_csw_fnc_handleProxyWeaponChanged + * + * Public: No + */ + +params ["_vehicle", "_turret", "", "_proxyWeapon"]; +TRACE_3("proxyWeaponChanged",_vehicle,_turret,_proxyWeapon); + +private _gunner = _vehicle turretUnit _turret; + +// Nobody to point anywhere, and players pick their own weapon +if (isNull _gunner || {[_gunner] call EFUNC(common,isPlayer)}) exitWith {}; + +// selectWeaponTurret has no documented locality, but AI weapon selection follows the gunner rather +// than the turret, and FUNC(proxyWeapon) already ran on whichever machine owns the turret +if !(local _gunner) exitWith {TRACE_2("gunner not local",_vehicle,_gunner)}; + +if ((_vehicle currentWeaponTurret _turret) isEqualTo _proxyWeapon) exitWith {TRACE_1("already selected",_proxyWeapon)}; + +TRACE_3("selecting proxy weapon",_vehicle,_turret,_proxyWeapon); +_vehicle selectWeaponTurret [_proxyWeapon, _turret]; diff --git a/addons/csw/functions/fnc_proxyWeapon.sqf b/addons/csw/functions/fnc_proxyWeapon.sqf index 554b5bfe012..a6f29db7ab3 100644 --- a/addons/csw/functions/fnc_proxyWeapon.sqf +++ b/addons/csw/functions/fnc_proxyWeapon.sqf @@ -49,3 +49,7 @@ TRACE_2("swapping to proxy weapon",_currentWeapon,_proxyWeapon); _vehicle removeWeaponTurret [_currentWeapon, _turret]; _vehicle addWeaponTurret [_proxyWeapon, _turret]; _vehicle setVariable [format [QGVAR(proxyHandled_%1), _turret], true, true]; + +// An AI already sitting in the turret had the weapon it selected taken away. Weapon selection has to +// happen where the gunner is local, which isn't necessarily this machine +[QGVAR(proxyWeaponChanged), [_vehicle, _turret, _currentWeapon, _proxyWeapon]] call CBA_fnc_globalEvent; From 5473bc6fb352b42e1159e4fae3678eaed7e294a0 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:09:33 -0300 Subject: [PATCH 6/9] refactor: send CSW turret mag events to the turret owner addTurretMag and removeTurretMag were raised globally and then discarded on every machine except the one owning the turret, which both handlers already check for themselves. CBA_fnc_turretEvent routes them there directly. reload_handleAddTurretMag drops its magazine source argument. It only used it as the default recipient for leftover ammo, and neither caller wants that: loadMagazine has to redirect to the unit when emptying the source deletes it, and ai_reload has already removed the magazine before the event fires. The recipient is now always passed explicitly. Neither the function nor the event is public, so there is no compatibility to keep. --- addons/csw/functions/fnc_ai_reload.sqf | 10 ++++------ addons/csw/functions/fnc_getUnloadActions.sqf | 8 ++------ .../functions/fnc_reload_handleAddTurretMag.sqf | 14 ++++++-------- addons/csw/functions/fnc_reload_loadMagazine.sqf | 10 +++------- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/addons/csw/functions/fnc_ai_reload.sqf b/addons/csw/functions/fnc_ai_reload.sqf index bf2f75daf1a..81beae2ffe5 100644 --- a/addons/csw/functions/fnc_ai_reload.sqf +++ b/addons/csw/functions/fnc_ai_reload.sqf @@ -43,18 +43,16 @@ _magazineInfo params ["_carryMag", "_turretPath", "", "_magSource", "_ammo"]; // Remove the mag from the source [_magSource, _carryMag, _ammo] call EFUNC(common,removeSpecificMagazine); -// No source: the magazine is already gone by the time the event fires, so there is nothing left to -// check it against. Leftover ammo goes to the gunner, passed explicitly since it can't default to -// the source any more -private _eventParams = [_vehicle, _turretPath, objNull, _carryMag, _ammo, _gunner]; +// Leftover ammo goes back to the gunner +private _eventParams = [_vehicle, _turretPath, _carryMag, _ammo, _gunner]; private _timeToLoad = GET_NUMBER(configOf _vehicle >> QUOTE(ADDON) >> "ammoLoadTime",1); TRACE_1("Reloading in progress",_timeToLoad); [{ - params ["_vehicle", "", "", "", "", "_gunner"]; + params ["_vehicle", "_turretPath", "", "", "_gunner"]; if !(alive _vehicle && {alive _gunner}) exitWith {TRACE_2("invalid state",alive _vehicle,alive _gunner)}; TRACE_1("calling addTurretMag event",_this); - [QGVAR(addTurretMag), _this] call CBA_fnc_globalEvent; + [QGVAR(addTurretMag), _this, _vehicle, _turretPath] call CBA_fnc_turretEvent; }, _eventParams, _timeToLoad] call CBA_fnc_waitAndExecute; diff --git a/addons/csw/functions/fnc_getUnloadActions.sqf b/addons/csw/functions/fnc_getUnloadActions.sqf index 48d7679d044..75145ac96b5 100644 --- a/addons/csw/functions/fnc_getUnloadActions.sqf +++ b/addons/csw/functions/fnc_getUnloadActions.sqf @@ -22,11 +22,7 @@ private _statement = { _args params ["_vehMag", "_turretPath", "_carryMag"]; TRACE_5("starting unload",_target,_turretPath,_player,_carryMag,_vehMag); - private _timeToUnload = 1; - private _config = configOf _target >> QUOTE(ADDON) >> "ammoUnloadTime"; - if (!isNull _config) then { - _timeToUnload = getNumber _config; - }; + private _timeToUnload = GET_NUMBER(configOf _target >> QUOTE(ADDON) >> "ammoUnloadTime",1); [ TIME_PROGRESSBAR(_timeToUnload), @@ -34,7 +30,7 @@ private _statement = { { (_this select 0) params ["_target", "_turretPath", "_player", "_carryMag", "_vehMag"]; TRACE_5("unload progressBar finish",_target,_turretPath,_carryMag,_vehMag,_player); - [QGVAR(removeTurretMag), [_target, _turretPath, _carryMag, _vehMag, _player]] call CBA_fnc_globalEvent; + [QGVAR(removeTurretMag), [_target, _turretPath, _carryMag, _vehMag, _player], _target, _turretPath] call CBA_fnc_turretEvent; }, {TRACE_1("unload progressBar fail",_this);}, format [LLSTRING(unloadX), getText (configFile >> "CfgMagazines" >> _carryMag >> "displayName")], diff --git a/addons/csw/functions/fnc_reload_handleAddTurretMag.sqf b/addons/csw/functions/fnc_reload_handleAddTurretMag.sqf index 881398cb95b..20164b150a9 100644 --- a/addons/csw/functions/fnc_reload_handleAddTurretMag.sqf +++ b/addons/csw/functions/fnc_reload_handleAddTurretMag.sqf @@ -7,23 +7,21 @@ * Arguments: * 0: CSW * 1: Turret Path - * 2: Source of magazine - * 3: Vehicle Magazine - * 4: Ammo in magazine - * 5: Unit or object to return ammo to (default: Source of magazine) + * 2: Vehicle Magazine + * 3: Ammo in magazine + * 4: Unit or object to return leftover ammo to * * Return Value: * None * * Example: - * [cursorTarget, [0], player, "200Rnd_127x99_mag_Tracer_Red", 70] call ace_csw_fnc_reload_handleAddTurretMag + * [cursorTarget, [0], "200Rnd_127x99_mag_Tracer_Red", 70, player] call ace_csw_fnc_reload_handleAddTurretMag * * Public: No */ -params ["_vehicle", "_turret", "_magSource", "_carryMag", "_ammoReceived"]; -private _returnTo = param [5, _magSource]; -TRACE_6("reload_handleAddTurretMag",_vehicle,_turret,_magSource,_carryMag,_ammoReceived,_returnTo); +params ["_vehicle", "_turret", "_carryMag", "_ammoReceived", "_returnTo"]; +TRACE_5("reload_handleAddTurretMag",_vehicle,_turret,_carryMag,_ammoReceived,_returnTo); TRACE_2("",local _vehicle,_vehicle turretLocal _turret); if !(_vehicle turretLocal _turret) exitWith {}; diff --git a/addons/csw/functions/fnc_reload_loadMagazine.sqf b/addons/csw/functions/fnc_reload_loadMagazine.sqf index 352935be4c2..fe35bc77185 100644 --- a/addons/csw/functions/fnc_reload_loadMagazine.sqf +++ b/addons/csw/functions/fnc_reload_loadMagazine.sqf @@ -53,15 +53,11 @@ private _onFinish = { // Workaround for removeSpecificMagazine and WeaponHolders being deleted when empty, give back to the unit if the weapon holder was deleted // TODO: Pass type and position of deleted object to create a new one - private _args = [_vehicle, _turret, _magSource, _carryMag, _bestAmmoToSend]; - - // If the source is set for deletion, give mag back to unit - if (_magSource getEntityInfo 14) then { - _args pushBack _unit; - }; + private _returnTo = [_magSource, _unit] select (_magSource getEntityInfo 14); + private _args = [_vehicle, _turret, _carryMag, _bestAmmoToSend, _returnTo]; TRACE_1("calling addTurretMag event",_args); - [QGVAR(addTurretMag), _args] call CBA_fnc_globalEvent; + [QGVAR(addTurretMag), _args, _vehicle, _turret] call CBA_fnc_turretEvent; _vehicle setVariable [QGVAR(reloader), _unit, true]; }; From e6931bfd0d62ee6803bd4b682bc5d3bba8ce3054 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:13:52 -0300 Subject: [PATCH 7/9] refactor: name the CSW load and unload strings after their actions loadX and unloadX become actionLoad and actionUnload, alongside the actionLink that already existed. Key rename only, every translation is carried over unchanged. --- addons/csw/functions/fnc_getLoadActions.sqf | 2 +- addons/csw/functions/fnc_getUnloadActions.sqf | 4 +- .../csw/functions/fnc_reload_loadMagazine.sqf | 2 +- addons/csw/stringtable.xml | 72 +++++++++---------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/addons/csw/functions/fnc_getLoadActions.sqf b/addons/csw/functions/fnc_getLoadActions.sqf index 59b368b3686..997a8646384 100644 --- a/addons/csw/functions/fnc_getLoadActions.sqf +++ b/addons/csw/functions/fnc_getLoadActions.sqf @@ -47,7 +47,7 @@ private _actions = []; private _text = if (_isBeltLinking) then { format [LLSTRING(actionLink), _displayName]; } else { - format [LLSTRING(loadX), _displayName]; + format [LLSTRING(actionLoad), _displayName]; }; private _action = [format ["load_%1", _forEachIndex], _text, _picture, _statement, _condition, {}, _x] call EFUNC(interact_menu,createAction); diff --git a/addons/csw/functions/fnc_getUnloadActions.sqf b/addons/csw/functions/fnc_getUnloadActions.sqf index 75145ac96b5..a386e0733da 100644 --- a/addons/csw/functions/fnc_getUnloadActions.sqf +++ b/addons/csw/functions/fnc_getUnloadActions.sqf @@ -33,7 +33,7 @@ private _statement = { [QGVAR(removeTurretMag), [_target, _turretPath, _carryMag, _vehMag, _player], _target, _turretPath] call CBA_fnc_turretEvent; }, {TRACE_1("unload progressBar fail",_this);}, - format [LLSTRING(unloadX), getText (configFile >> "CfgMagazines" >> _carryMag >> "displayName")], + format [LLSTRING(actionUnload), getText (configFile >> "CfgMagazines" >> _carryMag >> "displayName")], {(_this select 0) call FUNC(reload_canUnloadMagazine)}, ["isNotInside"] ] call EFUNC(common,progressBar); @@ -62,7 +62,7 @@ private _cfgMagazines = configFile >> "CfgMagazines"; if (_carryMag == "") exitWith {}; private _displayName = getText (_cfgMagazines >> _carryMag >> "displayName"); - private _text = format [LLSTRING(unloadX), _displayName]; + private _text = format [LLSTRING(actionUnload), _displayName]; private _picture = getText (_cfgMagazines >> _carryMag >> "picture"); private _action = [format ["unload_%1", _forEachIndex], _text, _picture, _statement, _condition, {}, [_xMag, _xTurret, _carryMag]] call EFUNC(interact_menu,createAction); _actions pushBack [_action, [], _vehicle]; diff --git a/addons/csw/functions/fnc_reload_loadMagazine.sqf b/addons/csw/functions/fnc_reload_loadMagazine.sqf index fe35bc77185..f358b8f28dc 100644 --- a/addons/csw/functions/fnc_reload_loadMagazine.sqf +++ b/addons/csw/functions/fnc_reload_loadMagazine.sqf @@ -24,7 +24,7 @@ TRACE_5("loadMagazine",_vehicle,_turret,_carryMag,_magSource,_unit); private _timeToLoad = GET_NUMBER(configOf _vehicle >> QUOTE(ADDON) >> "ammoLoadTime",1); -private _displayName = format [LLSTRING(loadX), getText (configFile >> "CfgMagazines" >> _carryMag >> "displayName")]; +private _displayName = format [LLSTRING(actionLoad), getText (configFile >> "CfgMagazines" >> _carryMag >> "displayName")]; private _onFinish = { (_this select 0) params ["_vehicle", "_turret", "_carryMag", "_magSource", "_unit"]; diff --git a/addons/csw/stringtable.xml b/addons/csw/stringtable.xml index 461cf83e5e5..bbf8dcfd7c8 100644 --- a/addons/csw/stringtable.xml +++ b/addons/csw/stringtable.xml @@ -516,6 +516,42 @@ 连接 %1 З'єднати %1 + + Load %1 + Nabít %1 + Charger %1 + Cargar %1 + Carica %1 + Załaduj %1 + Carregar %1 + Загрузить %1 + Lade %1 + %1 싣기 + %1 を装填 + 裝填 %1 + 装填 %1 + Yükle %1 + Load %1 + Завантажити %1 + + + Unload %1 + Vytáhnout zásobník z %1 + Décharger %1 + Descargar %1 + Scarica %1 + Rozładuj %1 + Descarregar %1 + Разгрузить %1 + Entlade %1 + %1 내리기 + %1 を除去 + 卸載 %1 + 卸载 %1 + Boşalt %1 + Unload %1 + Розвантажити %1 + [CSW] AGS-30 Gun Bag [CSW] ASG-30 zbraň v pouzdře @@ -900,24 +936,6 @@ [CSW] 9m113 Kornet Launcher [CSW] Сумка з 9К135 Корнет - - Load %1 - Nabít %1 - Charger %1 - Cargar %1 - Carica %1 - Załaduj %1 - Carregar %1 - Загрузить %1 - Lade %1 - %1 싣기 - %1 を装填 - 裝填 %1 - 装填 %1 - Yükle %1 - Load %1 - Завантажити %1 - [CSW] M220 Deployable Tripod [CSW] M220 trojnožka @@ -1318,23 +1336,5 @@ [CSW] BGM-71 TOW Launcher Bag [CSW] Сумка з BGM-71 TOW установкою - - Unload %1 - Vytáhnout zásobník z %1 - Décharger %1 - Descargar %1 - Scarica %1 - Rozładuj %1 - Descarregar %1 - Разгрузить %1 - Entlade %1 - %1 내리기 - %1 を除去 - 卸載 %1 - 卸载 %1 - Boşalt %1 - Unload %1 - Розвантажити %1 - From 7d24c86bd949f6b932cd60dd1730991f304b37e8 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:09:22 -0300 Subject: [PATCH 8/9] fix: reload CSWs whose crew were placed in them from the editor AI seated in a static from the editor are in it before initVehicle runs, so the GetIn handler it registers never fires for them. With ammo handling on, initVehicle then empties the weapon, leaving them nothing to fire and so no Fired event either. An AI that starts in a CSW never reloaded at all; one that walked up to the same CSW did. initVehicle now runs the same check against the existing crew, a frame later so the magazine handling above it has settled. Also stops compatibleMagazines gating on GVAR(initializedStaticTypes), which initVehicle only fills where there is an interface, so it is always empty on a dedicated server and every lookup came back with nothing. Reads the enabled property off the config instead. --- addons/csw/functions/fnc_compatibleMagazines.sqf | 4 +++- .../functions/fnc_handleProxyWeaponChanged.sqf | 16 +++++++++------- addons/csw/functions/fnc_initVehicle.sqf | 13 +++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/addons/csw/functions/fnc_compatibleMagazines.sqf b/addons/csw/functions/fnc_compatibleMagazines.sqf index e581d53586a..dcab9be46a5 100644 --- a/addons/csw/functions/fnc_compatibleMagazines.sqf +++ b/addons/csw/functions/fnc_compatibleMagazines.sqf @@ -19,7 +19,9 @@ params [["_csw", objNull, [objNull]]]; -if !((typeOf _csw) in GVAR(initializedStaticTypes)) exitWith {createHashMap}; +// Read from config rather than GVAR(initializedStaticTypes), which is only filled where there is an +// interface and so is always empty on a dedicated server +if ((getNumber (configOf _csw >> QUOTE(ADDON) >> "enabled")) != 1) exitWith {createHashMap}; // Caches are filled here rather than on weapon swap, a CSW without a proxy weapon needs them too private _fnc_cacheWeapon = { diff --git a/addons/csw/functions/fnc_handleProxyWeaponChanged.sqf b/addons/csw/functions/fnc_handleProxyWeaponChanged.sqf index 7bc08b9223c..c04bad9e503 100644 --- a/addons/csw/functions/fnc_handleProxyWeaponChanged.sqf +++ b/addons/csw/functions/fnc_handleProxyWeaponChanged.sqf @@ -1,7 +1,7 @@ #include "..\script_component.hpp" /* * Author: LinkIsGrim - * Points an AI gunner at the proxy weapon that just replaced the one they had selected. + * Points a gunner at the proxy weapon that just replaced the one they had selected. * Called from a global event, only does anything where the gunner is local. * * Arguments: @@ -24,14 +24,16 @@ TRACE_3("proxyWeaponChanged",_vehicle,_turret,_proxyWeapon); private _gunner = _vehicle turretUnit _turret; -// Nobody to point anywhere, and players pick their own weapon -if (isNull _gunner || {[_gunner] call EFUNC(common,isPlayer)}) exitWith {}; +if (isNull _gunner) exitWith {}; -// selectWeaponTurret has no documented locality, but AI weapon selection follows the gunner rather -// than the turret, and FUNC(proxyWeapon) already ran on whichever machine owns the turret +// selectWeaponTurret has no documented locality, but weapon selection follows the gunner rather than +// the turret, and FUNC(proxyWeapon) already ran on whichever machine owns the turret if !(local _gunner) exitWith {TRACE_2("gunner not local",_vehicle,_gunner)}; -if ((_vehicle currentWeaponTurret _turret) isEqualTo _proxyWeapon) exitWith {TRACE_1("already selected",_proxyWeapon)}; +// Only step in when the selection no longer exists, which is what the swap just did to it. A gunner +// who still has a valid weapon picked it deliberately, including a player cycling turret weapons +private _selected = _vehicle currentWeaponTurret _turret; +if (_selected in (_vehicle weaponsTurret _turret)) exitWith {TRACE_1("selection still valid",_selected)}; -TRACE_3("selecting proxy weapon",_vehicle,_turret,_proxyWeapon); +TRACE_4("selecting proxy weapon",_vehicle,_turret,_selected,_proxyWeapon); _vehicle selectWeaponTurret [_proxyWeapon, _turret]; diff --git a/addons/csw/functions/fnc_initVehicle.sqf b/addons/csw/functions/fnc_initVehicle.sqf index ce01dd08841..4faa0682e5e 100644 --- a/addons/csw/functions/fnc_initVehicle.sqf +++ b/addons/csw/functions/fnc_initVehicle.sqf @@ -157,3 +157,16 @@ if (hasInterface && {!(_typeOf in GVAR(initializedStaticTypes))}) then { [_typeOf, 1, ["ACE_SelfActions"], _disableAction] call EFUNC(interact_menu,addActionToClass); } }; + +// Crew placed in the CSW from the editor are already seated by the time this runs, so the GetIn +// handler above never fires for them, and emptying the weapon leaves them nothing to fire either. +// Next frame, so the magazine handling above has settled and QGVAR(initialising) has cleared +if (_configEnabled && {GVAR(ammoHandling) == 2}) then { + [{ + params ["_vehicle"]; + + { + [_vehicle, "", _x] call FUNC(ai_handleGetIn); + } forEach (crew _vehicle); + }, [_vehicle]] call CBA_fnc_execNextFrame; +}; From e93fc739092146f65f7c26e835fbb84781777988 Mon Sep 17 00:00:00 2001 From: LinkIsGrim <69561145+LinkIsGrim@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:21:41 -0300 Subject: [PATCH 9/9] fix: return CSW ammo within the radius it can be loaded from The ammo return paths searched 10m for an existing container to reuse while the loading side searches DISTANCE_SEARCH_RADIUS, so magazines could be put into a weapon holder that the CSW they came from cannot reach. Most visible with handleExtraMagazines, where a static's starting ammo was deposited out of its own range on init. All three searches now use the same constant. Newly created containers were never affected, those are placed next to the weapon. --- addons/csw/functions/fnc_reload_handleReturnAmmo.sqf | 5 +++-- .../csw/functions/fnc_staticWeaponInit_unloadExtraMags.sqf | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/csw/functions/fnc_reload_handleReturnAmmo.sqf b/addons/csw/functions/fnc_reload_handleReturnAmmo.sqf index 8cfe6051fed..6c95a5968e1 100644 --- a/addons/csw/functions/fnc_reload_handleReturnAmmo.sqf +++ b/addons/csw/functions/fnc_reload_handleReturnAmmo.sqf @@ -42,10 +42,11 @@ if ((_fullMagazines == 0) && {_bulletsRemaining == 0}) exitWith {}; // Try to use object inventory or existing container private _container = [_unloadTo, objNull] select _unloadToUnit; if ((maxLoad _container) isEqualTo 0) then { + // Same radius the loading side searches, or ammo lands somewhere it can't be loaded back from _container = _unloadTo getVariable [QGVAR(container), objNull]; - if ((_container distance _unloadTo) > 10) then { _container = objNull; }; + if ((_container distance _unloadTo) > DISTANCE_SEARCH_RADIUS) then { _container = objNull; }; if (isNull _container) then { - _container = (nearestObjects [_unloadTo, [["GroundWeaponHolder"], [QGVAR(ammo_holder)]] select GVAR(handleExtraMagazinesType), 10]) param [0, objNull]; + _container = (nearestObjects [_unloadTo, [["GroundWeaponHolder"], [QGVAR(ammo_holder)]] select GVAR(handleExtraMagazinesType), DISTANCE_SEARCH_RADIUS]) param [0, objNull]; }; }; diff --git a/addons/csw/functions/fnc_staticWeaponInit_unloadExtraMags.sqf b/addons/csw/functions/fnc_staticWeaponInit_unloadExtraMags.sqf index 32b689ab371..7730aa7e7c8 100644 --- a/addons/csw/functions/fnc_staticWeaponInit_unloadExtraMags.sqf +++ b/addons/csw/functions/fnc_staticWeaponInit_unloadExtraMags.sqf @@ -89,7 +89,8 @@ if (_secondaryWeaponMagazines isNotEqualTo []) then { } else { // Find a suitable container to place items in if necessary if (isNull _container) then { - _container = (nearestObjects [_vehicle, ["GroundWeaponHolder"], 10]) param [0, objNull]; + // Same radius the loading side searches, or ammo lands somewhere it can't be loaded back from + _container = (nearestObjects [_vehicle, ["GroundWeaponHolder"], DISTANCE_SEARCH_RADIUS]) param [0, objNull]; // Create ammo storage container if (isNull _container) then {