Skip to content

CSW - Fix AI reload edge cases, code cleanup - #11450

Open
LinkIsGrim wants to merge 17 commits into
masterfrom
csw-ai-reload
Open

LinkIsGrim wants to merge 17 commits into
masterfrom
csw-ai-reload

Conversation

@LinkIsGrim

@LinkIsGrim LinkIsGrim commented Jul 29, 2026

Copy link
Copy Markdown
Member

When merged this pull request will:

  • Fix AI never reloading a CSW they mounted while it was empty/had no weapon selected. 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. Sometimes this means currentWeapon is "" because the AI won't select a weapon with no ammo.
  • Fix AI never reloading a CSW they were placed in from the editor/Zeus. initVehicle runs a second after vehicle init, so crew placed in the editor are already seated before the GetIn handler exists, and an emptied weapon gives them no Fired event either.
  • Fix a gunner's own magazines being invisible to AI reloading. ai_reload read every candidate source with magazineCargo, which does not read a unit's inventory, so the gunner has been dead weight in that list since CSW - Add reloading from nearSupplies #8776.
  • Fix the loading unit never reaching reload_loadMagazine's progress bar arguments, leaving it nil. This silently disabled the fallback that returns a magazine when emptying a weapon holder deletes it, and ace_csw_reloader, which autofire_fire reads to setShotParents.
  • Fix ammo being returned outside the radius it can be loaded from.
  • Fix compatibleMagazines being dead on a dedicated server, where the array it gated on is never populated because initVehicle only fills it under hasInterface.
  • Fix AI keeping a weapon selection that proxyWeapon had just deleted. Weapon selection follows the gunner, who can be local to a different machine than the turret, so a new global event carries it.
  • Make ai_reload derive the turret from the gunner and ask reload_getLoadableMagazines what fits, so neither event handler needs a weapon.
  • Add getNearbySources, getSourceCompatibleMagazines and compatibleMagazines.
  • Move the compatible magazine caches out of proxyWeapon, which exits before writing them for any CSW without a proxy weapon or with assembly mode off, leaving those with no load actions at all.
  • Split source validation out of reload_canLoadMagazine into reload_canUseSource, and reduce its distance check from 10m to 5m with an objectParent fallback so a container nested in another container is measured by whatever is actually on the ground.
  • Change addTurretMag and removeTurretMag to CBA_fnc_turretEvent, which both handlers already filtered for themselves.
  • Remove the magazine source argument from reload_handleAddTurretMag. Neither it nor the event is public.
  • Rename loadX/unloadX to actionLoad/actionUnload, alongside the actionLink that already existed. Key rename only, translations carried over.
  • Partially address Mk6 Mortar Ammunition Handling Disables AI use of Mortars #7477.

Supersedes #9234, rewritten against current master rather than rebased. Based on reviews from that PR:

  • reload_loadMagazine keeps master's recompute of the ammo to send rather than trusting the count captured when the action list was built. Trusting it means a magazine taken mid-progressbar leaves the removal failing while the load still fires.
  • handleAddTurretMag, handleReturnAmmo and loadMagazine keep master's setMagazineTurretAmmo, canAdd and getEntityInfo 14, all of which replaced workarounds that CSW - Fix and Improve reloading #9234 still contains.

IMPORTANT

  • If the contribution affects the documentation, please include your changes in this pull request so the documentation will appear on the website.
  • Development Guidelines are read, understood and applied.
  • Title of this PR uses our standard template Component - Add|Fix|Improve|Change|Make|Remove {changes}.

@LinkIsGrim
LinkIsGrim requested a review from johnb432 July 29, 2026 19:32
@LinkIsGrim LinkIsGrim self-assigned this Jul 29, 2026
@LinkIsGrim LinkIsGrim added the kind/bug-fix Release Notes: **FIXED:** label Jul 29, 2026
@LinkIsGrim LinkIsGrim added this to the Ongoing milestone Jul 29, 2026

@johnb432 johnb432 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a cursory glance atm.

Comment thread addons/csw/functions/fnc_ai_reload.sqf
Comment thread addons/csw/functions/fnc_getCarryMagazine.sqf Outdated
Comment thread addons/csw/functions/fnc_getNearbySources.sqf
Comment thread addons/csw/functions/fnc_handleProxyWeaponChanged.sqf Outdated
Comment thread addons/csw/functions/fnc_compatibleMagazines.sqf Outdated

@johnb432 johnb432 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2nd surface level glance.

Comment thread addons/csw/functions/fnc_proxyWeapon.sqf Outdated
Comment thread addons/csw/functions/fnc_reload_canUseSource.sqf Outdated
Comment thread addons/csw/functions/fnc_reload_canUseSource.sqf Outdated
Comment thread addons/csw/functions/fnc_reload_getVehicleMagazine.sqf Outdated
Comment thread addons/csw/functions/fnc_reload_loadMagazine.sqf Outdated
@LinkIsGrim LinkIsGrim mentioned this pull request Aug 24, 2026
3 tasks
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.
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.
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.
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.
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.
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.
loadX and unloadX become actionLoad and actionUnload, alongside the
actionLink that already existed. Key rename only, every translation is
carried over unchanged.
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.
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.
compatibleMagazines was gated on the ace_csw enabled config property, which
locked out anything declaring enabled = 0, HMG_01_A_base_F and the other auto
turrets among them. They could still be unloaded but never loaded again.

