Changes

7,119 bytes added ,  19:22, 29 July 2023
no edit summary
Line 1: Line 1:  
← [[Modding:Index|Index]]
 
← [[Modding:Index|Index]]
{{stub}}
+
{{stub|Missing information<li>Requires more formal, objective language cleanup</li>}}
    
This page covers how to do common tasks in SMAPI mods. '''Before reading this page, see the [[Modding:Modder Guide/Get Started|Modder Guide]] and [[Modding:Modder Guide/Game Fundamentals|Game Fundamentals]].'''
 
This page covers how to do common tasks in SMAPI mods. '''Before reading this page, see the [[Modding:Modder Guide/Get Started|Modder Guide]] and [[Modding:Modder Guide/Game Fundamentals|Game Fundamentals]].'''
Line 6: Line 6:  
==Basic techniques==
 
==Basic techniques==
 
===Tracking changes to a value===
 
===Tracking changes to a value===
Mods often need to know when a value changed. If there's no SMAPI event for the value, you can create a private [https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/fields field] to track the value, and update it using the update tick event. For example, here's a fully functional mod which prints a console message when the player's stamina changes:
+
Mods often need to know when a value changed. If there's no SMAPI event for the value, you can create a private [https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/fields field] to track the value, and update it using the update tick event.
   −
