Modding:Modder Guide/APIs

From Stardew Valley Wiki
< Modding:Modder Guide
Revision as of 22:17, 18 June 2017 by Pathoschild (talk | contribs) (→‎Manifest: + dependency minimum version)
Jump to navigation Jump to search

Index

SMAPI simplifies mod development by providing APIs and events you can use, which are documented below. See Modding:Creating a SMAPI mod for a guide to creating a new SMAPI mod.

Manifest

Each SMAPI mod has a manifest.json file with metadata about it. The mod can read its own metadata using this.ModManifest, and read metadata for loaded mods using the mod registry.

Basic fields
field description
Name The mod name. SMAPI uses this in player messages, logs, and errors. Example:
"Name": "Lookup Anything"
Author The name of the person who created the mod. Ideally this should include the username used to publish mods. Example:
"Author": "Pathoschild"
Version The mod's semantic version. Make sure you update this for each release! (The Build field is a semantic version label, like beta.2 in 1.0-beta.2.) SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game). Example:
"Version": {
   "MajorVersion": 1,
   "MinorVersion": 0,
   "PatchVersion": 0,
   "Build": "beta.2"
}
Description A short explanation of what your mod does (one or two sentences), shown in the SMAPI log. Example:
"Description": "View metadata about anything by pressing a button."
UniqueID A unique identifier for your mod. The recommended format is <your name>.<mod name>, with no spaces or special characters. SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game). When another mod needs to reference this mod, it uses the unique ID. Example:
"UniqueID": "Pathoschild.LookupAnything"
EntryDll The name of the mod's compiled DLL in the mod folder. Example:
"EntryDll": "LookupAnything.dll"
Optional fields
field description
MinimumApiVersion The minimum SMAPI version needed to use this mod. If a player tries to use the mod with an older SMAPI version, they'll see a friendly message saying they need to update SMAPI. This also serves as a proxy for the minimum game version, since SMAPI itself enforces a minimum game version. Example:
"MinimumApiVersion": "1.10"
Dependencies The other mods required to use this mod. If a player tries to use the mod and the listed mods aren't installed, they'll see a friendly message saying they need to install those. Example:
"Dependencies": [
   {
      "UniqueID": "Entoarox.Framework"
   }
]

You can optionally specify a minimum version to require (in the upcoming SMAPI 1.15+ only):

"Dependencies": [
   {
      "UniqueID": "Entoarox.Framework",
      "MinimumVersion": "1.7.9"
   }
]
anything else Any other fields will be stored in the IManifest.ExtraFields dictionary, which is available through the mod registry. Extra fields are ignored by SMAPI, but may be useful for extended metadata intended for other mods.

Events

SMAPI publishes several C# events that tell you when something happens. For example, if you want to do something after the player loads their save, you can add this to your Entry method:

SaveEvents.AfterLoad += this.SaveEvents_AfterLoad;

Then declare a method like this. (The EventArgs e argument contains any extra data about the event.)

/// <summary>The method called after the player loads their save.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
public void SaveEvents_AfterLoad(object sender, EventArgs e)
{
   this.Monitor.Log("The player loaded their game! This is a good time to do things.");
   this.Monitor.Log("Everything in the world is ready to interact with at this point.");
}

The available events are documented below.

Content events

ContentEvents are raised when the game loads content from its XNB files or changes locale.

event summary
AfterLocaleChanged Raised after the content language changes.

Control events

