Guild
Persistent guilds: officer ranks, MOTD, invites/kicks, leader handoff.
Seams you implement 1
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayGuild(); and it wins.
IGuildPolicy game SPI
The game's say over guild membership: who may found a guild, invite, apply and join. Nothing is registered by default, and then the guild behaves exactly as it always has. Every member has a default (yes), so implement only what you need - a minimum level, a closed guild, a faction rule. Account-keyed, so it also judges server-side operations for offline players. Server code calling Join is not asked.
-
bool MayApply(long accountId, long guildId)May this account apply to the guild? Default: yes.
-
bool MayCreate(long accountId, string name)May this account found a guild with this name? Default: yes.
-
bool MayInvite(long guildId, long inviterAccountId, long targetAccountId)May this member invite that account? Default: yes.
-
bool MayJoin(long accountId, long guildId)May this account join the guild (accepting an invite, or an accepted application)? Default: yes.
-
bool MaySetRank(long guildId, long actorAccountId, long targetAccountId, byte rank)May the leader give this member this rank (e.g. an officer cap)? Asked after the built-in rules; not asked when the server sets a rank. Default: yes.
Providers you can swap 1
Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.
IGuildStore provider
Guild persistence SPI — the seam that owns where and how guild data lives. The GAME supplies the real store (its own schema, its own database); the base ships InMemoryGuildStore for dev and tests. Invariant: one guild per account.
-
void AddApplication(long accountId, long guildId)Records an account's application to join a guild.
-
void AddInvite(long accountId, long guildId)Records a pending invite of an account to a guild.
-
void AddMember(long guildId, long accountId)Adds an account as a member of a guild.
-
IReadOnlyList<long> AllGuildIds()Every guild id, ascending - what a "browse guilds" listing pages over. Defaulted to none (rule 25).
-
IReadOnlyList<long> ApplicantsOf(long guildId)The accounts that have applied to a guild.
-
IReadOnlyList<long> ApplicationsFor(long accountId)The guilds an account has applied to.
-
GuildRecord Create(string name, long leaderAccountId)Creates and persists a new guild; the leader is also added as a member.
-
void Delete(long guildId)Deletes an empty guild entirely (members/invites/officers included).
-
GuildRecord GetByAccount(long accountId)The guild the account belongs to, or null.
-
GuildRecord GetById(long id)A guild by id, or null.
-
byte[] GetData(long guildId)The guild's opaque game data (empty when unset).
-
string GetMotd(long guildId)The guild's message of the day (empty when unset).
-
bool HasApplication(long accountId, long guildId)True when the account has applied to the guild.
-
bool HasInvite(long accountId, long guildId)True if the account has a pending invite to the guild.
-
IReadOnlyList<long> InvitedTo(long guildId)The accounts a guild has a pending invite out to.
-
IReadOnlyList<long> InvitesFor(long accountId)The guilds an account has a pending invite to.
-
bool IsNameTaken(string name)True if the name is already in use (case-insensitive).
-
bool IsOfficer(long guildId, long accountId)True when the account is an officer of the guild.
-
bool IsOpen(long guildId)True when the guild accepts applications (closed by default).
-
IReadOnlyList<long> Members(long guildId)The account ids of a guild's members.
-
IReadOnlyDictionary<long, byte> RelationsOf(long guildId)This guild's stances toward other guilds (neutral ones are absent).
-
IReadOnlyDictionary<long, byte> RelationsToward(long guildId)Other guilds' stances toward this one (neutral ones are absent).
-
void RemoveApplication(long accountId, long guildId)Clears an account's application to a guild.
-
void RemoveApplicationsOf(long accountId)Clears every application an account has made (it just joined or founded a guild).
-
void RemoveInvite(long accountId, long guildId)Clears a pending invite of an account to a guild.
-
void RemoveMember(long guildId, long accountId)Removes an account from a guild.
-
void SetData(long guildId, byte[] data)Stores the guild's opaque game data (empty clears).
-
void SetLeader(long guildId, long accountId)Transfers guild leadership.
-
void SetMotd(long guildId, string motd)Sets the guild's message of the day (empty clears).
-
void SetOfficer(long guildId, long accountId, bool officer)Marks/unmarks an account as an officer of a guild.
-
void SetOpen(long guildId, bool open)Opens or closes a guild to applications.
-
void SetRelation(long guildId, long otherGuildId, byte relation)Sets this guild's stance toward another (0 = neutral, which clears it).
Services you call 2
Crossplay implements these. Resolve them from DI and call them from your own systems.
IGuildMembership
Narrow, READ-ONLY membership/rank query seam over the guild data — the small contract other pieces (e.g. a Guild Vault) depend on so they can gate on guild membership + rank WITHOUT reaching into the mutation surface of IGuildStore (Create/Delete/AddMember/…). Rank is the same byte vocabulary as GuildRank (Member < Officer < Leader).
-
IReadOnlyList<long> Members(long guildId)The account ids of a guild's members (empty when the guild is unknown).
-
bool TryGetMembership(long accountId, out long guildId, out byte rank)Resolves the guild an account belongs to and their rank. Returns false (with guildId = 0, rank = Member) when the account is in no guild.
IGuildService
Guild operations from SERVER code, by account id - no session needed, so game rules can drive membership for players who are offline. Each returns 0 on success or a GuildNotificationCodes value. Guild-scoped changes take the acting account; pass Server (0) to act as the server, which skips the rank checks. Online members get the same room events and pushes as when they act themselves.
-
ushort AcceptInvite(long accountId, long guildId)Accepts an invite sent to the account (the policy is asked).
-
event Action<long, long, bool> AccountMembershipChangedRaised on EVERY membership change, wire or server, online or offline: (accountId, guildId, joined).
-
ushort AnswerApplication(long guildId, long actorAccountId, long applicantAccountId, bool accept)Accepts (they join) or declines an application (officer/leader, or the server).
-
IReadOnlyList<long> ApplicantsOf(long guildId)The accounts that have applied to a guild.
-
event Action<long, long, bool, long> ApplicationResolvedRaised when a pending application ends: (guildId, accountId, accepted, actorAccountId). Accepted = the applicant joined that guild (actor = who accepted; the applicant when they joined it through an invite; 0 when the server put them in). Not accepted = declined by the actor, withdrawn by the applicant (actor = applicant), or dropped because the applicant joined or founded another guild (actor = applicant) or the guild disbanded (actor = the last member out). Every application that Applied announced ends here exactly once. Defaulted to an event that never fires (rule 25); the shipped GuildHandlers raises it.
-
IReadOnlyList<long> ApplicationsFor(long accountId)The guilds the account has applied to.
-
event Action<long, long> AppliedRaised when an account files a NEW application to a guild (not when it was already waiting): (guildId, accountId). Defaulted to an event that never fires (rule 25); the shipped GuildHandlers raises it.
-
ushort Apply(long accountId, long guildId)Applies to an open guild (the policy is asked).
-
ushort Create(long leaderAccountId, string name, out long guildId)Founds a guild with the account as leader (the game's policy is asked).
-
ushort DeclineInvite(long accountId, long guildId)Declines an invite sent to the account.
-
GuildListing Info(long guildId)A guild's public listing, or null.
-
ushort Invite(long guildId, long inviterAccountId, long targetAccountId)Invites an account to a guild - it waits for them if they are offline. Inviter Server = the server (the policy is not asked); otherwise the inviter must be a member.
-
IReadOnlyList<long> InvitedTo(long guildId)The accounts a guild has invited.
-
IReadOnlyList<long> InvitesFor(long accountId)The guilds that have invited the account.
-
ushort Join(long accountId, long guildId)Puts an account in a guild directly - no invite, no policy (the server is the authority). The member cap still applies.
-
ushort Kick(long guildId, long actorAccountId, long targetAccountId)Kicks a member (leader: anyone but themselves; officer: plain members; the server: anyone but the leader).
-
ushort Leave(long accountId)Takes an account out of its guild as if it left (a leaving leader hands over; the last one out disbands).
-
IReadOnlyList<GuildListing> List(int skip, int take, out int total)A page of every guild with members, by id (skip, take capped by GuildOptions.MaxListPage), and how many there are in all. Defaulted (none) so adding it breaks nothing.
-
event Action<long, long, long> MemberRemovedRaised when a member leaves or is removed, wire or server, beside AccountMembershipChanged: (guildId, accountId, actorAccountId). Actor == account = they left; any other account = kicked by that member; Server (0) = removed by the server. Defaulted to an event that never fires (rule 25); the shipped GuildHandlers raises it.
-
event Action<long, long, byte, byte, long> RankChangedRaised when a member's rank changes, wire or server: (guildId, accountId, oldRank, newRank, actorAccountId) with ranks as GuildRank values. Covers a set rank (only when it actually changes), a leadership hand-over (the new leader up to Leader; the old leader down to Member) and a leaving leader's heir (actor = the leaver). Actor Server (0) = the server. Defaulted to an event that never fires (rule 25), so an existing implementation keeps compiling; the shipped GuildHandlers raises it.
-
event Action<long, long, byte> RelationChangedRaised when a guild changes its stance toward another: (guildId, otherGuildId, relation).
-
IReadOnlyList<GuildRelationRecord> Relations(long guildId)A guild's relations, either side.
-
ushort SetData(long guildId, long actorAccountId, byte[] data)Sets a guild's opaque game data, size-capped (officer/leader, or the server).
-
ushort SetOpen(long guildId, long actorAccountId, bool open)Opens or closes a guild to applications (officer/leader, or the server).
-
ushort SetRank(long guildId, long actorAccountId, long targetAccountId, byte rank)Sets a member's rank, Member or Officer (the leader, or the server).
-
ushort SetRelation(long guildId, long actorAccountId, long otherGuildId, byte relation)Sets a guild's stance toward another (officer/leader, or the server); both guilds are told.
-
ushort TransferLeadership(long guildId, long actorAccountId, long newLeaderAccountId)Hands leadership to another member (the leader, or the server). The old leader stays as a member; the new leader's officer mark is cleared, as in a leaving leader's hand-off. Defaulted so adding it breaks nothing.
-
ushort WithdrawApplication(long accountId, long guildId)Withdraws the account's application.
-
ushort WithdrawInvite(long guildId, long actorAccountId, long targetAccountId)Withdraws a guild's pending invite (officer/leader, or the server).
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
GuildOptions
Configurable guild rules — every tunable for the Guild piece. Defaults live here (never inline in a handler), so a game overrides only what it needs via the configure callback of AddCrossplayGuild. All values have working defaults; the object is safe to use unconfigured.
-
int MaxApplicationsPerAccount { get; set; }Most guilds one account may have applied to at once. Default 5.
-
int MaxDataBytes { get; set; }Largest opaque guild data blob, in bytes. Default 256.
-
int MaxListPage { get; set; }The most guilds one listing page returns. Default 50.
-
int MaxMembers { get; set; }Maximum members per guild, including the leader; accepts over this are refused. Default 50.
-
int MaxMotdLength { get; set; }Longest allowed message of the day (trimmed); longer sets are rejected. Default 200.
-
int MaxNameLength { get; set; }Maximum guild name length (trimmed); longer creates are rejected. Default 24.
-
int MaxPendingApplications { get; set; }Most outstanding applications one guild may have. Default 50.
-
int MaxPendingInvites { get; set; }Most outstanding invites one guild may have. Default 50.
-
int MaxRelations { get; set; }Most non-neutral stances one guild may hold. Default 50.
-
int MinNameLength { get; set; }Minimum guild name length (trimmed); shorter creates are rejected. Default 3.
-
bool OfflineInvites { get; set; }A by-name invite to a player who is offline is stored for them (found through the account store) instead of failing with TargetOffline. Default off - the old behaviour. Invites by account id always wait.
Wire messages 40
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
GuildCreateRequest Client -> server: create a guild with the given name (the sender becomes leader).
GuildCreateResponse Server -> client: result of a guild creation.
GuildInviteRequest Client -> server: invite a player (by display name) to the sender's guild.
GuildInviteResponse Server -> client: result of an invite (delivered to the inviter).
GuildInviteNotification Server -> client: an invite delivered to the invited target.
GuildAcceptRequest Client -> server: accept a pending invite to the given guild.
GuildAcceptResponse Server -> client: result of accepting a guild invite.
GuildLeaveRequest Client -> server: leave the sender's current guild.
GuildLeaveResponse Server -> client: result of leaving a guild.
GuildRosterRequest Client -> server: request the sender's guild roster.
GuildRoster Server -> client: the full guild roster.
GuildEvent Server -> client: a guild room event (member joined/left), broadcast to the guild room.
GuildRankSetRequest Client → server: set a member's rank (leader only; Member ↔ Officer).
GuildRankSetResponse Server → client: rank-set result.
GuildKickRequest Client → server: kick a member (officer/leader; officers cannot kick officers).
GuildKickResponse Server → client: kick result.
GuildMotdSetRequest Client → server: set the guild message of the day (officer/leader; empty clears).
GuildMotdSetResponse Server → client: MOTD-set result.
GuildResult The answer to every guild change that returns no data.
GuildInviteByAccountRequest Client -> server: invite an account to my guild - it waits for them when they are offline.
GuildWithdrawInviteRequest Client -> server: withdraw an invite my guild sent (officer/leader).
GuildDeclineInviteRequest Client -> server: decline an invite sent to me.
GuildMyInvitesRequest Client -> server: the guilds that have invited me.
GuildListings Server -> client: guild listings (my invites, my applications).
GuildPendingRequest Client -> server: my guild's outstanding invites and applications (officer/leader).
GuildPending Server -> client: my guild's outstanding invites and applications.
GuildApplyRequest Client -> server: apply to join a guild (it must be open to applications).
GuildWithdrawApplicationRequest Client -> server: withdraw my application to a guild.
GuildMyApplicationsRequest Client -> server: the guilds I have applied to.
GuildAnswerApplicationRequest Client -> server: accept or decline an application to my guild (officer/leader).
GuildSetOpenRequest Client -> server: open or close my guild to applications (officer/leader).
GuildInfoRequest Client -> server: a guild's public listing.
GuildInfo Server -> client: a guild's public listing.
GuildSetDataRequest Client -> server: set my guild's opaque game data (officer/leader; empty clears).
GuildSetRelationRequest Client -> server: set my guild's stance toward another guild (officer/leader).
GuildRelationsRequest Client -> server: my guild's relations.
GuildRelations Server -> client: my guild's relations (every guild either side has a stance toward).
GuildRelationChanged Server -> client (push, to both guilds' rooms): a guild changed its stance toward another.
GuildListRequest Client -> server: a page of every guild with members, by id (browse).
GuildListPage Server -> client: a page of every guild, and how many there are.
Unity services you inject 2
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IGuildClient
Data-layer guild service (no UI): async create/invite/accept/leave/roster RPCs plus events for inbound invites and guild room events. Self-registers under the Crossplay client context via the framework's attribute discovery.
-
event Action<GuildInviteNotification> InviteReceivedRaised when an invite is delivered to this client.
-
event Action<GuildEvent> EventRaised for a guild room event (member joined/left).
-
UniTask<GuildCreateResponse> CreateAsync(string name, CancellationToken ct = default)Creates a guild with the given name (the sender becomes leader).
-
UniTask<GuildInviteResponse> InviteAsync(string targetName, CancellationToken ct = default)Invites a player (by display name) to the sender's guild.
-
UniTask<GuildAcceptResponse> AcceptAsync(long guildId, CancellationToken ct = default)Accepts a pending invite to the given guild.
-
UniTask<GuildLeaveResponse> LeaveAsync(CancellationToken ct = default)Leaves the sender's current guild.
-
UniTask<GuildRoster> RosterAsync(CancellationToken ct = default)Requests the sender's guild roster.
-
UniTask<GuildRankSetResponse> RankSetAsync(long targetAccountId, byte rank, CancellationToken ct = default)Sets a member's rank (leader only; Member ↔ Officer).
-
UniTask<GuildKickResponse> KickAsync(long targetAccountId, CancellationToken ct = default)Kicks a member (officer/leader).
-
UniTask<GuildMotdSetResponse> SetMotdAsync(string motd, CancellationToken ct = default)Sets the guild MOTD (officer/leader; empty clears).
IGuildMembershipClient
The request/reply guild verbs, beside IGuildClient: invites that wait for offline players, applications to open guilds, public guild listings, the guild's opaque data and relations between guilds. Each call is one awaitable request (Core's request/reply helper - it never throws; a timeout completes unsuccessfully). RelationChanged fires when my guild or one it has a stance with changes a stance. Pure SDK, no UI.
-
event Action<long, long, byte> RelationChangedA guild changed its stance toward another: (guildId, otherGuildId, relation - see GuildRelation).
-
UniTask<GuildResult> InviteAsync(long targetAccountId, CancellationToken ct = default)Invites an account to my guild; it waits for them if they are offline.
-
UniTask<GuildResult> WithdrawInviteAsync(long targetAccountId, CancellationToken ct = default)Withdraws an invite my guild sent (officer/leader).
-
UniTask<GuildResult> DeclineInviteAsync(long guildId, CancellationToken ct = default)Declines an invite sent to me.
-
UniTask<GuildListings> MyInvitesAsync(CancellationToken ct = default)The guilds that have invited me.
-
UniTask<GuildPending> PendingAsync(CancellationToken ct = default)My guild's outstanding invites and applications (officer/leader).
-
UniTask<GuildResult> ApplyAsync(long guildId, CancellationToken ct = default)Applies to an open guild.
-
UniTask<GuildResult> WithdrawApplicationAsync(long guildId, CancellationToken ct = default)Withdraws my application.
-
UniTask<GuildListings> MyApplicationsAsync(CancellationToken ct = default)The guilds I have applied to.
-
UniTask<GuildResult> AnswerApplicationAsync(long applicantAccountId, bool accept, CancellationToken ct = default)Accepts or declines an application to my guild (officer/leader).
-
UniTask<GuildResult> SetOpenAsync(bool open, CancellationToken ct = default)Opens or closes my guild to applications (officer/leader).
-
UniTask<GuildInfo> InfoAsync(long guildId, CancellationToken ct = default)A guild's public listing (name, member count, open, data).
-
UniTask<GuildResult> SetDataAsync(byte[] data, CancellationToken ct = default)Sets my guild's opaque game data (officer/leader; size-capped by the server).
-
UniTask<GuildResult> SetRelationAsync(long otherGuildId, byte relation, CancellationToken ct = default)Sets my guild's stance toward another guild (officer/leader).
-
UniTask<GuildRelations> RelationsAsync(CancellationToken ct = default)My guild's relations, both directions.
-
UniTask<GuildListPage> ListAsync(int skip = 0, int take = 0, CancellationToken ct = default)A page of every guild (browse), with the total. Defaulted so an existing implementation of this interface still compiles (rule 25).
UI views you can replace 1
Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.
ICrossplayGuildView
Narrow contract the presenter drives. UI-only: render calls in, user intents out.