Table of Contents

Class BuffController

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Controls the application, ticking, and removal of buffs for a character, including network synchronization.

public class BuffController : CharacterBehaviour, IBuffController, ICharacterBehaviour, IPredictableController, IModelReadyHandler
Inheritance
Object
Component
Behaviour
MonoBehaviour
NetworkBehaviour
BuffController
Implements
Inherited Members
NetworkBehaviour.IsSpawned
NetworkBehaviour.ComponentIndex
NetworkBehaviour.NetworkObject
NetworkBehaviour.MAXIMUM_NETWORKBEHAVIOURS
NetworkBehaviour.UNSET_NETWORKBEHAVIOUR_ID
NetworkBehaviour.ToString()
NetworkBehaviour.Reset()
NetworkBehaviour.OnValidate()
NetworkBehaviour.IsBehaviourReconciling
NetworkBehaviour.ClearReplicateCache()
NetworkBehaviour.CreateReconcile()
NetworkBehaviour.Reconcile_Reader<T>(PooledReader, ref T)
NetworkBehaviour.OnStartServerCalled
NetworkBehaviour.OnStartClientCalled
NetworkBehaviour.OnStopNetwork()
NetworkBehaviour.OnStartServer()
NetworkBehaviour.OnStopServer()
NetworkBehaviour.OnOwnershipServer(NetworkConnection)
NetworkBehaviour.OnDespawnServer(NetworkConnection)
NetworkBehaviour.OnStartClient()
NetworkBehaviour.OnStopClient()
NetworkBehaviour.ClearBuffedRpcs()
NetworkBehaviour.ExcludeOwnerFromUnbufferedObserversRpcs
NetworkBehaviour.IsClientOnly
NetworkBehaviour.IsServerOnly
NetworkBehaviour.IsHost
NetworkBehaviour.IsClient
NetworkBehaviour.IsServer
NetworkBehaviour.IsDeinitializing
NetworkBehaviour.NetworkManager
NetworkBehaviour.ServerManager
NetworkBehaviour.ClientManager
NetworkBehaviour.ObserverManager
NetworkBehaviour.TransportManager
NetworkBehaviour.TimeManager
NetworkBehaviour.SceneManager
NetworkBehaviour.PredictionManager
NetworkBehaviour.RollbackManager
NetworkBehaviour.NetworkObserver
NetworkBehaviour.IsClientInitialized
NetworkBehaviour.IsClientStarted
NetworkBehaviour.IsClientOnlyInitialized
NetworkBehaviour.IsClientOnlyStarted
NetworkBehaviour.IsServerInitialized
NetworkBehaviour.IsServerStarted
NetworkBehaviour.IsServerOnlyInitialized
NetworkBehaviour.IsServerOnlyStarted
NetworkBehaviour.IsHostInitialized
NetworkBehaviour.IsHostStarted
NetworkBehaviour.IsOffline
NetworkBehaviour.IsNetworked
NetworkBehaviour.GetIsNetworked()
NetworkBehaviour.IsManagerReconciling
NetworkBehaviour.Observers
NetworkBehaviour.IsOwner
NetworkBehaviour.IsController
NetworkBehaviour.HasAuthority
NetworkBehaviour.Owner
NetworkBehaviour.OwnerId
NetworkBehaviour.ObjectId
NetworkBehaviour.LocalConnection
NetworkBehaviour.OwnerMatches(NetworkConnection)
NetworkBehaviour.Despawn(GameObject, DespawnType?)
NetworkBehaviour.Despawn(NetworkObject, DespawnType?)
NetworkBehaviour.Despawn(DespawnType?)
NetworkBehaviour.Spawn(GameObject, NetworkConnection, Scene)
NetworkBehaviour.Spawn(NetworkObject, NetworkConnection, Scene)
NetworkBehaviour.RemoveOwnership()
NetworkBehaviour.GiveOwnership(NetworkConnection)
NetworkBehaviour.GetInstance<T>()
NetworkBehaviour.TryRegisterInstance<T>(T)
NetworkBehaviour.UnregisterInstance<T>()
NetworkBehaviour.CanLog(LoggingType)
MonoBehaviour.IsInvoking()
MonoBehaviour.CancelInvoke()
MonoBehaviour.StopCoroutine(Coroutine)
MonoBehaviour.StopAllCoroutines()
MonoBehaviour.destroyCancellationToken
MonoBehaviour.useGUILayout
MonoBehaviour.didStart
MonoBehaviour.didAwake
MonoBehaviour.runInEditMode
Behaviour.enabled
Behaviour.isActiveAndEnabled
Component.GetComponent<T>()
Component.TryGetComponent<T>(out T)
Component.GetComponentInChildren<T>()
Component.GetComponentsInChildren<T>()
Component.GetComponentInParent<T>()
Component.GetComponentsInParent<T>()
Component.GetComponents<T>()
Component.GetComponentIndex()
Component.CompareTag(TagHandle)
Component.transform
Component.transformHandle
Component.gameObject
Component.tag
Object.GetEntityId()
Object.GetInstanceID()
Object.GetHashCode()
Object.InstantiateAsync<T>(T)
Object.InstantiateAsync<T>(T, Transform)
Object.InstantiateAsync<T>(T, Vector3, Quaternion)
Object.InstantiateAsync<T>(T, Transform, Vector3, Quaternion)
Object.Instantiate(Object, Vector3, Quaternion)
Object.Instantiate(Object, Vector3, Quaternion, Transform)
Object.Instantiate(Object)
Object.Instantiate(Object, Scene)
Object.Instantiate<T>(T, InstantiateParameters)
Object.Instantiate<T>(T, Vector3, Quaternion, InstantiateParameters)
Object.Instantiate(Object, Transform)
Object.Instantiate<T>(T)
Object.Instantiate<T>(T, Vector3, Quaternion)
Object.Instantiate<T>(T, Vector3, Quaternion, Transform)
Object.Instantiate<T>(T, Transform)
Object.Destroy(Object)
Object.DestroyImmediate(Object)
Object.DontDestroyOnLoad(Object)
Object.DestroyObject(Object)
Object.FindObjectsOfType<T>()
Object.FindObjectsByType<T>(FindObjectsSortMode)
Object.FindObjectsByType<T>(FindObjectsInactive, FindObjectsSortMode)
Object.FindObjectOfType<T>()
Object.FindFirstObjectByType<T>()
Object.FindAnyObjectByType<T>()
Object.FindFirstObjectByType<T>(FindObjectsInactive)
Object.FindAnyObjectByType<T>(FindObjectsInactive)
Object.FindObjectsByType<T>()
Object.FindObjectsByType<T>(FindObjectsInactive)
Object.name
Object.hideFlags