ControlEvents are raised when the player uses a controller, keyboard, or mouse. They're raised before the game handles the input, so it's possible to selectively prevent the game from responding to it. (That's beyond the scope of this guide, but it involves overwriting Game1.oldKBState, Game1.oldMouseState, and Game1.oldPadState.)

Most of these events are split into two variants, XPressed and XReleased. The Pressed variant is raised when the player presses the button (holding the button down only triggers the event once), and the Released variant is raised when they release it.

event summary
ControllerButtonPressed
ControllerButtonReleased
Raised after the player pressed/released a button on a gamepad or controller. These events aren't raised for trigger buttons.
ControllerTriggerPressed
ControllerTriggerReleased
Raised after the player pressed/released a trigger button on a gamepad or controller.
KeyPressed
KeyReleased
Raised after the player pressed/released a keyboard key.
KeyboardChanged Raised after the game's KeyboardState changed. That happens when the player presses or releases a key.
MouseChanged Raised after the game's MouseState changed. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button.

Game events

GameEvents are raised when the game changes state.

event summary
UpdateTick Raised when the game updates its state (≈60 times per second).
SecondUpdateTick Raised every other tick (≈30 times per second).
FourthUpdateTick Raised every fourth tick (≈15 times per second).
EighthUpdateTick Raised every eighth tick (≈8 times per second).
QuarterSecondTick Raised every 15th tick (≈4 times per second).
HalfSecondTick Raised every 30th tick (≈twice per second).
OneSecondTick Raised every 60th tick (≈once per second).

Graphics events

GraphicsEvents are raised during the game's draw loop, when the game is rendering content to the window.

event summary
OnPreRenderEvent
OnPostRenderEvent
Raised before and after drawing the world to the screen.
OnPreRenderGuiEvent
OnPostRenderGuiEvent
When a menu is open (Game1.activeClickableMenu != null), raised before and after drawing that menu to the screen. This includes the game's internal menus like the title screen.
OnPreRenderHudEvent
OnPostRenderHudEvent
Raised before and 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 called even if a menu is open.)
Resize Raised after the game window is resized.

Location events

LocationEvents are raised when the player transitions between game locations, a location is added or removed, or the objects in the current location change.

event summary
CurrentLocationChanged Raised after the player warps to a new location. Handlers are given the previous and new locations as arguments.
LocationObjectsChanged Raised after the list of objects in the current location changes (e.g. an object is added or removed). Handlers are given the new list of objects as an argument.
LocationsChanged Raised after a game location is added or removed. Handlers are passed the new list of locations as an argument.

Menu events

MenuEvents are raised when a game menu is opened or closed (including internal menus like the title screen).

event summary
MenuChanged Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. Handlers are given the previous menu (if any) and new menu (if any).
MenuClosed Raised after a game menu is closed. Handlers are given the previous menu.

Mine events

MineEvents are raised when something happens in The Mines.

event summary
MineLevelChanged Raised after the player warps to a new level of the mine. Handlers are passed the previous and new mine level as arguments.

Player events

PlayerEvents are raised when the player data changes.

event summary
InventoryChanged Raised after the player's inventory changes in any way (added or removed item, sorted, etc).
LeveledUp 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.

Save events

SaveEvents are raised when the player saves or loads the game.

