Interface IGuildRankService
- Namespace
- FishMMO.Database.Npgsql.Services.Interfaces
- Assembly
- FishMMO-DB.dll
Service interface for a guild's editable rank ladder.
public interface IGuildRankService
Remarks
The rank rows are the authority for what a member may do. Nothing in this interface takes a requester: authorisation is the server's job and it happens before these calls, against the rows FetchManyAsync(long, CancellationToken) returns. A storage service that also decided permissions would be two responsibilities with one test surface.
Methods
DeleteAsync(long, byte, CancellationToken)
Deletes a rank row, refusing while any member still holds it.
Task<DatabaseResult> DeleteAsync(long guildId, byte rankOrder, CancellationToken cancellationToken = default)
Parameters
guildIdlongGuild ID.
rankOrderbyteThe rank position to delete.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult>
A result indicating success or failure.
Remarks
The occupancy test and the delete are ONE statement. Split across two round trips, a member could be moved into the rank between them and end up holding a rank that no longer exists — which reads back as no permissions at all, quietly, forever.
EnsureDefaultsAsync(long, IReadOnlyList<GuildRankData>, CancellationToken)
Creates the default rank ladder for a guild if it does not already have one.
Task<DatabaseResult<int>> EnsureDefaultsAsync(long guildId, IReadOnlyList<GuildRankData> defaults, CancellationToken cancellationToken = default)
Parameters
guildIdlongGuild ID.
defaultsIReadOnlyList<GuildRankData>The rows to seed, in any order.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<int>>
The number of rows actually inserted.
Remarks
IDEMPOTENT. Every row is inserted with ON CONFLICT (guild_id, rank_order) DO
NOTHING, so running this against a guild that already has ranks changes nothing and
returns zero. That is what makes it safe to call on every guild read, which is in turn
what makes the migration of existing guilds require no migration step at all: a guild
created before rank rows existed grows them the first time anybody looks at it, with
the permissions its old enum ranks implied.
FetchManyAsync(long, CancellationToken)
Fetches every rank row for a guild, ordered by rank order ascending.
Task<DatabaseResult<IReadOnlyList<GuildRankData>>> FetchManyAsync(long guildId, CancellationToken cancellationToken = default)
Parameters
guildIdlongGuild ID.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<IReadOnlyList<GuildRankData>>>
The guild's rank ladder.
InsertAsync(GuildRankData, int, byte, CancellationToken)
Inserts a new rank at a position, moving everything already at or above it up one rung.
Task<DatabaseResult> InsertAsync(GuildRankData rank, int maxRanks, byte maxRankOrder, CancellationToken cancellationToken = default)
Parameters
rankGuildRankDataThe rank to insert. Its rank order is the POSITION it takes.
maxRanksintMaximum rank rows one guild may own.
maxRankOrderbyteHighest legal rank order, which bounds the shift.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult>
A result indicating success or failure.
Remarks
Why this inserts rather than fills a hole. A guild is seeded with three CONTIGUOUS ranks — member 1, officer 2, leader 3 — and a rank may only be created strictly below the creator's own seat. A leader at order 3 therefore has orders 1 and 2 to choose from and both are taken, so a "create at a free position" service could never add a rank to a guild that had not already had one removed. Making room is the operation; finding a gap was never going to find one.
Membership rows move with the ladder. character_guild.rank holds the
same order byte, so every member at or above the insertion point is shifted by the same
one rung, in the same transaction. Relative standing is what the whole permission model
compares, and it is preserved exactly: a member who outranked another before the insert
still does, and the leader — defined as the highest order that exists — is still the
same character.
The shift is applied one row at a time, highest first.
(guild_id, rank_order) is UNIQUE and PostgreSQL checks it per row rather than at
statement end, so a single SET rank_order = rank_order + 1 over the whole range
raises a duplicate key the moment the planner happens to reach a row whose target is
still occupied. Descending, each target has just been vacated. Twelve ranks is the cap,
so this is at most eleven statements for something a guild does once in a while.
UpdateAsync(long, byte, string, long, long, CancellationToken)
Updates one rank's name and permission mask.
Task<DatabaseResult> UpdateAsync(long guildId, byte rankOrder, string name, long permissions, long incomingVersion, CancellationToken cancellationToken = default)
Parameters
guildIdlongGuild ID.
rankOrderbyteThe rank position to update.
namestringNew display name.
permissionslongNew permission bit mask.
incomingVersionlongThe authoritative, monotonic version for this update.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult>
A result indicating success or failure.