Skip to content

Generated reference. This page mirrors Code/Ui/README.md, this module's source-of-truth documentation (Standards/02_DOCUMENTATION_STANDARDS.md §1). Edit it there, not here -- this file is regenerated on every docs build and any local edit is silently overwritten.

Ui

Purpose. The registration points standing HUD elements and player-menu tabs attach to, the top-level panels that instantiate them, and the shared visual language every panel in this codebase draws from. See 11_UI_ARCHITECTURE.md, 23_UI_DESIGN_SYSTEM.md, and 24_PLAYER_MENU.md.

Responsibilities. - Owns IHudRegistry: the record of which panel type belongs in which HudSlot. - Owns HudRoot, the one PanelComponent that queries the registry and builds the actual panel tree once the local player's HUD is ready. - Owns IMenuRegistry (Milestone 11) — the same registration-time decoupling IHudRegistry already provides, generalized to player-menu tabs: a module contributes a tab (with a title, an icon name, and a display order) without PlayerMenu knowing that module exists. - Owns PlayerMenu, the one PanelComponent that builds a nav rail from IMenuRegistry.RegisteredTabs and mounts only the currently-selected tab's own panel into a single content container, swapping it on selection. - Owns PlayerMenuRootComponent — toggles the menu on the player_menu keybind, ungated (unlike AdminToolsRootComponent's admin-only toggle, since every player may open this menu). - Owns StaticContentConfiguration — the one asset backing the three tabs that are pure static text (Info, Changelog, Credits): WelcomeText, RulesText, ChangelogText, CreditsText. - Owns InfoTab/ChangelogTab/CreditsTab — read StaticContentConfiguration directly, no gameplay dependency. - Owns the shared stylesheet partials (_tokens.scss, _components.scss) every panel in this codebase (not just Ui's own) imports for its colors, spacing, and reusable component classes (nav rail icon, card, primary button, badge) — see 23_UI_DESIGN_SYSTEM.md for the values themselves.

