Now · Updated 2552.08.11.09.21

CurrentFocus — AI Session Primer

Update this file at the end of each session. It is the first thing to read at the start of a new one.

Update this file at the end of each session. It is the first thing to read at the start of a new one.

#Session 2026-08-02 — reusable hard-light bridge material

  • Created and editor-verified the procedural hard-light bridge set under
  • /Game/SystemLink/Environment/HardLightBridge/Materials: translucent surface master/default instance plus optional additive edge master/default instance.

  • ActivationProgress and ReverseConstructionDirection support progressive U-axis construction with
  • a hot leading edge. All requested color, pattern, flow, scanline, pulse, noise, Fresnel, opacity, and Depth Fade controls are exposed.

  • Reproducible authoring and validation scripts:
  • Tools/create_hard_light_bridge_materials.py and Tools/verify_hard_light_bridge_materials.py.

  • Rebuilt 2026-08-02 as a fully node-based graph (zero Custom HLSL), informed by the layering and
  • Frac/Sine/Power/Fresnel structure in SystemLinkCore's M_Textured_Fresnel_01.

  • Full mesh/UV, Blueprint/C++, multiplayer, tuning, performance, and Niagara guidance:
  • Docs/HardLightBridgeMaterial.md.

  • Maya sample bridge created by Tools/build_hard_light_bridge_sample.py: editable scene and three FBXs
  • under SourceArt/.../HardLightBridge, with a 12 m × 4 m normalized-UV surface, separate additive edge strips, projector housings, and UCX collision. Imported meshes live in /Game/SystemLink/Environment/HardLightBridge/Meshes; surface and edge instances are assigned.


#▶ RESUME HERE (2026-08-11) — ✅ Teleporters DONE; all three interactables working

Branch level1-interactables. Pickups, doors and teleporters are all built and working. Nothing on this branch is committed yet — see the loose ends below before starting anything new.

#Teleporters — done 2026-08-11

  • ASLTeleporter + USLScreenEffectWidget written, built clean, and **PIE-tested by Beepers:
  • "they test well." Full reference: Docs/Teleporters.md**.

  • BP_SL_Teleporter and BP_SL_TeleporterColumn built from the C++ base, a pair placed in TestMap, new
  • Niagara set authored (NS_SL_TeleporterField, NS_SL_SlipspaceArcs, NS_SL_TeleporterFilaments).

  • WB_Teleport reparented to USLScreenEffectWidget, so the overlay owns its own lifetime — no
  • animation-length-vs-ArrivalOverlayDuration pair to keep in sync.

  • Duplicate-asset cleanup done: the dead Content/SystemLink/UI/HUD/Teleporter/ copies of WB_Teleport,
  • MI_TeleporterUI and M_TeleporterUI were deleted via the asset registry (referencers checked first, one delete per bridge call). The live copies are all under Environment/Teleporter/. The empty folders may still show in the Content Browser until the editor restarts.

Still worth one deliberate pass: teleport as a non-host CLIENT in 2-player PIE. That is the case the whole class exists for and the one that is invisible when the host is the server. Docs/Teleporters.md §8 lists the rest of the matrix.

#USLScreenEffectWidget — new, reusable

Public/UI/SLScreenEffectWidget.h. Base for transient, non-interactive full-screen effects shown to ONE local player. The widget plays its own animation and calls FinishEffect(); the base class owns removal, with a MaxLifetime backstop, because a Blueprint that forgot would weld an overlay to that player's HUD for the match. Not USLCommonActivatableWidget (that is the interactive/stack side) and not USLDamageOverlayWidget (that one is persistent and attribute-driven).

Next users: respawn wash, shield break, overshield pickup, damage flash. ASLTeleporter detects the base at runtime and falls back to a plain UUserWidget + timer, so adopting it elsewhere needs no C++ change.

