Table of Contents

Struct ObservedBuffEntry

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

One buff on another character, as the SERVER has chosen to show it to observers. Display-only: nothing on the client applies an effect from this.

public struct ObservedBuffEntry : IEquatable<ObservedBuffEntry>
Implements
Inherited Members

Remarks

This is deliberately not Buff. A Buff is simulation state — it carries expiry in the owner's replicate-tick domain, a stack count that drives attribute modifiers, and a template whose OnApply/OnRemove mutate the character. Handing that to an observer would either desynchronise the observer's own prediction domain (ticks mean different things on different clients) or, worse, let a client run buff effects on a character it does not own.

Duration travels as SECONDS remaining at the moment the server sent it, not as an absolute tick, precisely because the receiving client's tick domain is its own. The observer counts down locally from receipt. That drifts by the one-way latency, which for a bar a few pixels tall on someone else's nameplate is not worth a tick-domain translation.

The struct is wider than the wire. WriteTo(Writer) sends seven bytes; the in-memory fields stay at their natural widths because every consumer — the FX diff, the target frame, the party frame — indexes templates by int and draws bars from float. Narrowing the fields themselves would ripple through all of that and save nothing, since the wire form is where the bytes are spent.

Fields

MaxEncodableSeconds

Largest remaining/total duration the wire form can carry, in seconds.

public const float MaxEncodableSeconds = 6553.5

Field Value

float

Remarks

RemainingSeconds travels as deciseconds in a ushort. Just over 109 minutes, against authored buff durations measured in seconds to minutes; anything longer clamps and displays as a bar that starts full and stays there slightly too long, which is the same thing a permanent buff already does.

MaxEncodableStacks

Largest stack count the wire form can carry.

public const int MaxEncodableStacks = 255

Field Value

int

Remarks

BaseBuffTemplate.MaxStacks is authored well below this; the clamp exists so a runaway stack cannot corrupt the stream, not because 255 stacks is expected.

RemainingSeconds

Seconds remaining when the server sent this, or 0 for a permanent buff.

public float RemainingSeconds

Field Value

float

Stacks

Stack count above the base application: 0 means one application, 2 means three.

public int Stacks

Field Value

int

Remarks

MaxStacks is the TOTAL number of applications, so this counts up to MaxStacks - 1: the base application is the first, and each stack is one more.

Both halves of that were wrong until the 2026-08-29 audit, in the same direction. A freshly applied stacking buff ran the new-buff branch AND the stack branch, so it arrived here reporting 1 while having applied its modifier twice; and the cap tested Stacks < MaxStacks, which let a MaxStacks=3 buff reach four applications. The first was fixed on 2026-08-28, the second on 2026-08-29 — see BuffController.ApplyResolved.

TemplateID

The buff template's cached ID.

public int TemplateID

Field Value

int

TotalSeconds

The buff's full authored duration in seconds, straight from the template — including for a permanent buff, whose authored Duration is reported as-is. Only RemainingSeconds is 0 for a permanent buff.

public float TotalSeconds

Field Value

float

Remarks

Not sent. This is BaseBuffTemplate.Duration — authored content the receiver already holds, not runtime state — so ReadFrom(Reader) resolves it from the template rather than paying for it once per entry per observer. A template the receiver cannot resolve yields 0, which every consumer already treats as "no bar to fill".

Methods

DecodeSeconds(ushort)

Deciseconds back to seconds.

public static float DecodeSeconds(ushort deciseconds)

Parameters

deciseconds ushort

Returns

float

EncodeSeconds(float)

Seconds to deciseconds, clamped to the encodable range.

public static ushort EncodeSeconds(float seconds)

Parameters

seconds float

Returns

ushort

Equals(ObservedBuffEntry)

public bool Equals(ObservedBuffEntry other)

Parameters

other ObservedBuffEntry

Returns

bool

Equals(object)

public override bool Equals(object obj)

Parameters

obj object

Returns

bool

GetHashCode()

public override int GetHashCode()

Returns

int

ReadFrom(Reader)

Reads one entry, resolving TotalSeconds from the template.

public static ObservedBuffEntry ReadFrom(Reader reader)

Parameters

reader Reader

Returns

ObservedBuffEntry

StructurallyEquals(ObservedBuffEntry)

Structural equality: the fields that make this a DIFFERENT buff rather than the same buff a moment later.

public bool StructurallyEquals(ObservedBuffEntry other)

Parameters

other ObservedBuffEntry

Returns

bool

Remarks

RemainingSeconds is deliberately excluded. It moves every tick on every buff, so including it would mark the whole list changed on every tick and collapse the delta back into a full resend. Ordinary countdown needs no message at all: the receiver counts its own bars down. The one case that does is a buff RENEWED after the receiver was last told about it, which would otherwise run out locally and be deleted while the character still holds it — BuffController.ObservedBuffWillLapse catches exactly that, and answers it with a full set.

WriteTo(Writer)

Writes one entry — seven bytes.

public void WriteTo(Writer writer)

Parameters

writer Writer

Remarks

WriteInt32Unpacked rather than WriteInt32: template ids are a deterministic 32-bit hash (CachedScriptableObject.AddToCache), so they occupy the whole range and FishNet's signed-packed form would spend FIVE bytes on one. Unpacked is exactly four.