using System;
using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;
using StardewValley.Locations;
using ChangeType = StardewModdingAPI.Events.ChangeType;
namespace StardewModdingAPI.Framework.StateTracking
{
/// Tracks changes to a player's data.
internal class PlayerTracker : IDisposable
{
/*********
** Properties
*********/
/// The player's inventory as of the last reset.
private IDictionary- PreviousInventory;
/// The player's inventory change as of the last update.
private IDictionary
- CurrentInventory;
/// The player's last valid location.
private GameLocation LastValidLocation;
/// The underlying watchers.
private readonly List Watchers = new List();
/*********
** Accessors
*********/
/// The player being tracked.
public Farmer Player { get; }
/// The player's current location.
public IValueWatcher LocationWatcher { get; }
/// The player's current mine level.
public IValueWatcher MineLevelWatcher { get; }
/// Tracks changes to the player's skill levels.
public IDictionary> SkillWatchers { get; }
/*********
** Public methods
*********/
/// Construct an instance.
/// The player to track.
public PlayerTracker(Farmer player)
{
// init player data
this.Player = player;
this.PreviousInventory = this.GetInventory();
// init trackers
this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation);
this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0);
this.SkillWatchers = new Dictionary>
{
[EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForNetValue(player.combatLevel),
[EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel),
[EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel),
[EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel),
[EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForNetValue(player.luckLevel),
[EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForNetValue(player.miningLevel)
};
// track watchers for convenience
this.Watchers.AddRange(new IWatcher[]
{
this.LocationWatcher,
this.MineLevelWatcher
});
this.Watchers.AddRange(this.SkillWatchers.Values);
}
/// Update the current values if needed.
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();
}
/// Reset all trackers so their current values are the baseline.
public void Reset()
{
foreach (IWatcher watcher in this.Watchers)
watcher.Reset();
this.PreviousInventory = this.CurrentInventory;
}
/// Get the player's current location, ignoring temporary null values.
/// The game will set 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.
public GameLocation GetCurrentLocation()
{
return this.Player.currentLocation ?? this.LastValidLocation;
}
/// Get the player inventory changes between two states.
public IEnumerable GetInventoryChanges()
{
IDictionary
- previous = this.PreviousInventory;
IDictionary
- current = this.GetInventory();
foreach (Item item in previous.Keys.Union(current.Keys))
{
if (!previous.TryGetValue(item, out int prevStack))
yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added };
else if (!current.TryGetValue(item, out int newStack))
yield return new ItemStackChange { Item = item, StackChange = -item.Stack, ChangeType = ChangeType.Removed };
else if (prevStack != newStack)
yield return new ItemStackChange { Item = item, StackChange = newStack - prevStack, ChangeType = ChangeType.StackChange };
}
}
/// Get the player skill levels which changed.
public IEnumerable>> GetChangedSkills()
{
return this.SkillWatchers.Where(p => p.Value.IsChanged);
}
/// Get the player's new location if it changed.
/// The player's current location.
/// Returns whether it changed.
public bool TryGetNewLocation(out GameLocation location)
{
location = this.LocationWatcher.CurrentValue;
return this.LocationWatcher.IsChanged;
}
/// Get the player's new mine level if it changed.
/// The player's current mine level.
/// Returns whether it changed.
public bool TryGetNewMineLevel(out int mineLevel)
{
mineLevel = this.MineLevelWatcher.CurrentValue;
return this.MineLevelWatcher.IsChanged;
}
/// Stop watching the player fields and release all references.
public void Dispose()
{
foreach (IWatcher watcher in this.Watchers)
watcher.Dispose();
}
/*********
** Private methods
*********/
/// Get the player's current inventory.
private IDictionary
- GetInventory()
{
return this.Player.Items
.Where(n => n != null)
.Distinct()
.ToDictionary(n => n, n => n.Stack);
}
}
}