add GameLoop events for SMAPI 3.0 (#310)

This commit is contained in:
Jesse Plamondon-Willard 2018-07-08 20:06:33 -04:00
parent 7e46cc2463
commit 3b078d55da
10 changed files with 186 additions and 23 deletions

View File

@ -0,0 +1,7 @@
using System;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IGameLoopEvents.Launched"/> event.</summary>
public class GameLoopLaunchedEventArgs : EventArgs { }
}

View File

@ -0,0 +1,36 @@
using System;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IGameLoopEvents.Updated"/> event.</summary>
public class GameLoopUpdatedEventArgs : EventArgs
{
/*********
** Accessors
*********/
/// <summary>The number of ticks elapsed since the game started, including the current tick.</summary>
public uint Ticks { get; }
/// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
public bool IsOneSecond { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="ticks">The number of ticks elapsed since the game started, including the current tick.</param>
public GameLoopUpdatedEventArgs(uint ticks)
{
this.Ticks = ticks;
this.IsOneSecond = this.IsMultipleOf(60);
}
/// <summary>Get whether <see cref="Ticks"/> is a multiple of the given <paramref name="number"/>. This is mainly useful if you want to run logic intermittently (e.g. <code>e.IsMultipleOf(30)</code> for every half-second).</summary>
/// <param name="number">The factor to check.</param>
public bool IsMultipleOf(uint number)
{
return this.Ticks % number == 0;
}
}
}

View File

