Concept: Bars, Pages and the Store
Three layers, deliberately separate: what the player bound (the store), where it shows (bars and pages), and what it is (providers). This page is the first two.
The store owns the bindings
UCrimsonSlotBindingComponent is a flat map LayoutId -> array of FCrimsonSlotBinding, server-authoritative, delta-replicated per layout to the owner, SaveGame all the way down. It knows nothing about bars, wheels or keys. Every bar page that uses the same LayoutId shows the same bindings - there is one truth per player, not one per view.
A bar is keys; a page is contents
FCrimsonActionBarDefinition fixes the things that do not change while playing: the bar's identity (BarTag), its keys (SlotInputActions, one dedicated action per position), how a press behaves (PressMode, bForwardReleaseToProvider), what happens to stale bindings, whether the player may rearrange it (EditBlockedQuery), and which of the asset's providers it takes (AllowedProviders - empty means all; a consumables strip lists only the Object provider and ability drops are refused with a reason). Every FCrimsonActionBarPageDefinition under it is one set of contents for those keys: its own LayoutId, its own Slots (FCrimsonSlotDefinition: lock, server-only, default binding, AcceptedEntries filter, empty icon), and - the interesting part - an optional ActivationQuery.
| Page kind | How it is chosen | Examples |
|---|---|---|
Manual (empty ActivationQuery) | PageUpAction / PageDownAction, SetManualPage, CyclePage. The choice replicates and saves. | WoW bar 1 pages 1-2, FFXIV hotbar sets. |
Automatic (ActivationQuery set) | Wins while the owner's tags match; highest ActivationPriority first. Not selectable by hand. | Bear form, stealth, a vehicle, a possessed pet. |
The effective page is best matching automatic page, else the manual page. It is computed on the server and on the owning client from the same replicated inputs - the PlayerState / ASC tags, the possessed pawn's tags, and the replicated manual page - so a stance change flips the bar with no round trip and both sides always agree on which LayoutId a key drives. Nothing about the effective page replicates.
Addresses, not indices
Every bar API speaks FCrimsonActionBarSlotAddress (BarTag, PageIndex, SlotIndex). A widget bound to (ActionBar.Main, -1, 3) means slot 3 of whatever page is effective, and keeps working when the page flips under it. -1 resolves to the effective page everywhere.
Who may write, and where it is checked
Assign / clear / move are conveniences that translate an address into a store write. Every client-originated write travels through the store's Server_ RPCs; on the server the store runs the write validators registered on it, and this component registers one for every LayoutId it owns: slot exists, not bLocked, not bServerOnly, bar not edit-blocked, payload is the provider's own struct, binding passes the slot filter. The same validator runs on the client as a pre-flight, so CanAssignBinding gives the exact reason the server would. Server-internal writes (auto-place, save restore, gameplay filling a server-only slot) bypass validation on purpose. SetBarsLocked is a UX guard against accidental drags; it neither replicates nor protects anything.
Stale, not gone
A binding whose thing no longer exists (unlearned, sold, deleted) is stale. The default policy KeepDisabled keeps it in place and grayed and re-resolves it every poll, so the moment the spell is re-learned or the potion restocked the slot lights back up with no rebinding. A layout's value is muscle memory; a bar that silently rearranges itself destroys exactly that. Clear empties the slot instead; Hide renders it empty but keeps the binding.