Table of Contents

Class Buff

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Represents a single instance of a buff applied to a character, tracking time, stacks, and template. All state is deterministic — timing is tick-based (ExpiryTick, NextTickTick) rather than float-second accumulators, eliminating floating-point drift across prediction ticks.

public class Buff
Inheritance
Buff
Inherited Members

Constructors

Buff(int, uint, float, int, int)

Creates a new buff instance from a fresh application. ExpiryTick and NextTickTick are computed from the template's duration and tick-rate via ceil(seconds / tickDelta), matching CooldownInstance's convention.

public Buff(int templateID, uint currentTick, float tickDelta, int stacks = 0, int tickCount = 0)

Parameters

templateID int

The template ID for the buff.

currentTick uint

The network tick at the moment of application.

tickDelta float

Fixed seconds-per-tick for the active session.

stacks int

The initial stack count.

tickCount int

The number of ticks that have already fired (for restoration).

Buff(int, uint, uint, float, int, int)

Restores a buff from serialized absolute-tick values (reconcile / payload restore). Does not recompute tick values from the template — uses the provided values directly.

public Buff(int templateID, uint expiryTick, uint nextTickTick, float tickDelta, int stacks, int tickCount)

Parameters

templateID int

The template ID for the buff.

expiryTick uint

Absolute tick at which the buff expires.

nextTickTick uint

Absolute tick at which the next OnTick fires.

tickDelta float

Fixed seconds-per-tick (for RemainingSeconds(uint) UI conversion).

stacks int

The current stack count.

tickCount int

The number of ticks that have already fired.

Fields

CumulativeTickMultiplier

Running sum of (1 + Stacks) for every tick that has fired on this buff instance. Tracks the exact cumulative multiplier applied by templates such as AttributeTickBuffTemplate so that OnRemove can reverse the precise total modifier regardless of how many times Stacks changed between tick firings. Reconciled via CumulativeTickMultiplier so rollback replay and post-reconcile removal both produce correct reversal.

public int CumulativeTickMultiplier

Field Value

int

ExpiryTick

The absolute network tick at which this buff expires. A buff has expired when (int)(currentTick - ExpiryTick) >= 0. Permanent buffs use FishNet.Managing.Timing.TimeManager.UNSET_TICK.

public uint ExpiryTick

Field Value

uint

NextTickTick

The absolute network tick at which the next OnTick(Buff, ICharacter) should fire. Advances by one tick-rate interval after each fire so missed intervals catch up without drifting the cadence to the late current tick.

public uint NextTickTick

Field Value

uint

RemainingCharges

What is LEFT of a buff that is spent rather than merely timed — damage points a shield can still absorb, deflections a guard can still make. Zero for every buff that counts nothing.

public int RemainingCharges

Field Value

int

Remarks

The unit belongs to the template, not to this field. DamageNegationBuffTemplate counts damage points and DeflectBuffTemplate counts deflections; a template that spends nothing leaves InitialCharges at zero and never looks at this. One counter rather than one per template is what keeps the reconcile entry from growing a field every time a new consumable effect is authored — and the reconcile is where the cost of this really lives.

Predicted state, so it has to travel. Spending happens on the server — the defender's own client does not resolve incoming hits — but REFILLING happens on the owner too: BuffController calls RefreshCharges() every time a buff is re-applied, which for a channelled block is every tick the button is held. Without an authoritative value coming back, the owner's pool would be topped up locally and never told what the server had spent, so its shield would read full while the server's was empty. It rides RemainingCharges for the same reason CumulativeTickMultiplier does: both are per-buff state the two peers can move independently.

It is not sent to observers. ObservedBuffEntry carries the strip a peer DRAWS, and no observer resolves damage against somebody else's shield — the server does, and tells them the resulting health. Sending it would disclose exactly how much more a player has to hit through, which is information the game does not otherwise give.

Stacks

The current number of stacks of this buff.

public int Stacks

Field Value

int

TickCount

The number of ticks that have fired for this buff instance. Used by cumulative tick templates to track total applied modifiers for clean reversal. Serialized in payload for deterministic restoration.

public int TickCount

Field Value

int

Version

Version number for this buff instance, used for database state-driven safety on updates. Incremented whenever the buff's state changes in a way that requires persistence (e.g., stacks added/removed, remaining time checkpointed).

public long Version

Field Value

long

Properties

Caster

The character to credit for this buff's periodic effects, or null when there is nobody left to credit.

public ICharacter Caster { get; }

Property Value

ICharacter

Remarks

Returns null once the caster has been destroyed — a player who disconnected, an NPC that despawned. That is deliberately not treated as a reason to stop ticking: a lingering poison is part of the simulation whether or not the person who applied it is still in the scene, so the effect lands with a null attacker and simply generates no threat and no kill credit.

The null check goes through UnityEngine.Object on purpose. A destroyed component compared through an interface reference uses reference equality and reports itself as non-null, so the plain test would hand out a dead object.

IsAuthoritative

True while the tick currently executing on this buff runs on the authoritative simulation (the server). False on every client, replayed or not.

public bool IsAuthoritative { get; }

Property Value

bool

Remarks

With state forwarding off, only the owning client (prediction) and the server tick a buff. The owner's tick keeps predicted health responsive, but everything that is not a pure resource mutation — ECA tick triggers, threat, kill credit, achievements — must happen exactly once, on the server. IsReplaying alone cannot express that: the owner's first, non-replayed pass is still not authoritative. Set by TryTick(ICharacter, uint, float, bool, bool) from the value the controller passes.

