Modding:Modder Guide/APIs/Events

From Stardew Valley Wiki
< Modding:Modder Guide‎ | APIs
Revision as of 18:48, 31 March 2020 by Kdau (talk | contribs) (→‎Player: make arg name format consistent)
Jump to navigation Jump to search

Creating SMAPI mods SMAPI mascot.png


Modding:Index

SMAPI provides several C# events which let your mod respond when something happens (like when the player places an object) or run code periodically (like once per update tick).

FAQs

What are events?

Events let you run code when something happens. You can add any number of event handlers (methods to call) when an event (what happened) is raised. You can think of events and handlers as a when..then statement:

WHEN the save is loaded,   <-- event
THEN run my code           <-- event handler

See Events in the C# programming guide for more info.

How do I use them?

Event handlers are usually added in your Entry method, though you can add and remove them anytime. For example, let's say you want to print a message when each day starts. First you would choose the appropriate event from the list below (GameLoop.DayStarted), then add an event handler, and do something in your method code:

/// <summary>The main entry point for the mod.</summary>
public class ModEntry : Mod
{
    /**********
    ** Public methods
    *********/
    /// <summary>The mod entry point, called after the mod is first loaded.</summary>
    /// <param name="helper">Provides simplified APIs for writing mods.</param>
    public override void Entry(IModHelper helper)
    {
        // event += method to call
        helper.Events.GameLoop.DayStarted += this.OnDayStarted;
    }

    /// <summary>The method called after a new day starts.</summary>
    /// <param name="sender">The event sender.</param>
    /// <param name="e">The event arguments.</param>
    private void OnDayStarted(object sender, DayStartedEventArgs e)
    {
       this.Monitor.Log("A new day dawns!");
    }
}

Tip: you don't need to memorise the method arguments. In Visual Studio, type helper.Events.GameLoop.SaveLoaded += and press TAB to auto-create a method. (There doesn't seem to be an equivalent feature in MonoDevelop.)

How do events fit into the game?

Events are raised on each game tick (when the game updates its state and renders to the screen), which is 60 times per second. An event may be raised multiple times (e.g. if the player pressed two keys simultaneously), but most events won't be raised 60 times per second (e.g. players are unlikely to be pressing 60 buttons per second).

Your event handlers are run synchronously: the game is paused and no other mod's code will run simultaneously, so there's no risk of conflicting changes. Since code runs very quickly, players won't notice any delay unless your code is unusually slow. That said, when using frequent events like UpdateTicked or Rendered, you should cache expensive operations (like loading an asset) instead of repeating them in each tick to avoid impacting performance.

What if a mod changes what the event was raised for?

Events are raised based on a snapshot of the game state. That's usually but not necessarily the current game state.

For example, consider this case:

  1. The GameMenu opens.
  2. SMAPI raises the MenuChanged event, which mods A and B listen to.
  3. Mod A receives the event and closes the menu.
  4. Mod B receives the event.

Each mod is still handling the MenuChanged event for the opened menu, even though the first mod closed it. SMAPI will raise a new MenuChanged event for the closed menu on the next tick.

This rarely affects mods, but it's something to keep in mind if you need the current state (e.g. check Game1.activeClickableMenu instead of e.NewMenu).

Events

The available events are documented below.

Display

this.Helper.Events.Display has events linked to UI and drawing to the screen.

event summary
#MenuChanged Raised after a game menu is opened, closed, or replaced.

Event arguments:

event arg type description
e.NewMenu IClickableMenu The new menu instance (or null if none).
e.OldMenu IClickableMenu The old menu instance (or null if none).
#Rendering 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#Rendered 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#RenderingWorld 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#RenderedWorld 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#RenderingActiveMenu When a menu is open (Game1.activeClickableMenu != 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#RenderedActiveMenu When a menu is open (Game1.activeClickableMenu != 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#RenderingHud 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#RenderedHud 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 arguments:

event arg type description
e.SpriteBatch SpriteBatch The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch.
#WindowResized Raised after the game window is resized.

Event arguments:

event arg type description
e.OldSize Point The previous window width (e.OldSize.X) and height (e.OldSize.Y).
e.NewSize Point The new window width (e.NewSize.X) and height (e.NewSize.Y).

Game loop

this.Helper.Events.GameLoop has 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 are often useful, but you should consider semantic events like Input where applicable.

