using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Deprecations; namespace StardewModdingAPI.Utilities { /// Manages a separate value for each player in split-screen mode. This can safely be used in non-split-screen mode too, it'll just have a single state in that case. /// The state class. public class PerScreen { /********* ** Fields *********/ /// Create the initial value for a screen. private readonly Func CreateNewState; /// The tracked values for each screen. private readonly IDictionary States = new Dictionary(); /// The last value for which this instance was updated. private int LastRemovedScreenId; /********* ** Accessors *********/ /// The value for the current screen. /// The value is initialized the first time it's requested for that screen, unless it's set manually first. public T Value { get => this.GetValueForScreen(Context.ScreenId); set => this.SetValueForScreen(Context.ScreenId, value); } /********* ** Public methods *********/ /// Construct an instance. /// Limitation with nullable reference types: when the underlying type is nullable, this sets the default value to null regardless of whether you marked the type parameter nullable. To avoid that, set the default value with the 'createNewState' argument instead. public PerScreen() : this(null, nullExpected: true) { } /// Construct an instance. /// Create the initial state for a screen. public PerScreen(Func createNewState) : this(createNewState, nullExpected: false) { } /// Get all active values by screen ID. This doesn't initialize the value for a screen ID if it's not created yet. public IEnumerable> GetActiveValues() { this.RemoveDeadScreens(); return this.States.ToArray(); } /// Get the value for a given screen ID, creating it if needed. /// The screen ID to check. public T GetValueForScreen(int screenId) { this.RemoveDeadScreens(); return this.States.TryGetValue(screenId, out T? state) ? state : this.States[screenId] = this.CreateNewState(); } /// Set the value for a given screen ID. /// The screen ID whose value set. /// The value to set. public void SetValueForScreen(int screenId, T value) { this.RemoveDeadScreens(); this.States[screenId] = value; } /// Remove all active values. public void ResetAllScreens() { this.RemoveScreens(_ => true); } /********* ** Private methods *********/ /// Construct an instance. /// Create the initial state for a screen. /// Whether a null value is expected. /// This constructor only exists to maintain backwards compatibility. In SMAPI 4.0.0, the overload that passes nullExpected: false should throw an exception instead. private PerScreen(Func? createNewState, bool nullExpected) { if (createNewState is null) { createNewState = (() => default!); if (!nullExpected) { SCore.DeprecationManager.Warn( null, $"calling the {nameof(PerScreen)} constructor with null", "3.14.0", DeprecationLevel.Notice ); } } this.CreateNewState = createNewState; } /// Remove screens which are no longer active. private void RemoveDeadScreens() { if (this.LastRemovedScreenId == Context.LastRemovedScreenId) return; this.LastRemovedScreenId = Context.LastRemovedScreenId; this.RemoveScreens(id => !Context.HasScreenId(id)); } /// Remove screens matching a condition. /// Returns whether a screen ID should be removed. private void RemoveScreens(Func shouldRemove) { foreach (var pair in this.States.ToArray()) { if (shouldRemove(pair.Key)) this.States.Remove(pair.Key); } } } }