Table of Contents

Class PartySystemRuntimeData

Namespace
FishMMO.Server.Implementation.World.SceneServer
Assembly
FishMMO.Server.dll

Runtime data container for party system state. Manages party invitations and database synchronization state separately from PartySystem logic.

public class PartySystemRuntimeData : RuntimeDataContainer, IRuntimeDataContainer<INetworkManagerWrapper, ServerManager, NetworkConnection, IRuntimeDataContainer>, IServerComponent<INetworkManagerWrapper, ServerManager, NetworkConnection, IRuntimeDataContainer>, IPartySystemRuntimeData, IRuntimeDataContainer, IServerComponent
Inheritance
PartySystemRuntimeData
Implements
Inherited Members

Properties

IngressGuard

Shared ingress guard for per-connection per-operation debounce and in-flight tracking.

public IngressGuard IngressGuard { get; }

Property Value

IngressGuard

LastFetchTime

Timestamp of the last successful database fetch for party updates.

public DateTime LastFetchTime { get; set; }

Property Value

DateTime

NextInvitationSweepUtc

Next scheduled UTC time for invitation cleanup.

public DateTime NextInvitationSweepUtc { get; set; }

Property Value

DateTime

Methods

BeginMembershipRemoval(long)

Marks a character's party membership as being removed.

public void BeginMembershipRemoval(long characterID)

Parameters

characterID long

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

Clear()

Clears all party runtime data.

public override void Clear()

ClearLeaderAbsence(long)

Forgets any absence being tracked for a party.

public void ClearLeaderAbsence(long partyID)

Parameters

partyID long

The party to clear.

EndMembershipRemoval(long)

Clears the membership-removal marker for a character.

public void EndMembershipRemoval(long characterID)

Parameters

characterID long

The character whose removal has finished.

EndPartyMutation(long, long)

Releases a claim taken by TryBeginPartyMutation(long, out long).

public void EndPartyMutation(long partyID, long token)

Parameters

partyID long

The party claimed.

token long

The token the claim was granted with.

EndUpdatePump()

Atomically transitions the update pump from in-flight back to idle.

public void EndUpdatePump()

HasProcessedPartyUpdate(long, DateTime)

Reports whether a party update has already been processed by this server.

public bool HasProcessedPartyUpdate(long partyID, DateTime lastUpdateUtc)

Parameters

partyID long

The party the update belongs to.

lastUpdateUtc DateTime

The update row's timestamp.

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.

InitializeOnce()

Initializes the party runtime data container.

public override ServerComponentInitializationStatus InitializeOnce()

Returns

ServerComponentInitializationStatus

IsMembershipRemovalInFlight(long)

Reports whether a character's party membership is currently being removed.

public bool IsMembershipRemovalInFlight(long characterID)

Parameters

characterID long

The character to test.

Returns

bool

True while a removal is in flight.

MarkPartyUpdateProcessed(long, DateTime)

Records a party update as processed.

public void MarkPartyUpdateProcessed(long partyID, DateTime lastUpdateUtc)

Parameters

partyID long

The party the update belongs to.

lastUpdateUtc DateTime

The update row's timestamp.

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.

OnDeinitialize()

Deinitializes the party runtime data container.

protected override void OnDeinitialize()

RemovePendingInvitation(long)

Removes a pending invitation for a target character.

public bool RemovePendingInvitation(long targetCharacterID)

Parameters

targetCharacterID long

Invited target character identifier.

Returns

bool

True when removed; otherwise false.

SweepExpiredInvitations(DateTime, TimeSpan, int, int)

Sweeps expired invitations using bounded scan/remove limits.

public int SweepExpiredInvitations(DateTime nowUtc, TimeSpan ttl, int maxScan, int maxRemove)

Parameters

nowUtc DateTime

Current UTC timestamp.

ttl TimeSpan

Invitation time-to-live.

maxScan int

Maximum queue entries to scan.

maxRemove int

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

public int SweepInviteCooldowns(DateTime nowUtc, TimeSpan ttl, int maxScan, int maxRemove)

Parameters

nowUtc DateTime

Current UTC timestamp.

ttl TimeSpan

Cooldown entry time-to-live.

maxScan int

Maximum queue entries to scan.

maxRemove int

Maximum expired entries to remove.

Returns

int

Number of removed entries.

SweepLeaderAbsences(DateTime, TimeSpan)

Drops absence observations that nothing has come back to finish.

public int SweepLeaderAbsences(DateTime nowUtc, TimeSpan ttl)

Parameters

nowUtc DateTime

Current UTC timestamp.

ttl TimeSpan

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

public int SweepProcessedPartyUpdates(DateTime nowUtc, TimeSpan ttl)

Parameters

nowUtc DateTime

Current UTC timestamp.

ttl TimeSpan

Age past which a record is discarded.

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.

public bool TryAddPendingInvitation(long targetCharacterID, PendingPartyInvitation invitation)

Parameters

targetCharacterID long

Invited target character identifier.

invitation PendingPartyInvitation

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

public bool TryBeginInviteCooldown(long inviterCharacterID, long targetCharacterID, TimeSpan cooldown, DateTime nowUtc)

Parameters

inviterCharacterID long

The character sending the invitation.

targetCharacterID long

The character being invited.

cooldown TimeSpan

Minimum interval between invitations to the same target.

nowUtc DateTime

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

public bool TryBeginPartyMutation(long partyID, out long token)

Parameters

partyID long

The party to claim.

token long

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

public bool TryBeginUpdatePump()

Returns

bool

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.

public bool TryConfirmLeaderAbsent(long partyID, long leaderCharacterID, DateTime nowUtc, TimeSpan grace, out DateTime dueUtc)

Parameters

partyID long

The party being examined.

leaderCharacterID long

The member currently holding the rank.

nowUtc DateTime

Current UTC timestamp.

grace TimeSpan

How long the leader must be continuously absent.

dueUtc DateTime

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

public bool TryGetPendingInvitation(long targetCharacterID, out PendingPartyInvitation invitation)

Parameters

targetCharacterID long

Invited target character identifier.

invitation PendingPartyInvitation

Resolved invitation when found.

Returns

bool

True when a pending invitation exists; otherwise false.

Remarks

The TTL the accept path enforces is measured against IssuedUtc, which a reader cannot move. The touch performed here only moves the SWEEP's clock.