Difference between revisions of "Modding:Common tasks"

From Stardew Valley Wiki
Jump to navigation Jump to search
Line 335: Line 335:
 
===Inject static content===
 
===Inject static content===
  
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 player later.  This can be an issue with "dynamic" letters, so use "static whenever possible.
+
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.
  
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 left arrow, ">", which will display a right arrow and the "`", which will display an up arrow.  There may be additional special cases as well that are not yet documented.
+
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 left arrow, ">", 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.
  
 
The example below adds 3 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.
 
The example below adds 3 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.

Revision as of 21:09, 21 September 2019

Index

Axe.png
Article Stub

This article is marked as a stub for the following reason:

  • Missing information
  • Requires more formal, objective language cleanup

This page covers how to do common tasks in SMAPI mods. Before reading this page, see the Modder Guide and Game Fundamentals.

Basic techniques

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 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:

/// <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;
    }
}

Items

Items are objects which represent things which can be put in an inventory. Tools, Crops, etc.

Create an item (Object)

All constructors for Object:

 public Object(Vector2 tileLocation, int parentSheetIndex, int initialStack);
 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(Vector2 tileLocation, int parentSheetIndex, string Givenname, bool canBeSetDown, bool canBeGrabbed, bool isHoedirt, bool isSpawnedObject);

Where parentSheetIndex is the ID of the item (can be found in ObjectInformation.xnb).

Spawn an item on the ground

 public virtual bool dropObject(Object obj, Vector2 dropLocation, xTile.Dimensions.Rectangle viewport, bool initialPlacement, Farmer who = null);

 // 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);

Add an item to an inventory

//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]));

Another example:

    // Add a weapon directly into player's inventory
    const int WEAP_ID = 19;                  // Shadow Dagger -- see Data/weapons
    Item weapon = new MeleeWeapon(WEAP_ID);  // MeleeWeapon is a class in StardewValley.Tools
    Game1.player.addItemByMenuIfNecessary(weapon);

    // Note: This code WORKS.

Remove an item from an inventory

This is dependent on the inventory - rarely will you be calling this directly, as the game has functions for this for the Player, located in Farmer (in the main namespace).

To do so, in most situations, just call .removeItemFromInventory(Item)

Locations

See Game Fundamentals#GameLocation.

Get all locations

The list of root locations is stored in Game1.locations, but constructed building interiors aren't included. This method provides all locations in the game for the main player:

/// <summary>Get all game locations.</summary>
public static IEnumerable<GameLocation> GetLocations()
{
    return Game1.locations
        .Concat(
            from location in Game1.locations.OfType<BuildableGameLocation>()
            from building in location.buildings
            where building.indoors.Value != null
            select building.indoors.Value
        );
}

Then you can use it to iterate all locations:

foreach (GameLocation location in this.GetLocations())
{
   // ...
}

Note that farmhands in multiplayer can't see all locations; see GetActiveLocations instead.

Edit a location map

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

A Character's position indicates the Character's coordinates in the current location.

Position Relative to the Map

Each location has an ``xTile`` map where the top-left corner of the map is (0, 0) and the bottom-right corner of the map is (location.Map.DisplayWidth, location.Map.DisplayHeight) in pixels.

There are two ways to get a Character's position in the current location: by absolute position and by tile position.

Position.X and Position.Y will give the XY coordinates in pixels.

getTileX() and getTileY() will give the XY coordinates in tiles.

Each tile is 64x64 pixels as specified by Game1.tileSize. The conversion between absolute and tile is as follows:

// Absolute position => Tile position
Math.Floor(Game1.player.Position.X / Game1.tileSize)
Math.Floor(Game1.player.Position.Y / Game1.tileSize)

// Tile position => Absolute position
Game1.player.getTileX() * Game1.tileSize
Game1.player.getTileY() * Game1.tileSize

// Tilemap dimensions
Math.Floor(Game1.player.currentLocation.Map.DisplayWidth / Game1.tileSize)
Math.Floor(Game1.player.currentLocation.Map.DisplayHeight / Game1.tileSize)

Position Relative to the Viewport

The viewport represents the visible area on the screen. Its dimensions are Game1.viewport.Width by Game1.viewport.Height in pixels; this is the same as the game's screen resolution.

The viewport also has an absolute position relative to the map, where the top-left corner of the viewport is at (Game1.viewport.X, Game1.viewport.Y).


The player's position in pixels relative to the viewport is as follows:

Game1.player.Position.X - Game1.viewport.X
Game1.player.Position.Y - Game1.viewport.Y

NPC

Creating Custom NPCs

Adding new NPCs involves editing a number of files:

  • New file: Characters\Dialogue\<name>
  • New file: Characters\schedules\<name>
  • New file: Portraits\<name>
  • New file: Characters\<name>
  • Add entries Data\EngagementDialogue for NPCs that are marriable
  • Add entry to Data\NPCDispositions
  • Add entry to Data\NPCGiftTastes
  • Add entries to Characters\Dialogue\rainy
  • Add entries to 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:

 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);

For spawning:

 Game1.getLocationFromName("Town").addCharacter(npc);

User-interface (UI)

The User-interface (UI) is a collection of separate elements which make up the HUD and occasional popups.

