From 270d436a176904ab39fc0ce97da2027dd6ac1114 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Dec 2018 20:15:39 -0500 Subject: remove shell code in Windows installer to reduce antivirus false positives --- src/SMAPI.Installer/InteractiveInstaller.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index d5866c74..95aed4ca 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -13,6 +12,9 @@ using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Utilities; +#if !SMAPI_FOR_WINDOWS +using System.Diagnostics; +#endif namespace StardewModdingApi.Installer { @@ -461,6 +463,8 @@ namespace StardewModdingApi.Installer // mark file executable // (MSBuild doesn't keep permission flags for files zipped in a build task.) + // (Note: exclude from Windows build because antivirus apps can flag the process start code as suspicious.) +#if !SMAPI_FOR_WINDOWS new Process { StartInfo = new ProcessStartInfo @@ -470,6 +474,7 @@ namespace StardewModdingApi.Installer CreateNoWindow = true } }.Start(); +#endif } // create mods directory (if needed) -- cgit From 7294cb3cc5aeed2849827b192c54db2059fe6a5f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 22 Dec 2018 16:08:52 -0500 Subject: add world_clear console command --- .../Framework/Commands/World/ClearCommand.cs | 246 +++++++++++++++++++++ .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 1 + 2 files changed, 247 insertions(+) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs new file mode 100644 index 00000000..9b5f07de --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Locations; +using StardewValley.Objects; +using StardewValley.TerrainFeatures; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which clears in-game objects. + internal class ClearCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// The valid types that can be cleared. + private readonly string[] ValidTypes = { "debris", "fruit-trees", "grass", "trees", "everything" }; + + /// The resource clump IDs to consider debris. + private readonly int[] DebrisClumps = { ResourceClump.stumpIndex, ResourceClump.hollowLogIndex, ResourceClump.meteoriteIndex, ResourceClump.boulderIndex }; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ClearCommand() + : base( + name: "world_clear", + description: "Clears in-game entities in a given location.\n\n" + + "Usage: world_clear \n" + + "- location: the location name for which to clear objects (like Farm), or 'current' for the current location.\n" + + " - object type: the type of object clear. You can specify 'debris' (stones/twigs/weeds and dead crops), 'grass', and 'trees' / 'fruit-trees'. You can also specify 'everything', which includes things not removed by the other types (like furniture or resource clumps)." + ) + { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // check context + if (!Context.IsWorldReady) + { + monitor.Log("You need to load a save to use this command.", LogLevel.Error); + return; + } + + // parse arguments + if (!args.TryGet(0, "location", out string locationName, required: true)) + return; + if (!args.TryGet(1, "object type", out string type, required: true, oneOf: this.ValidTypes)) + return; + + // get target location + GameLocation location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.InvariantCultureIgnoreCase)); + if (location == null && locationName == "current") + location = Game1.currentLocation; + if (location == null) + { + string[] locationNames = (from loc in Game1.locations where !string.IsNullOrWhiteSpace(loc.Name) orderby loc.Name select loc.Name).ToArray(); + monitor.Log($"Could not find a location with that name. Must be one of [{string.Join(", ", locationNames)}].", LogLevel.Error); + return; + } + + // apply + switch (type) + { + case "debris": + { + int removed = 0; + foreach (var pair in location.terrainFeatures.Pairs.ToArray()) + { + TerrainFeature feature = pair.Value; + if (feature is HoeDirt dirt && dirt.crop?.dead == true) + { + dirt.crop = null; + removed++; + } + } + + removed += + this.RemoveObjects(location, obj => obj.Name.ToLower().Contains("weed") || obj.Name == "Twig" || obj.Name == "Stone") + + this.RemoveResourceClumps(location, clump => this.DebrisClumps.Contains(clump.parentSheetIndex.Value)); + + monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); + break; + } + + case "fruit-trees": + { + int removed = this.RemoveTerrainFeatures(location, feature => feature is FruitTree); + monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); + break; + } + + case "grass": + { + int removed = this.RemoveTerrainFeatures(location, feature => feature is Grass); + monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); + break; + } + + case "trees": + { + int removed = this.RemoveTerrainFeatures(location, feature => feature is Tree); + monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); + break; + } + + case "everything": + { + int removed = + this.RemoveFurniture(location, p => true) + + this.RemoveObjects(location, p => true) + + this.RemoveTerrainFeatures(location, p => true) + + this.RemoveLargeTerrainFeatures(location, p => true) + + this.RemoveResourceClumps(location, p => true); + monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); + break; + } + + default: + monitor.Log($"Unknown type '{type}'. Must be one [{string.Join(", ", this.ValidTypes)}].", LogLevel.Error); + break; + } + } + + + /********* + ** Private methods + *********/ + /// Remove objects from a location matching a lambda. + /// The location to search. + /// Whether an entity should be removed. + /// Returns the number of removed entities. + private int RemoveObjects(GameLocation location, Func shouldRemove) + { + int removed = 0; + + foreach (var pair in location.Objects.Pairs.ToArray()) + { + if (shouldRemove(pair.Value)) + { + location.Objects.Remove(pair.Key); + removed++; + } + } + + return removed; + } + + /// Remove terrain features from a location matching a lambda. + /// The location to search. + /// Whether an entity should be removed. + /// Returns the number of removed entities. + private int RemoveTerrainFeatures(GameLocation location, Func shouldRemove) + { + int removed = 0; + + foreach (var pair in location.terrainFeatures.Pairs.ToArray()) + { + if (shouldRemove(pair.Value)) + { + location.terrainFeatures.Remove(pair.Key); + removed++; + } + } + + return removed; + } + + /// Remove large terrain features from a location matching a lambda. + /// The location to search. + /// Whether an entity should be removed. + /// Returns the number of removed entities. + private int RemoveLargeTerrainFeatures(GameLocation location, Func shouldRemove) + { + int removed = 0; + + foreach (LargeTerrainFeature feature in location.largeTerrainFeatures.ToArray()) + { + if (shouldRemove(feature)) + { + location.largeTerrainFeatures.Remove(feature); + removed++; + } + } + + return removed; + } + + /// Remove resource clumps from a location matching a lambda. + /// The location to search. + /// Whether an entity should be removed. + /// Returns the number of removed entities. + private int RemoveResourceClumps(GameLocation location, Func shouldRemove) + { + int removed = 0; + + // get resource clumps + IList resourceClumps = + (location as Farm)?.resourceClumps + ?? (IList)(location as Woods)?.stumps + ?? new List(); + + // remove matching clumps + foreach (var clump in resourceClumps.ToArray()) + { + if (shouldRemove(clump)) + { + resourceClumps.Remove(clump); + removed++; + } + } + + return removed; + } + + /// Remove furniture from a location matching a lambda. + /// The location to search. + /// Whether an entity should be removed. + /// Returns the number of removed entities. + private int RemoveFurniture(GameLocation location, Func shouldRemove) + { + int removed = 0; + + if (location is DecoratableLocation decoratableLocation) + { + foreach (Furniture furniture in decoratableLocation.furniture.ToArray()) + { + if (shouldRemove(furniture)) + { + decoratableLocation.furniture.Remove(furniture); + removed++; + } + } + } + + return removed; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index d1f16e41..a3237a3d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -62,6 +62,7 @@ + -- cgit From 4b325f61b370b24403fa10616178dceefa773420 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Dec 2018 16:51:38 -0500 Subject: allow Read/WriteSaveFile as soon as the save is loaded --- src/SMAPI/Framework/ModHelpers/DataHelper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index e5100aed..242b8ab1 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -77,9 +77,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The player hasn't loaded a save file yet or isn't the main player. public TModel ReadSaveData(string key) where TModel : class { - if (!Context.IsSaveLoaded) + if (!Game1.hasLoadedGame) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded."); - if (!Context.IsMainPlayer) + if (!Game1.IsMasterGame) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); return Game1.CustomData.TryGetValue(this.GetSaveFileKey(key), out string value) @@ -94,9 +94,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The player hasn't loaded a save file yet or isn't the main player. public void WriteSaveData(string key, TModel data) where TModel : class { - if (!Context.IsSaveLoaded) + if (!Game1.hasLoadedGame) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded."); - if (!Context.IsMainPlayer) + if (!Game1.IsMasterGame) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); string internalKey = this.GetSaveFileKey(key); -- cgit From 041bd2d6ba726eeea88afed3be307343a6f9286b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Dec 2018 19:26:02 -0500 Subject: add Specialised.SavePreloaded event --- src/SMAPI/Events/IGameLoopEvents.cs | 2 +- src/SMAPI/Events/ISpecialisedEvents.cs | 3 +++ src/SMAPI/Events/SavePreloadedEventArgs.cs | 7 +++++++ src/SMAPI/Framework/Events/EventManager.cs | 6 +++++- src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 2 +- src/SMAPI/Framework/Events/ModSpecialisedEvents.cs | 7 +++++++ src/SMAPI/Framework/SGame.cs | 23 ++++++++++++++++++---- src/SMAPI/StardewModdingAPI.csproj | 1 + 8 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 src/SMAPI/Events/SavePreloadedEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index e1900f79..ea79aa74 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Events /// Raised after the game finishes writing data to the save file (except the initial save creation). event EventHandler Saved; - /// Raised after the player loads a save slot. + /// Raised after the player loads a save slot and the world is initialised. event EventHandler SaveLoaded; /// Raised after the game begins a new day (including when the player loads a save). diff --git a/src/SMAPI/Events/ISpecialisedEvents.cs b/src/SMAPI/Events/ISpecialisedEvents.cs index 928cd05d..2a19113c 100644 --- a/src/SMAPI/Events/ISpecialisedEvents.cs +++ b/src/SMAPI/Events/ISpecialisedEvents.cs @@ -5,6 +5,9 @@ namespace StardewModdingAPI.Events /// Events serving specialised edge cases that shouldn't be used by most mods. public interface ISpecialisedEvents { + /// Raised immediately after the player loads a save slot, but before the world is fully initialised. The save and game data are available at this point, but some in-game content (like location maps) haven't been initialised yet. + event EventHandler SavePreloaded; + /// Raised before the game state is updated (≈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 event will trigger a stability warning in the SMAPI console. event EventHandler UnvalidatedUpdateTicking; diff --git a/src/SMAPI/Events/SavePreloadedEventArgs.cs b/src/SMAPI/Events/SavePreloadedEventArgs.cs new file mode 100644 index 00000000..03990f5a --- /dev/null +++ b/src/SMAPI/Events/SavePreloadedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SavePreloadedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 0ad85adf..bd862046 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -70,7 +70,7 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game finishes writing data to the save file (except the initial save creation). public readonly ManagedEvent Saved; - /// Raised after the player loads a save slot. + /// Raised after the player loads a save slot and the world is initialised. public readonly ManagedEvent SaveLoaded; /// Raised after the game begins a new day, including when loading a save. @@ -151,6 +151,9 @@ namespace StardewModdingAPI.Framework.Events /**** ** Specialised ****/ + /// Raised immediately after the player loads a save slot, but before the world is fully initialised. + public readonly ManagedEvent SavePreloaded; + /// Raised before the game performs its overall update tick (≈60 times per second). See notes on . public readonly ManagedEvent UnvalidatedUpdateTicking; @@ -408,6 +411,7 @@ namespace StardewModdingAPI.Framework.Events this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); + this.SavePreloaded = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.SavePreloaded)); this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index a5beac99..3a764ab0 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.Saved.Remove(value); } - /// Raised after the player loads a save slot. + /// Raised after the player loads a save slot and the world is initialised. public event EventHandler SaveLoaded { add => this.EventManager.SaveLoaded.Add(value); diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs index 17c32bb8..83e349cf 100644 --- a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs +++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs @@ -9,6 +9,13 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Raised immediately after the player loads a save slot, but before the world is fully initialised. The save and game data are available at this point, but some in-game content (like location maps) haven't been initialised yet. + public event EventHandler SavePreloaded + { + add => this.EventManager.SavePreloaded.Add(value); + remove => this.EventManager.SavePreloaded.Remove(value); + } + /// Raised before the game state is updated (≈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 event will trigger a stability warning in the SMAPI console. public event EventHandler UnvalidatedUpdateTicking { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d515d3ad..befd9cef 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -69,8 +69,11 @@ namespace StardewModdingAPI.Framework /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private readonly Countdown AfterLoadTimer = new Countdown(5); + /// Whether was raised for this session. + private bool RaisedPreloadedEvent; + /// Whether the after-load events were raised for this session. - private bool RaisedAfterLoadEvent; + private bool RaisedLoadedEvent; /// Whether the game is saving and SMAPI has already raised . private bool IsBetweenSaveEvents; @@ -217,6 +220,7 @@ namespace StardewModdingAPI.Framework private void OnReturnedToTitle() { this.Monitor.Log("Context: returned to title", LogLevel.Trace); + this.RaisedPreloadedEvent = false; this.Multiplayer.CleanupOnMultiplayerExit(); this.Events.ReturnedToTitle.RaiseEmpty(); #if !SMAPI_3_0_STRICT @@ -466,7 +470,7 @@ namespace StardewModdingAPI.Framework *********/ if (wasWorldReady && !Context.IsWorldReady) this.OnReturnedToTitle(); - else if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) + else if (!this.RaisedLoadedEvent && Context.IsWorldReady) { // print context string context = $"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}."; @@ -480,7 +484,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log(context, LogLevel.Trace); // raise events - this.RaisedAfterLoadEvent = true; + this.RaisedLoadedEvent = true; this.Events.SaveLoaded.RaiseEmpty(); this.Events.DayStarted.RaiseEmpty(); #if !SMAPI_3_0_STRICT @@ -824,8 +828,19 @@ namespace StardewModdingAPI.Framework ** Game update *********/ this.TicksElapsed++; + + // game launched if (this.TicksElapsed == 1) this.Events.GameLaunched.Raise(new GameLaunchedEventArgs()); + + // preloaded + if (Context.IsSaveLoaded && !this.RaisedPreloadedEvent) + { + this.RaisedPreloadedEvent = true; + this.Events.SavePreloaded.RaiseEmpty(); + } + + // update tick this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); this.Events.UpdateTicking.Raise(new UpdateTickingEventArgs(this.TicksElapsed)); try @@ -1639,7 +1654,7 @@ namespace StardewModdingAPI.Framework { Context.IsWorldReady = false; this.AfterLoadTimer.Reset(); - this.RaisedAfterLoadEvent = false; + this.RaisedLoadedEvent = false; } #if !SMAPI_3_0_STRICT diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9b00e777..36fa7e0b 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -150,6 +150,7 @@ + -- cgit From 6ad52d607c49b16c6933060375086830edd9a1f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Dec 2018 17:28:58 -0500 Subject: add Specialised.LoadStageChanged event --- src/SMAPI/Enums/LoadStage.cs | 36 +++++++ src/SMAPI/Events/ISpecialisedEvents.cs | 4 +- src/SMAPI/Events/LoadStageChangedEventArgs.cs | 31 ++++++ src/SMAPI/Events/SavePreloadedEventArgs.cs | 7 -- src/SMAPI/Framework/Events/EventManager.cs | 6 +- src/SMAPI/Framework/Events/ModSpecialisedEvents.cs | 8 +- src/SMAPI/Framework/SCore.cs | 13 +-- src/SMAPI/Framework/SGame.cs | 89 +++++++++++------ src/SMAPI/Patches/LoadForNewGamePatch.cs | 109 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 4 +- 10 files changed, 254 insertions(+), 53 deletions(-) create mode 100644 src/SMAPI/Enums/LoadStage.cs create mode 100644 src/SMAPI/Events/LoadStageChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/SavePreloadedEventArgs.cs create mode 100644 src/SMAPI/Patches/LoadForNewGamePatch.cs (limited to 'src') diff --git a/src/SMAPI/Enums/LoadStage.cs b/src/SMAPI/Enums/LoadStage.cs new file mode 100644 index 00000000..6ff7de4f --- /dev/null +++ b/src/SMAPI/Enums/LoadStage.cs @@ -0,0 +1,36 @@ +namespace StardewModdingAPI.Enums +{ + /// A low-level stage in the game's loading process. + public enum LoadStage + { + /// A save is not loaded or loading. + None, + + /// The game is creating a new save slot, and has initialised the basic save info. + CreatedBasicInfo, + + /// The game is creating a new save slot, and has initialised the in-game locations. + CreatedLocations, + + /// The game is creating a new save slot, and has created the physical save files. + CreatedSaveFile, + + /// The game is loading a save slot, and has read the raw save data into . Not applicable when connecting to a multiplayer host. This is equivalent to value 20. + SaveParsed, + + /// The game is loading a save slot, and has applied the basic save info (including player data). Not applicable when connecting to a multiplayer host. Note that some basic info (like daily luck) is not initialised at this point. This is equivalent to value 36. + SaveLoadedBasicInfo, + + /// The game is loading a save slot, and has applied the in-game location data. Not applicable when connecting to a multiplayer host. This is equivalent to value 50. + SaveLoadedLocations, + + /// The final metadata has been loaded from the save file. This happens before the game applies problem fixes, checks for achievements, starts music, etc. Not applicable when connecting to a multiplayer host. + Preloaded, + + /// The save is fully loaded, but the world may not be fully initialised yet. + Loaded, + + /// The save is fully loaded, the world has been initialised, and is now true. + Ready + } +} diff --git a/src/SMAPI/Events/ISpecialisedEvents.cs b/src/SMAPI/Events/ISpecialisedEvents.cs index 2a19113c..ecb109e6 100644 --- a/src/SMAPI/Events/ISpecialisedEvents.cs +++ b/src/SMAPI/Events/ISpecialisedEvents.cs @@ -5,8 +5,8 @@ namespace StardewModdingAPI.Events /// Events serving specialised edge cases that shouldn't be used by most mods. public interface ISpecialisedEvents { - /// Raised immediately after the player loads a save slot, but before the world is fully initialised. The save and game data are available at this point, but some in-game content (like location maps) haven't been initialised yet. - event EventHandler SavePreloaded; + /// Raised when the low-level stage in the game's loading process has changed. This is an advanced event for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use instead. + event EventHandler LoadStageChanged; /// Raised before the game state is updated (≈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 event will trigger a stability warning in the SMAPI console. event EventHandler UnvalidatedUpdateTicking; diff --git a/src/SMAPI/Events/LoadStageChangedEventArgs.cs b/src/SMAPI/Events/LoadStageChangedEventArgs.cs new file mode 100644 index 00000000..e837a5f1 --- /dev/null +++ b/src/SMAPI/Events/LoadStageChangedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using StardewModdingAPI.Enums; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class LoadStageChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous load stage. + public LoadStage OldStage { get; } + + /// The new load stage. + public LoadStage NewStage { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous load stage. + /// The new load stage. + public LoadStageChangedEventArgs(LoadStage old, LoadStage current) + { + this.OldStage = old; + this.NewStage = current; + } + } +} diff --git a/src/SMAPI/Events/SavePreloadedEventArgs.cs b/src/SMAPI/Events/SavePreloadedEventArgs.cs deleted file mode 100644 index 03990f5a..00000000 --- a/src/SMAPI/Events/SavePreloadedEventArgs.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an event. - public class SavePreloadedEventArgs : EventArgs { } -} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index bd862046..b7f00f52 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -151,8 +151,8 @@ namespace StardewModdingAPI.Framework.Events /**** ** Specialised ****/ - /// Raised immediately after the player loads a save slot, but before the world is fully initialised. - public readonly ManagedEvent SavePreloaded; + /// Raised when the low-level stage in the game's loading process has changed. See notes on . + public readonly ManagedEvent LoadStageChanged; /// Raised before the game performs its overall update tick (≈60 times per second). See notes on . public readonly ManagedEvent UnvalidatedUpdateTicking; @@ -411,7 +411,7 @@ namespace StardewModdingAPI.Framework.Events this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); - this.SavePreloaded = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.SavePreloaded)); + this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged)); this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs index 83e349cf..7c3e9dee 100644 --- a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs +++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs @@ -9,11 +9,11 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ - /// Raised immediately after the player loads a save slot, but before the world is fully initialised. The save and game data are available at this point, but some in-game content (like location maps) haven't been initialised yet. - public event EventHandler SavePreloaded + /// Raised when the low-level stage in the game's loading process has changed. This is an advanced event for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use instead. + public event EventHandler LoadStageChanged { - add => this.EventManager.SavePreloaded.Add(value); - remove => this.EventManager.SavePreloaded.Remove(value); + add => this.EventManager.LoadStageChanged.Add(value); + remove => this.EventManager.LoadStageChanged.Remove(value); } /// Raised before the game state is updated (≈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 event will trigger a stability warning in the SMAPI console. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 679838ba..00801b72 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -181,12 +181,6 @@ namespace StardewModdingAPI.Framework return; } #endif - - // apply game patches - new GamePatcher(this.Monitor).Apply( - new DialogueErrorPatch(this.MonitorForGame, this.Reflection), - new ObjectErrorPatch() - ); } /// Launch SMAPI. @@ -237,6 +231,13 @@ namespace StardewModdingAPI.Framework this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, SCore.DeprecationManager, this.OnLocaleChanged, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; + // apply game patches + new GamePatcher(this.Monitor).Apply( + new DialogueErrorPatch(this.MonitorForGame, this.Reflection), + new ObjectErrorPatch(), + new LoadForNewGamePatch(this.Reflection, this.GameInstance.OnLoadStageChanged) + ); + // add exit handler new Thread(() => { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index befd9cef..cb62de2a 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -69,11 +69,8 @@ namespace StardewModdingAPI.Framework /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private readonly Countdown AfterLoadTimer = new Countdown(5); - /// Whether was raised for this session. - private bool RaisedPreloadedEvent; - - /// Whether the after-load events were raised for this session. - private bool RaisedLoadedEvent; + /// The current stage in the game's loading process. + private LoadStage LoadStage = LoadStage.None; /// Whether the game is saving and SMAPI has already raised . private bool IsBetweenSaveEvents; @@ -216,16 +213,33 @@ namespace StardewModdingAPI.Framework this.Events.ModMessageReceived.RaiseForMods(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); } - /// A callback raised when the player quits a save and returns to the title screen. - private void OnReturnedToTitle() + /// A callback invoked when the game's low-level load stage changes. + /// The new load stage. + internal void OnLoadStageChanged(LoadStage newStage) { - this.Monitor.Log("Context: returned to title", LogLevel.Trace); - this.RaisedPreloadedEvent = false; - this.Multiplayer.CleanupOnMultiplayerExit(); - this.Events.ReturnedToTitle.RaiseEmpty(); + // nothing to do + if (newStage == this.LoadStage) + return; + + // update data + LoadStage oldStage = this.LoadStage; + this.LoadStage = newStage; + if (newStage == LoadStage.None) + { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); + this.Multiplayer.CleanupOnMultiplayerExit(); + } + this.Monitor.VerboseLog($"Context: load stage changed to {newStage}"); + + // raise events + this.Events.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage)); + if (newStage == LoadStage.None) + { + this.Events.ReturnedToTitle.RaiseEmpty(); #if !SMAPI_3_0_STRICT - this.Events.Legacy_AfterReturnToTitle.Raise(); + this.Events.Legacy_AfterReturnToTitle.Raise(); #endif + } } /// Constructor a content manager to read XNB files. @@ -284,7 +298,29 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log("Game loader synchronising...", LogLevel.Trace); while (Game1.currentLoader?.MoveNext() == true) - ; + { + // raise load stage changed + switch (Game1.currentLoader.Current) + { + case 20: + this.OnLoadStageChanged(LoadStage.SaveParsed); + break; + + case 36: + this.OnLoadStageChanged(LoadStage.SaveLoadedBasicInfo); + break; + + case 50: + this.OnLoadStageChanged(LoadStage.SaveLoadedLocations); + break; + + default: + if (Game1.gameMode == Game1.playingGameMode) + this.OnLoadStageChanged(LoadStage.Preloaded); + break; + } + } + Game1.currentLoader = null; this.Monitor.Log("Game loader done.", LogLevel.Trace); } @@ -411,6 +447,7 @@ 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); + this.OnLoadStageChanged(LoadStage.CreatedSaveFile); this.Events.SaveCreated.RaiseEmpty(); #if !SMAPI_3_0_STRICT this.Events.Legacy_AfterCreateSave.Raise(); @@ -434,7 +471,10 @@ namespace StardewModdingAPI.Framework *********/ bool wasWorldReady = Context.IsWorldReady; if ((Context.IsWorldReady && !Context.IsSaveLoaded) || Game1.exitToTitle) - this.MarkWorldNotReady(); + { + Context.IsWorldReady = false; + this.AfterLoadTimer.Reset(); + } else if (Context.IsSaveLoaded && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) { if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) @@ -469,8 +509,8 @@ namespace StardewModdingAPI.Framework ** Load / return-to-title events *********/ if (wasWorldReady && !Context.IsWorldReady) - this.OnReturnedToTitle(); - else if (!this.RaisedLoadedEvent && Context.IsWorldReady) + this.OnLoadStageChanged(LoadStage.None); + else if (Context.IsWorldReady && this.LoadStage != LoadStage.Ready) { // print context string context = $"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}."; @@ -484,7 +524,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log(context, LogLevel.Trace); // raise events - this.RaisedLoadedEvent = true; + this.OnLoadStageChanged(LoadStage.Ready); this.Events.SaveLoaded.RaiseEmpty(); this.Events.DayStarted.RaiseEmpty(); #if !SMAPI_3_0_STRICT @@ -834,11 +874,8 @@ namespace StardewModdingAPI.Framework this.Events.GameLaunched.Raise(new GameLaunchedEventArgs()); // preloaded - if (Context.IsSaveLoaded && !this.RaisedPreloadedEvent) - { - this.RaisedPreloadedEvent = true; - this.Events.SavePreloaded.RaiseEmpty(); - } + if (Context.IsSaveLoaded && this.LoadStage != LoadStage.Loaded && this.LoadStage != LoadStage.Ready) + this.OnLoadStageChanged(LoadStage.Loaded); // update tick this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); @@ -1649,14 +1686,6 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ - /// Perform any cleanup needed when a save is unloaded. - private void MarkWorldNotReady() - { - Context.IsWorldReady = false; - this.AfterLoadTimer.Reset(); - this.RaisedLoadedEvent = false; - } - #if !SMAPI_3_0_STRICT /// Raise the if there are any listeners. /// Whether to create a new sprite batch. diff --git a/src/SMAPI/Patches/LoadForNewGamePatch.cs b/src/SMAPI/Patches/LoadForNewGamePatch.cs new file mode 100644 index 00000000..9e788e84 --- /dev/null +++ b/src/SMAPI/Patches/LoadForNewGamePatch.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for which notifies SMAPI for save creation load stages. + /// This patch hooks into , checks if TitleMenu.transitioningCharacterCreationMenu is true (which means the player is creating a new save file), then raises after the location list is cleared twice (the second clear happens right before locations are created), and when the method ends. + internal class LoadForNewGamePatch : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// Simplifies access to private code. + private static Reflector Reflection; + + /// A callback to invoke when the load stage changes. + private static Action OnStageChanged; + + /// Whether was called as part of save creation. + private static bool IsCreating; + + /// The number of times that has been cleared since started. + private static int TimesLocationsCleared = 0; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(LoadForNewGamePatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Simplifies access to private code. + /// A callback to invoke when the load stage changes. + public LoadForNewGamePatch(Reflector reflection, Action onStageChanged) + { + LoadForNewGamePatch.Reflection = reflection; + LoadForNewGamePatch.OnStageChanged = onStageChanged; + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + MethodInfo method = AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Prefix)); + MethodInfo postfix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Postfix)); + + harmony.Patch(method, new HarmonyMethod(prefix), new HarmonyMethod(postfix)); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + private static bool Prefix() + { + LoadForNewGamePatch.IsCreating = Game1.activeClickableMenu is TitleMenu menu && LoadForNewGamePatch.Reflection.GetField(menu, "transitioningCharacterCreationMenu").GetValue(); + LoadForNewGamePatch.TimesLocationsCleared = 0; + if (LoadForNewGamePatch.IsCreating) + { + // raise CreatedBasicInfo after locations are cleared twice + ObservableCollection locations = (ObservableCollection)Game1.locations; + locations.CollectionChanged += LoadForNewGamePatch.OnLocationListChanged; + } + + return true; + } + + /// The method to call instead after . + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + private static void Postfix() + { + if (LoadForNewGamePatch.IsCreating) + { + // clean up + ObservableCollection locations = (ObservableCollection) Game1.locations; + locations.CollectionChanged -= LoadForNewGamePatch.OnLocationListChanged; + + // raise stage changed + LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedLocations); + } + } + + /// Raised when changes. + /// The event sender. + /// The event arguments. + private static void OnLocationListChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (++LoadForNewGamePatch.TimesLocationsCleared == 2) + LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedBasicInfo); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 36fa7e0b..fdb0c6c7 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -76,6 +76,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -123,6 +124,7 @@ + @@ -150,7 +152,6 @@ - @@ -327,6 +328,7 @@ + -- cgit From 8e0573d7d9f18792a19e741660b6a090cca1fb38 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 25 Dec 2018 15:10:22 -0500 Subject: add GameLoop.OneSecondUpdateTicking/Ticked --- src/SMAPI/Events/IGameLoopEvents.cs | 6 ++++ src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs | 32 ++++++++++++++++++++++ .../Events/OneSecondUpdateTickingEventArgs.cs | 32 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 8 ++++++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 14 ++++++++++ src/SMAPI/Framework/SGame.cs | 5 ++++ src/SMAPI/StardewModdingAPI.csproj | 2 ++ 7 files changed, 99 insertions(+) create mode 100644 src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs create mode 100644 src/SMAPI/Events/OneSecondUpdateTickingEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index ea79aa74..6fb56c8b 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -14,6 +14,12 @@ namespace StardewModdingAPI.Events /// Raised after the game state is updated (≈60 times per second). event EventHandler UpdateTicked; + /// Raised once per second before the game state is updated. + event EventHandler OneSecondUpdateTicking; + + /// Raised once per second after the game state is updated. + event EventHandler OneSecondUpdateTicked; + /// Raised before the game creates a new save file. event EventHandler SaveCreating; diff --git a/src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs b/src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs new file mode 100644 index 00000000..d330502a --- /dev/null +++ b/src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs @@ -0,0 +1,32 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class OneSecondUpdateTickedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + internal OneSecondUpdateTickedEventArgs(uint ticks) + { + this.Ticks = ticks; + } + + /// 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/OneSecondUpdateTickingEventArgs.cs b/src/SMAPI/Events/OneSecondUpdateTickingEventArgs.cs new file mode 100644 index 00000000..cdd9f4cc --- /dev/null +++ b/src/SMAPI/Events/OneSecondUpdateTickingEventArgs.cs @@ -0,0 +1,32 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class OneSecondUpdateTickingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + internal OneSecondUpdateTickingEventArgs(uint ticks) + { + this.Ticks = ticks; + } + + /// 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/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index b7f00f52..13244601 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -58,6 +58,12 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game performs its overall update tick (≈60 times per second). public readonly ManagedEvent UpdateTicked; + /// Raised once per second before the game performs its overall update tick. + public readonly ManagedEvent OneSecondUpdateTicking; + + /// Raised once per second after the game performs its overall update tick. + public readonly ManagedEvent OneSecondUpdateTicked; + /// Raised before the game creates the save file. public readonly ManagedEvent SaveCreating; @@ -380,6 +386,8 @@ namespace StardewModdingAPI.Framework.Events this.GameLaunched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); + this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking)); + this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked)); this.SaveCreating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); this.SaveCreated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); this.Saving = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index 3a764ab0..0177c22e 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -30,6 +30,20 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.UpdateTicked.Remove(value); } + /// Raised once per second before the game state is updated. + public event EventHandler OneSecondUpdateTicking + { + add => this.EventManager.OneSecondUpdateTicking.Add(value); + remove => this.EventManager.OneSecondUpdateTicking.Remove(value); + } + + /// Raised once per second after the game state is updated. + public event EventHandler OneSecondUpdateTicked + { + add => this.EventManager.OneSecondUpdateTicked.Add(value); + remove => this.EventManager.OneSecondUpdateTicked.Remove(value); + } + /// Raised before the game creates a new save file. public event EventHandler SaveCreating { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/F