Table of Contents

Struct ObservedResourcePushScheduler

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Decides, tick by tick, whether a character's resources go out to its observers.

public struct ObservedResourcePushScheduler
Inherited Members

Remarks

The observer resource stream is unreliable and change gated: a value is sent when it changes and then never again. Lose that one packet and every observer holds a stale bar until the next change — which for a creature that has just died, or has just been topped up to full, may be never. This schedules a single confirmation re-send of the last pushed state ConfirmDelayTicks later, provided the state is still what was pushed. One confirmation is pending at most; a new change replaces it rather than stacking.

Not every resource earns the same schedule. Health drives every observer-facing readout there is; mana and stamina drive none of them — no nameplate, target frame or party row reads a peer's mana, and the one genuine peer-side consumer is HasResourceCondition tested against a victim during a predicted hit. Gated as one OR across whole units, sprint's 5 stamina a second dirtied the gate on essentially every tick and turned a change-gated channel into a 2.5-5 Hz stream of the whole resource sheet. So health (and every maximum) is Primary and keeps the fine schedule, while mana and stamina are Secondary and move only when they cross a percentage bucket — the coarsest form anything could act on.

Pure state machine with no network dependency so the schedule is unit tested. The owner of this struct decides what "changed" means (whole units, for a bar) and passes in the intervals plus, for the confirmation rule, when its next PERIODIC change is due.

Fields

ConfirmDelayTicks

Ticks after a push before its confirmation re-send. 15 at tick rate 30 is half a second.

public const uint ConfirmDelayTicks = 15

Field Value

uint

ConfirmPending

True while a confirmation re-send is scheduled for ConfirmTick.

public bool ConfirmPending

Field Value

bool

ConfirmTick

Tick the pending confirmation is due on.

public uint ConfirmTick

Field Value

uint

HasPushed

False until the first push, which always happens regardless of change.

public bool HasPushed

Field Value

bool

LastPushed

The state most recently sent.

public CharacterAttributeResourceState LastPushed

Field Value

CharacterAttributeResourceState

NextPushTick

Earliest tick a Primary change push may occur on.

public uint NextPushTick

Field Value

uint

NextSecondaryPushTick

Earliest tick a Secondary-only change push may occur on.

public uint NextSecondaryPushTick

Field Value

uint

SecondaryResourceBuckets

Buckets the span between empty and full is divided into for a secondary resource.

public const int SecondaryResourceBuckets = 10

Field Value

int

Remarks

Ten, so a bucket is a tenth of the bar. Sprint drains stamina at 5 a second, which on a hundred-point bar crosses a boundary every two seconds rather than every tick, and the 4/s recovery ramp crosses one every two and a half. Empty and full are buckets of their own (see SecondaryResourceBucket(float, int)) because "has any at all" and "is capped" are the two facts a consumer actually branches on.

Methods

AllowImmediatePush(uint)

Drops the outstanding rate limit so the next CHANGE is sent on the tick it happens.

public void AllowImmediatePush(uint tick)

Parameters

tick uint

The current server tick; the deadline is moved back to it.

Remarks

NextPushTick is an absolute deadline stamped from the interval in force at the last push. A caller that widens its interval while a character is idle therefore leaves a deadline behind that outlives the reason for it — and the event that matters most (the first hit of a fight) is exactly the one that arrives while it is still pending. This clears the deadline without pushing anything: nothing is sent unless the state has actually changed.

Deliberately does NOT touch LastPushed, HasPushed or the pending confirmation. Clearing those would resend a value the observers already hold and would drop the loss repair for the previous push.

Nor does it touch NextSecondaryPushTick. The event this exists for is a health change; a mana or stamina bucket crossing is not made urgent by combat starting, and letting it through here would spend the packet combat entry is trying to reserve.

ClassifyChange(in CharacterAttributeResourceState, in CharacterAttributeResourceState)

How much two resource states differ, as far as an observer is concerned.

public static ObservedResourcePushScheduler.ChangeKind ClassifyChange(in CharacterAttributeResourceState a, in CharacterAttributeResourceState b)

Parameters

a CharacterAttributeResourceState
b CharacterAttributeResourceState

Returns

ObservedResourcePushScheduler.ChangeKind

Remarks

Health is compared at whole units because that is what a health bar renders, and every maximum exactly because it is the denominator that bar is drawn against. Sub-unit regeneration drift in health would otherwise mark every interval dirty and push continuously.

Mana and stamina are compared by percentage bucket. Nothing on any client renders a peer's mana or stamina at all; the only peer-side reader is a resource CONDITION evaluated against a victim, which asks "does it have at least N" — a question a tenth of the bar answers, and which the server re-answers exactly before anything is allowed to happen. Compared at whole units these two alone produced a continuous push stream for any character that was sprinting or recovering.

Evaluate(uint, in CharacterAttributeResourceState, in Schedule)

Evaluates one tick.

