Table of Contents

Class CharacterAttribute

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

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

characterAttributeController ICharacterAttributeController
templateID int

The template ID to use.

initialValue int

The initial base value.

initialModifier int

The initial modifier value.

Fields

OnAttributeUpdated

Event invoked when this attribute is updated (value, modifier, or final value changes).

public Action<CharacterAttribute> OnAttributeUpdated

Field Value

Action<CharacterAttribute>

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

long

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

ICharacterAttributeController

Properties

Children

Gets the child attributes (attributes this attribute depends on).

public Dictionary<string, CharacterAttribute> Children { get; }

Property Value

Dictionary<string, CharacterAttribute>

Dependencies

Gets the dependency attributes (additional dependencies for this attribute).

public Dictionary<string, CharacterAttribute> Dependencies { get; }

Property Value

Dictionary<string, CharacterAttribute>

ExternalModifier

Gets the modifier accumulated from external sources (items, buffs, regions).

public int ExternalModifier { get; }

Property Value

int

FinalValue

Gets the final value of the attribute after applying modifiers and clamping.

public int FinalValue { get; }

Property Value

int

FinalValueAsFloat

Returns the final value as a float.

public float FinalValueAsFloat { get; }

Property Value

float

FinalValueAsPct

Returns the final value as a percentage (FinalValue * 0.01f).

public float FinalValueAsPct { get; }

Property Value

float

FormulaModifier

Gets the modifier derived from child attribute formulas.

public int FormulaModifier { get; }

Property Value

int

Modifier

Gets the total modifier value (formula-derived + external).

public int Modifier { get; }

Property Value

int

ModifierSourceCount

Number of live contributors. For diagnostics and tests.

public int ModifierSourceCount { get; }

Property Value

int

Parents

Parents of this attribute (the attributes that depend on it), keyed by Template.ID.

public SortedDictionary<int, CharacterAttribute> Parents { get; }

Property Value

SortedDictionary<int, CharacterAttribute>

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

bool

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

CharacterAttributeTemplate

Value

Gets the base value of the attribute (before modifiers).

public int Value { get; }

Property Value

int

Methods

AddChild(CharacterAttribute)

Adds a child attribute (an attribute this one depends on).

public void AddChild(CharacterAttribute child)

Parameters

child CharacterAttribute

The child attribute to add.

AddDependant(CharacterAttribute)

Adds a dependency attribute.

public void AddDependant(CharacterAttribute dependency)

Parameters

dependency CharacterAttribute

The dependency attribute to add.

AddModifier(int)

Adds an unattributed amount to the external modifier.

public void AddModifier(int amount)

Parameters

amount int

The 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

parent CharacterAttribute

The 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

amount int

The amount to add (can be negative).

forceUpdate bool

If 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

source ModifierSource

The 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

kind ModifierSourceKind

The kind of contributor to release.

id long

Which 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

name string

The 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

name string

The 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

name string

The 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

name string

The 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

name string

The 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

name string

The 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

source ModifierSource

Returns

int

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

item CharacterAttribute

The 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

stampedVersion long

The 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

persistedVersion long

The 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

child CharacterAttribute

The child attribute to remove.

RemoveDependant(CharacterAttribute)

Removes a dependency attribute.

public void RemoveDependant(CharacterAttribute dependency)

Parameters

dependency CharacterAttribute

The dependency attribute to remove.

RemoveParent(CharacterAttribute)

Removes a parent attribute.

public void RemoveParent(CharacterAttribute parent)

Parameters

parent CharacterAttribute

The 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

newFinal int

The 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

newValue int

The 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

newValue int

The 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

source ModifierSource

Who is contributing.

value int

Their 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

newValue int

The new base value.

forceUpdate bool

If 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

newValue int

The new base value.

ToString()

Returns a string representation of the attribute (name and final value).

public override string ToString()

Returns

string

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

forceUpdate bool

If true, forces update even if value is unchanged.