Interface IPartySystemRuntimeData
- Namespace
- FishMMO.Server.Core.World.SceneServer
- Assembly
- FishMMO.Server.dll
Runtime data container for party system state. Provides invitation operations with O(1) touch semantics and bounded TTL cleanup.
public interface IPartySystemRuntimeData : IRuntimeDataContainer, IServerComponent
Properties
IngressGuard
Shared ingress guard for per-connection per-operation debounce and in-flight tracking.
IngressGuard IngressGuard { get; }
Property Value
LastFetchTime
Timestamp of the last successful database fetch for party updates.
DateTime LastFetchTime { get; set; }
Property Value
NextInvitationSweepUtc
Next scheduled UTC time for invitation cleanup.
DateTime NextInvitationSweepUtc { get; set; }
Property Value
Methods
BeginMembershipRemoval(long)
Marks a character's party membership as being removed.
void BeginMembershipRemoval(long characterID)
Parameters
characterIDlongThe character leaving or being kicked.
Remarks
Kick and leave both delete the membership row from a background task while the
character still holds a live IPartyController.ID. Disconnecting inside that
window ran the ordinary disconnect persist, which upserts the row back from that live
controller and puts the player straight back into the party they just left.
ClearLeaderAbsence(long)
Forgets any absence being tracked for a party.
void ClearLeaderAbsence(long partyID)
Parameters
partyIDlongThe party to clear.
EndMembershipRemoval(long)
Clears the membership-removal marker for a character.
void EndMembershipRemoval(long characterID)
Parameters
characterIDlongThe character whose removal has finished.
EndPartyMutation(long, long)
Releases a claim taken by TryBeginPartyMutation(long, out long).
void EndPartyMutation(long partyID, long token)
Parameters
EndUpdatePump()
Atomically transitions the update pump from in-flight back to idle.
void EndUpdatePump()
HasProcessedPartyUpdate(long, DateTime)
Reports whether a party update has already been processed by this server.
bool HasProcessedPartyUpdate(long partyID, DateTime lastUpdateUtc)
Parameters
Returns
- bool
True when this update, or a later one, has already been processed.
Remarks
The pump's watermark is deliberately held behind real time to absorb clock skew between scene servers, which means every update inside that window is fetched again on each tick — with a one-second pump and a five-second allowance, five times. Each re-fetch would otherwise re-read the roster, re-ask who is online, and re-broadcast the party to everybody in it, for an update whose work was finished on the first pass.
Recording what has been handled makes the allowance free, so it can be sized for the worst skew worth tolerating rather than traded off against pump cost. The timestamp is the update row's own, and the row is only ever replaced by a strictly later one, so an update this server has seen is identified exactly rather than approximately.
IsMembershipRemovalInFlight(long)
Reports whether a character's party membership is currently being removed.
bool IsMembershipRemovalInFlight(long characterID)
Parameters
characterIDlongThe character to test.
Returns
- bool
True while a removal is in flight.
MarkPartyUpdateProcessed(long, DateTime)
Records a party update as processed.
void MarkPartyUpdateProcessed(long partyID, DateTime lastUpdateUtc)
Parameters
Remarks
Called only once the work has actually been handed off, never at the point the update is read. Marking on read would drop the update entirely whenever the hand-off failed — the pump would skip it ever after, and the roster change it carried would never reach anybody.
RemovePendingInvitation(long)
Removes a pending invitation for a target character.
bool RemovePendingInvitation(long targetCharacterID)
Parameters
targetCharacterIDlongInvited target character identifier.
Returns
- bool
True when removed; otherwise false.
SweepExpiredInvitations(DateTime, TimeSpan, int, int)
Sweeps expired invitations using bounded scan/remove limits.
int SweepExpiredInvitations(DateTime nowUtc, TimeSpan ttl, int maxScan, int maxRemove)
Parameters
nowUtcDateTimeCurrent UTC timestamp.
ttlTimeSpanInvitation time-to-live.
maxScanintMaximum queue entries to scan.
maxRemoveintMaximum expired entries to remove.
Returns
- int
Number of removed invitations.
SweepInviteCooldowns(DateTime, TimeSpan, int, int)
Sweeps expired invite cooldown entries using bounded scan/remove limits.
int SweepInviteCooldowns(DateTime nowUtc, TimeSpan ttl, int maxScan, int maxRemove)
Parameters
nowUtcDateTimeCurrent UTC timestamp.
ttlTimeSpanCooldown entry time-to-live.
maxScanintMaximum queue entries to scan.
maxRemoveintMaximum expired entries to remove.
Returns
- int
Number of removed entries.
SweepLeaderAbsences(DateTime, TimeSpan)
Drops absence observations that nothing has come back to finish.
int SweepLeaderAbsences(DateTime nowUtc, TimeSpan ttl)
Parameters
nowUtcDateTimeCurrent UTC timestamp.
ttlTimeSpanAge past which an unfinished observation is discarded.
Returns
- int
The number of observations removed.
Remarks
An observation is resolved within one grace period by whatever scheduled the second look. One that outlives several belongs to a party nothing is examining any more — its last member left this server — and would otherwise sit in the map for the lifetime of the process.
SweepProcessedPartyUpdates(DateTime, TimeSpan)
Drops processed-update records for parties that have stopped changing.
int SweepProcessedPartyUpdates(DateTime nowUtc, TimeSpan ttl)
Parameters
Returns
- int
The number of records removed.
Remarks
A record only has to outlive the skew allowance — past that the watermark has moved beyond the update and it can never be re-fetched. Anything older belongs to a party that has gone quiet and would otherwise sit in the map for the lifetime of the process.
TryAddPendingInvitation(long, PendingPartyInvitation)
Attempts to add a new pending invitation.
bool TryAddPendingInvitation(long targetCharacterID, PendingPartyInvitation invitation)
Parameters
targetCharacterIDlongInvited target character identifier.
invitationPendingPartyInvitationThe invitation to hold.
Returns
- bool
True when inserted; false when an invitation is already pending.
TryBeginInviteCooldown(long, long, TimeSpan, DateTime)
Records an invitation attempt against a specific target and reports whether it is allowed by the per-target cooldown.
bool TryBeginInviteCooldown(long inviterCharacterID, long targetCharacterID, TimeSpan cooldown, DateTime nowUtc)
Parameters
inviterCharacterIDlongThe character sending the invitation.
targetCharacterIDlongThe character being invited.
cooldownTimeSpanMinimum interval between invitations to the same target.
nowUtcDateTimeCurrent UTC timestamp.
Returns
- bool
True when the invitation may proceed; false while the cooldown is active.
Remarks
The pending-invitation slot is not a rate limit — declining clears it immediately — and the ingress debounce is per connection rather than per target, so neither stops one player from keeping a modal permanently on another player's screen.
TryBeginPartyMutation(long, out long)
Claims exclusive rights to mutate one party's membership or ranks.
bool TryBeginPartyMutation(long partyID, out long token)
Parameters
partyIDlongThe party to claim.
tokenlongReceives the claim's identity, to be handed back to EndPartyMutation(long, long). A claim is abandoned after a generous timeout so a task that dies without releasing cannot lock a party out of every future change; the token is what stops the late release from that task then freeing a claim somebody else has since taken.
Returns
- bool
True when the claim was granted.
Remarks
This is what makes leadership free of races. Every write that can change who
leads a party — leaving, being kicked, a promotion, a leadership hand-off after a
disconnect, and the leaderless repair on an instance join — reads the party's rows,
decides from them, and writes back, across several awaits. Two of those running
against the same party interleave their reads and their writes, and both decisions are
then made from a roster that no longer exists.
The concrete failure is a party with two leaders. A leader promotes a member and then leaves in the same breath: the promotion reads the roster and starts handing the rank to its target, while the leave reads the same roster, sees a party about to be left without a leader, and hands the rank to somebody else. Both writes are version-gated and both succeed, because they touch different rows. Optimistic concurrency cannot see this — the two writes do not conflict; the two DECISIONS do.
Per party rather than per connection, because that is the granularity of the shared state. The existing ingress guard is per (connection, operation), so it does not stop one player's leave from racing their own promote, let alone two players racing each other. Refused rather than queued: the caller answers the client with a busy response it can retry, which is honest about a mutation not having happened in a way that silently dropping it or serialising it behind an unbounded queue would not be.
Process-local, and deliberately so. Two scene servers can each hold "the claim" for the same party at the same time; this removes the races within one server, not across the shard. Making it distributed would mean a lock in the database on a path taken every time anybody leaves a party, and it would not buy correctness — what makes the cross-server case safe is that every write is version-gated and every leadership decision is re-derived from the rows by a repair that converges. Two servers that disagree produce a state that is wrong for one pass and right afterwards; the claim is what stops the far more frequent same-server case from getting there at all.
TryBeginUpdatePump()
Atomically transitions the update pump from idle to in-flight. Returns true if this call won the race; false if a pump is already in flight.
bool TryBeginUpdatePump()
Returns
TryConfirmLeaderAbsent(long, long, DateTime, TimeSpan, out DateTime)
Records that a party's leader was observed to hold no session, and reports whether they have been observed that way for long enough to act on.
bool TryConfirmLeaderAbsent(long partyID, long leaderCharacterID, DateTime nowUtc, TimeSpan grace, out DateTime dueUtc)
Parameters
partyIDlongThe party being examined.
leaderCharacterIDlongThe member currently holding the rank.
nowUtcDateTimeCurrent UTC timestamp.
graceTimeSpanHow long the leader must be continuously absent.
dueUtcDateTimeWhen the grace elapses. Meaningful only when this returns false, so the caller can arrange to look again rather than leaving the observation to expire unread.
Returns
- bool
True when the absence has been confirmed and leadership may be moved. Confirming does not consume the observation: a caller that then fails to move the rank may try again at once rather than waiting out another grace period. Clear it with ClearLeaderAbsence(long) once the rank has moved, or once the holder turns out to be present.
Remarks
A leader who is not logged in has not necessarily gone. Moving between scene servers — walking through a teleporter, or entering the dungeon the party just opened — releases the character's session on the way out and re-claims it on arrival, and for the whole of that gap the database reports them exactly as it reports somebody who quit. Acting on a single observation would therefore take leadership away from a leader for the crime of leading their party into the instance, which is close to the worst possible moment for it.
So absence has to be observed twice, far enough apart that a scene load cannot span it. The first observation only starts the clock; the second, once the grace has elapsed, is what confirms it. The clock is per (party, leader), so a leader who returns, or one who is replaced by some other route, resets it rather than inheriting somebody else's.
Held in memory by whichever server is doing the observing rather than persisted. A server that dies mid-grace simply loses its half-finished observation, and the next server to look starts its own — which is the correct outcome, and one less piece of state that can be left behind.
TryGetPendingInvitation(long, out PendingPartyInvitation)
Tries to get a pending party invitation for the target character.
bool TryGetPendingInvitation(long targetCharacterID, out PendingPartyInvitation invitation)
Parameters
targetCharacterIDlongInvited target character identifier.
invitationPendingPartyInvitationResolved invitation when found.
Returns
- bool
True when a pending invitation exists; otherwise false.