//TODO: This section needs to be expanded. Please contribute if you have knowledge in this area.


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):

  public HUDMessage(string message);
  public HUDMessage(string message, int whatType);
  public HUDMessage(string type, int number, bool add, Color color, Item messageSubject = null);
  public HUDMessage(string message, string leaveMeNull)
  public HUDMessage(string message, Color color, float timeLeft, bool fadeIn)


So before we go over when you'd use them, I'm going to briefly note how the class HUDMessage uses these. (I encourage people to read the class if they have further questions, but I doubt most of us will need to know more than this)


All of the types for HUDMessage as they appear in-game.

Types available:

  • 1 - Achievement (HUDMessage.achievement_type)
  • 2 - New Quest (HUDMessage.newQuest_type)
  • 3 - Error (HUDMessage.error_type)
  • 4 - Stamina (HUDMessage.stamina_type)
  • 5 - Health (HUDMessage.health_type)


Color: Fairly obvious. It should be noted that while the first two don't give an option (they default to Color:OrangeRed), the fourth with the param 'leaveMeNull' displays as the same color as the game text.


For specifics:

  • 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, Color color, float timeLeft, bool fadeIn) - Displays a message that fades in for a set amount of time.


Note: For those of you who want a custom HUDMessage: - Almost all of these variables are public, excluding messageSubject, so feel free to customize!


For example: add a new HUDMessage to show Error-image-ingame.png toaster popup.

Game1.addHUDMessage(new HUDMessage("MESSAGE", 3));

Menus

// TODO: describe section


Get the Active Menu

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) {
  IList<IClickableMenu> pages = this.Helper.Reflection.GetField<List<IClickableMenu>>(menu, "pages").GetValue();
  IClickableMenu page = pages[menu.currentTab];

  // Example for getting the MapPage
  MapPage mapPage = (MapPage) pages[menu.currentTab];

  // Two examples of checking if MapPage is open
  pages[menu.currentTab] is MapPage || menu.currentTab == GameMenu.mapTab;
}


Set the Active Menu

Game1.activeClickableMenu = <Menu>


Simple Menu

Copy the menu you want from the game and make it your own


DialogueBox

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.

Within the message, use a caret "^" to put a linebreak.

Here is an example of a simple, choiceless output:

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.^";
Game1.activeClickableMenu = new DialogueBox(message);

// TODO: Examples with choices


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.

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:

Inject static content

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.

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 left arrow, ">", 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.

The example below adds 3 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.

using StardewModdingAPI;

namespace MyMod
{
    public class MyModMail : IAssetEditor
    {
        public MyModMail()
        {
        }

        public bool CanEdit<T>(IAssetInfo asset)
        {
            return asset.AssetNameEquals("Data\\mail");
        }

        public void Edit<T>(IAssetData asset)
        {
            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.
            // 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 money 250 601  %%  - this sends money (did not try it myself)
            // %item cookingRecipe %%   - this is for recipes (did not try myself)  Not sure how it know which recipe. 
            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["MyWizardMail"] = "Include Wizard in the mail Id to use the special background on a letter";
        }
    }
}

Inject dynamic content

If you want to send a letter that contains data that needs to change, like the number of purple mushrooms eaten today, then you have to create that letter each time you plan on sending it, especially if you want an up-to-date value. That is all that "dynamic" implies in terms of sending mail.

//TODO: This will be expanded soon... please check back!

Send a letter

To make uses of this class in your own project, thereby making the mail data available, hook into the OnGameLaunch event, for example

    /// <summary>
    /// Fires after game is launched, right before first update tick. Happens once per game session (unrelated to loading saves).
    /// All mods are loaded and initialized at this point, so this is a good time to set up mod integrations.
    /// </summary>
    private void OnGameLaunched(object sender, GameLaunchedEventArgs e)
    {
        Helper.Content.AssetEditors.Add(new MyModMail());
    }

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:

    Game1.player.mailbox.Add("MyModMail1");
    Game1.addMailForTomorrow("MyModMail2");

The first method (Game1.player.mailbox.Add) adds the letter directly into the mailbox for the current day. This can be accomplished in your "DayStaring" 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 next collection (mailRecieved) to be covered, but I have not confirmed that 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 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.

    Game1.player.mailReceived.Remove("MyModMail1");

That is all there is to sending a simple letter. Attaching items, although mentioned in the source code comments above, will need some additional explanation at a future time. Creating mail content dynamically, in respond to game conditions, is a little more complicated and will be covered in the future as well.

Harmony

“Here be dragons. Thou art forewarned.”

Harmony lets you patch Stardew Valley methods directly. This is very powerful, but comes with major caveats:

  • It's very easy to cause crashes, errors, or subtle bugs, including difficult-to-diagnose memory corruption errors.
  • SMAPI can't detect incompatible Harmony code.
  • 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.

Other

Add a small animation

location.temporarySprites.Add(new TemporaryAnimatedSprite(...))

See TemporaryAnimatedSprite for more details

Play a sound

location.playSound("SOUND");

(e.g. "junimoMeep1")

Open source

When all else fails, when you've looked at the decompiled source too long and it makes no sense, take a look at some open-source mod code! See the 'source' column in the mod compatibility list for source code.