Alliances
Alliances of groups - guilds when Guild is installed, otherwise single players: a group founds one, the leader invites, a group applies and the member groups vote it in (a configurable share), groups leave, the leader removes, hands over or disbands. Relationship markers between alliances, between groups and between accounts carry the game's own values (ally, enemy, neutral, whatever it defines). Durable across restarts. Elections, chat and seasonal scores stay with the game or other pieces: an election result calls SetLeader.
Seams you implement 2
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayAlliances(); and it wins.
IAllianceMembers game SPI
What an alliance is made of: the GROUP an account acts for (a guild, a clan, a house) and whether it may act for it. The default makes every account its own group - an alliance of players - so the piece works with no guild system at all; the Hosting bridge answers from Guild (rank decides who may act) when Guild is composed.
-
IReadOnlyList<long> AccountsOf(long groupId)The accounts of a group - who hears about its alliance.
-
bool TryGetGroup(long accountId, out long groupId, out bool mayAct)The group accountId belongs to, and whether it may act for that group (invite, apply, vote, leave, set relations). False when the account belongs to no group.
IAlliancePolicy game SPI
The game's say over alliances: who may found one, which group may apply where, which may walk straight in (e.g. "while the house has fewer than K groups"), and per-alliance numbers. Nothing is registered by default, and then alliances behave exactly as before. Every member has a default.
-
double? AdmitApprovalFraction(long allianceId)This alliance's approval share for applicants (null = AllianceOptions.AdmitApprovalFraction).
-
int? MaxMemberGroups(long allianceId)Most member groups this alliance may hold (null = AllianceOptions.MaxMemberGroups).
-
bool MayApply(long groupId, long allianceId)May this group apply to the alliance? Default: yes.
-
bool MayFound(long accountId)May this account found an alliance? Default: yes.
-
bool MayJoinDirectly(long groupId, long allianceId)Is this group admitted at once when it applies (no vote)? Default: no.
Providers you can swap 2
Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.
IAllianceClock provider
The clock alliances stamp founding times with (a seam so tests and replays control time).
-
long UtcNowMs { get; }Now, unix ms UTC.
IAllianceStore provider
Where alliances and relationship markers are kept. The default is in memory; persistence composes a document-backed one, so alliances survive a restart.
-
void Delete(long allianceId)Deletes an alliance.
-
IReadOnlyList<AllianceRecord> LoadAll()Every stored alliance.
-
IReadOnlyList<AllianceRelationRecord> LoadRelations()Every stored marker.
-
long NextId()The next alliance id.
-
void Save(AllianceRecord alliance)Saves (adds or replaces) an alliance.
-
void SaveRelation(AllianceRelationRecord relation)Sets a marker (relation 0 deletes it).
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IAllianceService
Alliances of groups (guilds, clans, houses - whatever IAllianceMembers says an account acts for): founding, invitations, applications admitted by a member vote, leaving, removal, hand-over, disbanding, and relationship markers between alliances, groups and accounts. Durable through IAllianceStore. Leadership elections, chat and seasonal scores are the game's (or other pieces'): SetLeader is the server-side hook an election result calls.
-
bool Add(long allianceId, long groupId)Server-side: admits a group directly (no invitation, no vote); the member cap still applies. Defaulted.
-
long AllianceOf(long groupId)The alliance a group belongs to (0 = none).
-
event Action<long, byte, long> ChangedRaised after every change: (alliance, AllianceChange, group concerned or 0).
-
long Create(string name, byte[] data, bool permanent)Server-side: creates an alliance with no founding group and no leader ("20 houses from world start"). permanent = it survives having no member groups and its leader cannot disband it. Returns the id (0 = the name is taken or invalid, or the data is too large). Defaulted so adding it breaks nothing (rule 25).
-
bool Disband(long allianceId)Server-side: disbands an alliance.
-
AllianceRecord Get(long allianceId)An alliance, or null.
-
byte Relation(byte scope, long fromId, long toId)A relationship marker (0 = none).
-
bool Remove(long allianceId, long groupId)Server-side: removes a member group (it may not be the leader).
-
bool SetData(long allianceId, byte[] data)Server-side: replaces the alliance's opaque public data.
-
bool SetLeader(long allianceId, long groupId)Server-side: makes a member group the leader (an election result, an admin action).
-
bool SetRelation(byte scope, long fromId, long toId, byte relation)Server-side: sets (or clears, with 0) a marker without any permission check.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
AllianceOptions
Alliances tuning (defaults here; never inlined in logic). Bound from Crossplay:Alliances.
-
double AdmitApprovalFraction { get; set; }Share of member groups whose approval admits an applicant: it needs floor(members x this) + 1 approvals, capped at the member count. 0 = any one member admits; 0.5 = a majority (the default); 1 = every member.
-
int ListPageSize { get; set; }Alliances per AllianceListRequest page.
-
int MaxDataBytes { get; set; }Largest opaque alliance data blob, in bytes.
-
int MaxMemberGroups { get; set; }Most member groups one alliance may hold.
-
int MaxNameLength { get; set; }Longest alliance name, in characters.
-
int MaxPendingPerAlliance { get; set; }Most invitations plus applications one alliance may have open at once.
-
int MaxRelationsPerOwner { get; set; }Most relationship markers one alliance, group or account may set in a scope.
-
int MinNameLength { get; set; }Shortest alliance name, in characters.
-
bool PublicAccountRelations { get; set; }When true, anyone may read an account's relationship markers; by default only the account itself.
Wire messages 17
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
AllianceCreateRequest Client -> server: found an alliance with my group as its leader.
AllianceResult The answer to every alliance verb.
AllianceGetRequest Client -> server: one alliance in full (0 = mine).
AllianceDetail Server -> client: one alliance in full.
AllianceListRequest Client -> server: a page of alliances, by id, after AfterId.
AllianceList Server -> client: a page of alliances.
AllianceInviteRequest Client -> server: the leader invites a group.
AllianceAnswerInviteRequest Client -> server: my group accepts or declines an invitation.
AllianceApplyRequest Client -> server: my group applies to join (or withdraws with Withdraw).
AllianceVoteRequest Client -> server: my member group votes on an applicant.
AllianceManageRequest Client -> server: leave, or (leader) disband / remove / hand over / uninvite.
AllianceSetRelationRequest Client -> server: set (or clear, with 0) a relationship marker from me - my alliance, my group or my account.
AllianceChanged Server -> client: something about an alliance you belong to (or are invited to) changed - refetch what you show.
AllianceRelationsRequest Client -> server: the markers FromId has set in Scope (account markers: only your own).
AllianceRelations Server -> client: relationship markers.
AllianceVotesRequest Client -> server: the yes and no votes on an alliance's pending applications.
AllianceVotes Server -> client: each pending application with its yes and no votes, and the caller's own vote.
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IAllianceClient
Client SDK for Alliances: every verb is one awaitable call (Core's request/reply helper - a call that gets no answer completes as a timeout, it never throws), and Changed tells you when an alliance you belong to (or are invited to) changed, so you refetch what you show. Groups are whatever the server's composition makes them (guilds when Guild is installed, otherwise each account). Pure SDK, no UI.
-
event Action<AllianceChanged> ChangedRaised when an alliance you belong to, or are invited to, changed.
-
UniTask<AllianceResult> CreateAsync(string name, CancellationToken ct = default)Founds an alliance led by my group.
-
UniTask<AllianceDetail> GetAsync(long allianceId = 0, CancellationToken ct = default)One alliance in full (0 = mine).
-
UniTask<AllianceList> ListAsync(long afterId = 0, CancellationToken ct = default)A page of alliances after afterId.
-
UniTask<AllianceResult> InviteAsync(long groupId, CancellationToken ct = default)(Leader) invites a group.
-
UniTask<AllianceResult> AnswerInviteAsync(long allianceId, bool accept, CancellationToken ct = default)Accepts or declines an invitation to my group.
-
UniTask<AllianceResult> ApplyAsync(long allianceId, bool withdraw = false, CancellationToken ct = default)My group applies to an alliance (or withdraws).
-
UniTask<AllianceResult> VoteAsync(long applicantGroupId, bool approve, CancellationToken ct = default)My member group votes on an applicant.
-
UniTask<AllianceResult> ManageAsync(byte action, long groupId = 0, CancellationToken ct = default)Leave, or (leader) disband / remove / hand over / uninvite - see AllianceManageAction.
-
UniTask<AllianceResult> SetRelationAsync(byte scope, long toId, byte relation, CancellationToken ct = default)Sets (or clears, with 0) a marker from my alliance, group or account - see RelationScope.
-
UniTask<AllianceRelations> RelationsAsync(byte scope, long fromId = 0, CancellationToken ct = default)The markers fromId set in a scope (0 = mine).
-
UniTask<AllianceVotes> VotesAsync(long allianceId = 0, CancellationToken ct = default)Each pending application of an alliance (0 = mine) with its yes and no votes, and how my group voted. Defaulted (rule 25).