Now · Updated Jun 26, 2026
System Link — Bug Tracker
System Link — Bug Tracker
Chronological record of bugs found during development. Each entry has the Bug, Root Cause Analysis (RCA), and Fix.
#⚠️ Before You Debug a Crash — Known Environment / Hardware Issues (NOT SystemLink bugs)
Read this first when you hit an editor or PIE crash. Some failures originate in the dev machine, not the project, and chasing them in the codebase is wasted effort.
#GPU "device hung" / DXGI_ERROR_DEVICE_HUNG crashes → suspect the CPU, not the game
Dev rig: Intel i9-14900KF + RTX 4080 SUPER, NVIDIA driver 581.95, UE 5.7.
The signature to recognize (seen 2026-06-08): editor GPU crash with
LogD3D12RHI: GPU crash detected: Device 0 Removed: DXGI_ERROR_DEVICE_HUNG
- NVIDIA Aftermath:
Status: Timeout,Page Fault Info: No information on faulting address
- GPU breadcrumb stuck on generic engine work (
UpdateAllPrimitiveSceneInfos/
FRDGBuilder::SubmitBufferUploads), not a named SystemLink material/effect/pass.
Why it's not us: when a real asset/shader kills the GPU, Aftermath reports a faulting address or pins a specific render pass. A generic breadcrumb with no page fault is the classic fingerprint of Intel 13th/14th-gen Raptor Lake i9 instability — an unstable CPU feeds a subtly-corrupt command stream and the GPU hangs as the victim. It commonly shows up in Unreal as "device hung" / "out of video memory," and can trigger at idle/light load.
Before blaming the project:
- BIOS → apply Intel Default Settings power profile (PL/IccMax at Intel spec, motherboard
"MCE/unlimited power" OFF). Ensure latest microcode (rig was on 0x12F — current).
- Don't leave the editor open 12–20h; restart periodically. Clean-install the NVIDIA Studio driver.
- If it also crashes under sustained load (cooking, shader compiles, gaming), the chip may be
permanently degraded — Intel extended the warranty to 5 years for this; RMA is an option.
Full analysis of the 2026-06-08 crash: dump was Saved/Logs/D3D12.0.2026.06.08-08.18.04.nv-gpudmp
SystemLink.log. Idle/background overnight (AppHasFocus=false, ~20h uptime), Sequencer layout open
(context, not cause).
#BUG-001 — Shotgun fire animation plays once then never replays
Date: 2026-05-19 Branch: melee-ability-polish
Bug: Shotgun fire animation in ABP_MC_TP_Shotgun Firing state played correctly on the first shot, then never played again on subsequent shots.
RCA: Non-looping asset players in a state machine state retain their time position across re-entries. After the first play, the asset player sits at t=end. Re-entering the state finds nothing left to play. AR didn't hit this because its fire anim is looping (wraps around continuously).
Fix: Enabled Always Reset on Entry on the Firing state in ABP_MC_TP_Shotgun. Asset player resets to t=0 on every state entry.
#BUG-002 — Inertialization warning on editor quit
Date: 2026-05-19 Branch: melee-ability-polish
Bug: UE printed a warning on PIE stop about Inertial Blend logic having no Inertialization node downstream in ABP_MC_TP_Shotgun.
RCA: The Not Firing → Firing transition used Inertial Blend but had no Inertialization node placed in the AnimGraph to consume it.
Fix: Added a single Inertialization node between the state machine output and the Layered Blend Per Bone in ABP_MC_TP_Shotgun.
#BUG-003 — Sequencer editor crash on save with Possessable bindings + constraints
Date: 2026-05-19 Branch: melee-ability-polish
Bug: Editor crashed on save/load of a Sequencer sequence after adding constraints to a Possessable binding. Asset LS_TP_Shotgun_Idle2.uasset corrupted.
RCA: Possessable bindings + constraints cause a TransformableComponentHandle harvest failure on save. The handle can't resolve the Possessable reference during serialization.
Fix: Every Sequencer binding must be converted to a Spawnable before adding any constraints. Right-click binding → Convert to Spawnable. See Docs/SequencerAuthoringWorkflow.md.
#BUG-004 — Respawn stuck — player unable to respawn after death
Date: ~2026-05-20 Branch: melee-ability-polish
Bug: After dying, the respawn flow stalled. Player stayed dead permanently.
RCA: See Docs/RespawnSystem.md for full RCA. Short version: InventoryLoaded state flag was not cleared before OnPawnInitialized on the new pawn, causing the equip flow to be skipped.
Fix: Clear stale InventoryLoaded before OnPawnInitialized. Use Client_OnLoadoutReady RPC (not OnRep_CarriedWeapons) to set InventoryLoaded. See feedback_respawn_inventory_flow.md.
#BUG-005 — Hit direction incorrect — using controller location instead of hit location
Date: ~2026-05-20 Branch: melee-ability (or health)
Bug: Hit direction indicator on the HUD pointed the wrong way. Hit reactions played in the wrong cardinal direction.
RCA: GetInstigator() on a GameplayEffectContext returns the Controller, which has no meaningful world location (it floats above the pawn or at origin). Subtracting controller location from victim location produced a wrong direction vector.
Fix: Use GetEffectCauser() instead of GetInstigator() to get the world location of the damage source. See feedback_gas_effect_context_causer.md.
#BUG-006 — Extra bullets fired per respawn (accumulating fire abilities)
Date: ~2026-04-16 Branch: weapon-swap
Bug: After each respawn, the player fired one additional bullet per shot. After 3 respawns, 3 extra bullets fired per trigger pull.
RCA: Fire ability FSLAbilitySet_GrantedHandles were stored on USLWeaponsComponent (pawn). Pawn is destroyed on respawn, zeroing out the component-local handles. TakeFromASC became a no-op (handles were zero/invalid). Fire ability specs accumulated on the ASC (on PlayerState, which survives respawn) without ever being revoked.
Fix: All GAS handles for player abilities must live on ASLPlayerState, not the pawn or any component. See feedback_ability_handles_playerstate.md.
#BUG-007 — HUD not initialized on first spawn (PossessedBy before HUD BeginPlay)
Date: ~2026-05-22 Branch: hud / weapon-swap
Bug: On first spawn, HUD widgets were not initialized — weapon name, ammo count blank. Worked fine on respawn.
RCA: PossessedBy fires before ASLPlayerHUD::BeginPlay constructs the HUD widget. The initialization call in PossessedBy tried to find a widget that didn't exist yet.
Fix: ASLPlayerHUD::BeginPlay calls InitializeLocalPlayerHUD on the already-possessed pawn after widget construction. See feedback_hud_init_timing.md.
#BUG-008 — Blueprint reparent causes fatal World Leak crash (REINST_ stuck in TransBuffer)
Date: Multiple occurrences
Bug: Reparenting a Blueprint mid-session causes a fatal crash on the next PIE stop or save. Log shows REINST_ class stuck in TransBuffer.
RCA: Reparenting a BP creates a REINST_ intermediate class. If the editor holds a reference to it in the transaction buffer, world teardown fails.
Fix: After reparenting a BP, always restart the editor before running PIE. See feedback_blueprint_reparent_crash.md.
#BUG-009 — Muzzle socket warning in ResolveMuzzleLocation when no weapon mesh
Date: 2026-05-XX Branch: melee-ability-polish
Bug: ResolveMuzzleLocation printed a warning when the character had no weapon mesh set (e.g. unarmed state).
RCA: Code attempted to get a socket location from an invalid/empty skeletal mesh component.
Fix: Added a validity check before socket lookup in ResolveMuzzleLocation. Returns character eye location as fallback when no mesh is present.
#BUG-010 — Two pistols visible in FP view when sidearm drawn
Date: 2026-06-05 Branch: sidearm-initial-imp
Bug: When the local player held LT to draw the sidearm in first-person view, two pistol meshes appeared — one for FPSidearmMesh and one for TPSidearmMesh.
RCA: OnSidearmTagChanged showed both FPSidearmMesh and TPSidearmMesh when the SidearmActive tag was applied, regardless of current view mode. TPSidearmMesh uses lighting channel 0 so it is visible to the local camera in FP.
Fix: Added IsFirstPersonView() check in OnSidearmTagChanged. In FP: show FPSidearmMesh, hide TPSidearmMesh. In TP: show TPSidearmMesh, hide FPSidearmMesh. Also requires ApplyViewMode BP implementation to swap sidearm mesh visibility when switching view modes.
#BUG-011 — TP sidearm mesh not visible on observing clients
Date: 2026-06-06 Branch: sidearm-initial-imp
Bug: A remote client watching another player draw their sidearm could not see the pistol mesh. The TPSidearmMesh stayed hidden.
RCA: Two compounding causes:
RegisterGameplayTagEventwithNewOrRemoveddoes not fire for a tag that is already active at bind time. Clients that joined mid-draw (or where binding was delayed) never received the "tag added" callback.
GetPawnASC()can return null duringOnRep_EquippedWeaponfor simulated proxies because the PlayerState hasn't replicated yet. The callback binding silently failed with no retry.
Fix:
- After calling
RegisterGameplayTagEvent, immediately checkGetTagCountand manually callOnSidearmTagChangedif the tag is already active.
- Added
BindSidearmTagCallback()call insideUpdateSidearmMesh(called fromOnRep_Sidearm) as a second retry point that fires later in the replication sequence when PlayerState is more likely valid.
See feedback_gas_tag_callback_patterns.md.
#BUG-012 — Sidearm mesh spawns at character's feet
Date: 2026-06-05 Branch: sidearm-initial-imp
Bug: After drawing the sidearm, the pistol mesh appeared at the character's feet instead of in the left hand.
RCA: UpdateSidearmMesh called SetSkeletalMeshAsset to assign the mesh but never called AttachToComponent to snap it to the socket. The component remained at its default attachment position (character root).
Fix: Added AttachToComponent call in UpdateSidearmMesh using AttachSocketName from the data asset's FSLWeaponSkeletalMeshData, with SnapToTargetNotIncludingScale transform rules.
#BUG-013 — Sidearm actor orphaned on player death (open)
Date: 2026-06-06 Branch: sidearm-initial-imp Status: Open — needs BP death cleanup work
Bug: When a player dies, the sidearm weapon actor is never destroyed or dropped. DropWeaponActor in the death cleanup BP iterates CarriedWeapons, but SidearmWeapon is a separate slot not in that array. The sidearm actor persists alive and attached to the ragdoll. On respawn, a new sidearm is spawned via LoadDefaultLoadout while the old one remains orphaned in the level.
RCA: Death cleanup was authored before the sidearm slot existed. No code path destroys or nulls SidearmWeapon on death. SetSidearmWeapon(nullptr) would drop it as a world pickup (undesirable — design says sidearm is non-droppable on death). A destroy-not-drop path doesn't exist yet.
Fix (pending): Add a DestroySidearmOnDeath() function that destroys the sidearm actor directly (no pickup spawn) and nulls SidearmWeapon. Call it from the death cleanup BP alongside DropWeaponActor for CarriedWeapons.
#BUG-014 — Pistol mesh invisible on listen-server host when viewing a client with the sidearm drawn
Date: 2026-06-08 Branch: sidearm-initial-imp
Bug: As the listen-server host, looking at a client player who had the sidearm drawn, the pistol mesh did not appear — even though the client's character was correctly in the sidearm pose.
RCA: The sidearm animation reads SidearmActive by polling HasMatchingGameplayTag every tick in BuildAnimSnapshots (reliable on all machines), but sidearm mesh visibility was driven by a one-shot RegisterGameplayTagEvent callback (OnSidearmTagChanged). On the listen-server host, that event path is unreliable for a simulated proxy (no OnRep on the server, bind/already-active timing differs from a pure client), so the visibility toggle never reached the mesh. The pose showed because it polls; the mesh stayed hidden because it relied on the event. The mesh was attached — SetSidearmWeapon calls OnRep_Sidearm directly on authority — only its visibility was stuck.
Fix: Drive visibility from the same per-tick poll the animation uses. Extracted the FP/TP SetHiddenInGame logic into USLWeaponsComponent::UpdateSidearmMeshVisibility(bool), called every tick from ASLCharacterBase::BuildAnimSnapshots with the polled bIsSidearmActive. OnSidearmTagChanged now delegates to it (kept as a harmless fast-path). SetHiddenInGame is idempotent so per-tick is cheap; BuildAnimSnapshots early-returns on dedicated servers. Confirmed in PIE 2026-06-08.
Lesson: Don't drive must-be-correct visual state from one-shot tag events on a listen server — poll it, same family as the AnimNotify-server-unreliable rule. See memory feedback_gas_tag_callback_patterns Rule 3.
#BUG-015 — Main-weapon fire FX play when firing the sidearm (pistol)
Date: 2026-06-10 Branch: sidearm-initial-imp
Bug: Firing the pistol (sidearm drawn) showed the equipped main weapon's fire FX instead of the pistol's. Observers saw the AR/Shotgun fire cue; the local shooter saw the main weapon's FP muzzle flash. No actual second shot — the main weapon's fire ability was correctly blocked by SidearmActive; this was cosmetic only.
RCA: Two independent cosmetic leaks, both downstream of duplicating the AR setup for the pistol:
- Cue:
GC_SL_Pistol_PrimaryFire/_Impactwere duplicated from the AR cues and kept the AR'sGameplayCue.Weapon.AssaultRifle.tag, andDA_SL_Pistol'sFireCueTag/ImpactCueTagstill pointed at the AR tags. So the pistol executed the AR cue and twoGameplayCueNotify_Burstassets shared one tag (an ambiguous collision affecting the AR too). Note the C++ dispatch was correct —DispatchFireCueroutes throughGetActiveFireMode()(the sidearm when drawn); only the asset tag wiring* was wrong.
- Local FP: the pistol fire ability's
OnLocallyPredictedShotFired(duplicated from the AR) spawned the muzzle flash attached to the main FP weapon mesh (GetFPWeaponMesh) instead of the sidearm mesh.
Fix:
- Registered
GameplayCue.Weapon.Pistol.PrimaryFire/.Impact, set theGC_SL_Pistol_*blueprints' Gameplay Cue Tags to them, and repointedDA_SL_Pistol'sFireCueTag/ImpactCueTag— clears the collision and gives the pistol its own cue. (Verified in PIE: local + cue both fire correctly.)
- Repointed the local FP muzzle-flash attach to the sidearm mesh (BP). Added
ASLCharacterBase::GetActiveWeaponMesh()(BlueprintPure, view- + slot-aware) so fire FX attach to whichever weapon is actually firing — FP pistol mesh on the shooter, TP pistol mesh on observers — without FP/TP branching in BP. (Helper pending rebuild.)
Lesson: When duplicating a weapon's cue/ability assets for a new weapon, immediately re-tag the cue and repoint the data asset — a duplicated GameplayCueNotify that keeps the source tag silently collides on that tag. Attach fire FX to GetActiveWeaponMesh(), never a hardcoded main-weapon mesh getter.
#BUG-016 — Sidearm animations stop after swapping the main weapon
Date: 2026-06-10 Branch: sidearm-initial-imp Status: Fixed in code — pending rebuild + PIE verification
Bug: With the pistol carried, equipping a different main weapon and back (e.g. Shotgun → AR) stopped the pistol's animations from playing — the sidearm pose went static.
RCA: The sidearm arm pose is a linked anim layer (ALI_SL_Sidearm) applied only in USLWeaponsComponent::UpdateSidearmMesh, which runs on a sidearm-slot change. The main-weapon equip path (FSLWeaponViewDriverBase::UnequipWeapon) cleared layers with LinkAnimClassLayers(nullptr), which unlinks every linked layer — including the sidearm's — and the equip then re-linked only the new main weapon's layer. Because the sidearm slot didn't change, its layer was never re-linked, so it stayed unlinked after the swap.
Fix: Replaced the all-layer nuke in UnequipWeapon with a targeted UnlinkAnimClassLayers(EquippedWeaponData.AnimLayerClass) (removes only the outgoing main weapon's layer; the sidearm uses a different interface so it survives), and removed the second all-layer clear in the "Ensure layers" fallback. Relies on the sidearm (ALI_SL_Sidearm) and main weapons (ALI_SL_Weapon) being distinct anim-layer interfaces.
Lesson: LinkAnimClassLayers(nullptr) is a global unlink. To remove one weapon's layer while other layers (sidearm) must persist, use targeted UnlinkAnimClassLayers(LayerClass).
#BUG-017 — Double footstep notifies with the Sidearm Blend node (both locomotion sets fire at once)
Date: 2026-06-12 Branch: sidearm-initial-imp Status: Fixed in code — pending rebuild + PIE verification
Bug: With the custom Sidearm Blend AnimGraph node in use, footstep anim notifies fired twice — both the sidearm walk/run blendspace and the main-weapon (default) walk/run blendspace played their footstep notifies simultaneously, even while holstered (only the default pose should be audible).
RCA: FAnimNode_SidearmBlend::Update_AnyThread called SidearmPose.Update(), LowerPose.Update(), and DefaultPose.Update() unconditionally at full weight every frame, while Evaluate_AnyThread was correctly lazy (pass-through to DefaultPose when CurrentAlpha == 0). Anim notifies fire during the Update phase as a sequence/blendspace player advances its play time — independent of the pose's output weight in Evaluate. So the holstered branches kept ticking and firing their footstep notifies alongside the active branch. Update and Evaluate relevance were out of sync.
Fix: Gated the per-pose Updates by relevance and passed weight-scaled contexts, matching FAnimNode_LayeredBoneBlend:
bSidearmRelevant = CurrentAlpha > ZERO_ANIMWEIGHT_THRESH→ updateSidearmPose/LowerPoseviaContext.FractionalWeight(CurrentAlpha).
bDefaultRelevant = CurrentAlpha < 1 - ZERO_ANIMWEIGHT_THRESH→ updateDefaultPoseviaContext.FractionalWeight(1 - CurrentAlpha).
Steady-state now ticks only the contributing side (holstered = default only; drawn = sidearm only). During the short crossfade both tick at fractional weight so the engine's weight-based notify filtering can suppress the fading-out side.
Lesson: A custom blend anim node's Update_AnyThread relevance must stay in sync with Evaluate_AnyThread. Notifies fire on Update regardless of Evaluate output weight — only tick poses that actually contribute, and pass Context.FractionalWeight(weight) so the engine can weight-filter notifies during a blend. Same family as BUG-001's "Always Reset on Entry."
#BUG-018 — Sidearm pistol fires full-auto when drawn mid-AR-burst (+ spam-tappable on release)
Date: 2026-06-13 Branch: sidearm-initial-imp Status: Fixed (Busy/Firing block live in BP; StopPrimaryFire fix pending rebuild) — PIE verification pending
Bug: Holding RT to fire the AR (full-auto), then tapping LT to draw the sidearm, made the pistol fire full-auto at the AR's cadence. Separately, the single-shot pistol could be tapped faster than its fire rate when drawn over a full-auto main weapon.
RCA — two independent issues:
- Full-auto carryover. Fire input (
PrimaryFireAction) is boundStarted-only (one event per pull). A FullAuto weapon's fire ability is a single activation that loops internally while RT is held, owningStates.Weapon.Firingthe whole time. Each loop iteration resolvesGetActiveFireWeapon(). Drawing the sidearm mid-loop flipped the active weapon to the pistol, so the already-running AR loop kept firing the pistol at the AR's full-auto rate.ActivationBlockedTagsdoesn't cancel a running ability, soBP_GA_SL_SidearmModeactivating didn't stop the loop.
- Premature cancel / spam.
ASLPlayerController::StopPrimaryFiredecided whether to cancel-on-release by readingWC->GetPrimaryFireMode()— the equipped/main weapon's mode — not the active (sidearm) mode. With a FullAuto main weapon + SingleShot sidearm drawn, releasing RT fell through toCancelAbilities(PrimaryFire), cancelling the pistol's fire ability and clearing itsPostFireDelay(0.2s) → the player could spam-tap the pistol faster than 400 RPM.
Fix:
- Weapon Action Lock (
Docs/WeaponActionLock.md):BP_GA_SL_SidearmModenow listsStates.Weapon.FiringinActivationBlockedTags, so the sidearm can't be drawn while the AR fire loop holdsFiring→ no mid-burst weapon switch → no full-auto carryover. (The AR loop owningFiringcontinuously is what makes this block solid.)
StopPrimaryFirenow readsWC->GetActiveFireMode(/bPrimary=/true)instead ofGetPrimaryFireMode(), so the cancel-on-release decision uses the sidearm's SingleShot mode when it's drawn — preserving itsPostFireDelaygate.
Lesson: With the "active fire weapon" indirection, every fire-path decision must go through the active getters (GetActiveFireMode/GetActiveFireWeaponData), never the equipped-weapon getters — StopPrimaryFire was a missed site from the original sidearm fire routing. And remember ActivationBlockedTags/BlockAbilitiesWithTag block new activations only; a running auto-fire loop is stopped by blocking the switch (via Firing), not by blocking the fire ability itself.
#BUG-019 — A dropped weapon hangs in mid-air when dropping all weapons on death
Date: 2026-06-14 Branch: sidearm-initial-imp Status: Fixed in code — pending rebuild + PIE verification
Bug: With DropAllWeaponsOnDeath dropping the full loadout on death (2 carried + the sidearm), one of the dropped weapon pickups would freeze floating in mid-air instead of arcing to the ground.
RCA: Pickups (ASLPickupBase) don't use rigid-body physics — they fall via a UProjectileMovementComponent (bShouldBounce, MaxBounces = 2) whose root BounceCollision sphere blocked both WorldStatic and WorldDynamic. Pickups themselves are ECC_WorldDynamic. All drops spawn at the same point (DropSpawnOffset = (40,0,50), only a 40 cm radius), so the 35 cm bounce spheres of the 3 simultaneously-launched pickups overlapped and bounced off each other — racking up MaxBounces immediately → StopMovementImmediately() → frozen mid-air. Only surfaced now because the old BP Drop Loot dropped fewer weapons (and never the sidearm); 3 at once tipped it over. (The frozen weapon looked like a shotgun only because the unfinished BP_SL_WeaponPickup_Pistol still shows the duplicated shotgun mesh — cosmetic, unrelated.)
Fix: First attempt blocked WorldStatic only (dropped the WorldDynamic block) — but that made pickups fall through any floor that isn't WorldStatic (the test-level floor was movable/dynamic). Final fix keeps blocking both WorldStatic and WorldDynamic (bounces off any floor), and instead suppresses pickup-vs-pickup collision per-actor: LaunchPickup calls BounceCollision->IgnoreActorWhenMoving(OtherPickup, true) mutually for every other ASLPickupBase (via GetAllActorsOfClass). Pickups pass through each other but still bounce off world geometry, regardless of how the floor is set up. Base-class fix → covers every multi-drop.
Lesson: ProjectileMovementComponent + bShouldBounce + low MaxBounces means any overlap at launch (including siblings of the same object type) can instantly stop the projectile. Don't solve it by un-blocking a world object channel (breaks bouncing off that floor type) — keep the world bounce and exclude just the sibling actors with IgnoreActorWhenMoving.
#BUG-020 — Sidearm ammo HUD reads 0 on the listen-server host (correct on clients)
Date: 2026-06-16 Branch: sidearm-initial-imp Status: Fixed — verified on listen server (host + client both read the correct count from spawn)
Bug: The new sidearm ammo indicator showed 0 until the first shot — but only on the listen-server HOST. Clients displayed the correct count (e.g. 24) immediately. Confirmed with print strings in the widget's OnAmmoChanged: Server: 0, Client 1: 24. The first pistol shot snapped the host's display to the real value.
RCA: USLAmmoWidget::SetWeapon binds the widget to the sidearm actor's OnAmmoChanged and shows whatever CurrentAmmo is at bind time, then updates on later broadcasts. The actor's initial fill happens in ASLWeaponActor::BeginPlay via a raw assignment CurrentAmmo = GetMaxAmmo() that does not broadcast OnAmmoChanged.
- Clients were correct anyway:
CurrentAmmoreplicates withREPNOTIFY_Always, soOnRep_CurrentAmmofires and broadcasts the value to the bound widget.
- Host (authority) has no
OnRep. The sidearm indicator binds around HUD-init time — beforeBeginPlayset the value — so its initial push read0, and nothing ever re-broadcast the fill. Only the first shot'sSetCurrentAmmo(which does broadcast) corrected it.
This is the authority-side mirror of the same gap the REPNOTIFY_Always on CurrentAmmo already fixed for clients (see the comment in GetLifetimeReplicatedProps).
Fix: ASLWeaponActor::BeginPlay now calls SetCurrentAmmo(GetMaxAmmo()) instead of the raw assign. SetCurrentAmmo broadcasts OnAmmoChanged on a value change (0 → MaxAmmo), so the host's already-bound widget receives the initial fill — mirroring OnRep for clients. Safe across bind orderings (late binders still read the value via SetWeapon's push; early binders now get the broadcast) and authority-only (inside HasAuthority()), so clients are unaffected. Function-body-only change → Live Coding picked it up without a restart.
Lesson: Authority-side initial state must broadcast for any UI bound before that init — clients lean on REPNOTIFY_Always, but the listen-server host has no OnRep to compensate. When a replicated value is seeded with a raw assign on authority, early-bound listeners silently miss it; use the setter that broadcasts (or broadcast explicitly). Same family as BUG-014 (don't rely on one-shot/no-broadcast paths on the host — poll or broadcast).
#BUG-021 — Sidearm draw silently lost when firing at the same instant (controller)
Date: 2026-06-16 Branch: grenade-initial-imp (sidearm fix, found during grenade work) Status: Fixed in code — pending PIE re-verify
Bug: Playtesting on a controller, pulling RT (fire) and LT (draw sidearm) at the same time would "cancel" the draw — the sidearm never appeared, even though LT was still held. Releasing and re-pulling LT worked.
RCA: Not a cancel — a swallowed one-shot input. Confirmed config (read live via the MCP bridge):
GA_SidearmMode.ActivationBlockedTags=Dead, Meleeing, Busy, RefireLock(noCancelAbilitiesWithTag— nothing cancels a running draw).
- The fire abilities apply
Firing(owned) andRefireLock(the loose tag held forRefireLockDurationafter a shot — the BUG-018 lock).
- The draw is dispatched as a one-shot
GameplayEventon LTStarted(ASLPlayerController::StartSidearmDraw → HandleGameplayEvent(Events.Weapon.SidearmDraw)).
When fire wins the same-frame race, the shot applies RefireLock; the SidearmDraw event then finds GA_SidearmMode blocked, so it never activates — and because it was a one-shot event, the intent is lost even though LT is still held. SidearmActive never sets, so there's no real draw (the "cancelled animation" is just the draw silently failing). Nothing retries until LT is released and re-pulled (by which time RefireLock has cleared).
Fix: Keep the RefireLock block (it's the BUG-018 fix — prevents drawing mid-burst → full-auto pistol). Instead make the held LT retry: bind SidearmAction on ETriggerEvent::Triggered (fires every frame held) → StartSidearmDraw, and guard StartSidearmDraw to no-op once States.Weapon.SidearmActive is set. The held LT now re-sends the draw each frame until the transient block clears (~RefireLockDuration), then draws; the guard stops the retry the instant it succeeds, so no event spam and no double-activation. Controller-only change; BUG-018 intact.
Lesson: A one-shot HandleGameplayEvent on a held input is fragile — if activation is transiently blocked at the press instant (a lock tag, cooldown, another ability), the intent is silently dropped. For held inputs, re-attempt on Triggered with an "already-active" guard (or buffer the intent), so the action fires as soon as the block clears instead of requiring a re-press.
#BUG-022 — Character frozen in the holster pose when pumping the draw (left trigger)
Date: 2026-06-17 Branch: grenade-initial-imp (sidearm fix during grenade work) Status: Fixed (Anim Blueprint)
Bug: Rapidly pumping LT (draw/holster the sidearm) could leave the character frozen in the holster pose indefinitely.
RCA: Confirmed via a gated on-screen debug readout (bDebugSidearm in BuildAnimSnapshots, printing the sidearm tags + anim flags + GetActiveReticleClass). When stuck: ActiveTag cnt=0 DrawingTag cnt=0 AnimDraw=0 AnimHolster=0 HolLeft=-20.83 ShouldReticle=<AR>. So every gameplay/cosmetic flag was correctly clear and the C++ latch self-cleared — the freeze was purely in the ABP. The Holster state plays a non-looping clip (the draw reversed); rapid re-entry retained the asset player's time at t=end (the holstered pose) and the state didn't transition back out. Classic non-looping-state issue — the BUG-001 family (feedback_animbp_state_reset_on_entry).
Console note:showdebug abilitysystemwas unusable here — opening the console steals input focus, which released the held LT and changed the state. The on-screenbDebugSidearmprint (fixed-key, updates in place) was the only way to read state mid-bug. Kept (gated, off by default) for future sidearm/anim debugging.
Fix: On the ABP Holster state, enable "Always Reset on Entry" (reset to t=0 each entry) and use an explicit NOT bIsSidearmHolstering exit transition (+ Inertialization) rather than an automatic time-remaining rule (which deadlocks when the non-looping asset is parked at t=end).
Lesson: Same as BUG-001 — every non-looping state-machine state needs "Always Reset on Entry." And when a stuck-state bug can't use the console (input-focus-sensitive, like held-trigger states), a gated on-screen debug readout of the relevant flags is the fastest way to split "C++ logic bug" from "ABP stuck" — here it instantly exonerated the C++.
#BUG-023 — Clients can't throw grenades (montage plays, no grenade spawns)
Date: 2026-06-22 Branch: grenade-initial-imp Status: Fixed (C++) — pending rebuild + the one BP rewire below
Bug: On a listen server, the host throws grenades fine, but a client sees the throw montage play with no grenade spawned. Deterministic for clients. (Same root cause as the earlier intermittent FP "no grenade" misfire.)
RCA: The authoritative spawn (ASLCharacterBase::ThrowActiveGrenade) was being triggered from the throw montage's Events.Grenade.Release anim notify. Anim notifies depend on the mesh ticking its montage — the server does not reliably tick a remote client's montage (VisibilityBasedAnimTickOption / not rendering), so the Release event never fires on the server instance of the client's Local-Predicted ability → ThrowActiveGrenade never runs → no spawn. The host's own pawn mesh ticks locally, so the host works. The sibling intermittent FP bug was the same fragility via a different trigger: a BP Delay feeding the spawn could be cancelled when a montage blend-out fired EndAbility first (~17–50 ms margin).
Fix: Decouple the spawn from both the anim notify and the ability lifetime. New authority-only ASLCharacterBase::BeginGrenadeThrow() (called once on ability activation) schedules ThrowActiveGrenade on a world timer on the character (GetWorldTimerManager, ThrowReleaseTime later). A world timer fires regardless of mesh-tick or whether the ability is still active. BP change required: GA_ThrowGrenade must call BeginGrenadeThrow once after the count gate, instead of routing the Release notify (or a Delay) into ThrowActiveGrenade.
Lesson: Authoritative gameplay effects must never hinge on anim notifies in multiplayer — the server doesn't tick remote clients' montages, so a notify that works for the listen-server host silently no-ops for clients. Drive authoritative timing off a world timer (or an ability task), never the mesh. Reuses the ThrowReleaseTime data field added precisely for this.
#BUG-024 — Other players' equip sound heard map-wide (no distance falloff)
Date: 2026-06-24 · Consolidated audit: Docs/AudioAudit.md Branch: grenade-initial-imp Status: Fixed (C++) — pending the BP cue-node swap below
Bug: During multiplayer play-testing, when a remote player dies and respawns you hear their weapon equip sound at full volume no matter where you are on the map. Most noticeable on respawn (the equip-first flow always fires an equip), but applies to any remote equip.
RCA (corrected — first guess was wrong, see note): USLGameplayAbility_Equip::ExecuteEquipCue fires ASC->ExecuteGameplayCue(EquipCueTag), which GAS multicasts to every client (correct — it's a third-person cosmetic), so a far player's equip cue executes on your client. The cue (GC_SL_AssaultRifle_Equip / GC_SL_Shotgun_Equip) is a GameplayCueNotify_Burst whose burst_sounds[0] = Rifle_Raise_Cue (which does have WeaponHandling_att attenuation), spawn policy AlwaysPlay, placement DoNotAttach. DoNotAttach → the Burst notify spawns the sound at the cue's world location. But ExecuteEquipCue never set CueParams.Location, so the sound spawned at world origin (0,0,0). The sound is spatialized and does attenuate — but from origin, and the compact TestMap playspace sits within Rifle_Raise_Cue's falloff of origin, so it reads as "heard everywhere."
⚠ First-guess correction: I initially blamed aPlay Sound 2Dnode bypassing attenuation. A binary scan of the cue.uassets found no sound node at all — these are data-driven Burst notifies (sound is a CDO property). The real cause is the missing cueLocation, not a 2D play node. Lesson reinforced: verify the actual play mechanism before writing the RCA.
Fix:
- C++ (sufficient):
ExecuteEquipCuenow setsCueParams.Location = Pawn->GetActorLocation()+Normal, so the Burst notify spawns the equip sound at the equipping player and attenuates from there. Needs rebuild.
- Robustness (DONE 2026-06-25, via bridge): the equip cues'
burst_sounds[0]placement is now AttachToTarget (override_placement_info=True) onGC_SL_AssaultRifle_Equip+GC_SL_Shotgun_Equip, so the sound anchors to the player's actor regardless of whether a caller setsLocation. Spawn policy left at ALWAYS/TargetActor. (Note:GameplayCueNotifystruct fields areEditDefaultsOnlyand can't be set on struct-value copies via the Python bridge — had to rebuild the wholeburst_effectsvia struct constructors and assign it top-level on the CDO.)
Lesson: A GameplayCueNotify_Burst sound with DoNotAttach plays at CueParameters.Location — if the caller never sets it, the sound plays at world origin. On a small map that sounds like "no distance falloff" even when attenuation is correct. When a cue sound seems un-spatialized, check (1) the cue Location is actually set by the caller, and (2) the notify's attach/placement — before suspecting the sound asset or a 2D node. Audit every EquipCueTag cue (and weapons duplicated from these). (FP-local sounds like the sidearm DrawSound/HolsterSound are deliberately PlaySound2D + IsLocallyControlled()-gated — those are correct.)
#BUG-025 — Player auto-collects its own death-drop on respawn (heard as map-wide "equip" sound)
Date: 2026-06-26 Branch: grenade-initial-imp Status: Fixed (C++) — pending rebuild + test
Bug: A stationary player, on respawn, triggers a pickup-collection sound — heard map-wide. Originally misread as "hearing the other player equip their AR on spawn." Persisted after gutting every equip cue/montage/sequence (those were never the source).
RCA (two independent layers):
- Why it fires:
DropAllWeaponsOnDeathdrops the AR/sidearm at the death spot, but the pickup launch is gentle (AR pickupLaunchHorizontalSpeed=150/Vertical=100, +40 cm offset, 30° pitch) → the drop lands only ~1 m from where you died. On a one-PlayerStart test map you die at the spawn andRestartPlayerrespawns you at that same spawn — on top of your own drop. The new pawn's begin-overlap with the pickup trigger →OnSphereOverlap→OnCollected. Since the pawn already owns a fresh AR (loadout),ASLWeaponPickup::OnCollectedtakes theAlreadyCarried → AddAmmopath (auto, no prompt) → plays the pickup sound. No movement needed — the drop comes to the spawn.
- Why map-wide: the pickup sounds (
Object_PickUp,HealthPickup) had no Attenuation Settings while played viaPlaySoundAtLocation(3D node, but a sound with no attenuation never falls off). Same class of issue as BUG-024, different asset.
Fix:
- Collection (root cause): spawn-grace on the character.
ASLCharacterBase::BeginPlaysetsPickupCollectionGraceEnd = Now + PickupCollectionGracePeriod(default 1 s);ASLPickupBase::OnSphereOverlapreturns early if!Character->CanCollectPickups(). Begin-overlap fires once on spawn, so a pawn that spawns on a drop won't auto-grab it — it must leave and re-enter. (A post-landing timer on the pickup does NOT work: the drop lands in ~0.4 s but respawn is seconds later, so it's long collectable by then.)
- Audio: assigned
SA_Default3Dattenuation toObject_PickUp+HealthPickup(via bridge) so legit pickups fall off with distance.
Lesson: Drop-on-death + respawn-at-death-location = self-collection. Any "X happens on spawn without the player doing anything" should be checked against what spawns/lands at the spawn point, not just placed actors. And (again, cf. BUG-024) a 3D PlaySoundAtLocation with a no-attenuation sound is heard everywhere — the node being 3D is necessary but not sufficient; the sound asset needs Attenuation Settings.
#BUG-026 — Red reticle (enemy target) works on host but not clients after the target respawns
Date: 2026-06-26 Branch: grenade-initial-imp Status: Fixed (C++) — pending rebuild + test
Bug: As a client, aiming at another player did not turn the reticle red; as the host it worked. Narrowed down: only after the target had died (via BP_SL_TestDamageEmitter) and respawned — i.e. an alive, respawned target reads as "enemy" on the host but not on observing clients.
RCA: USLWeaponsComponent::CheckForEnemyTarget (drives OnTargetDetected via the WBP_SL_HUDWidget "Red Reticle Check") traced ECC_Pawn and returned Cast<APawn>(Hit.GetActor()) != nullptr. The only thing blocking ECC_Pawn is the capsule — and the death/respawn flow toggles capsule collision via paths that don't reach an observing client: the death ability disables it server-side only (SLGameplayAbility_Death.cpp:86, SetCollisionEnabled is not replicated) and Client_OnRespawnBegin disables it on the owning client only. Respawn is a fresh RestartPlayer pawn, but with RagdollDestroyDelay the corpse lingers and the observer's view of the target's capsule state after a death is unreliable. Net: the host (authoritative) sees the correct capsule; observing clients diverge → no red.
Fix: trace ECC_WeaponTrace (the channel real shots use — against the mesh's physics-asset bodies, which authoritative hit detection already relies on and which works on respawned proxies) and require ASLCharacterBase instead of any pawn. Removes the capsule dependency entirely and makes the reticle agree with where a shot would actually land. (FFA: any non-owner SL character is hostile — there's no team system.)
Lesson: Don't drive client-visible state off component collision-enabled flags — SetCollisionEnabled is not replicated, so any per-frame check that depends on it (here, an ECC_Pawn/capsule trace) will diverge between host and clients whenever something toggles it (death, ragdoll, respawn). Trace the same channel you act on (ECC_WeaponTrace) so cosmetic feedback matches authoritative behavior.
Open edge case — corpse targetability (decision: leave as-is, 2026-06-26): with the weapon channel, a ragdoll corpse's mesh can briefly read as a target (red reticle on a body you just killed) until it self-destroys after RagdollDestroyDelay. Decision: do nothing for now (you can shoot ragdolls in most shooters; not worth code). Do NOT add a Dead-tag check in CheckForEnemyTarget: the ASC lives on the PlayerState, so the corpse and the respawned pawn share one ASC which reads alive after respawn — the tag can't distinguish them, and the corpse is UnPossess'd so it may resolve no ASC at all. If it ever matters, fix it in the collision layer (on ragdoll, set the corpse mesh to ignore ECC_WeaponTrace) so the reticle trace AND the fire trace ignore corpses from one source of truth — keep CheckForEnemyTarget a dumb "did I hit an ASLCharacterBase" check. A possession check (GetController() != nullptr) is a replication-safe fallback if it must live in the reticle, but collision is the principled home.