UI & Online · Updated 2552.07.26.14.59
MenuKit Extraction Plan — CommonUI Menu Framework as a Fab Product
Drafted 2026-07-16. Working name MenuKit (final name + class prefix = open decision 1). Strategy context: Fab is the scaling channel, not income-soon — this plugin is a byproduct o
Drafted 2026-07-16. Working name MenuKit (final name + class prefix = open decision #1).
Strategy context: Fab is the scaling channel, not income-soon — this plugin is a byproduct of
SystemLink's own menu work (Phase 0b+), so the diverted cost is packaging, decoupling, and docs only.
POC success metric: live listing + first sale + pipeline knowledge, not revenue.
#1. Product framing
What it is: a controller-first CommonUI menu framework — the layer stack, input pipeline, glyph handling, focus management, pause flow, and settings-row kit that CommonUI ships without.
Differentiation (state these on the listing):
- AI-native authoring (the wedge — §11) — menus defined in a declarative text manifest a generator
turns into real widget trees. Any AI assistant (Claude Code, Cursor, Ludus, local Ollama) can author menus for it because the authoring surface is text. No menu framework on Fab has this.
- C++-native architecture — incumbents (Hyper $149.99, Pro Main Menu, Easy Options) are Blueprint-heavy.
- Console-first — gamepad glyphs, focus restoration, input suppression are the core, not bolt-ons.
- Footgun-hardened — every fix in
UISystem.md§6.x is baked in (hover-gated glyph fallback,
bIsBackActionDisplayedInActionBar, focus-event pairing, two-input-systems trap, mid-fade focus no-op…). This is the "specialized knowledge" the market rewards.
Competitors (checked 2026-07-16): Hyper Menu v4 [CommonUI] $149.99 · Narrative Common UI (free loss-leader) · Pro Main Menu V3 · GameplayCommonUI Framework · Easy Options Menu. Crowded but differentiable. Two-tier structure (§12): MenuKit Core free (reviews/funnel) + MenuKit Pro ~$39.99 (undercut Hyper, stay out of impulse-tier junk pricing).
#2. Coupling audit (done 2026-07-16)
The widget kit is almost fully decoupled already. Exactly two game couplings exist in C++:
| # | File | Coupling | Fix |
|---|---|---|---|
| 1 | SLPrimaryGameLayout.cpp:16 | GetForPlayer() casts GetHUD() → ASLPlayerHUD | Layout self-registers with a ULocalPlayerSubsystem (§5.1) |
| 2 | SLCommonActivatableWidget.cpp:109 | SetGameplayInputSuppressed() casts owner → ASLPlayerController | Suppression moves to a controller component the widget looks up (§5.2) |
Everything else in Public/UI/ + Private/UI/ depends only on CommonUI/Engine/EnhancedInput.
#3. What moves vs. what stays
#Moves to MenuKit (tier 1 — the framework)
| Class | Notes |
|---|---|
USLCommonActivatableWidget (+ ESLUIInputMode) | Pause-on-activate, input suppression, gamepad focus restore, input-config |
USLScreenWidget | Menu-mode screen; bIsBackHandler + bIsBackActionDisplayedInActionBar; ValidateCompiledWidgetTree (keep WITH_EDITOR-guarded) |
USLModalWidget | Modal layer base |
USLButtonBase | Focus-driven glyph fix (§6.33), ButtonText bind |
USLTabListWidget, USLListEntryWidget | |
USLSettingsRowWidget + USLSliderRow / USLToggleRow / USLDropdownRow | The settings kit — a headline feature |
USLPrimaryGameLayout (+ ESLUILayer) | Push/pop/clear/getActive per layer |
USLGameViewportClient | Trivial UCommonGameViewportClient passthrough |
New in MenuKit (extracted from game classes):
UMenuKitLayoutSubsystem(ULocalPlayerSubsystem) — layout registry (§5.1).
UMenuKitInputComponent(UActorComponent) — suppression ref-count, UI-IMC swap, pause open/close/
toggle, RestoreGameInputMode (§5.2). Extracted from ASLPlayerController.
#Stays in SystemLinkCore (game HUD, not menu framework)
ASLPlayerHUD, USLHUDWidget, USLAmmoWidget/USLAmmoStrip, USLHealthWidget, USLDamageOverlayWidget, USLReticle, USLNotificationWidget, USLPickupPromptWidget, USLGrenadeIndicator. These consume the framework; they don't belong in it.
#Stays in game content (/Game/SystemLink/UI/)
All Halo-flavored styles/materials/fonts, WBP_SL_* menus, IMC_Default wiring. The plugin ships its own neutral examples (§6). SystemLink becomes consumer #1 of the plugin.
#4. Plugin scaffold
Plugins/MenuKit/
├── MenuKit.uplugin # CanContainContent=true, no engine-plugin deps beyond CommonUI/CommonInput/EnhancedInput
├── Config/
│ └── DefaultMenuKit.ini # (reserved; CoreRedirects live in SystemLinkCore's ini — see §7)
├── Content/ # neutral demo content only (§6)
└── Source/MenuKit/
├── MenuKit.Build.cs # Core, CoreUObject, Engine, UMG, Slate, SlateCore,
│ # CommonUI, CommonInput, EnhancedInput, GameplayTags(?—audit; drop if unused)
├── Public/
│ ├── MenuKitTypes.h # ESLUILayer + ESLUIInputMode → renamed
│ ├── Widgets/… # the tier-1 classes
│ ├── MenuKitLayoutSubsystem.h
│ └── MenuKitInputComponent.h
└── Private/…
- No
SYSTEMLINKCORE_API— newMENUKIT_API.
- New copyright header — decide the product header text (open decision #4); the "System Link
project" header must not ship on sold code.
- SystemLinkCore.Build.cs adds
"MenuKit"dependency (one-way: MenuKit never references SystemLinkCore —
same rule as our base-class discipline).
#5. Decoupling designs
#5.1 Layout lookup — subsystem registry (replaces the ASLPlayerHUD cast)
UMenuKitLayoutSubsystem : ULocalPlayerSubsystem
RegisterLayout(UPrimaryLayoutWidget*) // weak ptr store
UnregisterLayout(...)
GetLayout() / static GetForPlayer(const APlayerController*)
USLPrimaryGameLayout(renamed) registers inNativeConstruct/ unregisters inNativeDestructon its
owning local player's subsystem. GetForPlayer becomes a subsystem query — no HUD class involved.
ASLPlayerHUDchanges nothing except no longer being looked up — it still creates the layout widget.
Buyers can create the layout from HUD, PlayerController BP, or GameMode — any owner works.
#5.2 Input suppression + pause — controller component (replaces the ASLPlayerController cast)
Move from ASLPlayerController into UMenuKitInputComponent:
PushGameplayInputSuppression()/Pop...+GameplayInputSuppressionCount(ref-count semantics
preserved exactly — 0→1 removes, 1→0 restores).
UIInputMappingContext+AddUIMappingContext/RemoveUIMappingContext+ priority const.
Suppression-scoped IMC application is a hard invariant — applying the UI IMC during play silently eats gameplay keys (§4.2 / the jump/interact/grenade regression). Document it in the component header.
OpenPauseMenu/ClosePauseMenu/TogglePauseMenu/IsMenuOpen/PauseMenuClass.
RestoreGameInputMode()(footgun 6.30 — game is dead after last menu closes without it).
- Config:
TArray<TObjectPtr<UInputMappingContext>> SuppressedContextson the component. The game
(ASLPlayerController) feeds it its gameplay contexts at init — the component does NOT discover them.
Consumer-side after extraction:
ASLPlayerControllergains a default-subobjectUMenuKitInputComponent, forwards its existing
PauseAction binding to Toggle, passes InputMappingContexts into SuppressedContexts, and keeps thin OpenPauseMenu() wrappers only if BP callers need them (audit BP references via bridge first).
USLCommonActivatableWidget(renamed) resolves the component:
GetOwningPlayer()->FindComponentByClass<UMenuKitInputComponent>(); null → ensureMsgf (config/wiring error per our error-handling rule) and degrade gracefully (CommonUI Menu input mode still holds — the component is the belt-and-suspenders layer).
#5.3 Sequencing note
Finish the in-flight bound-action-bar work (CurrentFocus §"command bar") and PIE-gate it BEFORE starting extraction. Never refactor mid-feature; the extraction's PIE gates assume a working baseline.
#6. Content strategy (plugin Content/)
Ship neutral, self-contained demo content — zero /Game/ references from plugin content (hard Fab requirement, and the editor won't stop you from violating it):
- Input assets — plugin-local copies:
IA_MK_Confirm/Back/NextTab/PrevTab/Pause,IMC_MK_UI,
DA_MK_InputData (UCommonUIInputData), DT_MK_InputActions. SystemLink keeps its own IA_SL_UI_* set (or adopts the plugin's later — separate decision, don't couple it to the extraction).
- Controller glyph data (
CD_MK_Gamepad) — ⚠ licensing gate: the currentCD_SL_Xboxtextures'
provenance must be checked before anything ships. Safe default: rebuild on Kenney Input Prompts (CC0) — covers Xbox/PS/Switch styles, redistribution-safe. Ship KBM glyphs too — our KBM-less setup is a SystemLink design choice (console-first), not a product default; buyers expect both.
- Neutral style set — minimal clean styles (the Halo kit stays in the game). Small, tasteful,
obviously-replaceable: that's what a framework buyer wants.
- Example widgets —
WBP_MK_Button,WBP_MK_BoundActionButton,WBP_MK_PauseMenu,
WBP_MK_SettingsScreen (rows demo), WBP_MK_Modal, plus L_MenuKitDemo map with a walking pawn.
- Demo project (separate repo/zip, not in the plugin) — Fab convention for framework plugins.
#7. Rename + CoreRedirects strategy
One-shot rename, early (Phase A), while the BP surface is still small (4 WBP shells + pause menu + buttons reference these classes):
/Script/SystemLinkCore.SLScreenWidget→/Script/MenuKit.MK…etc. — **CoreRedirects entries in
DefaultSystemLinkCore.ini** (game-side ini so SystemLink's existing .uassets remap; the plugin itself ships clean with no redirect baggage).
- Order of operations (footgun-informed):
- Close editor. Move + rename code, add redirects, clean Binaries+Intermediate, full rebuild
- Open editor → verify every
WBP_SL_*loads with correct parent (spot-check via bridge: - Resave all affected
.uassets so the redirect is baked, then the redirects can be retired later. - PIE gate (§8 Phase A).
(feedback_clean_binaries_intermediate).
get_editor_property('ParentClass')).
- ⚠ Do NOT reparent any BP in-session (
feedback_blueprint_reparent_crash) — the redirect handles it;
no manual reparenting should be needed at all.
#8. Phases + gates
#Phase A — Scaffold + verbatim move (1–2 sessions)
Create plugin; move tier-1 classes with rename + CoreRedirects; new API macro + headers; SystemLinkCore depends on MenuKit; delete originals. Gate: full rebuild clean → all WBPs load → PIE: pause menu opens (Start), D-pad nav, one tile lit, "A" glyph follows focus, B closes, gameplay input restored after close, grenade/jump work during play. (Same checklist that proved Phase 0b — it's the regression suite now.)
#Phase B — Decouple (1–2 sessions)
Build UMenuKitLayoutSubsystem + UMenuKitInputComponent; strip the two casts; ASLPlayerController delegates to the component; audit BP callers via bridge before deleting any UFUNCTION. Gate: same PIE checklist + IsMenuOpen dedup + stacked-menu suppression ref-count (open pause → push settings stub → close both → input restored exactly once).
#Phase C — Neutral content + demo (2–3 sessions, mostly editor)
Plugin-local input assets; CC0 glyph sets (gamepad + KBM); neutral styles; example widgets; demo map. Gate: fresh blank project + MenuKit only → demo map: full menu flow on gamepad AND KBM with zero SystemLink assets. This gate is the real product test.
#Phase C½ — Manifest + generator (2–4 sessions, C++ editor module) — see §11
MenuKitEditor module: UMenuManifest data asset + widget-tree generator building WBPs from it. Gate: manifest describing a pause menu + settings screen → generated WBPs compile → full flow works in the blank-project demo (same harness as Phase C's gate). Feasibility spike result: §11.4.
#Phase D — Product-ize (2–3 sessions + ongoing)
- Docs: Quick Start (10-min integrate), class reference, "the footguns this solves" page (that's the
marketing). Gemma drafts from UISystem.md/MenuSystemPlan.md → Claude edits → Beepers approves.
- Store page: screenshots, 60–90s video, copy (Gemma variants). Price $39.99–$59.99.
- Fab publisher onboarding + submission (engine versions: 5.6+5.7 minimum — check what CommonUI API
differences bite; support thread + 90-day update cadence commitment).
#9. Open decisions
- Name + prefix — "MenuKit" is a placeholder; needs a Fab-searchable unique name (avoid "SL").
Free-lite tier?— RESOLVED 2026-07-16: split — free Core + paid Pro. See §12.
- **Settings *persistence*** — rows exist, but save/apply/revert +
GameUserSettingsglue doesn't.
Ship v1 without (rows only) or build it first? (It's also SystemLink's own roadmap item — likely v1.1.)
- Product copyright/license header text for plugin source.
- Engine version floor — 5.6 vs 5.7-only (first-person rendering isn't involved; CommonUI API is
the constraint).
#10. Risks
- Fab review friction — first submission always bounces on something (naming, content refs, docs
links). Budget a re-submission cycle.
- Glyph licensing (§6.2) — blocking for the listing; resolve early in Phase C.
- CommonUI engine churn — each UE release breaks something subtle (we have the scars); the 90-day
update cadence is also the maintenance tax. Price it in mentally.
- Class-rename fallout — mitigated by §7 order-of-ops + tiny current BP surface. Doing it now,
before more menus exist, is exactly why extraction shouldn't wait for a "finished" framework.
- Epic platform risk (AI) — Epic is moving on editor AI; native AI menu authoring could eventually
sherlock §11. Mitigation: speed + niche depth; the manifest/generator is a great human tool too, so the product survives the AI angle being commoditized.
#11. AI-native layer — menu manifest + generator (added 2026-07-16)
Thesis: when every dev has an AI copilot, the scarce thing is products the AI can operate. Every copilot hits the same UE wall: binary .uassets and hand-authored widget trees (Docs/CodexHandoff_WidgetTrees.md is our own proof of pain — a computer-use handoff written because AI could not build four widget trees). MenuKit's wedge: make the authoring surface text.
We deliberately do NOT compete with general copilots (Ludus AI, Ultimate Engine CoPilot, Aura — funded, crowded). We build the thing their users can point them at. Model-agnostic by design — cloud or local (Ollama), BYO assistant; "your assets never leave your machine" sells to IP-sensitive studios.
#11.1 Manifest schema (design before Phase A — it shapes widget config surfaces)
UMenuManifest (data asset; JSON import/export for AI round-tripping):
Manifest
├── Screens[] # name, base (Screen|Modal), title, layer, bPauseGameWhileActive
│ ├── FocusTarget # name of the widget to focus on activate
│ ├── Buttons[] # name, label, action (Close|PushScreen:<name>|Modal:<name>|Event:<id>), style override
│ └── Rows[] # settings rows: type (Slider|Toggle|Dropdown), label, range/options, binding id
└── Style refs # optional per-manifest style set (defaults to plugin neutral styles)
Event:<id>actions surface as BlueprintImplementableEvents on the generated screen — the game binds
gameplay behavior; the manifest never contains logic. Declarative only keeps the generator tractable.
- The generator composes MenuKit widgets only (screens/buttons/rows/modals/action bar) — a constrained
palette, not arbitrary UMG. That constraint is what makes this weeks, not months.
#11.2 Generator (MenuKitEditor module, editor-only)
- Creates/updates
UWidgetBlueprints from the manifest: constructUWidgetTree, set root, compose
children, name the C++ bind targets exactly (ButtonText, InputActionWidget, Text_ActionName — the BindWidget names the framework already enforces), compile, save.
- Idempotent regeneration: generated WBPs are marked (asset metadata tag
MenuKitGenerated);
regenerate overwrites them. Manual customization happens in subclasses of generated WBPs — the same pattern as C++-base/BP-skin we already use. v1 does NOT try to merge manual tree edits.
- Ships with
llms.txt+ a prompt-templates folder ("describe your menu → get a manifest") so any
assistant integrates in one paste. Docs written for AI consumption are part of the product.
#11.3 Dogfood path
SystemLink's own remaining menus (settings screen with its many rows — the exact case rows × boilerplate is worst for) get built from a manifest. The CodexHandoff widget trees become the generator's first test case, replacing the computer-use handoff entirely.
#11.4 Feasibility spike (2026-07-16)
Question: can widget trees be built programmatically at all, and at what level (editor Python via the bridge vs C++ editor module)? C++ is near-certain (UWidgetTree is public API; Epic's own template factories construct trees in code) — the spike tests how far script gets, which decides whether the generator can be iterated via the bridge or needs the compiled module from day one.
Result (2026-07-16): PASS — script builds widget trees, with one dodgeable constraint. Ran live via the bridge on /Game/Dev/Spike/ scratch assets:
| Capability | Verdict |
|---|---|
Create WBP asset (WidgetBlueprintFactory + AssetTools) | ✅ |
Reach WidgetTree (find_object(bp,'WidgetTree') — NOT get_editor_property) | ✅ |
Construct widgets (unreal.new_object(cls, outer=wt, name=…)) — incl. CommonUI classes (CommonTextBlock/CommonActionWidget/CommonBorder) and our own C++ bases (SLButtonBase exposed) | ✅ |
Attach children — PanelWidget.add_child() IS exposed, returns real slot objects (HorizontalBoxSlot etc.); ContentWidget via call_method('SetContent',…) | ✅ |
Exact BindWidget names (ButtonText, grafted SpikeGraftedText) + widget properties (set_editor_property('text',…)) | ✅ |
Compile + save (BlueprintEditorLibrary.compile_blueprint + EditorAssetLibrary.save_asset) → parent/child linkage verified after | ✅ |
Set WidgetTree.RootWidget | ❌ protected — Python reflection refuses read AND write; fresh 5.7 WBPs are rootless, so a from-scratch tree can't be rooted by script |
The dodge — seed templates: duplicate a WBP that already has a root (EditorAssetLibrary.duplicate_asset ✅ tested), then build everything under it — add_child covers the entire tree below the root. Plugin ships 2–3 hand-authored seeds (Overlay-root, Canvas-root); the generator never touches RootWidget.
Decision this enables: the generator can be prototyped and dogfooded via the bridge NOW (Python, seed-template pattern) — no MenuKitEditor module needed to start. The shipped product still gets the C++ module (buyers shouldn't need the Python plugin enabled, and C++ sets RootWidget natively, no seeds required). Prototype in script → port to C++ in Phase C½.
This also retires Docs/CodexHandoff_WidgetTrees.md — the computer-use handoff is no longer the only path; remaining SystemLink widget trees can be script-built (under seeded roots) via the bridge.
#12. Tier split — free Core + paid Pro (decided 2026-07-16, resolves §9.2)
Why split: the POC metric (§ preamble) needs a first sale — fully free can't produce one; fully paid can't cold-start (no reviews, no audience, $150 incumbent + free competitor). The split yields both signals: adoption (free downloads/reviews — Fab's scarce resource) and willingness-to-pay (paid conversions). The ratio between them is the data that shapes what comes after. Narrative's playbook (free loss-leader, 4.3★/23 reviews anchoring a paid ecosystem) — proven in this exact category.
Cut-line rule: free = the plumbing that gets you to ONE working pause menu; paid = everything that scales past it. Each tier must be genuinely useful alone — the free tier earns the reviews, so it cannot be crippleware.
#MenuKit Core (FREE)
- Activatable widget base (pause-on-activate, input suppression, gamepad focus restore)
- Screen + modal bases,
PrimaryGameLayoutlayer stack,LayoutSubsystem
InputComponent(suppression ref-count, UI IMC swap, pause open/close/toggle, input restore)
- Button base with the focus-driven glyph handling; viewport client
- Input assets + CC0 glyph sets (gamepad + KBM); ONE example pause menu; Quick Start docs
- Job: earn reviews + downloads; funnel to Pro, the course, and Discord/newsletter (links in docs +
listing). Free-tier users are the bug reporters before paying customers hit issues.
#MenuKit Pro (PAID — ~$39.99, launch price TBD at Phase D)
- Settings row kit (slider/toggle/dropdown) + settings screen example
- Bound action bar + action button
- Neutral style set + full demo content/map
- Manifest + widget-tree generator + AI-integration kit (§11) — the wedge stays behind the paywall
- Job: the willingness-to-pay experiment.
#Packaging consequences
- Two plugins, two listings, two Fab review cycles (a POC plus — double the pipeline learning).
MenuKitPro.uplugin declares a plugin dependency on MenuKit (Core); Pro never duplicates Core code.
- Module layout in §4 splits accordingly: Core = runtime module as specced; Pro = second runtime module
(rows/action bar) + MenuKitEditor (generator).
- Phase gates unchanged; Phase C's blank-project gate runs per tier (Core alone must pass it too).
- Pricing can move later; a $0 anchor on the full product can't be undone — never fold Pro features
into Core to chase reviews.