Remarks

Who simulates. The server and the owning client. Both are ticked from OnReplicate(ref CharacterReplicateData, ReplicateState, Channel), which CharacterPredictionController drives from its own [Replicate] method — an NPC's runs on the server only, a player's on the server and on the owner. State forwarding is authored OFF on every prefab, so an observer never runs Replicate or OnReconcile for somebody else's character.

What observers do. They hold the same Buffs entries everyone else does, so Inspect, the target frame and aggro logic read real state rather than a display projection, and they count those durations down locally from TimeManager.OnTick — the peer's controller is a spawned NetworkBehaviour with a perfectly good TimeManager, it just has no replicate to drive it. What they do NOT do is APPLY them: the attribute broadcast already carries every buff's contribution inside ExternalModifier, and the resource broadcast already carries the result of every damage-over-time tick, so running the effects here would count both twice. See SimulatesBuffEffects.

Properties

Buffs

Public accessor for the character's active buffs.

public SortedDictionary<int, Buff> Buffs { get; }

Property Value

SortedDictionary<int, Buff>

OnBuffApplyTriggers

Triggers invoked when a buff or debuff is applied to this character. EventData: BuffEventData.

public List<Trigger> OnBuffApplyTriggers { get; }

Property Value

List<Trigger>

OnBuffRemoveTriggers

Triggers invoked when a buff or debuff is removed from this character. EventData: BuffEventData.

public List<Trigger> OnBuffRemoveTriggers { get; }

Property Value

List<Trigger>

Order

Execution order in the unified prediction pipeline. Runs after KCCPlayer so movement/camera state is current, and before cooldowns, attributes, and ability activation.