@ -0,0 +1,36 @@
using System;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IGameLoopEvents.Updating"/> event.</summary>
public class GameLoopUpdatingEventArgs : EventArgs
{
/*********
** Accessors
*********/
/// <summary>The number of ticks elapsed since the game started, including the current tick.</summary>
public uint Ticks { get; }
/// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
public bool IsOneSecond { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="ticks">The number of ticks elapsed since the game started, including the current tick.</param>
public GameLoopUpdatingEventArgs(uint ticks)
{
this.Ticks = ticks;
this.IsOneSecond = this.IsMultipleOf(60);
}
/// <summary>Get whether <see cref="Ticks"/> is a multiple of the given <paramref name="number"/>. This is mainly useful if you want to run logic intermittently (e.g. <code>e.IsMultipleOf(30)</code> for every half-second).</summary>
/// <param name="number">The factor to check.</param>
public bool IsMultipleOf(uint number)
{
return this.Ticks % number == 0;
}
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace StardewModdingAPI.Events
{
/// <summary>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 <see cref="IInputEvents"/> if possible.</summary>
public interface IGameLoopEvents
{
/// <summary>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.</summary>
event EventHandler<GameLoopLaunchedEventArgs> Launched;
/// <summary>Raised before the game performs its overall update tick (≈60 times per second).</summary>
event EventHandler<GameLoopUpdatingEventArgs> Updating;
/// <summary>Raised after the game performs its overall update tick (≈60 times per second).</summary>
event EventHandler<GameLoopUpdatedEventArgs> Updated;
}
}

View File

@ -3,6 +3,9 @@ namespace StardewModdingAPI.Events
/// <summary>Manages access to events raised by SMAPI.</summary> /// <summary>Manages access to events raised by SMAPI.</summary>
public interface IModEvents public interface IModEvents
{ {
/// <summary>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 <see cref="Input"/> if possible.</summary>
IGameLoopEvents GameLoop { get; }
/// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary> /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
IInputEvents Input { get; } IInputEvents Input { get; }

View File

@ -11,6 +11,33 @@ namespace StardewModdingAPI.Framework.Events
/********* /*********
** Events (new) ** Events (new)
*********/ *********/
/****
** Game loop
****/
/// <summary>Raised after the game is launched, right before the first update tick.</summary>
public readonly ManagedEvent<GameLoopLaunchedEventArgs> GameLoop_Launched;
/// <summary>Raised before the game performs its overall update tick (≈60 times per second).</summary>
public readonly ManagedEvent<GameLoopUpdatingEventArgs> GameLoop_Updating;
/// <summary>Raised after the game performs its overall update tick (≈60 times per second).</summary>
public readonly ManagedEvent<GameLoopUpdatedEventArgs> GameLoop_Updated;
/****
** Input
****/
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
public readonly ManagedEvent<InputButtonPressedArgsInput> Input_ButtonPressed;
/// <summary>Raised after the player released a button on the keyboard, controller, or mouse.</summary>
public readonly ManagedEvent<InputButtonReleasedArgsInput> Input_ButtonReleased;
/// <summary>Raised after the player moves the in-game cursor.</summary>
public readonly ManagedEvent<InputCursorMovedArgsInput> Input_CursorMoved;
/// <summary>Raised after the player scrolls the mouse wheel.</summary>
public readonly ManagedEvent<InputMouseWheelScrolledEventArgs> Input_MouseWheelScrolled;
/**** /****
** World ** World
****/ ****/
@ -35,21 +62,6 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary> /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
public readonly ManagedEvent<WorldTerrainFeatureListChangedEventArgs> World_TerrainFeatureListChanged; public readonly ManagedEvent<WorldTerrainFeatureListChangedEventArgs> World_TerrainFeatureListChanged;
/****
** Input
****/
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
public readonly ManagedEvent<InputButtonPressedArgsInput> Input_ButtonPressed;
/// <summary>Raised after the player released a button on the keyboard, controller, or mouse.</summary>
public readonly ManagedEvent<InputButtonReleasedArgsInput> Input_ButtonReleased;
/// <summary>Raised after the player moves the in-game cursor.</summary>
public readonly ManagedEvent<InputCursorMovedArgsInput> Input_CursorMoved;
/// <summary>Raised after the player scrolls the mouse wheel.</summary>
public readonly ManagedEvent<InputMouseWheelScrolledEventArgs> Input_MouseWheelScrolled;
/********* /*********
** Events (old) ** Events (old)
@ -252,6 +264,9 @@ namespace StardewModdingAPI.Framework.Events
ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry);
// init events (new) // init events (new)
this.GameLoop_Updating = ManageEventOf<GameLoopUpdatingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating));
this.GameLoop_Updated = ManageEventOf<GameLoopUpdatedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated));
this.Input_ButtonPressed = ManageEventOf<InputButtonPressedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonPressed = ManageEventOf<InputButtonPressedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed));
this.Input_ButtonReleased = ManageEventOf<InputButtonReleasedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.Input_ButtonReleased = ManageEventOf<InputButtonReleasedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased));
this.Input_CursorMoved = ManageEventOf<InputCursorMovedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.Input_CursorMoved = ManageEventOf<InputCursorMovedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved));

View File

@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework.Events
/********* /*********
** Accessors ** Accessors
*********/ *********/
/// <summary>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 <see cref="IModEvents.Input"/> if possible.</summary>
public IGameLoopEvents GameLoop { get; }
/// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary> /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
public IInputEvents Input { get; } public IInputEvents Input { get; }
@ -23,6 +26,7 @@ namespace StardewModdingAPI.Framework.Events
/// <param name="eventManager">The underlying event manager.</param> /// <param name="eventManager">The underlying event manager.</param>
public ModEvents(IModMetadata mod, EventManager eventManager) public ModEvents(IModMetadata mod, EventManager eventManager)
{ {
this.GameLoop = new ModGameLoopEvents(mod, eventManager);
this.Input = new ModInputEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager);
this.World = new ModWorldEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager);
} }

View File

@ -0,0 +1,39 @@
using System;
using StardewModdingAPI.Events;
namespace StardewModdingAPI.Framework.Events
{
/// <summary>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 <see cref="IInputEvents"/> if possible.</summary>
internal class ModGameLoopEvents : ModEventsBase, IGameLoopEvents
{
/*********
** Accessors
*********/
/// <summary>Raised after the game is launched, right before the first update tick.</summary>
public event EventHandler<GameLoopLaunchedEventArgs> Launched;
/// <summary>Raised before the game performs its overall update tick (≈60 times per second).</summary>
public event EventHandler<GameLoopUpdatingEventArgs> Updating
{
add => this.EventManager.GameLoop_Updating.Add(value);
remove => this.EventManager.GameLoop_Updating.Remove(value);
}
/// <summary>Raised after the game performs its overall update tick (≈60 times per second).</summary>
public event EventHandler<GameLoopUpdatedEventArgs> Updated
{
add => this.EventManager.GameLoop_Updated.Add(value);
remove => this.EventManager.GameLoop_Updated.Remove(value);
}
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod which uses this instance.</param>
/// <param name="eventManager">The underlying event manager.</param>
internal ModGameLoopEvents(IModMetadata mod, EventManager eventManager)
: base(mod, eventManager) { }
}
}

View File

@ -94,8 +94,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether post-game-startup initialisation has been performed.</summary> /// <summary>Whether post-game-startup initialisation has been performed.</summary>
private bool IsInitialised; private bool IsInitialised;
/// <summary>Whether this is the very first update tick since the game started.</summary> /// <summary>The number of update ticks which have already executed.</summary>
private bool FirstUpdate; private uint TicksElapsed = 0;
/// <summary>Whether the next content manager requested by the game will be for <see cref="Game1.content"/>.</summary> /// <summary>Whether the next content manager requested by the game will be for <see cref="Game1.content"/>.</summary>
private bool NextContentManagerIsMain; private bool NextContentManagerIsMain;
@ -138,7 +138,6 @@ namespace StardewModdingAPI.Framework
// init SMAPI // init SMAPI
this.Monitor = monitor; this.Monitor = monitor;
this.Events = eventManager; this.Events = eventManager;
this.FirstUpdate = true;
this.Reflection = reflection; this.Reflection = reflection;
this.OnGameInitialised = onGameInitialised; this.OnGameInitialised = onGameInitialised;
this.OnGameExiting = onGameExiting; this.OnGameExiting = onGameExiting;
@ -669,25 +668,27 @@ namespace StardewModdingAPI.Framework
/********* /*********
** Game update ** 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 try
{ {
this.Input.UpdateSuppression();
base.Update(gameTime); base.Update(gameTime);
} }
catch (Exception ex) catch (Exception ex)
{ {
this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); 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 ** Update events
*********/ *********/
this.Events.Specialised_UnvalidatedUpdateTick.Raise(); this.Events.Specialised_UnvalidatedUpdateTick.Raise();
if (this.FirstUpdate) if (this.TicksElapsed == 1)
{
this.FirstUpdate = false;
this.Events.Game_FirstUpdateTick.Raise(); this.Events.Game_FirstUpdateTick.Raise();
}
this.Events.Game_UpdateTick.Raise(); this.Events.Game_UpdateTick.Raise();
if (this.CurrentUpdateTick % 2 == 0) if (this.CurrentUpdateTick % 2 == 0)
this.Events.Game_SecondUpdateTick.Raise(); this.Events.Game_SecondUpdateTick.Raise();

View File

@ -90,15 +90,19 @@
<Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link> <Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile> </Compile>
<Compile Include="Events\GameLoopUpdatedEventArgs.cs" />
<Compile Include="Events\GameLoopLaunchedEventArgs.cs" />
<Compile Include="Events\InputMouseWheelScrolledEventArgs.cs" /> <Compile Include="Events\InputMouseWheelScrolledEventArgs.cs" />
<Compile Include="Events\InputCursorMovedEventArgs.cs" /> <Compile Include="Events\InputCursorMovedEventArgs.cs" />
<Compile Include="Events\InputButtonReleasedEventArgs.cs" /> <Compile Include="Events\InputButtonReleasedEventArgs.cs" />
<Compile Include="Events\InputButtonPressedEventArgs.cs" /> <Compile Include="Events\InputButtonPressedEventArgs.cs" />
<Compile Include="Events\EventArgsLocationBuildingsChanged.cs" /> <Compile Include="Events\EventArgsLocationBuildingsChanged.cs" />
<Compile Include="Events\IInputEvents.cs" /> <Compile Include="Events\IInputEvents.cs" />
<Compile Include="Events\IGameLoopEvents.cs" />
<Compile Include="Events\IWorldEvents.cs" /> <Compile Include="Events\IWorldEvents.cs" />
<Compile Include="Events\MultiplayerEvents.cs" /> <Compile Include="Events\MultiplayerEvents.cs" />
<Compile Include="Events\WorldDebrisListChangedEventArgs.cs" /> <Compile Include="Events\WorldDebrisListChangedEventArgs.cs" />
<Compile Include="Events\GameLoopUpdatingEventArgs.cs" />
<Compile Include="Events\WorldNpcListChangedEventArgs.cs" /> <Compile Include="Events\WorldNpcListChangedEventArgs.cs" />
<Compile Include="Events\WorldLargeTerrainFeatureListChangedEventArgs.cs" /> <Compile Include="Events\WorldLargeTerrainFeatureListChangedEventArgs.cs" />
<Compile Include="Events\WorldTerrainFeatureListChangedEventArgs.cs" /> <Compile Include="Events\WorldTerrainFeatureListChangedEventArgs.cs" />
@ -125,6 +129,7 @@
<Compile Include="Framework\Content\ContentCache.cs" /> <Compile Include="Framework\Content\ContentCache.cs" />
<Compile Include="Framework\Events\ManagedEventBase.cs" /> <Compile Include="Framework\Events\ManagedEventBase.cs" />
<Compile Include="Framework\Events\ModEvents.cs" /> <Compile Include="Framework\Events\ModEvents.cs" />
<Compile Include="Framework\Events\ModGameLoopEvents.cs" />
<Compile Include="Framework\Events\ModInputEvents.cs" /> <Compile Include="Framework\Events\ModInputEvents.cs" />
<Compile Include="Framework\Input\GamePadStateBuilder.cs" /> <Compile Include="Framework\Input\GamePadStateBuilder.cs" />
<Compile Include="Framework\ModHelpers\InputHelper.cs" /> <Compile Include="Framework\ModHelpers\InputHelper.cs" />