<source lang="c#">
+
See [[Modding:Modder Guide/APIs/Events#Change monitoring]] for an example.
/// <summary>The mod entry point.</summary>
  −
internal class ModEntry : Mod
  −
{
  −
    /********* Properties *********/
  −
    /// <summary>The player's last stamina value.</summary>
  −
    private float LastStamina;
  −
 
  −
    /********* 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)
  −
    {
  −
        SaveEvents.AfterLoad += this.SaveEvents_AfterLoad;
  −
        GameEvents.UpdateTick += this.GameEvents_UpdateTick;
  −
    }
  −
 
  −
 
  −
    /********* Private methods *********/
  −
    /// <summary>The method invoked when the player loads a save.</summary>
  −
    /// <param name="sender">The event sender.</param>
  −
    /// <param name="e">The event arguments.</param>
  −
    private void SaveEvents_AfterLoad(object sender, EventArgs e)
  −
    {
  −
        this.LastStamina = Game1.player.Stamina;
  −
    }
  −
 
  −
    /// <summary>The method invoked after the game updates (roughly 60 times per second).</summary>
  −
    /// <param name="sender">The event sender.</param>
  −
    /// <param name="e">The event arguments.</param>
  −
    private void GameEvents_UpdateTick(object sender, EventArgs e)
  −
    {
  −
        // skip if save not loaded yet
  −
        if (!Context.IsWorldReady)
  −
            return;
  −
 
  −
        // skip if stamina not changed
  −
        float currentStamina = Game1.player.Stamina;
  −
        if (currentStamina == this.LastStamina)
  −
            return;
  −
 
  −
        // print message & update stamina
  −
        this.Monitor.Log($"Player stamina changed from {currentStamina} to {this.LastStamina}");
  −
        this.LastStamina = currentStamina;
  −
    }
  −
}
  −
</source>
      
==Items==
 
==Items==
 
Items are objects which represent things which can be put in an inventory. Tools, Crops, etc.  
 
Items are objects which represent things which can be put in an inventory. Tools, Crops, etc.  
   −
===Create an Item (Object)===
+
===Create an item (Object)===
 
All constructors for Object:
 
All constructors for Object:
   −
<source lang='c#'>
+
<syntaxhighlight lang='c#'>
 
  public Object(Vector2 tileLocation, int parentSheetIndex, int initialStack);
 
  public Object(Vector2 tileLocation, int parentSheetIndex, int initialStack);
 
  public Object(Vector2 tileLocation, int parentSheetIndex, bool isRecipe = false);
 
  public Object(Vector2 tileLocation, int parentSheetIndex, bool isRecipe = false);
 
  public Object(int parentSheetIndex, int initialStack, bool isRecipe = false, int price = -1, int quality = 0);
 
  public Object(int parentSheetIndex, int initialStack, bool isRecipe = false, int price = -1, int quality = 0);
 
  public Object(Vector2 tileLocation, int parentSheetIndex, string Givenname, bool canBeSetDown, bool canBeGrabbed, bool isHoedirt, bool isSpawnedObject);
 
  public Object(Vector2 tileLocation, int parentSheetIndex, string Givenname, bool canBeSetDown, bool canBeGrabbed, bool isHoedirt, bool isSpawnedObject);
</source>
+
</syntaxhighlight>
    
Where '''parentSheetIndex''' is the ID of the item (can be found in ObjectInformation.xnb).
 
Where '''parentSheetIndex''' is the ID of the item (can be found in ObjectInformation.xnb).
Line 73: Line 27:  
===Spawn an item on the ground===
 
===Spawn an item on the ground===
   −
<source lang='c#'>
+
You can spawn an item on the ground with the <samp>[[Modding:Modder_Guide/Game_Fundamentals#GameLocation_et_al|GameLocation]]</samp> class's <samp>dropObject</samp> method:
 +
 
 +
<syntaxhighlight lang='c#'>
 
  public virtual bool dropObject(Object obj, Vector2 dropLocation, xTile.Dimensions.Rectangle viewport, bool initialPlacement, Farmer who = null);
 
  public virtual bool dropObject(Object obj, Vector2 dropLocation, xTile.Dimensions.Rectangle viewport, bool initialPlacement, Farmer who = null);
    
  // Concrete code for spawning:
 
  // Concrete code for spawning:
 
  Game1.getLocationFromName("Farm").dropObject(new StardewValley.Object(itemId, 1, false, -1, 0), new Vector2(x, y) * 64f, Game1.viewport, true, (Farmer)null);
 
  Game1.getLocationFromName("Farm").dropObject(new StardewValley.Object(itemId, 1, false, -1, 0), new Vector2(x, y) * 64f, Game1.viewport, true, (Farmer)null);
</source>
+
</syntaxhighlight>
    
===Add an item to an inventory===
 
===Add an item to an inventory===
<source lang='c#'>
+
<syntaxhighlight lang='c#'>
 
//You can add items found in ObjectInformation using:
 
//You can add items found in ObjectInformation using:
 
     Game1.player.addItemByMenuIfNecessary((Item)new StardewValley.Object(int parentSheetIndex, int initialStack, [bool isRecipe = false], [int price = -1], [int quality = 0]));
 
     Game1.player.addItemByMenuIfNecessary((Item)new StardewValley.Object(int parentSheetIndex, int initialStack, [bool isRecipe = false], [int price = -1], [int quality = 0]));
</source>
+
</syntaxhighlight>
    
Another example:
 
Another example:
   −
<source lang='c#'>
+
<syntaxhighlight lang='c#'>
 
     // Add a weapon directly into player's inventory
 
     // Add a weapon directly into player's inventory
 
     const int WEAP_ID = 19;                  // Shadow Dagger -- see Data/weapons
 
     const int WEAP_ID = 19;                  // Shadow Dagger -- see Data/weapons
Line 95: Line 51:     
     // Note: This code WORKS.
 
     // Note: This code WORKS.
</source>
+
</syntaxhighlight>
    
===Remove an item from an inventory===
 
===Remove an item from an inventory===
Line 107: Line 63:     
===Get all locations===
 
===Get all locations===
The list of root locations is stored in <tt>Game1.locations</tt>, but constructed building interiors aren't included. This method provides all locations in the game for the main player:
+
The list of root locations is stored in <samp>Game1.locations</samp>, but constructed building interiors aren't included. Instead, use the method <samp>Utility.ForAllLocations</samp>
<source lang="c#">
+
<syntaxhighlight lang="c#">
/// <summary>Get all game locations.</summary>
+
Utility.ForAllLocations((GameLocation location) =>
public static IEnumerable<GameLocation> GetLocations()
   
{
 
{
    return Game1.locations
+
  // do things with location here.
        .Concat(
+
});
            from location in Game1.locations.OfType<BuildableGameLocation>()
+
</syntaxhighlight>
            from building in location.buildings
  −
            where building.indoors.Value != null
  −
            select building.indoors.Value
  −
        );
  −
}
  −
</source>
     −
Then you can use it to iterate all locations:
+
Note that farmhands in multiplayer can't see all locations; see [[Modding:Modder Guide/APIs/Multiplayer#GetActiveLocations|<samp>GetActiveLocations</samp>]] instead.
<source lang="c#">
  −
foreach (GameLocation location in this.GetLocations())
  −
{
  −
  // ...
  −
}
  −
</source>
  −
 
  −
Note that farmhands in multiplayer can't see all locations; see [[Modding:Modder Guide/APIs/Multiplayer#GetActiveLocations|<tt>GetActiveLocations</tt>]] instead.
      
===Edit a location map===
 
===Edit a location map===
 
See [[Modding:Maps]].
 
See [[Modding:Maps]].
  −
==Player==
  −
//todo describe section
  −
  −
===Custom Sprite===
  −
You will be needing:
  −
  −
-XNB extractor, from this link: "www.mediafire.com/file/4c0g0fk3tza384o/XNBExtract.zip"
  −
  −
-Image editor. I strongly recommend PS or another program that lets you edit the spritesheets in 8-bit mode.
  −
  −
-Farmer XNB. You'll find them here: ...StardewValley\Content\Characters\Farmer
  −
  −
-Multiplayer fix, so it doesn't make your friend's game crash.
  −
  −
So, what you'll be doing is extract the XNB you want to modify (mostly the hairstyles and the clothes) with the XNB extractor. Once there, follow the readme in the extractor.
  −
  −
As far as we know, you cannot "add" new hairstyles or clothes, but you can modify the ones there. So what you are going to do is select one line of three views of a hairstyle and one line of four views of clothing. There, you'll change them as you wish.
  −
  −
A point here. If you want to make a character like, let's say, Master Chief, you'll get one line of clothing, and draw it as Master Chief's armor. Then get one line of a kind of hairstyle, and make the first one (the one that shows when the character is facing south) and cover it completely as the helmet would do, then finish the other two views of the helmet. We say this because it's infinitely easier than going into the farmer_base spritesheet and changing one by one the faces. For characters without helmet, you'll either have to change the faces manually in the spritesheet or try to go around with the in-game creator. You can change the skin color and the boots too, accessing them in-game with the mod that enables the multiplayer fix and let's you change your clothes anytime.
      
===Position===
 
===Position===
Line 168: Line 89:     
Each tile is 64x64 pixels as specified by <code>Game1.tileSize</code>. The conversion between absolute and tile is as follows:
 
Each tile is 64x64 pixels as specified by <code>Game1.tileSize</code>. The conversion between absolute and tile is as follows:
<source lang='c#'>
+
<syntaxhighlight lang='c#'>
 
// Absolute position => Tile position
 
// Absolute position => Tile position
 
Math.Floor(Game1.player.Position.X / Game1.tileSize)
 
Math.Floor(Game1.player.Position.X / Game1.tileSize)
Line 180: Line 101:  
Math.Floor(Game1.player.currentLocation.Map.DisplayWidth / Game1.tileSize)
 
Math.Floor(Game1.player.currentLocation.Map.DisplayWidth / Game1.tileSize)
 
Math.Floor(Game1.player.currentLocation.Map.DisplayHeight / Game1.tileSize)
 
Math.Floor(Game1.player.currentLocation.Map.DisplayHeight / Game1.tileSize)
</source>
+
</syntaxhighlight>
    
====Position Relative to the Viewport====
 
====Position Relative to the Viewport====
Line 189: Line 110:     
The player's position in pixels relative to the viewport is as follows:
 
The player's position in pixels relative to the viewport is as follows:
<source lang='c#'>
+
<syntaxhighlight lang='c#'>
 
Game1.player.Position.X - Game1.viewport.X
 
Game1.player.Position.X - Game1.viewport.X
 
Game1.player.Position.Y - Game1.viewport.Y
 
Game1.player.Position.Y - Game1.viewport.Y
</source>
+
</syntaxhighlight>
    
==NPC==
 
==NPC==
Line 198: Line 119:  
Adding new NPCs involves editing a number of files:
 
Adding new NPCs involves editing a number of files:
   −
* New file: Characters\Dialogue\<name>
+
* New file: [[Modding:Dialogue|Characters\Dialogue\<name>.json]] (See also [[Modding:Event data]])
* New file: Characters\schedules\<name>
+
* New file: [[Modding:Schedule data|Characters\schedules\<name>.json]] (Note that the "s" in the "schedules" folder is lowercase!)
* New file: Portraits\<name>
+
* New file: [[Modding:NPC data#Portraits|Portraits\<name>.png]]
* New file: Characters\<name>
+
* New file: [[Modding:NPC data#Overworld sprites|Characters\<name>.png]]
* Add entries Data\EngagementDialogue for NPCs that are marriable
+
* Add entries [[Modding:Dialogue#Engagement dialogue|Data\EngagementDialogue]] for NPCs that are marriable
* Add entry to Data\NPCDispositions
+
* Add entry to [[Modding:NPC data#Basic info|Data\NPCDispositions]]
* Add entry to Data\NPCGiftTastes
+
* Add entry to [[Modding:Gift taste data|Data\NPCGiftTastes]]
* Add entries to Characters\Dialogue\rainy
+
* Add entries to [[Modding:Dialogue#Rain dialogue|Characters\Dialogue\rainy]]
* Add entries to Data\animationDescriptions (if you want custom animations in their schedule)
+
* Add entries to [[Modding:Schedule data#Schedule points|Data\animationDescriptions]] (if you want custom animations in their schedule)
 
  −
All of the above can be done with IAssetLoaders/IAssetEditors or Content Patcher. Finally, spawn the NPC with a SMAPI mod. The different constructors are:
  −
 
  −
<source lang='c#'>
  −
public NPC(AnimatedSprite sprite, Vector2 position, int facingDir, string name, LocalizedContentManager content = null);
  −
public NPC(AnimatedSprite sprite, Vector2 position, string defaultMap, int facingDir, string name, Dictionary<int, int[]> schedule, Texture2D portrait, bool eventActor);
  −
public NPC(AnimatedSprite sprite, Vector2 position, string defaultMap, int facingDirection, string name, bool datable, Dictionary<int, int[]> schedule, Texture2D portrait);
  −
</source>
     −
For spawning:
+
All of the above can be done with an AssetRequested event or Content Patcher. If you did all of this correctly, the game will spawn the NPC in for you. (If you didn't, it swallows the error)
   −
<source lang='c#'>
+
==User-interface (UI)==
Game1.getLocationFromName("Town").addCharacter(npc);
+
The User-interface (UI) is a collection of separate elements which make up the HUD and occasional popups.
</source>
     −
==UI==
+
//TODO: This section needs to be expanded.  Please contribute if you have knowledge in this area.
   −
The UI is a collection of separate elements which make up the HUD and occasional popups.
+
===Banner message===
 +
HUDMessage are those popups in the lower left hand screen. They have several constructors, which we will briefly go over here (a few non-relevant constructors are not included):
   −
//todo expand section.
+
<syntaxhighlight lang="c#">
 
  −
 
  −
===Banner Message===
  −
HUDMessage are those popups in the lower left hand screen. They have several constructors, which we will briefly go over here (a few non relevant ones have been snipped):
  −
 
  −
<source lang="c#">
   
   public HUDMessage(string message);
 
   public HUDMessage(string message);
 
   public HUDMessage(string message, int whatType);
 
   public HUDMessage(string message, int whatType);
Line 238: Line 145:  
   public HUDMessage(string message, string leaveMeNull)
 
   public HUDMessage(string message, string leaveMeNull)
 
   public HUDMessage(string message, Color color, float timeLeft, bool fadeIn)
 
   public HUDMessage(string message, Color color, float timeLeft, bool fadeIn)
</source>
+
</syntaxhighlight>
      Line 247: Line 154:     
Types available:
 
Types available:
*1 - Achievement (HUDMessage.achievement_type)
+
#Achievement (HUDMessage.achievement_type)
*2 - New Quest (HUDMessage.newQuest_type)
+
#New Quest (HUDMessage.newQuest_type)
*3 - Error (HUDMessage.error_type)
+
#Error (HUDMessage.error_type)
*4 - Stamina (HUDMessage.stamina_type)
+
#Stamina (HUDMessage.stamina_type)
*5 - Health (HUDMessage.health_type)
+
#Health (HUDMessage.health_type)
      Line 260: Line 167:  
*'' public HUDMessage(string type, int number, bool add, Color color, Item messageSubject = null);'' - This allows for expanded customization of the message. More often used for money.
 
*'' public HUDMessage(string type, int number, bool add, Color color, Item messageSubject = null);'' - This allows for expanded customization of the message. More often used for money.
 
*''  public HUDMessage(string message, string leaveMeNull)'' - Also displays no icon.
 
*''  public HUDMessage(string message, string leaveMeNull)'' - Also displays no icon.
 +
::Not only displaying no icon, this type of HUDMessage does not have the square on the left side; it will draw a simple rectangle with text within.
 
*''  public HUDMessage(string message, Color color, float timeLeft, bool fadeIn)'' - Displays a message that fades in for a set amount of time.
 
*''  public HUDMessage(string message, Color color, float timeLeft, bool fadeIn)'' - Displays a message that fades in for a set amount of time.
   Line 265: Line 173:  
Note: For those of you who want a custom HUDMessage:  
 
Note: For those of you who want a custom HUDMessage:  
 
- Almost all of these variables are public, excluding messageSubject, so feel free to customize!
 
- Almost all of these variables are public, excluding messageSubject, so feel free to customize!
 
+
To modify them make a HUDMessage variable like so : HUDMessage <name> = new HUDMessage(<message>) ,now you can even animate them with code!
 
      
For example: add a new HUDMessage to show [[File:Error-image-ingame.png]] toaster popup.  
 
For example: add a new HUDMessage to show [[File:Error-image-ingame.png]] toaster popup.  
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
Game1.addHUDMessage(new HUDMessage("MESSAGE", 3));
 
Game1.addHUDMessage(new HUDMessage("MESSAGE", 3));
</source>
+
</syntaxhighlight>
   −
==Menus==
+
Another example: add a HUDMessage that shows up like a simple rectangle with no icon, and no square for the icon:
 +
<syntaxhighlight lang="c#">
 +
Game1.addHUDMessage(new HUDMessage("MESSAGE", ""));  // second parameter is the 'leaveMeNull' parameter
 +
</syntaxhighlight>
   −
// TODO: describe section
+
===Active clickable menu===
 +
An ''active clickable menu'' is a UI drawn over everything else which accepts user input. For example, the game menu (shown in-game when you hit {{key|ESC}} or controller {{key|B}}) is an active clickable menu. The menu is stored in <code>Game1.activeClickableMenu</code>; if that field has a non-null value, the menu will be drawn and receive input automatically.
   −
 
+
Each menu is different, so you need to look at the menu code to know how to interact with it. Since mods often need to get the current tab on the game menu, here's an example which handles the map tab:
===Get the Active Menu===
+
<syntaxhighlight lang="c#">
You can use ''Reflection'' to get the current active menu in ''GameMenu''. The ''GameMenu'' contains the Inventory, Skills, Social, Map, Crafting, Collections, and Options pages in this respective order, accessed by the tab index with ''inventoryTab'' at 0.
+
if (Game1.activeClickableMenu is GameMenu menu)
 
+
{
<source lang="c#">
+
  // get the tab pages
if (Game1.activeClickableMenu is GameMenu menu) {
   
   IList<IClickableMenu> pages = this.Helper.Reflection.GetField<List<IClickableMenu>>(menu, "pages").GetValue();
 
   IList<IClickableMenu> pages = this.Helper.Reflection.GetField<List<IClickableMenu>>(menu, "pages").GetValue();
  IClickableMenu page = pages[menu.currentTab];
     −
   // Example for getting the MapPage
+
   // option A: check tab ID
   MapPage mapPage = (MapPage) pages[menu.currentTab];
+
   if (menu.currentTab == GameMenu.mapTab)
 +
  {
 +
    ...
 +
  }
   −
   // Two examples of checking if MapPage is open
+
   // option B: check page type
   pages[menu.currentTab] is MapPage || menu.currentTab == GameMenu.mapTab;
+
   switch (pages[menu.currentTab])
 +
  {
 +
    case MapPage mapPage:
 +
      ...
 +
      break;
 +
  }
 
}
 
}
</source>
+
</syntaxhighlight>
 
  −
 
  −
===Set the Active Menu===
  −
Game1.activeClickableMenu = <Menu>
  −
 
  −
 
  −
===Simple Menu===
  −
Copy the menu you want from the game and make it your own
  −
 
      +
To create a custom menu, you need to create a subclass of <samp>IClickableMenu</samp> and assign it to <samp>Game1.activeClickableMenu</samp>. At its most basic, a menu is basically just a few methods you override (usually <samp>draw</samp> and <samp>receiveLeftClick</samp> at a minimum). When <samp>draw</samp> is called, you draw whatever you want to the screen; when <samp>receiveLeftClick</samp> is called, you check if it's within one of the clickable areas and handle it. Normally you'd use some convenience classes like <samp>ClickableTextureButton</samp> (which has a texture and position, and simplifies checking if they were clicked), though that's not strictly necessary. Here's [https://github.com/janavarro95/Stardew_Valley_Mods/blob/d7d567a72f5feaf3a5a5afd3d054ac9598727a97/GeneralMods/HappyBirthday/Framework/Menus/BirthdayMenu.cs a simple menu] you can use as an example, which draws the birthday menu for {{nexus mod|520|Birthday Mod}}.
    
===DialogueBox===
 
===DialogueBox===
 
+
[[File:DialogueBox NoChoices Example.jpg|200px|thumb|right|Example of DialogueBox without choices.]]A '''DialogueBox''' is a text box with a slightly larger, slightly boldfaced text, with "typewriter-like" effect.
[[File:DialogueBox_NoChoices_Example.jpg|200px|thumb|right|Example of DialogueBox without choices.]]A '''DialogueBox''' is a text box with a slightly larger, slightly boldfaced text, with "typewriter-like" effect.
      
There are several variants, including ones with a dialogue/conversation choices.
 
There are several variants, including ones with a dialogue/conversation choices.
Line 314: Line 222:  
Here is an example of a '''simple, choiceless output:'''
 
Here is an example of a '''simple, choiceless output:'''
   −
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
using StardewValley.Menus;  // This is where the DialogueBox class lives
 
using StardewValley.Menus;  // This is where the DialogueBox class lives
    
string message = "This looks like a typewriter ... ^But it's not ...^It's a computer.^";
 
string message = "This looks like a typewriter ... ^But it's not ...^It's a computer.^";
 
Game1.activeClickableMenu = new DialogueBox(message);
 
Game1.activeClickableMenu = new DialogueBox(message);
</source>
+
</syntaxhighlight>
   −
// TODO: Examples with choices
+
// TODO: More examples with choices
   −
==Harmony==
+
To utilise options, you are better off using createQuestionDialogue.  
{{quote|Here be dragons. Thou art forewarned.}}
     −
[https://github.com/pardeike/Harmony Harmony] lets you patch Stardew Valley methods directly. This is very powerful, but comes with major caveats:
+
<syntaxhighlight lang="c#">
 +
private void SampleClick()
 +
{
 +
    // List of choices to give the farmer.
 +
    List<Response> choices = new List<Response>()
 +
            {
 +
                new Response("dialogue_id1","Choice 1" ),
 +
                new Response("dialogue_id2", "Choice 2"),
 +
                new Response("dialogue_id3", "Choice 3"),
 +
                new Response("dialogue_id4", "Choice 4")
 +
            };
   −
* It's very easy to cause crashes, errors, or subtle bugs, including difficult-to-diagnose memory corruption errors.
+
    // And here we case it to pop up on the farmer's screen. When the farmer has picked a choice, it sends that information to the method below (DialogueSet
* SMAPI can't detect incompatible Harmony code.
+
    Game1.currentLocation.createQuestionDialogue($"What is the question?", choices.ToArray(), new GameLocation.afterQuestionBehavior(DialogueSet));
* Crossplatform compatibility is not guaranteed, and should be tested on all three platforms.
+
}
* May conflict with other Harmony mods (e.g. if two mods patch the same method, or two mods try to load different versions of Harmony).
  −
* Harmony patches may have unpredictable effects on other mods that aren't using Harmony.
  −
* Harmony patches may prevent you from attaching a debugger when testing.
     −
Using Harmony should be a last resort, and is deliberately not documented.
+
public void DialogueSet(Farmer who, string dialogue_id)
 +
{
 +
// Here you get which option was picked as dialogue_id.
 +
Game1.addHUDMessage(new HUDMessage($"Farmer {who} chose option {dialogue_id}"));
 +
   
 +
}
 +
</syntaxhighlight>
 +
 
 +
==Mail==
 +
If you are new to SMAPI or to modding Stardew Valley in general, sending a simple letter to the player's mailbox is a great place to start your learning journey.  You will be treated to some simple to understand code and concepts, as well as receive some instant gratification in the form of a tangible, in-game letter that you can see in action.  If the examples in this section fall short, there are many folks available to assist you on the Discord channel (//TODO: Provide link).
 +
 
 +
===Mail content===
 +
 
 +
Before you can actually send any of your own custom mail to the player, you must decided how your letter will be composed.  By that I mean, is your letter static - always the same text - or is it dynamic - text changes based on a variable piece of information?  Obviously a static letter will be easier to implement, so if you are just starting off, go that route for now.  However, both static and dynamic methods are explained below.
   −
==Other==
+
To send mail, whether static or dynamic, you first have to let Stardew Valley know about your content, also referred to as an asset.  In the case of mail, you have to inject your additions into the mail data.  You accomplish this via the IAssetEditor interface.  You can implement IAssetEditor from your ModEntry class, or create a separate class that implements IAssetEditor to inject new mail content into "Data\Mail.xnb".  The examples cited below use the latter approach for clarity, easy of reuse, and encapsulation:
===Add a small animation===
+
 
<source lang="c#">
+
===Inject static content===
location.temporarySprites.Add(new TemporaryAnimatedSprite(...))
  −
</source>
  −
See ''TemporaryAnimatedSprite'' for more details
     −
===Play a sound===
+
Most times a static, predefined letter will suffice, whether you are including an attachment (''i.e.,'' object, money, etc.) or not.  "Static" simply means you do not need to change the text once it is typed before sending the letter. A "static" letter will always be available in the game (unless you remove it from the mod or the mod is removed by the player) so that means the letter is still available if the player quits with your letter still in the mailbox and then returns to play later. This can be an issue with "dynamic" letters, as explained in more detail in that section, so use "static" content whenever possible.
<source lang="c#">
  −
location.playSound("SOUND");
  −
</source>
  −
(e.g. "junimoMeep1")
     −
===Send a letter===
+
You can softly reference the player's name, using "@", but other replace codes that may work in dialog texts, like %pet or %farm, do not work in static mail content at this time.  However, you can make use of some special characters that display an icon in the letter, such as "=", which will display a purple star, "<", which will display a pink heart, the "$", which will be replaced with a gold coin, the ">", which will display a right arrow, the "`", which will display an up arrow, and the "+", which will display a head riding a skateboard (maybe?).  There may be additional special cases as well that are not yet documented.
   −
Before you can send any of your own custom mail you must use a class that implements (derives from) IAssetEditor to inject new mail content into Data\Mail.xnb. An example using SMAPI is shown below:
+
The example below adds 4 letters into the mail data collection.  Note, that the code below does not send any letters to the player, but simply makes them available to Stardew Valley game so they can be sent.
   −
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
using StardewModdingAPI;
 
using StardewModdingAPI;
    
namespace MyMod
 
namespace MyMod
 
{
 
{
     public class MyModMail : IAssetEditor
+
     internal sealed class ModEntry: Mod
 
     {
 
     {
         public MyModMail()
+
         public override void Entry(IModHelper helper)
 
         {
 
         {
 +
            helper.Events.Content.AssetRequested += this.OnAssetRequested;
 
         }
 
         }
 
+
       
         public bool CanEdit<T>(IAssetInfo asset)
+
         private void OnAssetRequested(object? sender, AssetRequestedEventArgs e)
 
         {
 
         {
             return asset.AssetNameEquals("Data\\mail");
+
             if (e.NameWithoutLocale.IsEquivalentTo("Data/mail"))
 +
            {
 +
                e.Edit(this.EditImpl);
 +
            }
 
         }
 
         }
   −
         public void Edit<T>(IAssetData asset)
+
         public void EditImpl(IAssetData asset)
 
         {
 
         {
 
             var data = asset.AsDictionary<string, string>().Data;
 
             var data = asset.AsDictionary<string, string>().Data;
    
             // "MyModMail1" is referred to as the mail Id.  It is how you will uniquely identify and reference your mail.
 
             // "MyModMail1" is referred to as the mail Id.  It is how you will uniquely identify and reference your mail.
             // The @ will be replaced with the player's name.  Other items do not seem to work (i.e. %pet or %farm)
+
             // The @ will be replaced with the player's name.  Other items do not seem to work (''i.e.,'' %pet or %farm)
 
             // %item object 388 50 %%  - this adds 50 pieces of wood when added to the end of a letter.
 
             // %item object 388 50 %%  - this adds 50 pieces of wood when added to the end of a letter.
             // %item money 250 601  %%  - this sends money (did not try it myself)
+
            // %item tools Axe Hoe %%  - this adds tools; may list any of Axe, Hoe, Can, Scythe, and Pickaxe
             // %item cookingRecipe %%  - this is for recipes (did not try myself)  Not sure how it know which recipe.  
+
             // %item money 250 601  %%  - this sends a random amount of gold from 250 to 601 inclusive.
 +
             // For more details, see: https://stardewvalleywiki.com/Modding:Mail_data
 
             data["MyModMail1"] = "Hello @... ^A single carat is a new line ^^Two carats will double space.";
 
             data["MyModMail1"] = "Hello @... ^A single carat is a new line ^^Two carats will double space.";
 
             data["MyModMail2"] = "This is how you send an existing item via email! %item object 388 50 %%";
 
             data["MyModMail2"] = "This is how you send an existing item via email! %item object 388 50 %%";
 +
            data["MyModMail3"] = "Coin $  Star =  Heart <  Dude +  Right Arrow >  Up Arrow `";
 
             data["MyWizardMail"] = "Include Wizard in the mail Id to use the special background on a letter";
 
             data["MyWizardMail"] = "Include Wizard in the mail Id to use the special background on a letter";
 
         }
 
         }
 
     }
 
     }
 
}
 
}
</source>
+
</syntaxhighlight>
 +
 
 +
===Send a letter (using static content)===
 +
 
 +
Now that you have your letter loaded, it's time to send it to the player.  There are a couple different methods available to accomplish this as well, depending on your need.  Two examples are shown below.  The distinction between the two methods will be explained below:
 +
 
 +
<syntaxhighlight lang="c#">
 +
    Game1.player.mailbox.Add("MyModMail1");
 +
    Game1.addMailForTomorrow("MyModMail2");
 +
</syntaxhighlight>
 +
 
 +
The first method (Game1.player.mailbox.Add) adds the letter directly into the mailbox for the current day.  This can be accomplished in your "DayStarting" event code, for example.  Mail added directly to the mailbox is not "remembered" as being sent even after a save.  This is useful in some scenarios depending on your need.
 +
 
 +
The second method (Game1.addMailForTomorrow) will, as the name implies, add the letter to the player's mailbox on the next day.  This method remembers the mail (Id) sent making it possible not to send the same letter over and over.  This can be handled in "DayStaring", "DayEnding" or other events, as dictated by your need.
 +
 
 +
You may be able to put the letter directly into the mailbox and also have it be remembered using the mailRecieved collection.  You can simply add your mailId manually if you want it to be remembered when using the add directly to mailbox method.
 +
 
 +
If you want Stardew Valley to forget that a specific letter has already been sent, you can remove it from the mailReceived collection.  You can iterate through the collection as well using a foreach should you need to remove mail en mass.
 +
 
 +
<syntaxhighlight lang="c#">
 +
    Game1.player.mailReceived.Remove("MyModMail1");
 +
</syntaxhighlight>
 +
 
 +
That is all there is to sending a simple letter.  Attaching objects and sending money via letter is straight-forward, but sending recipes is more complicated  and will need some additional explanation at a future time.
 +
 
 +
===Inject dynamic content===
 +
 
 +
If you want to send a letter that contains data that needs to change based on the situation, such as the number of purple mushrooms eaten today, then you have to create that letter content each time you plan on sending it, especially if you want an up-to-date value.  That is what I am referring to by "dynamic" letters.
 +
 
 +
Consider the following source code, which is basically an enhanced version of the static mail class shown above, that will also support "dynamic" content.  You could certainly always use the enhanced version of this code and just not make use of the dynamic content unless needed.  The code was separated for the purpose of illustrating the differences.
 +
 
 +
<syntaxhighlight lang="c#">
 +
using StardewModdingAPI;
 +
using System.Collections.Generic;
 +
 
 +
namespace MyMail
 +
{
 +
    internal sealed class ModEntry: Mod
 +
    {
 +
        // This collection holds any letters loaded after the initial load or last cache refresh
 +
        private Dictionary<string, string> dynamicMail = new();
 +
     
 +
        public override void Entry(IModHelper helper)
 +
        {
 +
            helper.Events.Content.AssetRequested += this.OnAssetRequested;
 +
        }
   −
To make uses of this class in your own project, thereby making the mail data available, hook into the OnGameLaunch event, for example
+
        private void OnAssetRequested(object? sender, AssetRequestedEventArgs e)
 +
        {
 +
            if (e.NameWithoutLocale.IsEquivalentTo("Data/mail"))
 +
                e.Edit(this.EditImpl);
 +
        }
 +
 
 +
        public void EditImpl(IAssetData asset)
 +
        {
 +
            var data = asset.AsDictionary<string, string>().Data;
 +
 
 +
            // This is just an example
 +
            data["StaticMail"] = "If there were any letters with static content they could be placed here.";
 +
 
 +
            // Inject any mail that was added after the initial load.
 +
            foreach (var item in dynamicMail)
 +
            {
 +
                data.Add(item);
 +
            }
 +
 
 +
            dynamicMail.Clear();    // For the usage of this MOD the letters are cleared
 +
        }
   −
<source lang="c#">
   
         /// <summary>
 
         /// <summary>
         /// Fires after game is launched, right before first update tick. Happens once per game session (unrelated to loading saves).
+
         /// Add a new mail asset into the collection so it can be injected by the next cache refresh. The letter will
         /// All mods are loaded and initialized at this point, so this is a good time to set up mod integrations.
+
         /// not be available to send until the cache is invalidated in the code.
 
         /// </summary>
 
         /// </summary>
         private void OnGameLaunched(object sender, GameLaunchedEventArgs e)
+
         /// <param name="mailId">The mail key</param>
 +
        /// <param name="mailText">The mail text</param>
 +
        public void Add(string mailId, string mailText)
 
         {
 
         {
             Helper.Content.AssetEditors.Add(new MyModMail());
+
             if (!string.IsNullOrEmpty(mailId))
 +
            {
 +
                dynamicMail[mailId] = mailText;
 +
            }
 
         }
 
         }
</source>
+
    }
 +
}
 +
 
 +
</syntaxhighlight>
 +
 
 +
You will notice that there is really very little difference in the code used for static mail and dynamic mail.  The class that supports dynamic mail has a private dictionary collection for holding on to any mail content waiting to be injected.  It could have been made public to allow mail to be added directly into the collection, but that is not good practice.  Instead a public Add method was provided so that mail could be sent, so to speak, to the collection.  This code is for a specific MOD, not a robust framework, so it isn't overly concerned with error handling. You can improve that based on your needs.
 +
 
 +
Notice the additional code in the Edit method, where any mail in the dynamicMail collection is injected into Stardew Valley's content.  There will be no mail in the dynamicMail collection when the MOD is loaded (in this case) the first time.  If you add mail after the original load, then the content will have to be reloaded by invalidating the cache.  Refer to [[Modding:Modder Guide/APIs/Content#Cache invalidation|Cache invalidation]] for more details.
 +
 
 +
===Send a letter (using dynamic content)===
   −
To actually put one of your custom letters into the player's in-game mailbox, you can use a couple different methods, depending on your needTwo examples are shown below. The distinction between the two methods will be explained below:
+
You can hook into other events, such as "Day Starting" or "Day Ending" to generate the letter you want to sendConsider this simple example, that is only for illustration purposes.
   −
<source lang="c#">
+
<syntaxhighlight lang="c#">
        Game1.player.mailbox.Add("MyModMail1");
+
    private void OnDayStarting(object sender, DayStartedEventArgs e)
         Game1.addMailForTomorrow("MyModMail2");
+
    {
</source>
+
         string mailMessage = $"@, you have gathered {Game1.stats.rabbitWoolProduced} units of rabbit wool!";
   −
The first method (Game1.player.mailbox.Add) adds the letter directly into the mailbox for the current day.  This is best accomplished in your OnNewDay event code.  Mail added directly to the mailbox does not seem to be remembered even after a save.  This is from my personal observation.  Each time Stardew Valley started, my mod would send the letter again.  That has some uses.
+
        mailData.Add("MyModMailWool", mailMessage);      // Add this new letter to the mail collection (for next refresh).
   −
The second method (Game1.addMailForTomorrow) will, as the name implies, add the letter to the player's mailbox on the next day. This method remembers the mail (Id) making it possible not to send the same letter over and over.  Again this is very useful as well.
+
        Game1.mailbox.Add("MyModMailWool");              // Add to mailbox and we don't need to track it
   −
There may be a way to put the letter directly into the mailbox and have it be remembered, maybe using the next collection (mailRecieved) to be covered, but that has not been confirmed so I don't want to lead you astray. Presumption is you have to add your mail manually if you want it to be remembered.
+
        this.Helper.GameContent.InvalidateCache("Data\\mail"); // note that as of SMAPI 3.14.0, this only invalidates the English version of the asset.  
 +
    }
 +
</syntaxhighlight>
   −
If you want Stardew Valley to forget that a specific letter has already been sent, you can remove it from the mailReceived collection.  You can iterate through the collection as well using a foreach should you need to remove mail en mass.
+
This example formats a letter, showing the up-to-date count of rabbit wool, makes it available to the mail collection, places that letter in the mailbox, and then invalidates the cache so that this new letter will be injected during the cache refreshIn this case there is no need to remember mailId as the letter will be recreated each time it needs to be sent, which in this example is everyday.  Again, this code is only for illustration of the concept.
 +
 
 +
There is an important caveat to understand when injecting mail in this simple fashion.  The various mail frameworks available handle this issue, and this section will be expanded to explain how to overcome the issue, but it is being covered here to ensure you have a complete understanding of how MODs work with Stardew Valley and SMAPI.
   −
<source lang="c#">
+
If you add a dynamic letter and inject it into the content at Day Ending, you have to add the mail for display tomorrow obviously.  That means the game will be saved with a reference to the dynamic letter ("MyMailModWool" in this example) pending in the mail box.  If the player quits the game at that point and returns later to continue playing, then that dynamic letter is not available, resulting in a "phantom letter". The mailbox will show a letter is available but when clicked on nothing will display.  This can be handled in several ways, including by saving the custom letters and loading them when the player continues, but again this example code does not cover that yet. That is why the example uses On Day Starting and makes the letter available right away.
          Game1.player.mailReceived.Remove("MyModMail1");
  −
</source>
     −
That is all there is to send a simple letter.   Attaching items, although mentioned in the source code comments above, will need some additional explanation at a future time.
+
==Other==
 +
===Add a small animation===
 +
<syntaxhighlight lang="c#">
 +
location.temporarySprites.Add(new TemporaryAnimatedSprite(...))
 +
</syntaxhighlight>
 +
See ''TemporaryAnimatedSprite'' for more details
 +
 
 +
===Play a sound===
 +
<syntaxhighlight lang="c#">
 +
location.playSound("SOUND");
 +
</syntaxhighlight>
 +
(''e.g.,'' "junimoMeep1")
    
==Open source==
 
==Open source==
Line 427: Line 444:     
[[ru:Модификации:Основные возможности]]
 
[[ru:Модификации:Основные возможности]]
 +
[[zh:模组:常用方法]]
106,033

edits