public int Order { get; }

Property Value

int

Remarks

Must stay below Order (95). Restoring a buff restates its own ledger entry through CharacterAttribute.SetSource; the attribute reconcile then installs the server's total as the residual over whatever those entries sum to. Raising this above 95 would have that residual computed before this tick's buffs are settled, so a buff gained on the reconciled tick counts twice. See the remarks on Order.

Methods

Apply(BaseBuffTemplate, PredictionTick, ICharacter)

Applies a buff using the provided prediction tick as the application time. This should be used by prediction-path callers to compute ExpiryTick deterministically rather than using TimeManager.LocalTick. Creates a new buff instance if needed and handles stacking.

public void Apply(BaseBuffTemplate template, PredictionTick currentTick, ICharacter caster = null)

Parameters

template BaseBuffTemplate

The buff template to apply.

currentTick PredictionTick

The prediction tick at the time of application.

caster ICharacter

The character applying the buff, snapshotted for attribution. May be null.

Apply(Buff, bool)

Applies a pre-constructed buff instance to the character if not already present. Restores attribute modifiers for the base application and each existing stack (e.g., from DB or network payload). Stacks are not incremented because they are already set.

public void Apply(Buff buff, bool suppressFX = false)

Parameters

buff Buff

The buff instance to apply.

suppressFX bool

ApplyAuthoritative(BaseBuffTemplate, uint, ICharacter)

Applies a buff from a server-authoritative context (Region triggers, Shrine interactions, and any ECA action that lacks a TickEventData and falls back to a raw tick).

serverTick is accepted as a fallback for callers that fire before the first OnReplicate(ref CharacterReplicateData, ReplicateState, Channel) (e.g., spawn-time application). Once OnReplicate(ref CharacterReplicateData, ReplicateState, Channel) has run at least once, raw authoritative ticks collapse to the current replicate-domain tick. They must not preserve elapsed LocalTick drift because Tick(uint) evaluates expiry against input.GetTick(), which can lag behind or stall relative to TimeManager.LocalTick.

public void ApplyAuthoritative(BaseBuffTemplate template, uint serverTick, ICharacter caster = null)

Parameters

template BaseBuffTemplate
serverTick uint
caster ICharacter

CreateReconcileSnapshot()

Creates a reconcile snapshot of all active buffs. Returns the cached array when buffs haven't changed since the last call. Returns null when no buffs are active.

public BuffReconcileEntry[] CreateReconcileSnapshot()

Returns

BuffReconcileEntry[]

Remarks

Always allocates a fresh array when dirty, even if the length matches. The delta serializer holds a reference to the previous tick's snapshot; mutating in-place would silently update that reference, making prev == next and masking the change (zero bytes sent when bytes should have been sent).

GetCurrentDomainTick()

Gets the current tick in the controller's replicate-domain. This is the reference tick used for all buff comparisons.

public uint GetCurrentDomainTick()

Returns

uint

The current replicate-domain tick.

OnCreateReconcile(ref CharacterReconcileData)

Writes buff reconcile state for this tick.

public void OnCreateReconcile(ref CharacterReconcileData reconcileData)

Parameters

reconcileData CharacterReconcileData

Mutable unified reconcile payload.

OnDestroying()

Called during OnDestroy for custom cleanup logic. Override in derived classes.

public override void OnDestroying()

OnModelReady()

Re-creates buff FX after the character's model (re)loads.

public void OnModelReady()

Remarks

InstantiateRaceModelFromIndex(RaceTemplate, int) destroys the children of MeshRoot before attaching the new model, and buff FX is parented there — so every instance showing at that moment is destroyed by the model swap, and any buff applied BEFORE the model finished loading never had a parent to attach to in the first place. Both cases are the same repair: anything tracked without a live instance is spawned again. EquipmentVisualController re-equips from the same callback for the same reason.

OnOwnershipClient(NetworkConnection)

Re-evaluates who advances this character's buffs when ownership moves.

public override void OnOwnershipClient(NetworkConnection prevOwner)

Parameters

prevOwner NetworkConnection

Remarks

A character that GAINS an owner starts running the replicate, so the observer tick has to stop or its buffs advance twice per tick. One that LOSES its owner is the reverse: the replicate stops and the durations would freeze. Deciding this once at spawn would be wrong in both directions.