event summary
#GameLaunched Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialised at this point, so this is a good time to set up mod integrations.
#UpdateTicking
UpdateTicked
Raised before/after the game state is updated (≈60 times per second).

Event arguments:

event arg type description
e.Ticks int The number of ticks elapsed since the game started, including the current tick.
e.IsOneSecond bool Whether e.TicksElapsed is a multiple of 60, which happens approximately once per second.
e.IsMultipleOf(int number) method returns bool Whether e.TicksElapsed is a multiple of the given number. This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second).
#OneSecondUpdateTicking
OneSecondUpdateTicked
Raised before/after the game state is updated, once per second.

Event arguments:

event arg type description
e.Ticks int The number of ticks elapsed since the game started, including the current tick.
e.IsMultipleOf(int number) method returns bool Whether e.TicksElapsed is a multiple of the given number. This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(120) for every two seconds).
#SaveCreating
SaveCreated
Raised before/after the game creates the save file (after the new-game intro). The save won't be written until all mods have finished handling this event. This is a somewhat specialised event, since the world isn't fully initialised at this point; in most cases you should use DayStarted, Saving, Saved instead.
#Saving
Saved
Raised before/after the game writes data to save file (except the initial save creation). The save won't be written until all mods have finished handling this event.
#SaveLoaded Raised before/after the game reads data from a save file and initialises the world (including when day one starts on a new save). This event is not raised after saving; if you want to do something at the start of each day, see DayStarted instead. Context.IsWorldReady is guaranteed to be true at this point.
#DayStarted Raised after a new in-game day starts. Everything has already been initialised at this point. (To run code before the game sets up the day, see DayEnding instead.)
#DayEnding Raised before the game ends the current day. This happens before it starts setting up the next day and before Saving.
#TimeChanged Raised after the in-game clock time changes, which happens in intervals of ten in-game minutes.

Event arguments:

event arg type description
e.OldTime int 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.
e.NewTime int 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.
#ReturnedToTitle Raised after the game returns to the title screen.

Input

this.Helper.Events.Input has events raised when the player uses a controller, keyboard, or mouse in some way. They can be used with the input API to access more info or suppress input.

event summary
#ButtonPressed
ButtonReleased
Raised after the player pressed/released a keyboard, mouse, or controller button. This includes mouse clicks.

Event arguments:

event arg type description
e.Button SButton The button pressed or released.
e.Cursor ICursorPosition The cursor position and grab tile.
e.IsDown method returns bool Indicates whether a given button is currently pressed.
e.IsSuppressed method returns bool A method which indicates whether a given button was suppressed by a mod, so the game itself won't see it. Note: mods won't receive input sent to the chatbox.
#CursorMoved Raised after the player moves the in-game cursor.

Event arguments:

event arg type description
e.OldPosition ICursorPosition The previous cursor position and grab tile.
e.NewPosition ICursorPosition The current cursor position and grab tile.
#MouseWheelScrolled Raised after the player scrolls the mouse wheel.

Event arguments:

event arg type description
e.Position ICursorPosition The current cursor position and grab tile.
e.Delta int The amount by which the mouse wheel was scrolled since the last update.
e.OldValue
e.NewValue
int The previous and current scroll value, cumulative since the game started. Mods should generally use e.Delta instead.

Multiplayer

this.Helper.Events.Multiplayer has events raised for multiplayer messages and connections.

event summary
#PeerContextReceived Raised after the mod context for a player is received. The event is raised for any player (whether host or farmhand), including when the connecting player doesn't have SMAPI installed. This is the earliest point where messages can be sent to the player via SMAPI.

This happens immediately before the game approves the connection, so the player doesn't exist in the game yet. When connecting to the host, contextual fields like Game1.IsMasterGame or Context.IsMultiplayer may not be set yet; you can check e.Peer.IsHost to know whether the current player is a farmhand, since the host context will always be received first. Assuming another mod doesn't block the connection, the connection will be approved on the next tick.

Event arguments:

event arg type description
e.Peer IMultiplayerPeer The peer whose context was received (see Multiplayer#Get connected player info).
#ModMessageReceived Raised after a mod message is received over the network.

Event arguments:

event arg type description
e.FromPlayerID long The unique ID of the player from whose computer the message was sent.
e.FromModID string The unique ID of the mod which sent the message.
e.Type string A message type which you can use to decide whether it's the one you want to handle. This isn't necessarily globally unique, so you should also check the FromModID field.
e.ReadAs<TModel>() method returns TModel Read the underlying message data into the given model type (like e.ReadAs<MyMessageClass>() or e.ReadAs<string>()). This will return a new instance each time.
#PeerDisconnected Raised after the connection to a player is severed.

Event arguments:

event arg type description
e.Peer IMultiplayerPeer The peer whose connection was severed (see Multiplayer#Get connected player info).

Player

this.Helper.Events.Player has events raised when the player data changes.

Currently these events are only raised for the current player. That will likely change in a future version, so make sure to check e.IsLocalPlayer if you only want to handle the current player.

event summary
#InventoryChanged Raised after items are added or removed from the player inventory.

Event arguments:

event arg type description
e.Player Farmer The player whose inventory changed.
e.Added IEnumerable<Item> The added item stacks.
e.Removed IEnumerable<Item> The removed item stacks.
e.QuantityChanged IEnumerable<ItemStackSizeChange> The item stacks whose quantity changed. Each ItemStackSizeChange instance includes Item (the affected item stack), OldSize (the previous stack size), and NewSize (the new stack size).
e.IsLocalPlayer bool Whether the affected player is the local one.
#LevelChanged Raised after a player's skill level changes. When the player levels up normally, this is raised immediately (not when the game notifies the player after they go to bed).

Event arguments:

event arg type description
e.Player Farmer The player whose skill level changed.
e.Skill SkillType The skill whose level changed. This is a constant like SkillType.Combat, which can be converted to the game's internal skill ID using (int)e.Skill.
e.OldLevel int The player's previous level for that skill.
e.NewLevel int The player's new level for that skill.
e.IsLocalPlayer bool Whether the affected player is the local one.
#Warped Raised after the current player moves to a new location.

Event arguments:

event arg type description
e.Player Farmer The player who warped to a new location.
e.OldLocation GameLocation The player's previous location.
e.NewLocation GameLocation The player's new location.
e.IsLocalPlayer bool Whether the affected player is the local one.

World

this.Helper.Events.World has events raised when the in-game world changes in some way.

event summary
#LocationListChanged Raised after a game location is added or removed (including building interiors).

Event arguments:

event arg type description
e.Added IEnumerable<GameLocation> A list of locations added since the last update tick.
e.Removed IEnumerable<GameLocation> A list of locations removed since the last update tick.
#BuildingListChanged Raised after buildings are added/removed in any location.

This event isn't raised for buildings already present when a location is added. If you need to handle those too, use LocationListChanged and check e.Added.OfType<BuildableGameLocation>()buildings.

Event arguments:

event arg type description
e.Location GameLocation The location which changed.
e.Added IEnumerable<Building> A list of buildings added since the last update tick.
e.Removed IEnumerable<Building> A list of buildings removed since the last update tick.
e.IsCurrentLocation bool Whether e.Location is the location containing the local player.
#ChestInventoryChanged Raised after items are added or removed from a chest's inventory.

Event arguments:

event arg type description
e.Chest Chest The chest whose inventory changed.
e.Location Location The location containing the chest.
e.Added IEnumerable<Item> The added item stacks.
e.Removed IEnumerable<Item> The removed item stacks.
e.QuantityChanged IEnumerable<ItemStackSizeChange> The item stacks whose quantity changed. Each ItemStackSizeChange instance includes Item (the affected item stack), OldSize (the previous stack size), and NewSize (the new stack size).
#DebrisListChanged Raised after debris is added/removed in any location (including dropped or spawned floating items).

This event isn't raised for debris already present when a location is added. If you need to handle those too, use LocationListChanged and check e.Addeddebris.

Event arguments:

event arg type description
e.Location GameLocation The location which changed.
e.Added IEnumerable<Debris> A list of debris added since the last update tick.
e.Removed IEnumerable<Debris> A list of debris removed since the last update tick.
e.IsCurrentLocation bool Whether e.Location is the location containing the local player.
#LargeTerrainFeatureListChanged Raised after large terrain features (like bushes) are added/removed in any location.

This event isn't raised for large terrain features already present when a location is added. If you need to handle those too, use LocationListChanged and check e.AddedlargeTerrainFeatures.

Event arguments:

event arg type description
e.Location GameLocation The location which changed.
e.Added IEnumerable<LargeTerrainFeature> A list of large terrain features added since the last update tick.
e.Removed IEnumerable<LargeTerrainFeature> A list of large terrain features removed since the last update tick.
e.IsCurrentLocation bool Whether e.Location is the location containing the local player.
#NpcListChanged Raised after NPCs are added/removed in any location (including villagers, horses, Junimos, monsters, and pets).

This event isn't raised for characters already present when a location is added. If you need to handle those too, use LocationListChanged and check e.Addedcharacters.

Event arguments:

event arg type description
e.Location GameLocation The location which changed.
e.Added IEnumerable<NPC> A list of NPCs added since the last update tick.
e.Removed IEnumerable<NPC> A list of NPCs removed since the last update tick.
e.IsCurrentLocation bool Whether e.Location is the location containing the local player.
#ObjectListChanged Raised after objects are added/removed in any location (including machines, furniture, fences, etc). For floating items, see DebrisListChanged.

This event isn't raised for objects already present when a location is added. If you need to handle those too, use LocationListChanged and check e.Addedobjects.

Event arguments:

event arg type description
e.Location GameLocation The location which changed.
e.Added IEnumerable<KeyValuePair<Vector2, Object>> A list of tile coordinate + object pairs added since the last update tick.
e.Removed IEnumerable<KeyValuePair<Vector2, Object>> A list of tile coordinate + object pairs removed since the last update tick.
e.IsCurrentLocation bool Whether e.Location is the location containing the local player.
#TerrainFeatureListChanged Raised after terrain features are added/removed in any location (including trees, hoed dirt, and flooring). For bushes, see LargeTerrainFeatureListChanged.

This event isn't raised for terrain features already present when a location is added. If you need to handle those too, use LocationListChanged and check e.AddedterrainFeatures.

Event arguments:

event arg type description
e.Location GameLocation The location which changed.
e.Added IEnumerable<KeyValuePair<Vector2, TerrainFeature>> A list of tile coordinate + terrain feature pairs added since the last update tick.
e.Removed IEnumerable<KeyValuePair<Vector2, TerrainFeature>> A list of tile coordinate + terrain feature pairs removed since the last update tick.
e.IsCurrentLocation bool Whether e.Location is the location containing the local player.

Specialised

this.Helper.Events.Specialised has events for specialised edge cases. These shouldn't be used by most mods.

event summary
#LoadStageChanged Raised when the low-level stage in the game's loading process has changed, for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use the game loop events instead.

Event arguments:

event arg type description
e.NewStage LoadStage The new load stage. The possible values are...
  • None: a save is not loaded or loading. (For example, the player is on the title screen.)
  • CreatedBasicInfo: the game is creating a new save slot, and has initialised the basic save info.
  • CreatedLocations: the game is creating a new save slot, and has initialised the in-game locations.
  • CreatedSaveFile: the game is creating a new save slot, and has created the physical save files.
  • SaveParsed: the game is loading a save slot, and has read the raw save data into StardewValley.SaveGame.loaded. Not applicable when connecting to a multiplayer host. This is equivalent to StardewValley.SaveGame.getLoadEnumerator value 20.
  • SaveLoadedBasicInfo: the game is loading a save slot, and has applied the basic save info (including player data). Not applicable when connecting to a multiplayer host. Note that some basic info (like daily luck) is not initialised at this point. This is equivalent to StardewValley.SaveGame.getLoadEnumerator value 36.
  • SaveLoadedLocations: the game is loading a save slot, and has applied the in-game location data. Not applicable when connecting to a multiplayer host. This is equivalent to StardewValley.SaveGame.getLoadEnumerator value 50.
  • Preloaded: the final metadata has been loaded from the save file. This happens before the game applies problem fixes, checks for achievements, starts music, etc. Not applicable when connecting to a multiplayer host.
  • Loaded: the save is fully loaded, but the world may not be fully initialised yet.
  • Ready: the save is fully loaded, the world has been initialised, and Context.IsWorldReady is now true.
e.OldStage LoadStage The previous load stage. See comments for e.NewStage.
#UnvalidatedUpdateTicking
UnvalidatedUpdateTicked
Raised before/after 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. Using this event will trigger a warning in the SMAPI console.

Event arguments:

event arg type description
e.Ticks int The number of ticks elapsed since the game started, including the current tick.
e.IsOneSecond bool Whether e.TicksElapsed is a multiple of 60, which happens approximately once per second.
e.IsMultipleOf(int number) method returns bool Whether e.TicksElapsed is a multiple of the given number. This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second).