IsReplaying

True while this buff is ticking inside a prediction replay rather than for the first time.

public bool IsReplaying { get; }

Property Value

bool

Remarks

Reconcile replays every input since the last authoritative state, so a DoT's tick fires again on each pass. The resource mutation must repeat — that is what keeps the client's predicted health in step — but the ECA triggers hanging off it must not, or a single tick of poison counts a dozen times toward an achievement.

IsSpent

True when this buff counted charges and has none left — the "disappears when the remaining amount hits 0" half of a consumable shield.

public bool IsSpent { get; }

Property Value

bool

Remarks

Asks the TEMPLATE whether charges were ever the point. A buff whose template counts nothing sits at zero for its whole life and must not be mistaken for a spent one.

Template

The template that defines this buff's behavior and properties.

public BaseBuffTemplate Template { get; }

Property Value

BaseBuffTemplate

Methods

AddStack(ICharacter)

Adds a stack to the buff and applies stack effects to the target.

public void AddStack(ICharacter target)

Parameters

target ICharacter

The character receiving the stack.

Apply(ICharacter)

Applies the buff's effects to the target character.

public void Apply(ICharacter target)

Parameters

target ICharacter

The character receiving the buff.

HasExpired(uint)

Returns true when this buff has expired at the given tick. Uses a signed cast for overflow-safe wrap-around comparison; safe for any duration < 2^31 ticks (~828 days at 30 tps).

public bool HasExpired(uint currentTick)

Parameters

currentTick uint

Returns

bool

RefreshCharges()

Fills RemainingCharges from the template, at this buff's current stack count.

public void RefreshCharges()

Remarks

Called on a fresh application and again whenever an existing buff is re-applied, so holding a block up refreshes the shield rather than continuing to spend a pool that is already half gone. Re-applying is the ONLY thing that refills it; nothing regenerates.

Scales as 1 + Stacks, the same multiplier AttributeBuffTemplate applies to its bonuses, so two applications of a stacking absorb shield hold twice the damage rather than the same amount twice. A template with no charges stays at zero whatever the stack count.

RemainingSeconds(uint)

Computes the remaining buff duration in seconds for the given tick. Returns zero if the buff has already expired. Used by the UI duration slider.

public float RemainingSeconds(uint currentTick)

Parameters

currentTick uint

Returns

float

Remove(ICharacter)

Removes the buff's effects from the target character.

public void Remove(ICharacter target)

Parameters

target ICharacter

The character losing the buff.

RemoveStack(ICharacter)

Removes a stack from the buff and removes stack effects from the target.

public void RemoveStack(ICharacter target)

Parameters

target ICharacter

The character losing the stack.

ResetDuration(uint, float)

Resets the expiry to currentTick + durationTicks, refreshing the buff's lifetime.

public void ResetDuration(uint currentTick, float tickDelta)

Parameters

currentTick uint
tickDelta float

ResetTickTime(uint, float)

Resets NextTickTick to currentTick + tickRateTicks.

public void ResetTickTime(uint currentTick, float tickDelta)

Parameters

currentTick uint
tickDelta float

SetCaster(ICharacter)

Snapshots the character responsible for this buff.

public void SetCaster(ICharacter caster)

Parameters

caster ICharacter

The character applying the buff, or null if unknown.

Remarks

Called again when an existing buff is re-applied or stacked, so a fresh application re-points attribution at whoever cast it most recently. A null is ignored rather than clearing an existing caster: a re-application from a source that does not know its initiator (a region trigger, a database restore) should not strip credit from one that did.

SetTickDelta(float)

Updates the cached tick-delta used by RemainingSeconds(uint). Called by Tick(uint) every tick so UI duration bars never get stuck reporting 0 when a buff was constructed before TimeManager became available. Ignores non-positive values to preserve the last good delta.

public void SetTickDelta(float tickDelta)

Parameters

tickDelta float

SpendCharges(int)

Spends up to requested charges and reports how many were available.

public int SpendCharges(int requested)

Parameters

requested int

How many charges the caller would like to spend.

Returns

int

How many were spent.

Remarks

Never spends more than it holds and never goes negative, so a caller can hand it the whole incoming amount and read the return value as "how much this buff absorbed". A buff that lands on zero is not removed here — IsSpent is what the caller tests, because removal has to happen outside the loop walking the buff container.

TryTick(ICharacter, uint, float, bool, bool)

Tries to trigger a tick for the buff if NextTickTick has been reached, then advances NextTickTick by the template tick-rate in ticks.

public bool TryTick(ICharacter target, uint currentTick, float tickDelta, bool isReplaying = false, bool isAuthoritative = true)

Parameters

target ICharacter

The character affected by the buff.

currentTick uint

The current network tick.

tickDelta float

Fixed seconds-per-tick of the active session.

isReplaying bool

True when this tick is part of a prediction replay. Exposed to templates via IsReplaying so effects that raise events can suppress them.

isAuthoritative bool

True when the caller is the server. Exposed to templates via IsAuthoritative so ECA triggers and other non-idempotent side effects run on the server only. Defaults to true for direct callers; Tick(uint) always passes its own server state explicitly.

Returns

bool

True if tick state changed (snapshot must be marked dirty).