using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;
using StardewValley.Locations;
using SObject = StardewValley.Object;
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; }
/// Tracks changes to the player's current location's objects.
public IDictionaryWatcher LocationObjectsWatcher { get; private set; }
/// 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.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects);
this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0);
this.SkillWatchers = new Dictionary>
{
[EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForEquatable(() => player.combatLevel),
[EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForEquatable(() => player.farmingLevel),
[EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForEquatable(() => player.fishingLevel),
[EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForEquatable(() => player.foragingLevel),
[EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForEquatable(() => player.luckLevel),
[EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForEquatable(() => player.miningLevel)
};
// track watchers for convenience
this.Watchers.AddRange(new IWatcher[]
{
this.LocationWatcher,
this.LocationObjectsWatcher,
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();
// replace location objects watcher
if (this.LocationWatcher.IsChanged)
{
this.Watchers.Remove(this.LocationObjectsWatcher);
this.LocationObjectsWatcher.Dispose();
this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects);
this.Watchers.Add(this.LocationObjectsWatcher);
}
// 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 object changes to the player's current location if they there as of the last reset.
/// The object change watcher.
/// Returns whether it changed.
public bool TryGetLocationChanges(out IDictionaryWatcher watcher)
{
if (this.LocationWatcher.IsChanged)
{
watcher = null;
return false;
}
watcher = this.LocationObjectsWatcher;
return watcher.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);
}
}
}