"My s&box UI won't update / is frozen"
▸ SYMPTOM
You change game state, but the panel on screen never reflects it. A counter stays stale, a modal won't close, a loading bar sits at 0% even though work is progressing. No error, no crash: the UI is simply not re-rendering.
▸ CAUSE
BuildHash() is the only re-render trigger in s&box Razor. The panel recomputes the hash each frame and re-renders only when that integer changes. Any value your markup reads but the hash omits is silently frozen.
Hashing a collection reference or object identity stays constant even as contents mutate, so the UI looks dead while data moves underneath.
▸ FIX
Hash everything the markup reads, collection contents, derived display strings (they're cheap), day/date, and every open/closed flag you branch on:
@inherits PanelComponent
@code {
protected override int BuildHash() => HashCode.Combine(
Counts.Values.Sum(), // collection CONTENTS, not the reference
Game?.Day ?? 0,
Progress, // loading bar / time-sliced work
UIState.ThisPanelOpen,
UIState.AnyModalOpen
);
// HashCode.Combine has NO 9-arg overload - a flat 9th
// argument is CS1501. Nest instead:
// HashCode.Combine(HashCode.Combine(a..h), i)
}Skeleton reminder:
@using Sandbox
@using Sandbox.UI
@namespace YourGame
@inherits PanelComponent
<root>
@if (Game is not null && UIState.ThisPanelOpen)
{
<div class="panel">@Counts.Values.Sum()</div>
}
</root>If the panel type itself can't be found from C#, fix @namespace first: razor-namespace-trap.
▸ WHY IT WORKS
Razor does not diff the full tree every frame. It treats BuildHash() as a dirty bit: same int → skip rebuild. Content-sensitive hashes (sums, counts, flags, progress) change when the player-visible state changes, so the markup runs again and the screen catches up.