Routing Component

UCrimsonInventoryRoutingComponent is a server-authoritative router that owns every Server_ RPC the plugin needs. Attach it to your PlayerController; your UI calls the component's BlueprintCallable methods directly. There are no Server_ RPCs to write in your game project.

It also owns: per-player container storage (used by PerPlayer chest scope), the access-control virtuals, the party / dungeon-instance scope key resolvers, and a data-in / data-out save struct for personal containers.

Setup

cpp
// MyPlayerController.h
#include "Routing/CrimsonInventoryRoutingComponent.h"
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UCrimsonInventoryRoutingComponent> InventoryRouting;
// MyPlayerController.cpp ctor
InventoryRouting = CreateDefaultSubobject<UCrimsonInventoryRoutingComponent>(TEXT("InventoryRouting"));

Client-callable API

Every method here is safe to call from a client widget. The component forwards to a Server_ RPC internally, runs the access check, and dispatches to the authoritative manager component.

MethodWhat it does
MoveItem(Target, EntryId, T)Move / swap / merge an entry within a target inventory
TransferItem(From, To, EntryId, Count)Atomic transfer between two inventories (player <-> chest)
SplitStack(Target, EntryId, Amount)Split off a sub-stack
DropItem(Target, EntryId, Count, Location)Drop to the world (spawns pickup actor if configured)
LockEntry(Target, EntryId, bLocked)Toggle the locked flag on an entry
OpenContainer(Container)Resolve scope + access, fire OnContainerOpened delegate on the client with the resolved UCrimsonInventoryManagerComponent*
TakePickupSlot(Pickup, SlotType, Index, Qty, Destination)Server-routed take of one slot from a ACrimsonPickupActorBase into Destination
TakePickupAll(Pickup, Destination)Server-routed take of every slot from a pickup; actor self-destroys when emptied

Open container flow (server -> client)

When OpenContainer(Chest) is called, the routing component:

1. Sends Server_RequestOpenContainer(Chest) to the server.
2. Server runs CanOpenContainer(Chest) - if false, sends a failure result back.
3. Resolves the inventory based on Chest->GetScope():
- Shared -> returns the chest's own component
- PerPlayer -> lazy-creates a private component on the requesting PC's routing component, keyed by ContainerTag
- PerParty / PerDungeonInstance -> asks the instance subsystem for an ACrimsonInstancedInventoryActor keyed by (ContainerTag, ResolvePartyKey() | ResolveDungeonInstanceKey()), adds the player to its viewer set
4. Sends Client_OnContainerOpened(Result) back to the requesting client.
5. Client's OnContainerOpened delegate fires with the resolved inventory pointer for UI binding.

cpp
// On the client (e.g. inventory widget):
InventoryRouting->OnContainerOpened.AddDynamic(this, &UMyInventoryWidget::HandleContainerOpened);
InventoryRouting->OpenContainer(InteractingChestActor);
void UMyInventoryWidget::HandleContainerOpened(const FCrimsonContainerOpenResult& Result)
{
if (Result.bSuccess)
{
BindContainerUI(Result.Inventory, Result.InstanceKey);
}
else
{
ShowError(Result.FailureReason);
}
}

Access control virtuals (override in BP or C++)

cpp
UFUNCTION(BlueprintNativeEvent, Category = "Routing|Access")
bool CanAccessInventory(UCrimsonInventoryManagerComponent* Inventory) const;
// default: owns own PC / PS / Pawn inventories + chest actors allowed.
// Override for stricter rules (e.g. only the chest you're currently interacting with).
UFUNCTION(BlueprintNativeEvent, Category = "Routing|Access")
bool CanOpenContainer(ACrimsonContainerActorBase* Container) const;
// default: allow. Override for proximity, faction, ownership, dungeon-membership checks.

Scope key resolvers (override for instanced scopes)

cpp
UFUNCTION(BlueprintNativeEvent, Category = "Routing|Resolvers")
FName ResolvePartyKey() const;
// default: NAME_None. Override to return the player's stable party id
// (any value - game-defined hash, UUID, party name). NAME_None aborts a
// PerParty open with a clear error message.
UFUNCTION(BlueprintNativeEvent, Category = "Routing|Resolvers")
FName ResolveDungeonInstanceKey() const;
// default: NAME_None. Override for PerDungeonInstance chests.

Personal containers (PerPlayer storage)

For PerPlayer chests, the routing component lazy-creates a UCrimsonInventoryManagerComponent as a dynamic subcomponent on the PC, keyed by the chest's ContainerTag. Because the routing component lives on the PC, these components replicate only to the owning client - privacy is automatic.

cpp
// Server: explicit lookup / creation
UCrimsonInventoryManagerComponent* GetOrCreatePersonalContainer(
FGameplayTag ContainerTag, UCrimsonInventoryLayout* Layout);
UCrimsonInventoryManagerComponent* FindPersonalContainer(FGameplayTag ContainerTag) const;
First-open BP hook
For PerPlayer / PerParty / PerDungeonInstance scopes, the chest actor's OnScopedInstanceCreated(NewInventory, Key) BP event fires once per scoped instance. Override it to roll loot, populate starter contents, or apply per-player customizations.

Save / Load - data-in / data-out

Snapshot the routing component's personal containers and hand the struct to your save system. No CrimsonSaveSystem dependency. Same flow works on dedicated server (server snapshots before disconnect), P2P / listen server (host snapshots), and standalone (client snapshots).

cpp
// Server: snapshot all personal containers
FCrimsonInventoryRoutingSaveData Save = Routing->GetSaveData();
MyGameSave->RoutingBlob = /* serialize Save */;
// Server: restore (typically after a re-login or load)
Routing->ApplySaveData(MyGameSave->RoutingBlob);
cpp
USTRUCT(BlueprintType)
struct FCrimsonRoutingPersonalContainerSave
{
FGameplayTag ContainerTag;
FCrimsonInventorySaveData Inventory; // reuses the per-component save format
};
USTRUCT(BlueprintType)
struct FCrimsonInventoryRoutingSaveData
{
TArray<FCrimsonRoutingPersonalContainerSave> PersonalContainers;
};
Server validates everything
The component's Server_ RPCs run CanAccessInventory / CanOpenContainer before any mutation. If you override those virtuals, keep the checks strict - never trust client-provided EntryIds without re-resolving them against the authoritative inventory.