What this deliberately does NOT do is reconcile the modifier ledger, and that is only safe while ownership never moves between two live clients. SimulatesBuffEffects decides whether this peer APPLIED a buff's modifiers, so flipping it mid-life desynchronises the applied set from the tracked set: a client that gained a character it had been observing holds materialised buffs whose modifiers it never added, and the next removal would subtract them; one that lost ownership has applied modifiers that MaterializeObservedBuffs would then drop without reversing. The only ownership mutation in the project is RemoveOwnership() on combat logout, where the character is being taken away from that client entirely, so neither case is reachable. Add a GiveOwnership anywhere and this method has to re-assert the ledger first — reverse what the old role applied, or apply what the new role now owns.

OnReconcile(CharacterReconcileData, Channel)

Restores buffs from authoritative reconcile state.

public void OnReconcile(CharacterReconcileData rd, Channel channel)

Parameters

rd CharacterReconcileData

Unified reconcile payload.

channel Channel

Transport channel.

OnReplicate(ref CharacterReplicateData, ReplicateState, Channel)

Runs deterministic buff simulation for the prediction tick.

public void OnReplicate(ref CharacterReplicateData input, ReplicateState state, Channel channel)

Parameters

input CharacterReplicateData

Unified replicate input containing the network tick.

state ReplicateState

Current replicate execution state.

channel Channel

Transport channel.

OnSpawnServer(NetworkConnection)

Replays the current visible buff list to a client that starts observing this character after the last change.

public override void OnSpawnServer(NetworkConnection connection)

Parameters

connection NetworkConnection

Remarks

The change-gated broadcast reaches whoever is observing when the set CHANGES; without this, a player targeting a character they just walked up to would see an empty buff bar until the next buff event on that character. This restores the replay-to-late-joiners behaviour the previous ObserversRpc(BufferLast) carried. An empty list is skipped because an empty bar is what the client already assumes.

The owner is skipped: it is not sent the observed list at all (see BroadcastObservedBuffs(ObservedBuffEntry[], int[], bool)) because it builds its own from the simulation dictionary the spawn payload just handed it.

OnStartNetwork()

Called when the network has initialized this object. May be called for server or client but will only be called once. When as host or server this method will run before OnStartServer. When as client only the method will run before OnStartClient.

public override void OnStartNetwork()

OnStopCharacter()

Called right before Character.OnStopClient. Use this for local client cleanup.

public override void OnStopCharacter()

PopulateInput(ref CharacterReplicateData)

Buffs do not contribute owner input into CharacterReplicateData.

public void PopulateInput(ref CharacterReplicateData input)

Parameters

input CharacterReplicateData

Unified replicate input for this tick.

ReadPayload(NetworkConnection, Reader)

Reads the buff state from the network payload.

public override void ReadPayload(NetworkConnection conn, Reader reader)

Parameters

conn NetworkConnection

The network connection.

reader Reader

The network reader to read from.

Remarks

Two shapes, chosen by the server. The owner receives its simulation — absolute ticks, hidden buffs, tick counters — because it is about to go on predicting it. Nobody else does. An observer used to receive the same block and feed it through Apply(buff, suppressFX: false), which instantiated the FX prefab and pushed the buff's attribute modifiers into that observer's local copy of somebody else's character — and then, because state forwarding is off and observers never tick, left them there forever. A poison that killed its victim was still slowing them, on every onlooker's screen, for as long as they stayed in view. It was also inconsistent: a buff gained while you were ALREADY watching arrived over CharacterBuffsBroadcast as an icon with no FX at all, so what an observer saw depended on when they happened to arrive.

The observer block therefore carries what an observer can actually use: template, stacks and remaining seconds — the same shape the broadcast carries, arriving through the same ApplyObservedBuffs(ObservedBuffEntry[]) path, so both routes produce identical icons and identical FX.

Never bare-return. FishNet packs every behaviour's payload into one buffer with no per-behaviour framing, so a reader that stops early leaves every behaviour after this one decoding from the wrong offset. Every exit below seeks to the end of this behaviour's frame first.

Remove(int)

