Reference · Updated May 29, 2026
Fire Ability — Network Behavior Testing
Fire Ability — Network Behavior Testing
Validates the multiplayer correctness of USLGameplayAbility_Fire after the 2026-05-14 fix pass. Three behaviors are covered, each with a regression case to make sure the fix didn't change anything on single-player or local hitscan.
#PIE Setup
All tests use Editor PIE with multiple instances. The bugs only surface on a non-locally-controlled pawn (the server-side puppet of a remote client) — single-player PIE will not exercise them.
- Top toolbar → Play dropdown → Number of Players = 2 (or 3 for the cosmetic-gating test)
- Net Mode = Play As Listen Server (default) — gives both a host-controlled pawn and a
server-puppeted remote pawn, which is enough for every test below
- Run Under One Process is fine; Use Single Process with PIE windows is easier for
reading server-side log output
- For the RPC-count test (Test 3) you can also switch Net Mode to Play As Client with a separate
listen server to confirm batching across the wire — but two PIE clients on a listen host already produces the relevant LogNet output
#Console commands to keep open
Drop these into the PIE console (` key) on each instance you want to inspect:
| Command | What it shows |
|---|---|
stat net | Per-frame RPC counts, packet sizes, channel usage — easiest way to see Test 3's RPC batching |
log LogNetTraffic Verbose | Itemised RPC names and payload sizes in the output log |
log LogAbilitySystem Verbose | GAS activation, commit, end events — confirms States.Weapon.Firing lifecycle |
showdebug abilitysystem | On-screen ability state for the target pawn |
For the cosmetic-gating test (#1) keep the Output Log open on the server PIE window — that's where any spurious BP event spam would show up.
#Test 1 — Local cosmetics never run on the server for remote clients
What was wrong: ExecuteFire was calling DispatchLocalCosmetics (recoil kick, FP muzzle BP event, OnWeaponFired BP event) unconditionally. For a LocalPredicted ability the server runs ActivateAbility for every remote-client activation, so the host's server was firing BP cosmetic events for the other player's shots.
Fix: DispatchLocalCosmetics is now gated on Pawn->IsLocallyControlled().
#Setup
- Open
BP_GA_SL_AssualtRifle_PrimaryFire(or any fire ability BP)
- In
OnLocallyPredictedShotFired, add aPrint Stringset to Print to Log = true, Duration = 0,
| with the text `"[LocalShotFired] Pawn = " + Self.GetOwningActor().GetName() + " | NetMode = " + GetNetMode()` |
|---|
- Do the same in
OnWeaponFiredonBP_WeaponActor_AssaultRifle
- Compile
- PIE: 2 players, Play As Listen Server
- Open the Output Log on the server PIE window (the one with Player 0)
#Procedure
- Player 0 (host): hold the trigger to fire a burst (don't move mouse)
- Player 1 (client): hold the trigger to fire a burst from a different position
- Watch the server's Output Log
#Expected
[LocalShotFired]for Player 0 only — the host's own pawn is locally controlled on the server
- No
[LocalShotFired]lines for Player 1 — server-side puppet is not locally controlled
- Same for
OnWeaponFiredfrom the weapon actor
#Regression check (single player)
- Drop to 1 player PIE and fire —
[LocalShotFired]must still print exactly once per shot. If it stops
printing, the gate was added in the wrong place and the local player is no longer getting cosmetics.
#Removing the test instrumentation
Delete the Print String nodes after verifying, or wrap them in a bDebug check via bDebug on SLWeaponsComponent if you want a permanent toggle.
#Test 2 — Predicted hits never appear through walls
What was wrong: Listen-host damage trace and Server_ProcessShots validation used ECC_Visibility, but the remote-client predicted trace used ECC_Pawn. ECC_Pawn ignores BlockVisibility-only geometry, so a client could predict a hit on a target standing on the other side of a wall the bullet does not actually penetrate. The phantom impact decal would spawn and then never be confirmed by the server.
Fix: TracePredictedHit now uses ECC_Visibility to match server authority.
#Setup
- PIE: 2 players, Play As Listen Server
- Place two players on opposite sides of a thick wall in TestMap (the cube props near the start work)
- Use a weapon with visible predicted impact FX —
BP_WeaponActor_AssaultRifleis fine since
OnProjectileHitPredicted on its BP fires a Niagara decal
#Procedure
- Player 1 (the client) aims at Player 0 through the wall and fires
- Player 1 also aims at the wall itself and fires
- Player 1 aims around the corner with line of sight and confirms a normal hit
#Expected
- Wall corner / through-wall: predicted impact lands on the wall surface (or doesn't appear at all
if outside the trace) — never inside the room behind the wall
- Direct hit with line of sight: predicted impact at the actual surface, confirmed by the server cue
shortly after (you'll see a brief duplicate if the GC blueprint doesn't dedupe, that's fine)
- No impacts spawn deeper than what the server-side trace would allow
#Failure signature (pre-fix)
A bullet-hole decal appears on Player 0's silhouette through the wall, then no confirmed impact follows.
#Regression check
Single-player fire at a wall must still produce a predicted impact decal — if it stops appearing, the trace channel was changed but the surface in TestMap doesn't actually block Visibility.
#Test 3 — Shotgun blast = 1 RPC, not 8
What was wrong: Every pellet's FSLShotInfo was queued separately via QueueShotForValidation, and the default MaxUnvalidatedShots = 1 flushed immediately. An 8-pellet shotgun blast produced 8 Server_ProcessShots RPCs.
Fix: The fire ability collects all pellets in a local TArray<FSLShotInfo> during the pellet loop and calls the new QueueShotsForValidation(TArray) once after the loop.
#Setup
- PIE: 2 players, Play As Listen Server
- Player 1 (client) is equipped with the shotgun (
BP_WeaponActor_Shotgunonce it exists; for now
you can temporarily bump PelletCount to 8 on DA_SL_AssaultRifle to test this with the AR without waiting on the shotgun BP work)
- On Player 1's PIE window, console:
stat netandlog LogNetTraffic Verbose
- Open Player 1's Output Log
#Procedure
- Fire one shotgun blast (single trigger press)
- Inspect the log for
Server_ProcessShotsentries within the same frame range
#Expected
- Exactly 1
Server_ProcessShotsRPC per trigger pull
- That RPC's payload contains 8
FSLShotInfoentries (visible as the structured array)
stat net"RPC Count" jumps by 1 per blast, not 8
#Failure signature (pre-fix)
8 sequential Server_ProcessShots log entries for one blast, RPC count climbs by 8.
#Regression check — AR full-auto
- Set
MaxUnvalidatedShots = 1(default) onBP_WeaponActor_AssaultRifle's WeaponsComponent
- Hold fire for 1 second on the AR (≈10 shots at 600 RPM)
- Should produce 10
Server_ProcessShotsRPCs — one per shot, unchanged from before
If batching unexpectedly applies across trigger releases on the AR, the threshold/timer logic in QueueShotsForValidation was broken.
#Regression check — High-RPM batched AR
- Set
MaxUnvalidatedShots = 4on the AR's WeaponsComponent
- Hold fire for 1 second
- Should produce 2–3 RPCs (flushed every 4 shots plus a final partial flush from the timer)
#Test 4 — Dying mid-pump cleanly tears down the fire ability
What was wrong: CanBeCanceled was overridden to return false while the PostFireDelay timer was active. Death's CancelAllAbilities() path silently skipped the firing ability, leaving SLTags.States.Weapon.Firing on the ASC for up to 0.8s after death.
Fix: the CanBeCanceled override was removed entirely. The default GAS behavior allows cancellation; EndAbility clears the PostFireDelayHandle and the firing tag comes off immediately.
#Setup
- PIE: 1 player is enough (single-player exercises the cancel path)
- Open the Output Log with
log LogAbilitySystem Verboseandshowdebug abilitysystemon the pawn
- Equip the shotgun (or any weapon with
PostFireDelay > 0)
#Procedure
- Fire one shotgun blast, then immediately suicide or take lethal damage within the 0.8s pump window
- Watch the showdebug overlay and the log for the
States.Weapon.Firingtag
#Expected
States.Weapon.Firingdisappears from the ASC within one frame of death
LogAbilitySystemshows the fire ability ending withbWasCancelled=true
- Respawn proceeds normally — no stale firing state on the new pawn
#Failure signature (pre-fix)
The "Firing" tag lingers for up to 0.8s after death; respawn animations may flicker; very rarely the new pawn can be observed firing-state-true for a frame after respawn.
#Regression check
- Fire a normal blast and let it complete naturally — pump animation must still play to its full 0.8s
length (the cancel-window fix does not affect natural completion)
#Test 5 — Local player hears the fire sound exactly once per blast
What's at stake: ExecuteGameplayCue multicasts to every client including the shooter. The shooter already played FP muzzle flash + fire sound via OnLocallyPredictedShotFired. If the GC_SL_*_PrimaryFire Blueprint does not early-return when the instigator is locally controlled, every shot plays the fire sound twice on the shooter's machine.
Fix: documented as a hard rule. See Docs/WeaponsSystem.md → "Authoring a Fire Cue" and the C++ comment on FireCueTag in SLWeaponTypes.h.
#Setup
- PIE: 2 players, Play As Listen Server
- Both players hold the AR or shotgun
- Headphones recommended — the doubling is fast and easy to miss visually
#Procedure
- Player 0 (host) and Player 1 (client) each fire several bursts
- Listen to the local fire sound on each PIE window
- Compare to a single-player session firing the same weapon
#Expected
- The shooter hears the fire sound exactly once per shot
- The other player hears the same fire sound positionally at the shooter's location
- Muzzle flash visible exactly once per shot in each viewport
#Failure signature
The shooter hears a hard double-tap on every shot (one from OnLocallyPredictedShotFired, one from the cue) and/or the muzzle flash flickers as two particle systems overlap.
#Quick way to confirm the BP guard exists
Open the weapon's GC_SL_*_PrimaryFire Blueprint. At the top of OnExecute / OnBurst there must be a branch checking CueParams.Instigator → Cast to Pawn → IsLocallyControlled that returns early when true. If the branch is missing or wired the wrong way, this test will fail.
#Sanity Checklist Before Calling It Done
- [ ] Test 1 passes in 2-player PIE with the print-string instrumentation; remove instrumentation
- [ ] Test 2 passes through wall corner; predicted decal never appears past blocking geometry
- [ ] Test 3 passes — 1 RPC per shotgun blast, AR full-auto still 1 RPC per shot
- [ ] Test 4 passes —
States.Weapon.Firingclears within one frame of death-during-pump
- [ ] Test 5 passes — shooter hears the fire sound exactly once; remote players hear it positionally
- [ ] Existing shotgun checklist in
Docs/Shotgun.mdstill passes (8 impacts per blast,
ammo decrements by 1, pump delay blocks refire for 0.8s)
- [ ] No new
ensureMsgffailures in Output Log during any of the above
#Network Emulation & Multi-Instance Testing
Many of the nastiest bugs (the respawn InventoryLoaded race, the melee stuck-tag) are timing / RPC-ordering races that rarely reproduce on a fast localhost. Manufacture the bad conditions:
#Inject latency / loss / reordering (single machine)
- Editor Preferences → Level Editor → Play → Network Emulation — enable, pick a profile
(Average, Bad, or custom lag/jitter/loss). Applies to all PIE instances.
- Or per-instance console:
Net PktLag=120,Net PktLoss=5,Net PktOrder=1,Net PktJitter=40.
- Re-run any respawn / equip / melee flow under emulation — races that "happened once" become
reliably reproducible. This is the cheapest way to stress replication-sensitive features.
#Separate processes (closer to real than PIE)
- PIE shares one process and throttles background windows ("Use Less CPU when in Background" in
Play prefs) — this alone can stall an unfocused client mid-respawn. Disable that setting for any multiplayer PIE test, or use Standalone.
- Standalone / packaged instances: launch the packaged
.exemultiple times — one?listen,
others 127.0.0.1. Real separate processes, no focus throttling. The authoritative single-machine test.
#Different machines (LAN) — the real test
- Same build on each machine; host runs
?listen, clientsopen <HostLANIP>; allow UDP 7777.
- You don't need a human on every machine — an idle connected client still verifies that a remote
client receives replication (respawns, melee, fire) correctly. That alone surfaces the race class.
#Standard pass for any networked feature
- [ ] Works in 2-3 player Standalone (not just PIE)
- [ ] Works under
Net PktLag=120 PktLoss=5 PktOrder=1
- [ ] Works with one client left idle/backgrounded during the tested flow
- [ ] "Use Less CPU when in Background" disabled while PIE-testing multiplayer
#Why These Tests Catch What They Catch
Single-player PIE will never trigger Test 1's bug because there is no separation between "locally controlled" and "server-puppeted remote pawn" — the local player is both. The bug only appears when the server is running a fire ability for a pawn it does not locally control. Any test that runs only in a single editor instance is structurally unable to see this class of error.
Same applies to Test 3: a single client's Server_ProcessShots count can only be observed on the authority side, so even if you instrument the client, you need the server window to confirm what actually arrived. stat net on a 2-player PIE setup is the cheapest way to read this without setting up a real dedicated server.