CSW - Fix AI reload edge cases, code cleanup - #11450
Open
LinkIsGrim wants to merge 17 commits into
Open
LinkIsGrim wants to merge 17 commits into
LinkIsGrim wants to merge 17 commits into
Conversation
johnb432
reviewed
Jul 30, 2026
johnb432
left a comment
Contributor
There was a problem hiding this comment.
Just a cursory glance atm.
PabstMirror
reviewed
Jul 31, 2026
johnb432
reviewed
Jul 31, 2026
johnb432
left a comment
Contributor
There was a problem hiding this comment.
2nd surface level glance.
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
force-pushed
the
csw-ai-reload
branch
from
August 24, 2026 07:53
e93fc73 to
98345b8
Compare
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When merged this pull request will:
ai_handleGetInpassedcurrentWeaponof the CSW intoai_reload, which drove every lookup in it.currentWeaponreports a selection, and on GetIn there is none: nothing has been fired yet, andproxyWeaponhas 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.initVehicleruns 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.ai_reloadread every candidate source withmagazineCargo, 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.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, andace_csw_reloader, whichautofire_firereads tosetShotParents.compatibleMagazinesbeing dead on a dedicated server, where the array it gated on is never populated becauseinitVehicleonly fills it underhasInterface.proxyWeaponhad 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.ai_reloadderive the turret from the gunner and askreload_getLoadableMagazineswhat fits, so neither event handler needs a weapon.getNearbySources,getSourceCompatibleMagazinesandcompatibleMagazines.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.reload_canLoadMagazineintoreload_canUseSource, and reduce its distance check from 10m to 5m with anobjectParentfallback so a container nested in another container is measured by whatever is actually on the ground.addTurretMagandremoveTurretMagtoCBA_fnc_turretEvent, which both handlers already filtered for themselves.reload_handleAddTurretMag. Neither it nor the event is public.loadX/unloadXtoactionLoad/actionUnload, alongside theactionLinkthat already existed. Key rename only, translations carried over.Supersedes #9234, rewritten against current master rather than rebased. Based on reviews from that PR:
reload_loadMagazinekeeps 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,handleReturnAmmoandloadMagazinekeep master'ssetMagazineTurretAmmo,canAddandgetEntityInfo 14, all of which replaced workarounds that CSW - Fix and Improve reloading #9234 still contains.IMPORTANT
Component - Add|Fix|Improve|Change|Make|Remove {changes}.