create watcher core (#310)

This commit is contained in:
Jesse Plamondon-Willard 2018-06-10 12:06:29 -04:00
parent e27ada0f61
commit 235d67623d
7 changed files with 219 additions and 120 deletions

View File

@ -36,5 +36,12 @@ namespace StardewModdingAPI.Framework
this.Tile = tile; this.Tile = tile;
this.GrabTile = grabTile; this.GrabTile = grabTile;
} }
/// <summary>Get whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
public bool Equals(ICursorPosition other)
{
return other != null && this.ScreenPixels == other.ScreenPixels;
}
} }
} }

View File

@ -14,7 +14,6 @@ using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.StateTracking; using StardewModdingAPI.Framework.StateTracking;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Framework.Utilities;
using StardewValley; using StardewValley;
using StardewValley.BellsAndWhistles; using StardewValley.BellsAndWhistles;
@ -85,38 +84,8 @@ namespace StardewModdingAPI.Framework
/**** /****
** Game state ** Game state
****/ ****/
/// <summary>The underlying watchers for convenience. These are accessible individually as separate properties.</summary> /// <summary>Monitors the entire game state for changes.</summary>
private readonly List<IWatcher> Watchers = new List<IWatcher>(); private WatcherCore Watchers;
/// <summary>Tracks changes to the window size.</summary>
private IValueWatcher<Point> WindowSizeWatcher;
/// <summary>Tracks changes to the current player.</summary>
private PlayerTracker CurrentPlayerTracker;
/// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
private IValueWatcher<int> TimeWatcher;
/// <summary>Tracks changes to the save ID.</summary>
private IValueWatcher<ulong> SaveIdWatcher;
/// <summary>Tracks changes to the game's locations.</summary>
private WorldLocationsTracker LocationsWatcher;
/// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary>
private IValueWatcher<IClickableMenu> ActiveMenuWatcher;
/// <summary>Tracks changes to the cursor position.</summary>
private IValueWatcher<Vector2> CursorWatcher;
/// <summary>Tracks changes to the mouse wheel scroll.</summary>
private IValueWatcher<int> MouseWheelScrollWatcher;
/// <summary>The previous content locale.</summary>
private LocalizedContentManager.LanguageCode? PreviousLocale;
/// <summary>The previous cursor position.</summary>
private ICursorPosition PreviousCursorPosition;
/// <summary>An index incremented on every tick and reset every 60th tick (059).</summary> /// <summary>An index incremented on every tick and reset every 60th tick (059).</summary>
private int CurrentUpdateTick; private int CurrentUpdateTick;
@ -186,23 +155,7 @@ namespace StardewModdingAPI.Framework
this.Input.TrueUpdate(); this.Input.TrueUpdate();
// init watchers // init watchers
this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.CursorPosition.ScreenPixels); this.Watchers = new WatcherCore(this.Input);
this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue);
this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0);
this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height));
this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay);
this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu);
this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations);
this.Watchers.AddRange(new IWatcher[]
{
this.CursorWatcher,
this.MouseWheelScrollWatcher,
this.SaveIdWatcher,
this.WindowSizeWatcher,
this.TimeWatcher,
this.ActiveMenuWatcher,
this.LocationsWatcher
});
// raise callback // raise callback
this.OnGameInitialised(); this.OnGameInitialised();
@ -372,44 +325,20 @@ namespace StardewModdingAPI.Framework
/********* /*********
** Update watchers ** Update watchers
*********/ *********/
// reset player this.Watchers.Update();
if (Context.IsWorldReady)
{
if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player)
{
this.CurrentPlayerTracker?.Dispose();
this.CurrentPlayerTracker = new PlayerTracker(Game1.player);
}
}
else
{
if (this.CurrentPlayerTracker != null)
{
this.CurrentPlayerTracker.Dispose();
this.CurrentPlayerTracker = null;
}
}
// update values
foreach (IWatcher watcher in this.Watchers)
watcher.Update();
this.CurrentPlayerTracker?.Update();
this.LocationsWatcher.Update();
/********* /*********
** Locale changed events ** Locale changed events
*********/ *********/
if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) if (this.Watchers.LocaleWatcher.IsChanged)
{ {
var oldValue = this.PreviousLocale; var was = this.Watchers.LocaleWatcher.PreviousValue;
var newValue = LocalizedContentManager.CurrentLanguageCode; var now = this.Watchers.LocaleWatcher.CurrentValue;
this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace);
this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged<string>(was.ToString(), now.ToString()));
if (oldValue != null) this.Watchers.LocaleWatcher.Reset();
this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged<string>(oldValue.ToString(), newValue.ToString()));
this.PreviousLocale = newValue;
} }
/********* /*********
@ -450,12 +379,12 @@ namespace StardewModdingAPI.Framework
// event because we need to notify mods after the game handles the resize, so the // event because we need to notify mods after the game handles the resize, so the
// game's metadata (like Game1.viewport) are updated. That's a bit complicated // game's metadata (like Game1.viewport) are updated. That's a bit complicated
// since the game adds & removes its own handler on the fly. // since the game adds & removes its own handler on the fly.
if (this.WindowSizeWatcher.IsChanged) if (this.Watchers.WindowSizeWatcher.IsChanged)
{ {
if (this.VerboseLogging) if (this.VerboseLogging)
this.Monitor.Log($"Context: window size changed to {this.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); this.Monitor.Log($"Context: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace);
this.Events.Graphics_Resize.Raise(); this.Events.Graphics_Resize.Raise();
this.WindowSizeWatcher.Reset(); this.Watchers.WindowSizeWatcher.Reset();
} }
/********* /*********
@ -470,21 +399,23 @@ namespace StardewModdingAPI.Framework
ICursorPosition cursor = this.Input.CursorPosition; ICursorPosition cursor = this.Input.CursorPosition;
// raise cursor moved event // raise cursor moved event
if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null) if (this.Watchers.CursorWatcher.IsChanged)
{ {
this.CursorWatcher.Reset(); ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue;
this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(this.PreviousCursorPosition, cursor)); ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue;
this.Watchers.CursorWatcher.Reset();
this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(was, now));
} }
this.PreviousCursorPosition = cursor;
// raise mouse wheel scrolled // raise mouse wheel scrolled
if (this.MouseWheelScrollWatcher.IsChanged) if (this.Watchers.MouseWheelScrollWatcher.IsChanged)
{ {
int oldValue = this.MouseWheelScrollWatcher.PreviousValue; int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue;
int newValue = this.MouseWheelScrollWatcher.CurrentValue; int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue;
this.MouseWheelScrollWatcher.Reset(); this.Watchers.MouseWheelScrollWatcher.Reset();
this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, oldValue, newValue)); this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, was, now));
} }
// raise input button events // raise input button events
@ -544,20 +475,20 @@ namespace StardewModdingAPI.Framework
/********* /*********
** Menu events ** Menu events
*********/ *********/
if (this.ActiveMenuWatcher.IsChanged) if (this.Watchers.ActiveMenuWatcher.IsChanged)
{ {
IClickableMenu previousMenu = this.ActiveMenuWatcher.PreviousValue; IClickableMenu was = this.Watchers.ActiveMenuWatcher.PreviousValue;
IClickableMenu newMenu = this.ActiveMenuWatcher.CurrentValue; IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue;
this.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards
if (this.VerboseLogging) if (this.VerboseLogging)
this.Monitor.Log($"Context: menu changed from {previousMenu?.GetType().FullName ?? "none"} to {newMenu?.GetType().FullName ?? "none"}.", LogLevel.Trace); this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace);
// raise menu events // raise menu events
if (newMenu != null) if (now != null)
this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu)); this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(was, now));
else else
this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(previousMenu)); this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(was));
} }
/********* /*********
@ -565,22 +496,22 @@ namespace StardewModdingAPI.Framework
*********/ *********/
if (Context.IsWorldReady) if (Context.IsWorldReady)
{ {
bool raiseWorldEvents = !this.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded
// raise location changes // raise location changes
if (this.LocationsWatcher.IsChanged) if (this.Watchers.LocationsWatcher.IsChanged)
{ {
// location list changes // location list changes
if (this.LocationsWatcher.IsLocationListChanged) if (this.Watchers.LocationsWatcher.IsLocationListChanged)
{ {
GameLocation[] added = this.LocationsWatcher.Added.ToArray(); GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray();
GameLocation[] removed = this.LocationsWatcher.Removed.ToArray(); GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray();
this.LocationsWatcher.ResetLocationList(); this.Watchers.LocationsWatcher.ResetLocationList();
if (this.VerboseLogging) if (this.VerboseLogging)
{ {
string addedText = this.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; string addedText = this.Watchers.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none";
string removedText = this.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; string removedText = this.Watchers.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none";
this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace);
} }
@ -591,7 +522,7 @@ namespace StardewModdingAPI.Framework
// raise location contents changed // raise location contents changed
if (raiseWorldEvents) if (raiseWorldEvents)
{ {
foreach (LocationTracker watcher in this.LocationsWatcher.Locations) foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations)
{ {
// buildings changed // buildings changed
if (watcher.BuildingsWatcher.IsChanged) if (watcher.BuildingsWatcher.IsChanged)
@ -652,15 +583,15 @@ namespace StardewModdingAPI.Framework
} }
} }
else else
this.LocationsWatcher.Reset(); this.Watchers.LocationsWatcher.Reset();
} }
// raise time changed // raise time changed
if (raiseWorldEvents && this.TimeWatcher.IsChanged) if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged)
{ {
int was = this.TimeWatcher.PreviousValue; int was = this.Watchers.TimeWatcher.PreviousValue;
int now = this.TimeWatcher.CurrentValue; int now = this.Watchers.TimeWatcher.CurrentValue;
this.TimeWatcher.Reset(); this.Watchers.TimeWatcher.Reset();
if (this.VerboseLogging) if (this.VerboseLogging)
this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace); this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace);
@ -668,12 +599,12 @@ namespace StardewModdingAPI.Framework
this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now));
} }
else else
this.TimeWatcher.Reset(); this.Watchers.TimeWatcher.Reset();
// raise player events // raise player events
if (raiseWorldEvents) if (raiseWorldEvents)
{ {
PlayerTracker curPlayer = this.CurrentPlayerTracker; PlayerTracker curPlayer = this.Watchers.CurrentPlayerTracker;
// raise current location changed // raise current location changed
if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) if (curPlayer.TryGetNewLocation(out GameLocation newLocation))
@ -708,11 +639,11 @@ namespace StardewModdingAPI.Framework
this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel));
} }
} }
this.CurrentPlayerTracker?.Reset(); this.Watchers.CurrentPlayerTracker?.Reset();
} }
// update save ID watcher // update save ID watcher
this.SaveIdWatcher.Reset(); this.Watchers.SaveIdWatcher.Reset();
/********* /*********
** Game update ** Game update

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace StardewModdingAPI.Framework.StateTracking.Comparers
{
/// <summary>Compares values using their <see cref="object.Equals(object)"/> method. This should only be used when <see cref="EquatableComparer{T}"/> won't work, since this doesn't validate whether they're comparable.</summary>
/// <typeparam name="T">The value type.</typeparam>
internal class GenericEqualsComparer<T> : IEqualityComparer<T>
{
/*********
** Public methods
*********/
/// <summary>Determines whether the specified objects are equal.</summary>
/// <returns>true if the specified objects are equal; otherwise, false.</returns>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
public bool Equals(T x, T y)
{
if (x == null)
return y == null;
return x.Equals(y);
}
/// <summary>Get a hash code for the specified object.</summary>
/// <param name="obj">The value.</param>
public int GetHashCode(T obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
}

