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.

  1. Top toolbar → Play dropdown → Number of Players = 2 (or 3 for the cosmetic-gating test)
  1. Net Mode = Play As Listen Server (default) — gives both a host-controlled pawn and a
  2. server-puppeted remote pawn, which is enough for every test below

  1. Run Under One Process is fine; Use Single Process with PIE windows is easier for
  2. reading server-side log output

  1. For the RPC-count test (Test 3) you can also switch Net Mode to Play As Client with a separate
  2. 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:

CommandWhat it shows
stat netPer-frame RPC counts, packet sizes, channel usage — easiest way to see Test 3's RPC batching
log LogNetTraffic VerboseItemised RPC names and payload sizes in the output log
log LogAbilitySystem VerboseGAS activation, commit, end events — confirms States.Weapon.Firing lifecycle
showdebug abilitysystemOn-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

  1. Open BP_GA_SL_AssualtRifle_PrimaryFire (or any fire ability BP)
  1. In OnLocallyPredictedShotFired, add a Print String set to Print to Log = true, Duration = 0,
with the text `"[LocalShotFired] Pawn = " + Self.GetOwningActor().GetName() + "NetMode = " + GetNetMode()`
  1. Do the same in OnWeaponFired on BP_WeaponActor_AssaultRifle
  1. Compile
  1. PIE: 2 players, Play As Listen Server
  1. 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 OnWeaponFired from 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

  1. PIE: 2 players, Play As Listen Server
  1. Place two players on opposite sides of a thick wall in TestMap (the cube props near the start work)
  1. Use a weapon with visible predicted impact FX — BP_WeaponActor_AssaultRifle is fine since
  2. 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

  1. PIE: 2 players, Play As Listen Server
  1. Player 1 (client) is equipped with the shotgun (BP_WeaponActor_Shotgun once it exists; for now
  2. you can temporarily bump PelletCount to 8 on DA_SL_AssaultRifle to test this with the AR without waiting on the shotgun BP work)

  1. On Player 1's PIE window, console: stat net and log LogNetTraffic Verbose
  1. Open Player 1's Output Log

#Procedure

  • Fire one shotgun blast (single trigger press)
  • Inspect the log for Server_ProcessShots entries within the same frame range

#Expected

  • Exactly 1 Server_ProcessShots RPC per trigger pull
  • That RPC's payload contains 8 FSLShotInfo entries (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) on BP_WeaponActor_AssaultRifle's WeaponsComponent
  • Hold fire for 1 second on the AR (≈10 shots at 600 RPM)
  • Should produce 10 Server_ProcessShots RPCs — 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 = 4 on 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

  1. PIE: 1 player is enough (single-player exercises the cancel path)
  1. Open the Output Log with log LogAbilitySystem Verbose and showdebug abilitysystem on the pawn
  1. 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.Firing tag

#Expected

  • States.Weapon.Firing disappears from the ASC within one frame of death
  • LogAbilitySystem shows the fire ability ending with bWasCancelled=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

  1. PIE: 2 players, Play As Listen Server
  1. Both players hold the AR or shotgun
  1. 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.Firing clears 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.md still passes (8 impacts per blast,
  • ammo decrements by 1, pump delay blocks refire for 0.8s)

  • [ ] No new ensureMsgf failures 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 .exe multiple 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, clients open <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.