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.
ActivationProgressandReverseConstructionDirectionsupport 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+USLScreenEffectWidgetwritten, built clean, and **PIE-tested by Beepers:
"they test well." Full reference: Docs/Teleporters.md**.
BP_SL_TeleporterandBP_SL_TeleporterColumnbuilt from the C++ base, a pair placed inTestMap, new
Niagara set authored (NS_SL_TeleporterField, NS_SL_SlipspaceArcs, NS_SL_TeleporterFilaments).
WB_Teleportreparented toUSLScreenEffectWidget, 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 ofWB_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_Teleporterfrom six BP screenshots (Docs/Screenshots/2026-08-10 0816–0817). - It teleported straight out of
OnComponentBeginOverlapwith no authority check — the client, not Enabled/Target Actorwere unreplicated but drove the rings and the hum.- A
Does Object Implement Interfacenode with an EMPTY interface gated the whole cosmetic - Its
Target Actorlatch was released only byEndOverlap, so **dying on a pad bricked the teleporter - Also:
Play Sound at Locationpinned to(0,0,0); arrival facing set via actor rotation, which does
Verdict: not safe for multiplayer, and it reproduced all three faults ASLDoor was written for.
the server, was deciding where a player ended up.
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.
for the rest of the match**.
nothing to where a possessed player looks.
- Wrote
ASLTeleporter(Public/World/SLTeleporter.h+Private/World/SLTeleporter.cpp) — the same - Authority-only overlap; server does the move;
bReadyreplicated withOnRepfor the pad visuals. - Readiness is a timer, not a latch — there is no occupant state a dead player can strand.
ClientSetRotationfor arrival facing;ExitMomentumenum (defaulted toRedirect— an assumption,- The old
CanBeTeleportedTocount-the-overlaps graph is one server-side sphere query. ASLPlayerController::Client_OnTeleportedadded: a level actor has no owning connection, so its own
wrap-in-C++ shape as ASLDoor. Builds clean. NOT PIE-tested and no Blueprint uses it yet.
not Beepers' decision) plus a MaxExitSpeed clamp for the 1625 cm/s dash case.
Client_ RPCs go nowhere. The controller makes the hop and calls back into the teleporter.
Docs/Teleporters.mdwritten — 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
BP_SL_TeleporterA 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:
- New BP from
SLTeleporter; meshes + rings Niagara + hum AudioComponent under the inheritedRoot.
- Resize the inherited
TriggerVolume— do not add your own, it would be bound to nothing.
- Move
ExitPoint; its +X is the arrival facing.
- Implement
OnTeleporterStateChanged(bIsReady, bImmediate)→ rings visibility =bIsReady,
audio paused = NOT bIsReady. Honour bImmediate (snap, don't animate).
- Class Defaults:
ArrivalOverlayClass=WB_Teleport,TeleportSound2D=HaloTeleporterTeleport_Cue.
That is the entire HUD hookup — C++ creates, shows and removes the overlay.
- Place two in Level 1 and point each instance's
TargetTeleporterat the other (EditInstanceOnly, so it
can only be set on placed instances).
⚠ Two WB_Teleport assets exist — Environment/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
929766fbIS 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 unconfirmed —
Level1.umapis 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
| System | C++ | Blueprints | State |
|---|---|---|---|
| Pickups | SLPickupBase, SLWeaponPickup, SLAmmoPickup, SLGrenadePickup, SLHealthPickup, SLOvershieldPickup | BP_SL_WeaponPickup_{AssaultRifle,Pistol,Shotgun2}, BP_AmmoPickup_AssaultRifle, BP_SL_GrenadePickup_Frag, BP_HealthPickup, BP_SL_OvershieldPickup | Complete. The work is PLACEMENT, not code. |
| Doors | none | BP_ForerunnerDoor1 + meshes/materials/HaloDoor1_Cue | Migrated from the 5.6 project, untested here |
| Teleporters | none | BP_Teleporter + NG_TeleporterRings/Sparks, sounds, WB_Teleport | Migrated, 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
- Play Level 1 as-is. It may already be walkable; the only known gaps are the interactables.
- Place the GAS pickups (old coordinates recorded in
Docs/Level1Import.md§4), swapping the
legacy overshield for BP_SL_OvershieldPickup.
- Doors — test
BP_ForerunnerDoor1in 2-player PIE, then decide rebuild vs wrap.
- 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_MasterChiefview-mode port is LIVE. The oldSetMeshVisibilitynode 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
- Two faults compounding: FP hiding sat behind
if (!IsLocallyControlled()) return;, and the only - Fix: new
ASLPlayerCharacter::ApplyFirstPersonMeshVisibility(), called fromApplyViewModeVisuals - THE RULE:
SetOwnerNoSeeis evaluated per viewer;SetVisibilityis per machine.
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.
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.
above the guard, plus from BeginPlay / PossessedBy / OnRep_PlayerState (each above their own early returns). Full RCA: Docs/BugTracker.md → BUG-030.
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
- The pack system was copied to
/Game/SystemLink/FX/Character/NS_Thrustersrather than edited in - Duration is a
User.Durationfloat user parameter, linked into the emitters' Emitter State - ⚠ 0.3 s FX vs 0.2 s dash.
DashDurationis not overridden inDA_SL_MobilityModule_MMA3, so - To make them track each other: add
CueParams.RawMagnitude = Module->DashDuration;in
session does not re-derive them the expensive way.
Content/Library/, so a pack update cannot clobber the tuning.
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.
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.
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.
MaxDashChargesis 1 and stays there (decided 2026-08-05).DA_SL_MobilityModule_MMA3was 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:502reads
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_GlobalScaleandNMS_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_Dashis aGameplayCueNotify_Burst(i.e._Static):OnExecuteonly, **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/ThrusterRightSocketon
/Game/SystemLink/Characters/MasterChief/Meshes/MasterChief. The misspelling Thurster… fails silently — Spawn 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 reorganization —
Content/{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
- ⚠ One risk:
level1-fort-flooris complex-as-simple and a zero-thickness sheet — a
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.
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]inDefaultEngine.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_assetin 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.mbonly — 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.md → Maya / 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-mcp0.6.1, pip-installed intoC:/Python313(user site-packages). Added to.mcp.jsonas
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_MobilityThrustersprefers a socket named exactlyMobilityModule_Socketon 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.mdPhase 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/_v3separate-script pattern already inTools/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:
- A native
UGameplayCueNotify_Staticsubclass is never registered. The manager asset-scans
GameplayCueNotifyPaths and reads GameplayCueName off asset data; a C++ class is not an asset. Fixed by adding Blueprint subclasses in the scanned path.
- The asset NAME must derive the tag.
AbilitySystemGlobals.h:129— when a child's tag equals its
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.
- The cue was gated on
HasAuthority(), which discardsExecuteGameplayCue's own predicted branch
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, NOTEquipping. Only the TP equip abilities ownEquipping;
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:
Busyis 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 MeleeLungeSpeed → MeleeTargetLungeSpeed 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
Targetdoes 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.
- ⚠
LaunchCharacteris not replayed on a client correction (FSavedMove_Characternever 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.LungeSpeedcovers 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.upluginnow 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.Dashand
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_Socketwhen authored and safely falls back tospine_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.maand 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_pitch → nozzle_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 2 —
USLPlayerPerceptionComponentonASLPlayerCharacter: 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 4 —
USLHUDWidget::InitializePerception()binds the delegate and drivesUSLReticle::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:ApplyRagdollStateoriginally usedIsSimulatingPhysics()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:
FellOutOfWorldfires every tick while below Kill Z, not once — needs a latch (bFellOutOfWorldHandled).
- Never call
Super— the engine default isDestroy(), 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:BuildAimRayhardcoded
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 inflation— FIXED 2026-07-29 (commita5dd138c).
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 untrackedDocs/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_0 → Text_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) — newFTableRowBase: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+USLSettingsRowWidgeteach 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.
ISLFocusablegained 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_Settingswiring (Beepers, in-editor): blocks namedHeadingText+DescriptionText; Construct →
Get Menu Focus Subsystem → Bind Event to On Focused Copy Changed → custom event On Focused Copy Changed → Set 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 removed —
Get Menu Focus Subsystemreturns 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_Settingsrows are nowRow_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_Accept → Virtual_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.
USLPlayerProfileSubsystemis now aULocalPlayerSubsystem, not aUGameInstanceSubsystem. 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.
BlueprintPuregetters 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 profile —
SLPlayerProfile_<Name>,SlotUserIndexalways 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_Sensitivity0.5–10 step 0.5 ·Row_ADS0.1–2.0 step 0.1 ·Row_FOV80–110 step 5 (Decimals 0).
- Screen Construct →
Sequence→ five seeds, all reading the new subsystem getters withBroadcastOFF.
SaveProfileonOn Deactivated, behind anIsValidon the subsystem (teardown guard).
Two bugs found and fixed on the way — both looked like "settings won't save":
- Rows were bound to
OnValueChanged, which carries the INDEX. Every change wrote 0/1/2… into a clamped
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).
- The save file already held that garbage — every field sat at its clamp minimum, so the (working) seed
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/) readsMouseSmoothingoff 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.
bMouseSmoothingon 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_SettingCopygained aMouseSmoothingrow;Row_MouseSmoothingadded 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; everySettingCopyhandle inWBP_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++:
SLTagsgainedEvents.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.
- Raw
Velocitywrites aren't network-predicted → rubber-banding for a non-host client. Now a root motion
source (ApplyRootMotionConstantForce), which CMC replicates and replays.
- The root motion task's
OnFinishnever fired → ability never ended →States.Character.Dashingstuck →
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.
- Dash direction must be resolved by the input owner. Inside the ability, the server recomputed from a
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).
AirControl0.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.
JumpMaxCountstays 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'sDoubleJumpZVelocity = 700is 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_Defaultnegated 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
DeadZonethat 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_Pause1 → WBP_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_Rotatoris fully built:Border → RowBox [HorizontalBox]→
Spacer · LabelText · LeftArrowButton · SizeBox_0(MyText) · RightArrowButton · Spacer, so the required
MyTextbind is satisfied. AndWBP_SL_Settingswent 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 (parentUSLRotatorRow, 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_childare
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 OnBoolChanged → SetInvertLookY/X). (This is also the deferred USLDropdownRow slot.)
PIE findings (verified via bridge + testing):
- ✅
WBP_SL_Settings+WBP_SL_PauseCDOs confirmedInputModeOnActivate=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(+USLProTipSettingsDeveloperSettings) 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_CommandBarbinds
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).
#Menu work — where we are (2026-07-06)
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— addedbPauseGameWhileActive; pause/unpause tied to activate/deactivate,
standalone-only (footgun 6.12), so it survives any close path (Start toggle, B/Esc, pop).
ASLPlayerController—OpenPauseMenu/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]withbEnableEnhancedInputSupport=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). ✅
bEnableEnhancedInputSupportin 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_Pausemapped inIMC_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:
USLScreenWidgetalready defaultsInputModeOnActivate = 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_InputActionscreated in/Game/SystemLink/UI/Input/(row structCommonInputActionDataBase):
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_CommonInputData—DefaultClickAction={DT,Confirm},DefaultBackAction={DT,Back}; compiled+saved.
- ✅
BP_SL_PlayerController—PauseAction=IA_SL_UI_Pauseset; compiled+saved. **PauseMenuClassstill
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/) —USLButtonBasesubclass,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 fires —
IA_SL_UI_Pausemapped inIMC_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_Button—ButtonTextlabel; 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_PauseMenuset onBP_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):
ASLPlayerController::RestoreGameInputMode— restores game input/viewport focus when the last menu closes
(footgun 6.30; else the game is dead after close).
USLScreenWidget::ValidateCompiledWidgetTree— compile error if a screen has noGet Desired Focus Target.
USLCommonActivatableWidget::AutoFocusWidget— un-exposed from BP (bareUPROPERTY()).
#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.
IMC_SL_UIwas never applied (asset registry: zero referencers — orphaned). CommonUI resolves glyphs- verified via bridge read-back).
via QueryKeysMappedToAction, which only sees contexts applied right now → no key → NoBrush → UpdateActionWidget 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
⚠ 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.
- The glyph fallback is hover-gated → dead on gamepad.
UCommonButtonBase::UpdateInputActionWidgetonly
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.
- Sticky focus highlight — all tiles stayed lit. FIXED + PIE-VERIFIED ✅ 2026-07-15 (BP). NOT sticky
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 dynamicCommonBoundActionBarbelow 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 Select | B 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):
WBP_SL_BoundActionButtontree — needs BOTH aUCommonActionWidgetnamed exactly
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 theText_ActionName->SetText()call is INSIDE it → no glyph widget means a
blank button, text and all. → §6.35(b).
- A
UCommonBoundActionBarwithActionButtonClass = WBP_SL_BoundActionButton(missing class = compile
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).
- Display name —
IA_SL_UI_Back.ActionDescription(data-driven, preferred) or
OverrideBackActionDisplayName per screen. Empty = correct glyph, blank label. → §6.35(c).
- PIE gate: open pause → bar shows
B Back(gamepad) → B closes. Expect no glyph on KBM — intentional,
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 → CastSLPlayerController→
Close Pause Menu.
- Settings → stub for now (push
WBP_SL_Settingsonce it exists) — or leave unwired.
- Exit → leave match / quit. Route through a
USLModalWidgetconfirm ("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:
- POC: "MenuKit" — extract the CommonUI menu framework into a standalone Fab plugin. Byproduct of
menu work already underway; POC metric = live listing + first sale + pipeline knowledge, NOT revenue.
- Main bet: "Console-Ready UI with CommonUI" course (after POC ships). 3. Multiplier: devlog from
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).
- Per-drop launch variance —
ASLPickupBase::LaunchPickupnow randomizes yaw + pitch (LaunchPitchSpread,
±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).
- Thrown grenade chains into pickups — extracted
ASLGrenadePickup::ChainDetonateNearby(...)static;
ASLGrenadeProjectile::Explode now detonates grenade pickups within its DamageOuterRadius. Pickup→pickup chain unchanged (ChainDetonationRadius). Cascades + kill-credit-to-thrower both work. See GrenadeSystem.md.
- Pistol ADS (auto-on-draw zoom) — SWELL FIXED via first-person rendering (2026-07-05).
ASLCharacterBasector:FPMesh/FPWeaponMesh/FPSidearmMesh→FirstPersonPrimitiveType::FirstPerson.ASLPlayerCharacterctor:Camera->bEnableFirstPersonFieldOfView = true,FirstPersonFieldOfView = 95Config/DefaultEngine.ini:r.FirstPerson.Enabled=True(read-only cvar → needs an editor restart).
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++:
The sidearm mesh missing this flag was the actual bug — pistol = sidearm = the only unflagged FP mesh.
(matches base FOV so arms are unchanged at rest).
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).
- Pistol muzzle flash — socket fix + FP-render helper (2026-07-05, C++ compiled). Two bugs, both from the
- Muzzle socket misplaced.
SK_Pistol'sMuzzlesocket (parented to theBarrelbone) was at - FX didn't inherit first-person rendering. A muzzle flash spawned via Niagara
Spawn System Attached
first-person-rendering switch surfacing pre-existing issues:
(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.
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 `SystemLink | FX`): 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 Attached → Spawn System Attached Matching View (delete any manual Set First Person Primitive Type node).
.gitignore— un-ignoredDocs/Screenshots/Site/so the curated gallery is version-controlled going
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 swap —BP_WeaponLibrary:Spawn System Attached→Spawn System Attached Matching View
(delete any manual Set First Person Primitive Type node).
2. Verify the muzzle socket — openSK_Pistol, nudge theMuzzlesocket 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. Commit — stage SPECIFIC paths, NOTgit add -A(the un-ignoredDocs/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 drop —
ASLCharacterBase::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
ISLDamageableinterface USLGameplayAbility_Fire::ExecuteFire_ListenServerHost) now route non-pawnECC_WeaponTracehits to
(Public/Interfaces/SLDamageable.h); the two authoritative fire sites (USLWeaponsComponent::Server_ProcessShots
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:
- Rename
USLGrenadeIndicator::Initialize→InitializeIndicator— clears C4263/C4264 shadowing of
UUserWidget::Initialize() (our feedback_uobject_function_name_clashes pattern). Verify BP callers via bridge first; update caller SLHUDWidget.cpp:103. Needs a rebuild.
- Auto-generate
CR_MC_TP_LeftHandIK— Control Rig to REPLACE the FABRIK LHIK on the TP body. Two Bone IK
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) withDoNotAttach, so it spawns atCueParameters.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):ExecuteEquipCuenow setsCueParams.Location/Normal. Robustness DONE:** equip cues'
burst-soundAttachPolicy→ AttachToTarget (both AR + Shotgun, via bridge). Full RCA:BugTracker.mdBUG-024. Also new reusable
SA_Default3Dattenuation (/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_FragThrowwas played 2D in a Local-Predicted
ability → listen-host heard every player's throw. Decision: thrower-only feedback (gate local). Done:
cleared the now-meaninglessSA_Default3DoffSC_FragThrow(bridge). PENDING BP STEP: gate the throw's
Play Sound 2Dnode behindIs 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 inAS_AbilitySet_Default.
GC_SL_Grenade_Frag_Explosioncue — item 4: exists;GameplayCue.Grenade.Frag.Explosionregistered.
- FP+TP throw montages (
AM_MC_FP_Frag_Throw,AM_MC_TP_Frag_Throw),DA_SL_Grenade_Fragfilled — item 3.
- Real frag mesh (
Frag/SM_Modern_Weapons_Grenade_01+ materials) + realBP_SL_GrenadeProjectile_Frag—
item 7. Bounce/explosion audio (SC_FragExplosion, SC_Frag_Bounce, tinks).
IMC_Defaultmapped (item 6),DA_DefaultLoadoutupdated.
- Beyond plan:
GE_SL_Grenade_Cooldown,CS_ExplosionShakecamera 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 mirroringASLAmmoPickup. 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
GrenadeAmount); (c) setDA_SL_Grenade_Frag.DroppedPickupClass = BP_SL_GrenadePickup_Frag; (d) test:
BP_SL_GrenadePickup_Frag (subclass ASLGrenadePickup, set bounce mesh + GrenadeData=DA_SL_Grenade_Frag
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):
- HUD grenade-count widget — DONE ✅.
WBP_SL_GrenadeIndicator(USLGrenadeIndicator subclass) binds
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.
- Rebuild — done (MP tested against live code).
- BUG-023 throw — VERIFIED ✅ via 2-player listen-server PIE: host AND client both spawn a real grenade;
both observers see both projectiles + explosions. BugTracker.md BUG-023 marked verified.
- REMAINING: radial-damage falloff check (full inside
InnerRadius→ 0 atOuterRadius) is the one
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, replicatedSidearmWeapon,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
USLWeaponsComponent— now poll-driven inBuildAnimSnapshots(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++ (
GetActiveFireWeaponindirection + actor-owned ammo, option A) acrossUSLWeaponsComponent+
USLGameplayAbility_Fire — committed. 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 allCarriedWeapons+ the sidearm (clears slots);
called from USLGameplayAbility_Death before OnDeathStarted (deterministic, authority).
ASLWeaponPickup::OnCollected/AcceptPickupbranch onWeaponData->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):
- Set the pistol pickup's display mesh —
BP_SL_WeaponPickup_Pistolstill shows the shotgun mesh (SCS
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).
- Remove weapon-drop nodes from the
BP_SL_MasterChiefOnDeathStartedoverride — C++ drops everything
now; leftover BP drops are redundant no-ops. Keep any non-drop death logic (ragdoll/score/etc.).
- Test: die → all carried weapons + the sidearm drop as pickups; pick up same pistol → ammo refill;
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):
- ⚠ REBUILD PENDING — 4 uncommitted C++ changes, not yet compiled. Close editor → build → reopen
ASLCharacterBase::GetActiveWeaponMesh()—BlueprintPure, view+slot-aware (FP/TP × sidearm/main).USLWeaponsComponent::OnSidearmActiveChanged(bool)—BlueprintAssignable, edge-detected fromFSLWeaponViewDriverBaseanim-layer swap fix (BUG-016) — targetedUnlinkAnimClassLayersinsteadSLGameplayAbility_Melee::GetWeaponData()→GetActiveFireWeaponData()(pistol-whip, #2).
(Live Coding won't register the new UFUNCTIONs/delegate for BP):
Attach fire FX to this. ApplyPistolFireEffects already works; optionally simplify it onto this.
UpdateSidearmMeshVisibility. For the reticle swap (#5).
of LinkAnimClassLayers(nullptr), so swapping the main weapon no longer drops the sidearm layer.
(Note: the controller's sidearm-redraw change was reverted to Started — net-zero diff there.)
- Pistol-whip melee (replaces auto-holster). Melee while sidearm drawn = pistol-whip: pistol stays
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.
- Cues — DONE ✅. Pistol-specific cues live: tags
GameplayCue.Weapon.Pistol.PrimaryFire/.Impact
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).
- TP crouch-fire anim (task #6). FP fire anim DONE. TP: firing while crouched stands the character up
bIsFiringare onAnimStateSnapshot.
— 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
- Reticle swap (task #7) — DONE ✅ (2026-06-15). C++:
OnSidearmActiveChanged(edge-correct) +
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.
- Verify after build: BUG-016 (pistol anims persist through Shotgun↔AR swap) + pistol-whip melee feel.
- Later: sidearm draw is INSTANT —
SidearmDrawDuration/SidearmDrawingare unused scaffolding until
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 +OnEndAbilityRemove,
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:
bIsSidearmDrawingrising-edge latch offSidearmActive(heldSidearmDrawDuration,
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 Blendnode'sIs Sidearm Activepin 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(bIsSidearmActive | bIsSidearmHolstering)` |
|---|
(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::DrawSoundand
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:
UpdateSidearmMeshVisibilitynow edge-detectsOnSidearmActiveChangedon 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 `active | holstering`; 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) + memoryproject_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.
MeleeImpactAnimNotify on FP + TP melee anims.
- C++ features: backstab multiplier,
MeleeImpactCueTag,OnMeleeWallHitBIE, 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:
DefaultUnarmedDataonUSLWeaponsComponent,GetEquippedWeaponData()
fallback, sway/lag/obstruction/scale all routed through it when unarmed.
FirstPersonMeshScalemoved from character to weapon data asset, re-applied on every weapon swap.
MuzzleSocketno-mesh warning fixed inResolveMuzzleLocation.
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'sFiringstate 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
Firingstate. 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 added — ABP_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 fromWeaponsComponent->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
TransformableComponentHandleharvest crash (caused by Possessable bindings + constraints) — lesson learned the hard way mid-session whenLS_TP_Shotgun_Idle2.uassetcorrupted 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, mirrorsECC_Visibilitybehavior). Channel landed onECC_GameTraceChannel1, aliased inTypes/SLCollisionChannels.h.
- Capsule ignores
ECC_WeaponTrace(Custom collision preset onBP_SL_MasterChief). Physics Asset bodies onSK_MasterChiefblock it via the SkeletalMeshComponent's Custom collision preset.
- C++ swap: all four trace call sites moved from
ECC_Visibility→ECC_WeaponTrace(Server_ProcessShotsline trace + sphere sweep,ExecuteFire_ListenServerHostline trace,TracePredictedHitline trace).
ESLBoneGroupenum (Body/Head/Arms/Legs),FSLDamageMultipliersstruct onFSLWeaponFireMode(defaults: Body 1.0, Head 2.0, Arms 0.75, Legs 0.75),BoneGroupAnchorsmap +GetBoneGroup()parent-chain walk onASLCharacterBase.
- 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:
RebuildSlotsadds the equipped weapon first (guarantees it's at container index 0 — no reorder pass needed)
HandleEquippedWeaponChangedcallsRebuildSlots(notUpdateActiveSlot) — ensures fresh ammo values at equip time
AddAmmoon the equipped-weapon path callsSetCurrentAmmoafterSetNumericAttributeBaseto broadcast the delegate (bypassesPostGameplayEffectExecute)
- See
Docs/WeaponsSystem.md→ Ammo Display for the full architecture
#Queued next (in order)
- Melee — ✅ done.
- Weapon Clipping — ✅ done (FP). Optional leftover: AR FP ABP retract additive + TP eval (likely ignore).
- Secondary Weapon — Left-Hand Sidearm — IN 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).
- Melee polish — minor remaining: unarmed melee anim, SFX assets, camera-shake asset assignment.
- 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. SeeDocs/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 cosmetics —
BP_GA_SL_Shotgun_PrimaryFire → OnLocallyPredictedShotFiredstill 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 sendingSLTags.Events.Weapon.ShellEject. AddShellEjectSocketto 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
| Doc | When to read |
|---|---|
Docs/Shotgun.md | Full spec — data asset values, tag names, asset locations, build order, authoritative test checklist |
Docs/WeaponsSystem.md | How the weapon system works end-to-end (includes "Authoring a Fire Cue" contract) |
Docs/WeaponFireAbility.md | Fire ability internals |
Docs/FireAbilityNetworkTesting.md | PIE procedure for verifying multiplayer correctness of the fire flow |
Docs/WeaponEquip.md | Equip ability pattern |
Docs/Progress.md | Full 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)
bIsFirstPersonavailable onAnimStateSnapshotfor FP/TP branching inside shared ABPs
- Fire cosmetics still cloned from AR (
OnLocallyPredictedShotFireduses 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)
#Assualt → Assault 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.<FirstPerson | ThirdPerson>.<Weapon>`: |
|---|
SLGameplayAbility_Equip.hdoc comment updated
Docs/Shotgun.mdCDO setup section updated
Config/DefaultGameplayTags.iniis the source of truth (was already correct)
#What NOT to Touch This Session
USLGameplayAbility_FireC++ — pellet loop is done, don't re-open it
- Weapon swap HUD — deferred, not part of this branch