From 68528f7decadccb4c5ed62f3fff10aeff22dcd43 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 23 Feb 2018 19:05:23 -0500 Subject: overhaul events to track the mod which added each handler, and log errors under their name (#451) --- src/SMAPI/Framework/Events/EventManager.cs | 249 +++++++++++++++++++++++++ src/SMAPI/Framework/Events/ManagedEvent.cs | 119 ++++++++++++ src/SMAPI/Framework/Events/ManagedEventBase.cs | 81 ++++++++ src/SMAPI/Framework/InternalExtensions.cs | 58 ------ src/SMAPI/Framework/SGame.cs | 142 +++++++------- 5 files changed, 526 insertions(+), 123 deletions(-) create mode 100644 src/SMAPI/Framework/Events/EventManager.cs create mode 100644 src/SMAPI/Framework/Events/ManagedEvent.cs create mode 100644 src/SMAPI/Framework/Events/ManagedEventBase.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs new file mode 100644 index 00000000..d7c89a76 --- /dev/null +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -0,0 +1,249 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Manages SMAPI events. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Private fields are deliberately named to simplify organisation.")] + internal class EventManager + { + /********* + ** Properties + *********/ + /**** + ** ContentEvents + ****/ + /// Raised after the content language changes. + public readonly ManagedEvent> Content_LocaleChanged; + + /**** + ** ControlEvents + ****/ + /// Raised when the changes. That happens when the player presses or releases a key. + public readonly ManagedEvent Control_KeyboardChanged; + + /// Raised when the player presses a keyboard key. + public readonly ManagedEvent Control_KeyPressed; + + /// Raised when the player releases a keyboard key. + public readonly ManagedEvent Control_KeyReleased; + + /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. + public readonly ManagedEvent Control_MouseChanged; + + /// The player pressed a controller button. This event isn't raised for trigger buttons. + public readonly ManagedEvent Control_ControllerButtonPressed; + + /// The player released a controller button. This event isn't raised for trigger buttons. + public readonly ManagedEvent Control_ControllerButtonReleased; + + /// The player pressed a controller trigger button. + public readonly ManagedEvent Control_ControllerTriggerPressed; + + /// The player released a controller trigger button. + public readonly ManagedEvent Control_ControllerTriggerReleased; + + /**** + ** GameEvents + ****/ + /// Raised once after the game initialises and all methods have been called. + public readonly ManagedEvent Game_FirstUpdateTick; + + /// Raised when the game updates its state (≈60 times per second). + public readonly ManagedEvent Game_UpdateTick; + + /// Raised every other tick (≈30 times per second). + public readonly ManagedEvent Game_SecondUpdateTick; + + /// Raised every fourth tick (≈15 times per second). + public readonly ManagedEvent Game_FourthUpdateTick; + + /// Raised every eighth tick (≈8 times per second). + public readonly ManagedEvent Game_EighthUpdateTick; + + /// Raised every 15th tick (≈4 times per second). + public readonly ManagedEvent Game_QuarterSecondTick; + + /// Raised every 30th tick (≈twice per second). + public readonly ManagedEvent Game_HalfSecondTick; + + /// Raised every 60th tick (≈once per second). + public readonly ManagedEvent Game_OneSecondTick; + + /**** + ** GraphicsEvents + ****/ + /// Raised after the game window is resized. + public readonly ManagedEvent Graphics_Resize; + + /// Raised before drawing the world to the screen. + public readonly ManagedEvent Graphics_OnPreRenderEvent; + + /// Raised after drawing the world to the screen. + public readonly ManagedEvent Graphics_OnPostRenderEvent; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public readonly ManagedEvent Graphics_OnPreRenderHudEvent; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public readonly ManagedEvent Graphics_OnPostRenderHudEvent; + + /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public readonly ManagedEvent Graphics_OnPreRenderGuiEvent; + + /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public readonly ManagedEvent Graphics_OnPostRenderGuiEvent; + + /**** + ** InputEvents + ****/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonPressed; + + /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonReleased; + + /**** + ** LocationEvents + ****/ + /// Raised after the player warps to a new location. + public readonly ManagedEvent Location_CurrentLocationChanged; + + /// Raised after a game location is added or removed. + public readonly ManagedEvent Location_LocationsChanged; + + /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + public readonly ManagedEvent Location_LocationObjectsChanged; + + /**** + ** MenuEvents + ****/ + /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. + public readonly ManagedEvent Menu_Changed; + + /// Raised after a game menu is closed. + public readonly ManagedEvent Menu_Closed; + + /**** + ** MineEvents + ****/ + /// Raised after the player warps to a new level of the mine. + public readonly ManagedEvent Mine_LevelChanged; + + /**** + ** PlayerEvents + ****/ + /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). + public readonly ManagedEvent Player_InventoryChanged; + + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public readonly ManagedEvent Player_LeveledUp; + + /**** + ** SaveEvents + ****/ + /// Raised before the game creates the save file. + public readonly ManagedEvent Save_BeforeCreate; + + /// Raised after the game finishes creating the save file. + public readonly ManagedEvent Save_AfterCreate; + + /// Raised before the game begins writes data to the save file. + public readonly ManagedEvent Save_BeforeSave; + + /// Raised after the game finishes writing data to the save file. + public readonly ManagedEvent Save_AfterSave; + + /// Raised after the player loads a save slot. + public readonly ManagedEvent Save_AfterLoad; + + /// Raised after the game returns to the title screen. + public readonly ManagedEvent Save_AfterReturnToTitle; + + /**** + ** SpecialisedEvents + ****/ + /// Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console. + public readonly ManagedEvent Specialised_UnvalidatedUpdateTick; + + /**** + ** TimeEvents + ****/ + /// Raised after the game begins a new day, including when loading a save. + public readonly ManagedEvent Time_AfterDayStarted; + + /// Raised after the in-game clock changes. + public readonly ManagedEvent Time_TimeOfDayChanged; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public EventManager(IMonitor monitor, ModRegistry modRegistry) + { + // create shortcut initialisers + ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); + ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); + + // init events + this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); + + this.Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + + this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); + this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); + this.Game_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); + this.Game_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); + this.Game_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); + this.Game_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); + this.Game_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); + this.Game_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); + + this.Graphics_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); + this.Graphics_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); + this.Graphics_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); + this.Graphics_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); + this.Graphics_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); + this.Graphics_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); + this.Graphics_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); + + this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + + this.Location_CurrentLocationChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); + this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Location_LocationObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + + this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); + this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + + this.Mine_LevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); + + this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); + this.Player_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + + this.Save_BeforeCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); + this.Save_AfterCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); + this.Save_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); + this.Save_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); + this.Save_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); + this.Save_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); + + this.Specialised_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); + + this.Time_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); + this.Time_TimeOfDayChanged = ManageEventOf(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); + } + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs new file mode 100644 index 00000000..e54a4fd3 --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -0,0 +1,119 @@ +using System; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// An event wrapper which intercepts and logs errors in handler code. + /// The event arguments type. + internal class ManagedEvent : ManagedEventBase> + { + /********* + ** Properties + *********/ + /// The underlying event. + private event EventHandler Event; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// Add an event handler. + /// The event handler. + public void Add(EventHandler handler) + { + this.Event += handler; + this.AddTracking(handler, this.Event?.GetInvocationList().Cast>()); + } + + /// Remove an event handler. + /// The event handler. + public void Remove(EventHandler handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); + } + + /// Raise the event and notify all handlers. + /// The event arguments to pass. + public void Raise(TEventArgs args) + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } + + /// An event wrapper which intercepts and logs errors in handler code. + internal class ManagedEvent : ManagedEventBase + { + /********* + ** Properties + *********/ + /// The underlying event. + private event EventHandler Event; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// Add an event handler. + /// The event handler. + public void Add(EventHandler handler) + { + this.Event += handler; + this.AddTracking(handler, this.Event?.GetInvocationList().Cast()); + } + + /// Remove an event handler. + /// The event handler. + public void Remove(EventHandler handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast()); + } + + /// Raise the event and notify all handlers. + public void Raise() + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, EventArgs.Empty); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs new file mode 100644 index 00000000..cc4d89ec --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// The base implementation for an event wrapper which intercepts and logs errors in handler code. + internal abstract class ManagedEventBase + { + /********* + ** Properties + *********/ + /// A human-readable name for the event. + private readonly string EventName; + + /// Writes messages to the log. + private readonly IMonitor Monitor; + + /// The mod registry with which to identify mods. + private readonly ModRegistry ModRegistry; + + /// The display names for the mods which added each delegate. + private readonly IDictionary SourceMods = new Dictionary(); + + /// The cached invocation list. + protected TEventHandler[] CachedInvocationList { get; private set; } + + + /********* + ** Public methods + *********/ + /// Get whether anything is listening to the event. + public bool HasListeners() + { + return this.CachedInvocationList?.Length > 0; + } + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + protected ManagedEventBase(string eventName, IMonitor monitor, ModRegistry modRegistry) + { + this.EventName = eventName; + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// Track an event handler. + /// The event handler. + /// The updated event invocation list. + protected void AddTracking(TEventHandler handler, IEnumerable invocationList) + { + this.SourceMods[handler] = this.ModRegistry.GetFromStack(); + this.CachedInvocationList = invocationList.ToArray(); + } + + /// Remove tracking for an event handler. + /// The event handler. + /// The updated event invocation list. + protected void RemoveTracking(TEventHandler handler, IEnumerable invocationList) + { + this.SourceMods.Remove(handler); + this.CachedInvocationList = invocationList.ToArray(); + } + + /// Log an exception from an event handler. + /// The event handler instance. + /// The exception that was raised. + protected void LogError(TEventHandler handler, Exception ex) + { + if (this.SourceMods.TryGetValue(handler, out IModMetadata mod)) + mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + } + } +} diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index ce67ae18..bff4807c 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; using Newtonsoft.Json.Linq; @@ -15,63 +14,6 @@ namespace StardewModdingAPI.Framework /**** ** IMonitor ****/ - /// Safely raise an event, and intercept any exceptions thrown by its handlers. - /// Encapsulates monitoring and logging. - /// The event name for error messages. - /// The event handlers. - /// The event sender. - /// The event arguments (or null to pass ). - public static void SafelyRaisePlainEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender = null, EventArgs args = null) - { - if (handlers == null) - return; - - foreach (EventHandler handler in handlers.Cast()) - { - // handle SMAPI exiting - if (monitor.IsExiting) - { - monitor.Log($"SMAPI shutting down: aborting {name} event.", LogLevel.Warn); - return; - } - - // raise event - try - { - handler.Invoke(sender, args ?? EventArgs.Empty); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - - /// Safely raise an event, and intercept any exceptions thrown by its handlers. - /// The event argument object type. - /// Encapsulates monitoring and logging. - /// The event name for error messages. - /// The event handlers. - /// The event sender. - /// The event arguments. - public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) - { - if (handlers == null) - return; - - foreach (EventHandler handler in handlers.Cast>()) - { - try - { - handler.Invoke(sender, args); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - /// Log a message for the player or developer the first time it occurs. /// The monitor through which to log the message. /// The hash of logged messages. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 2d3bad55..5c45edca 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -10,6 +10,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; @@ -35,6 +36,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// Manages SMAPI events for mods. + private readonly EventManager Events; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -117,6 +121,9 @@ namespace StardewModdingAPI.Framework /// The current game instance. private static SGame Instance; + /// A callback to invoke after the game finishes initialising. + private readonly Action OnGameInitialised; + /**** ** Private wrappers ****/ @@ -132,9 +139,9 @@ namespace StardewModdingAPI.Framework set => SGame.Reflection.GetField(typeof(Game1), nameof(_fps)).SetValue(value); } private static Task _newDayTask => SGame.Reflection.GetField(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetField(this, nameof(bgColor)).GetValue(); + private Color bgColor => SGame.Reflection.GetField(this, nameof(this.bgColor)).GetValue(); public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetField(this, nameof(lightingBlend)).GetValue(); + public BlendState lightingBlend => SGame.Reflection.GetField(this, nameof(this.lightingBlend)).GetValue(); private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke(); private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); @@ -158,13 +165,17 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// Encapsulates monitoring and logging. /// Simplifies access to private game code. - internal SGame(IMonitor monitor, Reflector reflection) + /// Manages SMAPI events for mods. + /// A callback to invoke after the game finishes initialising. + internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised) { // initialise this.Monitor = monitor; + this.Events = eventManager; this.FirstUpdate = true; SGame.Instance = this; SGame.Reflection = reflection; + this.OnGameInitialised = onGameInitialised; // set XNA option required by Stardew Valley Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -229,7 +240,7 @@ namespace StardewModdingAPI.Framework if (SGame._newDayTask != null) { base.Update(gameTime); - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } @@ -237,7 +248,7 @@ namespace StardewModdingAPI.Framework if (Game1.gameMode == Game1.loadingMode) { base.Update(gameTime); - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } @@ -256,20 +267,20 @@ namespace StardewModdingAPI.Framework { this.IsBetweenCreateEvents = true; this.Monitor.Log("Context: before save creation.", LogLevel.Trace); - SaveEvents.InvokeBeforeCreate(this.Monitor); + this.Events.Save_BeforeCreate.Raise(); } - + // raise before-save if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); + this.Events.Save_BeforeSave.Raise(); } // suppress non-save events base.Update(gameTime); - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } if (this.IsBetweenCreateEvents) @@ -277,22 +288,22 @@ namespace StardewModdingAPI.Framework // raise after-create this.IsBetweenCreateEvents = false; this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterCreated(this.Monitor); + this.Events.Save_AfterCreate.Raise(); } if (this.IsBetweenSaveEvents) { // raise after-save this.IsBetweenSaveEvents = false; this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterSave(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); + this.Events.Save_AfterSave.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } /********* - ** Game loaded events + ** Notify SMAPI that game is initialised *********/ if (this.FirstUpdate) - GameEvents.InvokeInitialize(this.Monitor); + this.OnGameInitialised(); /********* ** Locale changed events @@ -305,7 +316,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); + this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged(oldValue.ToString(), newValue.ToString())); + this.PreviousLocale = newValue; } @@ -322,8 +334,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); Context.IsWorldReady = true; - SaveEvents.InvokeAfterLoad(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); + this.Events.Save_AfterLoad.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } } @@ -341,7 +353,7 @@ namespace StardewModdingAPI.Framework this.IsExitingToTitle = false; this.CleanupAfterReturnToTitle(); - SaveEvents.InvokeAfterReturnToTitle(this.Monitor); + this.Events.Save_AfterReturnToTitle.Raise(); } /********* @@ -354,7 +366,7 @@ namespace StardewModdingAPI.Framework if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) { Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); - GraphicsEvents.InvokeResize(this.Monitor); + this.Events.Graphics_Resize.Raise(); this.PreviousWindowSize = size; } @@ -394,47 +406,47 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - ControlEvents.InvokeKeyPressed(this.Monitor, key); + this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right); + this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); else - ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton); + this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - ControlEvents.InvokeKeyReleased(this.Monitor, key); + this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right); + this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); else - ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton); + this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } } } // raise legacy state-changed events if (inputState.KeyboardState != this.PreviousInput.KeyboardState) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousInput.KeyboardState, inputState.KeyboardState); + this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); if (inputState.MouseState != this.PreviousInput.MouseState) - ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition); + this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); // track state this.PreviousInput = inputState; @@ -461,9 +473,9 @@ namespace StardewModdingAPI.Framework // raise menu events if (newMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); + this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu)); else - MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); + this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(previousMenu)); // update previous menu // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) @@ -480,46 +492,46 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(this.PreviousGameLocation, Game1.currentLocation)); } // raise location list changed if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { // raise player leveled up a skill if (Game1.player.combatLevel != this.PreviousCombatLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel)); if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel)); if (Game1.player.fishingLevel != this.PreviousFishingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel)); if (Game1.player.foragingLevel != this.PreviousForagingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel)); if (Game1.player.miningLevel != this.PreviousMiningLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel)); if (Game1.player.luckLevel != this.PreviousLuckLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel)); // raise player inventory changed ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); if (changedItems.Any()) - PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems); + this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.items, changedItems.ToList())); // raise current location's object list changed if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(Game1.currentLocation.objects)); // raise time changed if (Game1.timeOfDay != this.PreviousTime) - TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.PreviousTime, Game1.timeOfDay)); // raise mine level changed if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); + this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(this.PreviousMineLevel, Game1.mine.mineLevel)); } // update state @@ -553,25 +565,25 @@ namespace StardewModdingAPI.Framework /********* ** Update events *********/ - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); if (this.FirstUpdate) { this.FirstUpdate = false; - GameEvents.InvokeFirstUpdateTick(this.Monitor); + this.Events.Game_FirstUpdateTick.Raise(); } - GameEvents.InvokeUpdateTick(this.Monitor); + this.Events.Game_UpdateTick.Raise(); if (this.CurrentUpdateTick % 2 == 0) - GameEvents.InvokeSecondUpdateTick(this.Monitor); + this.Events.Game_SecondUpdateTick.Raise(); if (this.CurrentUpdateTick % 4 == 0) - GameEvents.InvokeFourthUpdateTick(this.Monitor); + this.Events.Game_FourthUpdateTick.Raise(); if (this.CurrentUpdateTick % 8 == 0) - GameEvents.InvokeEighthUpdateTick(this.Monitor); + this.Events.Game_EighthUpdateTick.Raise(); if (this.CurrentUpdateTick % 15 == 0) - GameEvents.InvokeQuarterSecondTick(this.Monitor); + this.Events.Game_QuarterSecondTick.Raise(); if (this.CurrentUpdateTick % 30 == 0) - GameEvents.InvokeHalfSecondTick(this.Monitor); + this.Events.Game_HalfSecondTick.Raise(); if (this.CurrentUpdateTick % 60 == 0) - GameEvents.InvokeOneSecondTick(this.Monitor); + this.Events.Game_OneSecondTick.Raise(); this.CurrentUpdateTick += 1; if (this.CurrentUpdateTick >= 60) this.CurrentUpdateTick = 0; @@ -680,9 +692,9 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -704,9 +716,9 @@ namespace StardewModdingAPI.Framework try { Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -771,9 +783,9 @@ namespace StardewModdingAPI.Framework { try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -858,7 +870,7 @@ namespace StardewModdingAPI.Framework Game1.bloom.BeginDraw(); this.GraphicsDevice.Clear(this.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); + this.Events.Graphics_OnPreRenderEvent.Raise(); if (Game1.background != null) Game1.background.draw(Game1.spriteBatch); Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); @@ -1129,9 +1141,9 @@ namespace StardewModdingAPI.Framework this.drawBillboard(); if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) { - GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); + this.Events.Graphics_OnPreRenderHudEvent.Raise(); this.drawHUD(); - GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); + this.Events.Graphics_OnPostRenderHudEvent.Raise(); } else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); @@ -1263,9 +1275,9 @@ namespace StardewModdingAPI.Framework { try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -1350,11 +1362,11 @@ namespace StardewModdingAPI.Framework /// Whether to create a new sprite batch. private void RaisePostRender(bool needsNewBatch = false) { - if (GraphicsEvents.HasPostRenderListeners()) + if (this.Events.Graphics_OnPostRenderEvent.HasListeners()) { if (needsNewBatch) Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); + this.Events.Graphics_OnPostRenderEvent.Raise(); if (needsNewBatch) Game1.spriteBatch.End(); } -- cgit