Concept: Slot Bindings
An authored radial option and a player-bound one look identical on screen and are built completely differently. This page explains why, because almost every question about the bindable wheel is really a question about that difference.
Radial/: FCrimsonRadialBinding, FCrimsonRadialSlotDefinition, the store UCrimsonRadialBindingComponent, the provider base UCrimsonRadialBindingProvider and UCrimsonRadialBindingProvider_Tag, UCrimsonRadialBindingLibrary (maps a provider's FCrimsonRadialBindingDisplay + FCrimsonRadialBindingLiveState onto a FCrimsonRadialSlice), the picker, and UCrimsonRadialBindingDragDropOperation. It shares no types with any other plugin. (CrimsonCore - requires all Crimson plugins) UCrimsonCoreRadialBindingProvider_SlotBinding wraps action-bar providers so abilities, items and equipment can be bound to a wheel with the same identity as on the action bar.Why a binding is not a slice
An authored option is a FCrimsonRadialSlice. It carries its own visuals and an Instanced UCrimsonRadialAction saying what choosing it does. That is exactly right for a designer typing options into an asset: one array element is one complete, self-contained option.
It is useless for a player's choice, for one reason: an Instanced UObject cannot round-trip through a save file. Whatever the player picked has to come back next session, and an object authored inline on an asset cannot.
So the two are separated. What the player picks is stored as a small, plain FCrimsonRadialBinding. The live slice is rebuilt from it every time the wheel opens and never stored at all.
FCrimsonRadialBinding { ProviderId, Data }| stored in the savevthe provider resolves it|vFCrimsonRadialSlice { icon, name, count, cooldown, action }rebuilt on open, never stored
A binding is two fields. ProviderId is a gameplay tag naming who understands it. Data is an FInstancedStruct that provider defined - an ability's trigger tag, an item definition, an equipment slot. Nothing in CrimsonUI ever looks inside `Data`, which is what lets the wheel carry things this plugin has never heard of.
ProviderId is empty, and the layout array is always dense and sized to the page.The four questions a provider answers
A provider is the only thing in the system that knows what a category of bindable thing actually is. It answers four questions and nothing else needs to know the answers.
| Verb | Called when | Answers |
|---|---|---|
GatherBindableEntries | The picker opens | What may the player choose right now? |
ResolveBinding | The wheel opens | What does this saved binding look like today - and does it still exist? |
QueryBindingState | 10 Hz while open | Stack count, cooldown, availability. |
ExecuteBinding | The player confirms | Do the thing. |
That split mirrors UCrimsonRadialAction's GetDisplayContent / QueryState / Execute deliberately. The extra verb is Gather, which is the one thing an editable wheel needs and an authored one does not: somebody has to enumerate the options before a player can pick from them.
The expensive work - loading an icon, formatting a name - belongs in ResolveBinding, which runs once per slot per open. QueryBindingState runs ten times a second, so it should only touch things that actually change.
ResolveBinding defaults to gathering and searching for a match, so a provider with a small catalog implements GatherBindableEntries and ExecuteBinding and stops. Override it once your catalog is big enough that a linear scan per slot per open starts to matter.One action object for the whole wheel
Resolved slices still need an Action, because that is how UCrimsonRadialMenuWidget runs things. A single UCrimsonRadialAction_Binding is created per menu and assigned to every bound slice.
It works because it holds no per-slice state. It reads the binding back out of Slice.Payload - which already existed to 'carry an item handle without CrimsonUI knowing the type' - and routes to the right provider by ProviderId. Eight bound slices share one object instead of allocating an action apiece on every open.
A provider that wants its own behavior per entry can still set OutSlice.Action itself during ResolveBinding; the shared action is only installed where the provider left it empty.
Stale bindings
The item was sold. The ability was unlearned. The asset was deleted. ResolveBinding returns false and the layout's ECrimsonStaleBindingPolicy decides what the slot does.
| Policy | Result | Cost |
|---|---|---|
KeepDisabled (default) | Grayed out with a reason, still in place. | A dead icon the player has to clear themselves. |
Clear | Back to a placeholder. | The arrangement changed without the player asking. |
Hide | Dropped from the wheel entirely. | Changes the slice count, so every remaining slice moves. |
The default keeps the binding, because a wheel's whole value is muscle memory - the player learns that healing is at twelve o'clock and stops reading it. A layout that quietly rearranges itself between sessions destroys exactly the thing that made it worth having.
Note what is not stale: an item the player has none of, and an equipment slot that is currently empty. The player will restock and re-equip, so those resolve normally and are grayed out by QueryBindingState instead.
Resolution itself never writes to the store. Clear is applied by the menu widget afterwards, because resolution is a pure read that a HUD bar might also be running - and a second view of the same layout must not silently rewrite the player's save.
Where the bindings live, and who may change them
UCrimsonRadialBindingComponent is server-authoritative. The layout container replicates COND_OwnerOnly - nobody but the owning client has any use for another player's wheel, and sending it would leak what they have equipped. Clients call the same functions and are routed through Server_ RPCs; the owning client rebuilds from PostReplicatedReceive, once per replication batch rather than once per changed slot.
There is deliberately no client-side prediction. Every reason a write can be rejected - bad index, locked slot, filter mismatch - is checkable on the client before it sends, so an optimistic local apply would have nothing to roll back on the one path that can actually fail. One round trip on a menu edit is the correct trade.
_Validate functions exist to drop malformed indices, not to stop an exploit.Pages are just more layouts
Monster Hunter World ships four radial sets of eight slots, cycled while the wheel is open. That needs no new concept here: each page in a UCrimsonRadialLayoutDefinition carries its own LayoutId, so the store stays a flat map from layout tag to bindings and turning a page is reading a different tag.
A single-page wheel is one array entry. A carry does not survive a page turn - the slot it came from is no longer on screen, so there would be nowhere to put it back.
See also
- How-To: Build a Player-Editable Wheel - the recipe
- Concept: Radial Selection & Slice State - how the wheel itself picks a slice
- API Reference -> Slot bindings