public ObservedResourcePushScheduler.Decision Evaluate(uint tick, in CharacterAttributeResourceState state, in ObservedResourcePushScheduler.Schedule schedule)

Parameters

tick uint

The current server tick.

state CharacterAttributeResourceState

The current resource state, with fields observers do not use zeroed.

schedule ObservedResourcePushScheduler.Schedule

The intervals and periodic-change forecast in force this tick.

Returns

ObservedResourcePushScheduler.Decision

What, if anything, to send. On Push, LastPushed is now state.

Evaluate(uint, in CharacterAttributeResourceState, uint)

Evaluates one tick against a single interval and no periodic forecast.

public ObservedResourcePushScheduler.Decision Evaluate(uint tick, in CharacterAttributeResourceState state, uint pushInterval)

Parameters

tick uint
state CharacterAttributeResourceState
pushInterval uint

Returns

ObservedResourcePushScheduler.Decision

Remarks

The shape the schedule had before mana and stamina were split off. Kept because it is the honest signature for a caller with one resource and no pulse, and because the confirmation rule it produces is the unconditional one.

Reset()

Forgets everything, so the next evaluation pushes unconditionally.

public void Reset()

ResolveConfirmTick(uint, in Schedule)

When the confirmation for a push made on pushTick falls due.

public static uint ResolveConfirmTick(uint pushTick, in ObservedResourcePushScheduler.Schedule schedule)

Parameters

pushTick uint

The tick the push being confirmed went out on.

schedule ObservedResourcePushScheduler.Schedule

The schedule in force, carrying the periodic-change forecast.

Returns

uint

The tick Confirm becomes due on.

Remarks

A confirmation exists to repair a loss that nothing else will. It repeats the final value of a burst of changes, on the reliable channel, because that one send has nothing behind it to supersede it. A burst that is still running needs no repair: the next push is a few ticks away and carries the same field again.

Regeneration made "a burst that stops" untrue. It pulses once a second — thirty ticks, against a fifteen-tick confirmation delay — so every character below full on any resource sat in a two-message steady state forever: an unreliable push on the pulse, then a reliable confirmation fifteen ticks later repeating a value that the next pulse was about to replace. The confirmation was repairing a value with a successor already scheduled, which is precisely what it is not for.

So a pending periodic change DEFERS the confirmation to one delay past it. Note that "inside the confirmation window" is not the test: the pulse period is TWICE the delay, so the pulse never falls inside the window and a window test would have changed nothing at all. What matters is that a further push is already scheduled, whenever it is due.

Deferral rather than cancellation, because the forecast is a schedule and not a promise: a pulse lands inside a consumption lockout, or on a resource already at its maximum, and changes nothing. Cancelling on such a pulse would throw away the loss repair for the push that preceded it and strand observers on whatever they last received — the stale-corpse failure this whole mechanism exists to remove. Deferred, the confirmation fires the moment the pulses stop moving the values, and never while they are.

The cost is that a burst which ends between two pulses waits for the pulse before it is repaired: one pulse period plus one delay, rather than one delay. At the authored one-second regeneration cadence that is at most 45 ticks against 15. A packet lost at the end of a fight is therefore repaired a second later than it used to be, which is the price of not sending a reliable repeat every single second of every character's life.

ResourcesDifferForObservers(in CharacterAttributeResourceState, in CharacterAttributeResourceState)

True when two resource states differ by enough for an observer to notice.

public static bool ResourcesDifferForObservers(in CharacterAttributeResourceState a, in CharacterAttributeResourceState b)

Parameters

a CharacterAttributeResourceState
b CharacterAttributeResourceState

Returns

bool

Remarks

The boolean face of ClassifyChange(in CharacterAttributeResourceState, in CharacterAttributeResourceState), for callers that only need to know whether anything is owed rather than which schedule owes it.

SecondaryResourceBucket(float, int)

Which percentage bucket a secondary resource sits in.

public static int SecondaryResourceBucket(float current, int max)

Parameters

current float

The resource's current value.

max int

The resource's maximum; zero or less means the resource is absent.

Returns

int

A bucket index; only equality between two of these is meaningful.

Remarks

Empty is bucket 0 and full is the last bucket, both exactly, with SecondaryResourceBuckets proportional buckets between them. The two ends are singled out because they are the only two values with a meaning of their own: a resource at zero cannot pay for anything, and one at its maximum cannot regenerate. A plain floor would have put 0 and 1 of 100 in the same bucket and 99 and 100 likewise.

TicksUntil(uint, uint)

Ticks from from forward to to, floored at zero.

public static uint TicksUntil(uint from, uint to)

Parameters

from uint
to uint

Returns

uint

Remarks

A duration, deliberately, not a tick. A caller's periodic schedule is very often kept in the REPLICATE tick domain — the owning client's own unsynchronised counter — and comparing that against TimeManager.LocalTick is meaningless. A difference taken inside one domain is a number of ticks and transfers to any other, so this is how a replicate-domain pulse is handed to a LocalTick-domain schedule.