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.
- StaticContentConfiguration — WelcomeText, 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.