/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; // Cut 2 (docs/stats-and-power-cut.md): one resolver per Entity, owning every resolved (item, stat) value for // that entity. PerformanceStat -- the catalog object -- used to hold two Dictionary fields and grew // one entry per entity that ever evaluated it, for the life of the process (§0.3). A stat is shared; a resolved // value is not (§0b) -- that sentence is this type's whole reason to exist. It is reachable only from the Entity // that owns it, so it dies when the entity does, not when the catalog does. // // "Owner" below is whatever called Resolve/AttachModifier for a stat -- an EquippedItem or a ConsumableItemEffect, // compared by reference, never by value. Two ships equipping the same design share the PerformanceStat instance // but never share an owner, so they never share a cached value or a modifier. public class StatResolver { private readonly struct Entry : IEquatable { public readonly object Owner; public readonly PerformanceStat Stat; public Entry(object owner, PerformanceStat stat) { Owner = owner; Stat = stat; } public bool Equals(Entry other) => ReferenceEquals(Owner, other.Owner) && ReferenceEquals(Stat, other.Stat); public override bool Equals(object obj) => obj is Entry other && Equals(other); public override int GetHashCode() => unchecked( RuntimeHelpers.GetHashCode(Owner) * 397 ^ RuntimeHelpers.GetHashCode(Stat)); } // A resolved value is valid only as long as every source it read is still at the generation it read. // Generations are per (owner, source): a term's *source kind*, not the specific stat, so invalidating "this // owner's heat changed" invalidates every stat of that owner's that declared a Heat term, and no others. private class CachedValue { public float Value; public Dictionary Generations; } private class ModifierSet { public readonly Dictionary Scale = new Dictionary(); public readonly Dictionary Constant = new Dictionary(); } private readonly Dictionary _cache = new Dictionary(); private readonly Dictionary _modifiers = new Dictionary(); private readonly Dictionary> _generations = new Dictionary>(); private int GenerationOf(object owner, StatSource source) { if (_generations.TryGetValue(owner, out var perSource) && perSource.TryGetValue(source, out var generation)) return generation; return 0; } // Called by an owner when one of its own sources moves: EquippedItem.UpdatePerformance every tick for Heat // and Durability, ConsumableItemEffect.Update every tick for ConsumableProgress. Invalidates every cached // value of this owner's that declared a term reading this source -- and only those. public void InvalidateSource(object owner, StatSource source) { if (!_generations.TryGetValue(owner, out var perSource)) _generations[owner] = perSource = new Dictionary(); perSource[source] = GenerationOf(owner, source) + 1; } // The only place a resolved value is computed. Returns the cached value when every source the stat's Terms // declared is still at the generation it was computed against; otherwise evaluates once, against the live // context, and caches the new value against the current generations. public float Resolve(object owner, PerformanceStat stat, IStatContext context) { var entry = new Entry(owner, stat); if (_cache.TryGetValue(entry, out var cached) && IsCurrent(owner, cached)) return cached.Value; var value = stat.Evaluate(context); if (float.IsNaN(value)) value = stat.Min; var generations = new Dictionary(); foreach (var term in stat.Terms) if (!generations.ContainsKey(term.Source)) generations[term.Source] = GenerationOf(owner, term.Source); _cache[entry] = new CachedValue { Value = value, Generations = generations }; return value; } private bool IsCurrent(object owner, CachedValue cached) { foreach (var pair in cached.Generations) if (GenerationOf(owner, pair.Key) != pair.Value) return false; return true; } public float ScaleModifier(object owner, PerformanceStat stat) { if (!_modifiers.TryGetValue(new Entry(owner, stat), out var set)) return 1f; var result = 1f; foreach (var value in set.Scale.Values) result *= value; return result; } public float ConstantModifier(object owner, PerformanceStat stat) { if (!_modifiers.TryGetValue(new Entry(owner, stat), out var set)) return 0f; var result = 0f; foreach (var value in set.Constant.Values) result += value; return result; } // Attached by StatModifier when its behaviour executes and detached when it stops (StatModifier.cs); the // modifier's own instance is the key both ways, exactly as it was on the catalog object before this cut -- // only the dictionary this now lives in has changed owner. Invalidates the entry directly: a modifier is not // one of the stat's declared Terms, so the source-generation scheme above does not see it move. public void AttachModifier(object owner, PerformanceStat stat, object modifierKey, StatModifierType type, float value) { var entry = new Entry(owner, stat); if (!_modifiers.TryGetValue(entry, out var set)) _modifiers[entry] = set = new ModifierSet(); (type == StatModifierType.Constant ? set.Constant : set.Scale)[modifierKey] = value; _cache.Remove(entry); } public void DetachModifier(object owner, PerformanceStat stat, object modifierKey) { var entry = new Entry(owner, stat); if (_modifiers.TryGetValue(entry, out var set)) { set.Scale.Remove(modifierKey); set.Constant.Remove(modifierKey); } _cache.Remove(entry); } }