World container actor

ACrimsonContainerActorBase is a replicating actor representing a chest, lootbox, vendor stash, or dungeon cache. Its Scope field selects who sees the contents.

ECrimsonContainerScope

ScopeStorageReplicationUse case
SharedComponent on the chest actorFastArray live to all relevant clientsCo-op shared loot, guild chests viewable by anyone in range
PerPlayerDynamic component on the requesting player's PlayerController routing componentOwner-only - only that player sees itPersonal stash, instanced single-player loot, private vendor inventories
PerPartyACrimsonInstancedInventoryActor spawned by UCrimsonInventoryInstanceSubsystemRestricted by IsNetRelevantFor to viewers in AllowedViewersParty-shared dungeon loot, group expedition chests
PerDungeonInstanceSame actor; keyed by dungeon-instance idSame - viewer set is everyone in the instancePer-run raid loot, dungeon-scoped trade caches
How scope keys are resolved
The routing component exposes two virtuals you override per game:

- ResolvePartyKey() -> FName - returns the player's stable party id (any value, including a hash). Used for PerParty containers.
- ResolveDungeonInstanceKey() -> FName - returns the current dungeon/instance id. Used for PerDungeonInstance.

Return NAME_None to opt out (the container open will fail with a clear error message rather than mis-route).

ACrimsonContainerActorBase

cpp
UCLASS(Abstract, Blueprintable)
class ACrimsonContainerActorBase : public AActor, public ICrimsonInteractableTarget
{
/** Stable identifier; required for non-Shared scopes (keys the per-scope instance). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
FGameplayTag ContainerTag;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
ECrimsonContainerScope Scope = ECrimsonContainerScope::Shared;
/** Assigned to the Shared inventory at BeginPlay, or to scoped instances on first creation. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
TObjectPtr<UCrimsonInventoryLayout> DefaultLayout;
/** Only used in Shared scope - null at runtime in other scopes. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Container")
TObjectPtr<UCrimsonInventoryManagerComponent> InventoryComponent;
/** BP hook fired once when a scoped instance is first created (PerPlayer / PerParty / PerDungeonInstance).
Override to roll loot or fill defaults. */
UFUNCTION(BlueprintImplementableEvent)
void OnScopedInstanceCreated(UCrimsonInventoryManagerComponent* NewInventory,
const FCrimsonContainerInstanceKey& Key);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container|Interaction")
TSubclassOf<UGameplayAbility> OpenContainerUIAbility;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container|Interaction")
FText InteractionText;
};

ACrimsonInstancedInventoryActor (PerParty / PerDungeonInstance backend)

Hidden server-spawned actor used as the storage for PerParty and PerDungeonInstance scopes. Wraps a UCrimsonInventoryManagerComponent and restricts replication to its AllowedViewers set via IsNetRelevantFor. UCrimsonInventoryInstanceSubsystem owns spawn/despawn - game code never deals with this actor directly; it's an implementation detail of the scope system.

cpp
// Server-only API (called by UCrimsonInventoryInstanceSubsystem):
void InitializeInstance(const FCrimsonContainerInstanceKey& Key, UCrimsonInventoryLayout* Layout);
void AddViewer(APlayerController* Viewer); // grants visibility
void RemoveViewer(APlayerController* Viewer); // revokes (close UI / leave party)
bool HasViewer(APlayerController* Viewer) const;
int32 GetViewerCount() const;
// IsNetRelevantFor returns true iff the viewer's PC is in AllowedViewers.

UCrimsonInventoryInstanceSubsystem

UWorldSubsystem that exists only on server / standalone. Tracks all PerParty and PerDungeonInstance instance actors keyed by FCrimsonContainerInstanceKey { ContainerTag, ScopeKey }. Created lazily on first open, torn down with the world. Game code rarely calls this directly - the routing component does.

cpp
ACrimsonInstancedInventoryActor* GetOrCreateInstance(
const FCrimsonContainerInstanceKey& Key, UCrimsonInventoryLayout* Layout,
APlayerController* Viewer, bool& bWasCreated);
ACrimsonInstancedInventoryActor* FindInstance(const FCrimsonContainerInstanceKey& Key) const;
void RemoveViewer(const FCrimsonContainerInstanceKey& Key, APlayerController* Viewer);
void RemoveViewerFromAll(APlayerController* Viewer); // call on disconnect
void DestroyInstance(const FCrimsonContainerInstanceKey& Key); // party disbands, dungeon ends
void GetAllInstanceKeys(TArray<FCrimsonContainerInstanceKey>& OutKeys) const;
Pick the right tool
Shared world chest (anyone in range sees the same loot) -> ACrimsonContainerActorBase with Scope = Shared.

Private personal stash (each player has their own) -> Scope = PerPlayer. Storage on the PC routing component.

Party-shared dungeon loot -> Scope = PerParty + override ResolvePartyKey().

Per-instance raid cache -> Scope = PerDungeonInstance + override ResolveDungeonInstanceKey().

Persistent virtual stash that lives outside the world (bank, mailbox) -> UCrimsonContainerSubsystem (next section).

Container subsystem (purely virtual)

UCrimsonContainerSubsystem manages named persistent inventories keyed by FGameplayTag. It is not replicated - it exists independently on the server and on each client. Use it for banks, guild vaults, and any stash that doesn't have a world actor.

Container accessibility scopes

ScopeDefault tagWhen accessible
GlobalTAG_Crimson_Container_GlobalAlways
LevelTAG_Crimson_Container_Local_LevelWhen the container tag contains the current level name
ZoneTAG_Crimson_Container_Local_ZoneWhen SetCurrentZone has been called with a matching tag
cpp
UCrimsonContainerSubsystem* CS = GetGameInstance()->GetSubsystem<UCrimsonContainerSubsystem>();
// Server only
CS->RegisterContainer(TAG_Container_GuildBank, 0 /*unlimited*/);
FCrimsonInventoryPickup* Data = CS->GetContainerData(TAG_Container_GuildBank);
FCrimsonInventoryPickup Copy = CS->GetContainerDataBP(TAG_Container_GuildBank);
// Move uses the player entry's stable EntryId (server-only)
CS->MoveItemToContainer(PlayerInventory, SourceEntryId,
TAG_Container_GuildBank, /*StackCount*/ 1);
CS->MoveItemFromContainer(PlayerInventory, TAG_Container_GuildBank,
ItemInstance, /*StackCount*/ 1);
// Zone scoping (call before opening a crafting station or zone chest)
CS->SetCurrentZone(TAG_Container_Local_Zone_Blacksmith);
// ... interaction ...
CS->ClearCurrentZone();

Cross-inventory consumption

cpp
TArray<FCrimsonItemConsumptionRequest> Ingredients = {
{ UIronOreDefinition::StaticClass(), 3 },
{ UWoodDefinition::StaticClass(), 2 }
};
bool bSuccess = CS->ConsumeItemsWithPriority(
PlayerInventory,
TAG_Crimson_Inventory_Player,
Ingredients,
{ TAG_Crimson_Inventory_Player, TAG_Container_Local_Zone_Blacksmith });