event summary
AfterLoad Raised after the player loads a saved game. The world is ready for mods to modify at this point.
BeforeSave Raised before the game updates the save file. (The save won't be written until all mods have finished handling this event.)
AfterSave Raised after the game finishes updating the save file.
AfterReturnToTitle Raised after the player exits to the title screen.

Time events

TimeEvents are raised when the in-game date or time changes.

event summary
AfterDayStarted Raised after the game begins a new day, including when loading a save.
TimeOfDayChanged Raised after the in-game clock changes.

Mod APIs

Configuration

You can let users configure your mod through a config.json file. SMAPI will automatically create the file and take care of reading, normalising, and updating it.

Basic configuration

Here's the simplest way to use config.json:

  1. Create your model. This is just a class with properties for the config options you want, and it can contain almost anything from a few boolean fields to a complex object graph. (You should try to keep it simple for your users, though.) You can set defaults directly:
    class ModConfig
    {
       public bool ExampleBoolean { get; set; } = true;
       public float ExampleFloat { get; set; } = 0.5;
    }
    

    ...or with a constructor:

    class ModConfig
    {
       public bool ExampleBoolean { get; set; }
       public float ExampleFloat { get; set; }
    
       public ModConfig()
       {
          this.ExampleBoolean = true;
          this.ExampleFloat = 0.5;
       }
    }
    
  2. In your ModEntry::Entry method, add this line to read the config options:
    ModConfig config = helper.ReadConfig<ModConfig>();
    

That's it! When the player launches the game, SMAPI will create the config.json file automatically if it doesn't exist yet, using the default config options you provided in your model. If you need to edit and save the config, you can use helper.SaveConfig(config). You can access the helper in other methods using this.Helper.

Custom JSON files

Sometimes one config.json isn't enough, or you need to store data that's not meant to be edited by the user. This is pretty easy using the ModHelper:

  1. Create your model (just like the previous section).
  2. In your mod code, use this.Helper to read and write to a named file:
    // read file
    var model = this.Helper.ReadJsonFile<ModData>("data.json") ?? new ModData();
    
    // save file (if needed)
    this.Helper.WriteJsonFile("data.json", model);
    
    Note that ReadJsonFile will return null if the file doesn't exist. The above example will create a default instance if that happens; if you don't want to do that, just remove the ?? new ModData() part.

Per-save JSON files

You can also specify a directory path (relative to your mod directory) instead of just the file name. The directories will be created automatically if needed. For example, here's how you'd use per-save config files:

// read file
var model = this.Helper.ReadJsonFile<ModData>($"{Constants.SaveFolderName}/config.json") ?? new ModData();

// write file (if needed)
this.Helper.WriteJsonFile($"{Constants.SaveFolderName}/config.json", model);

Content

If your mod needs custom textures or maps, you can load them with SMAPI's content API. You can load any .xnb file the game supports, or load a .png file into a texture.

Example usage:

  • Load an image from your mod folder (from an assets subfolder in this example):
    // load an XNB file
    var texture = helper.Content.Load<Texture2D>(@"assets\texture.xnb", ContentSource.ModFolder);
    
    // load a PNG file
    var texture = helper.Content.Load<Texture2D>(@"assets\texture.png", ContentSource.ModFolder);
    
  • Load a custom tilesheet for a map from your mod folder:
    tilesheet.ImageSource = helper.Content.GetActualAssetKey(@"assets\tilesheet.png", ContentSource.ModFolder);
    
  • Load an asset from game's content folder:
    var data = helper.Content.Load<Dictionary<int, string>>(@"Data\ObjectInformation.xnb", ContentSource.GameContent);
    

Notes:

  • Don't call content.Load<T> in draw code for performance reasons, since drawing happens ≈60 times per second. Instead, load your content ahead of time and reuse it.
  • Don't worry about which path separators you use; SMAPI will normalise them automatically.

Logging

Your mod can write messages to the console window and log file using the monitor. For example, this code:

this.Monitor.Log("a trace message", LogLevel.Trace);
this.Monitor.Log("a debug message", LogLevel.Debug);
this.Monitor.Log("an info message", LogLevel.Info);
this.Monitor.Log("a warning message", LogLevel.Warn);
this.Monitor.Log("an error message", LogLevel.Error);

will log something like this:

[18:00:00 TRACE Mod Name] a trace message
[18:00:00 DEBUG Mod Name] a debug message
[18:00:00 INFO Mod Name] an info message
[18:00:00 WARN Mod Name] a warning message
[18:00:00 ERROR Mod Name] an error message

Note that LogLevel.Trace messages won't appear in the console window by default, they'll only be written to the log file. Trace messages are for troubleshooting details that are useful when someone sends you their error log, but which the player normally doesn't need to see. (You can see trace messages in the console if you install the "SMAPI for developers" version.)

Reflection

SMAPI provides an API for robustly accessing the game's private fields or methods. You can use it from helper.Reflection in your entry method, or this.Helper.Reflection elsewhere in your entry class. It consists of three methods:

  • GetPrivateValue<TValue>(...) returns the value of a private field.
  • GetPrivateField<TValue>(...) returns an object you can use to get or set a field's value.
  • GetPrivateMethod(...) returns an object you can use to invoke a method.

Here are a few examples of what this lets you do:

// did you pet your pet today?
bool wasPet = this.Helper.Reflection.GetPrivateValue<bool>(pet, "wasPetToday");

// what is the spirit forecast today?
string forecast = this.Helper.Reflection
   .GetPrivateMethod(new TV(), "getFortuneForecast")
   .Invoke<string>();

// randomise the mines
if(Game1.currentLocation is MineShaft)
   this.Helper.Reflection.GetPrivateField<Random>(Game1.currentLocation, "mineRandom").SetValue(new Random());

This works with static or instance fields/methods, caches the reflection to improve performance, and will throw useful errors automatically when reflection fails.

If you need to do more, you can also switch to C#'s underlying reflection API:

FieldInfo field = this.Helper.Reflection.GetPrivateField<string>().FieldInfo;
MethodInfo method = this.Helper.Reflection.GetPrivateMethod().MethodInfo;

Mod registry

Your mod can get information about loaded mods, or check if a particular mod is loaded. (All mods are loaded by the time your mod's Entry(…) method is called.)

// check if a mod is loaded
bool isLoaded = this.Helper.ModRegistry.IsLoaded("UniqueModID");

// get manifest info for a mod (name, description, version, etc.)
IManifest manifest = this.Helper.ModRegistry.Get("UniqueModID");

// get manifest info for all loaded mods
foreach(IManifest manifest in this.Helper.ModRegistry.GetAll()) {  }

Translation

The translation API lets you translate your mod into the player's current language, accounting for locale fallback automatically (e.g. if a translation isn't available in pt-BR.json, SMAPI will check pt.json and default.json). Translations can be a simple string, or they can include tokens to inject values into the translation.

File structure

SMAPI reads translations from JSON files in a structure like this:

YourMod/
   i18n/
      default.json
      es.json
      pt.json
   manifest.json
   YourMod.dll

The default.json file should contain a flat key→value lookup with your default text. Each key can contain alphanumeric, underscore, hyphen, and dot characters. Feel free to add JavaScript comments to organise or document your translations. For example:

{
    // example translations
    "item-type.label": "Item type",
    "item-type.fruit-tree": "{{fruitName}} tree",
}

You can then add translation files for each language you want to support, by copying the default.json file and translating its values. Each translation file should have one of these file names:

Language File name
Chinese zh.json
German de.json
Japanese ja.json
Portuguese pt.json
Spanish es.json

Reading translations

Once your i18n files are set up, you can read translations for the current locale:

// read a simple translation
string label = helper.Translation.Get("item-type.label");

// read a translation which uses tokens (accepts an anonymous object, dictionary, or model)
string text = helper.Translation.Get("item-type.fruit-tree", new { fruitName = "apple" });

The helper.Translate(…) method returns a fluent interface — you can keep calling methods on its return value to customise the translation. (See IntelliSense for a description of the available methods.) To get the text, simply assign it to a string:

// use fluent chain
string text = helper.Translate(key).Tokens(tokens).Tokens(moreTokens).Assert();

If your code has a lot of translation calls, you can make it less verbose by aliasing the translation helper:

var i18n = helper.Translation;

i18n.Get("item-type.fruit-tree", new { fruitName = i18n.Get("apple") });

Tips for translators

  • Save i18n files with UTF-8 encoding to avoid broken symbols in-game.
  • Type reload_i18n into the SMAPI console and hit enter to reload translations without exiting the game. (If a mod internally cached a translation, it may not be updated.)

Utilities

SMAPI provides some C# objects you can use to simplify your code.

Dates

This is a proposed API that may be part of SMAPI 1.15. It's only available in preview versions of SMAPI and may change before release.

Use SDate for calculating in-game dates. You start by creating a basic date:

// get the current date
var date = SDate.Now();

// get a date in the current year
var date = new SDate(28, "spring");

// get a date in the given year
var date = new SDate(28, "spring", 2);

You can calculate offsets from any date:

// add days
new SDate(28, "spring", 1).AddDays(370); // 06 fall in year 4

// subtract days
new SDate(01, "spring", 2).AddDays(-1); // 28 winter in year 1

Note that SDate won't let you create invalid dates:

// ArgumentException: Invalid day '-1', must be a value from 1 to 28.
new SDate(-1, "spring");

// ArithmeticException: Adding -1 days to 01 spring Y1 would result in invalid date 28 winter Y0.
new SDate(1, "spring", 1).AddDays(-1);

Semantic versions

Use SemanticVersion to manipulate and compare versions per the Semantic Versioning 2.0 standard. Example usage:

// build version from parts
ISemanticVersion version = new SemanticVersion(5, 1, 0, "beta");

// build version from string
ISemanticVersion version = new SemanticVersion("5.1.0-beta");

// compare versions (also works with SemanticVersion instances instead of strings)
new SemanticVersion("5.2").IsOlderThan("5.10"); // true
new SemanticVersion("5.10").IsNewerThan("5.10-beta"); // true
new SemanticVersion("5.1").IsBetween("5.0", "5.2"); // true

Note that game versions before 1.2.0 and some mod versions are non-standard (e.g. Stardew Valley 1.11 comes before 1.2). All SMAPI versions are standard.