From 3b078d55daccd13332e2cba1fd5c76f775505d7a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 20:06:33 -0400 Subject: [PATCH] add GameLoop events for SMAPI 3.0 (#310) --- src/SMAPI/Events/GameLoopLaunchedEventArgs.cs | 7 +++ src/SMAPI/Events/GameLoopUpdatedEventArgs.cs | 36 +++++++++++++++ src/SMAPI/Events/GameLoopUpdatingEventArgs.cs | 36 +++++++++++++++ src/SMAPI/Events/IGameLoopEvents.cs | 17 +++++++ src/SMAPI/Events/IModEvents.cs | 3 ++ src/SMAPI/Framework/Events/EventManager.cs | 45 ++++++++++++------- src/SMAPI/Framework/Events/ModEvents.cs | 4 ++ .../Framework/Events/ModGameLoopEvents.cs | 39 ++++++++++++++++ src/SMAPI/Framework/SGame.cs | 17 +++---- src/SMAPI/StardewModdingAPI.csproj | 5 +++ 10 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/SMAPI/Events/GameLoopLaunchedEventArgs.cs create mode 100644 src/SMAPI/Events/GameLoopUpdatedEventArgs.cs create mode 100644 src/SMAPI/Events/GameLoopUpdatingEventArgs.cs create mode 100644 src/SMAPI/Events/IGameLoopEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModGameLoopEvents.cs diff --git a/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs b/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs new file mode 100644 index 00000000..6a42e4f9 --- /dev/null +++ b/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopLaunchedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs new file mode 100644 index 00000000..3ad34b69 --- /dev/null +++ b/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopUpdatedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public GameLoopUpdatedEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs new file mode 100644 index 00000000..d6a8b5c2 --- /dev/null +++ b/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopUpdatingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public GameLoopUpdatingEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs new file mode 100644 index 00000000..a56b3de3 --- /dev/null +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public interface IGameLoopEvents + { + /// Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialised at this point, so this is a good time to set up mod integrations. + event EventHandler Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + event EventHandler Updating; + + /// Raised after the game performs its overall update tick (≈60 times per second). + event EventHandler Updated; + } +} diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 16ec6557..cf2f8cb8 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,6 +3,9 @@ namespace StardewModdingAPI.Events /// Manages access to events raised by SMAPI. public interface IModEvents { + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + IGameLoopEvents GameLoop { get; } + /// Events raised when the player provides input using a controller, keyboard, or mouse. IInputEvents Input { get; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index b05d82ce..3d5d0124 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -11,6 +11,33 @@ namespace StardewModdingAPI.Framework.Events /********* ** Events (new) *********/ + /**** + ** Game loop + ****/ + /// Raised after the game is launched, right before the first update tick. + public readonly ManagedEvent GameLoop_Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent GameLoop_Updating; + + /// Raised after the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent GameLoop_Updated; + + /**** + ** Input + ****/ + /// Raised after the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonPressed; + + /// Raised after the player released a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonReleased; + + /// Raised after the player moves the in-game cursor. + public readonly ManagedEvent Input_CursorMoved; + + /// Raised after the player scrolls the mouse wheel. + public readonly ManagedEvent Input_MouseWheelScrolled; + /**** ** World ****/ @@ -35,21 +62,6 @@ namespace StardewModdingAPI.Framework.Events /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent World_TerrainFeatureListChanged; - /**** - ** Input - ****/ - /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; - - /// Raised after the player released a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; - - /// Raised after the player moves the in-game cursor. - public readonly ManagedEvent Input_CursorMoved; - - /// Raised after the player scrolls the mouse wheel. - public readonly ManagedEvent Input_MouseWheelScrolled; - /********* ** Events (old) @@ -252,6 +264,9 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.GameLoop_Updating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating)); + this.GameLoop_Updated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated)); + this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 90853141..9e474457 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public IGameLoopEvents GameLoop { get; } + /// Events raised when the player provides input using a controller, keyboard, or mouse. public IInputEvents Input { get; } @@ -23,6 +26,7 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event manager. public ModEvents(IModMetadata mod, EventManager eventManager) { + this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs new file mode 100644 index 00000000..1a142b0f --- /dev/null +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -0,0 +1,39 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + internal class ModGameLoopEvents : ModEventsBase, IGameLoopEvents + { + /********* + ** Accessors + *********/ + /// Raised after the game is launched, right before the first update tick. + public event EventHandler Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + public event EventHandler Updating + { + add => this.EventManager.GameLoop_Updating.Add(value); + remove => this.EventManager.GameLoop_Updating.Remove(value); + } + + /// Raised after the game performs its overall update tick (≈60 times per second). + public event EventHandler Updated + { + add => this.EventManager.GameLoop_Updated.Add(value); + remove => this.EventManager.GameLoop_Updated.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModGameLoopEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d3865316..777bc478 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -94,8 +94,8 @@ namespace StardewModdingAPI.Framework /// Whether post-game-startup initialisation has been performed. private bool IsInitialised; - /// Whether this is the very first update tick since the game started. - private bool FirstUpdate; + /// The number of update ticks which have already executed. + private uint TicksElapsed = 0; /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; @@ -138,7 +138,6 @@ namespace StardewModdingAPI.Framework // init SMAPI this.Monitor = monitor; this.Events = eventManager; - this.FirstUpdate = true; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; @@ -669,25 +668,27 @@ namespace StardewModdingAPI.Framework /********* ** Game update *********/ - this.Input.UpdateSuppression(); + this.TicksElapsed++; + if (this.TicksElapsed == 1) + this.Events.GameLoop_Launched.Raise(new GameLoopLaunchedEventArgs()); + this.Events.GameLoop_Updating.Raise(new GameLoopUpdatingEventArgs(this.TicksElapsed)); try { + this.Input.UpdateSuppression(); base.Update(gameTime); } catch (Exception ex) { this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } + this.Events.GameLoop_Updated.Raise(new GameLoopUpdatedEventArgs(this.TicksElapsed)); /********* ** Update events *********/ this.Events.Specialised_UnvalidatedUpdateTick.Raise(); - if (this.FirstUpdate) - { - this.FirstUpdate = false; + if (this.TicksElapsed == 1) this.Events.Game_FirstUpdateTick.Raise(); - } this.Events.Game_UpdateTick.Raise(); if (this.CurrentUpdateTick % 2 == 0) this.Events.Game_SecondUpdateTick.Raise(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 57c2c9e8..27e98f3a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -90,15 +90,19 @@ Properties\GlobalAssemblyInfo.cs + + + + @@ -125,6 +129,7 @@ +