View File

@ -12,6 +12,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/********* /*********
** Public methods ** Public methods
*********/ *********/
/// <summary>Get a watcher which compares values using their <see cref="object.Equals(object)"/> method. This method should only be used when <see cref="ForEquatable{T}"/> won't work, since this doesn't validate whether they're comparable.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="getValue">Get the current value.</param>
public static ComparableWatcher<T> ForGenericEquality<T>(Func<T> getValue) where T : struct
{
return new ComparableWatcher<T>(getValue, new GenericEqualsComparer<T>());
}
/// <summary>Get a watcher for an <see cref="IEquatable{T}"/> value.</summary> /// <summary>Get a watcher for an <see cref="IEquatable{T}"/> value.</summary>
/// <typeparam name="T">The value type.</typeparam> /// <typeparam name="T">The value type.</typeparam>
/// <param name="getValue">Get the current value.</param> /// <param name="getValue">Get the current value.</param>

View File

@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.Xna.Framework;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.StateTracking;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;
using StardewValley.Menus;
namespace StardewModdingAPI.Framework
{
/// <summary>Monitors the entire game state for changes, virally spreading watchers into any new entities that get created.</summary>
internal class WatcherCore
{
/*********
** Public methods
*********/
/// <summary>The underlying watchers for convenience. These are accessible individually as separate properties.</summary>
private readonly List<IWatcher> Watchers = new List<IWatcher>();
/*********
** Accessors
*********/
/// <summary>Tracks changes to the window size.</summary>
public readonly IValueWatcher<Point> WindowSizeWatcher;
/// <summary>Tracks changes to the current player.</summary>
public PlayerTracker CurrentPlayerTracker;
/// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
public readonly IValueWatcher<int> TimeWatcher;
/// <summary>Tracks changes to the save ID.</summary>
public readonly IValueWatcher<ulong> SaveIdWatcher;
/// <summary>Tracks changes to the game's locations.</summary>
public readonly WorldLocationsTracker LocationsWatcher;
/// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary>
public readonly IValueWatcher<IClickableMenu> ActiveMenuWatcher;
/// <summary>Tracks changes to the cursor position.</summary>
public readonly IValueWatcher<ICursorPosition> CursorWatcher;
/// <summary>Tracks changes to the mouse wheel scroll.</summary>
public readonly IValueWatcher<int> MouseWheelScrollWatcher;
/// <summary>Tracks changes to the content locale.</summary>
public readonly IValueWatcher<LocalizedContentManager.LanguageCode> LocaleWatcher;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="inputState">Manages input visible to the game.</param>
public WatcherCore(SInputState inputState)
{
// init watchers
this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition);
this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.RealMouse.ScrollWheelValue);
this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0);
this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height));
this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay);
this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu);
this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations);
this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode);
this.Watchers.AddRange(new IWatcher[]
{
this.CursorWatcher,
this.MouseWheelScrollWatcher,
this.SaveIdWatcher,
this.WindowSizeWatcher,
this.TimeWatcher,
this.ActiveMenuWatcher,
this.LocationsWatcher,
this.LocaleWatcher
});
}
/// <summary>Update the watchers and adjust for added or removed entities.</summary>
public void Update()
{
// reset player
if (Context.IsWorldReady)
{
if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player)
{
this.CurrentPlayerTracker?.Dispose();
this.CurrentPlayerTracker = new PlayerTracker(Game1.player);
}
}
else
{
if (this.CurrentPlayerTracker != null)
{
this.CurrentPlayerTracker.Dispose();
this.CurrentPlayerTracker = null;
}
}
// update values
foreach (IWatcher watcher in this.Watchers)
watcher.Update();
this.CurrentPlayerTracker?.Update();
this.LocationsWatcher.Update();
}
/// <summary>Reset the current values as the baseline.</summary>
public void Reset()
{
foreach (IWatcher watcher in this.Watchers)
watcher.Reset();
this.CurrentPlayerTracker?.Reset();
this.LocationsWatcher.Reset();
}
}
}

