Class BuffController
Controls the application, ticking, and removal of buffs for a character, including network synchronization.
public class BuffController : CharacterBehaviour, IBuffController, ICharacterBehaviour, IPredictableController, IModelReadyHandler
- Inheritance
-
ObjectComponentBehaviourMonoBehaviourNetworkBehaviourBuffController
- Implements
- Inherited Members
-
NetworkBehaviour.IsSpawnedNetworkBehaviour.ComponentIndexNetworkBehaviour.NetworkObjectNetworkBehaviour.MAXIMUM_NETWORKBEHAVIOURSNetworkBehaviour.UNSET_NETWORKBEHAVIOUR_IDNetworkBehaviour.ToString()NetworkBehaviour.Reset()NetworkBehaviour.OnValidate()NetworkBehaviour.IsBehaviourReconcilingNetworkBehaviour.ClearReplicateCache()NetworkBehaviour.CreateReconcile()NetworkBehaviour.Reconcile_Reader<T>(PooledReader, ref T)NetworkBehaviour.OnStartServerCalledNetworkBehaviour.OnStartClientCalledNetworkBehaviour.OnStopNetwork()NetworkBehaviour.OnStartServer()NetworkBehaviour.OnStopServer()NetworkBehaviour.OnOwnershipServer(NetworkConnection)NetworkBehaviour.OnDespawnServer(NetworkConnection)NetworkBehaviour.OnStartClient()NetworkBehaviour.OnStopClient()NetworkBehaviour.ClearBuffedRpcs()NetworkBehaviour.ExcludeOwnerFromUnbufferedObserversRpcsNetworkBehaviour.IsClientOnlyNetworkBehaviour.IsServerOnlyNetworkBehaviour.IsHostNetworkBehaviour.IsClientNetworkBehaviour.IsServerNetworkBehaviour.IsDeinitializingNetworkBehaviour.NetworkManagerNetworkBehaviour.ServerManagerNetworkBehaviour.ClientManagerNetworkBehaviour.ObserverManagerNetworkBehaviour.TransportManagerNetworkBehaviour.TimeManagerNetworkBehaviour.SceneManagerNetworkBehaviour.PredictionManagerNetworkBehaviour.RollbackManagerNetworkBehaviour.NetworkObserverNetworkBehaviour.IsClientInitializedNetworkBehaviour.IsClientStartedNetworkBehaviour.IsClientOnlyInitializedNetworkBehaviour.IsClientOnlyStartedNetworkBehaviour.IsServerInitializedNetworkBehaviour.IsServerStartedNetworkBehaviour.IsServerOnlyInitializedNetworkBehaviour.IsServerOnlyStartedNetworkBehaviour.IsHostInitializedNetworkBehaviour.IsHostStartedNetworkBehaviour.IsOfflineNetworkBehaviour.IsNetworkedNetworkBehaviour.GetIsNetworked()NetworkBehaviour.IsManagerReconcilingNetworkBehaviour.ObserversNetworkBehaviour.IsOwnerNetworkBehaviour.IsControllerNetworkBehaviour.HasAuthorityNetworkBehaviour.OwnerNetworkBehaviour.OwnerIdNetworkBehaviour.ObjectIdNetworkBehaviour.LocalConnectionNetworkBehaviour.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.destroyCancellationTokenMonoBehaviour.useGUILayoutMonoBehaviour.didStartMonoBehaviour.didAwakeMonoBehaviour.runInEditModeBehaviour.enabledBehaviour.isActiveAndEnabledComponent.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.transformComponent.transformHandleComponent.gameObjectComponent.tagObject.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.nameObject.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
OnBuffApplyTriggers
Triggers invoked when a buff or debuff is applied to this character. EventData: BuffEventData.
public List<Trigger> OnBuffApplyTriggers { get; }
Property Value
OnBuffRemoveTriggers
Triggers invoked when a buff or debuff is removed from this character. EventData: BuffEventData.
public List<Trigger> OnBuffRemoveTriggers { get; }
Property Value
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
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
templateBaseBuffTemplateThe buff template to apply.
currentTickPredictionTickThe prediction tick at the time of application.
casterICharacterThe 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
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
templateBaseBuffTemplateserverTickuintcasterICharacter
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
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
reconcileDataCharacterReconcileDataMutable 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
prevOwnerNetworkConnection
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
rdCharacterReconcileDataUnified reconcile payload.
channelChannelTransport 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
inputCharacterReplicateDataUnified replicate input containing the network tick.
stateReplicateStateCurrent replicate execution state.
channelChannelTransport 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
connectionNetworkConnection
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
inputCharacterReplicateDataUnified 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
connNetworkConnectionThe network connection.
readerReaderThe 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
buffIDintThe 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
ignoreInvokeRemoveboolIf true, does not invoke OnRemoveBuff/OnRemoveDebuff events.
includePermanentboolTrue 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.
preserveFXbool
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
rngDeterministicRNGThe random number generator to use.
includeBuffsboolWhether to include buffs in the selection.
includeDebuffsboolWhether 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
asServerboolWhether 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
serverTickuintFallback 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
entriesBuffReconcileEntry[]Authoritative buff snapshot.
reconcileTickuintReplicate 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
currentTickuintThe 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
connNetworkConnectionThe network connection.
writerWriterThe 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.