Removes a buff by template ID, cleaning up all stack modifiers and the base application, then invoking removal events.

public void Remove(int buffID)

Parameters

buffID int

The template ID of the buff to remove.

RemoveAll(bool, bool, bool)

Removes all non-permanent buffs from the character, cleaning up all stack modifiers.

public void RemoveAll(bool ignoreInvokeRemove = false, bool includePermanent = false, bool preserveFX = false)

Parameters

ignoreInvokeRemove bool

If true, does not invoke OnRemoveBuff/OnRemoveDebuff events.

includePermanent bool

True to remove permanent buffs as well. Default false, so gameplay dispels leave them alone; the two lifecycle callers (ResetState(bool) and ReadPayload(NetworkConnection, Reader)) pass true because a pooled object must not inherit the previous occupant's buffs — and a permanent buff carries attribute modifiers, so leaving one behind leaked them into the next character to use that instance.

preserveFX bool

RemoveRandom(DeterministicRNG, bool, bool)

Removes a random non-permanent buff or debuff, filtered by inclusion flags. Uses a single pass to build eligible candidates, avoiding retry loops.

public void RemoveRandom(DeterministicRNG rng, bool includeBuffs = false, bool includeDebuffs = false)

Parameters

rng DeterministicRNG

The random number generator to use.

includeBuffs bool

Whether to include buffs in the selection.

includeDebuffs bool

Whether to include debuffs in the selection.

Remarks

Uses a dedicated FishMMO.Shared.BuffController.eligibleBuffer instead of the shared FishMMO.Shared.BuffController.keysToRemove to avoid clearing mid-iteration if called from within a Tick(uint) callback (e.g., a buff's OnTick triggers a dispel).

ResetState(bool)

Resets the buff controller state, properly removing all buffs to undo attribute modifiers. Without this, buffs.Clear() alone would leave phantom modifiers on the attribute controller after a reconnect or scene transfer.

public override void ResetState(bool asServer)

Parameters

asServer bool

Whether the reset is being performed on the server.

ResolveAuthoritativeTick(uint)

Maps a raw authoritative tick to the current replicate-domain tick when available.

public uint ResolveAuthoritativeTick(uint serverTick)

Parameters

serverTick uint

Fallback authoritative tick.

Returns

uint

The current replicate-domain tick if one can be derived, otherwise serverTick.

RestoreFromReconcile(BuffReconcileEntry[], uint)

Restores buff state from a reconcile snapshot using a diff-first approach. Only modifies buffs that actually differ from the authoritative state, avoiding redundant Remove+Apply cycles that would churn attribute modifiers and fire non-idempotent side effects (sound, VFX, DB writes) on every reconcile tick.

public void RestoreFromReconcile(BuffReconcileEntry[] entries, uint reconcileTick)

Parameters

entries BuffReconcileEntry[]

Authoritative buff snapshot.

reconcileTick uint

Replicate tick associated with the reconcile snapshot.

Remarks

For new buffs, the constructor receives 0 stacks and then AddStack(ICharacter) is called incrementally. This matches the normal Apply path where each stack sees the correct Stacks value at the time of application. Calling OnApplyStack directly with the final Stacks value pre-set would produce different results if any template inspects buff.Stacks to scale modifiers.

Tick(uint)

Deterministic buff tick. Evaluates expiry and tick conditions for all active buffs, triggers effects, removes expired stacks, and queues fully expired buffs for removal. Tick-based timing (ExpiryTick, NextTickTick) produces zero float drift; FishMMO.Shared.BuffController.snapshotDirty is only set when state actually changes, restoring the delta serializer's ReferenceEquals fast-path.

public void Tick(uint currentTick)

Parameters

currentTick uint

The current network tick.

WritePayload(NetworkConnection, Writer)

Writes this character's buff state into the spawn payload, in the shape the receiving connection can use.

public override void WritePayload(NetworkConnection conn, Writer writer)

Parameters

conn NetworkConnection

The network connection.

writer Writer

The network writer to write to.

Remarks

The first field is the current reference tick for the serialized absolute buff ticks; it is only meaningful to the owner, and sits outside the frame because it predates it. See ReadPayload(NetworkConnection, Reader) for the two shapes.