View File

@ -1,9 +1,10 @@
using System;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>Represents a cursor position in the different coordinate systems.</summary> /// <summary>Represents a cursor position in the different coordinate systems.</summary>
public interface ICursorPosition public interface ICursorPosition : IEquatable<ICursorPosition>
{ {
/// <summary>The pixel position relative to the top-left corner of the visible screen.</summary> /// <summary>The pixel position relative to the top-left corner of the visible screen.</summary>
Vector2 ScreenPixels { get; } Vector2 ScreenPixels { get; }

View File

@ -129,6 +129,8 @@
<Compile Include="Framework\Models\ManifestDependency.cs" /> <Compile Include="Framework\Models\ManifestDependency.cs" />
<Compile Include="Framework\ModHelpers\InputHelper.cs" /> <Compile Include="Framework\ModHelpers\InputHelper.cs" />
<Compile Include="Framework\Serialisation\SemanticVersionConverter.cs" /> <Compile Include="Framework\Serialisation\SemanticVersionConverter.cs" />
<Compile Include="Framework\StateTracking\Comparers\GenericEqualsComparer.cs" />
<Compile Include="Framework\WatcherCore.cs" />
<Compile Include="IInputHelper.cs" /> <Compile Include="IInputHelper.cs" />
<Compile Include="Framework\Input\SInputState.cs" /> <Compile Include="Framework\Input\SInputState.cs" />
<Compile Include="Framework\Input\InputStatus.cs" /> <Compile Include="Framework\Input\InputStatus.cs" />