#What the earlier part of this session did

  • Analysed the migrated BP_Teleporter from six BP screenshots (Docs/Screenshots/2026-08-10 0816–0817).
  • Verdict: not safe for multiplayer, and it reproduced all three faults ASLDoor was written for.

    • It teleported straight out of OnComponentBeginOverlap with no authority check — the client, not
    • the server, was deciding where a player ended up.

    • Enabled / Target Actor were unreplicated but drove the rings and the hum.
    • A Does Object Implement Interface node with an EMPTY interface gated the whole cosmetic
    • Sequence. Always false — which is exactly why the teleport worked but the sound, the ring hide and the HUD call never ran. That node was the real blocker on the HUD hookup, not missing wiring.

    • Its Target Actor latch was released only by EndOverlap, so **dying on a pad bricked the teleporter
    • for the rest of the match**.

    • Also: Play Sound at Location pinned to (0,0,0); arrival facing set via actor rotation, which does
    • nothing to where a possessed player looks.

  • Wrote ASLTeleporter (Public/World/SLTeleporter.h + Private/World/SLTeleporter.cpp) — the same
  • wrap-in-C++ shape as ASLDoor. Builds clean. NOT PIE-tested and no Blueprint uses it yet.

    • Authority-only overlap; server does the move; bReady replicated with OnRep for the pad visuals.
    • Readiness is a timer, not a latch — there is no occupant state a dead player can strand.
    • ClientSetRotation for arrival facing; ExitMomentum enum (defaulted to Redirect — an assumption,
    • not Beepers' decision) plus a MaxExitSpeed clamp for the 1625 cm/s dash case.

    • The old CanBeTeleportedTo count-the-overlaps graph is one server-side sphere query.
    • ASLPlayerController::Client_OnTeleported added: a level actor has no owning connection, so its own
    • Client_ RPCs go nowhere. The controller makes the hop and calls back into the teleporter.

  • Docs/Teleporters.md written — full usage reference, and §8 lists the six 2-player PIE cases in order.
  • Docs/Footguns.md — seven new entries (overlap authority, unowned-actor RPCs, ClientSetRotation,
  • the before-the-move ordering rule, latch-vs-timer, counter-for-one-shot-cosmetics, empty migrated pins).

#NEXT: build BP_SL_Teleporter — DONE 2026-08-11, kept for the recipe

A fresh subclass, not a reparent (reparenting kills the editor mid-operation). Everything structural is in C++, so the Blueprint is small — full steps in Docs/Teleporters.md §3:

  1. New BP from SLTeleporter; meshes + rings Niagara + hum AudioComponent under the inherited Root.
  1. Resize the inherited TriggerVolumedo not add your own, it would be bound to nothing.
  1. Move ExitPoint; its +X is the arrival facing.
  1. Implement OnTeleporterStateChanged(bIsReady, bImmediate) → rings visibility = bIsReady,
  2. audio paused = NOT bIsReady. Honour bImmediate (snap, don't animate).

  1. Class Defaults: ArrivalOverlayClass = WB_Teleport, TeleportSound2D = HaloTeleporterTeleport_Cue.
  2. That is the entire HUD hookup — C++ creates, shows and removes the overlay.

  1. Place two in Level 1 and point each instance's TargetTeleporter at the other (EditInstanceOnly, so it
  2. can only be set on placed instances).

Two WB_Teleport assets existEnvironment/Teleporter/ and UI/HUD/Teleporter/. Confirm which one you have been editing before assigning it.

#⚠ Loose ends carried into the next session

  • 🔴 NOTHING ON THIS BRANCH SINCE 929766fb IS COMMITTED. All the teleporter C++, both teleporter
  • Blueprints, the three new Niagara systems, the reparented WB_Teleport, the door BP work (BP_ForerunnerDoor1 + untracked BP_ForerunnerDoor1Back), TestMap, Level1.umap, BP_SL_MasterChief, NS_ShieldBreak, dash-progress.uasset. Several are untracked .uasset binaries with no recovery path. Commit before starting anything else.

  • Docs/Doors.md §7 status is still unverified — the §4 property table is now complete (fixed
  • 2026-08-11), but §7 claims ASLDoor is unused and un-PIE-tested. Five follow-up commits on 08-07 (switches eating shots, MaxOpenTime, the pending-close cancel) read as live-test findings, so the claim is probably wrong — but nobody has confirmed it. Update it from memory of that session, not from guesswork.

  • Pickup placement status unconfirmedLevel1.umap is modified but it is not clear whether that is
  • door placement, pickups, or both. Level 1 also still has the legacy BP_OverShieldPickup_C rather than the GAS BP_SL_OvershieldPickup.

  • Level 1 vs TestMap: the teleporter pair went into TestMap. Level 1 still needs its own placement.

#Previous resume note (2026-08-07) — 🎯 Level 1 interactables: pickups, doors, teleporters

Branch level1-interactables, off main at 99941455 (PR #35 merged level1-import).

The goal is a Level 1 you can actually play through. The GameMode blocker turned out to be already fixed, so what remains is world interaction.

#Inventory — what exists before starting

SystemC++BlueprintsState
PickupsSLPickupBase, SLWeaponPickup, SLAmmoPickup, SLGrenadePickup, SLHealthPickup, SLOvershieldPickupBP_SL_WeaponPickup_{AssaultRifle,Pistol,Shotgun2}, BP_AmmoPickup_AssaultRifle, BP_SL_GrenadePickup_Frag, BP_HealthPickup, BP_SL_OvershieldPickupComplete. The work is PLACEMENT, not code.
DoorsnoneBP_ForerunnerDoor1 + meshes/materials/HaloDoor1_CueMigrated from the 5.6 project, untested here
TeleportersnoneBP_Teleporter + NG_TeleporterRings/Sparks, sounds, WB_TeleportMigrated, untested here

#⚠ Level 1 has the LEGACY overshield pickup placed, not the GAS one

Confirmed live in PIE 2026-08-05: the two overshield actors in Level 1 are BP_OverShieldPickup_C (Content/SystemLink/Environment/OverShieldPickup/) — the migrated 5.6 asset — not BP_SL_OvershieldPickup (Content/SystemLink/Pickups/Overshield/), which is the one deriving from ASLOvershieldPickup and going through the GAS path. Swap them when placing. Expect the same trap for any other interactable carried over from the old project: two assets, similar names, only one wired to the current systems.

#The decision to make before writing door/teleporter code

Both are world state that every machine must agree on, and neither has a C++ base today. The old project's Blueprints were written for a different (largely non-GAS, likely single-player) setup, so "it worked before" is not evidence it works here.

  • A door opened client-side is a desync: one player walks through what another sees closed.
  • A teleporter is worse — moving a pawn is authoritative gameplay, not cosmetics. Client-side
  • teleport is both a desync and a trivial cheat vector.

So the likely shape is a small server-authoritative C++ base for each (ASLDoor, ASLTeleporter) with the visuals and timing left in Blueprint, matching how the pickups are built. Test the migrated Blueprints in 2-player PIE first — that tells us how much of them survives rather than guessing.

#Order of work

  1. Play Level 1 as-is. It may already be walkable; the only known gaps are the interactables.
  1. Place the GAS pickups (old coordinates recorded in Docs/Level1Import.md §4), swapping the
  2. legacy overshield for BP_SL_OvershieldPickup.

  1. Doors — test BP_ForerunnerDoor1 in 2-player PIE, then decide rebuild vs wrap.
  1. Teleporters — same, and treat authority as non-negotiable.

⚠ Watch for the level1-fort-floor paper floor while playing: zero-thickness complex collision, and a 1625 cm/s dash is exactly what punches through it. → Docs/CollisionMeshes.md §0.


#Previous resume note (2026-08-05) — 🎯 FOCUS: still "make both levels playable"

Branch level1-import. Goals below are unchanged; this session was view-mode and dash FX.

#What this session changed

  • The BP_SL_MasterChief view-mode port is LIVE. The old SetMeshVisibility node soup is gone and
  • the timeline calls Apply View Mode Visuals. The 2026-08-03 "half-finished" item is done.

  • BUG-030 — other players' first-person arms rendered in the world, floating and disembodied, while
  • their TP body rendered correctly at the same time. Surfaced because the port went live: the BP function had been masking it, and ApplyViewModeVisuals became the single point of truth. ✅ FIXED AND PIE-VERIFIED by Beepers 2026-08-05 — both halves: remote players no longer show FP arms, and the owner's own FP↔TP toggle still behaves.

    • Two faults compounding: FP hiding sat behind if (!IsLocallyControlled()) return;, and the only
    • call site was the view-mode timeline, which runs solely on the pawn's own client — so a remote pawn was never called at all.

    • Fix: new ASLPlayerCharacter::ApplyFirstPersonMeshVisibility(), called from ApplyViewModeVisuals
    • above the guard, plus from BeginPlay / PossessedBy / OnRep_PlayerState (each above their own early returns). Full RCA: Docs/BugTracker.md → BUG-030.

    • THE RULE: SetOwnerNoSee is evaluated per viewer; SetVisibility is per machine.
    • Anything hidden with SetVisibility must be driven on every machine and can never sit behind IsLocallyControlled().

  • BUG-031 — the "Press E to equip" prompt stuck on the HUD permanently, advertising a weapon that was
  • no longer in the world. Two independent holes, both fixed: the prompt was never retracted when a pickup was destroyed under a standing player (PendingPickup is a UPROPERTY, so UE nulls it silently and the state reads clean), and a hide issued at death reached nothing because GetHUDWidget() resolves through GetController(), which an unpossessed pawn no longer has. Fixed in ASLWeaponPickup::EndPlay, in InitializeLocalPlayerHUD (clears the prompt on every possession), and by having both prompt RPCs bail on a null HUD. PIE shutdown errors confirmed gone; the die-on-a-pickup path is still untested.

  • BUG-032 — the overshield indicator showed for players with no overshield.PIE-verified fixed.
  • Not a state bug: live values were CurrentShield 50 / MaxShield 50 with no Overshield tag, so OnOvershieldChanged(0.0) was firing correctly and nothing was acting on it. Three defects in one area — a ProgressBar at 0% still draws its background brush; PrevOvershieldPercent was not reset per life, so the activate/deplete edges could fire spuriously or be missed entirely; and NativeConstruct (the listen-server host path) never called OnReset() while InitializeHealthWidget (the client path) always did.

  • Controller rumble on fire — plumbing built, NOT wired to assets yet.
  • FSLWeaponFireMode::FireForceFeedback (per weapon and per fire mode, beside FireCueTag), a bControllerRumble player setting, and dispatch from DispatchLocalCosmetics — local-only by construction, and the setting is read per shot so a pause-menu toggle applies immediately. Still to do: author the UForceFeedbackEffect assets, assign them per weapon, and add the settings row. ⚠ Keep each effect SHORTER than that fire mode's shot interval or full-auto retriggers it into a continuous buzz.

  • Dash thruster cue is DONE — FX and sound both in (Beepers). Reference facts below so the next
  • session does not re-derive them the expensive way.

    • The pack system was copied to /Game/SystemLink/FX/Character/NS_Thrusters rather than edited in
    • Content/Library/, so a pack update cannot clobber the tuning.

    • Duration is a User.Duration float user parameter, linked into the emitters' Emitter State
    • Loop Duration (Loop Behavior Once). ✅ Wired end to end 2026-08-05: CueParams.RawMagnitude = Module->DashDuration in SLGameplayAbility_Dash::ActivateAbility, read in the cue graph as Parameters → Raw Magnitude into Set Niagara Variable (Float). The FX now tracks whatever DashDuration the equipped mobility module specifies, so the two can no longer drift.

    • 0.3 s FX vs 0.2 s dash. DashDuration is not overridden in DA_SL_MobilityModule_MMA3, so
    • it is still the C++ default 0.2f (SLMobilityModuleDataAsset.h:41). The thrusters therefore burn 50% longer than the movement. Fine as a deliberate trailing flourish — but it was not chosen, and the two values will silently drift apart the first time either is retuned.

    • To make them track each other: add CueParams.RawMagnitude = Module->DashDuration; in
    • SLGameplayAbility_Dash::ActivateAbility next to the existing Location/Normal, then feed Parameters → Raw Magnitude into Set Niagara Variable (Float) "User.Duration" in the cue BP. ⚠ A one-shot emitter reads Loop Duration once at emitter spawn — so uncheck Auto Activate on the spawn node, set the variable, then Activate, or the first and only loop uses the default.

#▶ NEXT UP — the two items Beepers named 2026-08-05

1. Dash HUD indicator — C++ side BUILT, widget not started. USLMobilityComponent now exposes GetDashRechargeProgress() (0–1, BlueprintPure) and IsRechargingDash(), alongside the existing OnDashChargesChanged(Current, Max) delegate and GetDashCharges() / GetMaxDashCharges().

  • Progress is derived, not replicated: the server replicates RechargeWindowStartTime +
  • RechargeWindowDuration (two floats that change only on a dash or a charge landing) and each machine interpolates locally against GameState->GetServerWorldTimeSeconds(). Replicating the fraction itself would be per-frame traffic for a HUD bar. Never use GetWorld()->TimeSeconds here — it is per-machine and the client's bar would disagree with the server's timer.

  • The window is closed on equip and unequip as well as at a full pool, so the bar cannot creep toward
  • full for a module that is gone.

  • MaxDashCharges is 1 and stays there (decided 2026-08-05). DA_SL_MobilityModule_MMA3 was already
  • set to 1; the C++ default was 2 and is now 1 so future modules start there too. A dash is a committed decision, not a pooled resource. The indicator is therefore a single bar, not segments.

  • Live values on the MMA-3: MaxDashCharges 1, DashRechargeDelay 0.0, DashRechargeTime 1.25,
  • DashDuration 0.2, DashSpeed 1625. Because the delay is zero, the bar simply fills 0→1 over 1.25 s from the moment of the dash — the "dead beat vs immediate creep" question does not arise for this module. It would return if any module ever sets a non-zero delay.

  • Still to do: the widget itself — empty the instant a dash is spent, fill over the recharge. Bind
  • OnDashChargesChanged for the full/empty state and poll GetDashRechargeProgress() for the fill.

2. ADS for AR + shotgun — NOT started. Two decisions first. The pistol keeps its current behaviour (input already remapped to left shoulder). Left trigger becomes normal ADS, which does not display the weapon mesh in FP, and the reticle must know so it can adjust.

  • The blocker is one line. SLPlayerCharacter.cpp:502 reads
  • bAiming = WC && WC->IsSidearmActive() && Data && Data->ADSFieldOfView > 0.f — ADS is hardcoded to "sidearm is active", which is the pistol implementation. Everything downstream already generalises: ADSFieldOfView is per-weapon on USLWeaponDataAsset, and look sensitivity already scales by GetADSAlpha() (SLPlayerController.cpp:244).

  • The FP mesh hide must go through ApplyFirstPersonMeshVisibility(), not a second writer. That
  • function is the single owner of FP visibility as of BUG-030. A separate ADS setter means toggling view mode while aiming un-hides the weapon — the exact class of bug just fixed. ADS should be a factor it reads.

  • Left trigger may be double-booked. The planned secondary-fire design is "LT held = mode, RT =
  • fire" (two-phase chord via GAS tags). If LT is now ADS, one of the two needs rebinding. Settle it before the input and reticle work, not after.


#Reference — RocketThrusterExhaustFX pack + the dash cue (learned 2026-08-05)

Derived by reading the compiled HLSL inside the .uasset; Niagara's Python API exposes none of this.

  • Most of the pack's User Parameters are decoys. Only these are actually consumed by the compiled
  • scripts: User.Emissive_Boost and the eight User.Particulate_*. **Every User.Thrusters_, User.Smoke_, User.EnergyCore_ and User.HeatHaze_ parameter exists in the parameter store and is read by nothing.** Setting them from Blueprint is a silent no-op. The other emitters' values are baked constants (Constants.NE_Thrusters.InitializeParticle.Lifetime, …EmitterState.Loop Duration).

  • Scale already works via component scale. NMS_GlobalScale and NMS_GlobalVelocity (in every
  • emitter) read Engine.Owner.Scale, so Set Relative Scale 3D on the spawned component scales sprite size and velocity — i.e. the plume shortens as it narrows. This bypasses the user parameter store entirely, and it works regardless of local/world space. Per-emitter NMS_GlobalVelocity.VeloticyReduction (pack's typo) tunes how hard length follows scale.

  • GC_Mobility_Dash is a GameplayCueNotify_Burst (i.e. _Static): OnExecute only, **no
  • OnRemove, and CDO-based with no per-instance state — so you cannot** stash the spawned NiagaraComponent and Deactivate it later. To make an FX stop with the ability you either make it self-terminating (Loop Behavior Once + Loop Duration) or move to a GameplayCueNotify_Looping actor cue driven by AddGameplayCue/RemoveGameplayCue.

  • The socket is ThrusterLeftSocket / ThrusterRightSocket on
  • /Game/SystemLink/Characters/MasterChief/Meshes/MasterChief. The misspelling Thurster… fails silentlySpawn System Attached does not warn on an unknown socket, it just attaches at the component origin, so the FX plays from Chief's root instead of his back.


#Previous resume note (2026-08-03) — the content move

Branch level1-import. The tree was clean — everything below is committed.

#Then the two goals, unchanged

1. Level 1 playable.GameMode is DONE — verified 2026-08-06, World Settings points at /Game/SystemLink/Game/BP_SL_GameMode, which exists. (The old note here said it still pointed at a missing BP_SystemLinkGameMode; that was stale.) Collision is already done, author none (details in the 2026-07-31 note below, including the level1-fort-floor paper-floor risk), and 2 PlayerStarts are placed. So Level 1 may already be playable — nobody has actually run it. Play it before planning anything else here; the only known gap is that the old weapon pickups were removed and GAS pickups have not been re-placed (coordinates recorded in Docs/Level1Import.md §4).

2. MainMenu showing a real menu, built on the CommonUI stack over the existing diorama.

3. Then cleanup — the 207-asset delete list in Docs/Level1Import.md §3/§8. Nothing deleted yet.

#What this session changed

  • Committed the interrupted content reorganizationContent/{Environment,Levels,LevelPrototyping}
  • Content/SystemLink/…, 226 assets moved, 179 deletions. It had been sitting uncommitted on disk. Before committing, every one of the 48 files left at the old paths was checked and confirmed to be an ObjectRedirector stub — no real asset was stranded, and the levels are intact at the new paths (Level1 452 KB, MainMenu 2 MB). The redirectors are committed deliberately so old references still resolve; fixing them up is a separate pass and needs the editor.

  • Committed the hard-light bridge scripts, doc and source textures (the materials/meshes themselves
  • rode along with the move), the RocketThrusterExhaustFX pack, BP_GA_EquipDefaultLoadout, the new site screenshots, and the devlog's Halo military-calendar timestamps.

#Machine stability — resolved enough to stop worrying about

Turbo Boost off (2026-07-25) is what stopped the BSODs. Confirmed 2026-08-03: zero unexpected shutdowns in the 9 days since, against 14 in the 14 days before. That confirms the diagnosis — degraded silicon failing at high boost — but it is a workaround, not a repair, and the RMA case is now stronger, not weaker. Do not re-enable Turbo to speed up builds. Full detail in project memory.


#Previous resume note (2026-07-31, end of night) — make both levels playable

Branch level1-import (off mobility-cues, which is still unmerged — 15 commits ahead of main, a clean fast-forward whenever you want it).

Level 1 and MainMenu are imported from the old UE 5.6 project and both LOAD CLEAN. MainMenu runs. Full reference: Docs/Level1Import.md.

#The two goals, in order

1. Get Level 1 playable. It opens (164 actors, lighting intact) but nothing plays yet:

  • World Settings GameMode still points at the missing BP_SystemLinkGameMode — repoint
  • it at the ASLGameModeBase Blueprint. 2 PlayerStarts are already placed.

  • Collision — ALREADY DONE, do not author any. Verified in engine 2026-07-31: **zero
  • level meshes lack collision.** All 18 FBX_Level1_ / level1- meshes are set to CTF_USE_COMPLEX_AS_SIMPLE; the 8 others (SM_* prototyping, pMainTop, the two forrunner walls) use default with a convex hull each. The old project had already chosen complex collision — the same conclusion Docs/CollisionMeshes.md §5a reaches independently. The level should already be walkable.

    • One risk: level1-fort-floor is complex-as-simple and a zero-thickness sheet — a
    • paper floor with nothing beneath it. Sweeps handle it, but a dash (1625 cm/s) or a low spawn could punch through. If anything falls out of the world near the fort floor, that is the cause; fix by thickening it in Maya (Docs/CollisionMeshes.md §0), not by authoring hulls.

  • Weapon pickups were removed (they were the old non-GAS chain). Their exact coordinates
  • are recorded in Docs/Level1Import.md §4 — re-place GAS pickups there.

2. Get MainMenu showing an actual main menu. The diorama is intact — 729 actors, 6 BP_MasterChiefMenuGuy, and the CineCameraActor + CameraRig_Rail driving LS_MainIntro — but the menu logic is deliberately gone. BP_MainMenuPlayerController and the old WB_* widgets were not migrated because CommonUI supersedes them, and its GameMode now uses the stock PlayerController. Build the screen on the existing CommonUI stack (Docs/UISystem.md, Docs/MenuSystemPlan.md) over the diorama, with LS_MainIntro behind it.

3. Then cleanup — the verified-safe 207-asset delete list and the naming pass are already computed and waiting in Docs/Level1Import.md §3 and §8. Nothing has been deleted.

#What this session actually changed

  • Migrated Level1 + MainMenu + Environment/MasterChief/Weapons/Generic/
  • LevelPrototyping (~706 assets). 3rdParty_Assets (3.8 GB) and Blueprints deliberately left behind.

  • Fixed Level 1's compile errors by deleting the old pickup/MasterChief BP cluster
  • (7 assets) and removing 4 weapon pickup actors.

  • Fixed MainMenu's ~200 load errors with [CoreRedirects] in DefaultEngine.ini
  • CreateExport failures 200 → 0, 207 crate actors recovered. See Level1Import.md §5a.

  • Discovered the migration repaired a pre-existing bug: the project's own CommonUI text
  • and button styles had been referencing a font and two UI sound cues that did not exist in 5.7. They resolve now. Those 3 assets must never be deleted.

  • Main floor collision built + validated in Maya (95 hulls) — but see the ⚠ below; it was
  • an experiment and is not being shipped.

#⚠ Two things not to re-derive

  • **The Maya collision pipeline (Tools/maya_*_main_floor_collision.py) was an experiment
  • Beepers is not using. The reusable part is the reference: Docs/CollisionMeshes.md (§0 hull thickness rule, §5a whether authored collision is needed at all**).

  • EditorAssetLibrary.delete_asset in a loop CRASHES the editor — access violation in a
  • background worker on the third delete. One delete per bridge call.


#Previous resume note (2026-07-31) — main floor collision is BUILT; UE import not yet done

mobility-cues is merged. main-floor.mb now carries 95 UE-ready collision hulls (91 boxes + 4 ramp wedges, 1140 tris) named UCX_SM_MainFloor_00..94. Built, validated and exported by script — not yet imported into Unreal, so nothing is PIE-proven.

Next step: import C:\3D-DEV\HaloProject\Maya\Level1\FBX\SM_MainFloor.fbx (Interchange → Assets → Collision → Import Collisions ON, auto-generate collision OFF), confirm 95 hulls land on the static mesh, then walk the ramps and the pit edge in PIE.

Open question that may retire this whole pipeline — Docs/CollisionMeshes.md §5a. The render
mesh is 436 triangles; the authored collision is 1140, i.e. **2.6× heavier than the thing it
approximates**. Use Complex Collision As Simple would be exactly as accurate, with no level merge,
no diagonal overhang and no maintenance. The hulls' only real advantage is solidity: the source is
an open shell whose ground plane is literally zero-thickness, so complex collision there is a paper
floor a dash could punch through. Import it both ways and test before relying on either — and
consider the upstream fix instead (give the floor thickness in Maya, then use complex).

Three scripts, all headless via mayapy.exe:

  • Tools/maya_build_main_floor_collision.py — generates the hulls into the scene (idempotent; clears
  • and rebuilds). Pristine pre-collision copy kept once at main-floor.pre-collision.mb.

  • Tools/maya_validate_main_floor_collision.py — naming, closed, convex, no overlaps, and a coverage
  • sweep over sampled walkable points. Currently: hulls=95 overlaps=0 coverage=1479/1479 VALIDATION PASSED.

  • Tools/maya_export_main_floor.py — combines duplicates of the 21 authored pieces into one render
  • mesh SM_MainFloor, exports Z-up FBX with the hulls, and re-imports it to verify the names survived. The authored geometry is never modified or saved by the export.

Method (why it isn't bounding boxes): every up-facing face is projected to plan, the union is cut into non-overlapping rectangles, and each rectangle is extruded from its own surface down to the next surface beneath it — so outlines, level heights and the vertical walls between them all survive. The four 16° ramps stay sloped convex prisms rather than stair-steps.

  • Deliberate 3.2 cm simplification: the plates sit 3.2 cm proud of the ground plane. Honouring that
  • step costs 282 hulls instead of 91. LEVEL_MERGE_TOLERANCE in the builder is the one edit that buys the exact-shape version.

  • Scope: main-floor.mb only — 21 near-flat pieces, no walls. The rest of Level 1 (layerFortMain,
  • layerMainWallsUE, layerMainTop, layerBasement) lives in other scenes and has no collision yet.

  • The superseded Tools/maya_create_main_floor_collision.py (bounding-box approach, writes a sibling
  • main-floor_collision.mb) is still in the tree — delete it or it will get run by mistake.

Full reference: Docs/CollisionMeshes.md — UE's naming contract, the per-type rules, the import settings, the case study with the real numbers, and what bit us. Footguns condensed in Docs/Footguns.mdMaya / Collision Export: the polygon-normal iterator crash, the by-name-only collision contract, the one-FBX/first-mesh-only limit, and the centre-tested grid quietly punching holes in the floor.


#Previous resume note (2026-07-31) — Maya MCP, then the MobilityModule socket

A Maya MCP was installed and wired up but has NOT been used yet — it needs a Claude Code restart.

  • maya-mcp 0.6.1, pip-installed into C:/Python313 (user site-packages). Added to .mcp.json as
  • maya, launched as C:/Python313/python.exe -m maya_mcp.server — the explicit interpreter, not the console script, because maya-mcp.exe landed in the user Scripts dir and PATH here needs a restart to pick things up. Import verified under that interpreter.

  • It does not launch or embed Maya. It talks to a running Maya over TCP on localhost:7001. With
  • Maya open, in the Script Editor (Python):


  import maya.cmds as cmds

  cmds.commandPort(name=":7001", sourceType="python")

Failure is loud (MayaUnavailableError naming host/port/attempts), not silent. There is also a maya_mcp.maya_panel module (controller + preferences) that looks like a Maya-side UI for this.

#First task: add MobilityModule_Socket to MasterChief_Skeleton

Chosen because it is small, entirely objective, verifiable from both ends, and it unblocks the last loose end on the thruster cues.

  • USLGameplayCue_MobilityThrusters prefers a socket named exactly MobilityModule_Socket on the TP
  • mesh and silently falls back to spine_04 when it is missing. It is falling back today.

  • Socket goes on the upper back, between the shoulder blades (per MobilityAssistModule.md Phase 6).
  • Verify across the seam: socket exists in Maya → survives export → Unreal's skeleton reports it → the cue
  • attaches to it rather than the fallback. Then the nozzle placement pass (LeftNozzleOffset / RightNozzleOffset) can finally be judged against real geometry.

⚠ **Working rule for anything Maya: edit the generator script and re-run it — do not hand-mutate a
scene. SourceArt/ is gitignored** (3ef4515e), so unlike every C++ change there is no version
history to recover from. The _v1/_v2/_v3 separate-script pattern already in Tools/ is the safety net.

Good fit for the MCP: objective checks (axis/units/bounds/pivots/normals/joint orientation), closing the generator loop without a human look-check, the motion-clearance test at the duct's pitch/yaw limits, and the Maya↔Unreal contract (bone names, up-axis, cm units, sockets, material slots). Not a fit: silhouette, proportion, whether it reads as Halo, animation weight. Those stay with Beepers.


#Session 2026-07-31 — cues fire, held-trigger fire; branch mobility-cues

Three things PIE-verified by Beepers: held-trigger fire, dash cue, double-jump cue. melee-target-lunge merged to main (PR-less, local merge + push).

Mobility cues now actually fire — they never had before. Full writeup: Docs/MobilityCues.md. Three separate silent failures, each of which alone was enough to produce nothing:

  1. A native UGameplayCueNotify_Static subclass is never registered. The manager asset-scans
  2. GameplayCueNotifyPaths and reads GameplayCueName off asset data; a C++ class is not an asset. Fixed by adding Blueprint subclasses in the scanned path.

  1. The asset NAME must derive the tag. AbilitySystemGlobals.h:129 — when a child's tag equals its
  2. parent's, the engine clears the child's tag, re-derives from the asset name, and on failure restores the tag but not GameplayCueName. Our native classes are named GC_Mobility_Dash, so the editor derives a valid tag onto them; the Blueprint inherited it, tripped the branch, and GC_SL_Mobility_Dash derived the non-existent GameplayCue.SL.Mobility.Dash. Renamed the assets to GC_Mobility_Dash / GC_Mobility_DoubleJump — the SL convention is what broke them.

  1. The cue was gated on HasAuthority(), which discards ExecuteGameplayCue's own predicted branch
  2. and cost the acting player a round trip. Removed; it does not double-play.

An in-editor tag edit registers a cue for that session only (PostEditChangeProperty
HandleAssetAdded). A broken asset looks fine until the next restart. **Never accept "it fired once"
— restart and fire again.** This wasted most of a session and made a correct diagnostic look like a
false negative.

Held-trigger fire — pressing fire during an equip now fires when the weapon is ready. ASLPlayerController::RetryPrimaryFireAfterEquip, bound to Triggered, latches on the blocked→available edge. Decisions: SingleShot gets exactly one shot (falls out of the latch for free), respawn included, pure hold-state.

  • Latch on States.Weapon.Busy, NOT Equipping. Only the TP equip abilities own Equipping;
  • FP owns only Busy, and every fire ability blocks on Busy. Watching Equipping fired the retry while the FP equip still held Busy, so it was blocked exactly like the original press — a silent no-op that looked like the input binding not working. New USLWeaponsComponent::IsWeaponBusy() is the accessor to use for anything gating on fire availability.

  • Consequence: Busy is broader than "equipping", so fire resumes whenever the weapon frees up. Wider
  • than the original ask; kept deliberately.

Next on the cues (the actual ask): real effects instead of the placeholder NS_RocketTrail, audio (dash sound was mid-authoring), a nozzle placement pass, and a montage. Authoring guidance + the Parent: On Execute pure-node trap are in Docs/MobilityCues.md.

#Session 2026-07-30 — melee target lunge; branch melee-target-lunge

aim-target-refactor merged (PR #33). New branch melee-target-lunge off main.

Melee now lunges at an acquired enemy — built, built clean, suite 18/18, NOT yet PIE-tested. Full reference: Docs/Melee.md → Target Lunge. Direction points at the target (horizontal only); speed interpolates MeleeLungeSpeedMeleeTargetLungeSpeed across MeleeLungeRange, so a point-blank swing keeps the old nudge and a real gap gets a committed leap. Three new per-weapon values on USLWeaponDataAsset; no target = exactly the old behaviour.

  • The target is chosen once, on the machine that owns the input (ASLPlayerController::Melee
  • FGameplayEventData::Target), and each machine recomputes the vector from its own copy of that actor. This is the dash direction lesson applied to a world-derived choice rather than an input-derived one. Verified in engine source that the whole event struct reaches the server (AbilitySystemComponent_Abilities.cpp:1923).

  • The one thing that fails silently: if Target does not survive the RPC, the server does a plain
  • forward lunge while the client leaps — reading as "melee feels inconsistent", not as replication. sl.Melee.Debug 1 logs the decision per machine; 2 draws it (server red, client green). Check it as a non-host client before trusting the feature.

  • LaunchCharacter is not replayed on a client correction (FSavedMove_Character never stores
  • PendingLaunchVelocity). Pre-existing, but 1600 stresses it ~2.7× harder than 600 did. If it rubber-bands the fix is a root motion source, as the dash did — don't apply that blind, test first.

  • SystemLink.Melee.LungeSpeed covers the curve as a pure function (endpoints, clamp past range, zero
  • range not dividing by zero, a misconfigured asset never making the lunge worse, monotonicity).

  • Also: SystemLinkCore.uplugin now declares its Niagara dependency — UBT had been warning on every
  • build since the thruster cues landed, and it only worked because the project happens to enable Niagara.

Still open on melee: never PIE-tested; MeleeImpactDelay is a fixed 0.3 s and not gap-aware; you can lunge off a ledge (deliberate, Kill Z catches it); corpses are valid lunge targets (consistent with the reticle and the damage sweep, but worth a pass if bodies persist).

Mobility (2026-07-29 art session, committed 2026-07-30): the dash + double-jump thruster cues (UGC_Mobility_Dash / UGC_Mobility_DoubleJump) were written and Live-Coding-verified on 07-29 but had never been committed — they are in now. Phase 4 (double jump) is code-complete and PIE-confirmed working by Beepers. Remaining wish for jump/dash: a montage to go with the thrust FX. There is deliberately no DoubleJumpZVelocity — see the note on USLMobilityModuleDataAsset; the second jump is CMC's native jump, which is what makes it predict correctly.

#Session 2026-07-29 — mobility suit rocket exhaust implemented

  • Added native GameplayCue notifies for GameplayCue.Mobility.Dash and
  • GameplayCue.Mobility.DoubleJump in Mobility/GameplayCues/SLGameplayCue_MobilityThrusters.

  • Both cues spawn twin, armour-mounted Niagara jets using the existing NS_RocketTrail: dash exhaust points
  • opposite the replicated dash vector; double-jump exhaust points down and burns slightly larger/longer.

  • Uses MobilityModule_Socket when authored and safely falls back to spine_04; jets auto-deactivate and return
  • to Niagara's component pool after the short burst.

  • UE 5.7 Live Coding compile verified: Result: Succeeded (2026-07-29).
  • Visual placement still needs a PIE feel pass. If the nozzles do not sit on Chief's back plates, tune
  • LeftNozzleOffset / RightNozzleOffset or add the dedicated socket to MasterChief_Skeleton.

  • Generated the first Maya 2026 MMA-3 backpack model from the approved concept via
  • Tools/build_mma3_thruster_backpack.py: editable .ma plus UE-ready skeletal FBX under SourceArt. Current LOD0 blockout is 65 mesh pieces / 16,328 vertices / 32,428 triangles, Z-up centimeters, with root, articulated thruster_l / thruster_r, FX_Thruster_L / FX_Thruster_R, and MobilityModule_Socket. Next art pass: fit-check against Chief, silhouette/detail refinement, final UV/bake, textures, collision and LOD1/LOD2.

  • Screenshot-driven refinement completed as non-destructive v2 (MMA3_ThrusterBackpack_v2.ma and matching FBX):
  • replaced rectangular housing slabs with clean manifold custom-prism armor, tapered the chassis, split the rear shell into center/shoulder/lower plates, reduced and integrated the gimbals/nozzles, and tightened the envelope to ~43 cm wide × 50 cm tall. Maya rebuild verified clean at 66 pieces / 16,880 vertices / 33,528 triangles; no non-manifold/bevel warnings remain. Awaiting a v2 viewport look-check before final surface-detail/texture pass.

  • v2 viewport review exposed reversed custom-panel normals and weak stacked-cylinder nozzles. Fixed non-destructively
  • in v3: custom prisms now conform normals and explicitly orient the outer/rear face toward -X; nozzle assemblies are continuous multi-station tapered bodies and flared bells with recessed dark throats and defined steel lips. Maya 2026 rebuild/export succeeded cleanly: 62 pieces / 17,168 vertices / 34,104 triangles. Files: MMA3_ThrusterBackpack_v3.ma and SK_MMA3_ThrusterBackpack_v3.fbx under the existing SourceArt folders.

  • New rotating-duct design built as its own asset (earlier twin-nozzle iterations preserved):
  • MMA3_RotatingDuct_v1.ma + SK_MMA3_RotatingDuct_v1.fbx, generated by Tools/build_mma3_rotating_duct_v1.py. It uses the approved single paddle-duct concept and contains a static plenum/race, nested nozzle_pitchnozzle_yaw joints, refractory-lined rectangular outlet, replaceable lip, four flow vanes, actuator/gearbox, and FX_Thruster. Maya 2026 build/export succeeded cleanly at 67 mesh pieces, 11,280 vertices, 22,304 triangles; Z-up bounds ~27.8 cm deep × 34 cm wide × 47.6 cm tall. Next: viewport review, motion-clearance check at pitch/yaw limits, then final UV/bake/textures/collision/LODs and Unreal integration.

  • First Maya viewport review showed the v1 scene lying sideways: design coordinates were Z-up but Maya reopened
  • them in its normal Y-up workspace. Fixed non-destructively in MMA3_RotatingDuct_v2.ma and matching FBX by converting/freeze-baking the GEO and skeleton hierarchies into Maya-native Y-up before rigid skinning; FBX still exports with Z-up for Unreal. Clean rebuild verified with upright bounds: 47.6 cm Maya-Y height × 34 cm width.

#Active Branch

level1-interactables — branched from main at 99941455 (PR #35 merged level1-import). Carries ASLDoor + USLShootableComponent (8 commits, 2026-08-07) and ASLTeleporter (2026-08-10). References: Docs/Doors.md, Docs/Teleporters.md.

<details><summary>Previous: <code>mobility-cues</code> (merged to main, PR #34)</summary>

Carried the cue registration/prediction fixes and the held-trigger fire feature (unrelated, but landed there). Reference: Docs/MobilityCues.md. All three features PIE-verified 2026-07-31.

</details>

<details><summary>Previous: <code>melee-target-lunge</code> (merged to main)</summary>

Branched from main after aim-target-refactor merged (PR #33). Reference: Docs/Melee.md → Target Lunge.

</details>

<details><summary>Previous: <code>aim-target-refactor</code> (merged, PR #33)</summary>

Branched from main (PR #32 merged). Plan: Docs/AimTargetRefactor.md (flowchart: Docs/AimTargetRefactorFlowchart.md).

</details>

#Session 2026-07-28 — Phases 1–4 done; BUG-028 reticle SOLVED; BUG-029 found

Done and verified in PIE:

  • Phase 1 — shared aim vocabulary: ESLAimTarget / FSLAimRay / FSLAimTarget (Public/Types/SLAimTarget.h),
  • BuildAimRay() / ClassifyHit() / ResolveAimTarget() on USLWeaponsComponent.

  • Phase 2USLPlayerPerceptionComponent on ASLPlayerCharacter: local-player only, throttled by
  • sl.AimTarget.Rate (30 Hz), broadcasts OnAimTargetChanged only on kind change. sl.AimTarget.Debug 1 draws the ray + classification.

  • Phase 4USLHUDWidget::InitializePerception() binds the delegate and drives USLReticle::OnTargetDetected;
  • called from InitializeLocalPlayerHUD on every possession. SwapReticle now seeds the new reticle with the current aim state. Red Reticle Check deleted from WBP_SL_HUDWidget. Verified: host ✅, client ✅, weapon swap across two reticle classes ✅, client die→respawn→aim ✅.

  • Debug commands (Private/Debug/SLDebugCommands.cpp): sl.DumpTags (every owned tag WITH ITS COUNT, both
  • worlds, flags any count > 1), sl.DumpWidgets, sl.ClearWeaponLocks. These are what cracked BUG-028 — use them before theorising. (Known wart: sl.DumpWidgets' NOT ON SCREEN flag is wrong for layer-hosted widgets — IsInViewport() is false by design when a widget is parented into the primary layout. Read slateRealised.)

BUG-028 reticle symptom: root-caused and fixed — see the BUG-028 RESOLUTION entry in Docs/BugTracker.md. Short version: the HUD BP's Event Tick chain had unwired Is Not Valid pins on validated GETs of cached per-pawn references; a respawn invalidated one and the whole chain died silently, permanently, with no log.

#✅ DONE 2026-07-29: BUG-029 — ragdoll never cleared on observing clients

Fixed and verified in 3-player PIE. bRagdollActive is now replicated with OnRep_RagdollActive; authority is the only writer and EnableRagdoll() is a no-op off authority, so a client can no longer ragdoll a pawn the server considers alive. DisableRagdoll() now exists and restores capsule collision, mesh collision profile and the mesh's authored relative transform (captured in BeginPlay). Tests: SystemLink.Character.RagdollRoundTrip + RagdollIdempotence.

A bug the test caught while being written: ApplyRagdollState originally used IsSimulatingPhysics() as its
own idempotence guard, so wherever simulation declined to start (no physics asset) the enable path still
disabled the capsule while the disable path restored nothing. Idempotence is now tracked against an explicit
bRagdollApplied flag. Lesson: don't key an exit path on a symptom of the entry path.

<details><summary>Original report (kept for the RCA)</summary>

In 3-player PIE the host is invisible to every client after the host respawns: the observing clients' proxy is still ragdolling and has free-fallen to Z ≈ −54,559 (mesh ≈ −1,153,901) at terminal velocity, while the server's copy walks normally. EnableRagdoll() is driven by an anim notify (per-machine, non-authoritative — same family as BUG-023) and DisableRagdoll() does not exist; the design assumes the pawn is destroyed, so any machine that ragdolls a pawn it doesn't destroy is stuck forever.

Likely also the true cause of BUG-026 (a client can't red-reticle a respawned host because that proxy's physics bodies are a million units underground — the ECC_WeaponTrace change treated a symptom).

Full RCA: BUG-029 in BugTracker.md.

</details>

#✅ DONE 2026-07-29: Kill Z failsafe — falling out of the world kills you

ASLCharacterBase::FellOutOfWorld routes into the normal damage pipeline (FellOutOfWorldDamageEffect = GE_Damage, magnitude FellOutOfWorldDamage = 99999), so death, cues, scoring and respawn all behave as any other kill. Map side: TestMap World Settings KillZ. Verified in PIE with and without overshield.

Three things it took three iterations to get right, all worth remembering:

  • FellOutOfWorld fires every tick while below Kill Z, not once — needs a latch (bFellOutOfWorldHandled).
  • Never call Super — the engine default is Destroy(), which cancels the respawn scheduled by
  • StartServerDeathTimeout's weak lambda. Symptom: you die and simply never respawn.

  • Overshield forces health damage to zero (SLDamageExecution.cpp) no matter the magnitude, so the failsafe
  • clears the Overshield tag first — falling out of the world is removal, not combat, and no pickup may prevent it. Uses SetLooseGameplayTagCount(..., 0) rather than RemoveLooseGameplayTag, since loose-tag counts are known to inflate here.

Open decision: a fall currently records itself as the source object, so it scores as a suicide.

#✅ DONE 2026-07-29: ALL SIX PHASES of the aim refactor

WBP_SL_HUDWidget's Event Tick is empty. Suite 17/17. Verified in 3-player PIE.

  • Phase 3 — fire path adopts BuildAimRay(). Found two bugs while unifying: BuildAimRay hardcoded
  • bPrimary=true (secondary fire got the primary's range), and firing with no player controller traced from the world origin silently.

  • Phase 5 — spread + equip-visibility moved to the perception component. Spread reads GroundSpeed (2D)
  • rather than the BP's 3D velocity, so vertical motion no longer blooms the crosshair.

  • Phase 6 — reticles cached, never destroyed. Measured with sl.DumpWidgets Reticle: 6 → 10 across many
  • swaps, every instance _C_0 (growth is two new classes, not churn), exactly one non-collapsed per machine. The re-apply on activation is load-bearing — cached reticles hold stale state while collapsed.

#Requested: fall-depth failsafe — a player below a Z threshold dies automatically

Asked for 2026-07-29, straight after BUG-029. A player who ends up below a depth threshold should die rather than fall forever. Worth having on its own merits, and it would have turned BUG-029 from "the host is mysteriously invisible" into "the host keeps dying" — a far louder, far cheaper symptom.

Likely needs almost no new machinery. UE already has this: AWorldSettings::KillZ (World Settings → Kill Z) plus AActor::FellOutOfWorld, which fires when an actor drops below it. The work is to override FellOutOfWorld on ASLCharacterBase so it routes into the GAS death flow (authority applies the death effect / respawn) instead of the default behaviour, which just destroys the actor — a silent Destroy() on a player pawn is its own debugging trap. Set KillZ in the map's World Settings, well below any reachable geometry.

Two things to decide when building it: whether falling out of the world is a suicide for scoring purposes (it goes through the normal death path, so it will need a killer of some sort), and whether it should also fire for ragdolled corpses — with BUG-029 fixed a corpse should not be falling, so a corpse tripping KillZ is a signal worth logging loudly rather than silently handling.

#Also still open

  • Loose-tag counter inflationFIXED 2026-07-29 (commit a5dd138c).
  • USL_BlueprintLibrary::SetBooleanStateTag assigns via SetLooseGameplayTagCount instead of incrementing; all 15 C++ call sites converted and no raw Add/RemoveLooseGameplayTag remain outside the helper. Verify with sl.DumpTags, which flags any count above 1.

  • HUD "out of ammo" message.
  • MMA-3 Phase 5 — dash charge HUD indicator, still unbuilt. Phase 6's cues are built and committed
  • (2026-07-30); what's still wanted there is a montage to play alongside the thruster FX, and a PIE look at nozzle placement (tune LeftNozzleOffset/RightNozzleOffset or add MobilityModule_Socket to MasterChief_Skeleton).

  • Undecided: Docs/072326-12265-01.dmp (7.5 MB) and 59 untracked Docs/Screenshots/Site/ files. **Never
  • git add -A** — that sweeps in ~286 MB.


#Previous focus — CommonUI menus (paused)

menu-foundation — branched from main after grenade-refinement merged (PR #28, commit 5079aca). All the grenade drop/chain feel, pistol ADS (first-person-rendering swell fix), and muzzle-flash work is merged.

Focus: CommonUI menus — Phase 0b (controller-input pipeline + pause wiring). Plan spine is Docs/MenuSystemPlan.md; plumbing/footguns in Docs/UISystem.md; flow/online in Docs/MenusAndOnline.md. NEW (2026-07-16): the menu framework is becoming a Fab product — Docs/MenuKitExtractionPlan.md (extraction + AI-native manifest/generator; see the 2026-07-16 section below). Action bar still ships first.

#🎯 NEXT MILESTONE — Settings menu = the menu "stopping point" (decided 2026-07-20)

Finish menus to a stopping point, and that point is a working Settings menu. After it lands, the menu push pauses and the next initiative is the Foundry Game Agent Bridge (Docs/Foundry/Game_Agent_Bridge.md, deferred until this milestone is met).

Definition of done (the gate): a Settings screen reachable from the pause menu, fully gamepad-navigable, that reads and persists player settings (hybrid settings plan in Docs/MenusAndOnline.md — player settings ship first), with the command bar showing Back.

Open menu threads to clear on the way there:

  • Command bar — DECISION 2026-07-20: HARDCODE Back + Accept, skip the dynamic CommonBoundActionBar. The
  • bar is display-only (B already closes via bIsBackHandler; A already clicks the focused button), so a fixed strip is safe and simpler. Full recipe: Docs/CommandBarBackButton.md. Build WBP_SL_CommandPrompt (CommonActionWidget glyph + CommonTextBlock label, 2 exposed vars) → put 2 in WBP_SL_CommandBar (Back = IA_SL_UI_Back, Accept = IA_SL_UI_Confirm) → embed in each screen (Option A: constructs with the screen so the glyph resolves while IMC_SL_UI is live). Dynamic bar reframed as a MenuKit-product upgrade, not a game blocker.

  • WBP_SL_BoundActionButton — already fully built (verified via bridge 2026-07-20): tree is
  • HorizontalBox_0Text_ActionName [CommonTextBlock] · Spacer · InputActionWidget [CommonActionWidget], both bind names/types correct. The old "needs a root-swap / missing glyph" note was STALE — a prior bridge session built it. It is unused by the hardcoded approach; parked for the MenuKit dynamic-bar upgrade. (Note: reading BindWidget props off the CDO returns None even when bound — binding is per-instance, not on the CDO; don't use CDO reads to test a bind.)

  • Bridge fact learned 2026-07-20: it can build widget trees under an existing root (duplicate a rooted
  • seed → clear → build), but cannot mint or swap a root (RootWidget protected read+write).

  • Verified state (2026-07-20): IA_SL_UI_Back.ActionDescription="Back", IA_SL_UI_Confirm.ActionDescription
  • ="Select"; WBP_SL_PauseMenu CDO has bIsBackHandler + bIsBackActionDisplayedInActionBar both true; shared layout WBP_SL_PrimaryGameLayout root = Overlay_54 holding HUDLayer/GameStack/MenuStack/ModalStack.

#Session 2026-07-26 — setting descriptions (data-driven) DONE; sensitivity merged to one row

Setting copy is now data-driven and focus-published. PIE-verified ✅ (Beepers: "all that is working").

  • FSLSettingCopy (Public/UI/SLSettingCopy.h) — new FTableRowBase: DisplayName + Description, plus a
  • static Resolve(Handle, OutName, OutDesc) that no-ops on an unset handle so a row can still author both inline. Asset: DT_SL_SettingCopy (/Game/SystemLink/Data/, next to DA_SL_ProTips), 5 rows — LookSensitivity, InvertLookY, InvertLookX, ADSSensitivity, FieldOfView.

Why a DataTable and not a ProTips-style data asset: the row NAME is the setting id (no duplicate key
field), CSV round-trips for bulk copy edits, and FDataTableRowHandle gives the designer a **dropdown of
valid rows** instead of a typed FName. Pro Tips are a pool you sample; setting copy is a keyed lookup.
Deliberately no UDeveloperSettings pointer — the handle already carries its table, so a settings
pointer would be a second source of truth the picker ignores. (Cost: a MenuKit licensee swapping tables
re-points rows.)
  • Rows carry one picker. USLRotatorRow + USLSettingsRowWidget each gained
  • UPROPERTY SettingCopy (meta=(RowType="/Script/SystemLinkCore.SLSettingCopy") — the row-name dropdown is filtered by the asset's RowStructure tag, which is the path name), Description, and ApplySettingCopy() called from NativePreConstruct — so the designer previews real copy, and the duplicate SetDisplayName-per-row graph work is gone. Table wins over inline values when it resolves.

  • Delivery via the focus coordinator, not per-row wiring. ISLFocusable gained non-pure
  • virtual FText GetFocusDescription() + GetFocusDisplayName() (existing implementers compile unchanged; buttons return empty = "clear the panel"). USLMenuFocusSubsystem publishes OnFocusedCopyChanged(DisplayName, Description) (BlueprintAssignable) + GetFocusedDisplayName() / GetFocusedDescription(), broadcast from SetFocused only — NOT on focus loss, because Slate sends focus-lost before focus-gain and blanking there flickers the panel on every row move. Its static Get is now BlueprintPure + DefaultToSelf (one node in BP). One screen binds once; every future screen/control gets descriptions free. (Shipped first as one-param OnFocusedDescriptionChanged; renamed when the heading was added — a delegate named "description" carrying a title is the kind of drift that costs a session later.)

  • WBP_SL_Settings wiring (Beepers, in-editor): blocks named HeadingText + DescriptionText; Construct →
  • Get Menu Focus SubsystemBind Event to On Focused Copy Changed → custom event On Focused Copy ChangedSet Text on both (DisplayName → heading, Description → body). PIE-verified ✅. (A SET on Text is fine — TextBlock.h declares it BlueprintSetter="SetText", so it's a real SetText() call, not a silent property poke.)

  • Destruct-unbind footgun hit and removedGet Menu Focus Subsystem returns None during teardown
  • ("Accessed None … CallFunc_Get_ReturnValue" on the unbind node), and the unbind is unnecessary anyway: BP bindings are weak and AddUnique compacts stale entries. Also: "Unbind ALL Events" clears the delegate for every listener (EX_ClearMulticastDelegate) — never use it on a subsystem delegate. → Docs/Footguns.md.

  • Sensitivity merged to ONE row (Beepers): WBP_SL_Settings rows are now Row_Sensitivity, Row_InvertY,
  • Row_InvertX, Row_ADS, Row_FOV under Overlay_0 → SettingsBox → RowBox. Both LookSensitivityX/Y stay in the save game (SLPlayerController::Look reads them independently) — the single row drives both setters, so an "advanced / separate axes" toggle later is pure UI, no save migration.

  • Build clean (Result: Succeeded). ⚠ Pre-existing deprecation to fix before the next engine bump:
  • SLSettingsRowWidget.cpp:29 uses EKeys::Virtual_AcceptVirtual_Gamepad_Accept.GetVirtualKey().

Profile subsystem re-scoped for split-screen + multiple profiles (built ✅, PIE-UNTESTED). Decision this session: profiles must support split-screen, and it's fine for a player to pick a profile.

  • USLPlayerProfileSubsystem is now a ULocalPlayerSubsystem, not a UGameInstanceSubsystem. The old
  • scope was a split-screen bug: one instance for the whole game meant both players shared a profile, so P2 inverting Y flipped P1 too. Now each local player owns its profile, and a widget resolves its own player's automatically.

  • New accessors GetForWidget (BP: Get Player Profile Subsystem (Widget), defaults to self) /
  • GetForPlayerController. The 4 C++ call sites (SLPlayerController.cpp ×3, SLPlayerCharacter.cpp) use them.

  • BlueprintPure getters added (GetLookSensitivityX, GetFieldOfView, …) so graphs read AND write
  • through the subsystem and never touch the save game type. They fall back to the save game's class defaults, so defaults live in exactly one place.

  • One save slot per profileSLPlayerProfile_<Name>, SlotUserIndex always 0 (profiles disambiguate by
  • name, not controller slot). LoadProfile(Name) flushes the outgoing profile, loads/creates, broadcasts. Pre-profile saves in the unsuffixed SLPlayerProfile slot are adopted once into Default.

  • BP BREAKAGE EXPECTED: any graph using Get Game Instance Subsystem → USLPlayerProfileSubsystem (that's
  • how WBP_SL_Settings reads today) will fail to compile. Swap to Get Player Profile Subsystem (Widget), and take the chance to read via the new getters instead of GetProfile → field.

  • Full writeup: Docs/PlayerProfiles.md (scope rationale, storage, lifecycle, read/write contract, how to
  • add a setting, and §8 what's NOT built: profile index/registry, create/rename/delete, picker UI, EOS identity).

Split-screen is now a committed target (decided 2026-07-26) — NOT implemented. Profiles + menu focus are per-local-player and therefore ready; nothing yet creates a second local player, splits the viewport, routes a second input device, or gives each player their own UI layers. Work item + checklist: MenusAndOnline.md §9 (biggest unknown: whether USLPrimaryGameLayout/ASLPlayerHUD construct per local player — that path was written single-player).

#🏁 MILESTONE GATE MET — Settings menu seeds, writes back, and persists (2026-07-26)

All five settings survive a full PIE stop/restart. Beepers verified: change every row → B to close → stop PIE → relaunch → reopen Settings → all values intact. That was the defined stopping point for the menu push, so the Foundry Game Agent Bridge (Docs/Foundry/Game_Agent_Bridge.md) is now unblocked.

Row config pattern that landed (better than the original plan). Range lives on the ROW, not the screen: WBP_SL_SettingsRow_Rotator exposes Use Numeric Range / Minimum / Maximum / Step / Decimals, and its own Event Pre Construct calls SetNumericRange behind a Branch on Use Numeric Range. Per-instance in the details panel; Off/On rows leave the box unticked and keep their authored Options. Child PreConstruct runs before the parent's Event Construct, so the screen's seeds always land after the range exists.

  • Row_Sensitivity 0.5–10 step 0.5 · Row_ADS 0.1–2.0 step 0.1 · Row_FOV 80–110 step 5 (Decimals 0).
  • Screen Construct → Sequence → five seeds, all reading the new subsystem getters with Broadcast OFF.
  • SaveProfile on On Deactivated, behind an IsValid on the subsystem (teardown guard).

Two bugs found and fixed on the way — both looked like "settings won't save":

  1. Rows were bound to OnValueChanged, which carries the INDEX. Every change wrote 0/1/2… into a clamped
  2. setter, flooring it (FOV→70, ADS→0.1, sens→0.05). Rebound to OnFloatChanged / OnBoolChanged; verified in the saved asset (OnValueChanged now absent, all five BndEvt names are SLOnRotator*Changed).

  1. The save file already held that garbage — every field sat at its clamp minimum, so the (working) seed
  2. faithfully displayed junk, and FOV showed 80 because SetSelectedByFloat(70) picks the nearest notch. Deleted SLPlayerProfile_Default.sav + the legacy SLPlayerProfile.sav for a clean baseline → FOV opened at 95. If settings ever look wrong after a wiring change, suspect the .sav before the graph.

Debug note: a bound event's BndEvt__… function name is baked at creation and **survived both a widget
rename and a class change** — it advertised SLOnFloatSettingChanged (a slider delegate) on a rotator row and
caused a wrong diagnosis. Don't trust it; check the name table or re-create the node. → Docs/Footguns.md.

#Mouse smoothing as a player setting + test suite grown to 11 (2026-07-27)

Mouse smoothing is now a player choice, not a global default. Beepers plays on a trackball, which produces coarse, low-poll deltas that average out well — but the same smoothing on a 1000 Hz mouse is pure added latency. Shipping the developer's device preference as everyone's default was the thing to avoid.

  • USLInputModifierMouseSmooth (Public/Input/) reads MouseSmoothing off the player profile each frame and
  • passes input through untouched when off. Smoothing is a half-life in seconds, so feel is identical at 60 and 240 fps. Engine's stock Smooth couldn't be reused: it's baked into the mapping with no runtime switch, and it's MinimalAPI, so its implementation isn't linkable from another module.

  • bMouseSmoothing on the profile, default OFF (raw is right for a normal mouse). Applied to the
  • Mouse2D mapping only — the gamepad keeps its dead zone and nothing else.

  • Assets done: DT_SL_SettingCopy gained a MouseSmoothing row; Row_MouseSmoothing added to
  • WBP_SL_Settings. ⚠ Beepers still owes the graph wiring (seed from Get Mouse Smoothing on Construct, bind On Bool Changed → Set Mouse Smoothing) — the bridge can build widget trees but not Blueprint graphs.

Automation suite now 11 tests, all passing headless in ~1s. Added this session:

  • Mobility.Dash.Direction + .YawRoundTrip — the direction rules that broke in play (back folds forward,
  • strafe-back keeps only the sideways half, pitch can't leak in) and the client→server yaw encoding, which is load-bearing for MP. Required extracting a pure ProjectDashDirection(Input, Yaw) — an improvement anyway.

  • Content.* ×4 — every setting-copy row has non-empty text; every SettingCopy handle in WBP_SL_Settings
  • resolves; no null modifiers in IMC_Default; the mobility module's ability set actually grants the dash ability + attribute set. These target content that silently doesn't match code, this project's most expensive bug class — and three of the four would have caught a mistake made in this very session.

Run: `UnrealEditor-Cmd.exe <project> -ExecCmds="Automation RunTests SystemLink" -unattended -nopause
-nosplash -nullrhi -testexit="Automation Test Queue Empty"` (PowerShell, not the Bash tool).

#📌 Captured backlog (not started)

  • HUD "out of ammo" message for the active weapon. Nothing tells the player why firing stopped — the
  • ammo strip goes empty but there's no callout. Wants a HUD message (and probably a dry-fire cue) when the active weapon's magazine and reserve are both empty. Bind off the existing weapon delegates rather than polling — see feedback_weapon_hud_binding / Docs/UISystem.md. Noted 2026-07-27.

#Mobility Assist Module — Phases 1–3 done, dash working (2026-07-27) — branch movement-feel

Dash is in and PIE-verified (Beepers: "it works well"). Plan + the five deviations from it: Docs/MobilityAssistModule.md (status block at the top of Implementation Phases).

  • C++: SLTags gained Events.Mobility.*, States.Character.{Dashing,DashCooldown,HasDoubleJumped},
  • Abilities.Mobility., GameplayCue.Mobility.; USLMobilityAttributeSet (DashCharges/MaxDashCharges); USLMobilityModuleDataAsset; USLMobilityComponent on ASLCharacterBase; USLGameplayAbility_Dash; DashAction + Dash() on the controller.

  • Assets: AS_MobilityModule (grants the dash ability + attribute set), DA_SL_MobilityModule_MMA3
  • (/Game/SystemLink/Data/), IA_SL_Dash mapped to B and Left Shift in IMC_Default (32 → 34 mappings, read-back verified), BP_SL_MasterChief.DefaultMobilityModule set.

  • Input note: B and Left Shift were the only comfortable free binds — every other face button, shoulder,
  • trigger, stick-click and DPad_Down is taken. Shift is free because sprint was cut.

  • Still missing by design: no dash VFX/SFX (cues = Phase 6, tag fires but nothing is registered), no
  • HUD charge indicator (Phase 5), and double jump is still raw JumpMaxCount with no cost or cue (Phase 4).

Dash tuned + three bugs fixed (2026-07-27, all PIE-verified). Final feel: 3.25 m over 0.2 s, exit speed 250, ONE charge back after 1.25 s — all on DA_SL_MobilityModule_MMA3, tunable without a rebuild.

  1. Raw Velocity writes aren't network-predicted → rubber-banding for a non-host client. Now a root motion
  2. source (ApplyRootMotionConstantForce), which CMC replicates and replays.

  1. The root motion task's OnFinish never fired → ability never ended → States.Character.Dashing stuck →
  2. its own BlockAbilitiesWithTag blocked every later dash. Presented as "one dash and it never recharges" while charges were full at 1/1. Lifetime is now a WaitDelay. Diagnosing this took reading the live ASC tags over the bridge — the resource was a red herring.

  1. Dash direction must be resolved by the input owner. Inside the ability, the server recomputed from a
  2. remote pawn's stale input, fell back to "forward", and its correction overrode the client — every dash went forward on screen. Now resolved in ASLPlayerController::Dash and sent as a yaw in EventMagnitude.

All three are in Docs/Footguns.md under GAS — abilities that move the character; they generalise to any
directional predicted ability (dodge, thrust, directional melee).

#Base movement authored (2026-07-27) — branch movement-feel

Every movement value was an untouched engine default until this pass. Base feel is now authored in C++ (ASLPlayerCharacter::ConfigureMovementDefaults), not on the character Blueprint, so it diffs and has history — same rationale as the FP camera work. Full reference + tuning method: Docs/MovementFeel.md.

  • Jump arc 685 / gravity 1.85 → 1.29 m apex in 0.378 s (was 0.90 m in 0.43 s). AirControl 0.05 → 0.35
  • (0.05 is effectively none — you were committed the moment you left the ground). PIE-verified, Beepers: "I like that a lot." CDO read-back confirms the Blueprint isn't shadowing the C++ values.

  • The arc is solved, not guessed: apex = v²/2g, t = v/g, g = 980 × scale. Pick the feel, solve for the pair.
  • JumpMaxCount stays 1 — the second jump is granted by the Mobility Module on equip.
  • Sprint is deliberately out (decided 2026-07-27): Doom doesn't have it and it competes with dash.
  • MobilityAssistModule.md's DoubleJumpZVelocity = 700 is now stale — it was "+70% of base jump"
  • against the old 420, but against 685 it's a second identical jump. Re-derive before Phase 4.

#Look feel — invert baseline fixed, sensitivity rescaled (2026-07-27, merged PR #30)

  • "Invert Look Y is backwards" was a BASELINE bug, not a toggle bug. IMC_Default negated look Y and
  • bEnableLegacyInputScales=True applies InputPitchScale = -2.5 — two negations, so the un-inverted state was already inverted and the toggle faithfully flipped a wrong start. Removed the Negate from both IA_SL_Look mappings (mouse + gamepad, must be both) leaving exactly one negation. Off = normal, On = inverted. Ruled out on the way: option order (['Off','On'], correct) and device mismatch (both mappings identical).

  • Also removed a DeadZone that had appeared on the Mouse2D mapping — dead zones suit stick deflection, not
  • mouse deltas. It got there because changing a modifier's type in the dropdown replaces the entry instead of deleting it. → new Input / Enhanced Input section in Docs/Footguns.md.

  • Sensitivity rescaled + integer scale. 0.5–10 step 0.5 was 20 notches with ~14 unusable. SetNumericRange
  • gained a trailing bLabelAsSteps: labels become the notch number 1..N while NumericValues keeps real magnitudes, so the setter/clamp/profile stay in multiplier units and no save migration is needed (SetSelectedByFloat re-selects the nearest notch). Look = 0.25–2.5 shown as 1–10 (4 = 1.0, 6 = 1.5, matching Halo's common 6); ADS = 0.1–1.0 shown as 1–10 (6 = 0.6 default). FOV stays in real degrees.

  • Known gaps in look feel (not addressed): one multiplier serves both mouse and gamepad (they need
  • separate values — do this before split-screen, where mixed devices are normal), and gamepad look is likely framerate-dependent (AddPitchInput gets a per-frame stick position with no DeltaTime scaling), which quietly invalidates any sensitivity tuning done at a different FPS.

NEXT: Video/Audio tabs, real tab navigation, and key rebinds are all still deferred (§7 of
SettingsMenuBuildout.md). Before more UI, the two structural follow-ups are the profile index + picker
(Docs/PlayerProfiles.md §8) and split-screen (Docs/MenusAndOnline.md §9).

#Session 2026-07-25 — WBP_SL_Page page base started; editor hard-crash diagnosed

  • WBP_SL_Page = the new PAGE BASE for menu screens (/Game/SystemLink/UI/Menus/). Created by duplicating
  • WBP_SL_Pause → renaming WBP_SL_Pause1WBP_SL_Page. Screens should build on this going forward instead of each re-deriving the pause layout. Last good save 2026-07-25 3:40 PM.

  • Editor hard-crashed while iterating on it (3:42:58 PM) — died with no LogExit/fatal in the UE log.
  • Cause was not the GPU: Windows Error Reporting shows UnrealEditor-Slate.dll, 0xc0000409 P9=2 (FAST_FAIL_STACK_COOKIE_CHECK_FAILURE), and the System log is clean (no nvlddmkm/WHEA/TDR) — real Slate memory corruption, distinct from the Raptor Lake "device hung" family. Preceded by two ensures: Attempting to enqueue CD_SL_Xbox for compile while compiling: WBP_SL_Page_C, then Slate Array has changed during ranged-for iteration!. Compile WBP_SL_Page from the Content Browser with its widget editor closed; restart the editor after duplicating a CommonUI screen.Docs/Footguns.md.

  • Context: graphics driver switched Studio → Game Ready this session (now 610.74, 7-2-2026); the driver
  • initialized cleanly (RTX 4080 SUPER, D3D12 SM6) and is not implicated in the above crash.

#Session 2026-07-21 — Settings rows reviewed, KBM cursor FIXED, rotator row started

✅ RESOLVED (verified via bridge 2026-07-26) — this whole "rootless / one manual step" block was STALE.
WBP_SL_SettingsRow_Rotator is fully built: Border → RowBox [HorizontalBox]
Spacer · LabelText · LeftArrowButton · SizeBox_0(MyText) · RightArrowButton · Spacer, so the required
MyText bind is satisfied. And WBP_SL_Settings went further than the old plan — every row is a
WBP_SL_SettingsRow_Rotator_C now, not just the two Invert rows. See the 2026-07-26 section above.

Status: UMG for the Gameplay tab is authored (trees/graphs built since 2026-07-20). Slider Row_SensX adjusts via arrow keys AND controller. C++ this session is REBUILT + live (cursor fix + USLRotatorRow).

The "only a button is focusable in CommonUI" problem + how it's handled now. A raw USlider/CheckBox won't take gamepad focus (only a UCommonButtonBase does). Current fix (C++): USLSettingsRowWidget is itself SetIsFocusable(true) and intercepts input in NativeOnKeyDown — Left/Right → HandleAdjust(±1), Accept → HandleAccept(), Up/Down fall through to Slate nav; focus highlight via OnRowFocusChanged(bool) BP hook (new NativeOnAddedToFocusPath/RemovedFromFocusPath). Slider steps by StepSize (5% fallback), toggle sets off/on + Accept flips. InputCore added to Build.cs for EKeys. Review verdict: sound, no correctness bugs.

UMG requirement for the row approach: the inner USlider / CheckBox must have Is Focusable = OFF so
the ROW is the only nav stop — else focus lands on the inner control and the row's OnKeyDown never fires.

DECISION — use a UCommonRotator for discrete settings (it IS a UCommonButtonBase = a button → focusable for free). Sidesteps the focusable-row hack for discrete choices. Port the 5.6 pattern from C:\3D-DEV\HaloProject\SystemLink\Source\SystemLink\UI\SystemLinkSettingsRotator.*: extends UCommonRotator, PopulateTextLabels/ShiftTextLeft/Right, NativeOnClicked(){} (suppress click-cycle), Left/RightArrowButton BindWidgets for mouse, focus/hover → description broadcast, SetIsFocusable(true).

  • Plan: HYBRID + rotator-IS-the-row. Rotators for discrete (Invert Look Y/X now → Off/On; later Hold/Toggle,
  • quality presets); keep the slider row for continuous (Sens X/Y, ADS, FOV — too many steps to cycle).

  • USLRotatorRow : UCommonRotator — WRITTEN + REBUILT + registered ✅. SLRotatorRow.h/.cpp (confirmed
  • loadable via bridge: /Script/SystemLinkCore.SLRotatorRow). Verified against engine source (UE_5.7/.../CommonUI/.../CommonRotator.h/.cpp): UCommonRotator is a UCommonButtonBase (focusable natively) and handles gamepad Left/Right itself via NativeOnNavigation/HandleNavigation → no directional wiring needed. It requires a BindWidget UCommonTextBlock named MyText (the value display). SetSelectedItem (seeding) does NOT fire the rotate event; ShiftText*/nav/click DO fire OnRotatedEvent(Index, bFromNavigation) → bound in NativeOnInitialized, so seeding won't feedback-loop. Our class adds: SetOptions, SetSelectedIndex(idx,bBroadcast), SetBoolValue(b,bBroadcast)/GetBoolValue, OnValueChanged(int32) + OnBoolChanged(bool), optional LabelText/DisplayName + mouse Left/RightArrow Button, NativeOnClicked→ShiftTextRight (A advances), OnRowFocusChanged highlight hook.

  • The two bullets below are HISTORY — the row is built and every settings row is a rotator (see 2026-07-26).
  • WBP_SL_SettingsRow_Rotator — CREATED via bridge (parent USLRotatorRow, saved) but ROOTLESS. Made with
  • WidgetBlueprintFactory(parent_class=USLRotatorRow) so no reparent was needed (dodged the reparent-crash footgun). Confirmed in 5.7: WidgetTree.RootWidget has no Python setter (set_editor_property('root_widget') → "Failed to find property"), and WidgetTree/ParentClass are read-protected on the WBP (use unreal.find_object(bp,'WidgetTree')). So the root must be added by hand — see RESUME HERE above. There's a harmless orphan RowBox HorizontalBox left in the tree from the probe; ignore/replace it.

  • NEXT after the root exists (bridge): new_object(cls, outer=wt, name=...) + PanelWidget.add_child are
  • proven — build LabelText/MyText/Left+RightArrowButton under the root, set arrow bIsFocusable=false, compile+save. Then WBP_SL_Settings: swap the two Invert toggle rows → rotator (SetOptions({Off,On}), SetDisplayName; graph SetBoolValue(profile,false) seed on construct + bind OnBoolChangedSetInvertLookY/X). (This is also the deferred USLDropdownRow slot.)

PIE findings (verified via bridge + testing):

  • WBP_SL_Settings + WBP_SL_Pause CDOs confirmed InputModeOnActivate=MENU, bPauseGameWhileActive=True.
  • ✅ Controller B backs out of Settings→Pause and Pause→resume (Back handling solid).
  • ✅ CommonInput supports mouse (bSupportsMouseAndKeyboard=True, DefaultInputType=MouseAndKeyboard).
  • ℹ️ Esc quits PIE — the editor reserves Esc to stop the session; NOT a Back bug (Esc = Back is fine in a
  • build). For KBM testing add a non-Esc back key (e.g. Backspace) to IA_SL_UI_Back.

  • ℹ️ Settings widget Navigation rules all = Escape — benign: rows consume Left/Right in OnKeyDown so the nav
  • rule never fires; vertical nav resolves through the parent box.

  • KBM mouse cursor vanishes in the menu — FIXED + REBUILT + WORKING (Beepers confirmed cursor works). Root cause via live
  • bridge inspection of the PIE PC: bShowMouseCursor=False while the menu was up. CommonUI hides the OS cursor whenever the active input device is Gamepad (pads navigate by focus); entering the menu from the pad left it hidden, and it never flipped back on when the mouse was used. Forcing bShowMouseCursor=True on the live PC restored the cursor + clicks (proved the rest of the pipeline is fine). Fix in USLCommonActivatableWidget (Menu-mode only): HandleInputMethodChanged now shows the cursor for MouseAndKeyboard / hides for Gamepad (called with the current type on activate → correct initial state); NativeOnMouseMove reveals it on physical mouse use as a fallback for when CommonUI hasn't flipped the device yet. SetMenuCursorShown gates on InputModeOnActivate==Menu. Close path already hides it (ASLPlayerController::RestoreGameInputMode sets bShowMouseCursor=false on last pop). Rebuilt + confirmed working in PIE. Remaining nicety to spot-check when convenient: tap pad after using mouse → cursor hides again; close → cursor gone in gameplay.

#Done this session (2026-07-20)

  • Pro Tips converted to a Data Asset. USLProTipDataAsset (+ USLProTipSettings DeveloperSettings) replace
  • the raw-JSON loader; DA_SL_ProTips (/Game/SystemLink/Data/) populated with 40 tips via the bridge; per-tip DisplayLabel; DefaultGame.ini points the setting at it. PIE-verified data resolution. Old SystemLinkProTips.json now orphaned (safe to delete after a PIE gameplay check). ⚠ C++ rebuilt already.

  • New forward-looking docs: Docs/AutoresearchOpportunities.md, Docs/Foundry/Game_Agent_Bridge.md.
  • Command bar keyboard handling — DONE. Bar is gamepad-only: WBP_SL_CommandBar binds
  • CommonInputSubsystem::OnInputMethodChanged (+ GetCurrentInputType on Construct) and hides its prompts on KBM. Briefly authored then deleted CD_SL_KBM (glyphs never render when hidden). Swapped CD_SL_Xbox glyphs to the Retro set (PIE-verified, Beepers likes it). Full recipe: Docs/CommandBarBackButton.md.

  • Settings menu — STARTED (Gameplay tab MVP). Backend done + compiled: USLPlayerProfileSaveGame
  • (USaveGame, BlueprintReadOnly fields) + USLPlayerProfileSubsystem (GameInstance; load/create, clamped Set*, OnPlayerProfileChanged, SaveProfile); SLPlayerController::Look applies sensitivity + invert + ADS-mul; SLPlayerCharacter::SetBaseFieldOfView for FOV. Invert-Y/sensitivity NOT yet PIE-tested (no UI yet). Three EMPTY WBPs created (parents set, no tree — bridge can't add a root): WBP_SL_SettingsRow_Slider/_Toggle (USLSliderRow/USLToggleRow), WBP_SL_Settings (USLScreenWidget). NEXT = author the trees/graphs + wire the pause Settings button per Docs/SettingsMenuBuildout.md (full recipe: row widgets → screen seed/bind/SaveProfile → PushWidgetToLayer(Menu, WBP_SL_Settings)). Rotator/ dropdown, Video/Audio/rebinds, real tabs all deferred (§7 there).

The C++ foundation base-class kit is already built (USLPrimaryGameLayout push/pop/clear/getActive, enhanced USLCommonActivatableWidget, USLScreenWidget/USLModalWidget/USLButtonBase/USLTabListWidget/ USLListEntryWidget/settings rows). This session — Phase 0b C++ (UNCOMMITTED, needs rebuild):

  • USLCommonActivatableWidget — added bPauseGameWhileActive; pause/unpause tied to activate/deactivate,
  • standalone-only (footgun 6.12), so it survives any close path (Start toggle, B/Esc, pop).

  • ASLPlayerControllerOpenPauseMenu/ClosePauseMenu/TogglePauseMenu + PauseAction (bound Started) +
  • PauseMenuClass; IsMenuOpen() dedups. Pushes to the Menu layer via the HUD; unpause is the widget's job.

  • Config/DefaultGame.ini[CommonInputSettings] with bEnableEnhancedInputSupport=True (asset refs
  • commented out until the data assets exist, to avoid dangling-path startup errors).

Phase 0b progress:

  • ✅ C++ pause wiring compiled (rebuild done). ✅ bEnableEnhancedInputSupport in ini.
  • Input actions authored (bridge, 2026-07-06): 7 IA_SL_UI_* in /Game/SystemLink/UI/Input/Actions/
  • (Confirm/Back/Navigate/NextTab/PrevTab/Pause/Scoreboard); IMC_SL_UI (11 mappings) in /Game/SystemLink/UI/Input/. Navigate intentionally unmapped (Slate default nav drives DPad/stick/arrows).

  • IA_SL_UI_Pause mapped in IMC_Default (Gamepad_Special_Right + P) — FIXED 2026-07-09. The
  • earlier "added" claim was WRONG: the mapping was never in the asset (writes to the deprecated mappings array read back as 0; the real store is default_key_mappings.mappings). Verified by reading it back — 32 mappings now, Pause present. This was why the pause menu wouldn't open. → Footguns (Bridge/Editor).

Global input-leak guard — DONE (C++, 2026-07-06, needs rebuild): menus can't leak gameplay actions, no per-mapping code. Two layers:

  • USLScreenWidget already defaults InputModeOnActivate = Menu → CommonUI suppresses gameplay input.
  • Belt-and-suspenders: ASLPlayerController::Push/PopGameplayInputSuppression() (ref-counted) removes/re-adds the
  • gameplay InputMappingContexts; USLCommonActivatableWidget calls it on Menu-mode activate/deactivate.

  • Consequence: while a menu is up, Start no longer closes it (that action is in the suppressed gameplay
  • context) — close is CommonUI Back (B/Esc). Start opens, Back closes.

CommonUI data asset — DONE (bridge): DA_SL_CommonInputData (BP : UCommonUIInputData) with EnhancedInputClickAction=IA_SL_UI_Confirm, EnhancedInputBackAction=IA_SL_UI_Back (fixes the two-input-systems pitfall). DefaultGame.ini InputData now points at it. DefaultClickAction/DefaultBackAction (DataTable rows) left empty — that's the action-bar display layer, bundled with glyphs below.

Phase 0b continued — DONE this session (bridge, 2026-07-06, all saved):

  • Rebuild done (suppression methods + activatable wiring live).
  • DT_SL_InputActions created in /Game/SystemLink/UI/Input/ (row struct CommonInputActionDataBase):
  • rows Confirm→DisplayName "Accept", Back→"Back". Text placeholder (decision: text now, real CD_SL_ glyph sets deferred — Confirm/Back already function* via the EI actions; this is only the prompt-display layer).

  • DA_SL_CommonInputDataDefaultClickAction={DT,Confirm}, DefaultBackAction={DT,Back}; compiled+saved.
  • BP_SL_PlayerControllerPauseAction=IA_SL_UI_Pause set; compiled+saved. **PauseMenuClass still
  • None** — blocked on WBP_SL_PauseMenu's tree existing.

  • WBP shells created in /Game/SystemLink/UI/Menus/: WBP_SL_TestScreen + WBP_SL_PauseMenu (both
  • parented to USLScreenWidget, verified) + WBP_SL_BoundActionButton (parented to CommonBoundActionButton — the action-bar entry widget; needs a TextBlock named exactly Text_ActionName added, then it's the Action Button class both action bars reference). Widget trees are empty — manual authoring next (confirmed the bridge can't build widget trees: no WidgetTree type / widget_tree prop exposed to Python).

Button assets — DONE (bridge, 2026-07-06→07): discovered the plan's "add USLButtonBase" step was a hole — the raw C++ class has Style=None + no label, so it renders blank. Resolved by migrating the real style library from the 5.6 project (C:\3D-DEV\HaloProject\SystemLink, UE 5.6 → 5.7):

  • Full style kit migrated to /Game/SystemLink/UI/Styles/Buttons/ (8: Clear/Small/Medium/Large/XL/
  • XXL/Row/Rotator) + Text/ (13), plus dep tree: Materials/ (11), Textures/ (4), Microgramma font in UI/Fonts/, UI sounds in UI/Sounds/. Copied 34 files → editor auto-completed the closure (Rotator arrow materials my grep missed) → relocated into /Game/SystemLink/UI/ via ref-safe rename. Verified: brushes resolve halo-gradiant-2/MI_UI_BodyBackground, text resolves Microgramma; no redirectors/leftovers. The 3 "null base" styles (Clear/Row/Rotator) are intentional (transparent / ROUNDED_BOX tint fills).

  • WBP_SL_Button (/Game/SystemLink/UI/Menus/) — USLButtonBase subclass, Style=Style_ButtonMedium.
  • Placeholder CBS_SL_MenuButton deleted. Manual step left: author its tree (CommonTextBlock named exactly ButtonText, centered — C++ binds that name; per-instance caption = ButtonDisplayText). Recipe in MenuSystemPlan.md §5.0a. Delegated: Docs/CodexHandoff_WidgetTrees.md is a computer-use handoff spec for building all four widget trees (WBP_SL_Button + BoundActionButton + PauseMenu + TestScreen).

⚠ Migrated assets are UE 5.6→5.7 upconverts — not yet committed; git-add the new Content/SystemLink/UI/
paths deliberately (stay away from git add -A per the screenshots note).

#Phase 0b — pause menu (controller menu pipeline) — status 2026-07-10

Goal: open a menu with Start, navigate fully on a gamepad, A confirms / B closes. Proof that the CommonUI foundation works so real menus (Main/Settings/Lobby) can build on it.

Not about pausing. "Pause menu" = the Start-opened overlay; it only freezes the game standalone/PIE
(bPauseGameWhileActive), networked it's a pure UI overlay (decision #4). Title reads "PAUSE MENU" anyway.

DONE (this session, 2026-07-09/10):

  • Pause key firesIA_SL_UI_Pause mapped in IMC_Default (Start + P). The "added" claim was false;
  • fixed + verified (footgun: bridge IMC writes can miss the 5.7 default_key_mappings.mappings).

  • WBP_SL_ButtonButtonText label; angled Halo style (Style_ButtonMedium); a Border DMI wired
  • (Get Dynamic Material on the Border, target = Border) for the focus visual. Docs/ButtonWidget.md.

  • WBP_SL_PauseMenu — title "PAUSE MENU" + 3 styled buttons (Resume · Settings · Exit);
  • bPauseGameWhileActive=true. Initial focus via a Get Desired Focus Target graph override (the AutoFocusWidget picker reverts/crashes — footgun 6.29; the field is now C++-only).

  • PauseMenuClass=WBP_SL_PauseMenu set on BP_SL_PlayerController. Menu opens + displays in PIE.
  • Selection reverted to Hovered for focus highlight (select-on-focus is sticky — footgun 6.31).

⚠ Pending C++ rebuild (3 changes, uncommitted):

  1. ASLPlayerController::RestoreGameInputMode — restores game input/viewport focus when the last menu closes
  2. (footgun 6.30; else the game is dead after close).

  1. USLScreenWidget::ValidateCompiledWidgetTree — compile error if a screen has no Get Desired Focus Target.
  1. USLCommonActivatableWidget::AutoFocusWidget — un-exposed from BP (bare UPROPERTY()).

#Button glyphs ("A" on the tile) — 2026-07-15

DONE — the "A" glyph now renders in PIE on mouse hover. WBP_SL_ButtonTile showed the glyph in the UMG designer but never at runtime. Two independent blockers; full writeups in UISystem.md §6.32 / §6.33.

  1. IMC_SL_UI was never applied (asset registry: zero referencers — orphaned). CommonUI resolves glyphs
  2. via QueryKeysMappedToAction, which only sees contexts applied right now → no key → NoBrushUpdateActionWidget collapses itself, silently. Fix (C++, built): new ASLPlayerController::UIInputMappingContext, added/removed inside Push/PopGameplayInputSuppression so it's live exactly while a menu owns input. BP_SL_PlayerController.UIInputMappingContext = IMC_SL_UI (set

    • verified via bridge read-back).
It must NOT be applied during play. First attempt applied it persistently at priority 1 and
silently broke jump/interact/cycle-weapon/sidearm/grenade — UI actions default to bConsumeInput=True
and share keys (Confirm=A/Space vs Jump=A/Space, NextTab=E/RB vs Interact/CycleWeapon, PrevTab=Q/LB vs
SidearmMode/Grenade). Caught by reading, not testing. → §4.2 known-limit note.
  1. The glyph fallback is hover-gated → dead on gamepad. UCommonButtonBase::UpdateInputActionWidget only
  2. reaches the default-click-action branch via IsHovered() — and SCommonButton sets bHovered solely from OnMouseEnter/OnMouseLeave, so focus never counts. FIXED in C++ — PIE-VERIFIED ✅ 2026-07-15: USLButtonBase now overrides HandleFocusReceived/HandleFocusLost (the internal Slate button's focus delegates — the only reliable signal) + UpdateInputActionWidget, re-pushing the same ICommonInputModule::GetSettings() click action the base would use, keyed off focus. Deliberately NOT via TriggeringEnhancedInputAction — that registers competing A bindings resolved by registration order + reachability, not focus → A would always click the first tile. → §6.33.

Glyphs are gamepad-only, and the command bar is HIDDEN on KBM (final decision 2026-07-20). Only CD_SL_Xbox (input_type=GAMEPAD) is registered. Rather than show keyboard glyphs, the whole command bar is hidden when the active device is mouse/keyboard: WBP_SL_CommandBar binds CommonInputSubsystem::OnInputMethodChanged (+ reads GetCurrentInputType on Construct for the initial state) and collapses its prompts unless the device is Gamepad. (Interim step, now REVERTED: we briefly authored a CD_SL_KBM White key-cap set + registered it in DefaultGame.ini; deleted once the bar was hidden on KBM — the glyphs would never render. DefaultGame.ini back to a single +ControllerData.) Recipe: Docs/CommandBarBackButton.md §"Gamepad-only visibility". PIE gate: open menu on mouse → bar hidden; tap pad → bar + Ⓐ/Ⓑ appear.

  1. Sticky focus highlight — all tiles stayed lit. FIXED + PIE-VERIFIED ✅ 2026-07-15 (BP). NOT sticky
  2. selection (§6.31): selectable/toggleable/bShouldSelectUponReceivingFocus are all False and Style_ButtonTile's brushes are transparent — the visual is the Border DMI via SetBorderFocus. The graph looked symmetric but paired On Focused (UCommonButtonBase) with On Focus Lost (UUserWidget — has an In Focus Event pin), which never fires because focus is forwarded to the inner SCommonButton. Fix: swapped for On Unfocused (CommonButton's real partner). → §6.34.

Phase 0b controller pipeline is PROVEN ✅ (2026-07-15). Start opens → focus lands on Resume → D-pad/Tab
nav moves focus with exactly one tile lit → the "A" glyph follows focus on a gamepad → gameplay input
(grenade et al.) still works during play. The CommonUI foundation is validated; real menus can build on it.
The designer preview is not evidence. UCommonActionWidget::GetIcon() has an editor-only branch that
renders DesignTimeKey and bypasses the whole runtime path. It showed a perfect "A" the entire time both
blockers were live. Validate prompt glyphs in PIE only. → §6.32.

✅ REBUILT + PIE-VERIFIED (2026-07-15): the suppression-scoped wiring is live and tested — the "A" glyph renders in the pause menu and grenade/gameplay input still works during play (the regression check the first gate missed). Both halves of §4.2 confirmed on the same build.

NEXT — gamepad focus glyph (blocker 2): drive the action widget from focus on USLButtonBase (set the enhanced input action on focus received, clear on focus lost). Do NOT just set TriggeringEnhancedInputAction on the tile: that registers competing A bindings resolved by registration order + reachability, not focus → A always clicks the first tile. → §6.33.

Focus highlight — RESOLVED ✅ (2026-07-15). The answer to "which event drives the Border DMI on gamepad": On Hovered + On Focused → set, On Unhovered + On Unfocused → clear. All four are UCommonButtonBase events bound to the internal Slate button, so they fire for mouse, gamepad and Tab alike. CanSafelyRouteCall gates them but was never the problem — the bug was pairing On Focused with UMG's On Focus Lost (§6.34). Selection + UCommonButtonGroupBase is not needed and would reintroduce §6.31's stickiness.

#Bound action bar ("command bar") — ⚠ SUPERSEDED 2026-07-20 (kept for MenuKit reference)

This whole dynamic-bar section is superseded. Decision 2026-07-20: hardcode Back + Accept for the
game (Docs/CommandBarBackButton.md); the dynamic CommonBoundActionBar below is now a MenuKit-product
upgrade, not active work. WBP_SL_BoundActionButton is already fully built (see the NEXT MILESTONE section).
The text below is retained only for when the MenuKit dynamic-bar task is picked up.

(historical) Bound action bar — was IN PROGRESS 2026-07-15

Goal: the strip showing `A SelectB Back`. Back is the priority. Note B/Esc already closes the menu

(USLScreenWidget ctor sets bIsBackHandler) — what's missing is the bar displaying it.

DONE (C++, built ✅, PIE-UNTESTED): USLScreenWidget ctor now also sets bIsBackActionDisplayedInActionBar = true. It defaults to false in the engine, and it is a separate opt-in from bIsBackHandler — that's why back worked while staying invisible. Every SL screen is closeable, so every SL screen advertises it. → §6.35(a).

REMAINING (editor — state not yet inspected; editor was closed):

  1. WBP_SL_BoundActionButton tree — needs BOTH a UCommonActionWidget named exactly
  2. InputActionWidget and a UCommonTextBlock named exactly Text_ActionName.

⚠ The old note in this file said "needs a TextBlock named Text_ActionName" — **incomplete and
actively misleading.** ALL of UCommonBoundActionButton::UpdateInputActionWidget() is wrapped in
if (InputActionWidget), and the Text_ActionName->SetText() call is INSIDE it → no glyph widget means a
blank button, text and all. → §6.35(b).
  1. A UCommonBoundActionBar with ActionButtonClass = WBP_SL_BoundActionButton (missing class = compile
  2. error, so this link is self-enforcing). It does NOT need to be in the same widget tree — it reads the local player's action router, so one bar in the shared layout serves every screen. Decide: shared layout vs. per-screen. → §5.3 (that section's "same widget tree" claim was WRONG and is now corrected).

  1. Display nameIA_SL_UI_Back.ActionDescription (data-driven, preferred) or
  2. OverrideBackActionDisplayName per screen. Empty = correct glyph, blank label. → §6.35(c).

  1. PIE gate: open pause → bar shows B Back (gamepad) → B closes. Expect no glyph on KBM — intentional,
  2. there's no CD_SL_KBM (the text should still show).

NEXT — hook up the button actions:

  • Resume — ✅ DONE (wired by Beepers): OnClicked → Get Owning PC → Cast SLPlayerController
  • Close Pause Menu.

  • Settings → stub for now (push WBP_SL_Settings once it exists) — or leave unwired.
  • Exit → leave match / quit. Route through a USLModalWidget confirm ("Leave match?") — see §confirm flow.
The old "then rebuild (the 3 C++ changes)" note here is obsolete — those landed, were PIE-verified, and
are committed in 9ea2dd5a along with the glyph/focus fixes.

#Monetization strategy + MenuKit extraction plan — 2026-07-16 (planning session, no code changes)

Strategy decided (memory project_fab_monetization): goal = passive income → work for self. Stack:

  1. POC: "MenuKit" — extract the CommonUI menu framework into a standalone Fab plugin. Byproduct of
  2. menu work already underway; POC metric = live listing + first sale + pipeline knowledge, NOT revenue.

  1. Main bet: "Console-Ready UI with CommonUI" course (after POC ships). 3. Multiplier: devlog from
  2. the footgun docs. 4. Later flagship: multiplayer GAS FPS template. Market research: Fab averages ~$1,200/publisher/yr; menus/icons/FP-kits all crowded — specialized long-tail + deep docs is what sells. Gemma (local Ollama gemma4:12b, installed + verified today) = bulk text drafts only, never UE code.

Plan drafted: Docs/MenuKitExtractionPlan.md. Coupling audit done — the UI kit has exactly 2 game couplings (SLPrimaryGameLayout.cpp:16 HUD cast → layout subsystem; SLCommonActivatableWidget.cpp:109 controller cast → UMenuKitInputComponent). Phases A–D + gates; open decisions §9 (name/prefix, free-lite tier, settings persistence, license header, engine floor).

AI-native wedge (plan §11): menus defined in a declarative text manifest (UMenuManifest) + a generator that builds the WBP widget trees — so any AI assistant (or a human) can author menus as text. No Fab menu competitor has this; general AI copilots (Ludus, Ultimate Engine CoPilot) are a funded crowded race we deliberately do NOT enter — we make products their users' AI can operate.

Widget-tree generator spike — PASS ✅ (bridge, live editor): script CAN build widget trees: new_object(cls, outer=wt, name=…) constructs anything (CommonUI classes + SL bases included), PanelWidget.add_child IS exposed (returns real slots), compile+save+linkage verified. Sole wall: WidgetTree.RootWidget is protected (no script read/write) and fresh 5.7 WBPs are rootless → dodge: duplicate a seed template that has a root, build under it. Full matrix: plan §11.4; memory feedback_bridge_widget_tree_property_edit updated (the old "bridge can't build trees" belief is superseded; Docs/CodexHandoff_WidgetTrees.md is retired — trees are script-buildable now).

🧹 Scratch assets pending cleanup: /Game/Dev/Spike/WBP_MK_SpikeGen + WBP_MK_SeedTest (the
latter shows "GRAFTED BY SCRIPT" in its HorizontalBox as visual proof). Delete after inspection.

Sequencing unchanged: finish the bound action bar (PIE gate above) FIRST, then MenuKit Phase A.


#Merged history (grenade-refinement, PR #28) — kept for reference below

The items below are MERGED. Left in place as design/RCA reference; not active work.

All C++ below is UNCOMMITTED and needs a rebuild. When committing, stage specific paths (Plugins/…, .gitignore, Docs/*.md) — NOT git add -A — because Docs/Screenshots/Site was just un-ignored (~286 MB of local screenshots would otherwise sweep in).

  1. Per-drop launch varianceASLPickupBase::LaunchPickup now randomizes yaw + pitch (LaunchPitchSpread,
  2. ±15°) + speed (LaunchSpeedVariance, ±25%) per drop, so death-drop piles scatter instead of arcing in lockstep. On the base class → applies to ALL pickups (weapons, sidearm, grenades, ammo, health).

  1. Thrown grenade chains into pickups — extracted ASLGrenadePickup::ChainDetonateNearby(...) static;
  2. ASLGrenadeProjectile::Explode now detonates grenade pickups within its DamageOuterRadius. Pickup→pickup chain unchanged (ChainDetonationRadius). Cascades + kill-credit-to-thrower both work. See GrenadeSystem.md.

  1. Pistol ADS (auto-on-draw zoom) — SWELL FIXED via first-person rendering (2026-07-05).
  2. USLWeaponDataAsset::ADSFieldOfView (0 = off). ASLPlayerCharacter::TickADS lerps the world camera FOV between base and the sidearm's ADSFieldOfView while the pistol is drawn, timed to SidearmDrawDuration (in) / SidearmHolsterDuration (out). Local-only. Reads the sidearm's OWN data (not GetActiveFireWeaponData, which flips on holster). The viewmodel-swell problem is SOLVED. Narrowing the world FOV used to magnify the FP viewmodel too (the pistol ballooned/detached at 65 — it looked like a socket bug, wasn't). Fix = UE 5.5+ native first-person rendering: the FP meshes render at a separate fixed FOV so they don't zoom with the world. Now in C++:

    • ASLCharacterBase ctor: FPMesh/FPWeaponMesh/FPSidearmMeshFirstPersonPrimitiveType::FirstPerson.
    • The sidearm mesh missing this flag was the actual bug — pistol = sidearm = the only unflagged FP mesh.

    • ASLPlayerCharacter ctor: Camera->bEnableFirstPersonFieldOfView = true, FirstPersonFieldOfView = 95
    • (matches base FOV so arms are unchanged at rest).

    • Config/DefaultEngine.ini: r.FirstPerson.Enabled=True (read-only cvar → needs an editor restart).
    • PIE-verified 2026-07-05 (screenshot): world zooms, pistol stays seated in hand, Substrate + VSM clean. C++ compiled ✅. The abandoned counter-scale journey is now just a footgun record in Docs/ADS.md; the dead ADSScaleCompensation member stays stripped. BP_SL_MasterChief CDO overrides CLEARED — the spike's redundant per-instance deltas were dropped by a force-save once the C++ archetype matched (bridge), so C++ is the single source of truth (.uasset re-saved). Option B (kept, orthogonal to the swell fix): the FP sidearm inherits the arms' scale (driven by the equipped MAIN weapon's FirstPersonMeshScale), so ApplyFPWeaponObstructionScale divides that baseline back out → sidearm world scale = DA_SL_Pistol.FirstPersonMeshScale × obstruction, independent of the primary. Tune pistol on-screen size via DA_SL_Pistol.FirstPersonMeshScale (live). Editor step: set DA_SL_Pistol.ADSFieldOfView (~65; base FOV is 95).

  1. Pistol muzzle flash — socket fix + FP-render helper (2026-07-05, C++ compiled). Two bugs, both from the
  2. first-person-rendering switch surfacing pre-existing issues:

    • Muzzle socket misplaced. SK_Pistol's Muzzle socket (parented to the Barrel bone) was at
    • (12, 0, 0) — authored as if the barrel pointed +X, but this mesh is +Y-forward (geometry runs Y:[-5..19], X only ±2.6), so the flash spawned ~12 cm out the side near mid-body. Corrected via bridge to loc (0, 18.5, 8), yaw 90 (X centred, Y at the barrel tip, Z ~bore height; yaw aligns socket-forward +X down the barrel +Y). ⚠ Z and yaw are ESTIMATES — verify visually in the SK editor + test-fire the flash direction. SK_Pistol.uasset saved.

    • FX didn't inherit first-person rendering. A muzzle flash spawned via Niagara Spawn System Attached
    • does NOT inherit the FP mesh's FirstPersonPrimitiveType, so under ADS (world FOV 65 vs FP FOV 95) it desyncs from the gun — and the pistol always fires while aiming, so it's always visible. Fix = new

USL_BlueprintLibrary::SpawnSystemAttachedMatchingView (Category `SystemLinkFX`): drop-in for

Spawn System Attached that walks up the attach chain and flags the FX with the parent's FP mode (FP → FP, TP/world → None). No per-weapon data bool — correct by construction; reusable for shell eject / tracers. ⚠ BP STEP: in BP_WeaponLibrary, swap Spawn System AttachedSpawn System Attached Matching View (delete any manual Set First Person Primitive Type node).

  1. .gitignore — un-ignored Docs/Screenshots/Site/ so the curated gallery is version-controlled going
  2. forward (the root Docs/Screenshots/ stays ignored).

Test after rebuild: die with several items → scatter; throw a grenade near a pickup / into a pile → chain; draw the pistol → smooth FOV zoom (whole viewmodel magnifies uniformly, pistol stays in hand), holster → zoom back; fire the pistol while aiming → muzzle flash sits on the barrel tip and tracks it at the zoomed FOV.

RESUME HERE (2026-07-05): ADS viewmodel swell SOLVED + productionized + compiled ✅ via UE native
first-person rendering (item 3). CDO spike overrides cleared; C++ is the single source of truth. Then found +
fixed the pistol muzzle flash (item 4): socket was misplaced on the wrong axis (fixed via bridge, estimate)
and the flash didn't inherit FP rendering (new SpawnSystemAttachedMatchingView helper — compiled).
Uncommitted, all compiled: SLCharacterBase.cpp (mesh FP flags), SLPlayerCharacter.cpp/.h (camera FP FOV +
earlier ADS/Option-B), SLWeaponDataAsset.h, SL_BlueprintLibrary.cpp/.h (FX helper), Config/DefaultEngine.ini
(r.FirstPerson.Enabled=True), BP_SL_MasterChief.uasset (cleaned), SK_Pistol.uasset (muzzle socket).
NEXT (all editor, no more C++ pending):
1. BP node swapBP_WeaponLibrary: Spawn System AttachedSpawn System Attached Matching View
(delete any manual Set First Person Primitive Type node).
2. Verify the muzzle socket — open SK_Pistol, nudge the Muzzle socket Z/yaw so the flash sits on the
barrel tip and points forward (current (0,18.5,8)/yaw 90 are estimates).
3. PIE-test: fire pistol while aiming → flash on the barrel, tracks under ADS; ADS still correct off the C++
path; then the other refinements (scatter, grenade→pickup chains).
4. Optional: Footgun line (attached FX to FP meshes → use SpawnSystemAttachedMatchingView) + note the
muzzle-socket fix in Docs/ADS.md.
5. Commitstage SPECIFIC paths, NOT git add -A (the un-ignored Docs/Screenshots/Site/ would sweep
in ~286 MB). Paths: the C++ files above, DefaultEngine.ini, BP_SL_MasterChief.uasset, SK_Pistol.uasset,
Docs/ADS.md, Docs/CurrentFocus.md, Docs/Footguns.md.

#Where We Left Off

2026-07-02 — grenade Increment 1 done + committed (9175298); now on grenade extras + a TP LHIK Control Rig. HUD grenade indicator DONE, BUG-023 verified (2-player PIE), committed. Since then, two grenade tweaks + one rig task are IN PROGRESS (⚠ C++ UNCOMMITTED, needs rebuild):

  • Per-grenade death dropASLCharacterBase::DropGrenadesOnDeath() now spawns ONE pickup per grenade
  • (GrenadeAmount = 1 each) instead of a single pickup carrying the count. Existing LaunchPickup yaw-spread + pickup-vs-pickup ignore already handle scatter / no mid-air freeze. Watch: collect-sound spam on a tight pile.

  • Shootable grenade pickups (+ chain reactions) — NEW ISLDamageable interface
  • (Public/Interfaces/SLDamageable.h); the two authoritative fire sites (USLWeaponsComponent::Server_ProcessShots

    • USLGameplayAbility_Fire::ExecuteFire_ListenServerHost) now route non-pawn ECC_WeaponTrace hits to
    • ReceiveWeaponDamage. ASLGrenadePickup implements it: accumulates weapon damage → detonates past DetonateDamageThreshold (default 1) → full grenade blast credited to the shooter's ASC → chain-detonates other pickups within ChainDetonationRadius (default 350) on a short random delay (recursion-guarded via bDetonated). Blast logic extracted to a shared static ASLGrenadeProjectile::ApplyGrenadeExplosion(...) so projectile + pickup detonate identically. Pickup gains a small HitCollision sphere that blocks ONLY ECC_WeaponTrace (so shots register; doesn't affect the arc). New tunables on BP_SL_GrenadePickup_Frag: DetonateDamageThreshold, ChainDetonationRadius, HitCollision radius. Melee does NOT detonate (fire only — easy to add later via the same interface call). Build note: first build failed C2679 (incomplete UAbilitySystemComponent for the TWeakObjectPtr assign) → fixed with #include "AbilitySystemComponent.h" in SLGrenadePickup.cpp. Needs a clean rebuild + 2-player PIE test (shoot → detonate, kill-credit, chain).

Two tasks queued (task list #1/#2), editor-dependent:

  1. Rename USLGrenadeIndicator::InitializeInitializeIndicator — clears C4263/C4264 shadowing of
  2. UUserWidget::Initialize() (our feedback_uobject_function_name_clashes pattern). Verify BP callers via bridge first; update caller SLHUDWidget.cpp:103. Needs a rebuild.

  1. Auto-generate CR_MC_TP_LeftHandIK — Control Rig to REPLACE the FABRIK LHIK on the TP body. Two Bone IK
  2. on upperarm_l→lowerarm_l→hand_l + pole vector, exposed as CR vars (LeftHandTarget/PoleVector/Alpha) the TP ABP feeds from LHIK_TP_Snapshot. Asset-only (no rebuild). Via bridge.

2026-06-23 — grenade Increment 1 is ~90% done. Most of the 2026-06-19 "remaining" checklist has since been built (assets are in the tree, uncommitted). Recon this session reconciled the doc to reality.

BUG-024 (2026-06-24) — equip sound heard map-wide. Remote players' equip sound played at full volume
everywhere (most visible on respawn). RCA (corrected): the equip GameplayCueNotify_Burst plays
Rifle_Raise_Cue (which HAS attenuation) with DoNotAttach, so it spawns at CueParameters.Location
which ExecuteEquipCue never set → played at world origin (0,0,0), within earshot of the whole compact
TestMap. (NOT a Play Sound 2D node — there is no sound node; first guess was wrong.) **C++ fix (sufficient,
needs rebuild): ExecuteEquipCue now sets CueParams.Location/Normal. Robustness DONE:** equip cues'
burst-sound AttachPolicy → AttachToTarget (both AR + Shotgun, via bridge). Full RCA: BugTracker.md BUG-024. Also new reusable
SA_Default3D attenuation (/Game/SystemLink/Audio/Attenuation/) for future 3D sounds.
Audio audit (2026-06-25): swept all Play Sound 2D (C++ + BP). C++ clean (sidearm draw/holster gated).
BPs: WBP_SL_RespawnOverlay (UI, fine), BP_SL_MasterChief (correct SC_/SC_TP_ local-2D + attenuated-3D
pairs — fine). One finding → BP_GA_SL_Grenade_Throw: SC_FragThrow was played 2D in a Local-Predicted
ability → listen-host heard every player's throw. Decision: thrower-only feedback (gate local). Done:
cleared the now-meaningless SA_Default3D off SC_FragThrow (bridge). PENDING BP STEP: gate the throw's
Play Sound 2D node behind Is Locally Controlled (Branch, True → play) so only the thrower hears it.

Grenade C++ — DONE + committed (a308b5e, then polished in ee1109a): tags, USLGrenadeDataAsset + ASLGrenadeProjectile, ASLCharacterBase grenade count (GrenadeCount + CurrentGrenadeData, both replicated) + OnGrenadeCountChanged delegate + ThrowActiveGrenade() + loadout DefaultGrenade + controller GrenadeAction. ee1109a MP polish: BeginGrenadeThrow() schedules the authoritative spawn on a character world timer (ThrowReleaseTime) — fixes BUG-023 (clients couldn't throw: the spawn was on a Events.Grenade.Release anim notify and the server doesn't tick a remote client's montage). Plus cosmetic SpinMovement, Halo-style accelerating FuseLight, grenade blocks the Pawn channel (bounces off players, thrower excluded at launch).

Grenade — built but UNCOMMITTED (the bulk of the old checklist):

  • BP_GA_SL_Grenade_Throw (ability graph) — items 1+2: exists, granted in AS_AbilitySet_Default.
  • GC_SL_Grenade_Frag_Explosion cue — item 4: exists; GameplayCue.Grenade.Frag.Explosion registered.
  • FP+TP throw montages (AM_MC_FP_Frag_Throw, AM_MC_TP_Frag_Throw), DA_SL_Grenade_Frag filled — item 3.
  • Real frag mesh (Frag / SM_Modern_Weapons_Grenade_01 + materials) + real BP_SL_GrenadeProjectile_Frag
  • item 7. Bounce/explosion audio (SC_FragExplosion, SC_Frag_Bounce, tinks).

  • IMC_Default mapped (item 6), DA_DefaultLoadout updated.
  • Beyond plan: GE_SL_Grenade_Cooldown, CS_ExplosionShake camera shake.
  • Uncommitted C++ refinement (SLGrenadeProjectile.*, SLGrenadeDataAsset.h): replicates the whole
  • GrenadeData (was just FuseDuration) so clients sync spin + blink; new cosmetic BounceSound field + OnProjectileBounced. ⚠ Needs a rebuild to go live (editor/Live-Coding won't surface the new field).

Grenade pickup + death-drop — C++ DONE (2026-06-23), ⚠ pending rebuild + BP:

  • ASLGrenadePickup (World/SLGrenadePickup.h/.cpp) — auto-collect pickup mirroring ASLAmmoPickup. Tops
  • up the character's count for its GrenadeData type (clamped to MaxCount); ignores characters whose active type doesn't match or who are already full (left in world). No prompt — collects on overlap.

  • USLGrenadeDataAsset::DroppedPickupClass — which pickup BP to spawn on death (mirrors the weapon data's
  • field). Null = drop nothing.

  • ASLCharacterBase::DropGrenadesOnDeath() — spawns ONE pickup carrying the full current count, launches it
  • with the same DropSpawnOffset/DropLaunchPitch as weapon drops, then zeroes the count. Called from USLGameplayAbility_Death right after DropAllWeaponsOnDeath().

  • Editor steps: (a) rebuild (new class + UPROPERTYs — Live Coding won't surface them); (b) create
  • BP_SL_GrenadePickup_Frag (subclass ASLGrenadePickup, set bounce mesh + GrenadeData=DA_SL_Grenade_Frag

    • GrenadeAmount); (c) set DA_SL_Grenade_Frag.DroppedPickupClass = BP_SL_GrenadePickup_Frag; (d) test:
    • place one → walk over → count rises (capped at MaxCount, ignored when full); die holding grenades → a pickup drops carrying the count → collect it back.

Grenade — Increment 1 essentially DONE ✅ (2026-07-01):

  1. HUD grenade-count widget — DONE ✅. WBP_SL_GrenadeIndicator (USLGrenadeIndicator subclass) binds
  2. OnGrenadeCountChanged; icon + count live. Gotcha that bit: the widget instance in WBP_SL_HUDWidget must be named EXACTLY GrenadeIndicator (the BindWidgetOptional name) — it was named WBP_SL_GrenadeIndicator → bound to null → BIE never fired, no compile warning. See memory feedback_bindwidgetoptional_silent_noop.

  1. Rebuild — done (MP tested against live code).
  1. BUG-023 throw — VERIFIED ✅ via 2-player listen-server PIE: host AND client both spawn a real grenade;
  2. both observers see both projectiles + explosions. BugTracker.md BUG-023 marked verified.

  1. REMAINING: radial-damage falloff check (full inside InnerRadius → 0 at OuterRadius) is the one
  2. test-checklist item not yet ticked. Everything else on the Increment 1 checklist passes.

Build env note: one UBT build flagged a banned MSVC toolchain (14.40–14.43 → install 14.44.35207).
Seen once; if a full "close editor + build" fails on the toolchain, install the recommended MSVC component.

The dated lists further down are historical context from earlier sessions.

Sidearm — done & committed:

  • C++ foundation: tags, FP/TPSidearmMesh, replicated SidearmWeapon, SetSidearmWeapon,
  • LoadDefaultLoadout spawns DefaultSidearmClass, SLAnimState.bIsSidearmActive, loadout/data-asset fields.

  • BP_GA_SL_SidearmMode (hold-LT draw, pure tag manager) + LT input wiring.
  • Mesh visibility via USLWeaponsComponentnow poll-driven in BuildAnimSnapshots (BUG-014: the
  • one-shot tag-event path was unreliable on the listen-server host for proxies; see feedback_gas_tag_callback_patterns Rule 3).

  • Sidearm Blend AnimGraph node — custom C++ node (FAnimNode_SidearmBlend + new
  • SystemLinkCoreEditor module) compositing sidearm-over-lowered-primary at clavicle_r, gated by Is Sidearm Active. Committed + pushed (2234ce7). Doc: SidearmMode.md §4.1.

  • TP sidearm ABP worked out (idle / run-walk / crouch) using the node.

Sidearm — shooting (VERIFIED ✅ 2026-06-09):

  • C++ (GetActiveFireWeapon indirection + actor-owned ammo, option A) across USLWeaponsComponent +
  • USLGameplayAbility_Firecommitted. Primary fire unchanged when no sidearm is drawn. Doc: SidearmMode.md §8.1; memory project_sidearm_fire_routing.

  • Editor tasks 1–4 done via the Unreal MCP bridge: BP_GA_SL_Pistol_PrimaryFire
  • (RequiredWeaponClass=Pistol actor, ActivationRequiredTags=SidearmActive), AR+Shotgun primary blocked while SidearmActive, pistol fire ability granted via AS_AbilitySet_Default (sidearm is a separate always-carried slot — never runs through GrantWeaponAbilities, so it MUST be set-granted), DA_SL_Pistol.PrimaryFireMode filled (single-shot, 400 RPM, 26 dmg, 12 ammo, Muzzle socket, no AmmoDecrementEffect).

  • PIE-verified (listen server, 2 players): single-shot fire, independent sidearm ammo, and the
  • host-validates-client-damage RPC path (GetActiveFireWeaponData() resolves the sidearm server-side). Committed as d5e8ddf.

Weapon Action Lock — States.Weapon.Busy (done 2026-06-13). GAS-native lock so exclusive actions block other weapon actions. Native tag added (SLTags.h/.cpp) + full BP matrix wired via the bridge (fire/melee/sidearm/equip × 13 abilities). Exclusive actions own Busy; everything blocks Busy; switching also blocks Firing (fixes "switch to pistol mid-AR-fire → pistol full-autos"). Equip OWNS but does NOT block Busy (FP+TP equip run concurrently — blocking would mutually cancel them). Full spec + matrix + caveats: Docs/WeaponActionLock.md.

Pistol full-auto (BUG-018) — root-caused + fixed. Was the switch-mid-AR-fire path: the AR's full-auto fire ability is one looping activation that re-resolves GetActiveFireWeapon each iteration, so drawing the sidearm mid-loop fired the pistol at AR cadence. Fixed by the Firing block on SidearmMode (the AR loop owns Firing continuously, so the draw is blocked). No separate clean-draw full-auto bug — pistol reads GetActiveFireMode = SingleShot and fires once. Also fixed a latent bug: StopPrimaryFire used GetPrimaryFireMode() (equipped) instead of GetActiveFireMode(true) (active) → swapped to active (spam-tap fix). See BUG-018.

RefireLock split + sidearm action-lock fixes — DONE & PIE-verified ✅ (2026-06-13). All three C++ changes rebuilt and live: StopPrimaryFire uses GetActiveFireMode; RequestEquip blocks main-weapon swap/pickup while sidearm drawn; RefireLock split (native tag + RefireLockDuration field + fire-ability loose-tag management). BP re-wire done via bridge (SidearmMode + 6 equips block on RefireLock not Firing). DA_Shotgun.RefireLockDuration = 0.2 (PostFireDelay 0.5) — switch to pistol 0.2s after a shot while re-fire stays gated. Tested good, feels good. Full design: Docs/WeaponActionLock.md.

Design decisions (2026-06-14): Sidearm now drops on death as a pickup (supersedes BUG-013's "destroy"). Pistol pickup: same type → ammo refill on the sidearm; different → swap into sidearm slot, old drops as a pickup.

Drop-all-on-death + sidearm pickup routing — DONE in C++, ⚠ PENDING REBUILD (2026-06-14):

  • ASLCharacterBase::DropAllWeaponsOnDeath() drops all CarriedWeapons + the sidearm (clears slots);
  • called from USLGameplayAbility_Death before OnDeathStarted (deterministic, authority).

  • ASLWeaponPickup::OnCollected/AcceptPickup branch on WeaponData->bIsSidearm → route to the sidearm
  • slot (same type = ammo via AddAmmo; different = SetSidearmWeapon, which drops the old). Data-driven, so placed AND dropped/death-dropped pistols route correctly.

  • DA_SL_Pistol.bIsSidearm = True (confirmed). DroppedPickupClass = None (needs the pistol pickup BP).

Rebuilt + wired (2026-06-14): C++ live. BP_SL_WeaponPickup_Pistol created (/Game/SystemLink/Pickups/Weapons/Pistol/, dup of Shotgun2) with WeaponActorClass = BP_SL_WeaponActor_Pistol; DA_SL_Pistol.DroppedPickupClass → it.

REMAINING (Beepers, editor):

  1. Set the pistol pickup's display meshBP_SL_WeaponPickup_Pistol still shows the shotgun mesh (SCS
  2. component from the dup; not reachable via bridge). Set it to the pistol mesh (SK_Pistol) — unless the pickup shows the weapon actor's mesh at runtime, in which case it's already right (check on placement).

  1. Remove weapon-drop nodes from the BP_SL_MasterChief OnDeathStarted override — C++ drops everything
  2. now; leftover BP drops are redundant no-ops. Keep any non-drop death logic (ragdoll/score/etc.).

  1. Test: die → all carried weapons + the sidearm drop as pickups; pick up same pistol → ammo refill;
  2. different pistol → swaps into sidearm slot + old drops; pistol pickups route to the sidearm slot (not main inventory); placing BP_SL_WeaponPickup_Pistol in the level is collectable.

ACTIVE TASK — Pistol HUD (2026-06-15): sidearm ammo indicator — pistol icon + ammo count. C++ scaffolding landed (see "Sidearm HUD" below); BP authoring + data-asset icon are the remaining editor steps. Reticle swap on draw (OnSidearmActiveChanged, now edge-correct) is the sibling task — the indicator is persistent (keyed off OnSidearmChanged actor validity), while the reticle swap still keys off the draw/holster OnSidearmActiveChanged toggle.

Sidearm HUD — C++ done (2026-06-15): reuses the existing USLAmmoWidget (its SetWeapon(actor) binds the actor's OnAmmoChanged + pushes the initial value — the sidearm is an ASLWeaponActor, so no new widget class needed). USLHUDWidget gains an optional SidearmAmmoWidget (BindWidgetOptional) + an InitializeSidearmIndicator(Character) helper, called from ASLPlayerCharacter::InitializeLocalPlayerHUD alongside the ammo strip. Visibility is persistent — shown whenever the player has a sidearm (drawn or holstered), hidden only when there's no sidearm — driven off OnSidearmChanged actor validity. Icon comes from WeaponData->WeaponIcon via the new OnWeaponSet BIE on USLAmmoWidget. Remaining (editor): author the sidearm indicator BP (USLAmmoWidget subclass: Image + ammo Text, implement OnAmmoChanged + OnWeaponSet), bind it as SidearmAmmoWidget in the HUD BP, set DA_SL_Pistol.WeaponIcon. See Docs/SidearmMode.md §12.

OPEN THREADS (pick up here — as of 2026-06-11):

  1. ⚠ REBUILD PENDING — 4 uncommitted C++ changes, not yet compiled. Close editor → build → reopen
  2. (Live Coding won't register the new UFUNCTIONs/delegate for BP):

    • ASLCharacterBase::GetActiveWeaponMesh()BlueprintPure, view+slot-aware (FP/TP × sidearm/main).
    • Attach fire FX to this. ApplyPistolFireEffects already works; optionally simplify it onto this.

    • USLWeaponsComponent::OnSidearmActiveChanged(bool)BlueprintAssignable, edge-detected from
    • UpdateSidearmMeshVisibility. For the reticle swap (#5).

    • FSLWeaponViewDriverBase anim-layer swap fix (BUG-016) — targeted UnlinkAnimClassLayers instead
    • of LinkAnimClassLayers(nullptr), so swapping the main weapon no longer drops the sidearm layer.

    • SLGameplayAbility_Melee::GetWeaponData()GetActiveFireWeaponData() (pistol-whip, #2).

(Note: the controller's sidearm-redraw change was reverted to Started — net-zero diff there.)

  1. Pistol-whip melee (replaces auto-holster). Melee while sidearm drawn = pistol-whip: pistol stays
  2. out, swing uses the pistol's melee values. C++ done + rebuilt (#1). EDITOR STEP — DONE ✅ (2026-06-13): verified BP_GA_SL_Melee.CancelAbilitiesWithTag is empty (no SLTags.Abilities.SidearmMode), so melee no longer cancels sidearm mode. Full tag config confirmed: AbilityTags=Abilities.Melee, ActivationOwnedTags=States.Character.Meleeing, ActivationBlockedTags=States.Character.Dead+Meleeing. ANIM (remaining): pistol-whip melee state gated on bIsMeleeing && bIsSidearmActive (Always Reset on Entry + Inertialization + MeleeImpact notify); put the DisableLHIK curve on the pistol melee clip so off-hand IK releases during the swing (Docs/LeftHandIK.md). Doc: SidearmMode.md top callout.

  1. Cues — DONE ✅. Pistol-specific cues live: tags GameplayCue.Weapon.Pistol.PrimaryFire/.Impact
  2. registered, GC_SL_Pistol_PrimaryFire/_Impact tagged, DA_SL_Pistol Fire/ImpactCueTag repointed (cleared the AR duplicate-tag collision — BUG-015). FP/TP/local/non-local all route through ApplyPistolFireEffects in BP_WeaponLibrary. PIE-verified. PostFireDelay = 0.2 placeholder (tune to the fire clip; AM_Pistol_Fire is 0.667s).

  1. TP crouch-fire anim (task #6). FP fire anim DONE. TP: firing while crouched stands the character up
  2. — needs crouch-aware Firing. Either a CrouchFiring state gated on bIsCrouched (crouch-fire clip), or upper-body-only fire via Layered Blend Per Bone (lower body keeps crouch, no extra clip). bIsCrouched

    • bIsFiring are on AnimStateSnapshot.
  1. Reticle swap (task #7) — DONE ✅ (2026-06-15). C++: OnSidearmActiveChanged (edge-correct) +
  2. USLWeaponsComponent::GetActiveReticleClass() (BlueprintPure: sidearm's PrimaryReticleClass while SidearmActive, else equipped's; falls back to equipped's when the active weapon has none) + SwapReticle no-ops on an unchanged class. BP wired in WBP_SL_HUDWidget (Initialize Variables / Bind to Equip Event / Bind to Sidearm Events groups): equip + OnSidearmActiveChanged + OnSidearmChanged all route through SwapReticle(GetActiveReticleClass(), GetOwningPlayer), so draw/holster AND pickup-swap-while-drawn all update correctly. DA_SL_Pistol.PrimaryReticleClass set. Verified working. Full reference: SidearmMode.md §13.

  1. Verify after build: BUG-016 (pistol anims persist through Shotgun↔AR swap) + pistol-whip melee feel.
  1. Later: sidearm draw is INSTANT — SidearmDrawDuration/SidearmDrawing are unused scaffolding until
  2. a draw anim exists (then gate fire on not-SidearmDrawing); HUD sidearm ammo indicator (bind SidearmWeapon->OnAmmoChanged); BUG-013 sidearm-on-death cleanup (still open).

Sidearm DRAW — DONE & feels good ✅ (2026-06-14). Full draw landed: gameplay windup + fire-block + observer-correct animation + None-guard. FSLAnimState has bIsSidearmDrawing + bIsSidearmHolstering, both PURELY COSMETIC anim drivers latched in BuildAnimSnapshots off the rising (draw) / falling (holster) edge of the replicated SidearmActive tag — so observers see them. Decoupled from the SidearmDrawing gameplay tag, which does the fire-block (owning client + server only).

  • Gameplay: BP_GA_SL_SidearmMode §3 windup graph (Commit-gated Add + OnEndAbility Remove,
  • Should Replicate=FALSE, Is Valid(GetSidearmWeaponData) guard on the Delay duration); SidearmDrawing in BP_GA_SL_Pistol_PrimaryFire.ActivationBlockedTags; DA_SL_Pistol.SidearmDrawDuration=0.5.

  • Observer fix: bIsSidearmDrawing rising-edge latch off SidearmActive (held SidearmDrawDuration,
  • fallback SidearmDrawCosmeticFallback 0.5s) — the loose tag doesn't replicate to proxies.

  • New BP accessors on USLGameplayAbility: GetSidearmWeapon() / GetSidearmWeaponData() (null-safe).
  • Full spec: Docs/SidearmMode.md §3 + §4.2.

HOLSTER — DONE ✅ (2026-06-15, cosmetic). Pose + mesh now both hold through the holster tail. Implementation (all cosmetic — gameplay returns to the primary the instant SidearmActive drops):

  • Pose: the Sidearm Blend node's Is Sidearm Active pin is driven by `Is Sidearm Active OR Is
  • Sidearm Holstering (OR node off Break SLAnim State) in ABP_MC_TP_Pistol — keeps the sidearm branch blended in during the tail (the node's bIsSidearmActive alone would crossfade it out over BlendTime`).

- Mesh: BuildAnimSnapshots now calls `UpdateSidearmMeshVisibility(bIsSidearmActivebIsSidearmHolstering)`

(SLCharacterBase.cpp:658) so the pistol stays visible until the holster anim ends instead of popping out.

  • Duration is now per-weapon: new USLWeaponDataAsset::SidearmHolsterDuration (default 0.5s, tooltip
  • flagged PURELY COSMETIC). The falling-edge latch reads it (SLCharacterBase.cpp:644), falling back to the character's SidearmHolsterCosmeticTime (0.3s) when no weapon/data resolves. Set DA_SL_Pistol.SidearmHolsterDuration to the holster-clip length (matches SidearmDrawDuration since the holster is the draw played in reverse).

  • Draw + holster sounds (local-only, data-asset driven): new USLWeaponDataAsset::DrawSound and
  • HolsterSound (USoundBase, tooltips flagged PURELY COSMETIC / LOCAL-ONLY). Played via UGameplayStatics::PlaySound2D in BuildAnimSnapshots on the draw rising edge / holster falling edge, gated on IsLocallyControlled() — not replicated, observers don't hear them (the gate also prevents a listen-server host hearing remote players' sounds). Draw sound was MOVED out of BP_GA_SL_SidearmMode onto the data asset so the ability stays weapon-agnostic/reusable. Editor step: delete the draw-sound nodes from BP_GA_SL_SidearmMode, then set DA_SL_Pistol.DrawSound + .HolsterSound.

  • Delegate-edge fix: UpdateSidearmMeshVisibility now edge-detects OnSidearmActiveChanged on the
  • gameplay SidearmActive tag (IsSidearmActive()), NOT the visibility flag — otherwise the reticle-swap delegate would fire ~0.5s late (at the end of the cosmetic tail) instead of when gameplay returns to the

primary. Mesh visibility still uses `activeholstering`; the broadcast uses the tag.
  • ⚠ New UPROPERTYs (SidearmHolsterDuration, DrawSound, HolsterSound) → **full rebuild + editor
  • restart** (Live Coding won't surface the fields), then set the data-asset values.

Holster currently reuses the draw clip in reverse and feels good. Optional later: a dedicated draw/holster ABP state pair (bIsSidearmDrawing / bIsSidearmHolstering flags + windup timing are already in place).

Side work (non-branch): the ClaudeFace repo has uncommitted Loom changes this session — per-model
substrate signature, Face retired, manual-posture minimum-dwell, session-start auto-open hook. See
Docs/WorkingWithClaude.md (2026-06-10 addendum) + memory project_claude_face.

Melee — fully done:

  • Input, BP_GA_SL_Melee, ability-set grant, fire-block tag — done.
  • ABP melee state for AR + Shotgun + Unarmed (FP + TP), Always Reset on Entry, Inertialization.
  • MeleeImpact AnimNotify on FP + TP melee anims.
  • C++ features: backstab multiplier, MeleeImpactCueTag, OnMeleeWallHit BIE, camera shakes
  • (Swing/Hit/WallHit on weapon data asset — swing only plays on clean miss), timer-driven authority damage, C++-owned ability end timer, eye-origin sphere sweep.

  • All melee values (damage, range, radius, backstab, shakes, timing) moved to USLWeaponDataAsset.
  • Unarmed-as-default-weapon: DefaultUnarmedData on USLWeaponsComponent, GetEquippedWeaponData()
  • fallback, sway/lag/obstruction/scale all routed through it when unarmed.

  • FirstPersonMeshScale moved from character to weapon data asset, re-applied on every weapon swap.
  • MuzzleSocket no-mesh warning fixed in ResolveMuzzleLocation.

Weapon Clipping — done (FP): Docs/WeaponClipping.md. Optional leftover: AR FP ABP retract additive.

Respawn stuck-bug — FIXED. See RespawnSystem.md.

#Future-work doc also written this branch (not part of melee work)

Docs/MobilityAssistModule.md — full plan for Doom Eternal-style double jump + dash via an equippable Mobility Assist Module (MMA-3). Added as item #6 in Docs/Progress.md backlog. Not in scope for the melee-ability branch.

#What Was Done This Session (2026-05-19)

TP shotgun ABP — non-looping fire anim fix

  • Symptom: shotgun fire animation in ABP_MC_TP_Shotgun's Firing state played once on first shot, then never again on subsequent shots. AR equivalent worked fine.
  • Root cause: non-looping asset players in state-machine states 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.
  • Fix: enabled "Always Reset on Entry" on the Firing state. Asset player resets to t=0 on every state entry. Looping anims (AR, crouch) don't hit this because they wrap around continuously.
  • Memory pointer: feedback_animbp_state_reset_on_entry.md — this fix is documented for future state-machine work.

Inertialization node addedABP_MC_TP_Shotgun AnimGraph: the Not Firing → Firing transition used Inertial Blend logic but had no Inertialization node downstream, producing a warning on quit. Single Inertialization node placed between the state machine output and the Layered Blend Per Bone.

FSLAnimState::bIsFirstPerson added

  • New UPROPERTY(BlueprintReadOnly) bool on the anim snapshot, sourced from WeaponsComponent->IsFirstPersonView()
  • Always false on remote/non-locally-controlled pawns — observers naturally see TP anim paths
  • Wired in BuildAnimSnapshots() alongside existing bools
  • Use case: shared shotgun ABP branches between FP and TP fire animations on this bool

Sequencer animation authoring workflow documented

  • Docs/SequencerAuthoringWorkflow.md — full pipeline (create sequence from Content Browser, drag mesh assets directly, convert to Spawnable before adding constraints, bake to AnimSequence)
  • Recovery from the TransformableComponentHandle harvest crash (caused by Possessable bindings + constraints) — lesson learned the hard way mid-session when LS_TP_Shotgun_Idle2.uasset corrupted and crashed the editor on save
  • Memory pointer: feedback_sequencer_spawnables.md

#What's Next (in order)

#Hit Detection Refinement — complete ✅ (2026-05-20)

Per-bone collision via Physics Asset + custom ECC_WeaponTrace channel landed and verified end-to-end. Hit.BoneName populates with real bone names; bone-group anchors + per-weapon damage multipliers wired through both authoritative damage sites.

Implementation summary:

  • Custom trace channel ECC_WeaponTrace (Default Response: Block, mirrors ECC_Visibility behavior). Channel landed on ECC_GameTraceChannel1, aliased in Types/SLCollisionChannels.h.
  • Capsule ignores ECC_WeaponTrace (Custom collision preset on BP_SL_MasterChief). Physics Asset bodies on SK_MasterChief block it via the SkeletalMeshComponent's Custom collision preset.
  • C++ swap: all four trace call sites moved from ECC_VisibilityECC_WeaponTrace (Server_ProcessShots line trace + sphere sweep, ExecuteFire_ListenServerHost line trace, TracePredictedHit line trace).
  • ESLBoneGroup enum (Body/Head/Arms/Legs), FSLDamageMultipliers struct on FSLWeaponFireMode (defaults: Body 1.0, Head 2.0, Arms 0.75, Legs 0.75), BoneGroupAnchors map + GetBoneGroup() parent-chain walk on ASLCharacterBase.
  • Debug prints gated on bDebug (on the fire ability and weapons component independently). Prints bone name + resolved group + multiplier + final damage on every confirmed hit.

Editor configuration on BP_SL_MasterChief.BoneGroupAnchors (~5 entries — anchor walk handles descendants):

  • neck_01 → Head, upperarm_l/r → Arms, thigh_l/r → Legs

#Multi-Slot Weapon HUD — complete ✅ (2026-05-22)

Halo 3-style stacked weapon-slot strip. Both carried weapons show live ammo simultaneously; equipped weapon on top (active scale/opacity), secondary below (dimmed). Ammo updates instantly on shots and pickups. Legacy SwapAmmoWidget path fully removed.

Key implementation notes for future reference:

  • RebuildSlots adds the equipped weapon first (guarantees it's at container index 0 — no reorder pass needed)
  • HandleEquippedWeaponChanged calls RebuildSlots (not UpdateActiveSlot) — ensures fresh ammo values at equip time
  • AddAmmo on the equipped-weapon path calls SetCurrentAmmo after SetNumericAttributeBase to broadcast the delegate (bypasses PostGameplayEffectExecute)
  • See Docs/WeaponsSystem.md → Ammo Display for the full architecture

#Queued next (in order)

  1. Melee — ✅ done.
  1. Weapon Clipping — ✅ done (FP). Optional leftover: AR FP ABP retract additive + TP eval (likely ignore).
  1. Secondary Weapon — Left-Hand SidearmIN PROGRESS. Foundation + draw + mesh + TP animation done & committed. Open: shooting (C++ written, uncommitted/untested → build, editor tasks, PIE, commit). Then HUD ammo indicator + death cleanup (BUG-013).
  1. Melee polish — minor remaining: unarmed melee anim, SFX assets, camera-shake asset assignment.
  1. Menus / CommonUI foundation — next after sidearm. Design + footguns fully documented in Docs/UISystem.md. Layer scaffolding in place; missing input data, controller data assets, C++ base classes, pop API, pause wiring. See Docs/Progress.md #5.

#Reference for any future new weapon

Docs/NewWeaponChecklist.md — end-to-end checklist. AR is the reference pattern; duplicate and customize.

#Reference for any future new weapon

Docs/NewWeaponChecklist.md — end-to-end checklist (tags, meshes, sockets, data asset, abilities, cues, animations, HUD, pickup, test). Use as the canonical guide when adding a new weapon. AR is the reference pattern; duplicate and customize.

#Deferred shotgun polish (still outstanding from previous branch, not blocking new work)

  • Shotgun fire cosmeticsBP_GA_SL_Shotgun_PrimaryFire → OnLocallyPredictedShotFired still uses AR sound + AR muzzle flash Niagara. Swap when shotgun-specific assets exist.
  • Shell ejection — designed but not implemented. Plan: Niagara mesh emitter + GameplayCue (GC_SL_Shotgun_ShellEject) triggered from a pump-anim AnimNotify sending SLTags.Events.Weapon.ShellEject. Add ShellEjectSocket to the shotgun skeletal mesh.
  • FP shotgun fire animation polish — barrel-snap-up + pump cycle on the FP weapon mesh.

#Test Checklist

Authoritative checklist lives in Docs/Shotgun.md → Test Checklist. Update it there as items pass. For the recent multiplayer-correctness fixes (cosmetic gating, trace channel parity, RPC batching, cancelable fire ability, cue contract), follow Docs/FireAbilityNetworkTesting.md.


#Key Docs for This Work

DocWhen to read
Docs/Shotgun.mdFull spec — data asset values, tag names, asset locations, build order, authoritative test checklist
Docs/WeaponsSystem.mdHow the weapon system works end-to-end (includes "Authoring a Fire Cue" contract)
Docs/WeaponFireAbility.mdFire ability internals
Docs/FireAbilityNetworkTesting.mdPIE procedure for verifying multiplayer correctness of the fire flow
Docs/WeaponEquip.mdEquip ability pattern
Docs/Progress.mdFull project feature list and backlog

#State of the Shotgun (2026-05-19)

  • FP idle animation in place — looks correct, hand grip and weapon position good
  • TP fire/pump animation working via state machine in ABP_MC_TP_Shotgun (Firing state, non-looping anim, Always Reset on Entry + Inertialization)
  • bIsFirstPerson available on AnimStateSnapshot for FP/TP branching inside shared ABPs
  • Fire cosmetics still cloned from AR (OnLocallyPredictedShotFired uses AR sound + muzzle flash) — deferred polish
  • Shell ejection — designed (Niagara mesh emitter + GameplayCue + AnimNotify), not implemented — deferred polish

#Deferred Cleanup (do in a content-pass session, not mid-feature)

#AssualtAssault content rename

14 .uasset files still carry the Assualt typo. Rename each in the Editor's Content Browser (right-click → Rename) so reference updates are automatic; do NOT rename on disk. After the renames, do a single project search-and-replace across Docs/ for "Assualt" → "Assault" to clean up doc references.

Code-referenced (most important — named as the parent BP to duplicate in Docs/Shotgun.md and Docs/WeaponFireAbility.md):

  • Content/SystemLink/AbilitySystem/Abilities/Weapons/AssaultRifle/BP_GA_SL_AssualtRifle_PrimaryFire

Character animations (6 — AssaultRifle/ and AssaultRifle/FP/ subfolders):

  • assualt-rifle-idle, assualt-rifle-idle-2, assualt-rifle-idle-breathing, assualt-rifle-run, FP/assualt-rifle-idle
  • Content/SystemLink/Characters/MasterChief/Anims/Library/Animations1/AssaultRifle/assualt-rifle-idle

Weapon visuals (6):

  • Content/SystemLink/Weapons/AssaultRifle/Mesh/M_AssualtRifleClip
  • …/Materials/assault-rifle-clip_M_AssualtRifleClip_BaseColor + _Normal + _OcclusionRoughnessMetallic
  • …/Materials/HUD/TX_AssualtRifleHud
  • …/UI/assualt-rifle-reticle + _Resized

#Equip-ability tag pattern — reconciled ✅ (2026-05-22)

All three sources now use the canonical registered pattern `SLTags.Abilities.Equip.<FirstPersonThirdPerson>.<Weapon>`:
  • SLGameplayAbility_Equip.h doc comment updated
  • Docs/Shotgun.md CDO setup section updated
  • Config/DefaultGameplayTags.ini is the source of truth (was already correct)

#What NOT to Touch This Session

  • USLGameplayAbility_Fire C++ — pellet loop is done, don't re-open it
  • Weapon swap HUD — deferred, not part of this branch