Not responsible for. Any gameplay state. Ui never holds money, inventory contents, or any other domain value itself -- it only hosts panels that other modules register, and those panels read their own module's replicated state (a [Sync] Component such as Code/Economy/CharacterEconomyComponent.cs) or, for an on-demand panel reading host-only, never a value cached here. HudRoot/PlayerMenu's GameObject/ScreenPanel are now placed into Assets/scenes/main.scene (Hud/PlayerMenu/PlayerMenuRoot, see that file's own README) -- unlike the rest of that scene's db7c83a additions, this wiring has not been run in a live editor session, same unverified-until-checked caveat every Component in this codebase already carries.

RESOLVED: TypeLibrary.Create<Panel>(Type) avoided entirely, not fixed in place. HudElementRegistration/MenuTabRegistration carry a Func<Panel> CreatePanel closure built at RegisterHudElement<TPanel>/RegisterMenuTab<TPanel>'s own call site, where TPanel is still compile-time-known (() => new TPanel()) -- HudRoot.razor/PlayerMenu.razor call that delegate directly, never TypeLibrary.Create<Panel>(Type) with a runtime Type. Sidesteps the addon-side Roslyn analyzer question entirely rather than answering it.

RESOLVED: the Panel-vs-PanelComponent registry question, by NOT routing PanelComponents through the registry at all. IHudRegistry.RegisterHudElement<TPanel>/ IMenuRegistry.RegisterMenuTab<TPanel> both still require TPanel : Panel, new() -- that constraint is correct and unchanged, not a bug. A PanelComponent (one with [Property] bindings to a scene-wired sibling Component -- MoneyDisplay, NeedsBar, InventoryPanel, CharacterTab, LawsTab) can only be constructed through the engine's own GameObject/prefab pipeline; new TPanel() produces one with a null GameObject, no Scene, and no working Components.Get<T>(), per Sandbox.Engine's own PanelComponent/Component source (GameObject's setter is internal). These panels are scene-placed directly instead -- either as a sibling Component on the same GameObject as their nearest ScreenPanel/root panel (MoneyDisplay on Hud, see Assets/scenes/README.md), or via the existing on-demand pattern this codebase already uses for AdminToolsRootComponent/PlayerMenuRootComponent (§7 below). Each resolves the live Component it needs (e.g. the local player's own CharacterEconomyComponent) via Applejack.Movement.LocalPawn.Find(Scene) rather than a [Property], since no prefab/scene file could statically reference a GameObject (PlayerPawn.prefab's spawned instance) that doesn't exist until a connection's pawn spawns -- see Code/Movement/LocalPawn.cs's header comment for the full reasoning. This means: only Panel-shaped, no-[Property] panels go through IHudRegistry/IMenuRegistry (InfoTab, ChangelogTab, CreditsTab, HelpTab, RicherEconomyHudPanel); every PanelComponent standing panel is scene data, not a registry entry. Registered/scene-placed status per panel:

Panel Base type Standing-element path
InfoTab/ChangelogTab/CreditsTab Panel IMenuRegistry.RegisterMenuTab (UiModule.RegisterServices)
HelpTab Panel IMenuRegistry.RegisterMenuTab (CommandsModule.Ready)
RicherEconomyHudPanel Panel IHudRegistry.RegisterHudElement (RicherEconomyPlugin.Ready)
MoneyDisplay, NeedsBar, InventoryPanel PanelComponent Scene-placed, sibling of Hud's ScreenPanel/HudRoot
AdminPanel, DevMenuPanel, PlayerListPanel PanelComponent Prefab-placed (PlayerPawn.prefab), each its own on-demand ScreenPanel sibling toggled by AdminToolsRootComponent (issue #36)
CharacterTab, LawsTab PanelComponent Still not scene-placed -- see the open composition question below, unchanged
Code/Chat/Ui/ChatPanel PanelComponent Scene-placed standalone (ChatPanel GameObject, Assets/scenes/README.md), toggled by ChatRootComponent -- never went through either registry in the first place, since it's on-demand like AdminPanel/DevMenuPanel, not a standing element or menu tab

SUPERSEDED by the "RESOLVED: TypeLibrary.Create<Panel>(Type) really was a compiler whitelist violation" entry further down this file -- left here for the historical record only, do not read this paragraph as current status. RegisterHudElement/RegisterMenuTab call sites are live today (UiModule.cs, CommandsModule.cs, RicherEconomyPlugin.cs), confirmed by grep, not commented out.

BLOCKED (2026-07-31, found while investigating why nothing but movement worked on a real session): HudRoot/PlayerMenu render as empty shells even once scene-placed. Both HudRoot.razor's and PlayerMenu.razor's actual per-module content step -- TypeLibrary.Create<Panel>(element.PanelType) / TypeLibrary.Create<Panel>(tab.PanelType) -- is commented out, citing a suspected compiler whitelist violation. Confirmed by grep that this isn't cosmetic: every single RegisterHudElement<T>/RegisterMenuTab<T> call across every module (Code/Economy/EconomyModule.cs's MoneyDisplay, this file's own UiModule.cs for InfoTab/ChangelogTab/CreditsTab, etc.) is also commented out, so IHudRegistry.RegisteredElements/IMenuRegistry.RegisteredTabs are unconditionally empty right now -- scene-placing either panel produces a real container with nothing inside it. Checked Sandbox.Internal.TypeLibrary.Create<T>(Type, object[]) directly against ~/Projects/sbox-public (engine/Sandbox.Reflection/TypeLibrary/Instancing.cs:53-67): the engine's own implementation is an IsAllowedType whitelist check plus Activator.CreateInstance, nothing obviously incompatible with a whitelisted addon type -- but whether the addon-side Roslyn compiler restrictions actually reject calling this generic method with a runtime Type from game code is a closed-source compiler-analyzer question this public engine mirror can't settle. See ROADMAP.md Verification debt's 2026-07-31 entry for the full citation. Not fixed this pass -- flipping it blind under WarningsAsErrors: true risks a hard compile failure with no local way to check against the real analyzer.

Dependencies. Core (services, configuration). Nothing gameplay-specific -- the whole point of both registries is that Ui stays ignorant of which gameplay modules exist; they depend on Ui instead, per 11_UI_ARCHITECTURE.md §3 and 24_PLAYER_MENU.md §1.

Public API. - IHudRegistry.RegisterHudElement<TPanel>(HudSlot). - IMenuRegistry.RegisterMenuTab<TPanel>(string title, string icon, int order), IMenuRegistry.RegisteredTabs. - StaticContentConfigurationWelcomeText, RulesText, ChangelogText, CreditsText. - InfoTab, ChangelogTab, CreditsTab — not called directly by anything outside IMenuRegistry's own instantiation.

Events published. None.

Persistence. None -- HUD/menu layout is not saved state. StaticContentConfiguration is a GameResource asset (author-time content), not runtime-persisted player data, the same category CharacterConfiguration already is.

Future improvements. - No unregister path on either registry (nothing has ever needed to remove a standing HUD element or a menu tab once registered); add if a module can be unloaded at runtime. HudSlot currently only names the four screen corners plus center -- extend as real elements need finer positioning, not speculatively. - The icon set IMenuRegistry.RegisterMenuTab's icon parameter names is genuinely undecided -- see 23_UI_DESIGN_SYSTEM.md §6. Every registration in this codebase currently passes a Tabler-style class name ("ti ti-user") as a placeholder pending that decision. - Whether a panel's .razor.scss can actually @import _tokens.scss/_components.scss the way every player-menu panel's own stylesheet now assumes is unverified without a compiler -- see those two files' own header comments for the named fallback if imports aren't supported.

Not unit-tested. HudRegistry/MenuRegistry's own bookkeeping is a one-line-per- registration list -- trivially correct -- but both registries' generic constraint (where TPanel : Panel, new(), taken directly from 11_UI_ARCHITECTURE.md §3) means even calling either requires a real Sandbox.UI.Panel-derived type. Every other test in this codebase avoids an engine-type dependency entirely, per Standards/03_TESTING_STANDARDS.md §1; faking a Panel subclass here would be a first, with no compiler this pass to confirm it actually satisfies the engine's own constructor requirements. Left unverified rather than built on a guess.

Resolved: TypeLibrary.Create<Panel>(Type) really was a compiler whitelist violation. HudRoot/PlayerMenu no longer call it. HudElementRegistration/MenuTabRegistration now carry a Func<Panel> CreatePanel, built by HudRegistry.RegisterHudElement<TPanel>/ MenuRegistry.RegisterMenuTab<TPanel> themselves, where TPanel is still compile-time-known at that generic call site -- no runtime reflection anywhere in the path. See HudElementRegistration.cs's own header comment. Code/Commands/Ui/HelpTab.razor and Code/Ui/InfoTab.razor/ChangelogTab.razor/CreditsTab.razor are converted, working examples (@inherits Panel, resolved via Game.ActiveScene, not Scene, mirroring Code/Plugins/RicherEconomy/RicherEconomyHudPanel.razor's own precedent).

MoneyDisplay/NeedsBar/InventoryPanel no longer use [Property] bindings at all -- each resolves the local player's own live Character-bound Component via Applejack.Movement.LocalPawn.Find(Scene) instead (Code/Movement/LocalPawn.cs), and all three are now scene-placed as siblings of Hud's ScreenPanel/HudRoot (see Assets/scenes/README.md), matching the resolved pattern this section describes above.

CharacterTab/LawsTab remain genuinely blocked, and it is not the same gap. Both already use the identical LocalPawn.Find(Scene) self-resolution as the three HUD elements above -- the [Property]-binding gap is closed for them too. What's still open is composition: they are conceptually player-menu tabs, meant to appear only while their own nav-rail icon is selected, not always-on like a HUD element. PlayerMenu.razor's tab-swap only mounts a Panel-typed child into its own _content container via IMenuRegistry's Func<Panel> factories -- a PanelComponent structurally can't go through that path (see this file's "RESOLVED: the Panel-vs-PanelComponent registry question" entry above). Scene-placing them as siblings the MoneyDisplay way (same GameObject as PlayerMenu's own ScreenPanel) would attach their root panel directly into that ScreenPanel's tree, bypassing _content entirely -- always rendered, never gated by tab selection, and not laid out inside .aj-shell's flex row at all, since .aj-content's flex: 1 (Code/Ui/_components.scss) only means anything as a flex child of .aj-shell, which a same-GameObject sibling panel isn't. Reusing that class on an unrelated sibling would not reproduce the intended layout -- verified by reading the stylesheet, not guessed. Closing this needs either a real design pass (extending IMenuRegistry to track Component-backed tabs and gating their visibility off PlayerMenu's own selected-tab state) or editor access to confirm how a PanelComponent's root panel actually threads into an ancestor's tree, neither attempted this pass -- named here rather than shipped as an unverified guess, the same discipline this file already applies to the TypeLibrary.Create question above.