Class CharacterAttribute
Represents a character attribute, including its value, modifier, dependencies, and hierarchical relationships. Supports parent/child/dependency relationships and value propagation for complex attribute systems.
public class CharacterAttribute
- Inheritance
-
CharacterAttribute
- Derived
- Inherited Members
Constructors
CharacterAttribute(ICharacterAttributeController, int, int, int)
Constructs a new CharacterAttribute from a template ID, initial value, and initial modifier.
public CharacterAttribute(ICharacterAttributeController characterAttributeController, int templateID, int initialValue, int initialModifier)
Parameters
characterAttributeControllerICharacterAttributeControllertemplateIDintThe template ID to use.
initialValueintThe initial base value.
initialModifierintThe initial modifier value.
Fields
OnAttributeUpdated
Event invoked when this attribute is updated (value, modifier, or final value changes).
public Action<CharacterAttribute> OnAttributeUpdated
Field Value
Version
Version number for this attribute instance, used for client synchronization and updates. Incremented whenever the attribute's state changes in a way that requires client updates ( e.g., value or modifier changes that affect the final value). Not incremented for changes that do not affect client state (e.g., internal tracking of dependencies that doesn't meet the next update threshold).
public long Version
Field Value
characterAttributeController
Reference to the controller that manages this attribute, allowing for callbacks and interactions with the owning character or system.
protected ICharacterAttributeController characterAttributeController
Field Value
Properties
Children
Gets the child attributes (attributes this attribute depends on).
public Dictionary<string, CharacterAttribute> Children { get; }
Property Value
Dependencies
Gets the dependency attributes (additional dependencies for this attribute).
public Dictionary<string, CharacterAttribute> Dependencies { get; }
Property Value
ExternalModifier
Gets the modifier accumulated from external sources (items, buffs, regions).
public int ExternalModifier { get; }
Property Value
FinalValue
Gets the final value of the attribute after applying modifiers and clamping.
public int FinalValue { get; }
Property Value
FinalValueAsFloat
Returns the final value as a float.
public float FinalValueAsFloat { get; }
Property Value
FinalValueAsPct
Returns the final value as a percentage (FinalValue * 0.01f).
public float FinalValueAsPct { get; }
Property Value
FormulaModifier
Gets the modifier derived from child attribute formulas.
public int FormulaModifier { get; }
Property Value
Modifier
Gets the total modifier value (formula-derived + external).
public int Modifier { get; }
Property Value
ModifierSourceCount
Number of live contributors. For diagnostics and tests.
public int ModifierSourceCount { get; }
Property Value
Parents
Parents of this attribute (the attributes that depend on it), keyed by Template.ID.
public SortedDictionary<int, CharacterAttribute> Parents { get; }
Property Value
Remarks
SortedDictionary<TKey, TValue> with the default int comparer
guarantees ascending-ID iteration across all platforms, runtimes and rehash events, so
listeners observe the cascade in a stable order. It is NOT what makes the arithmetic
deterministic — ApplyChildren accumulates ints, and integer addition is
associative, so no iteration order can change the value it produces. Do not unsort it on
the strength of that; the notification order is the reason it is a SortedDictionary.
Keying by ID rather than name also survives template renames without affecting sort order.
PersistenceDirty
Whether this attribute has changed since the database last confirmed it.
public bool PersistenceDirty { get; }
Property Value
Remarks
The periodic save used to write every attribute of every resident character on every pass, because it had no way to tell which had moved. Most have not: strength does not drift while a player stands in a bank, and a character out of combat at full health changes nothing at all between one save and the next.
Marked by the writers of the PERSISTED fields, and by nothing else. Those fields
are Value for every attribute and CurrentValue for a resource —
ExternalModifier and FinalValue are not written to the
database, because they are rebuilt from the ledger and the formula graph on load. Every
writer compares before it assigns, so setting a field to the value it already holds marks
nothing.
It used to be marked from Internal_OnAttributeChanged(CharacterAttribute) instead, which is the funnel every change passes through — including changes that move no persisted field at all. Equipping an item, a buff ticking, walking into a region: each marked its attribute dirty, and the periodic save then rewrote a row whose contents were identical. In combat that was most of the sheet, most of the time, which is precisely the case the flag was introduced to avoid.
Template
The template that defines this attribute's configuration and formulas.
public CharacterAttributeTemplate Template { get; }
Property Value
Value
Gets the base value of the attribute (before modifiers).
public int Value { get; }
Property Value
Methods
AddChild(CharacterAttribute)
Adds a child attribute (an attribute this one depends on).
public void AddChild(CharacterAttribute child)
Parameters
childCharacterAttributeThe child attribute to add.
AddDependant(CharacterAttribute)
Adds a dependency attribute.
public void AddDependant(CharacterAttribute dependency)
Parameters
dependencyCharacterAttributeThe dependency attribute to add.
AddModifier(int)
Adds an unattributed amount to the external modifier.
public void AddModifier(int amount)
Parameters
amountintThe amount to add (can be negative).
Remarks
Prefer SetSource(ModifierSource, int). This writes into the Unattributed bucket, which nothing can release except by adding the negation — the exact failure the ledger exists to end. Nothing in the shipped call graph uses it; it survives as a visible escape hatch rather than an absent one, and as the shape the notification-suppression tests exercise.
AddParent(CharacterAttribute)
Adds a parent attribute (an attribute that depends on this one).
public void AddParent(CharacterAttribute parent)
Parameters
parentCharacterAttributeThe parent attribute to add.
AddValue(int, bool)
Adds or subtracts an amount from the base value of the attribute. Addition: AddValue(123) | Subtraction: AddValue(-123)
public void AddValue(int amount, bool forceUpdate = false)
Parameters
amountintThe amount to add (can be negative).
forceUpdateboolIf true, forces update even if value is unchanged.
ClearAllModifierSources()
Drops every contribution, attributed or not.
public void ClearAllModifierSources()
Remarks
For a character being recycled, where the whole sheet belongs to the previous occupant.
Distinct from SetModifierDirect(0), which would install a residual of minus the
attributed sum and leave those sources in place — a total of zero today and the previous
occupant's contributors still in the ledger tomorrow.
ClearSource(ModifierSource)
Removes one named source's contribution.
public void ClearSource(ModifierSource source)
Parameters
sourceModifierSourceThe contributor to release.
Remarks
A source that is not present is a no-op, and that is deliberate: it is the correct answer for a peer that never applied it. The old shape subtracted a stored value unconditionally, so a client whose add had been suppressed still ran the subtraction and drove the sheet negative until the next authoritative push corrected it.
ClearSourceGroup(ModifierSourceKind, long)
Removes every contribution from one contributor, whichever of its entries they are.
public void ClearSourceGroup(ModifierSourceKind kind, long id = 0)
Parameters
kindModifierSourceKindThe kind of contributor to release.
idlongWhich contributor, within that kind.
Remarks
The release half of Index. A contributor that writes to this
attribute more than once — an item with two affixes raising Armor, a buff whose
BonusAttributes names Strength twice — holds one ledger entry per contribution, and
ClearSource(ModifierSource) would let go of exactly one of them. Releasing by contributor
rather than by entry means the apply side is free to pick whatever index scheme suits it
(a template id, a list position) without the release side having to reconstruct it.
A contributor that is not present is a no-op, for the same reason ClearSource(ModifierSource) is: it is the correct answer for a peer that never applied it. An observer applies neither items nor buffs, so every release reaches this and correctly does nothing.
GetDependant(string)
Gets a dependency attribute by name.
public CharacterAttribute GetDependant(string name)
Parameters
namestringThe name of the dependency attribute.
Returns
- CharacterAttribute
The dependency attribute, or null if not found.
GetDependantFinalValue(string)
Gets the final value of a dependency attribute by name.
public int GetDependantFinalValue(string name)
Parameters
namestringThe name of the dependency attribute.
Returns
- int
The final value of the dependency attribute, or 0 if not found.
GetDependantMaxValue(string)
Gets the maximum value of a dependency attribute by name.
public int GetDependantMaxValue(string name)
Parameters
namestringThe name of the dependency attribute.
Returns
- int
The maximum value of the dependency attribute, or 0 if not found.
GetDependantMinValue(string)
Gets the minimum value of a dependency attribute by name.
public int GetDependantMinValue(string name)
Parameters
namestringThe name of the dependency attribute.
Returns
- int
The minimum value of the dependency attribute, or 0 if not found.
GetDependantModifier(string)
Gets the modifier of a dependency attribute by name.
public int GetDependantModifier(string name)
Parameters
namestringThe name of the dependency attribute.
Returns
- int
The modifier of the dependency attribute, or 0 if not found.
GetDependantValue(string)
Gets the value of a dependency attribute by name.
public int GetDependantValue(string name)
Parameters
namestringThe name of the dependency attribute.
Returns
- int
The value of the dependency attribute, or 0 if not found.
GetSourceValue(ModifierSource)
The contribution currently recorded for one source, or zero when it has none.
public int GetSourceValue(ModifierSource source)
Parameters
sourceModifierSource
Returns
Internal_OnAttributeChanged(CharacterAttribute)
Invokes the OnAttributeUpdated event for the given attribute. During graph propagation, the notification is deferred until all values stabilize.
protected virtual void Internal_OnAttributeChanged(CharacterAttribute item)
Parameters
itemCharacterAttributeThe attribute that was changed.
MarkPersistPending(long)
Records the snapshot a save is about to write, so its confirmation can be checked against what the attribute has done since.
public void MarkPersistPending(long stampedVersion)
Parameters
stampedVersionlongThe version written onto the snapshot row.
Remarks
Called on the main thread as the batch is built, immediately after Version is stamped. The mark deliberately stays set until the write is confirmed: an attribute with a save in flight is still one the database does not have, so a second save path — a logout, a despawn — that runs in the meantime must still pick it up.
MarkPersisted(long)
Clears PersistenceDirty if the confirmed write is still the newest thing this attribute has to say.
public void MarkPersisted(long persistedVersion)
Parameters
persistedVersionlongThe version that was successfully written.
Remarks
Two guards, and both are load-bearing. The version must match the snapshot that is being confirmed, so a stale confirmation — an earlier save landing after a later one already snapshotted — cannot clear a mark it knows nothing about. The change count must match what the snapshot saw, so a value that moved while the write was in flight stays dirty and is carried to the next pass.
A save that fails never calls this at all, so nothing is lost — the attribute is simply written again on the following pass. That matters more than it looks: the periodic save has no retry of its own, because writing everything every time WAS the retry.
MarkPersistenceDirty()
Records that this attribute has changed in a way the database does not have yet.
protected void MarkPersistenceDirty()
RemoveChild(CharacterAttribute)
Removes a child attribute.
public void RemoveChild(CharacterAttribute child)
Parameters
childCharacterAttributeThe child attribute to remove.
RemoveDependant(CharacterAttribute)
Removes a dependency attribute.
public void RemoveDependant(CharacterAttribute dependency)
Parameters
dependencyCharacterAttributeThe dependency attribute to remove.
RemoveParent(CharacterAttribute)
Removes a parent attribute.
public void RemoveParent(CharacterAttribute parent)
Parameters
parentCharacterAttributeThe parent attribute to remove.
SetFinalDerivingModifier(int)
Installs an authoritative final value AND back-solves ExternalModifier so a later recompute reproduces it.
public void SetFinalDerivingModifier(int newFinal)
Parameters
newFinalintThe authoritative final value.
Remarks
Writing finalValue directly is what the resource reconcile wants — the server's
number must not be overwritten by a local formula pass. But writing it ALONE (which is
all the removed SetFinal did) leaves value and externalModifier
untouched, and those are what
FishMMO.Shared.CharacterAttribute.CalculateFinalValue() reads. Resource attributes carry neither of them in the
reconcile, so the very next thing that called UpdateValues on the resource — any
AddModifier(int) from a buff, an equip or an unequip — recomputed the final from
state the reconcile had never corrected and threw the authoritative maximum away.
Choosing the modifier that closes the gap makes the two agree: the value is right now, and
it is still right after the next recompute. The clamp is applied deliberately rather than
bypassed, so this peer lands on exactly the number the server's own clamped
CalculateFinalValue produced for the same template.
SetModifier(int)
Installs an authoritative TOTAL, preserving what this peer has attributed.
public void SetModifier(int newValue)
Parameters
newValueintThe server's total external modifier.
Remarks
The total is the server's answer and must be reproduced exactly, but it is not a contributor — so it lands in the Authoritative entry as the RESIDUAL between the server's number and the sum of everything this peer attributed. The observable total is identical to the old wholesale overwrite; what changes is that the attributed sources survive it.
That survival is the point. Collapsing the ledger to a single entry every reconcile would leave the owner unable to release an item or a buff between reconciles — the release would find nothing to remove and silently keep the bonus until the next authoritative push.
SetModifierDirect(int)
Sets the external modifier total without recomputing derived values or notifying listeners. Used exclusively for two-phase reconcile alongside SetValueDirect(int).
public void SetModifierDirect(int newValue)
Parameters
newValueintThe server's total external modifier.
Remarks
The silent twin of SetModifier(int), and it installs the same residual — see there for why the attributed sources are preserved rather than collapsed. Silence is what the two-phase reconcile needs: phase one writes every raw value, phase two runs one graph pass.
SetSource(ModifierSource, int)
Installs or replaces one named source's contribution.
public void SetSource(ModifierSource source, int value)
Parameters
sourceModifierSourceWho is contributing.
valueintTheir whole contribution, not a delta.
Remarks
Idempotent, which is the whole point. Calling this twice with the same source and value leaves one entry worth that value — so applying an item or a buff a second time, from a database load, a payload restore or a reconcile replay, cannot double it. That property is what replaced a set of carefully ordered suppressions.
A value of zero removes the entry. A source contributing nothing is not a contributor, and keeping it would make "is this source applied?" un-answerable.
SetValue(int, bool)
Sets the base value of the attribute and updates dependent values if changed.
public void SetValue(int newValue, bool forceUpdate = false)
Parameters
newValueintThe new base value.
forceUpdateboolIf true, forces update even if value is unchanged.
SetValueDirect(int)
Sets the base value directly without recomputing derived values or notifying listeners. Used exclusively for two-phase reconcile in ApplyAttributeSnapshot(AttributeReconcileEntry[]); the caller is responsible for calling UpdateValues(bool) after all values have been applied to guarantee a single correct graph evaluation pass with no intermediate states.
public void SetValueDirect(int newValue)
Parameters
newValueintThe new base value.
ToString()
Returns a string representation of the attribute (name and final value).
public override string ToString()
Returns
UpdateValues()
Updates the attribute's values and propagates changes to parent attributes if needed.
public void UpdateValues()
UpdateValues(bool)
Updates the attribute's values, propagates changes to parent attributes if needed, and notifies listeners after propagation completes.
The outermost call brackets the entire graph walk with BeginPropagation() / EndPropagation(). Intermediate nodes enqueue notifications instead of firing them, so listeners only see fully-stabilized values.
public void UpdateValues(bool forceUpdate)
Parameters
forceUpdateboolIf true, forces update even if value is unchanged.