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.

Prerequisites
CrimsonCommon enabled. A player controller that carries (or will carry) a 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).

Screenshot pendingImages/CrimsonCommon/howto-contextmenu-action.png
A UCrimsonContextAction Blueprint subclass overriding Is Available and Execute Action.

2. Expose actions from an object

Easiest path: an Action Set Data Asset
Skip inline-instance editing entirely: Content Browser -> Miscellaneous -> Data Asset -> Crimson Context Action Set, fill its 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).

Actions are NOT assets - tick Instanced
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.
  1. Class Settings -> Implemented Interfaces -> add CrimsonContextActionProvider.
  2. Add a variable ContextActions: type Crimson Context Action -> Object Reference, make it an Array.
  3. Select the variable -> Details panel -> expand Advanced -> tick Instanced. (This is the step everyone misses - without it the array shows an empty asset picker.)
  4. Compile, open Class Defaults, press + on the array and pick your action subclass from the dropdown. Fill DisplayName, ActionTag, SectionTag, etc. inline.
  5. In My Blueprint -> Interfaces, double-click Gather Context Actions and connect the ContextActions variable to the Out Actions output pin.
Screenshot pendingImages/CrimsonCommon/howto-contextmenu-provider-bp.png
Left: the ContextActions variable with Instanced ticked and two inline action instances in Class Defaults. Right: the Gather Context Actions interface function returning the variable.

For 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.

Verify
Open the actor's Class Defaults: every array element shows an expandable inline action (not an empty picker). At runtime, CrimsonCore's context-menu subsystem (ShowContextMenuForActor) lists your entries.
Interactables come for free
An object that already implements 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.

text
[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 used
4. ResolveAction(Actions, ActionId, Occurrence) <- stable id, not an index
5. Action->GetAvailability(Context) == Enabled <- re-validated server-side
6. Action->ValidateExecution(Context) <- anti-cheat (e.g. range)
7. Action->ExecuteAction(Context, Payload) <- runs under HasAuthority()
Attach the component
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.
Verify
In a Client PIE session, pick an action on a client: it round-trips and only executes server-side. A spoofed/unavailable action is rejected by steps 5-6, and executing a door action from across the map fails the range check.

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.

cpp
// 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.

Register on BOTH sides
The registry exists per world on server and client. A contributor registered only client-side produces menu entries the server cannot re-gather - they are dropped at execution with a 'gather list shifted' warning. Register from a system that exists symmetrically (world subsystem, replicated manager) in the same lifecycle hook, and keep the gather deterministic.

See also

  • CrimsonCore -> Context Menu - the full end-to-end wiring (presentation + input)
  • API Reference - context-menu types and native tags