Shared Containers (Group Kit)
Containers owned by a group rather than a character — a crew kit, a party stash, a team's shared crate — over the Container engine: the contents are pushed to every session the game's audience seam admits, and only those sessions may read or rearrange them. Items enter and leave through server-side game verbs (atomic exchanges with a member's bag); the wire carries request and move only, so a client is never trusted to say what it takes. Inert until an audience is composed.
Seams you implement 1
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplaySharedContainers(); and it wins.
ISharedContainerAudience game SPI
The game's answer to "who sees this container?" — the one decision that makes a container SHARED. Consulted on every wire request and move (may this session act on it?) and on every change (who gets the fresh contents pushed?). The framework never knows what a scope is: a crew, a party, a team, a match, a household, a guild — the game names its containers and answers for them here.
// A crew kit: "crew:{matchId}" is visible to everyone in that match.
public sealed class CrewKitAudience : ISharedContainerAudience
{
public bool CanSee(ISession s, ContainerId c)
=> c.TryGetOwner("crew", out long matchId) && _matches.IsIn(matchId, s);
public void CollectAudience(ContainerId c, List<ISession> into)
{
if (c.TryGetOwner("crew", out long matchId))
_matches.CollectSessions(matchId, into);
}
} -
bool CanSee(ISession session, ContainerId container)May session read and rearrange container?
-
void CollectAudience(ContainerId container, List<ISession> into)Adds every session that should receive container's contents after a change to into (which the piece clears and reuses — never keep a reference).
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
ISharedContainerService
A container owned by a SCOPE rather than a character — a crew kit, a party stash, a team's shared ammo crate, a household chest — whose contents are pushed to every session the game says can see it. The item engine underneath is Crossplay.Container (atomic moves, stacking policy, persistence); this role adds the two things a shared container needs on top: WHICH containers are shared, and WHO sees them (ISharedContainerAudience).
-
event Action<ContainerId> ChangedRaised after any change to a shared container — the handlers push its contents to the audience.
-
int CountOf(ContainerId id, byte[] item)Total count of an item across the container.
-
void Ensure(ContainerId id)Marks id as a shared container (creating it with the configured shape if it does not exist). Every other call does this implicitly.
-
IReadOnlyList<SharedContainerSlot> Get(ContainerId id)Occupied slots (a fresh list — a snapshot, not a view).
-
bool Give(ContainerId id, byte[] item, int count)Adds count of an opaque item, stacking then filling; all-or-nothing.
-
bool IsShared(ContainerId id)True once id has been shared through this service.
-
ushort Move(ContainerId id, int fromSlot, int toSlot)Moves / merges / swaps two slots. 0 on success, else a SharedContainerNotificationCodes value.
-
bool Take(ContainerId id, byte[] item, int count)Removes count of an item across stacks; all-or-nothing.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
SharedContainerOptions
Tunables for shared containers (defaults here, never inline — rule 4). Bound from Crossplay:SharedContainers by the composition layer. One configuration applies to every shared container the piece creates; a game that needs a differently-shaped shared container ensures it on the engine itself with its own ContainerConfig before this piece first touches it.
-
bool GridFootprints { get; set; }When true, items occupy multi-cell footprints on a grid of GridWidth columns.
-
int GridWidth { get; set; }Grid width in cells when GridFootprints is on (0 = linear slots).
-
int MaxContainerIdLength { get; set; }Longest container id a client may name on the wire. An id is a small engine key (crew:42); anything longer is refused before it reaches the audience seam, so a hostile client cannot make the server hash, log or echo kilobytes of its choosing.
-
int MaxItemBytes { get; set; }Maximum opaque item blob size, in bytes.
-
int MaxStack { get; set; }Maximum stack count per slot (the engine's default policy).
-
int SlotCount { get; set; }Slots per shared container.
Wire messages 4
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
SharedContainerRequest Client → server: send me the contents of the shared container ContainerId. The server answers only when this session is in the container's audience (the game's ISharedContainerAudience decides); otherwise the reply carries NotVisible and nothing else — a client cannot enumerate containers it was not given.
SharedContainerContents Server → client: a shared container's full contents. Sent in reply to a request, and PUSHED to every session in the container's audience after any change (a move, a game-side give/take), so a crew all see the same kit without polling.
SharedContainerMoveRequest Client → server: move / merge / swap two slots inside a shared container this session can see.
SharedContainerMoveResponse Server → client: the verdict on a SharedContainerMoveRequest. On success the fresh contents follow as a pushed SharedContainerContents, to every member of the audience.
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
ISharedContainersClient
Client SDK for shared containers: request the contents of a container this session may see, move slots inside it, and observe the snapshot the server pushes to every member after any change. The server decides who sees what; a refused request completes with a non-zero ReasonCode and no slots. What a container IS — a crew kit, a stash, a crate — and how it looks are the game's.
-
bool TryGetContents(string containerId, out SharedContainerContents contents)The last contents received for containerId, if any.
-
event Action<SharedContainerContents> ContentsChangedRaised whenever contents arrive — a reply, or a push after a change by anyone.
-
UniTask<SharedContainerContents> RequestAsync(string containerId, CancellationToken ct = default)Requests the contents. Completes with ReasonCode != 0 when the server refuses.
-
UniTask<SharedContainerMoveResponse> MoveAsync(string containerId, int fromSlot, int toSlot, CancellationToken ct = default)Moves / merges / swaps two slots. On success the fresh contents follow as a push.