From 63fb4dbe8ae4d611c4854f863b9b29265e02fdee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 5 Oct 2018 21:59:57 -0400 Subject: tweak new event naming convention (#310) --- src/SMAPI/Framework/Events/EventManager.cs | 276 ++++++++++++------------ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 18 +- src/SMAPI/Framework/Events/ModInputEvents.cs | 24 +-- src/SMAPI/Framework/Events/ModWorldEvents.cs | 42 ++-- 4 files changed, 180 insertions(+), 180 deletions(-) (limited to 'src/SMAPI/Framework/Events') diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 168ddde0..1435976a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -15,52 +15,52 @@ namespace StardewModdingAPI.Framework.Events ** Game loop ****/ /// Raised after the game is launched, right before the first update tick. - public readonly ManagedEvent GameLoop_Launched; + public readonly ManagedEvent GameLaunched; /// Raised before the game performs its overall update tick (≈60 times per second). - public readonly ManagedEvent GameLoop_Updating; + public readonly ManagedEvent UpdateTicking; /// Raised after the game performs its overall update tick (≈60 times per second). - public readonly ManagedEvent GameLoop_Updated; + public readonly ManagedEvent UpdateTicked; /**** ** Input ****/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; + public readonly ManagedEvent ButtonPressed; /// Raised after the player released a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; + public readonly ManagedEvent ButtonReleased; /// Raised after the player moves the in-game cursor. - public readonly ManagedEvent Input_CursorMoved; + public readonly ManagedEvent CursorMoved; /// Raised after the player scrolls the mouse wheel. - public readonly ManagedEvent Input_MouseWheelScrolled; + public readonly ManagedEvent MouseWheelScrolled; /**** ** World ****/ /// Raised after a game location is added or removed. - public readonly ManagedEvent World_LocationListChanged; + public readonly ManagedEvent LocationListChanged; /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent World_BuildingListChanged; + public readonly ManagedEvent BuildingListChanged; /// Raised after debris are added or removed in a location. - public readonly ManagedEvent World_DebrisListChanged; + public readonly ManagedEvent DebrisListChanged; /// Raised after large terrain features (like bushes) are added or removed in a location. - public readonly ManagedEvent World_LargeTerrainFeatureListChanged; + public readonly ManagedEvent LargeTerrainFeatureListChanged; /// Raised after NPCs are added or removed in a location. - public readonly ManagedEvent World_NpcListChanged; + public readonly ManagedEvent NpcListChanged; /// Raised after objects are added or removed in a location. - public readonly ManagedEvent World_ObjectListChanged; + public readonly ManagedEvent ObjectListChanged; /// Raised after terrain features (like floors and trees) are added or removed in a location. - public readonly ManagedEvent World_TerrainFeatureListChanged; + public readonly ManagedEvent TerrainFeatureListChanged; /********* @@ -70,185 +70,185 @@ namespace StardewModdingAPI.Framework.Events ** ContentEvents ****/ /// Raised after the content language changes. - public readonly ManagedEvent> Content_LocaleChanged; + public readonly ManagedEvent> Legacy_LocaleChanged; /**** ** ControlEvents ****/ /// Raised when the changes. That happens when the player presses or releases a key. - public readonly ManagedEvent Legacy_Control_KeyboardChanged; + public readonly ManagedEvent Legacy_KeyboardChanged; /// Raised after the player presses a keyboard key. - public readonly ManagedEvent Legacy_Control_KeyPressed; + public readonly ManagedEvent Legacy_KeyPressed; /// Raised after the player releases a keyboard key. - public readonly ManagedEvent Legacy_Control_KeyReleased; + public readonly ManagedEvent Legacy_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 Legacy_Control_MouseChanged; + public readonly ManagedEvent Legacy_MouseChanged; /// The player pressed a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Legacy_Control_ControllerButtonPressed; + public readonly ManagedEvent Legacy_ControllerButtonPressed; /// The player released a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Legacy_Control_ControllerButtonReleased; + public readonly ManagedEvent Legacy_ControllerButtonReleased; /// The player pressed a controller trigger button. - public readonly ManagedEvent Legacy_Control_ControllerTriggerPressed; + public readonly ManagedEvent Legacy_ControllerTriggerPressed; /// The player released a controller trigger button. - public readonly ManagedEvent Legacy_Control_ControllerTriggerReleased; + public readonly ManagedEvent Legacy_ControllerTriggerReleased; /**** ** GameEvents ****/ /// Raised once after the game initialises and all methods have been called. - public readonly ManagedEvent Game_FirstUpdateTick; + public readonly ManagedEvent Legacy_FirstUpdateTick; /// Raised when the game updates its state (≈60 times per second). - public readonly ManagedEvent Game_UpdateTick; + public readonly ManagedEvent Legacy_UpdateTick; /// Raised every other tick (≈30 times per second). - public readonly ManagedEvent Game_SecondUpdateTick; + public readonly ManagedEvent Legacy_SecondUpdateTick; /// Raised every fourth tick (≈15 times per second). - public readonly ManagedEvent Game_FourthUpdateTick; + public readonly ManagedEvent Legacy_FourthUpdateTick; /// Raised every eighth tick (≈8 times per second). - public readonly ManagedEvent Game_EighthUpdateTick; + public readonly ManagedEvent Legacy_EighthUpdateTick; /// Raised every 15th tick (≈4 times per second). - public readonly ManagedEvent Game_QuarterSecondTick; + public readonly ManagedEvent Legacy_QuarterSecondTick; /// Raised every 30th tick (≈twice per second). - public readonly ManagedEvent Game_HalfSecondTick; + public readonly ManagedEvent Legacy_HalfSecondTick; /// Raised every 60th tick (≈once per second). - public readonly ManagedEvent Game_OneSecondTick; + public readonly ManagedEvent Legacy_OneSecondTick; /**** ** GraphicsEvents ****/ /// Raised after the game window is resized. - public readonly ManagedEvent Graphics_Resize; + public readonly ManagedEvent Legacy_Resize; /// Raised before drawing the world to the screen. - public readonly ManagedEvent Graphics_OnPreRenderEvent; + public readonly ManagedEvent Legacy_OnPreRenderEvent; /// Raised after drawing the world to the screen. - public readonly ManagedEvent Graphics_OnPostRenderEvent; + public readonly ManagedEvent Legacy_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; + public readonly ManagedEvent Legacy_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; + public readonly ManagedEvent Legacy_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; + public readonly ManagedEvent Legacy_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; + public readonly ManagedEvent Legacy_OnPostRenderGuiEvent; /**** ** InputEvents ****/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Legacy_Input_ButtonPressed; + public readonly ManagedEvent Legacy_ButtonPressed; /// Raised after the player releases a keyboard key on the keyboard, controller, or mouse. - public readonly ManagedEvent Legacy_Input_ButtonReleased; + public readonly ManagedEvent Legacy_ButtonReleased; /**** ** LocationEvents ****/ /// Raised after a game location is added or removed. - public readonly ManagedEvent Legacy_Location_LocationsChanged; + public readonly ManagedEvent Legacy_LocationsChanged; /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent Legacy_Location_BuildingsChanged; + public readonly ManagedEvent Legacy_BuildingsChanged; /// Raised after objects are added or removed in a location. - public readonly ManagedEvent Legacy_Location_ObjectsChanged; + public readonly ManagedEvent Legacy_ObjectsChanged; /**** ** 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; + public readonly ManagedEvent Legacy_MenuChanged; /// Raised after a game menu is closed. - public readonly ManagedEvent Menu_Closed; + public readonly ManagedEvent Legacy_MenuClosed; /**** ** MultiplayerEvents ****/ /// Raised before the game syncs changes from other players. - public readonly ManagedEvent Multiplayer_BeforeMainSync; + public readonly ManagedEvent Legacy_BeforeMainSync; /// Raised after the game syncs changes from other players. - public readonly ManagedEvent Multiplayer_AfterMainSync; + public readonly ManagedEvent Legacy_AfterMainSync; /// Raised before the game broadcasts changes to other players. - public readonly ManagedEvent Multiplayer_BeforeMainBroadcast; + public readonly ManagedEvent Legacy_BeforeMainBroadcast; /// Raised after the game broadcasts changes to other players. - public readonly ManagedEvent Multiplayer_AfterMainBroadcast; + public readonly ManagedEvent Legacy_AfterMainBroadcast; /**** ** MineEvents ****/ /// Raised after the player warps to a new level of the mine. - public readonly ManagedEvent Mine_LevelChanged; + public readonly ManagedEvent Legacy_MineLevelChanged; /**** ** PlayerEvents ****/ /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). - public readonly ManagedEvent Player_InventoryChanged; + public readonly ManagedEvent Legacy_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; + public readonly ManagedEvent Legacy_LeveledUp; /// Raised after the player warps to a new location. - public readonly ManagedEvent Player_Warped; + public readonly ManagedEvent Legacy_PlayerWarped; /**** ** SaveEvents ****/ /// Raised before the game creates the save file. - public readonly ManagedEvent Save_BeforeCreate; + public readonly ManagedEvent Legacy_BeforeCreateSave; /// Raised after the game finishes creating the save file. - public readonly ManagedEvent Save_AfterCreate; + public readonly ManagedEvent Legacy_AfterCreateSave; /// Raised before the game begins writes data to the save file. - public readonly ManagedEvent Save_BeforeSave; + public readonly ManagedEvent Legacy_BeforeSave; /// Raised after the game finishes writing data to the save file. - public readonly ManagedEvent Save_AfterSave; + public readonly ManagedEvent Legacy_AfterSave; /// Raised after the player loads a save slot. - public readonly ManagedEvent Save_AfterLoad; + public readonly ManagedEvent Legacy_AfterLoad; /// Raised after the game returns to the title screen. - public readonly ManagedEvent Save_AfterReturnToTitle; + public readonly ManagedEvent Legacy_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; + public readonly ManagedEvent Legacy_UnvalidatedUpdateTick; /**** ** TimeEvents ****/ /// Raised after the game begins a new day, including when loading a save. - public readonly ManagedEvent Time_AfterDayStarted; + public readonly ManagedEvent Legacy_AfterDayStarted; /// Raised after the in-game clock changes. - public readonly ManagedEvent Time_TimeOfDayChanged; + public readonly ManagedEvent Legacy_TimeOfDayChanged; /********* @@ -264,84 +264,84 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) - this.GameLoop_Launched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Launched)); - 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)); - this.Input_MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); - - this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); - this.World_DebrisListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged)); - this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); - this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); - this.World_NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); - this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); - this.World_TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); + 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.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); + this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + + this.BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.DebrisListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged)); + this.LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); + this.LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); + this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); // init events (old) - this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); - - this.Legacy_Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); - this.Legacy_Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); - this.Legacy_Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); - this.Legacy_Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); - this.Legacy_Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); - this.Legacy_Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); - this.Legacy_Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); - this.Legacy_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.Legacy_Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); - this.Legacy_Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - - this.Legacy_Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Legacy_Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); - this.Legacy_Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); - - this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); - this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); - - this.Multiplayer_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); - this.Multiplayer_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); - this.Multiplayer_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); - this.Multiplayer_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); - - 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.Player_Warped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); - - 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)); + this.Legacy_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); + + this.Legacy_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Legacy_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Legacy_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Legacy_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Legacy_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Legacy_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Legacy_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Legacy_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + + this.Legacy_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); + this.Legacy_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); + this.Legacy_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); + this.Legacy_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); + this.Legacy_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); + this.Legacy_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); + this.Legacy_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); + this.Legacy_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); + + this.Legacy_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); + this.Legacy_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); + this.Legacy_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); + this.Legacy_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); + this.Legacy_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); + this.Legacy_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); + this.Legacy_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); + + this.Legacy_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Legacy_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + + this.Legacy_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Legacy_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); + this.Legacy_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); + + this.Legacy_MenuChanged = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); + this.Legacy_MenuClosed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + + this.Legacy_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); + this.Legacy_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); + this.Legacy_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); + this.Legacy_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); + + this.Legacy_MineLevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); + + this.Legacy_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); + this.Legacy_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + this.Legacy_PlayerWarped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); + + this.Legacy_BeforeCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); + this.Legacy_AfterCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); + this.Legacy_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); + this.Legacy_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); + this.Legacy_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); + this.Legacy_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); + + this.Legacy_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); + + this.Legacy_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); + this.Legacy_TimeOfDayChanged = ManageEventOf(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); } } } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index 379a4e96..781597ef 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -10,24 +10,24 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after the game is launched, right before the first update tick. - public event EventHandler Launched + public event EventHandler GameLaunched { - add => this.EventManager.GameLoop_Launched.Add(value); - remove => this.EventManager.GameLoop_Launched.Remove(value); + add => this.EventManager.GameLaunched.Add(value); + remove => this.EventManager.GameLaunched.Remove(value); } /// Raised before the game performs its overall update tick (≈60 times per second). - public event EventHandler Updating + public event EventHandler UpdateTicking { - add => this.EventManager.GameLoop_Updating.Add(value); - remove => this.EventManager.GameLoop_Updating.Remove(value); + add => this.EventManager.UpdateTicking.Add(value); + remove => this.EventManager.UpdateTicking.Remove(value); } /// Raised after the game performs its overall update tick (≈60 times per second). - public event EventHandler Updated + public event EventHandler UpdateTicked { - add => this.EventManager.GameLoop_Updated.Add(value); - remove => this.EventManager.GameLoop_Updated.Remove(value); + add => this.EventManager.UpdateTicked.Add(value); + remove => this.EventManager.UpdateTicked.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index feca34f3..6a4298b4 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -10,31 +10,31 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public event EventHandler ButtonPressed + public event EventHandler ButtonPressed { - add => this.EventManager.Input_ButtonPressed.Add(value); - remove => this.EventManager.Input_ButtonPressed.Remove(value); + add => this.EventManager.ButtonPressed.Add(value); + remove => this.EventManager.ButtonPressed.Remove(value); } /// Raised after the player releases a button on the keyboard, controller, or mouse. - public event EventHandler ButtonReleased + public event EventHandler ButtonReleased { - add => this.EventManager.Input_ButtonReleased.Add(value); - remove => this.EventManager.Input_ButtonReleased.Remove(value); + add => this.EventManager.ButtonReleased.Add(value); + remove => this.EventManager.ButtonReleased.Remove(value); } /// Raised after the player moves the in-game cursor. - public event EventHandler CursorMoved + public event EventHandler CursorMoved { - add => this.EventManager.Input_CursorMoved.Add(value); - remove => this.EventManager.Input_CursorMoved.Remove(value); + add => this.EventManager.CursorMoved.Add(value); + remove => this.EventManager.CursorMoved.Remove(value); } /// Raised after the player scrolls the mouse wheel. - public event EventHandler MouseWheelScrolled + public event EventHandler MouseWheelScrolled { - add => this.EventManager.Input_MouseWheelScrolled.Add(value); - remove => this.EventManager.Input_MouseWheelScrolled.Remove(value); + add => this.EventManager.MouseWheelScrolled.Add(value); + remove => this.EventManager.MouseWheelScrolled.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index dc9c0f4c..b85002a3 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -10,52 +10,52 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after a game location is added or removed. - public event EventHandler LocationListChanged + public event EventHandler LocationListChanged { - add => this.EventManager.World_LocationListChanged.Add(value, this.Mod); - remove => this.EventManager.World_LocationListChanged.Remove(value); + add => this.EventManager.LocationListChanged.Add(value, this.Mod); + remove => this.EventManager.LocationListChanged.Remove(value); } /// Raised after buildings are added or removed in a location. - public event EventHandler BuildingListChanged + public event EventHandler BuildingListChanged { - add => this.EventManager.World_BuildingListChanged.Add(value, this.Mod); - remove => this.EventManager.World_BuildingListChanged.Remove(value); + add => this.EventManager.BuildingListChanged.Add(value, this.Mod); + remove => this.EventManager.BuildingListChanged.Remove(value); } /// Raised after debris are added or removed in a location. - public event EventHandler DebrisListChanged + public event EventHandler DebrisListChanged { - add => this.EventManager.World_DebrisListChanged.Add(value, this.Mod); - remove => this.EventManager.World_DebrisListChanged.Remove(value); + add => this.EventManager.DebrisListChanged.Add(value, this.Mod); + remove => this.EventManager.DebrisListChanged.Remove(value); } /// Raised after large terrain features (like bushes) are added or removed in a location. - public event EventHandler LargeTerrainFeatureListChanged + public event EventHandler LargeTerrainFeatureListChanged { - add => this.EventManager.World_LargeTerrainFeatureListChanged.Add(value, this.Mod); - remove => this.EventManager.World_LargeTerrainFeatureListChanged.Remove(value); + add => this.EventManager.LargeTerrainFeatureListChanged.Add(value, this.Mod); + remove => this.EventManager.LargeTerrainFeatureListChanged.Remove(value); } /// Raised after NPCs are added or removed in a location. - public event EventHandler NpcListChanged + public event EventHandler NpcListChanged { - add => this.EventManager.World_NpcListChanged.Add(value); - remove => this.EventManager.World_NpcListChanged.Remove(value); + add => this.EventManager.NpcListChanged.Add(value); + remove => this.EventManager.NpcListChanged.Remove(value); } /// Raised after objects are added or removed in a location. - public event EventHandler ObjectListChanged + public event EventHandler ObjectListChanged { - add => this.EventManager.World_ObjectListChanged.Add(value); - remove => this.EventManager.World_ObjectListChanged.Remove(value); + add => this.EventManager.ObjectListChanged.Add(value); + remove => this.EventManager.ObjectListChanged.Remove(value); } /// Raised after terrain features (like floors and trees) are added or removed in a location. - public event EventHandler TerrainFeatureListChanged + public event EventHandler TerrainFeatureListChanged { - add => this.EventManager.World_TerrainFeatureListChanged.Add(value); - remove => this.EventManager.World_TerrainFeatureListChanged.Remove(value); + add => this.EventManager.TerrainFeatureListChanged.Add(value); + remove => this.EventManager.TerrainFeatureListChanged.Remove(value); } -- cgit From 14fab29370310a762a000c50b23075326b4e95da Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Oct 2018 00:25:48 -0400 Subject: add save and day-started events for 3.0 (#310) --- src/SMAPI/Events/DayStartedEventArgs.cs | 7 +++++ src/SMAPI/Events/IGameLoopEvents.cs | 18 +++++++++++ src/SMAPI/Events/SaveCreatedEventArgs.cs | 7 +++++ src/SMAPI/Events/SaveCreatingEventArgs.cs | 7 +++++ src/SMAPI/Events/SaveLoadedEventArgs.cs | 7 +++++ src/SMAPI/Events/SavedEventArgs.cs | 7 +++++ src/SMAPI/Events/SavingEventArgs.cs | 7 +++++ src/SMAPI/Framework/Events/EventManager.cs | 24 ++++++++++++++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 42 +++++++++++++++++++++++++ src/SMAPI/Framework/InternalExtensions.cs | 12 +++++++ src/SMAPI/Framework/SGame.cs | 9 ++++++ src/SMAPI/Framework/Singleton.cs | 10 ++++++ src/SMAPI/StardewModdingAPI.csproj | 7 +++++ 13 files changed, 164 insertions(+) create mode 100644 src/SMAPI/Events/DayStartedEventArgs.cs create mode 100644 src/SMAPI/Events/SaveCreatedEventArgs.cs create mode 100644 src/SMAPI/Events/SaveCreatingEventArgs.cs create mode 100644 src/SMAPI/Events/SaveLoadedEventArgs.cs create mode 100644 src/SMAPI/Events/SavedEventArgs.cs create mode 100644 src/SMAPI/Events/SavingEventArgs.cs create mode 100644 src/SMAPI/Framework/Singleton.cs (limited to 'src/SMAPI/Framework/Events') diff --git a/src/SMAPI/Events/DayStartedEventArgs.cs b/src/SMAPI/Events/DayStartedEventArgs.cs new file mode 100644 index 00000000..45823628 --- /dev/null +++ b/src/SMAPI/Events/DayStartedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class DayStartedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index 8ab86c9e..165aa0ce 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -13,5 +13,23 @@ namespace StardewModdingAPI.Events /// Raised after the game state is updated (≈60 times per second). event EventHandler UpdateTicked; + + /// Raised before the game creates a new save file. + event EventHandler SaveCreating; + + /// Raised after the game finishes creating the save file. + event EventHandler SaveCreated; + + /// Raised before the game begins writes data to the save file (except the initial save creation). + event EventHandler Saving; + + /// 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. + event EventHandler SaveLoaded; + + /// Raised after the game begins a new day (including when the player loads a save). + event EventHandler DayStarted; } } diff --git a/src/SMAPI/Events/SaveCreatedEventArgs.cs b/src/SMAPI/Events/SaveCreatedEventArgs.cs new file mode 100644 index 00000000..5ae22531 --- /dev/null +++ b/src/SMAPI/Events/SaveCreatedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveCreatedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SaveCreatingEventArgs.cs b/src/SMAPI/Events/SaveCreatingEventArgs.cs new file mode 100644 index 00000000..3c83f421 --- /dev/null +++ b/src/SMAPI/Events/SaveCreatingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveCreatingEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SaveLoadedEventArgs.cs b/src/SMAPI/Events/SaveLoadedEventArgs.cs new file mode 100644 index 00000000..f8aaa7f7 --- /dev/null +++ b/src/SMAPI/Events/SaveLoadedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveLoadedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SavedEventArgs.cs b/src/SMAPI/Events/SavedEventArgs.cs new file mode 100644 index 00000000..a4e90729 --- /dev/null +++ b/src/SMAPI/Events/SavedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SavedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SavingEventArgs.cs b/src/SMAPI/Events/SavingEventArgs.cs new file mode 100644 index 00000000..f323ca9e --- /dev/null +++ b/src/SMAPI/Events/SavingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SavingEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 1435976a..023c45de 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -23,6 +23,24 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game performs its overall update tick (≈60 times per second). public readonly ManagedEvent UpdateTicked; + /// Raised before the game creates the save file. + public readonly ManagedEvent SaveCreating; + + /// Raised after the game finishes creating the save file. + public readonly ManagedEvent SaveCreated; + + /// Raised before the game begins writes data to the save file (except the initial save creation). + public readonly ManagedEvent Saving; + + /// 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. + public readonly ManagedEvent SaveLoaded; + + /// Raised after the game begins a new day, including when loading a save. + public readonly ManagedEvent DayStarted; + /**** ** Input ****/ @@ -267,6 +285,12 @@ 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.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)); + this.Saved = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saved)); + this.SaveLoaded = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveLoaded)); + this.DayStarted = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayStarted)); this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index 781597ef..cf7e54aa 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -30,6 +30,48 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.UpdateTicked.Remove(value); } + /// Raised before the game creates a new save file. + public event EventHandler SaveCreating + { + add => this.EventManager.SaveCreating.Add(value); + remove => this.EventManager.SaveCreating.Remove(value); + } + + /// Raised after the game finishes creating the save file. + public event EventHandler SaveCreated + { + add => this.EventManager.SaveCreated.Add(value); + remove => this.EventManager.SaveCreated.Remove(value); + } + + /// Raised before the game begins writes data to the save file. + public event EventHandler Saving + { + add => this.EventManager.Saving.Add(value); + remove => this.EventManager.Saving.Remove(value); + } + + /// Raised after the game finishes writing data to the save file. + public event EventHandler Saved + { + add => this.EventManager.Saved.Add(value); + remove => this.EventManager.Saved.Remove(value); + } + + /// Raised after the player loads a save slot. + public event EventHandler SaveLoaded + { + add => this.EventManager.SaveLoaded.Add(value); + remove => this.EventManager.SaveLoaded.Remove(value); + } + + /// Raised after the game begins a new day (including when the player loads a save). + public event EventHandler DayStarted + { + add => this.EventManager.DayStarted.Add(value); + remove => this.EventManager.DayStarted.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index ff3925fb..b51ff6a8 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Reflection; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -39,6 +40,17 @@ namespace StardewModdingAPI.Framework metadata.Monitor.Log(message, level); } + /**** + ** ManagedEvent + ****/ + /// Raise the event and notify all handlers. + /// The empty event arguments type to construct. + /// The event to raise. + public static void RaiseEmpty(this ManagedEvent @event) where TEventArgs : new() + { + @event.Raise(Singleton.Instance); + } + /**** ** Exceptions ****/ diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a432e844..ef851afc 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -323,6 +323,7 @@ namespace StardewModdingAPI.Framework { this.IsBetweenCreateEvents = true; this.Monitor.Log("Context: before save creation.", LogLevel.Trace); + this.Events.SaveCreating.RaiseEmpty(); this.Events.Legacy_BeforeCreateSave.Raise(); } @@ -331,6 +332,7 @@ namespace StardewModdingAPI.Framework { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); + this.Events.Saving.RaiseEmpty(); this.Events.Legacy_BeforeSave.Raise(); } @@ -344,6 +346,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.Events.SaveCreated.RaiseEmpty(); this.Events.Legacy_AfterCreateSave.Raise(); } if (this.IsBetweenSaveEvents) @@ -351,6 +354,9 @@ namespace StardewModdingAPI.Framework // raise after-save this.IsBetweenSaveEvents = false; this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.Events.Saved.RaiseEmpty(); + this.Events.DayStarted.RaiseEmpty(); + this.Events.Legacy_AfterSave.Raise(); this.Events.Legacy_AfterDayStarted.Raise(); } @@ -410,6 +416,9 @@ namespace StardewModdingAPI.Framework // raise events this.RaisedAfterLoadEvent = true; + this.Events.SaveLoaded.RaiseEmpty(); + this.Events.DayStarted.RaiseEmpty(); + this.Events.Legacy_AfterLoad.Raise(); this.Events.Legacy_AfterDayStarted.Raise(); } diff --git a/src/SMAPI/Framework/Singleton.cs b/src/SMAPI/Framework/Singleton.cs new file mode 100644 index 00000000..399a8bf0 --- /dev/null +++ b/src/SMAPI/Framework/Singleton.cs @@ -0,0 +1,10 @@ +namespace StardewModdingAPI.Framework +{ + /// Provides singleton instances of a given type. + /// The instance type. + internal static class Singleton where T : new() + { + /// The singleton instance. + public static T Instance { get; } = new T(); + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 5ddee30c..4c8f2ffa 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,6 +79,12 @@ Properties\GlobalAssemblyInfo.cs + + + + + + @@ -125,6 +131,7 @@ + -- cgit From 79705448f57c962e9331fb802097c24d2424476c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Oct 2018 00:51:45 -0400 Subject: add DayEnding event (#310) --- src/SMAPI/Events/DayEndingEventArgs.cs | 7 +++++ src/SMAPI/Events/IGameLoopEvents.cs | 3 +++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 7 +++++ src/SMAPI/Framework/SGame.cs | 7 +++++ src/SMAPI/Framework/SModHooks.cs | 34 +++++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 63 insertions(+) create mode 100644 src/SMAPI/Events/DayEndingEventArgs.cs create mode 100644 src/SMAPI/Framework/SModHooks.cs (limited to 'src/SMAPI/Framework/Events') diff --git a/src/SMAPI/Events/DayEndingEventArgs.cs b/src/SMAPI/Events/DayEndingEventArgs.cs new file mode 100644 index 00000000..5cb433bc --- /dev/null +++ b/src/SMAPI/Events/DayEndingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class DayEndingEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index 165aa0ce..5f531ba7 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -31,5 +31,8 @@ namespace StardewModdingAPI.Events /// Raised after the game begins a new day (including when the player loads a save). event EventHandler DayStarted; + + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + event EventHandler DayEnding; } } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 023c45de..616db7d9 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -41,6 +41,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game begins a new day, including when loading a save. public readonly ManagedEvent DayStarted; + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + public readonly ManagedEvent DayEnding; + /**** ** Input ****/ @@ -291,6 +294,7 @@ namespace StardewModdingAPI.Framework.Events this.Saved = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saved)); this.SaveLoaded = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveLoaded)); this.DayStarted = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayStarted)); + this.DayEnding = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayEnding)); this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index cf7e54aa..d1def91b 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -72,6 +72,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.DayStarted.Remove(value); } + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + public event EventHandler DayEnding + { + add => this.EventManager.DayEnding.Add(value); + remove => this.EventManager.DayEnding.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index ef851afc..bd266ce1 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -151,6 +151,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); + Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables Game1.locations = new ObservableCollection(); @@ -182,6 +183,12 @@ namespace StardewModdingAPI.Framework /**** ** Intercepted methods & events ****/ + /// A callback invoked before runs. + protected void OnNewDayAfterFade() + { + this.Events.DayEnding.RaiseEmpty(); + } + /// Constructor a content manager to read XNB files. /// The service provider to use to locate services. /// The root directory to search for content. diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs new file mode 100644 index 00000000..9f0201c8 --- /dev/null +++ b/src/SMAPI/Framework/SModHooks.cs @@ -0,0 +1,34 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// Invokes callbacks for mod hooks provided by the game. + internal class SModHooks : ModHooks + { + /********* + ** Properties + *********/ + /// A callback to invoke before runs. + private readonly Action BeforeNewDayAfterFade; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A callback to invoke before runs. + public SModHooks(Action beforeNewDayAfterFade) + { + this.BeforeNewDayAfterFade = beforeNewDayAfterFade; + } + + /// A hook invoked when is called. + /// The vanilla logic. + public override void OnGame1_NewDayAfterFade(Action action) + { + this.BeforeNewDayAfterFade?.Invoke(); + action(); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 4c8f2ffa..efb85c52 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,6 +79,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From ec6025aad35addab8121a31d1c4abf667fd5210a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Oct 2018 18:57:09 -0400 Subject: add more events (#310) --- docs/release-notes.md | 7 +- src/SMAPI/Enums/SkillType.cs | 26 +++++ src/SMAPI/Events/EventArgsInventoryChanged.cs | 2 +- src/SMAPI/Events/EventArgsLevelUp.cs | 13 ++- src/SMAPI/Events/IDisplayEvents.cs | 39 +++++++ src/SMAPI/Events/IGameLoopEvents.cs | 6 + src/SMAPI/Events/IModEvents.cs | 9 ++ src/SMAPI/Events/IPlayerEvents.cs | 17 +++ src/SMAPI/Events/ISpecialisedEvents.cs | 14 +++ src/SMAPI/Events/InventoryChangedEventArgs.cs | 56 +++++++++ src/SMAPI/Events/ItemStackSizeChange.cs | 35 ++++++ src/SMAPI/Events/LevelChangedEventArgs.cs | 42 +++++++ src/SMAPI/Events/MenuChangedEventArgs.cs | 31 +++++ src/SMAPI/Events/RenderedActiveMenuEventArgs.cs | 16 +++ src/SMAPI/Events/RenderedEventArgs.cs | 16 +++ src/SMAPI/Events/RenderedHudEventArgs.cs | 16 +++ src/SMAPI/Events/RenderedWorldEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingActiveMenuEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingHudEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingWorldEventArgs.cs | 16 +++ src/SMAPI/Events/ReturnedToTitleEventArgs.cs | 7 ++ src/SMAPI/Events/SpecialisedEvents.cs | 2 +- src/SMAPI/Events/TimeChangedEventArgs.cs | 30 +++++ .../Events/UnvalidatedUpdateTickedEventArgs.cs | 36 ++++++ .../Events/UnvalidatedUpdateTickingEventArgs.cs | 36 ++++++ src/SMAPI/Events/WarpedEventArgs.cs | 37 ++++++ src/SMAPI/Events/WindowResizedEventArgs.cs | 31 +++++ src/SMAPI/Framework/Events/EventManager.cs | 80 +++++++++++++ src/SMAPI/Framework/Events/ModDisplayEvents.cs | 93 +++++++++++++++ src/SMAPI/Framework/Events/ModEvents.cs | 12 ++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 15 +++ src/SMAPI/Framework/Events/ModPlayerEvents.cs | 43 +++++++ src/SMAPI/Framework/Events/ModSpecialisedEvents.cs | 36 ++++++ src/SMAPI/Framework/InternalExtensions.cs | 4 +- src/SMAPI/Framework/SGame.cs | 81 ++++++++++--- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 19 +-- src/SMAPI/StardewModdingAPI.csproj | 128 +++++++++++++-------- 38 files changed, 1029 insertions(+), 86 deletions(-) create mode 100644 src/SMAPI/Enums/SkillType.cs create mode 100644 src/SMAPI/Events/IDisplayEvents.cs create mode 100644 src/SMAPI/Events/IPlayerEvents.cs create mode 100644 src/SMAPI/Events/ISpecialisedEvents.cs create mode 100644 src/SMAPI/Events/InventoryChangedEventArgs.cs create mode 100644 src/SMAPI/Events/ItemStackSizeChange.cs create mode 100644 src/SMAPI/Events/LevelChangedEventArgs.cs create mode 100644 src/SMAPI/Events/MenuChangedEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedActiveMenuEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedHudEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedWorldEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingActiveMenuEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingHudEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingWorldEventArgs.cs create mode 100644 src/SMAPI/Events/ReturnedToTitleEventArgs.cs create mode 100644 src/SMAPI/Events/TimeChangedEventArgs.cs create mode 100644 src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs create mode 100644 src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs create mode 100644 src/SMAPI/Events/WarpedEventArgs.cs create mode 100644 src/SMAPI/Events/WindowResizedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModDisplayEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModPlayerEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModSpecialisedEvents.cs (limited to 'src/SMAPI/Framework/Events') diff --git a/docs/release-notes.md b/docs/release-notes.md index beba403a..63db5dc3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,10 +23,13 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). - * Added support for overlaying image assets with semi-transparency using the content API. * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation for the non-developers version of SMAPI. - * Mods are no longer prevented from loading a PNG while the game is drawing. + * Added more events to the prototype `helper.Events` for SMAPI 3.0. + * Added `SkillType` enum constant. + * Improved content API: + * added support for overlaying image assets with semi-transparency; + * mods can now load PNGs even if the game is currently drawing. * When comparing mod versions, SMAPI now considers `-unofficial` to be lower-precedence than any other value (e.g. `1.0-beta` is now considered newer than `1.0-unofficial` regardless of normal sorting). * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. * Fixed trace logs not showing path for invalid mods. diff --git a/src/SMAPI/Enums/SkillType.cs b/src/SMAPI/Enums/SkillType.cs new file mode 100644 index 00000000..10518ec9 --- /dev/null +++ b/src/SMAPI/Enums/SkillType.cs @@ -0,0 +1,26 @@ +using StardewValley; + +namespace StardewModdingAPI.Enums +{ + /// The player skill types. + public enum SkillType + { + /// The combat skill. + Combat = Farmer.combatSkill, + + /// The farming skill. + Farming = Farmer.farmingSkill, + + /// The fishing skill. + Fishing = Farmer.fishingSkill, + + /// The foraging skill. + Foraging = Farmer.foragingSkill, + + /// The mining skill. + Mining = Farmer.miningSkill, + + /// The luck skill. + Luck = Farmer.luckSkill + } +} diff --git a/src/SMAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs index 1fdca834..3a2354b6 100644 --- a/src/SMAPI/Events/EventArgsInventoryChanged.cs +++ b/src/SMAPI/Events/EventArgsInventoryChanged.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The player's inventory. /// The inventory changes. - public EventArgsInventoryChanged(IList inventory, List changedItems) + public EventArgsInventoryChanged(IList inventory, ItemStackChange[] changedItems) { this.Inventory = inventory; this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); diff --git a/src/SMAPI/Events/EventArgsLevelUp.cs b/src/SMAPI/Events/EventArgsLevelUp.cs index fe6696d4..e9a697e7 100644 --- a/src/SMAPI/Events/EventArgsLevelUp.cs +++ b/src/SMAPI/Events/EventArgsLevelUp.cs @@ -1,4 +1,5 @@ using System; +using StardewModdingAPI.Enums; namespace StardewModdingAPI.Events { @@ -18,22 +19,22 @@ namespace StardewModdingAPI.Events public enum LevelType { /// The combat skill. - Combat, + Combat = SkillType.Combat, /// The farming skill. - Farming, + Farming = SkillType.Farming, /// The fishing skill. - Fishing, + Fishing = SkillType.Fishing, /// The foraging skill. - Foraging, + Foraging = SkillType.Foraging, /// The mining skill. - Mining, + Mining = SkillType.Mining, /// The luck skill. - Luck + Luck = SkillType.Luck } diff --git a/src/SMAPI/Events/IDisplayEvents.cs b/src/SMAPI/Events/IDisplayEvents.cs new file mode 100644 index 00000000..dbf8d90f --- /dev/null +++ b/src/SMAPI/Events/IDisplayEvents.cs @@ -0,0 +1,39 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Events related to UI and drawing to the screen. + public interface IDisplayEvents + { + /// Raised after a game menu is opened, closed, or replaced. + event EventHandler MenuChanged; + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + event EventHandler Rendering; + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + event EventHandler Rendered; + + /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. + event EventHandler RenderingWorld; + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. + event EventHandler RenderedWorld; + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. + event EventHandler RenderingActiveMenu; + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. + event EventHandler RenderedActiveMenu; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. + event EventHandler RenderingHud; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. + event EventHandler RenderedHud; + + /// Raised after the game window is resized. + event EventHandler WindowResized; + } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index 5f531ba7..e1900f79 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -34,5 +34,11 @@ namespace StardewModdingAPI.Events /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . event EventHandler DayEnding; + + /// Raised after the in-game clock time changes. + event EventHandler TimeChanged; + + /// Raised after the game returns to the title screen. + event EventHandler ReturnedToTitle; } } diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index cf2f8cb8..76da7751 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,13 +3,22 @@ namespace StardewModdingAPI.Events /// Manages access to events raised by SMAPI. public interface IModEvents { + /// Events related to UI and drawing to the screen. + IDisplayEvents Display { get; } + /// 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; } + /// Events raised when the player data changes. + IPlayerEvents Player { get; } + /// Events raised when something changes in the world. IWorldEvents World { get; } + + /// Events serving specialised edge cases that shouldn't be used by most mods. + ISpecialisedEvents Specialised { get; } } } diff --git a/src/SMAPI/Events/IPlayerEvents.cs b/src/SMAPI/Events/IPlayerEvents.cs new file mode 100644 index 00000000..81e17b1a --- /dev/null +++ b/src/SMAPI/Events/IPlayerEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player data changes. + public interface IPlayerEvents + { + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player. + event EventHandler InventoryChanged; + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the current player. + event EventHandler LevelChanged; + + /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the current player. + event EventHandler Warped; + } +} diff --git a/src/SMAPI/Events/ISpecialisedEvents.cs b/src/SMAPI/Events/ISpecialisedEvents.cs new file mode 100644 index 00000000..928cd05d --- /dev/null +++ b/src/SMAPI/Events/ISpecialisedEvents.cs @@ -0,0 +1,14 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mods. + public interface ISpecialisedEvents + { + /// 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; + + /// Raised after 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 UnvalidatedUpdateTicked; + } +} diff --git a/src/SMAPI/Events/InventoryChangedEventArgs.cs b/src/SMAPI/Events/InventoryChangedEventArgs.cs new file mode 100644 index 00000000..a081611b --- /dev/null +++ b/src/SMAPI/Events/InventoryChangedEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class InventoryChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose inventory changed. + public Farmer Player { get; } + + /// The added items. + public IEnumerable Added { get; } + + /// The removed items. + public IEnumerable Removed { get; } + + /// The items whose stack sizes changed, with the relative change. + public IEnumerable QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player whose inventory changed. + /// The inventory changes. + public InventoryChangedEventArgs(Farmer player, ItemStackChange[] changedItems) + { + this.Player = player; + this.Added = changedItems + .Where(n => n.ChangeType == ChangeType.Added) + .Select(p => p.Item) + .ToArray(); + + this.Removed = changedItems + .Where(n => n.ChangeType == ChangeType.Removed) + .Select(p => p.Item) + .ToArray(); + + this.QuantityChanged = changedItems + .Where(n => n.ChangeType == ChangeType.StackChange) + .Select(change => new ItemStackSizeChange( + item: change.Item, + oldSize: change.Item.Stack - change.StackChange, + newSize: change.Item.Stack + )) + .ToArray(); + } + } +} diff --git a/src/SMAPI/Events/ItemStackSizeChange.cs b/src/SMAPI/Events/ItemStackSizeChange.cs new file mode 100644 index 00000000..35369be2 --- /dev/null +++ b/src/SMAPI/Events/ItemStackSizeChange.cs @@ -0,0 +1,35 @@ +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// An inventory item stack size change. + public class ItemStackSizeChange + { + /********* + ** Accessors + *********/ + /// The item whose stack size changed. + public Item Item { get; } + + /// The previous stack size. + public int OldSize { get; } + + /// The new stack size. + public int NewSize { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The item whose stack size changed. + /// The previous stack size. + /// The new stack size. + public ItemStackSizeChange(Item item, int oldSize, int newSize) + { + this.Item = item; + this.OldSize = oldSize; + this.NewSize = newSize; + } + } +} diff --git a/src/SMAPI/Events/LevelChangedEventArgs.cs b/src/SMAPI/Events/LevelChangedEventArgs.cs new file mode 100644 index 00000000..174094c7 --- /dev/null +++ b/src/SMAPI/Events/LevelChangedEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using StardewModdingAPI.Enums; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class LevelChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose skill level changed. + public Farmer Player { get; } + + /// The skill whose level changed. + public SkillType Skill { get; } + + /// The previous skill level. + public int OldLevel { get; } + + /// The new skill level. + public int NewLevel { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player whose skill level changed. + /// The skill whose level changed. + /// The previous skill level. + /// The new skill level. + public LevelChangedEventArgs(Farmer player, SkillType skill, int oldLevel, int newLevel) + { + this.Player = player; + this.Skill = skill; + this.OldLevel = oldLevel; + this.NewLevel = newLevel; + } + } +} diff --git a/src/SMAPI/Events/MenuChangedEventArgs.cs b/src/SMAPI/Events/MenuChangedEventArgs.cs new file mode 100644 index 00000000..e1c049a2 --- /dev/null +++ b/src/SMAPI/Events/MenuChangedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class MenuChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous menu. + public IClickableMenu OldMenu { get; } + + /// The current menu. + public IClickableMenu NewMenu { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous menu. + /// The current menu. + public MenuChangedEventArgs(IClickableMenu oldMenu, IClickableMenu newMenu) + { + this.OldMenu = oldMenu; + this.NewMenu = newMenu; + } + } +} diff --git a/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs b/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs new file mode 100644 index 00000000..efd4163b --- /dev/null +++ b/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedActiveMenuEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderedEventArgs.cs b/src/SMAPI/Events/RenderedEventArgs.cs new file mode 100644 index 00000000..d6341b19 --- /dev/null +++ b/src/SMAPI/Events/RenderedEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderedHudEventArgs.cs b/src/SMAPI/Events/RenderedHudEventArgs.cs new file mode 100644 index 00000000..46e89013 --- /dev/null +++ b/src/SMAPI/Events/RenderedHudEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedHudEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderedWorldEventArgs.cs b/src/SMAPI/Events/RenderedWorldEventArgs.cs new file mode 100644 index 00000000..56145381 --- /dev/null +++ b/src/SMAPI/Events/RenderedWorldEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedWorldEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs b/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs new file mode 100644 index 00000000..103f56df --- /dev/null +++ b/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingActiveMenuEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingEventArgs.cs b/src/SMAPI/Events/RenderingEventArgs.cs new file mode 100644 index 00000000..5acbef09 --- /dev/null +++ b/src/SMAPI/Events/RenderingEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingHudEventArgs.cs b/src/SMAPI/Events/RenderingHudEventArgs.cs new file mode 100644 index 00000000..84c96ecd --- /dev/null +++ b/src/SMAPI/Events/RenderingHudEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingHudEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingWorldEventArgs.cs b/src/SMAPI/Events/RenderingWorldEventArgs.cs new file mode 100644 index 00000000..d0d44789 --- /dev/null +++ b/src/SMAPI/Events/RenderingWorldEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingWorldEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/ReturnedToTitleEventArgs.cs b/src/SMAPI/Events/ReturnedToTitleEventArgs.cs new file mode 100644 index 00000000..96309cde --- /dev/null +++ b/src/SMAPI/Events/ReturnedToTitleEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ReturnedToTitleEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SpecialisedEvents.cs b/src/SMAPI/Events/SpecialisedEvents.cs index af84b397..bdf25ccb 100644 --- a/src/SMAPI/Events/SpecialisedEvents.cs +++ b/src/SMAPI/Events/SpecialisedEvents.cs @@ -3,7 +3,7 @@ using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { - /// Events serving specialised edge cases that shouldn't be used by most mod. + /// Events serving specialised edge cases that shouldn't be used by most mods. public static class SpecialisedEvents { /********* diff --git a/src/SMAPI/Events/TimeChangedEventArgs.cs b/src/SMAPI/Events/TimeChangedEventArgs.cs new file mode 100644 index 00000000..fd472092 --- /dev/null +++ b/src/SMAPI/Events/TimeChangedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class TimeChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous time of day in 24-hour notation (like 1600 for 4pm). The clock time resets when the player sleeps, so 2am (before sleeping) is 2600. + public int OldTime { get; } + + /// The current time of day in 24-hour notation (like 1600 for 4pm). The clock time resets when the player sleeps, so 2am (before sleeping) is 2600. + public int NewTime { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous time of day in 24-hour notation (like 1600 for 4pm). + /// The current time of day in 24-hour notation (like 1600 for 4pm). + public TimeChangedEventArgs(int oldTime, int newTime) + { + this.OldTime = oldTime; + this.NewTime = newTime; + } + } +} diff --git a/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs new file mode 100644 index 00000000..5638bdb7 --- /dev/null +++ b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UnvalidatedUpdateTickedEventArgs : 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 UnvalidatedUpdateTickedEventArgs(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/UnvalidatedUpdateTickingEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs new file mode 100644 index 00000000..ebadbb99 --- /dev/null +++ b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UnvalidatedUpdateTickingEventArgs : 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 UnvalidatedUpdateTickingEventArgs(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/WarpedEventArgs.cs b/src/SMAPI/Events/WarpedEventArgs.cs new file mode 100644 index 00000000..1b1c7381 --- /dev/null +++ b/src/SMAPI/Events/WarpedEventArgs.cs @@ -0,0 +1,37 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class WarpedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who warped to a new location. + public Farmer Player { get; } + + /// The player's previous location. + public GameLocation OldLocation { get; } + + /// The player's current location. + public GameLocation NewLocation { get; } + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who warped to a new location. + /// The player's previous location. + /// The player's current location. + public WarpedEventArgs(Farmer player, GameLocation oldLocation, GameLocation newLocation) + { + this.Player = player; + this.NewLocation = newLocation; + this.OldLocation = oldLocation; + } + } +} diff --git a/src/SMAPI/Events/WindowResizedEventArgs.cs b/src/SMAPI/Events/WindowResizedEventArgs.cs new file mode 100644 index 00000000..a990ba9d --- /dev/null +++ b/src/SMAPI/Events/WindowResizedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class WindowResizedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous window size. + public Point OldSize { get; } + + /// The current window size. + public Point NewSize { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous window size. + /// The current window size. + public WindowResizedEventArgs(Point oldSize, Point newSize) + { + this.OldSize = oldSize; + this.NewSize = newSize; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 616db7d9..31b0346a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -11,6 +11,39 @@ namespace StardewModdingAPI.Framework.Events /********* ** Events (new) *********/ + /**** + ** Display + ****/ + /// Raised after a game menu is opened, closed, or replaced. + public readonly ManagedEvent MenuChanged; + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + public readonly ManagedEvent Rendering; + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + public readonly ManagedEvent Rendered; + + /// Raised before the game world is drawn to the screen. + public readonly ManagedEvent RenderingWorld; + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. + public readonly ManagedEvent RenderedWorld; + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. + public readonly ManagedEvent RenderingActiveMenu; + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. + public readonly ManagedEvent RenderedActiveMenu; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. + public readonly ManagedEvent RenderingHud; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. + public readonly ManagedEvent RenderedHud; + + /// Raised after the game window is resized. + public readonly ManagedEvent WindowResized; + /**** ** Game loop ****/ @@ -44,6 +77,12 @@ namespace StardewModdingAPI.Framework.Events /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . public readonly ManagedEvent DayEnding; + /// Raised after the in-game clock time changes. + public readonly ManagedEvent TimeChanged; + + /// Raised after the game returns to the title screen. + public readonly ManagedEvent ReturnedToTitle; + /**** ** Input ****/ @@ -59,6 +98,18 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player scrolls the mouse wheel. public readonly ManagedEvent MouseWheelScrolled; + /**** + ** Player + ****/ + /// Raised after items are added or removed to a player's inventory. + public readonly ManagedEvent InventoryChanged; + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public readonly ManagedEvent LevelChanged; + + /// Raised after a player warps to a new location. + public readonly ManagedEvent Warped; + /**** ** World ****/ @@ -83,6 +134,15 @@ namespace StardewModdingAPI.Framework.Events /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent TerrainFeatureListChanged; + /**** + ** Specialised + ****/ + /// Raised before the game performs its overall update tick (≈60 times per second). See notes on . + public readonly ManagedEvent UnvalidatedUpdateTicking; + + /// Raised after the game performs its overall update tick (≈60 times per second). See notes on . + public readonly ManagedEvent UnvalidatedUpdateTicked; + /********* ** Events (old) @@ -285,6 +345,17 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); + this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); + this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); + this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); + this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); + this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); + this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); + this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); + this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); + this.WindowResized = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); + 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)); @@ -295,12 +366,18 @@ namespace StardewModdingAPI.Framework.Events this.SaveLoaded = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveLoaded)); this.DayStarted = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayStarted)); this.DayEnding = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayEnding)); + this.TimeChanged = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.TimeChanged)); + this.ReturnedToTitle = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.ReturnedToTitle)); this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); + this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); + this.Warped = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); + this.BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.DebrisListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged)); this.LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); @@ -309,6 +386,9 @@ namespace StardewModdingAPI.Framework.Events this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); + this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); + this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); + // init events (old) this.Legacy_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); diff --git a/src/SMAPI/Framework/Events/ModDisplayEvents.cs b/src/SMAPI/Framework/Events/ModDisplayEvents.cs new file mode 100644 index 00000000..e383eec6 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModDisplayEvents.cs @@ -0,0 +1,93 @@ +using System; +using StardewModdingAPI.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events related to UI and drawing to the screen. + internal class ModDisplayEvents : ModEventsBase, IDisplayEvents + { + /********* + ** Accessors + *********/ + /// Raised after a game menu is opened, closed, or replaced. + public event EventHandler MenuChanged + { + add => this.EventManager.MenuChanged.Add(value); + remove => this.EventManager.MenuChanged.Remove(value); + } + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + public event EventHandler Rendering + { + add => this.EventManager.Rendering.Add(value); + remove => this.EventManager.Rendering.Remove(value); + } + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + public event EventHandler Rendered + { + add => this.EventManager.Rendered.Add(value); + remove => this.EventManager.Rendered.Remove(value); + } + + /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. + public event EventHandler RenderingWorld + { + add => this.EventManager.RenderingWorld.Add(value); + remove => this.EventManager.RenderingWorld.Remove(value); + } + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. + public event EventHandler RenderedWorld + { + add => this.EventManager.RenderedWorld.Add(value); + remove => this.EventManager.RenderedWorld.Remove(value); + } + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. + public event EventHandler RenderingActiveMenu + { + add => this.EventManager.RenderingActiveMenu.Add(value); + remove => this.EventManager.RenderingActiveMenu.Remove(value); + } + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. + public event EventHandler RenderedActiveMenu + { + add => this.EventManager.RenderedActiveMenu.Add(value); + remove => this.EventManager.RenderedActiveMenu.Remove(value); + } + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. + public event EventHandler RenderingHud + { + add => this.EventManager.RenderingHud.Add(value); + remove => this.EventManager.RenderingHud.Remove(value); + } + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. + public event EventHandler RenderedHud + { + add => this.EventManager.RenderedHud.Add(value); + remove => this.EventManager.RenderedHud.Remove(value); + } + + /// Raised after the game window is resized. + public event EventHandler WindowResized + { + add => this.EventManager.WindowResized.Add(value); + remove => this.EventManager.WindowResized.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModDisplayEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 9e474457..7a318e8b 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -8,15 +8,24 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Events related to UI and drawing to the screen. + public IDisplayEvents Display { get; } + /// 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; } + /// Events raised when the player data changes. + public IPlayerEvents Player { get; } + /// Events raised when something changes in the world. public IWorldEvents World { get; } + /// Events serving specialised edge cases that shouldn't be used by most mods. + public ISpecialisedEvents Specialised { get; } + /********* ** Public methods @@ -26,9 +35,12 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event manager. public ModEvents(IModMetadata mod, EventManager eventManager) { + this.Display = new ModDisplayEvents(mod, eventManager); this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); + this.Player = new ModPlayerEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); + this.Specialised = new ModSpecialisedEvents(mod, eventManager); } } } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index d1def91b..a5beac99 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -79,6 +79,21 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.DayEnding.Remove(value); } + /// Raised after the in-game clock time changes. + public event EventHandler TimeChanged + { + + add => this.EventManager.TimeChanged.Add(value); + remove => this.EventManager.TimeChanged.Remove(value); + } + + /// Raised after the game returns to the title screen. + public event EventHandler ReturnedToTitle + { + add => this.EventManager.ReturnedToTitle.Add(value); + remove => this.EventManager.ReturnedToTitle.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/Events/ModPlayerEvents.cs b/src/SMAPI/Framework/Events/ModPlayerEvents.cs new file mode 100644 index 00000000..ca7cfd96 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModPlayerEvents.cs @@ -0,0 +1,43 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when the player data changes. + internal class ModPlayerEvents : ModEventsBase, IPlayerEvents + { + /********* + ** Accessors + *********/ + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player. + public event EventHandler InventoryChanged + { + add => this.EventManager.InventoryChanged.Add(value); + remove => this.EventManager.InventoryChanged.Remove(value); + } + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the local player. + public event EventHandler LevelChanged + { + add => this.EventManager.LevelChanged.Add(value); + remove => this.EventManager.LevelChanged.Remove(value); + } + + /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the local player. + public event EventHandler Warped + { + add => this.EventManager.Warped.Add(value); + remove => this.EventManager.Warped.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModPlayerEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs new file mode 100644 index 00000000..17c32bb8 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs @@ -0,0 +1,36 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mods. + internal class ModSpecialisedEvents : ModEventsBase, ISpecialisedEvents + { + /********* + ** Accessors + *********/ + /// 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 + { + add => this.EventManager.UnvalidatedUpdateTicking.Add(value); + remove => this.EventManager.UnvalidatedUpdateTicking.Remove(value); + } + + /// Raised after 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 UnvalidatedUpdateTicked + { + add => this.EventManager.UnvalidatedUpdateTicked.Add(value); + remove => this.EventManager.UnvalidatedUpdateTicked.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModSpecialisedEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index b51ff6a8..f52bfe2b 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -43,8 +43,8 @@ namespace StardewModdingAPI.Framework /**** ** ManagedEvent ****/ - /// Raise the event and notify all handlers. - /// The empty event arguments type to construct. + /// Raise the event using the default event args and notify all handlers. + /// The event args type to construct. /// The event to raise. public static void RaiseEmpty(this ManagedEvent @event) where TEventArgs : new() { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index bd266ce1..57f48d11 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -11,6 +11,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Netcode; +using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; @@ -265,7 +266,9 @@ namespace StardewModdingAPI.Framework // update tick are neglible and not worth the complications of bypassing Game1.Update. if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) { + this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); base.Update(gameTime); + this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed)); this.Events.Legacy_UnvalidatedUpdateTick.Raise(); return; } @@ -344,7 +347,9 @@ namespace StardewModdingAPI.Framework } // suppress non-save events + this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); base.Update(gameTime); + this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed)); this.Events.Legacy_UnvalidatedUpdateTick.Raise(); return; } @@ -406,6 +411,7 @@ namespace StardewModdingAPI.Framework if (wasWorldReady && !Context.IsWorldReady) { this.Monitor.Log("Context: returned to title", LogLevel.Trace); + this.Events.ReturnedToTitle.RaiseEmpty(); this.Events.Legacy_AfterReturnToTitle.Raise(); } else if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) @@ -441,6 +447,11 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + + Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; + Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue; + + this.Events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize)); this.Events.Legacy_Resize.Raise(); this.Watchers.WindowSizeWatcher.Reset(); } @@ -551,6 +562,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); // raise menu events + this.Events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); if (now != null) this.Events.Legacy_MenuChanged.Raise(new EventArgsClickableMenuChanged(was, now)); else @@ -673,6 +685,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); + this.Events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); this.Events.Legacy_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } else @@ -681,39 +694,45 @@ namespace StardewModdingAPI.Framework // raise player events if (raiseWorldEvents) { - PlayerTracker curPlayer = this.Watchers.CurrentPlayerTracker; + PlayerTracker playerTracker = this.Watchers.CurrentPlayerTracker; // raise current location changed - if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) + if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) { if (this.VerboseLogging) this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); - this.Events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(curPlayer.LocationWatcher.PreviousValue, newLocation)); + + GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; + this.Events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation)); + this.Events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(oldLocation, newLocation)); } // raise player leveled up a skill - foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) + foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) { if (this.VerboseLogging) this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); - this.Events.Legacy_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); + + this.Events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); + this.Events.Legacy_LeveledUp.Raise(new EventArgsLevelUp((EventArgsLevelUp.LevelType)pair.Key, pair.Value.CurrentValue)); } // raise player inventory changed - ItemStackChange[] changedItems = curPlayer.GetInventoryChanges().ToArray(); + ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); if (changedItems.Any()) { if (this.VerboseLogging) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); + this.Events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); + this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems)); } // raise mine level changed - if (curPlayer.TryGetNewMineLevel(out int mineLevel)) + if (playerTracker.TryGetNewMineLevel(out int mineLevel)) { if (this.VerboseLogging) this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); - this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); + this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(playerTracker.MineLevelWatcher.PreviousValue, mineLevel)); } } this.Watchers.CurrentPlayerTracker?.Reset(); @@ -728,6 +747,7 @@ namespace StardewModdingAPI.Framework this.TicksElapsed++; if (this.TicksElapsed == 1) this.Events.GameLaunched.Raise(new GameLaunchedEventArgs()); + this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); this.Events.UpdateTicking.Raise(new UpdateTickingEventArgs(this.TicksElapsed)); try { @@ -738,6 +758,7 @@ namespace StardewModdingAPI.Framework { this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } + this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed)); this.Events.UpdateTicked.Raise(new UpdateTickedEventArgs(this.TicksElapsed)); /********* @@ -844,10 +865,13 @@ namespace StardewModdingAPI.Framework if (activeClickableMenu != null) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); try { + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -855,7 +879,9 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); activeClickableMenu.exitThisMenu(); } - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); + Game1.spriteBatch.End(); } if (Game1.overlayMenu != null) @@ -872,11 +898,15 @@ namespace StardewModdingAPI.Framework if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + + this.Events.Rendering.RaiseEmpty(); try { Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -884,7 +914,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); Game1.activeClickableMenu.exitThisMenu(); } - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) @@ -904,10 +935,12 @@ namespace StardewModdingAPI.Framework else if (Game1.gameMode == (byte)11) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); Game1.spriteBatch.End(); } else if (Game1.currentMinigame != null) @@ -932,12 +965,15 @@ namespace StardewModdingAPI.Framework else if (Game1.showingEndOfNightStuff) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); if (Game1.activeClickableMenu != null) { try { + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -946,7 +982,7 @@ namespace StardewModdingAPI.Framework Game1.activeClickableMenu.exitThisMenu(); } } - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel == 1.0) @@ -960,6 +996,7 @@ namespace StardewModdingAPI.Framework else if (Game1.gameMode == (byte)6 || Game1.gameMode == (byte)3 && Game1.currentLocation == null) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); string str1 = ""; for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) str1 += "."; @@ -971,6 +1008,7 @@ namespace StardewModdingAPI.Framework int x = 64; int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1); + this.Events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) @@ -991,11 +1029,15 @@ namespace StardewModdingAPI.Framework } else { + byte batchOpens = 0; // used for rendering event + Microsoft.Xna.Framework.Rectangle rectangle; Viewport viewport; if (Game1.gameMode == (byte)0) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (++batchOpens == 1) + this.Events.Rendering.RaiseEmpty(); } else { @@ -1004,6 +1046,8 @@ namespace StardewModdingAPI.Framework this.GraphicsDevice.SetRenderTarget(Game1.lightmap); this.GraphicsDevice.Clear(Color.White * 0.0f); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (++batchOpens == 1) + this.Events.Rendering.RaiseEmpty(); Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.Name.StartsWith("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight)); for (int index = 0; index < Game1.currentLightSources.Count; ++index) { @@ -1017,6 +1061,9 @@ namespace StardewModdingAPI.Framework Game1.bloom.BeginDraw(); this.GraphicsDevice.Clear(this.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (++batchOpens == 1) + this.Events.Rendering.RaiseEmpty(); + this.Events.RenderingWorld.RaiseEmpty(); this.Events.Legacy_OnPreRenderEvent.Raise(); if (Game1.background != null) Game1.background.draw(Game1.spriteBatch); @@ -1328,8 +1375,10 @@ namespace StardewModdingAPI.Framework this.drawBillboard(); if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) { + this.Events.RenderingHud.RaiseEmpty(); this.Events.Legacy_OnPreRenderHudEvent.Raise(); this.drawHUD(); + this.Events.RenderedHud.RaiseEmpty(); this.Events.Legacy_OnPostRenderHudEvent.Raise(); } else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) @@ -1438,8 +1487,10 @@ namespace StardewModdingAPI.Framework { try { + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -1455,7 +1506,9 @@ namespace StardewModdingAPI.Framework string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1); } - this.RaisePostRender(); + this.Events.RenderedWorld.RaiseEmpty(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); this.renderScreenBuffer(); diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 59e80d8a..6a705e50 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; @@ -41,7 +42,7 @@ namespace StardewModdingAPI.Framework.StateTracking public IValueWatcher MineLevelWatcher { get; } /// Tracks changes to the player's skill levels. - public IDictionary> SkillWatchers { get; } + public IDictionary> SkillWatchers { get; } /********* @@ -58,14 +59,14 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); - this.SkillWatchers = new Dictionary> + this.SkillWatchers = new Dictionary> { - [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), - [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), - [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), - [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), - [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), - [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) + [SkillType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), + [SkillType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), + [SkillType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), + [SkillType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), + [SkillType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), + [SkillType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) }; // track watchers for convenience @@ -124,7 +125,7 @@ namespace StardewModdingAPI.Framework.StateTracking } /// Get the player skill levels which changed. - public IEnumerable>> GetChangedSkills() + public IEnumerable>> GetChangedSkills() { return this.SkillWatchers.Where(p => p.Value.IsChanged); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index efb85c52..d58aee56 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,32 +79,90 @@ Properties\GlobalAssemblyInfo.cs + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -121,9 +179,7 @@ - - @@ -132,6 +188,7 @@ + @@ -191,33 +248,10 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -256,12 +290,6 @@ - - - - - - @@ -278,8 +306,6 @@ - - -- cgit From 02a46bf13f29ce0dd8ac2f422113083c59dae42d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 3 Nov 2018 01:29:01 -0400 Subject: add APIs to send/receive messages in multiplayer (#480) --- docs/release-notes.md | 1 + src/SMAPI/Events/IModEvents.cs | 3 + src/SMAPI/Events/IMultiplayerEvents.cs | 11 + src/SMAPI/Events/ModMessageReceivedEventArgs.cs | 46 +++ src/SMAPI/Framework/Events/EventManager.cs | 8 + src/SMAPI/Framework/Events/ManagedEvent.cs | 24 ++ src/SMAPI/Framework/Events/ManagedEventBase.cs | 12 +- src/SMAPI/Framework/Events/ModEvents.cs | 4 + src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 29 ++ .../Framework/ModHelpers/MultiplayerHelper.cs | 31 +- src/SMAPI/Framework/Networking/ModMessageModel.cs | 72 +++++ src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 14 +- src/SMAPI/Framework/SCore.cs | 5 +- src/SMAPI/Framework/SGame.cs | 25 +- src/SMAPI/Framework/SMultiplayer.cs | 339 ++++++++++++++++----- src/SMAPI/IMultiplayerHelper.cs | 13 +- src/SMAPI/IMultiplayerPeer.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 22 +- 18 files changed, 545 insertions(+), 116 deletions(-) create mode 100644 src/SMAPI/Events/IMultiplayerEvents.cs create mode 100644 src/SMAPI/Events/ModMessageReceivedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModMultiplayerEvents.cs create mode 100644 src/SMAPI/Framework/Networking/ModMessageModel.cs (limited to 'src/SMAPI/Framework/Events') diff --git a/docs/release-notes.md b/docs/release-notes.md index cb83e7ec..2111e35e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -31,6 +31,7 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). + * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info. * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation for the non-developers version of SMAPI. * Added more events to the prototype `helper.Events` for SMAPI 3.0. diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 76da7751..bd7ab880 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Events /// Events raised when the player provides input using a controller, keyboard, or mouse. IInputEvents Input { get; } + /// Events raised for multiplayer messages and connections. + IMultiplayerEvents Multiplayer { get; } + /// Events raised when the player data changes. IPlayerEvents Player { get; } diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs new file mode 100644 index 00000000..a6ac6fd3 --- /dev/null +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -0,0 +1,11 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised for multiplayer messages and connections. + public interface IMultiplayerEvents + { + /// Raised after a mod message is received over the network. + event EventHandler ModMessageReceived; + } +} diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs new file mode 100644 index 00000000..b1960a22 --- /dev/null +++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs @@ -0,0 +1,46 @@ +using System; +using StardewModdingAPI.Framework.Networking; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a mod receives a message over the network. + public class ModMessageReceivedEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// The underlying message model. + private readonly ModMessageModel Message; + + + /********* + ** Accessors + *********/ + /// The unique ID of the player from whose computer the message was sent. + public long FromPlayerID => this.Message.FromPlayerID; + + /// The unique ID of the mod which sent the message. + public string FromModID => this.Message.FromModID; + + /// A message type which can be used to decide whether it's the one you want to handle, like SetPlayerLocation. This doesn't need to be globally unique, so mods should check the . + public string Type => this.Message.Type; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The received message. + internal ModMessageReceivedEventArgs(ModMessageModel message) + { + this.Message = message; + } + + /// Read the message data into the given model type. + /// The message model type. + public TModel ReadAs() + { + return this.Message.Data.ToObject(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 31b0346a..519cf48a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -98,6 +98,12 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player scrolls the mouse wheel. public readonly ManagedEvent MouseWheelScrolled; + /**** + ** Multiplayer + ****/ + /// Raised after a mod message is received over the network. + public readonly ManagedEvent ModMessageReceived; + /**** ** Player ****/ @@ -374,6 +380,8 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); this.Warped = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index c1ebf6c7..65f6e38e 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -67,6 +67,30 @@ namespace StardewModdingAPI.Framework.Events } } } + + /// Raise the event and notify all handlers. + /// The event arguments to pass. + /// A lambda which returns true if the event should be raised for the given mod. + public void RaiseForMods(TEventArgs args, Func match) + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + if (match(this.GetSourceMod(handler))) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } } /// An event wrapper which intercepts and logs errors in handler code. diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs index f3a278dc..defd903a 100644 --- a/src/SMAPI/Framework/Events/ManagedEventBase.cs +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -69,12 +69,22 @@ namespace StardewModdingAPI.Framework.Events this.SourceMods.Remove(handler); } + /// Get the mod which registered the given event handler, if available. + /// The event handler. + protected IModMetadata GetSourceMod(TEventHandler handler) + { + return this.SourceMods.TryGetValue(handler, out IModMetadata mod) + ? mod + : null; + } + /// 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)) + IModMetadata mod = this.GetSourceMod(handler); + if (mod != null) 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/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 7a318e8b..8ad3936c 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Events /// Events raised when the player provides input using a controller, keyboard, or mouse. public IInputEvents Input { get; } + /// Events raised for multiplayer messages and connections. + public IMultiplayerEvents Multiplayer { get; } + /// Events raised when the player data changes. public IPlayerEvents Player { get; } @@ -38,6 +41,7 @@ namespace StardewModdingAPI.Framework.Events this.Display = new ModDisplayEvents(mod, eventManager); this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); + this.Multiplayer = new ModMultiplayerEvents(mod, eventManager); this.Player = new ModPlayerEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); this.Specialised = new ModSpecialisedEvents(mod, eventManager); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs new file mode 100644 index 00000000..a830a54a --- /dev/null +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised for multiplayer messages and connections. + internal class ModMultiplayerEvents : ModEventsBase, IMultiplayerEvents + { + /********* + ** Accessors + *********/ + /// Raised after a mod message is received over the network. + public event EventHandler ModMessageReceived + { + add => this.EventManager.ModMessageReceived.Add(value); + remove => this.EventManager.ModMessageReceived.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModMultiplayerEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index 86f8e012..eedad0bc 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using StardewModdingAPI.Framework.Networking; using StardewValley; @@ -26,18 +27,18 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Multiplayer = multiplayer; } - /// Get the locations which are being actively synced from the host. - public IEnumerable GetActiveLocations() - { - return this.Multiplayer.activeLocations(); - } - /// Get a new multiplayer ID. public long GetNewID() { return this.Multiplayer.getNewID(); } + /// Get the locations which are being actively synced from the host. + public IEnumerable GetActiveLocations() + { + return this.Multiplayer.activeLocations(); + } + /// Get a connected player. /// The player's unique ID. /// Returns the connected player, or null if no such player is connected. @@ -53,5 +54,23 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.Multiplayer.Peers.Values; } + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + public void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null) + { + this.Multiplayer.BroadcastModMessage( + message: message, + messageType: messageType, + fromModID: this.ModID, + toModIDs: modIDs, + toPlayerIDs: playerIDs + ); + } } } diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs new file mode 100644 index 00000000..7ee39863 --- /dev/null +++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Framework.Networking +{ + /// The metadata for a mod message. + internal class ModMessageModel + { + /********* + ** Accessors + *********/ + /**** + ** Origin + ****/ + /// The unique ID of the player who broadcast the message. + public long FromPlayerID { get; set; } + + /// The unique ID of the mod which broadcast the message. + public string FromModID { get; set; } + + /**** + ** Destination + ****/ + /// The players who should receive the message, or null for all players. + public long[] ToPlayerIDs { get; set; } + + /// The mods which should receive the message, or null for all mods. + public string[] ToModIDs { get; set; } + + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + public string Type { get; set; } + + /// The custom mod data being broadcast. + public JToken Data { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModMessageModel() { } + + /// Construct an instance. + /// The unique ID of the player who broadcast the message. + /// The unique ID of the mod which broadcast the message. + /// The players who should receive the message, or null for all players. + /// The mods which should receive the message, or null for all mods. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The custom mod data being broadcast. + public ModMessageModel(long fromPlayerID, string fromModID, long[] toPlayerIDs, string[] toModIDs, string type, JToken data) + { + this.FromPlayerID = fromPlayerID; + this.FromModID = fromModID; + this.ToPlayerIDs = toPlayerIDs; + this.ToModIDs = toModIDs; + this.Type = type; + this.Data = data; + } + + /// Construct an instance. + /// The message to clone. + public ModMessageModel(ModMessageModel message) + { + this.FromPlayerID = message.FromPlayerID; + this.FromModID = message.FromModID; + this.ToPlayerIDs = message.ToPlayerIDs?.ToArray(); + this.ToModIDs = message.ToModIDs?.ToArray(); + this.Type = message.Type; + this.Data = message.Data; + } + } +} diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index e97e36bc..c7f8ffad 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.Networking public long PlayerID { get; } /// Whether this is a connection to the host player. - public bool IsHostPlayer => this.PlayerID == Game1.MasterPlayer.UniqueMultiplayerID; + public bool IsHost { get; } /// Whether the player has SMAPI installed. public bool HasSmapi => this.ApiVersion != null; @@ -57,9 +57,11 @@ namespace StardewModdingAPI.Framework.Networking /// The server through which to send messages. /// The server connection through which to send messages. /// The client through which to send messages. - public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client) + /// Whether this is a connection to the host player. + public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client, bool isHost) { this.PlayerID = playerID; + this.IsHost = isHost; if (model != null) { this.Platform = model.Platform; @@ -84,7 +86,8 @@ namespace StardewModdingAPI.Framework.Networking model: model, server: server, serverConnection: serverConnection, - client: null + client: null, + isHost: false ); } @@ -99,7 +102,8 @@ namespace StardewModdingAPI.Framework.Networking model: model, server: null, serverConnection: null, - client: client + client: client, + isHost: true ); } @@ -119,7 +123,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to send. public void SendMessage(OutgoingMessage message) { - if (this.IsHostPlayer) + if (this.IsHost) this.Client.sendMessage(message); else this.Server.SendMessage(this.ServerConnection, message); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 69b33699..ca343389 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -209,7 +209,7 @@ namespace StardewModdingAPI.Framework // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose, this.Settings.VerboseLogging); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -340,9 +340,6 @@ namespace StardewModdingAPI.Framework /// Initialise SMAPI and mods after the game starts. private void InitialiseAfterGameStart() { - // load settings - this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; - // load core components this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 6b19f538..c7f5962f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -51,6 +51,12 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// Whether SMAPI should log more information about the game context. + private readonly bool VerboseLogging; + /// 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 @@ -116,9 +122,6 @@ namespace StardewModdingAPI.Framework /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; - /// Whether SMAPI should log more information about the game context. - public bool VerboseLogging { get; set; } - /// A list of queued commands to execute. /// This property must be threadsafe, since it's accessed from a separate console input thread. public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); @@ -136,7 +139,8 @@ namespace StardewModdingAPI.Framework /// Tracks the installed mods. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting) + /// Whether SMAPI should log more information about the game context. + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting, bool verboseLogging) { SGame.ConstructorHack = null; @@ -151,11 +155,13 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor; this.MonitorForGame = monitorForGame; this.Events = eventManager; + this.ModRegistry = modRegistry; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; + this.VerboseLogging = verboseLogging; Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging, this.OnModMessageReceived); Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables @@ -191,6 +197,15 @@ namespace StardewModdingAPI.Framework this.Events.DayEnding.RaiseEmpty(); } + /// A callback invoked when a mod message is received. + /// The message to deliver to applicable mods. + private void OnModMessageReceived(ModMessageModel message) + { + // raise events for applicable mods + HashSet modIDs = new HashSet(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.InvariantCultureIgnoreCase); + this.Events.ModMessageReceived.RaiseForMods(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); + } + /// Constructor a content manager to read XNB files. /// The service provider to use to locate services. /// The root directory to search for content. diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index a151272e..e4f912d2 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using Lidgren.Network; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; @@ -38,6 +40,9 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI should log more detailed information. private readonly bool VerboseLogging; + /// A callback to invoke when a mod message is received. + private readonly Action OnModMessageReceived; + /********* ** Accessors @@ -45,9 +50,15 @@ namespace StardewModdingAPI.Framework /// The message ID for a SMAPI message containing context about a player. public const byte ContextSyncMessageID = 255; + /// The message ID for a mod message. + public const byte ModMessageID = 254; + /// The metadata for each connected peer. public IDictionary Peers { get; } = new Dictionary(); + /// The metadata for the host player, if the current player is a farmhand. + public MultiplayerPeer HostPeer; + /********* ** Public methods @@ -59,7 +70,8 @@ namespace StardewModdingAPI.Framework /// Tracks the installed mods. /// Simplifies access to private code. /// Whether SMAPI should log more detailed information. - public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging) + /// A callback to invoke when a mod message is received. + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging, Action onModMessageReceived) { this.Monitor = monitor; this.EventManager = eventManager; @@ -67,6 +79,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.Reflection = reflection; this.VerboseLogging = verboseLogging; + this.OnModMessageReceived = onModMessageReceived; this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); } @@ -113,7 +126,7 @@ namespace StardewModdingAPI.Framework return server; } - /// Process an incoming network message from an unknown farmhand. + /// Process an incoming network message from an unknown farmhand, usually a player whose connection hasn't been approved yet. /// The server instance that received the connection. /// The raw network message that was received. /// The message to process. @@ -123,69 +136,99 @@ namespace StardewModdingAPI.Framework if (!Game1.IsMasterGame) return; - // sync SMAPI context with connected instances - if (message.MessageType == SMultiplayer.ContextSyncMessageID) + switch (message.MessageType) { - // get server - if (!(server is SLidgrenServer customServer)) - { - this.Monitor.Log($"Received context from farmhand {message.FarmerID} via unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); - return; - } - - // parse message - string data = message.Reader.ReadString(); - RemoteContextModel model = this.JsonHelper.Deserialise(data); - if (model.ApiVersion == null) - model = null; // no data available for unmodded players - - // log info - if (model != null) - this.Monitor.Log($"Received context for farmhand {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - else - this.Monitor.Log($"Received context for farmhand {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - - // store peer - MultiplayerPeer newPeer = this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, customServer, rawMessage.SenderConnection); - - // reply with known contexts - if (this.VerboseLogging) - this.Monitor.Log(" Replying with context for current player...", LogLevel.Trace); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); - foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) - { - if (this.VerboseLogging) - this.Monitor.Log($" Replying with context for player {otherPeer.PlayerID}...", LogLevel.Trace); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); - } + // sync SMAPI context with connected instances + case SMultiplayer.ContextSyncMessageID: + { + // get server + if (!(server is SLidgrenServer customServer)) + { + this.Monitor.Log($"Received context from farmhand {message.FarmerID} via unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); + return; + } + + // parse message + string data = message.Reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + if (model.ApiVersion == null) + model = null; // no data available for unmodded players + + // log info + if (model != null) + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + else + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer newPeer = this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, customServer, rawMessage.SenderConnection); + + // reply with known contexts + this.VerboseLog(" Replying with context for current player..."); + newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); + foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + { + this.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); + newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); + } + + // forward to other peers + if (this.Peers.Count > 1) + { + object[] fields = this.GetContextSyncMessageFields(newPeer); + foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + { + this.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); + otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + } + } + } + break; - // forward to other peers - if (this.Peers.Count > 1) - { - object[] fields = this.GetContextSyncMessageFields(newPeer); - foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + // handle intro from unmodded player + case Multiplayer.playerIntroduction: + if (!this.Peers.ContainsKey(message.FarmerID)) { - if (this.VerboseLogging) - this.Monitor.Log($" Forwarding context to player {otherPeer.PlayerID}...", LogLevel.Trace); - otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + // get server + if (!(server is SLidgrenServer customServer)) + { + this.Monitor.Log($"Received connection from farmhand {message.FarmerID} with unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); + return; + } + + // store peer + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + var peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, customServer, rawMessage.SenderConnection); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; } - } + break; + + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + break; } + } - // handle intro from unmodded player - else if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) + /// Process an incoming message from an approved connection. + /// The message to process. + public override void processIncomingMessage(IncomingMessage message) + { + switch (message.MessageType) { - // get server - if (!(server is SLidgrenServer customServer)) - { - this.Monitor.Log($"Received connection from farmhand {message.FarmerID} with unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); - return; - } - - // store peer - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, customServer, rawMessage.SenderConnection); + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + break; + + // let game process message + default: + base.processIncomingMessage(message); + break; } + } /// Process an incoming network message from the server. @@ -194,33 +237,50 @@ namespace StardewModdingAPI.Framework /// Returns whether the message was handled. public bool TryProcessMessageFromServer(SLidgrenClient client, IncomingMessage message) { - // receive SMAPI context from a connected player - if (message.MessageType == SMultiplayer.ContextSyncMessageID) + switch (message.MessageType) { - // parse message - string data = message.Reader.ReadString(); - RemoteContextModel model = this.JsonHelper.Deserialise(data); - - // log info - if (model != null) - this.Monitor.Log($"Received context for {(model.IsHost ? "host" : "farmhand")} {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - else - this.Monitor.Log($"Received context for player {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - - // store peer - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); - return true; - } + // receive SMAPI context from a connected player + case SMultiplayer.ContextSyncMessageID: + { + // parse message + string data = message.Reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + + // log info + if (model != null) + this.Monitor.Log($"Received context for {(model.IsHost ? "host" : "farmhand")} {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + else + this.Monitor.Log($"Received context for player {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } + return true; - // handle intro from unmodded player - if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) - { - // store peer - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client); - } + // handle intro from unmodded player + case Multiplayer.playerIntroduction: + if (!this.Peers.ContainsKey(message.FarmerID)) + { + // store peer + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + var peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } + return false; + + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + return true; - return false; + default: + return false; + } } /// Remove players who are disconnecting. @@ -235,10 +295,117 @@ namespace StardewModdingAPI.Framework base.removeDisconnectedFarmers(); } + /// Broadcast a mod message to matching players. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The unique ID of the mod sending the message. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + public void BroadcastModMessage(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs) + { + // validate + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (string.IsNullOrWhiteSpace(messageType)) + throw new ArgumentNullException(nameof(messageType)); + if (string.IsNullOrWhiteSpace(fromModID)) + throw new ArgumentNullException(nameof(fromModID)); + if (!this.Peers.Any()) + { + this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); + return; + } + + // filter player IDs + HashSet playerIDs = null; + if (toPlayerIDs != null && toPlayerIDs.Any()) + { + playerIDs = new HashSet(toPlayerIDs); + playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id)); + if (!playerIDs.Any()) + { + this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); + return; + } + } + + // get data to send + ModMessageModel model = new ModMessageModel( + fromPlayerID: Game1.player.UniqueMultiplayerID, + fromModID: fromModID, + toModIDs: toModIDs, + toPlayerIDs: playerIDs?.ToArray(), + type: messageType, + data: JToken.FromObject(message) + ); + string data = JsonConvert.SerializeObject(model, Formatting.None); + + // log message + if (this.VerboseLogging) + this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace); + + // send message + if (Context.IsMainPlayer) + { + foreach (MultiplayerPeer peer in this.Peers.Values) + { + if (playerIDs == null || playerIDs.Contains(peer.PlayerID)) + { + model.ToPlayerIDs = new[] { peer.PlayerID }; + peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, data)); + } + } + } + else if (this.HostPeer != null && this.HostPeer.HasSmapi) + this.HostPeer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, this.HostPeer.PlayerID, data)); + else + this.VerboseLog(" Can't send message because no valid connections were found."); + + } + /********* ** Private methods *********/ + /// Receive a mod message sent from another player's mods. + /// The raw message to parse. + private void ReceiveModMessage(IncomingMessage message) + { + // parse message + string json = message.Reader.ReadString(); + ModMessageModel model = this.JsonHelper.Deserialise(json); + HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); + if (this.VerboseLogging) + this.Monitor.Log($"Received message: {json}."); + + // notify local mods + if (playerIDs.Contains(Game1.player.UniqueMultiplayerID)) + this.OnModMessageReceived(model); + + // forward to other players + if (Context.IsMainPlayer && playerIDs.Any(p => p != Game1.player.UniqueMultiplayerID)) + { + ModMessageModel newModel = new ModMessageModel(model); + foreach (long playerID in playerIDs) + { + if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) + { + newModel.ToPlayerIDs = new[] { peer.PlayerID }; + this.VerboseLog($" Forwarding message to player {peer.PlayerID}."); + peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); + } + } + } + } + + /// Get all connected player IDs, including the current player. + private IEnumerable GetKnownPlayerIDs() + { + yield return Game1.player.UniqueMultiplayerID; + foreach (long peerID in this.Peers.Keys) + yield return peerID; + } + /// Get the fields to include in a context sync message sent to other players. private object[] GetContextSyncMessageFields() { @@ -271,7 +438,7 @@ namespace StardewModdingAPI.Framework RemoteContextModel model = new RemoteContextModel { - IsHost = peer.IsHostPlayer, + IsHost = peer.IsHost, Platform = peer.Platform.Value, ApiVersion = peer.ApiVersion, GameVersion = peer.GameVersion, @@ -287,5 +454,13 @@ namespace StardewModdingAPI.Framework return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; } + + /// Log a trace message if is enabled. + /// The message to log. + private void VerboseLog(string message) + { + if (this.VerboseLogging) + this.Monitor.Log(message, LogLevel.Trace); + } } } diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs index b01a7bed..4067a676 100644 --- a/src/SMAPI/IMultiplayerHelper.cs +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using StardewValley; @@ -12,8 +13,6 @@ namespace StardewModdingAPI /// Get the locations which are being actively synced from the host. IEnumerable GetActiveLocations(); - /* disable until ready for release: - /// Get a connected player. /// The player's unique ID. /// Returns the connected player, or null if no such player is connected. @@ -21,6 +20,14 @@ namespace StardewModdingAPI /// Get all connected players. IEnumerable GetConnectedPlayers(); - */ + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null); } } diff --git a/src/SMAPI/IMultiplayerPeer.cs b/src/SMAPI/IMultiplayerPeer.cs index e314eba5..0d4d3261 100644 --- a/src/SMAPI/IMultiplayerPeer.cs +++ b/src/SMAPI/IMultiplayerPeer.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI long PlayerID { get; } /// Whether this is a connection to the host player. - bool IsHostPlayer { get; } + bool IsHost { get; } /// Whether the player has SMAPI installed. bool HasSmapi { get; } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 2fdf4d97..11fa5d35 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -116,6 +116,7 @@ + @@ -130,6 +131,7 @@ + @@ -160,10 +162,20 @@ - + + + + + + + + + + + @@ -183,15 +195,8 @@ - - - - - - - @@ -224,7 +229,6 @@ - -- cgit From 222265816d803e8e145c0a500568412d03dd49da Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 22:41:31 -0500 Subject: add ContextReceived event (#480) --- src/SMAPI/Events/ContextReceivedEventArgs.cs | 25 +++++++++++ src/SMAPI/Events/IMultiplayerEvents.cs | 3 ++ src/SMAPI/Framework/Events/EventManager.cs | 4 ++ src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 7 +++ src/SMAPI/Framework/SMultiplayer.cs | 50 +++++++++++++++------- src/SMAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Events/ContextReceivedEventArgs.cs (limited to 'src/SMAPI/Framework/Events') diff --git a/src/SMAPI/Events/ContextReceivedEventArgs.cs b/src/SMAPI/Events/ContextReceivedEventArgs.cs new file mode 100644 index 00000000..c715cf1c --- /dev/null +++ b/src/SMAPI/Events/ContextReceivedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ContextReceivedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player to whom a connection is being established. + internal ContextReceivedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs index a6ac6fd3..91e0789c 100644 --- a/src/SMAPI/Events/IMultiplayerEvents.cs +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -5,6 +5,9 @@ namespace StardewModdingAPI.Events /// Events raised for multiplayer messages and connections. public interface IMultiplayerEvents { + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + event EventHandler ContextReceived; + /// Raised after a mod message is received over the network. event EventHandler ModMessageReceived; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 519cf48a..63ac17ee 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -101,6 +101,9 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + public readonly ManagedEvent ContextReceived; + /// Raised after a mod message is received over the network. public readonly ManagedEvent ModMessageReceived; @@ -380,6 +383,7 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.ContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ContextReceived)); this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index a830a54a..432a92d3 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -16,6 +16,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ModMessageReceived.Remove(value); } + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + public event EventHandler ContextReceived + { + add => this.EventManager.ContextReceived.Add(value); + remove => this.EventManager.ContextReceived.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 70f1a89a..9f649639 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Lidgren.Network; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; @@ -186,7 +188,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); return; } - this.Peers[message.FarmerID] = newPeer; + this.AddPeer(newPeer, canBeHost: false, raiseEvent: false); // reply with own context this.VerboseLog(" Replying with host context..."); @@ -209,20 +211,23 @@ namespace StardewModdingAPI.Framework otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } } + + // raise event + this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(newPeer)); } break; // handle player intro case (byte)MessageType.PlayerIntroduction: + // store peer if new + if (!this.Peers.ContainsKey(message.FarmerID)) { - // get peer - if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) - { - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); - } - + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); + this.AddPeer(peer, canBeHost: false); } + + resume(); break; // handle mod message @@ -262,9 +267,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error); return; } - this.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; + this.AddPeer(peer, canBeHost: true); } break; @@ -275,7 +278,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true); + this.AddPeer(MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true), canBeHost: false); } resume(); break; @@ -289,10 +292,11 @@ namespace StardewModdingAPI.Framework { peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: this.HostPeer == null); this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; + this.AddPeer(peer, canBeHost: true); } + + resume(); + break; } // handle mod message @@ -390,6 +394,22 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// Save a received peer. + /// The peer to add. + /// Whether to track the peer as the host if applicable. + /// Whether to raise the event. + private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true) + { + // store + this.Peers[peer.PlayerID] = peer; + if (canBeHost && peer.IsHost) + this.HostPeer = peer; + + // raise event + if (raiseEvent) + this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(peer)); + } + /// Read the metadata context for a player. /// The stream reader. private RemoteContextModel ReadContext(BinaryReader reader) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index dfe55eb9..09ecddcd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ + -- cgit From b4a5b3829f0f738e5b7e05048068eaec9d2d01d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:07:10 -0500 Subject: add PeerDisconnected event (#480) --- src/SMAPI/Events/ContextReceivedEventArgs.cs | 25 --------------------- src/SMAPI/Events/IMultiplayerEvents.cs | 7 ++++-- src/SMAPI/Events/PeerContextReceivedEventArgs.cs | 25 +++++++++++++++++++++ src/SMAPI/Events/PeerDisconnectedEventArgs.cs | 25 +++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 10 ++++++--- src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 15 +++++++++---- src/SMAPI/Framework/SMultiplayer.cs | 26 ++++++++++++++++------ src/SMAPI/StardewModdingAPI.csproj | 3 ++- 8 files changed, 94 insertions(+), 42 deletions(-) delete mode 100644 src/SMAPI/Events/ContextReceivedEventArgs.cs create mode 100644 src/SMAPI/Events/PeerContextReceivedEventArgs.cs create mode 100644 src/SMAPI/Events/PeerDisconnectedEventArgs.cs (limited to 'src/SMAPI/Framework/Events') diff --git a/src/SMAPI/Events/ContextReceivedEventArgs.cs b/src/SMAPI/Events/ContextReceivedEventArgs.cs deleted file mode 100644 index c715cf1c..00000000 --- a/src/SMAPI/Events/ContextReceivedEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an event. - public class ContextReceivedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The player whose metadata was received. - public IMultiplayerPeer Peer { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player to whom a connection is being established. - internal ContextReceivedEventArgs(IMultiplayerPeer peer) - { - this.Peer = peer; - } - } -} diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs index 91e0789c..4a31f48e 100644 --- a/src/SMAPI/Events/IMultiplayerEvents.cs +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -5,10 +5,13 @@ namespace StardewModdingAPI.Events /// Events raised for multiplayer messages and connections. public interface IMultiplayerEvents { - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - event EventHandler ContextReceived; + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + event EventHandler PeerContextReceived; /// Raised after a mod message is received over the network. event EventHandler ModMessageReceived; + + /// Raised after the connection with a peer is severed. + event EventHandler PeerDisconnected; } } diff --git a/src/SMAPI/Events/PeerContextReceivedEventArgs.cs b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs new file mode 100644 index 00000000..151a295c --- /dev/null +++ b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerContextReceivedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer whose metadata was received. + internal PeerContextReceivedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Events/PeerDisconnectedEventArgs.cs b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs new file mode 100644 index 00000000..8517988a --- /dev/null +++ b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerDisconnectedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer who disconnected. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer who disconnected. + internal PeerDisconnectedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 63ac17ee..b9d1c453 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -101,12 +101,15 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - public readonly ManagedEvent ContextReceived; + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public readonly ManagedEvent PeerContextReceived; /// Raised after a mod message is received over the network. public readonly ManagedEvent ModMessageReceived; + /// Raised after the connection with a peer is severed. + public readonly ManagedEvent PeerDisconnected; + /**** ** Player ****/ @@ -383,8 +386,9 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); - this.ContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ContextReceived)); + this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.PeerDisconnected = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 432a92d3..152c4e0c 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -9,6 +9,13 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public event EventHandler PeerContextReceived + { + add => this.EventManager.PeerContextReceived.Add(value); + remove => this.EventManager.PeerContextReceived.Remove(value); + } + /// Raised after a mod message is received over the network. public event EventHandler ModMessageReceived { @@ -16,11 +23,11 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ModMessageReceived.Remove(value); } - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - public event EventHandler ContextReceived + /// Raised after the connection with a peer is severed. + public event EventHandler PeerDisconnected { - add => this.EventManager.ContextReceived.Add(value); - remove => this.EventManager.ContextReceived.Remove(value); + add => this.EventManager.PeerDisconnected.Add(value); + remove => this.EventManager.PeerDisconnected.Remove(value); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 9f649639..2d0f8b9b 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -140,6 +140,9 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"SERVER SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + resume(); } @@ -149,6 +152,9 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + switch (message.MessageType) { // sync mod context (step 1) @@ -171,6 +177,8 @@ namespace StardewModdingAPI.Framework /// Process the message using the game's default logic. public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) { @@ -213,7 +221,7 @@ namespace StardewModdingAPI.Framework } // raise event - this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(newPeer)); + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer)); } break; @@ -248,8 +256,8 @@ namespace StardewModdingAPI.Framework /// Returns whether the message was handled. public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) { - if (message.MessageType != Multiplayer.farmerDelta && message.MessageType != Multiplayer.locationDelta && message.MessageType != Multiplayer.teamDelta && message.MessageType != Multiplayer.worldDelta) - this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Alert); + if (this.VerboseLogging) + this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) { @@ -315,8 +323,12 @@ namespace StardewModdingAPI.Framework { foreach (long playerID in this.DisconnectingFarmers) { - this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); - this.Peers.Remove(playerID); + if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) + { + this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); + this.Peers.Remove(playerID); + this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer)); + } } base.removeDisconnectedFarmers(); @@ -397,7 +409,7 @@ namespace StardewModdingAPI.Framework /// Save a received peer. /// The peer to add. /// Whether to track the peer as the host if applicable. - /// Whether to raise the event. + /// Whether to raise the event. private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true) { // store @@ -407,7 +419,7 @@ namespace StardewModdingAPI.Framework // raise event if (raiseEvent) - this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(peer)); + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer)); } /// Read the metadata context for a player. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 09ecddcd..8c12c1f9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,7 +85,6 @@ - @@ -137,6 +136,8 @@ + + -- cgit