How-To: Add a Context Menu Action
Goal: define a project-wide context-menu action (Use / Drop / Inspect / Lock / Open...) that any object can expose and the server validates before running. CrimsonCommon owns the contracts; presentation lives in CrimsonUI and the wiring example is in CrimsonCore.
UCrimsonContextActionPlayerComponent to ferry the chosen action to the server.1. Author an action
Subclass UCrimsonContextAction - it is Blueprintable, EditInlineNew, DefaultToInstanced, so designers can author actions inline on item fragments without C++. Override IsAvailable (enable/disable) or GetAvailability (adds the Hidden state) plus GetDisabledReason, and ExecuteAction (do the work; server-side under HasAuthority()). Set ActionTag - it names the action on the wire (GetActionId(); falls back to the class name when unset).
Optional behavior properties: SectionTag (grouping; UI.ContextMenu.Section.*), SortPriority, bIsDefault (double-click / tap fast path), bIsDestructive (styles red, always sorts last), ConfirmationMode (Dialog / HoldToConfirm friction before executing) with ConfirmationPrompt, and CloseBehavior (CloseMenu / KeepOpen / RefreshMenu).
Images/CrimsonCommon/howto-contextmenu-action.png2. Expose actions from an object
Actions list once, and reference the asset from the provider (e.g. ACrimsonInteractableActor::ContextActionSets) through a plain asset picker. One asset can serve every door/lever that offers the same actions, edits propagate to all of them immediately, and it is deterministic by construction (the same loaded asset on client and server). On level-placed actors, action sets are the ONLY reliable path - inline Instanced arrays on placed actors do not pick up class-default edits (stale per-instance copies); reserve inline Instanced arrays for asset/class-default contexts like item fragments.Implement ICrimsonContextActionProvider on any UObject (actor, replicated subobject, item instance) and return your actions from GatherContextActions. The list must be deterministic - client and server have to produce the same set, because the chosen action is resolved by (ActionId, Occurrence) against a server-side re-gather. Both sides gather through UCrimsonContextActionGatherer::GatherActions - never hand-roll a second gather path. Native providers can also override ValidateContextActionRequest to reject wrong instigators outright (e.g. an item in another player's inventory).
UCrimsonContextAction is EditInlineNew, DefaultToInstanced: instances live inline inside their owner, never in the Content Browser. A plain object-reference variable therefore shows an empty picker - it looks like your actions "don't show up". In Blueprint you must tick Instanced on the variable (Details -> Advanced); in C++ mark the UPROPERTY with the Instanced specifier. Only then does each array element offer the "+ create new instance" dropdown listing every non-abstract action subclass. Also make sure the variable is an Object Reference (not Class Reference) and your action subclass is not marked Abstract.- Class Settings -> Implemented Interfaces -> add
CrimsonContextActionProvider. - Add a variable
ContextActions: typeCrimson Context Action-> Object Reference, make it an Array. - Select the variable -> Details panel -> expand Advanced -> tick Instanced. (This is the step everyone misses - without it the array shows an empty asset picker.)
- Compile, open Class Defaults, press + on the array and pick your action subclass from the dropdown. Fill
DisplayName,ActionTag,SectionTag, etc. inline. - In My Blueprint -> Interfaces, double-click Gather Context Actions and connect the
ContextActionsvariable to the Out Actions output pin.
Images/CrimsonCommon/howto-contextmenu-provider-bp.pngFor a dynamic list, use Construct Object From Class (Outer = Self) inside Gather Context Actions and set the fields before adding to Out Actions - just keep the logic deterministic, because the server runs the same function to validate the pick.
ShowContextMenuForActor) lists your entries.ICrimsonInteractableTarget surfaces in the menu automatically: the gatherer wraps each FCrimsonInteractionOption (from the actor and its interactable components) in a UCrimsonContextAction_FromInteractionOption. You don't have to also implement the provider interface. These wrapped actions get a server-side range check (crimson.ContextMenu.MaxExecuteDistance, default 1000 cm).3. The server round-trip
The client picks an entry; UCrimsonContextActionPlayerComponent sends its stable identity - GetActionId() plus its occurrence among same-id entries - and an optional FInstancedStruct payload. The server re-gathers through the same gatherer, resolves the id, re-runs `GetAvailability` and `ValidateExecution` (anti-cheat), then executes under authority. Stale requests (provider destroyed, list shifted) are dropped with a log - never a disconnect.
[Client] pick an entry-> UCrimsonContextActionGatherer::MakeActionHandle(GatheredList, Action) -> (ActionId, Occurrence)-> UCrimsonContextActionPlayerComponent::ExecuteContextAction(Provider, ActionId, Occurrence, Payload)-> Server_ExecuteContextAction(Handle{ Provider, ActionId, Occurrence, Payload }) [RPC][Server]1. Resolve Provider (weak ptr -> live UObject; stale = graceful drop, never a kick)2. Provider->ValidateContextActionRequest(Context) <- ownership gate (native providers)3. UCrimsonContextActionGatherer::GatherActions(...) <- the SAME path the client used4. ResolveAction(Actions, ActionId, Occurrence) <- stable id, not an index5. Action->GetAvailability(Context) == Enabled <- re-validated server-side6. Action->ValidateExecution(Context) <- anti-cheat (e.g. range)7. Action->ExecuteAction(Context, Payload) <- runs under HasAuthority()
UCrimsonContextActionPlayerComponent is not auto-attached - your project decides how (Blueprint component, a GameFeatureAction_AddComponents, or a player-controller subclass). Without it there is no path to the server.4. Contribute actions from other systems
A system that is not the target can inject actions into its menu - quests add "Turn In" to an NPC, trading adds "Offer to Player" on items - without the target knowing. Implement ICrimsonContextActionContributor and register with UCrimsonContextActionContributorRegistry (a world subsystem). The gatherer consults every registered contributor after the target's own gather, so contributed actions flow through the identical display / validation / execution pipeline.
// A quest world subsystem injecting "Turn In Quest" into NPC menus.UCLASS()class UMyQuestSubsystem : public UWorldSubsystem, public ICrimsonContextActionContributor{GENERATED_BODY()public:virtual void OnWorldBeginPlay(UWorld& InWorld) override{Super::OnWorldBeginPlay(InWorld);// World subsystems exist on server AND client -> symmetric registration.InWorld.GetSubsystem<UCrimsonContextActionContributorRegistry>()->RegisterContributor(this);}virtual void GatherContextActionsForTarget_Implementation(const FCrimsonContextActionContext& Context, TArray<UCrimsonContextAction*>& OutActions) override{AActor* NPC = Cast<AActor>(Context.Target.Get());if (NPC && HasCompletableQuestFor(NPC, Context.InstigatorController.Get())){OutActions.Add(MakeTurnInAction(NPC)); // deterministic on client and server}}};
In Blueprint: implement the CrimsonContextActionContributor interface on a Blueprint class that exists on both server and client (e.g. a replicated manager actor), override Gather Context Actions For Target, and call Register Contributor on the CrimsonContextActionContributorRegistry world subsystem from BeginPlay.
See also
- CrimsonCore -> Context Menu - the full end-to-end wiring (presentation + input)
- API Reference - context-menu types and native tags