Spec (contract of record): docs/specs/2026-07-10-ecs-debugger-redesign.md Visual contract: docs/specs/ecs-debugger-redesign-mockup.html (open in browser; Design notes = 12 ideas) Mode: Fable end-to-end, one-shot campaign, phased with hard gates. Per-phase plan is written into this file at each phase start; the spec is not restated here.
| Decision | Outcome |
|---|---|
| Routing | Fable implements all phases; spec-first; no monolithic upfront plan |
| Archetype home | CkEcs (CkFoundation) — framework primitive; debugger/overlay consume |
Typed CK_DEFINE_ARCHETYPE struct |
In-campaign, last phase (B), C++ only |
| HAS-rollup boundary | Internals only; stops at primary-entity boundaries |
| AS authoring | Primary path for game archetypes; Get_FeatureIds is BPNE (TArray-default gotcha); asset-registry discovery |
| Perf | Feature-flag bit cache (registry-ctx table, EnTT signals, cvar-gated) + incremental model; steady-state = zero O(n); baseline trace in Phase 0 |
| Today-parity | Fold + group are toggles; classic tree reachable |
- EnTT 3.16.0 (vendored,
CkThirdParty/entt-3.16.0) per-storageon_construct/on_destroysignals — verify availability given the globalin_place_delete=truetrait (CkHandle.h:71-77) (Phase A). Fallback: hook feature Utils' Add paths. - Uniform member-type→Cast trait for typesafe handles (Phase B). Fallback: add one trait template in CkEcs.
- Slate SVG rasterizer vs stroke-based SVGs — visual check in editor (Phase 0 gate). Fallback: stroke→outline conversion pass.
- Large-map perf baseline needs a real map / stress gym — coordinate with maintainer (Phase 0 gate).
- AS
asset X of <AS-defined subclass>+defaultctor ergonomics — Phase A test asset.
| Phase | Scope (see spec §6) | Status |
|---|---|---|
| 0 | Icons + style + inspector metadata + perf baseline | IN PROGRESS |
| A | Feature-flag cache + archetype registry (CkEcs) | pending |
| 1 | Classification/rollup/names/query logic + incremental model | pending |
| 2 | Tree rework (fold/coalesce/badges/identity) + perf gate | pending |
| 3 | Query tokens + rail + pinned/recent | pending |
| 4 | Overview archetype map + Archetypes lens | pending |
| 5 | Activity feed + churn flashes | pending |
| B | CK_DEFINE_ARCHETYPE typed struct + TryCast |
pending |
Gate for every phase: toolbox build green (CkAuto/UnrealToolbox.exe, never raw Build.bat),
debugger fully usable, phase log updated here. Phases 1/A/B add automation specs.
Editor must be closed for builds (PreToolUse hook enforces).
- ✅
Resources/Icons/*.svg— 23 monochrome white glyphs (geometry = mockupICtable 1:1; stroke-based — see unknowns re: rasterizer). - ✅
FCkDebuggerStyle— content root → pluginResources/(plugin NAME isCkDebugger);CreateIconBrushesregistersCkDebugger.Icon.<Id>viaFSlateVectorImageBrush; publicGet_IconBrush(FName)resolver (unknown id → Slate no-brush). - ✅ Inspector metadata:
ICkDebuggerComponentInspector_Base::Get_IconName/Get_FeatureColor(defaulted virtuals — zero behavior change);FCkDebuggerInspectorMetadata+ registryGet_AllMetadatapropagate both. Deferred to Phase 2 (when badges render): per-inspector overrides across the 40 inspectors + InspectorFilter curated-color migration. Decision: icons live inFCkDebuggerStyle(this plugin's documented brush home), NOT CkFoundation's CkEditorStyle — revisit only if the overlay needs the same glyphs. - ⬜ Perf baseline: Insights trace with debugger open on the largest available map — needs a human/PIE session, coordinate with maintainer. Record numbers + trace path here.
- Gate status: ✅ toolbox build green (
Result: Succeeded, Development, 2026-07-10); ✅ editor-boot smoke via--test --test-pattern Timer→ 36/36 passed (brush registration runs at module startup, so the boot exercises it). One build iteration:IPluginManager::Get()→ LNK2019 (no Projects dep) → swapped to CkCore'sUCk_Utils_IO_UE::Get_PluginsDir(<folder name>), the CkEditorStyle pattern. ⬜[EDITOR-VERIFY]icon render check below; ⬜ perf baseline (item 4).
[EDITOR-VERIFY] Phase 0 icon check (agents cannot PIE):
- Build, open editor, console
ck.EcsDebuggerto open the tab (Tools menu → Debug section). - Temporary check until Phase 2 renders badges: in Widget Reflector or a scratch widget,
confirm brush
CkDebugger.Icon.Timerresolves and draws (not empty). Alternative: wait for Phase 2 where every tree row exercises the brushes; then this check is subsumed. - If glyphs render blank/garbled → stroke-support gap in the SVG rasterizer → run the stroke→outline conversion fallback (unknowns above).
Inspector section headers now render each inspector's feature glyph + color:
SCkDebug_InspectorPanel gained optional IconBrush/IconColor (nullptr = unchanged),
the ECS inspector panel passes them on inspector-titled headers, and 27 inspectors
declare Get_IconName/Get_FeatureColor (colors = mockup FEAT table). Commit 1a5aa3e,
gate: build green + Timer 36/36. [EDITOR-VERIFY] PASSED 2026-07-10 — user screenshot
confirms crisp glyphs (Timer amber clock, Network blue globe): stroke-based SVGs rasterize
fine; no outline-conversion fallback needed. Phase 2 may lean on the pipeline freely.
Bonus fix (b46fd5e): the long-standing garbled collapse indicator — text chevrons
(▾/▸) in the shared SCkDebug_InspectorPanel + SCkDebug_ExpandableColumn render as
unknown-glyph boxes — replaced with engine Icons.ChevronDown/Right SVG brushes; every
debugger composing these widgets is fixed. Follow-up (user-visible text glyphs elsewhere,
NOT yet fixed): SM debugger ▶ play/resume buttons, EQS "View ▾" label, GOAP graph ▸ strips.
Remaining without glyphs (need new SVGs or a mapping decision, Phase 2): EntityInfo,
Relationships, DynamicFragments, EntityCollections, Tween, Physics, Shapes, OverlapBody,
Resolver, AStar, UI, MontagePlayer, AnimPlans.
Recon resolved (all cited first-hand):
- ✅ EnTT sinks:
registry.hpp:1022/:1070(on_construct/on_destroy<T>());mixin.hpp:79-99publishes destruction on the in-place branches → tombstone trait does NOT disable signals. Unknown #1 CLOSED. FCk_Registry= non-owning view; raw entt registry viack::registry_table::Resolve(FCk_RegistryHandle)(public,CkRegistry_SlotTable.h:61);EnttRegistryType = entt::basic_registry<FCk_Entity::IdType,...>(:17). Ctx pattern:namespace ck { struct FCtx_X {...}; }+SetContext/GetContext/TryGetContext.- Handle→registry:
Get_Registrywas RENAMED (view semantics,CkHandle.h:273) — resolve registry via the handle's registry view orUCk_EcsWorld_Subsystem_UE::Get_Registry(). - Specs home:
CkTests/Source/CkTests/Private/UnitTests/*.spec.cpp(in-module.spec.cppare EMPTY placeholders — do not copy them). New spec needs touch+rebuild relink. - entt include style:
#include "entt/entity/registry.hpp".
Design (locked):
CkEcs/Public/CkEcs/DebugFeatureFlags/—ck::FCtx_DebugFeatureFlags(uint64 row per entity index, grows on demand; rows self-clear because each feature'son_destroyfires on fragment remove AND entity destroy). Global connector registry:ck::debug_feature_flags::RegisterFlag<TFragment>(FeatureId)(≤64 flags, ensure on overflow); per-bitFBitListenerinstances stored in ctx and connected as entt sink payloads (sinks don't take capturing lambdas). Enable(reg) = connect + O(n) seed viaview<T>(); Disable = disconnect + drop ctx. BP/AS surface:UCk_Utils_DebugFeatureFlags_UE(Request_Enable/Disable, Get_IsEnabled, Get_Flags→int64, Get_HasFeature, Get_BitIndex).- Feature→fragment registration lives DEBUGGER-side (CkEcsDebugger startup registers ~20 marker fragments; CkEcs stays feature-agnostic — tier rule: CkEcs (T2) cannot see T4 feature modules). Phase A ships mechanism + specs with test-local fragment types.
CkEcs/Public/CkEcs/Archetype/—FCk_ArchetypeDescriptor(Name, DisplayName, FeatureIds:FName[], RequiredLabel:FName — label MATCHING is consumer-side, CkEcs can't dep CkLabel (T2→T2 sibling, inverted dep), NamePattern, IconSvgPath, Color, Priority, optional native matcher fn),FCk_ArchetypeRegistry(static; Register/Unregister/Find/ Get_All/TryGet_BestMatch using flag-cache bits for FeatureIds),UCk_ArchetypeDefinitionUDataAsset (BPNEGet_FeatureIds),UCk_Utils_Archetype_UE(BP/AS surface).- AS test asset (authoring validation): CkTests
Script/— with the archetype phase gate. CK_DEFINE_ARCHETYPEtyped struct = Phase B, untouched.
RESULT — Phase A gate GREEN (2026-07-10):
CkEcs/DebugFeatureFlags/(4 files) +CkEcs/Archetype/(6 files) build green. One compile iteration: entt::any (ctx backing) requires copy-constructible payloads → move-only sink listeners/connections live behindTSharedPtr<FImpl>in the ctx struct.- Specs 4/4:
Ck.DebugFeatureFlags.{RegisterEnableSeedAndLiveBits, IdempotentRegisterAndUnknownQueries}+Ck.Archetype.{RegisterFindReplaceUnregister, MatchingViaFlagCacheAndNativeMatcher}(CkTests/Private/UnitTests). Behaviorally proves sinks fire under in_place_delete: seed scan, live bits, erase/destroy clearing. - AS authoring VALIDATED:
CkTests/Script/CkEcs/CkArchetype_AuthoringValidation.as(asset X of UCk_ArchetypeDefinition+FeatureIds.Add(n"...")) compiles clean at editor boot. Spec §3.3 amended: plain TArray UPROPERTY + imperative.Add()(the UCkDynamic_HandleDefinition pattern) — the BPNE accessor was unnecessary. - Feature→fragment flag registration (the ~20 markers) is Phase 1, debugger-side.
- Phase B note: native matcher param on
ck::archetype_registry::Registeris theCK_DEFINE_ARCHETYPETryCast hook.
Chunk 1 (this session): flag registration + enable wiring.
CkEcsDebugger/FeatureFlags/CkEcsDebugger_FeatureFlags.h/.cpp—RegisterAll()maps 10 verified marker fragments: Timer, Transform, SceneNode(Current), Probe, FloatAttribute(Current — on the ATTR entity), StateMachine(Sm_Params), Aggro(Current), Label(GameplayLabel), InteractionResolver, AudioTrack. Called fromFCkEcsDebuggerModule::StartupModule. Enable hook:FCkDebuggerModel_WorldContext::Refresh_EntityCache→EnableFor(registry view)(idempotent; ctx + sinks die with the registry on PIE end — no explicit teardown).- Remaining flag candidates: Inventory, Objective, Vfx, Tween, Ism/IskmProxy, EntityTag, Net, ActorBridge, Camera (verify marker fragment + include per feature).
Chunk 2 — DONE (2026-07-10, gate green: build + editor boot + Ck.DebugFeatureFlags 2/2).
Test_Entity → bit tests. PARITY VERDICTS (all cited first-hand — the contract for any
future wiring):
CK_DEFINE_HAS_CAST_CONV_HANDLE_TYPESAFEgeneratesHas=Has_All<every listed fragment>(CkHandle_TypeSafe.h:195-203) — a single bit is parity ONLY when the listed fragments are added/removed atomically with the registered marker.- WIRED (5) —
Get_FeatureFlagIdoverrides: Timer (Has_All<Current,Params>, both added in one construct lambda CkTimer_Utils.cpp:58-59, no individual removal); Transform (single-fragmentHas_All<FFragment_Transform>); SceneNode (Has_All<SceneNodeParent,Current>, co-added DoAdd :143-145, co-removed ONLY in Request_Detach :229-230); Probe (Has_All<Params,Current>, co-added :30-32); InteractionResolver (Has_All<Params,Current>, co-added :32-33). - DO-NOT-WIRE: FloatAttributes (
Has_AnyOWNER-side record — bit is on the attr sub-entity); StateMachine (Has_Anyover 7+ Sm fragments); Aggro (Has_Any<Aggro_Current,AggroOwner_Current>); Audio (Has_Any<AudioTrack_Current,AudioDirector_Current>). Their flags stay registered for classification/rollup only. - Chunk-1 defect fixed: Transform flag was registered on
FFragment_Transform_Params— a ParamsData ALIAS never added to any entity (real pool =FFragment_Transform, CkTransform_Utils.cpp:41). The bit read permanently zero. Re-pointed. - Implementation: base
Get_FeatureFlagId()(default NAME_None = legacy) with the parity contract in its doc comment; metadata propagates it; filter-model entries resolve id→bit at construction;Test_Entity+Test_Entity_IsExcludedbit-test when the entity's registry has the cache enabled, else instantiate via registry (legacy byte-for-byte when cache off). Bonus: exclusion token→inspector-ID substring matching (entity-independent) precomputed per exclusion-set change instead of per entity per pass (DoRebuildExclusionMatches), identical union semantics.
Chunk 3 — DONE (2026-07-10, gate green: build + Ck.EcsDebugger.Classification 2/2): classification + HAS-rollup as pure logic over the bit cache.
CkEcsDebugger/Classification/CkEcsDebugger_Classification.h/.cpp—BuildTable(internal-set)snapshots flag bits (structural mask = Transform|Label);Classify(ownBits)= internal iff exactly one non-structural bit AND it's in the internal mask (spec §3.1 literally: structural-only entities are PRIMARY);ComputeRollups(bits[], ownerIndex[])= each internal's X counts toward its nearest PRIMARY ancestor, walks through internal chains, stops at primaries, cycle-guarded (path-compressed memo; owner cycles contribute nothing — snapshot-restore history). Pure arrays in/out → Phase 2 feeds cached entities, specs feed synthetic forests.UCkEcsDebuggerSettings::InternalFeatureIds(Config, seeded: Timer, SceneNode, Probe, Float/Byte/IntegerAttribute, CueRelay).- Flags: +ByteAttribute, +IntegerAttribute (
FFragment_<X>Attribute_Current, verified single-fragmentHaslike Float). 12 registered total. - Specs (in-module,
Private/Tests/, overlay-module pattern):Ck.EcsDebugger.Classification.{SignatureAndInternalSet, RollupStopsAtPrimaries}— discovered on first link (no relink trap this time). - Known gaps (recorded): (a) features without registered flags are blind spots —
an entity carrying only unregistered features + one internal feature misclassifies
internal; mitigation = keep extending RegisterAll (remaining candidates in chunk 1
note) + the set is user-editable data. (b) CueRelay is an ACTOR (
ACk_CueRelay_UE), no marker fragment — its internal-set entry is inert until a marker exists.
Chunk 4 — DONE (2026-07-10, gate green: build + Ck.EcsDebugger 5/5).
- Query tokenizer
CkEcsDebugger/Query/(spec §3.5): Parse (incomplete tokens DROP, unknown keys/malformed ids → fuzzy), FFeatureTokenTable (prefix, id+display-name → bits), Matches (has: own∪rollup; is: own; is:primary/aux; net: prefix-forgiving; id:; arch: substring; fuzzy via ck::fuzzy), Get_InferredArchetypeKey ("Base#signature"). 3 specs. NOTE:EQueryNetMode(engine ENetMode collides). - O(1) churn detection: CkEcs
debug_feature_flags::Get_Revision(sink-bumped counter) + debugger flag_TreeEntityon FFragment_LifetimeOwner (underscore prefix = infrastructure → structural in classification, skipped by badges/tokens). World model polls revision in IsCacheDirty → tree LIVE-TRACKS spawns (it never did!). - Incremental world model: Refresh_EntityCache diffs vs previous set (Get_LastAdded/Removed + OnCacheDiff broadcast — Phase 5's feed source); dead-world refresh drops handles without broadcasting.
- Tree: nodes cache DebugName/CleanName/DisplayText at creation (rows previously derived names PER ROW PER FRAME via bound attributes); ApplyCacheDiff incremental add/remove (stable TSharedPtr identity); RecomputeNodeSignatures per churn (bits, classification, rollup, registry-first ArchetypeKey, NetRole); selection restore + auto-expand now gated to full rebuilds/filters (were about to fire at 10 Hz). Manual Refresh button = ForceFullRefresh (re-derives names).
Phase 2 — DONE (same gate). Tree presentation layer:
- Fold internals (settings default + toolbar toggle + per-owner "+N" chip — ASCII on purpose, decorated glyphs = tofu). Filter-overrides-fold rule. ExpandToReveal auto-unfolds the owner on external navigation to a folded internal.
- Sibling coalescing: same-ArchetypeKey runs ≥ threshold (settings, default 5) → synthetic "Base xN" group rows; stable identity via GroupNodeCache keyed (owner ptr, key), pruned on node removal (ptr-reuse guard); group click never clobbers entity selection; context menu on group = Copy all N names/IDs.
- Identity glyphs (internal = feature icon, primary = Cube) + IS/HAS badge strip (solid = own, hollow+count = rollup; cap 6 + "+k"); feature visuals from inspector metadata (flag-wired 5) + manual rows for flag-only ids (SM/Aggro/Audio/Label/attrs).
- Status bar: "Entities: N (P primary · I internal)". Deviation from spec §5.3: no 30-row "Show all" cap inside expanded groups — STreeView virtualization already bounds row materialization; revisit only if group expansion measures hot.
Phase 1 leftovers folded forward: computed names §3.4 partially (cleaned names cached; archetype DisplayName + role-name pipeline lands with Phase 4's lens); refresh time-budget (§5.4) not needed yet — steady state is zero-O(n) by revision gate.
Phase 3 (85cbc68): Filter+Highlight inputs parse the §3.5 grammar and evaluate
against cached node signatures (plain text = fuzzy, unchanged UX); "?" help button;
feature rail (icon chips, include-any semantics, own∪rollup); Pin/Unpin context menu +
Pinned/Recent sections above the tree (Recent = last 5 selections, panel-tracked).
Rail/filter suppress folding so matches stay reachable. Feature visuals shared via
Presentation/CkEcsDebugger_FeatureVisuals. Deviation: rail is include-only (spec's
rail-exclusion composes through the existing exclusion popover instead).
Phases 4+5 (5327429): Archetypes page (hero count + per-archetype cards,
registered-first with [GAME] tag + signature badges; click → arch:<name> into the
Filter bar via new page-context RequestEntityFilter + SCkDebug_DualSearchBar:: Set_FilterText; 1 Hz rebuild only while visible). Activity page (spawn/destroy feed
from OnCacheDiff, 200-event ring, click-to-select spawns, world-switch clears).
Spawn rows flash the tree status dot green 0.6s. Deviations: no sparklines/family
bars; no unseen-count tab badge; SM-transition rows not tapped (stretch item).
Phase B: CkEcs/Archetype/CkArchetype_Typed.h — CK_ARCHETYPE_BODY +
CK_DEFINE_ARCHETYPE(Struct, "Name", Members...) (≤8): generates all-or-nothing
TryCast from the member-name==feature-name==Utils-class convention (purely textual —
utils resolve at expansion site, CkEcs stays feature-agnostic; the open cast-trait
question from §3.3 resolved as unnecessary) + deferred descriptor registration
(EndOfEngineInit) with TryCast as the native matcher. USTRUCT optional (spec's sketch
implied required — relaxed). Spec: Ck.Archetype.TypedTryCastAndAutoRegistration
over real Transform+Timer fragments.
Polish pass (post user PIE test, 2026-07-10): three defects from first hands-on:
(1) Fold/Group toggles + rail chips rendered as grey squares — FAppStyle
"ToggleButton" doesn't exist in this engine, so Slate fell back to a
default-constructed FCheckBoxStyle; replaced with own CkDebugger.ToggleChip
(rounded chip, accent fill+outline when checked). (2) Archetypes cards flickered —
ClearChildren()+recreate every 1 s violated the spec §4 stable-identity contract;
now a key→widget card cache updates counts in place and only re-slots on key-set/order
change, with a deterministic sort tie-break (unstable TArray::Sort over equal counts
would otherwise reorder every tick). (3) Cards restyled to the mockup shape:
CkDebugger.Card rounded hover-outline button, tinted icon well, GAME pill, 16 pt
hero count + "instances", badges row; hero line adds archetype count. Gate:
build + 5/5 Ck.EcsDebugger specs.
Polish pass 2 (second PIE round, 2026-07-10): (1) Archetype cards became
multi-select toggles: repeated arch: terms now OR-compose in the query (an entity
has exactly one archetype — AND was always empty); tokenizer honors double quotes so
multi-word tokens (arch:"UnrealComponent: BackWall") survive whole; cards derive
checked state from the live Filter text (context gained GetEntityFilter) and
add/remove their token with word-boundary-safe string surgery. +4 spec cases.
(2) Uniform grid: fixed 200 px card width + name ellipsis. (3) Cube fallback replaced
by dominant-feature glyph (first flagged feature in the signature, feature-colored);
registered bespoke icons validated at bucket time. (4) Distinct attribute icons:
new AttributeFloat/Byte/Integer/Vector SVGs, per-type hues, inspectors re-pointed;
VectorAttribute flag registered (same TUtils_Attribute machinery = same parity).
(5) Rail narrowing now auto-expands to reveal matches and greys non-matching
ancestors (IsNarrowingMatch on tree nodes, also applies to Filter queries).
(6) Pinned/Recent headers are label+rule separators with spacing. Known gap:
features without registered flags (Message, CueRelay, EntityScript, relays) still
have no glyph — needs marker-fragment flags per feature (Phase 1 tracker list).
Polish pass 5 (fourth PIE round, 2026-07-11): (1) Rail chips were stretching to
the rail's full width — the grouped rail's SVerticalBox slots default to HAlign_Fill
and the AutoWidth column is sized by the widest group label; chips now HAlign_Left.
(2) Full feature-coverage audit, per the maintainer's "count the Ck*_Fragment
files" method: 127 fragment headers across ~55 CkFoundation modules inventoried
(struct decls + using aliases + Add<>/AddOrGet<> call sites). Result: 32 new
flags, every one keyed on a fragment verified to be added at feature-Add() time —
EntityExtension, UnrealComponent, Snapshot (SaveKey), TagSet, EntityTag,
RotatorAttribute (+ bespoke AttributeRotator glyph), CrowdAgent, Grid (+ bespoke
glyph), Marker, Sensor, RaySense, Velocity, Spline, InteractSource, InteractTarget,
Inventory (Params alias existed after all), Item, Team, Player, Projectile
(tag-keyed), ResolverSource, ResolverTarget, GeometryCollection, AnimPlan,
MontagePlayer, VatProxy, RenderTarget, WorldSpaceWidget, CameraShake, Vfx (CkFx
leaf), AudioDirector, Sfx. Existing "Vfx" flag renamed VfxCue (it keys on
CkVfx's cue coordinator; CkFx's leaf Niagara wrapper now owns "Vfx"). 56/64 flag
bits used — the next batch this size needs the cache widened past one uint64 row.
Deliberately unflagged with reasons (recorded in FeatureFlags.cpp): Cue/Messaging
(record/transient-entity based), Nav (request-driven), AStar (rides on Goap
planners), EntitySpawner (transient PendingSpawn), Variables/Dynamic (no single
static type), Shapes, sub-features (Homing/Ballistic/CameraLayer/AnimAsset), owner
sides, renderer singletons, LagCompensation (revisit), infrastructure. (3) Rail
regrouped into 12 clusters (Core/Tags/Attributes/AI/Spatial/Motion/Gameplay/Combat/
Animation/Rendering/Audio/Other), ~53 chips; new features' glyphs are hand-picked
semantic matches from the 148-icon general pool (People, Backpack, Coin, Sword,
Shield, Tv, FilmReel, …) with per-feature hues. CkEcsDebugger.Build.cs gained 10
module deps (Chaos, Crowd, Fx, Projectile, RaySense, RenderTarget, Snapshot,
Spline, UnrealComponent, Vat). Inspector fast-path (Get_FeatureFlagId) remains
unwired for the new batch, same as batch 2.
Polish pass 4 (third PIE round, 2026-07-11): (1) Overview became the mockup
dashboard: hero count + live 60-sample sparkline (new volatile SLeafWidget
SCkEcsDebugger_Sparkline), population-by-family bars (descriptor gained _Family
in CkEcs; internals/dominant-feature/Other for unregistered), top-12 archetype cards
with per-card sparklines + family labels, singletons list with EntityRef pills; the
hierarchy graph moved to a "Graph" tab; Activity tab gained the unseen-count badge
(Get_BadgeCount on the page interface). Aggregation extracted to shared
Presentation/CkEcsDebugger_ArchetypeAggregation (keys/glyphs/colors/family resolved
once for both pages); arch-token toggle helpers moved into the query namespace.
(2) Selection highlight was unreadable (engine TableView.Row saturated fill) —
own CkDebugger.TableView.Row style with translucent accent selection, applied to
tree + activity rows. (3) Icons got color: 16-hue curated palette, stable
FCrc-hash-assigned per archetype key; registered descriptors' explicit _Color
wins; feature glyphs keep feature hues. (4) Rail: 10 new feature flags (Objective,
Vfx=VfxCue, Camera, Goap=Planner, Eqs=QueryState, IsmProxy, IskmProxy, ActorBridge=
OwningActor, Tween, EntityCollection — 24 bits total), 4 new glyphs
(Goap/Eqs/Tween/EntityCollection), chips clustered under tiny group labels
(Core/Attributes/AI/Gameplay/Rendering) in a scrollable rail. Known gap: Inventory
has no single always-present marker fragment — flag deferred.
Polish pass 3 (2026-07-10): (1) Archetype grid density is user-adjustable:
ArchetypeGridColumns setting (persisted, Project Settings → Ck ECS Debugger) +
2..6 chips in the page header; SWrapBox → SUniformGridPanel with explicit
(col,row) slotting — true uniform cells, re-slot on density change, card cache
reused. (2) General icon library: 148 glyphs in Resources/Icons/General/
(combat/items/nature/creatures/buildings/vehicles/tech/food/people/symbols/
office/tools + Rewind99 flavor: VhsTape, Register, Shelf, FilmReel, Popcorn,
Remote …), generated in the house 16x16 stroke style. Icon registration is now a
directory scan (no hand-list); General/* forms a sorted pool exposed via
FCkDebuggerStyle::Get_GeneralIconPool. Card icon resolution:
registered-bespoke → dominant-feature glyph (inferred only) → FCrc-hash pick
from the pool (stable per key) → Cube only if the pool is empty. Registered
archetypes may name any pool glyph directly via descriptor IconSvgPath.
Spec: docs/specs/2026-07-11-debugger-world-interaction.md.
Six workstreams (A–F), all code-complete in one session; gate = toolbox build + Ck.EcsDebugger.*
specs + the spec §3 [EDITOR-VERIFY] table for the maintainer's next PIE round.
| WS | What shipped | Key files |
|---|---|---|
| A | Viewport-picker selections now broadcast on the selection-sync bus (they were applied Direct and never re-broadcast — the confirmed root cause of "world-picked entity doesn't show up in Crowd/GOAP"). Crowd/GOAP receives scroll-into-view + 1.2 s fading row flash; Crowd one-shot re-centers the 2D map via new Request_FrameSelectedAgent VM signal. |
CkDebuggerModel_ViewportPicker.cpp (broadcast), both *_AgentListPanel.*, CkCrowdDebugger_ViewModel.h, SCkCrowdDebugger_ViewportPanel.* |
| B | Selection gizmo is now a persistent PMG RGB triad (FCkDebug_PmgGizmoSet, pooled procmesh, moved per tick) replacing per-tick DrawDebugTransformGizmo — kills the refresh-gate blink and per-tick line re-batching. Transform + SceneNode inspectors swapped (SceneNode also re-homes the parent gizmo on re-parent); Camera inspector untouched. NOT built on PMG Pivot: it is a Composite whose child arrows bake world transforms at setup (CkPmg_Processor_DirectionalShapes.cpp:261-310) — moving the parent moves nothing; the set creates/re-aims 3 Arrows directly. |
CkDebuggerCommon/Markers/CkDebug_PmgGizmoSet.{h,cpp}, CkInspector_Transform.*, CkInspector_SceneNode.* |
| C | ck::DebugViewportView (CkDebuggerCommon) now owns ejected-detection / deproject / view-camera-location — picker + overlay duplicates re-pointed. ck::DebugFocus::Focus_Entity = editor-F for entities: bounds (ISM mesh → direct actor → 1 m box at transform → recursive actor) framed by pulling the ejected editor camera back along its current view dir (manual fit — no engine FocusViewportOnBox dependency). Wired: F key + context menu on ECS tree, Crowd list, GOAP list; Focus button in Crowd detail. Possessed = deliberate no-op. |
CkDebuggerCommon/Navigation/CkDebug_ViewportView.{h,cpp}, CkDebug_Focus.{h,cpp} + panel wiring |
| D | Crowd 2D map: player pawn chevron + view-camera FOV wedge (collector samples pawn/camera pose; ejected camera included); PlayerProxy status finally assigned (lineage match vs the possessed pawn's entity). RMB on the map = command (auto-arms debug override), RMB-drag still pans (Slate drag-threshold defer), MMB pan unchanged, LMB-command branch removed. In-world: FCkCrowdDebugger_WorldCommandProcessor (passive pre-processor, never consumes — consuming would wedge the viewport's RMB camera-look capture) — RMB-click on ground while ejected commands the selected agent + 1.5 s PMG ring ping. "Take Control" button kept as explicit release. |
CkCrowdDebugger_DataCollector.*, SCkCrowdDebugger_ViewportPanel.*, CkCrowdDebugger/Input/CkCrowdDebugger_WorldCommandProcessor.{h,cpp}, SCkCrowdDebuggerWindow.*, SCkCrowdDebugger_AgentDetailPanel.cpp |
| E | ECS inspector gets a pinned owner-chain breadcrumb (root › … › selected; SCkDebug_EntityRef pills, depth-capped 8, transient root omitted, rebuilt only on selection change). Crowd rows show a muted → Owner name (pre-sampled in the collector — no per-paint ECS walks); Crowd detail gets an Owner pill row. GOAP already lists owners. |
CkDebuggerPanel_Inspector.*, CkCrowdDebugger_Types.h, SCkCrowdDebugger_AgentListPanel.cpp, SCkCrowdDebugger_AgentDetailPanel.cpp |
| F | Picker: trace hits on ISM components resolve to the proxy ENTITY via instance-transform match against the marker snapshot (FHitResult.Item → GetInstanceTransform → nearest IsmProxy-carrying entry ≤ 1 m) — deliberately transform-based, no version-sensitive instance-id↔index component API and no renderer-internal poking. Hover on mesh-resolvable entities draws a one-frame bounds box (DebugFocus::Get_EntityWorldBounds). New "Meshes First" toolbar toggle (persisted UCkEcsDebuggerSettings::PickerMeshesFirst) suppresses diamonds for geometry-pickable entities via the marker SuppressedEntityNums mechanism — still gathered, still pickable. |
CkDebuggerModel_ViewportPicker.*, CkEcsDebuggerSettings.h, CkDebuggerWindow_Main.cpp |
Deliberate calls (don't re-litigate without new evidence):
- Gizmo alpha 0.85 / no wireframe lines; entity label stays
DrawDebugString(still gate-blinks — separate follow-up if it bothers). - In-world command processor is ejected-only and passive; if PIE shows an editor context menu popping on RMB command, the recorded fallback is consuming the down+up pair when gates hold at down-time (costs RMB-look only while an agent is selected).
- ISM resolve precision bound: co-located instances within 1 m are ambiguous (nearest wins); exact resolution via
GetInstanceIdForInstanceIndexrecorded as follow-up if it bites. - ISKM batched clusters: no per-instance resolve this phase — entities keep diamond picking (spec §4 limitation).
Recorded follow-ups: re-point CkCrowdDebugger_DataCollector's own ejected-yaw block onto DebugViewportView (still uses bIsSimulatingInEditor); ISKM bounds for focus (currently 1 m fallback box); consider PMG text label to kill the label blink.
[EDITOR-VERIFY] (maintainer PIE round): spec §3 table A1–F1 — sync flash + map ping; gizmo stability at low refresh caps + zero orphans after deselect/EndPIE; F-focus framing from all three lists (ejected only); player chevron/wedge; map + in-world RMB command (incl. no stuck camera-look, no context-menu conflict); breadcrumb + owner column; ISM mesh pick + hover box + Meshes First declutter.
Maintainer PIE-verified the world-interaction phase: A (sync) and E (owner linkage) passed; seven revision items came back (spec §5 has the full verdict table). All code-complete:
| # | Item | Root cause → change | Key files |
|---|---|---|---|
| R1 | In-world RMB command dropped (maintainer accepted): the editor viewport's context menu owns the ejected RMB click, so the passive processor's clean-click never fires usefully. Map-only commanding stays; the map command now draws the in-world PMG destination ring itself. | CkCrowdDebugger_WorldCommandProcessor.{h,cpp} deleted; window registration removed; ping + Destination local added to the map RMB branch; banner/tooltip strings say "map destination". |
SCkCrowdDebuggerWindow.{h,cpp}, SCkCrowdDebugger_ViewportPanel.cpp, SCkCrowdDebugger_AgentDetailPanel.cpp |
| R2 | Flat gizmo arrows read edge-on-invisible → real solids with bright outlines. | FCkDebug_PmgGizmoSet now builds 6 parts/gizmo (cylinder shaft + cone tip per axis, UCk_Utils_Pmg_BasicShapes); InDrawLines=true outlines bake as a second procmesh section at alpha 1 (FProcessor_Pmg_DebugShape_BakeLines) — retained geometry, moves with the part, cannot blink. Aim = FRotationMatrix::MakeFromZ(WorldDir) (both solids build along +Z; ECk_Plane_Axis::XY = identity, verified Get_PlaneAxisRotation). |
CkDebuggerCommon/Markers/CkDebug_PmgGizmoSet.{h,cpp} |
| R3 | Focus: force-eject when possessed; glide; 2× distance (tunable). | Possessed → queued focus + GEditor->RequestToggleBetweenPIEandSIE() + core-ticker poll until ejected (2 s expiry; FEditorDelegates::EndPIE clears the queued handle — handle contract). Framing now GetViewTransform().TransitionToLocation(...) (fork-verified EditorViewportClient.h:283/490 — the editor's own animated F). New cvar ck.Debug.Focus.DistanceScale (default 2.0) scales the FOV-fit distance. Get_CanFocus loosened to "ejected OR PIE exists". Tooltips updated in all three lists + detail button. |
CkDebug_Focus.{h,cpp} + 4 string sites |
| R4 | Chevron didn't rotate: collector sampled GetActorRotation().Yaw — orient-to-movement bodies don't follow the camera. |
Now GetBaseAimRotation().Yaw. |
CkCrowdDebugger_DataCollector.cpp |
| R5 | Mesh picking dead in ISKM stress gyms — TWO causes: (1) ISKM entities carry IskmProxy (CkIskmRenderer), not IsmProxy — every mesh gate was blind; (2) renderer ISMs run NoCollision (CkIsmRenderer_Processor.h:157 param-driven) so the visibility trace can never hit. |
DoIsMeshResolvable accepts UCk_Utils_IskmProxy_UE::Has; new analytic ray-vs-AABB candidate stage in DoPickAtRay (slab test w/ near-T) over _MeshPickBounds — per-tick cache built in DoRefreshMarkers from DebugFocus::Get_EntityWorldBounds (collision-independent; nearest-T competes with the trace hit). ISKM pick volume = the 1 m-box fallback (no Get_MeshBounds on IskmProxy utils yet — recorded CkFoundation follow-up). |
CkDebuggerModel_ViewportPicker.{h,cpp} |
| R6 | Transform missing from rail + has/is badges: deliberate exclusion in Get_BadgeFeatures(). |
Exclusion removed (Label stays excluded — the name column IS the label). Also enables the has:transform query token. Transform visual already existed via the inspector's metadata (CkInspector_Transform.h:11-12 + Transform.svg). |
CkEcsDebugger_FeatureVisuals.cpp |
| R7 | One icon rail needs scrolling → split across both flanks of the tree. | Build_FeatureRail(bool InRightFlank): whole groups greedily assigned to the lighter flank in display order (weight = chips + header); deterministic across both calls; each flank keeps its own scrollbox for short windows. Right flank slotted after the tree. |
CkDebuggerPanel_EntityList.{h,cpp} |
Deliberate calls: in-world commanding could return later behind a modifier key (e.g. Ctrl+RMB) if wanted — dropped per maintainer, not for technical impossibility. R5 keeps the physics trace as a precision assist (actor-backed meshes with collision); nearest-T between trace and box candidates is accepted ranking noise for a debug picker.
Recorded follow-ups (new): Get_MeshBounds (or bounds getter) on UCk_Utils_IskmProxy_UE
in CkFoundation for exact ISKM pick volumes / focus framing (1 m box today); per-row badge
strips near MaxBadges now spend one slot on Transform for most entities — revisit the cap if
rows start truncating badges that matter.
[EDITOR-VERIFY] (round 2): spec §5 re-verify queue — gizmo solids/outlines; focus
auto-eject + glide + distance feel (ck.Debug.Focus.DistanceScale); chevron rotation;
map-RMB world ping; ISKM hover/pick + Meshes First in the stress gym; two-flank rail;
Transform chip + badge.
Spec: docs/specs/2026-07-11-overlay-aggregated-focus-card.md. The focus card becomes the one-screen compressed union of all debuggers for one NPC.
| WS | What shipped | Key files |
|---|---|---|
| A — subtree aggregation | Build_EntityModel walks the focus entity + ALL lifetime descendants (BFS, visited guard) and emits one section per (provider × source). Sections carry SourceName (dim chip on the card for sub-entity sections), SourceEntityId (history bucketing — same field on two sub-entities must not share a trail), SourceOrder (stable sort tie-break — an unstable sort over equal priorities is the archetype-cards flicker bug all over again). Both the overlay AND the picker's reused cards get this for free (shared builder). |
CkDebugOverlay_Present.cpp, CkDebugOverlay_Model.h, SCkDebugOverlay_FocusCard.cpp |
| B — top-left + tall + quiet | PlateAnchor default → TopLeft; new PlateMaxHeightFraction (0.66 default) — the card strip is height-budgeted to the viewport fraction via a live MaxDesiredHeight attribute and CLIPS past it (hit-test-invisible root — a scrollbar would be uninteractable). Engine on-screen debug text (AddOnScreenDebugMessage / ck::Trace) is suppressed while the overlay is active and restored on deactivate (primary-owner-gated; pairing guarded by the ticker handle). |
CkDebugOverlay_Settings.h, SCkDebugOverlay_Root.{h,cpp}, CkDebugOverlay_Subsystem.{h,cpp}, picker Set_PlateLayout call |
| C — attribute filter | AttributeFilterPatterns (case-insensitive substring vs attribute label) + bAttributeFilterIsExclusion on the overlay settings; Get_PassesAttributeFilter consulted per row by all five attribute providers. Editor UI: "OVERLAY ATTRIBUTES" block in the ECS Debugger picker-toolbar popover (pattern text box + "Exclude listed" checkbox → SaveConfig, applies live). |
CkDebugOverlay_Settings.{h,cpp}, the 5 attribute providers, CkDebuggerWindow_Main.cpp |
| D — content | NEW Crowd provider (status incl. DEBUG OVERRIDE badge / speed vs max / active goal + distance while walking / neighbors + separation — mirrors the crowd collector's read set; module dep CkCrowd added). NEW VectorAttributes / RotatorAttributes providers (Float pattern clones). SM History + GOAP Cost fields flipped to default-enabled. All three new tags registered in the "All" layout (+ Crowd in "AI") + abbrev map (vATT/rATT/CRWD). |
Providers/CkDebugOverlay_Provider_{Crowd,VectorAttributes,RotatorAttributes}.{h,cpp}, CkDebugOverlay_Provider_{StateMachine,Goap}.cpp, CkDebugOverlay_Settings.cpp, CkDebugOverlay_Tags.cpp, CkEntityDebugOverlay.Build.cs |
Deliberate calls: clip-not-scroll at the height cap (interactive scrolling needs a hit-testable root — recorded follow-up if wanted); planned-path waypoints deliberately NOT on the crowd card (too verbose; the crowd debugger map owns that); aggregation cost is per-tick × descendants × providers — debug-only, revisit only if a stress NPC feels heavy.
[EDITOR-VERIFY] (spec §3): A1 sub-entity sections with dim source chips; B1 top-left,
≤2/3 height, on-screen text suppressed while active and restored after ck.DebugOverlay 0;
C1 filter edits (popover) apply live + persist; D1 Crowd/Vector/Rotator/SM-history/GOAP-cost
visible on an NPC.
PIE round defect (2026-07-11) — "overlay kills WASD/mouse" — root-caused + fixed:
3,194 identical label-CK_ENSURE fires in one session (log-verified, all inside the
overlay-active windows): the subtree BFS reached ability sub-entities owning UNNAMED
cooldown timers, and UCk_Utils_Timer_UE::Get_Name was an ungated
GameplayLabel::Get_Label — ~2 fires/frame, each capturing native+BP+AS stacks →
~45–80 ms frames → input felt dead; the phase's own on-screen-text suppression hid the
red spam (log was the only witness). Ruled out by evidence: gesture keys (log lines
absent), hit-testable widgets (HitTestInvisible end-to-end), focus/capture calls (none in
module). Fix at the source: CkFoundation cba35adf6 — Get_Name label-gates and
returns an invalid tag (unnamed timers are a designed state per Add); overlay provider
already handled invalid tags. Also defuses the identical latent storm in Gen-2
CkInspector_Timer. Regate: 35/35. Ship note: the overlay phase now spans TWO submodules
(CkGameplayDebugger 55c904f+3a41ea1 + CkFoundation cba35adf6), all local/unpushed.
Follow-up recorded: regression AutoTest for unnamed-timer Get_Name (CkTests-land,
other session's territory today). Error-strip idea REJECTED by maintainer — the
CkWatermark panel (pure Slate, Get_EnsureCount() live) already surfaces ensures and is
unaffected by the on-screen-text suppression; policy is fix ensures at source, not
re-display them.
PIE round 2 (2026-07-11) — two UX fixes (45a11de, gate 35/35): (1) selecting your
own possessed pawn put the PMG coordinate triad + floating label in the first-person
camera — new shared ck::DebugViewportView::Get_IsLocalPlayerSelf (possessed AND entity
resolves via owning-actor-recursive to local pawn/controller/attachments; always false
ejected) gates the Transform + SceneNode inspectors' in-world visuals; other entities'
gizmos still show while possessed, eject shows everything (mirrors overlay self-marker
suppression). (2) The aggregated card emitted one boilerplate INFO + T section per
unnamed sub-entity — providers can now opt out of subtree collection
(Get_CollectsFromSubSources, default true); EntityInfo + Transform opt out, feature
sections still aggregate from the whole subtree.
- 2026-07-10 (Fable): Spec + mockup written and reviewed (user + colleague ideas
incorporated: IS/HAS, archetypes, perf architecture). Campaign tracker created. Phase 0
implemented: 23 SVGs, style content root + icon brushes +
Get_IconBrush, inspector icon/color metadata plumbing (registry + base interface). Toolbox build launched. Docs commit todevpending user approval. - 2026-07-11 (Fable, world-interaction phase): Maintainer declared the prior polish passes
done and pivoted the campaign to world interaction (5 new asks). Design brainstormed +
approved (4 forks decided: Fable e2e; map+world RTS command; one-shot ejected-only focus;
trace+ISM picking). Spec
2026-07-11-debugger-world-interaction.mdwritten; workstreams A–F implemented (section above). Backlog untouched: pooling-interaction question, inspector fast-path wiring, flag-cache widening, perf acceptance. - 2026-07-11 (Fable, PIE feedback round 1): Maintainer's PIE verdicts: A + E pass; seven
revisions (spec §5, section above) — in-world RMB dropped (editor context menu owns the
click), gizmo → solid cylinder/cone triad with baked outlines, focus auto-eject + glide +
distance cvar, chevron aim-yaw fix, ISKM-aware collision-independent mesh picking (IskmProxy
blindness + NoCollision were the two root causes), Transform badge/chip enabled, icon rail
split across both tree flanks (the last two were the maintainer's "other comments").
Round shipped via ck-ship-dev (submodule
eb54323, gitlink bumped883ba47) after the maintainer's green verdict + ship instruction;ck-slate-toolsskill added (Slate pitfall catalog) with an index row in CkFoundation's CLAUDE.md. - 2026-07-11 (Fable, overlay aggregated focus card): Maintainer-armed goal — the focus card becomes the one-screen NPC debugger: subtree aggregation (sections from the focus entity + all lifetime descendants, source-labeled, per-source history), top-left anchor + 2/3-height budget + on-screen-text suppression while active, attribute allow/deny filter (settings + ECS-debugger popover editor), new Crowd/VectorAttributes/RotatorAttributes providers, SM history + GOAP cost on by default. Section above has the file map.
- 2026-07-11 (Fable, overlay PIE round — ensure-storm fix): Maintainer's first PIE pass
hit "overlay kills WASD/mouse". Root-caused from the PIE log (defect block above):
unnamed-timer label ensures, 2/frame, storming under the new subtree BFS with the red
spam hidden by the phase's own suppression. Fixed at the source in CkFoundation
(
cba35adf6,Get_Namelabel-gates); regated 35/35. All overlay-phase commits remain local/unpushed across BOTH submodules pending the maintainer's re-verify. - 2026-07-11 (Fable, overlay PIE round 2): Two maintainer reports fixed (
45a11de, 35/35): possessed self-gizmo suppression (newGet_IsLocalPlayerSelfin DebuggerCommon, gating Transform + SceneNode inspector visuals) and focus-only EntityInfo/Transform on the aggregated card (Get_CollectsFromSubSourcesprovider opt-out — kills the per-sub-entity INFO/T boilerplate the maintainer screenshotted). Error-strip proposal rejected (watermark owns ensure visibility). - 2026-07-11 (Fable, overlay PIE round 2b): Overlay now survives PIE restart
(
441d4b6, 35/35): the master cvar persists across sessions but the per-LP subsystem is recreated and was only edge-triggered by the cvar CHANGE callback —PlayerControllerChangednow level-triggers activation when the cvar is already on (primary-gated, idempotent; ini-set cvar auto-activates the first session too).