Weapons · Updated Jun 19, 2026

GASidearmMode — Editor Implementation Guide

GASidearmMode — Editor Implementation Guide

Implements the hold-LT sidearm draw. While active, applies SLTags.States.Weapon.SidearmActive which drives the anim BP pose and gates primary / sidearm fire. Pure Blueprint ability — no C++ base class needed (unlike Melee/Fire which have complex server logic).

⚠ Implementation status (updated 2026-06-14): the draw WINDUP is live; the draw ANIMATION is not.
BP_GA_SL_SidearmMode now runs the full §3 windup graph — it applies SidearmDrawing for
SidearmDrawDuration (data asset, 0.5s) with OnEndAbility cleanup, and SidearmDrawing is in
BP_GA_SL_Pistol_PrimaryFire.ActivationBlockedTags (added 2026-06-14 via the bridge — it was
previously absent, not merely inert), so firing is now blocked during the draw window. What's
still missing is the draw clip: visually the sidearm pops out via the Sidearm Blend pose-in (no
authored windup anim yet). bIsSidearmDrawing (§4) is true during the window — gate a Draw state on
it once the clip exists.
Anim-state flags (2026-06-14): FSLAnimState carries bIsSidearmDrawing and bIsSidearmHolstering
(see §4). Both are PURELY COSMETIC anim drivers, latched in BuildAnimSnapshots off the rising
(draw) / falling (holster) edge of the replicated SidearmActive tag — so observers see them too. They
are decoupled from the SidearmDrawing gameplay tag: that tag (managed by the §3 windup graph) does
the fire-block on the owning client + server only, while the anim flags ride SidearmActive for
all-client reach. See §4.2.
Holster is PURELY COSMETIC + DONE ✅ (2026-06-15) — it carries no gameplay state (the player is back
on the primary the instant SidearmActive drops: fire routing, primary-fire block, and swap gating all
return that frame), so there is no holster tag, no ability change, and no controller change.
bIsSidearmHolstering is latched in BuildAnimSnapshots off the falling edge of SidearmActive and held
for the per-weapon WeaponData->SidearmHolsterDuration (default 0.5s; falls back to the character's
SidearmHolsterCosmeticTime, 0.3s). Both the Sidearm Blend pose pin (OR'd with Is Sidearm Holstering)
> and the mesh visibility (`UpdateSidearmMeshVisibility(activeholstering)`) hold through the tail, so the
pistol pose + mesh stay visible until the holster anim ends. Currently reuses the draw clip in reverse. See §4.2.
Melee interaction — PISTOL-WHIP (2026-06-11, supersedes the earlier auto-holster approach): while
the sidearm is drawn, melee is a pistol-whip — the pistol stays out and the swing uses the pistol's
own melee values. SLGameplayAbility_Melee::GetWeaponData() returns GetActiveFireWeaponData() (the
sidearm when SidearmActive, else the main weapon), so melee damage/range/radius/shakes come from
DA_SL_Pistol during the whip — identical to before when no sidearm is drawn. GA_SidearmMode keeps
running through the swing (ActivationBlockedTags blocks only new activation, not the running
ability), so SidearmActive persists and the pistol stays visible; pistol fire is still blocked
mid-swing by Meleeing. Anim (TODO): add a pistol-whip melee state gated on
bIsMeleeing && bIsSidearmActive (Always Reset on Entry + Inertialization, MeleeImpact notify).
Editor step: BP_GA_SL_Melee.CancelAbilitiesWithTag must be empty (the earlier auto-holster
put SLTags.Abilities.SidearmMode there — remove it). The holster-then-main-melee + auto-redraw
approach was dropped: too many chained animations, didn't flow together.

#1. Create the Ability Blueprint

  • Location: Content/SystemLink/AbilitySystem/Abilities/Sidearm/BP_GA_SL_SidearmMode
  • Parent class: USLGameplayAbility (the project base, not the engine base)
  • Net Execution Policy: Local Predicted
  • Instancing Policy: Instanced Per Actor

#2. CDO Settings

FieldValue
AbilityTagsSLTags.Abilities.SidearmMode
ActivationOwnedTagsSLTags.States.Weapon.SidearmActive
ActivationBlockedTagsSLTags.States.Character.Dead, SLTags.States.Character.Meleeing, SLTags.States.Weapon.Busy, SLTags.States.Weapon.RefireLock
Triggers[0] TagSLTags.Events.Weapon.SidearmDraw
Triggers[0] SourceGameplayEvent
Left-hand mutual exclusion with grenades: the States.Weapon.Busy block above also stops the sidearm
draw while a grenade is being thrown (the throw owns Busy). The reciprocal half — blocking the throw
while the sidearm is drawn — lives on the grenade ability (SidearmActive in its ActivationBlockedTags).
Both share the left hand; see Docs/GrenadeSystem.mdLeft-hand mutual exclusion.

#3. BP Activation Graph

WINDUP GRAPH BUILT & VERIFIED (2026-06-14). The draw-windup graph below is implemented in
BP_GA_SL_SidearmMode and reviewed. It manages the SidearmDrawing tag (gates fire mid-draw);
SidearmActive is still the auto-applied ActivationOwnedTag. The instant-draw note in the top
callout refers to the animation (no draw clip authored yet) — the tag windup is live.

The ability manages the SidearmDrawing tag + draw delay. Mesh visibility is handled by USLWeaponsComponent (§5). Animation is driven by the ABP responding to bIsSidearmActive / bIsSidearmDrawing (§4).


Event Activate Ability

  → CommitAbility

  → Branch (Condition = CommitAbility Return Value)        // early-out if commit failed

      True →

        Get Avatar Actor from Actor Info → (Actor)

        Make Gameplay Tag Container from Tag (SidearmDrawing)

          → Add Loose Gameplay Tags (Actor, Tags, Should Replicate = FALSE)

        → Delay (SidearmDrawDuration, from GetSidearmWeapon → WeaponData)

        → Remove Loose Gameplay Tags (Avatar Actor, SidearmDrawing, Should Replicate = FALSE)

      [ability sits active until LT released]



Event OnEndAbility                                          // fires on normal end AND cancel

  → Get Avatar Actor from Actor Info → (Actor)

  → Make Gameplay Tag Container from Tag (SidearmDrawing)

  → Remove Loose Gameplay Tags (Actor, SidearmDrawing, Should Replicate = FALSE)

The ability sits active while LT is held. LT release triggers Cancel Abilities With Tags from the input binding (§6), which ends the ability and removes SidearmActive automatically.

Why the OnEndAbility Remove is mandatory — stranded-tag lockup. The player can release LT
during the Delay, which cancels the ability before the inline Remove runs. Without the
OnEndAbility cleanup the SidearmDrawing loose tag is added but never removed → it sits on the ASC
forever → pistol fire is blocked permanently (it's in the fire ability's ActivationBlockedTags).
Only triggers on a fast tap-during-draw, so it's easy to miss in casual testing. OnEndAbility fires
on both normal end and cancel, so removing there closes the hole. The inline post-Delay Remove is
kept as belt-and-suspenders for the normal case (draw window ends, fire unblocks while the sidearm
stays out); the double-remove is safe because loose tags are ref-counted and clamp at zero.
Keep Should Replicate = FALSE on every Add/Remove. GA_SidearmMode is Local Predicted, so the
graph runs on the owning client (blocks the predicted fire) and the server (blocks the
authoritative fire) — exactly the two machines where fire activates. Simulated proxies never fire
your pistol, so replicating buys nothing for gameplay. And Should Replicate = TRUE is actively
wrong here: it routes through the authority-only replicated-loose-tag path, which misbehaves when the
predicting client runs it.
Observers and the draw animation (resolved 2026-06-14). Because the loose tag doesn't reach
simulated proxies, observers would never play the draw windup off it — confirmed in PIE (a client
watching another player saw the snap-in but not the draw clip). Fix: the anim flag bIsSidearmDrawing
is now a cosmetic rising-edge latch off the replicated SidearmActive tag in BuildAnimSnapshots
(mirrors the holster tail), held for the sidearm's WeaponData->SidearmDrawDuration. Every machine —
incl. observers — sees SidearmActive flip and plays the draw. The SidearmDrawing gameplay tag is
unchanged and still does the owning-client/server fire-block. Do NOT replicate the gameplay tag.
ActivationOwnedTag = SidearmActive applies automatically on activation and is removed automatically
on end — no manual management needed beyond the SidearmDrawing windup tag above.
Draw delay risk: GA_SidearmMode is Local Predicted — the delay runs independently on
client and server. See Docs/Risks.md RISK-001. Keep SidearmDrawDuration >= 0.4s.

#4. Animation Architecture

The sidearm ABP state machine (SidearmStateMachine inside ALI_SL_Sidearm) has these states:

StateEntry ConditionExit Condition
DrawbIsSidearmDrawing true (cosmetic windup)bIsSidearmDrawing false
IdleAfter Draw completesSidearmState.bIsFiring true, or bIsSidearmActive false
FireSidearmState.bIsFiring trueSidearmState.bIsFiring false
HolsterbIsSidearmHolstering true (cosmetic tail)bIsSidearmHolstering false

Two separate ABPs per sidearm — do not conflate:

ABPSkeletonDrivesSet via
ABP_MC_FP_<Sidearm> / ABP_MC_TP_<Sidearm>Character skeletonLeft arm poseAnimLayerClass on data asset
ABP_<Sidearm>_WeaponWeapon skeletonWeapon mesh animationsWeaponMeshAnimClass on data asset

ALI_SL_Sidearm is implemented by the character arm ABPs, NOT the weapon mesh ABP. Linked into FPMesh/TPMesh via LinkAnimClassLayers from UpdateSidearmMesh when SidearmWeapon changes.

Anim state sourcingBuildAnimSnapshots() reads SidearmActive/Firing from GAS tags, then derives the cosmetic draw/holster flags off the SidearmActive edges (see §4.2 — observers need this).


S.bIsSidearmActive       = ASC->HasMatchingGameplayTag(SLTags::States::Weapon::SidearmActive);

S.SidearmState.bIsFiring = ASC->HasMatchingGameplayTag(SLTags::States::Weapon::Firing);

// Draw + holster flags are cosmetic, latched off the SidearmActive rising/falling edge (NOT the

// SidearmDrawing loose tag, which doesn't replicate to observers):

if (!bPrevSidearmActive && S.bIsSidearmActive)        // rising → draw window

    SidearmDrawEndTime = Now + (SidearmWeapon ? SidearmWeapon->WeaponData->SidearmDrawDuration

                                              : SidearmDrawCosmeticFallback);

if (bPrevSidearmActive && !S.bIsSidearmActive)        // falling → holster tail

    SidearmHolsterEndTime = Now + SidearmHolsterCosmeticTime;

bPrevSidearmActive     = S.bIsSidearmActive;

S.bIsSidearmDrawing    =  S.bIsSidearmActive && Now < SidearmDrawEndTime;

S.bIsSidearmHolstering = !S.bIsSidearmActive && Now < SidearmHolsterEndTime;

For early testing, use instant state blends instead of authored draw/holster animations.
The GAS ability and input can be proved before the ABP content is final.

#4.2 Draw + holster anim flags (cosmetic, edge-latched)

The draw and holster animations are cosmetic. Both flags are latched in C++ in BuildAnimSnapshots off the edges of bIsSidearmActive, which comes from the replicated SidearmActive tag — so every machine, including observers, plays them with no extra replication. This is deliberately decoupled from the SidearmDrawing gameplay tag (the fire-block), which is loose and owning-client/server-only.

  • bIsSidearmDrawing — latched on the rising edge, held for the sidearm's
  • WeaponData->SidearmDrawDuration (falls back to SidearmDrawCosmeticFallback, EditDefaultsOnly, 0.5s, if the weapon/data can't resolve). Tracks the gameplay windup window so the anim and the fire-unblock line up.

  • bIsSidearmHolstering — latched on the falling edge, held for the sidearm's
  • WeaponData->SidearmHolsterDuration (EditDefaultsOnly, 0.5s; falls back to ASLCharacterBase::SidearmHolsterCosmeticTime, default 0.3s, when the weapon/data can't resolve). Holster has no gameplay side at all — releasing LT drops SidearmActive immediately and the player is back on the primary that frame (fire routing, primary-fire block, and swap gating all return that frame).

  • Why off SidearmActive and not SidearmDrawing: the SidearmDrawing loose tag doesn't reach
  • simulated proxies, so observers never saw the draw windup when the flag read that tag (PIE-confirmed 2026-06-14). SidearmActive is a replicated ActivationOwnedTag, so its edges are visible everywhere.

  • Keeping pose + mesh alive through the tail (DONE 2026-06-15): both bIsSidearmActive and
  • bIsSidearmHolstering must drive the cosmetic consumers, or they cut off the instant SidearmActive

drops. Two consumers were extended to `bIsSidearmActivebIsSidearmHolstering`:
  • Pose: the Sidearm Blend node's Is Sidearm Active pin is wired through an OR with
  • Is Sidearm Holstering (Break SLAnim State). Without this the node crossfades the sidearm branch out over BlendTime (0.1s) the moment active drops — the holster clip would barely show.

- Mesh: BuildAnimSnapshots calls `UpdateSidearmMeshVisibility(bIsSidearmActivebIsSidearmHolstering)`

(SLCharacterBase.cpp:658) so the pistol mesh stays visible until the holster anim ends.

  • To author: add non-looping Draw and Holster states to SidearmStateMachine, entered on
  • bIsSidearmDrawing / bIsSidearmHolstering and exited when they clear. "Always Reset on Entry" is REQUIRED on both (+ Inertialization), and the exit must be an explicit NOT bIsSidearm{Drawing/Holstering} transition — NOT an automatic time-remaining rule. Missing this on the Holster state caused BUG-022 (pumping the draw froze the character in the holster pose at the clip's last frame). Tune SidearmDrawDuration / SidearmHolsterDuration (per-weapon on the data asset) to roughly the clip lengths.

  • Holster currently reuses the draw clip played in reverse and feels good; set
  • SidearmHolsterDuration to match SidearmDrawDuration in that case. A dedicated draw/holster clip pair is optional polish — the Sidearm Blend node's built-in 0.1s crossfade covers it visually until then.

#4.1 Sidearm Blend node (custom C++ AnimGraph node)

The sidearm pose is composited onto each locomotion state with a reusable custom AnimGraph node, Sidearm Blend (right-click an AnimGraph → System Link → Sidearm Blend). It replaces the hand-built Layered Blend Per Bone + Blend Poses by bool pair that used to live in every state.

Why a C++ node and not an Animation Layer: an Animation Layer can only be instanced once per ABP (UE error: "layers can be used only once in an animation blueprint"), so it can't be called from idle/walk/run/crouch in the same ABP. A custom AnimGraph node has no such limit — every placement is its own instance. It's the only "define once, reuse in many states / many ABPs, pose-in/pose-out" option in UE.

What it does (per placement):

  • bIsSidearmActive true → LayeredBlend(Base = SidearmPose, Overlay = LowerPose from BlendBone down),
  • crossfaded in from DefaultPose over BlendTime.

  • bIsSidearmActive false → returns DefaultPose (holstered pass-through).
  • For a left-hand sidearm, BlendBone = clavicle_r: the right-arm branch is taken from
  • LowerPose (the arm holding the lowered primary), while the body + left hand stay on the sidearm.

Pins / settings:

Pin / settingWire toNotes
Sidearm Pose (pose)the sidearm locomotion blendspace for that statethe base
Lower Pose (pose)the weapon-lowered pose inputoverlaid on the BlendBone branch
Default Pose (pose)the holstered pass-through poseused when not active
Is Sidearm Active (bool pin)Break SLAnim State → Is Sidearm Active
Blend Bone (details)clavicle_rroot of the overlaid branch; children inherit
Blend Time (details)0.1draw/holster crossfade seconds (built in — covers it until authored draw/holster exists)

Source / build:

  • Runtime: FAnimNode_SidearmBlendSystemLinkCore/Public+Private/Animation/AnimNode_SidearmBlend.*
  • (ships in packaged builds).

  • Editor: UAnimGraphNode_SidearmBlend in a new SystemLinkCoreEditor module (UncookedOnly,
  • so it does NOT ship). Adding this module means a one-time regenerate project files before building.

  • In use as of 2026-06-08 in ABP_TP_PistolSidearm Crouch and Sidearm Run Walk; stamp it into
  • Sidearm Idle and the rifle/shotgun ABPs to retire the remaining two-node pairs.


#5. Mesh Visibility and Asset Assignment

Mesh concerns belong to USLWeaponsComponent, not the ability.

Asset assignment — OnRep_Sidearm (fires on all clients when SidearmWeapon changes): UpdateSidearmMesh handles: mesh asset, socket attachment, WeaponMeshAnimClass on sidearm components, and AnimLayerClass linked into FPMesh/TPMesh.

Visibility — ASC tag callback in USLWeaponsComponent: WeaponsComponent binds RegisterGameplayTagEvent(States::Weapon::SidearmActive) on the ASC. Shows the correct mesh for the current view — FP mesh in FP view, TP mesh in TP view:


Tag added (FP view) → FPSidearmMesh shown, TPSidearmMesh hidden

Tag added (TP view) → TPSidearmMesh shown, FPSidearmMesh hidden

Tag removed         → both sidearm meshes hidden

Also update ApplyViewMode in BP to swap sidearm mesh visibility when the player switches FP/TP — check AnimStateSnapshot.bIsSidearmActive to only show the correct mesh when active.

This fires on all clients because SidearmActive is a replicated GAS tag — no extra replication needed.

Gotcha: RegisterGameplayTagEvent does not fire for a tag already active at bind time.
After binding, BindSidearmTagCallback manually checks GetTagCount and calls
OnSidearmTagChanged if the tag is already set — handles clients that join mid-draw.
Also: GetPawnASC() may return null during OnRep_EquippedWeapon for simulated proxies —
UpdateSidearmMesh provides a second retry point. See feedback_gas_tag_callback_patterns.md.

#6. Wire LT Input in BP_SL_PlayerController

Bound via C++ in SetupInputComponent to SidearmAction (input action: IA_SL_SidearmMode):

  • StartedStartSidearmDraw()HandleGameplayEvent(ASC, SLTags.Events.Weapon.SidearmDraw)
  • Completed / CanceledStopSidearmDraw()CancelAbilities(SLTags.Abilities.SidearmMode)

#7. Update Primary Fire Abilities

Add SLTags.States.Weapon.SidearmActive to ActivationBlockedTags on:

  • BP_GA_SL_AssualtRifle_PrimaryFire
  • BP_GA_SL_Shotgun_PrimaryFire

This ensures RT cannot fire the primary weapon while the sidearm is drawn.


#8. Create the Sidearm Fire Ability

Duplicate BP_GA_SL_AssualtRifle_PrimaryFire → rename BP_GA_SL_Pistol_PrimaryFire.

FieldValue
ActivationRequiredTagsSLTags.States.Weapon.SidearmActive
ActivationBlockedTagsSLTags.States.Character.Dead, SLTags.States.Weapon.SidearmDrawing
Triggers[0] TagSLTags.Events.Weapon.PrimaryFire (same RT trigger as primary)

#8.1 How fire is routed to the sidearm (C++, implemented 2026-06-08)

The fire pipeline (USLGameplayAbility_Fire + Server_ProcessShots) was equipped-weapon-centric. It now reads an "active fire weapon" so the sidearm fires its own data/ammo/muzzle, while sway/obstruction/IK keep using the equipped (primary) weapon. Added to USLWeaponsComponent:

  • IsSidearmActive()SidearmActive tag on the owner's ASC (the single source of truth).
  • GetActiveFireWeapon() — sidearm while SidearmActive, else EquippedWeapon.
  • GetActiveFireWeaponData() / GetActiveFireMode(bPrimary) / GetActiveFirstPersonWeaponMesh().

The fire ability's call sites (CanActivate, ExecuteFire, EndAbility, muzzle, cosmetics, stop-cue) and Server_ProcessShots's damage lookup now go through these. Because the active weapon is driven by the SidearmActive tag — and the sidearm fire ability requires it while the primary blocks on it — the active weapon always matches whichever fire ability is running. When no sidearm is drawn, every "active" accessor returns the equipped weapon, so primary fire is unchanged.

Ammo (option A — actor-owned): the sidearm is a separate, droppable slot, so it does NOT use the ASC ammo attribute. The fire ability branches on IsSidearmActive(): the ammo gate checks the sidearm actor's replicated CurrentAmmo, and the decrement does Sidearm->SetCurrentAmmo(CurrentAmmo - 1) on authority (one per trigger pull). HUD reads the sidearm actor's OnAmmoChanged directly. The equipped weapon keeps its GE/attribute-set ammo path untouched.

#8.2 Editor tasks (after the C++ builds)

  1. BP_GA_SL_Pistol_PrimaryFire — duplicate of the AR fire BP, CDO per the §8 table. Set
  2. RequiredWeaponClass = the sidearm weapon actor class, ActivationRequiredTags = SidearmActive, ActivationOwnedTags = States.Weapon.Firing, same PrimaryFire trigger.

  1. Block primary while drawn — add States.Weapon.SidearmActive to ActivationBlockedTags on
  2. BP_GA_SL_AssualtRifle_PrimaryFire and BP_GA_SL_Shotgun_PrimaryFire. (The C++ CanActivate also blocks it via the active-weapon class check — this tag is the explicit, readable belt-and-suspenders.)

  1. Grant BP_GA_SL_Pistol_PrimaryFire via AS_AbilitySet_Default.
  1. Sidearm weapon data asset — fill PrimaryFireMode: RoundsPerMinute, FireMode
  2. (SingleShot/FullAuto), BaseDamage, DamageMultipliers, MuzzleSocketName (must exist on the pistol mesh), FireCueTag / ImpactCueTag, and MaxAmmo. AmmoDecrementEffect is NOT needed for the sidearm.

  1. Fire cosmetics — pistol GC_SL_Pistol_PrimaryFire cue + OnLocallyPredictedShotFired FP
  2. muzzle/sound (reuse AR temporarily if pistol assets aren't ready).

  1. HUD sidearm ammo indicator (separate task) — bind SidearmWeapon->OnAmmoChanged to a fixed
  2. indicator so the count is visible; the C++ above already keeps CurrentAmmo correct + replicated.

SidearmDrawing in ActivationBlockedTags prevents firing during the draw delay. RT fires whichever ability the current tag state allows — primary when holstered, sidearm when active and draw complete.


#9. Grant via Ability Set ✅

BP_GA_SL_SidearmMode added to AS_AbilitySet_Default at Index [3]. BP_GA_SL_Pistol_PrimaryFire — add when created (same set or dedicated sidearm set).


#10. Assign Default Sidearm in Loadout ✅

DefaultSidearmClass on the loadout data asset → BP_SL_WeaponActor_Pistol.


#11. Test Checklist

Input + tags:

  • [ ] LT held → SidearmActive tag applied (showdebug abilitysystem)
  • [ ] LT held → SidearmDrawing tag applied, removed after SidearmDrawDuration seconds
  • [ ] LT held during draw → RT does NOT fire sidearm (SidearmDrawing blocks it)
  • [ ] LT held after draw completes → RT fires sidearm ability
  • [ ] LT held → RT does NOT fire primary (SidearmActive blocks it)
  • [ ] LT released → SidearmActive tag removed, meshes hidden
  • [ ] LT released → RT fires primary again

Visibility:

  • [ ] FP view: only FPSidearmMesh visible when drawn
  • [ ] TP view / remote client: only TPSidearmMesh visible when drawn
  • [ ] Switch FP↔TP while drawn → correct mesh shown, other hidden

Edge cases:

  • [ ] Melee while sidearm drawn → NOT possible (Meleeing blocks GA_SidearmMode)
  • [ ] Death while drawing → GA_SidearmMode cancelled (Dead blocks it)
  • [ ] Multiplayer: bIsSidearmActive drives TP pose on all clients

#12. Sidearm Ammo HUD (pistol icon + count)

C++ done (2026-06-15, container approach — mirrors the ammo strip). Rather than a fixed USLAmmoWidget bound on the HUD, the HUD has a container panel that C++ populates at runtime with a widget created from the sidearm's WeaponData->AmmoWidgetClass — the same per-weapon ammo widget class the strip uses. So each sidearm can bring its own ammo widget, and there's one source of truth for the ammo widget class.

Wiring (all in SystemLinkCore):

  • USLAmmoWidget::OnWeaponSet(const USLWeaponDataAsset*) — BIE fired from SetWeapon. BP uses it to set the
  • icon from WeaponData->WeaponIcon / WeaponIconMaterial. (Ammo count stays on OnAmmoChanged.) Required here because the sidearm widget is created standalone, NOT via the strip's CreateSlotWidget (where strip slots usually set their icon).

  • USLHUDWidget::SidearmAmmoContainermeta=(BindWidgetOptional) UPanelWidget (any panel type). Optional:
  • HUDs without a sidearm display omit it.

  • USLHUDWidget::HandleSidearmChanged — clears the container, CreateWidgets
  • NewSidearm->WeaponData->AmmoWidgetClass, AddChilds it, and calls SetWeapon(NewSidearm). Tracks the created widget in SidearmAmmoWidgetInstance (exposed via GetSidearmAmmoWidget()).

  • USLHUDWidget::InitializeSidearmIndicator(Character) — binds ASLCharacterBase::OnSidearmChanged (new
  • delegate, broadcast from OnRep_Sidearm) → rebuild on swap. Called from ASLPlayerCharacter::InitializeLocalPlayerHUD after InitializeAmmoStrip.

  • Visibility policy: persistent — the container is shown whenever the player has a sidearm (drawn OR
  • holstered); hidden only when there is no sidearm (dropped on death, pre-respawn). Off actor validity, NOT the draw state.

Remaining (editor):

  1. The sidearm's ammo widget is its DA_SL_Pistol.AmmoWidgetClass (the same USLAmmoWidget subclass the
  2. strip uses). Ensure that widget implements OnAmmoChanged(Current, Max) → count text AND OnWeaponSet(WeaponData) → icon from WeaponData->WeaponIcon (the icon MUST come from OnWeaponSet here, since the standalone sidearm widget never goes through the strip's CreateSlotWidget).

  1. In WBP_SL_HUDWidget, add a panel (Overlay/SizeBox/Border/NamedSlot — any UPanelWidget) named
  2. SidearmAmmoContainer (case-sensitive, matches the BindWidgetOptional), positioned/sized where the sidearm count should sit. Leave it empty — C++ fills it at runtime.

  1. Set DA_SL_Pistol.WeaponIcon (Weapon Icon Generator) if not already set.
  1. ⚠ Header change (new BindWidget container + UPROPERTYs) → full rebuild + editor restart (Live Coding
  2. won't surface the new SidearmAmmoContainer bind name).

  1. Test: indicator shows the pistol icon + ammo at all times once a sidearm is held; fire → count decrements
  2. live; pick up a different sidearm → widget rebuilds for the new pistol; die (sidearm drops) → container hides until respawn re-equips one.


#13. Reticle Swap on Draw

Swaps the crosshair to the sidearm's reticle while it's drawn, back to the primary's when holstered.

C++ done (2026-06-15):

  • USLWeaponsComponent::GetActiveReticleClass()BlueprintPure. Returns the sidearm's
  • PrimaryReticleClass while SidearmActive, else the equipped weapon's; falls back to the equipped weapon's reticle when the active weapon has none set, so a missing sidearm reticle never blanks the crosshair.

  • USLWeaponsComponent::OnSidearmActiveChanged(bool) — fires on the gameplay draw/holster edge (edge-correct
  • as of 2026-06-15: it edge-detects on the SidearmActive tag, not the cosmetic visibility flag), so the swap-back happens the instant you holster.

  • USLHUDWidget::SwapReticle(ReticleClass, PC) — already existed; reticle swap is BP-only (no C++ caller).
BP location note: the weapon-delegate/reticle bindings live in WBP_SL_HUDWidget (the
USLHUDWidget BP subclass) — that's where the existing OnEquippedWeaponChanged → reticle binding is —
NOT in BP_SL_PlayerHUD (that's the AHUD actor, ASLPlayerHUD, which just creates the widget). Earlier
revisions of this doc wrongly said BP_SL_PlayerHUD.

BP wiring — in WBP_SL_HUDWidget, alongside the existing OnEquippedWeaponChanged reticle binding. The reticle must react to BOTH the draw/holster toggle AND the sidearm actor changing — otherwise picking up a different sidearm while it's drawn leaves the old reticle up (the SidearmActive tag doesn't toggle on an actor swap, so OnSidearmActiveChanged never fires). This mirrors the normal reticle, which swaps on the weapon actor changing (OnEquippedWeaponChanged); OnSidearmChanged is the sidearm-slot equivalent.

Make a reusable function RefreshReticleSwapReticle( WeaponsComp → GetActiveReticleClass , Get Owning Player ), and bind all three events to it:

  1. WeaponsCompAssign OnSidearmActiveChangedRefreshReticle (draw/holster). Ignore the bActive
  2. bool — GetActiveReticleClass re-evaluates the tag each call.

  1. WeaponsComp → Get Owner → Cast To SLCharacterBaseAssign OnSidearmChangedRefreshReticle
  2. (sidearm actor swapped — e.g. picked up a different pistol, drawn or holstered).

  1. WeaponsCompOnEquippedWeaponChangedRefreshReticle (main weapon equip — one source of truth).

GetActiveReticleClass() returns the sidearm's reticle while SidearmActive, else the equipped weapon's, so the single RefreshReticle node is correct for every case. Redundant calls are cheap: SwapReticle no-ops when the reticle class is unchanged (early-out added 2026-06-15, so no flicker on same-type re-equip or a holstered sidearm swap).

GOTCHA (cost a debug session 2026-06-15): the per-tick reticle driver (Event Tick → OnSpreadChanged
/ OnTargetDetected) must push into the current active reticle, because SwapReticle replaces the
reticle widget on every swap. Either set your cached reticle variable from SwapReticle's return value
inside RefreshReticle, or point the tick's Target pins at self → Get Reticle (the BlueprintPure
returning ActiveReticle). A reticle ref cached once at init goes stale after the first swap → spread +
red-target silently stop driving (looks like both features are broken for all weapons).

Data:

  • Author a USLReticle subclass for the pistol (e.g. WBP_SL_Reticle_Pistol; duplicate an existing one).
  • Set DA_SL_Pistol.PrimaryReticleClass to it.

Note: the reticle keys off BOTH OnSidearmActiveChanged (draw/holster) and OnSidearmChanged (actor swap), whereas the ammo indicator (§12) is persistent and keys off OnSidearmChanged validity only — the indicator doesn't care about the draw toggle, the reticle does.