summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
blob: 367eafeace74d98996dcc3da023e0e2108645e93 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#nullable disable

using System;
using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;

namespace StardewModdingAPI.Framework.StateTracking
{
    /// <summary>Tracks changes to a player's data.</summary>
    internal class PlayerTracker : IDisposable
    {
        /*********
        ** Fields
        *********/
        /// <summary>The player's inventory as of the last reset.</summary>
        private IDictionary<Item, int> PreviousInventory;

        /// <summary>The player's inventory change as of the last update.</summary>
        private IDictionary<Item, int> CurrentInventory;

        /// <summary>The player's last valid location.</summary>
        private GameLocation LastValidLocation;

        /// <summary>The underlying watchers.</summary>
        private readonly List<IWatcher> Watchers = new();


        /*********
        ** Accessors
        *********/
        /// <summary>The player being tracked.</summary>
        public Farmer Player { get; }

        /// <summary>The player's current location.</summary>
        public IValueWatcher<GameLocation> LocationWatcher { get; }

        /// <summary>Tracks changes to the player's skill levels.</summary>
        public IDictionary<SkillType, IValueWatcher<int>> SkillWatchers { get; }


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="player">The player to track.</param>
        public PlayerTracker(Farmer player)
        {
            // init player data
            this.Player = player;
            this.PreviousInventory = this.GetInventory();

            // init trackers
            this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation);
            this.SkillWatchers = new Dictionary<SkillType, IValueWatcher<int>>
            {
                [SkillType.Combat] = WatcherFactory.ForNetValue(player.combatLevel),
                [SkillType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel),
                [SkillType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel),
                [SkillType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel),
                [SkillType.Luck] = WatcherFactory.ForNetValue(player.luckLevel),
                [SkillType.Mining] = WatcherFactory.ForNetValue(player.miningLevel)
            };

            // track watchers for convenience
            this.Watchers.Add(this.LocationWatcher);
            this.Watchers.AddRange(this.SkillWatchers.Values);
        }

        /// <summary>Update the current values if needed.</summary>
        public void Update()
        {
            // update valid location
            this.LastValidLocation = this.GetCurrentLocation();

            // update watchers
            foreach (IWatcher watcher in this.Watchers)
                watcher.Update();

            // update inventory
            this.CurrentInventory = this.GetInventory();
        }

        /// <summary>Reset all trackers so their current values are the baseline.</summary>
        public void Reset()
        {
            foreach (IWatcher watcher in this.Watchers)
                watcher.Reset();

            this.PreviousInventory = this.CurrentInventory;
        }

        /// <summary>Get the player's current location, ignoring temporary null values.</summary>
        /// <remarks>The game will set <see cref="Character.currentLocation"/> to null in some cases, e.g. when they're a secondary player in multiplayer and transition to a location that hasn't been synced yet. While that's happening, this returns the player's last valid location instead.</remarks>
        public GameLocation GetCurrentLocation()
        {
            return this.Player.currentLocation ?? this.LastValidLocation;
        }

        /// <summary>Get the inventory changes since the last update, if anything changed.</summary>
        /// <param name="changes">The inventory changes, or <c>null</c> if nothing changed.</param>
        /// <returns>Returns whether anything changed.</returns>
        public bool TryGetInventoryChanges(out SnapshotItemListDiff changes)
        {
            IDictionary<Item, int> current = this.GetInventory();

            ISet<Item> added = new HashSet<Item>(new ObjectReferenceComparer<Item>());
            ISet<Item> removed = new HashSet<Item>(new ObjectReferenceComparer<Item>());
            foreach (Item item in this.PreviousInventory.Keys.Union(current.Keys))
            {
                if (!this.PreviousInventory.ContainsKey(item))
                    added.Add(item);
                else if (!current.ContainsKey(item))
                    removed.Add(item);
            }

            return SnapshotItemListDiff.TryGetChanges(added: added, removed: removed, stackSizes: this.PreviousInventory, out changes);
        }

        /// <summary>Release watchers and resources.</summary>
        public void Dispose()
        {
            this.PreviousInventory.Clear();
            this.CurrentInventory?.Clear();

            foreach (IWatcher watcher in this.Watchers)
                watcher.Dispose();
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Get the player's current inventory.</summary>
        private IDictionary<Item, int> GetInventory()
        {
            return this.Player.Items
                .Where(n => n != null)
                .Distinct()
                .ToDictionary(n => n, n => n.Stack);
        }
    }
}