The gate was only ever an early-out. Without it a weapon whose magazines have
no carry equivalent maps to nothing and returns an empty hashmap on its own,
which is the same answer with less code, and it still avoids the
GVAR(initializedStaticTypes) check that is empty on a dedicated server.
- ai_reload honours removeSpecificMagazine's return. The source list is cached
  for a few seconds, so it could name a crate somebody had emptied, and the
  reload went ahead regardless with a magazine nobody paid for.
- proxyWeapon selects the replacement weapon inline rather than through a
  global event. selectWeaponTurret is turret local and proxyWeapon already is,
  so there was nothing for the event to carry. It also reads the selection
  before the swap, so it only redirects a gunner who was pointing at the
  weapon that got taken, rather than guessing afterwards from what the engine
  left behind.
- reload_getVehicleMagazine goes back to asking the engine for compatible
  magazines, and GVAR(compatibleVehicleMagsCache) is gone with it.
- reload_canUseSource takes a source rather than defaulting one. Its only
  caller has one, and the null case belongs in reload_canLoadMagazine where
  it means something.
- getCarryMagazine drops toLowerANSI, every caller passes config case.
- Drops an obsolete TODO in reload_loadMagazine, getEntityInfo 14 is the thing
  it was asking for.
@LinkIsGrim

Copy link
Copy Markdown
Member Author

I know we don't like force-pushes but I don't like master branch merges and I wanted to update the base

reload_getVehicleMagazine walked the magazine groups config on every call to
find which vehicle magazines a carry magazine maps to. That mapping is pure
config, so getMagazineGroup reads it once per carry magazine and keeps it.

The lookup becomes an intersection against what the weapon accepts, rather
than a config read per candidate.
The best fit search read a magazine's round count from the ammo property,
which holds the ammo classname rather than a number, so getNumber returned 0
for every candidate. Every one then satisfied the comparison and overwrote
the last, leaving whichever the config happened to enumerate last, and
desiredAmmo never influenced anything.

Rounds live in count. A group listing several sizes now resolves to the
smallest magazine that still meets desiredAmmo, which is what the function
has always claimed to do.

Only groups holding more than one size are affected, mostly the .50 and
Soviet 12.7 families. Present since b1d5bbe added CSW in 2019.
getNearbySources replaces a unit with their containers, because magazineCargo
cannot read a unit's inventory. That made the source a vest or backpack, so
reload_handleReturnAmmo saw something that is not a CAManBase, skipped the
canAdd checks entirely and pushed ammo straight into the container with no
capacity test and no spilling to the ground. It fired on every belt link that
left a remainder.

It now resolves a worn container back to whoever is wearing it, which fixes
every caller rather than the one that noticed.

Also stops initVehicle reloading for passengers. crew includes everyone, and
ai_handleGetIn takes its third argument as the gunner, so a passenger
triggered a reload of turret 0 and two occupants each pulled a magazine
before either load resolved.

Review cleanups alongside:
- getNearbySources loses the vehicle and crew flags, which nothing calls, and
  with them the hashed cache key they needed
- restores the deliberately commented out TRACE in reload_canLoadMagazine,
  which runs every frame in an interact menu condition
- drops the CAManBase branch in reload_loadMagazine, unreachable now that
  sources are always containers
- sorts compatibleMagazines into the PREP block
@LinkIsGrim
LinkIsGrim requested a review from johnb432 August 24, 2026 08:02
ai_reload took the fullest magazine it could reach, ignoring how much room
the turret actually had. Belt linking fifty rounds consumed a hundred round
belt and handed half of it straight back. The load info already carries what
is needed, so it now prefers the largest magazine that still fits, which is
what reload_loadMagazine does on the player side.

Also reads the compatible magazines once per CSW instead of once per source.
compatibleMagazines hands back a copy so callers cannot edit the cache, and
getSourceCompatibleMagazines was asking for one per source, so opening the
menu beside a few crates rebuilt and copied the same map several times over.
- compatibleMagazines builds its set of carry magazines without values, the
  keys are all that is ever read
- the best fit comparison becomes IS_BETTER_FIT rather than being written out
  in both ai_reload and reload_loadMagazine, and drops the lazy evaluation,
  both sides are a comparison
- single argument calls lose the array around the argument
getNearbySources gathers within DISTANCE_SEARCH_RADIUS of the unit while
reload_canUseSource measured the same radius from the CSW, so a source had to
satisfy both and the usable area was the overlap of two spheres rather than
one. Standing on the far side of the gun from a crate could put it out of
reach at well under the nominal distance.

The unit doing the loading is the one reaching for the magazine, so that is
what both now measure from. reload_canLoadMagazine carries them through,
which its two callers with a source already had to hand.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/bug-fix Release Notes: **FIXED:**

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants