guild

Guild

Persistent guilds: officer ranks, MOTD, invites/kicks, leader handoff.

Category Social Seams 2 Services 2 Options 1 Wire ids 40 Unity types 3
Server
services.AddCrossplayGuild();
Unity package
com.crossplay.guild
Depends on
core

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> AccountMembershipChanged

    Raised 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> ApplicationResolved

    Raised 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> Applied

    Raised 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> MemberRemoved

    Raised 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> RankChanged

    Raised 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> RelationChanged

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

1101 GuildCreateRequest

Client -> server: create a guild with the given name (the sender becomes leader).

1102 GuildCreateResponse

Server -> client: result of a guild creation.

1103 GuildInviteRequest

Client -> server: invite a player (by display name) to the sender's guild.

1104 GuildInviteResponse

Server -> client: result of an invite (delivered to the inviter).

1105 GuildInviteNotification

Server -> client: an invite delivered to the invited target.

1106 GuildAcceptRequest

Client -> server: accept a pending invite to the given guild.

1107 GuildAcceptResponse

Server -> client: result of accepting a guild invite.

1108 GuildLeaveRequest

Client -> server: leave the sender's current guild.

1109 GuildLeaveResponse

Server -> client: result of leaving a guild.

1110 GuildRosterRequest

Client -> server: request the sender's guild roster.

1111 GuildRoster

Server -> client: the full guild roster.

1112 GuildEvent

Server -> client: a guild room event (member joined/left), broadcast to the guild room.

1113 GuildRankSetRequest

Client → server: set a member's rank (leader only; Member ↔ Officer).

1114 GuildRankSetResponse

Server → client: rank-set result.

1115 GuildKickRequest

Client → server: kick a member (officer/leader; officers cannot kick officers).

1116 GuildKickResponse

Server → client: kick result.

1117 GuildMotdSetRequest

Client → server: set the guild message of the day (officer/leader; empty clears).

1118 GuildMotdSetResponse

Server → client: MOTD-set result.

1119 GuildResult

The answer to every guild change that returns no data.

1120 GuildInviteByAccountRequest

Client -> server: invite an account to my guild - it waits for them when they are offline.

1121 GuildWithdrawInviteRequest

Client -> server: withdraw an invite my guild sent (officer/leader).

1122 GuildDeclineInviteRequest

Client -> server: decline an invite sent to me.

1123 GuildMyInvitesRequest

Client -> server: the guilds that have invited me.

1124 GuildListings

Server -> client: guild listings (my invites, my applications).

1125 GuildPendingRequest

Client -> server: my guild's outstanding invites and applications (officer/leader).

1126 GuildPending

Server -> client: my guild's outstanding invites and applications.

1127 GuildApplyRequest

Client -> server: apply to join a guild (it must be open to applications).

1128 GuildWithdrawApplicationRequest

Client -> server: withdraw my application to a guild.

1129 GuildMyApplicationsRequest

Client -> server: the guilds I have applied to.

1130 GuildAnswerApplicationRequest

Client -> server: accept or decline an application to my guild (officer/leader).

1131 GuildSetOpenRequest

Client -> server: open or close my guild to applications (officer/leader).

1132 GuildInfoRequest

Client -> server: a guild's public listing.

1133 GuildInfo

Server -> client: a guild's public listing.

1134 GuildSetDataRequest

Client -> server: set my guild's opaque game data (officer/leader; empty clears).

1135 GuildSetRelationRequest

Client -> server: set my guild's stance toward another guild (officer/leader).

1136 GuildRelationsRequest

Client -> server: my guild's relations.

1137 GuildRelations

Server -> client: my guild's relations (every guild either side has a stance toward).

1138 GuildRelationChanged

Server -> client (push, to both guilds' rooms): a guild changed its stance toward another.

1139 GuildListRequest

Client -> server: a page of every guild with members, by id (browse).

1140 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> InviteReceived

    Raised when an invite is delivered to this client.

  • event Action<GuildEvent> Event

    Raised 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> RelationChanged

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