Weapons · Updated Jun 16, 2026

Weapon Action Lock — States.Weapon.Busy

Weapon Action Lock — States.Weapon.Busy

A single gameplay-tag lock that blocks weapon actions while an exclusive action is in progress. GAS-native (no custom arbitration code): exclusive actions apply Busy; everything blocks on Busy. Switching also blocks on Firing.

#The tag

SLTags.States.Weapon.Busy — native, declared in SLTags.h/.cpp (same family as Firing, SidearmActive, SidearmDrawing). Added 2026-06-13.

#Wiring matrix (ability CDOs)

AbilityActivationOwnedTagsActivationBlockedTags
Fire — PrimaryFireBase + AR/Pistol/Shotgun/PlaceholderFiringBusy (+ Dead, Meleeing, [SidearmActive on AR/Shotgun])
BP_GA_SL_SidearmMode (draw/hold)SidearmActiveBusy, RefireLock (+ Dead, Meleeing)
BP_GA_SL_MeleeMeleeing, BusyBusy (+ Dead, Meleeing)
Equip FP/TP (AR/Shotgun/Placeholder)Busy (+ Equipping on TP)RefireLock (+ Dead, [Equipping on TP])
SidearmMode and equip block on RefireLock, not Firing (see "Post-fire switch lock" below).
Draw-delay / Holster / Grenade / Reload (when built)BusyBusy, Firing

Notes:

  • Fire blocks on Busy via inheritance for Pistol & Placeholder (they don't override
  • PrimaryFireBase's blocked list); AR & Shotgun override, so Busy was added explicitly.

  • Fire does NOT own Busy — it has its own Firing tag, so auto-fire never self-blocks.
  • Equip owns Busy but does NOT block on Busy. Reason: a single weapon equip grants/runs
  • both an FP and a TP equip ability (EquipAbilityClass + FPEquipAbilityClass, granted in USLWeaponsComponent). If both blocked on Busy, whichever activated first would set Busy and cancel the other → broken equip. Owning Busy is concurrency-safe (additive); only blocking on it is the hazard. Equip blocks Firing (no swap mid-fire) which is the goal.

  • SidearmMode does NOT own Busy — it's a hold-state (LT held = sidearm out), not a
  • transient action. Owning Busy would block pistol fire while LT is held. The future draw delay (a transient) is what should own Busy.

#What it delivers

  • No fire / melee / draw during an equip or melee.
  • No drawing the sidearm or swapping weapons while firing (SidearmMode + equips block
  • Firing) — fixes the "switch to pistol mid-AR-burst → pistol full-autos" path.

  • Pistol still fires while LT held.
  • Grenade (when built) applies Busy → locks all weapon actions during the throw.

#Post-fire switch lock — RefireLock (split from Firing)

Firing (the fire ability's ActivationOwnedTag) does three jobs, all tied to PostFireDelay: drives the recovery/pump animation (bIsFiring), gates re-fire (the ability stays alive), and — until this change — blocked switching. To let the player switch to the sidearm (or cycle) before a single-shot weapon can re-fire, the switch-block was split off onto its own tag:

  • States.Weapon.RefireLock — applied by USLGameplayAbility_Fire while firing. SidearmMode and
  • the equip abilities block on RefireLock (not Firing).

  • FSLWeaponFireMode::RefireLockDuration — seconds the lock is held after a SingleShot. <= 0
  • (default) = full PostFireDelay (legacy: switch unlocks with re-fire). Set shorter to allow an early switch (e.g. interrupt a shotgun pump with a quick pistol draw). FullAuto holds it for the whole burst.

  • Firing is unchanged — animation and re-fire gating still run for the full PostFireDelay.

Implemented as a loose tag (not ActivationOwnedTags) so it can be released early; fire is LocalPredicted, so it's applied on the owning client + server (the only machines that gate switching) — no replication needed. Cleared in EndAbility (idempotent with the early-release timer).

#Weapon swap while sidearm active — guarded in C++, NOT via Busy

Cycling/picking up a main weapon while the sidearm is drawn is blocked in ASLCharacterBase::RequestEquip_Implementation (early-return on WeaponsComponent->IsSidearmActive()), which is the authority-side chokepoint for both Server_CycleWeapon and pickup auto-equip.

Why not a gameplay tag? Weapon swapping is not gated by an ability activation. RequestEquip does the real state change (FinalizeEquip — swaps EquippedWeapon, grants fire abilities, replicates) directly in C++, then separately fires the equip event for the animation. An ActivationBlockedTags/Busy entry on the equip ability would only block the animation, leaving the actual swap to proceed → mismatched state (new main weapon equipped underneath the pistol, wrong weapon on holster). So the guard must be in the swap path, not on the ability.

Consequence: picking up a weapon while the sidearm is out adds it to inventory but does not auto-equip it — holster first. Spawn/respawn first-equip is unaffected (sidearm isn't active then).

#Caveats / open items

  • Firing must be held across the whole burst. If the auto-fire ability re-activates per
  • shot rather than running one activation with an internal loop, Firing pulses and a draw/swap could slip through a between-shots gap. Verify in PIE.

  • Equip-during-draw/grenade is currently allowed (equip doesn't block Busy). If FP/TP
  • equip are confirmed to never overlap, Busy can be re-added to equip's blocked tags for stricter behavior.

  • CancelAbilitiesWithTag / ActivationBlockedTags / BlockAbilitiesWithTag only block new
  • activations — they do not cancel a running ability. To interrupt a running action (e.g. grenade cancelling auto-fire), cancel it explicitly. See feedback_gas_networking_patterns.

#Editing these via the MCP bridge

GameplayTagContainer.gameplay_tags is read-only from Python, and string→FGameplayTag coercion fails. To mint a registered tag: build a container with import_text( '(GameplayTags=((TagName="...")))') then break_gameplay_tag_container. Add/remove with GameplayTagLibrary.add_gameplay_tag / remove_gameplay_tag (return a new container), then set_editor_property on the CDO → compile_blueprintsave_asset.