RELEASED Guide: How to make SMAPI 0.37 mod

Discussion in 'Mods' started by OrSpeeder, Mar 15, 2016.

  1. OrSpeeder

    OrSpeeder Phantasmal Quasar

    I know many people want to make mods for SMAPI, but... how? Here is my answer!

    Also I want to note two things: one, this guide is for SMAPI 0.37 "branch" only, I will update it only for 0.37.something, because 0.38 will be different, and 0.36 is also different.

    So, what we will need to make a SMAPI mod? We will need SMAPI itself (of course!), StardewValley (of course too!), and some mods might require you knowing where are your XNA dlls. As for the editor, I am using Visual Studio 2015... it probably can be done with other IDEs, maybe even mono-stuff without Windows, but I can't explain how, since I never tried.

    So, first thing you want to do, is create a Visual Studio project for your mod, then choose "Library" to create a .dll, and choose a name.

    [​IMG]

    That done, you will need to add the "References" to StardewModdingAPI (SMAPI) and Stardew Valley, this is needed so the compiler know what you are referencing in your code.

    This is done by right-clicking "references", clicking "add reference", then clicking "browse", and finally, choosing the "Stardew Valley.exe" file, and then the "StardewModdingAPI.exe" file

    [​IMG]

    [​IMG]


    Now for the code!

    The first thing, is make your code a Mod for the SMAPI, this is done by adding to the imports list, the line "using StardewModdingAPI", then putting in your class (that Visual Studio already created, and by the way, you can name it whatever way that you want) the "Boilerplate" code for the Mod name, author, version, and description. Don't write bad stuff there, this is what shows when SMAPI loads, you don't want people see your "fart joke" when the game launches.

    Note I also added a "Entry" there, this is mandatory, it will always execute when SMAPI finishes loading your mod, so you should be aware of 2 things: 1. You CAN use that place to setup your mod overall. 2. You CAN'T use this place to do stuff that require the game already loaded, SMAPI might run "Entry" before the game itself. If you need to do something that requires the game loaded, there are ways to do it later.

    [​IMG]

    If you compile, copy the resulting dll (it will end in your project folder/bin/Debug ) to the game mod folder with other SMAPI mods, it will already work (it will write the mod information stuff).

    If you want to print something to test, you can add to your Entry function Log.Verbose("My First Mod Worked!");

    Now to the next part!

    So, how we do something?

    We add an event! In this case I wrote the tutorial while making a mod about fences, so I needed an event that fired every night, thus I choose the event that triggers when the day changes, since this happen at night, it fires at night.

    [​IMG]

    Note that in the code above, I did "EventName += somefunction", the += is to add your mod events to the SMAPI list of stuff to run, if you do only "=" your mod might work, but it will probably break SMAPI, and any other mods that use the same event, thus don't do it please :)

    So, after that, we have an event, but this is still only SMAPI stuff, how we do Stardew stuff? In SMAPI case, different from Storm, we can meddle with the game directly, if you go upward, you will see that when I added using StardewModdingAPI; I also added using StardewValley; to my list of imports...

    When you then type StardewValley. in your code, and wait a bit, Visual Studio will offer to you all the stuff you can use, this is CA's code though, thus there is no documentation, and it might chance between StardewValley versions, thus if you can, make your mod use it the least as possible.

    In my case, I wanted to mod the fences, I thus used StardewValley.Game1.getLocationFromName("farm") to get the farm, and then got the fences on the farm.

    [​IMG]

    The tutorial ends here.

    The source of the mod used in the tutorial:

    https://gitlab.com/speeder1/FenceSlowDecayMod

    I will also slowly add here some SMAPI 0.37 events documentation.

    Appendix 1: SMAPI Events

    This is based on SMAPI 0.37.3 that I have the source here, maybe something is missing when compared to SMAPI 0.37.1

    Events in SMAPI for now are grouped into types of events, they are:

    Controls, Game, Graphics, Location, Menu, Player, Time

    Game events

    GameLoaded, no arguments

    Happens after the game.exe is loaded in the memory

    Initialize, no arguments

    Happens while game.exe initializes XNA

    LoadContent, no arguments

    Happens after the game loaded all the xnb files, you can attempt to load stuff here, or edit things the game loaded, for example you can theoretically use RenderTarget to overwrite things in the game sprite sheets

    UpdateTick, no arguments

    Happens every frame after the game updated all the logic... Avoid using this please! If too many modders use this, the game will slow down! You have been warned!

    Graphics

    DrawTick, no arguments

    Happens every frame, after the game drew its own things, like UpdateTick, please avoid abusing it, it might slow down the game, also be aware that anything you do will alter the game current rendered frame, so there is no way of using this to draw things "below" objects and UI, only "above", a good use for this is to make UI mods anyway, like the health bar mod.

    Resize, same arguments as System.Windows.Forms.Form.Resize

    Happens when the window is resized, useful if you for some reason need to always know the size of the window (for example if you are making some UI mod that resizes itself with the window).

    Time

    TimeOfDayChanged, (int32)PriorInt, (int32)NewInt

    Happens when the current time changes, the number in PriorInt and NewInt is hhmm format, for example when the protagonist wakes up at 6:00 the game sets this variable to 600, and the game checks if this variable is bigger than 2000 to see if it is time for the dog to sleep.

    InvokeDayOfMonthChanged, (int32)PriorInt, (int32)NewInt

    Happens when the day of the month changes, at night, or if someone uses a cheat mod or debug to change this value manually. It is a good place to update things that happen at night, for example to make a sprinkler mod, numbers are the day of the month

    InvokeYearOfGameChanged, (int32)PriorInt, (int32)NewInt

    Happens when the year changes.

    InvokeSeasonOfYearChanged, (String)PriorString, (String)NewString

    Happens when the month changes, due to quirks in Stardew Valley code, this is tracked by the season literal string name "Summer", "Fall", etc... so please, don't meddle with this, don't change the month name to "Verão" for example in an attempt to translate the game, it will break internal code.

    Appendix 2: Some mod sources

    Health Bar Mod: Use to see how to draw stuff over the existing content
    Fence Last Longer Mod: The one used on this tutorial
    Sprinkler Mod: Use to see how to iterate through farm items and tiles
    Chest Name with Hovering Labels Mod: Complicated mod, using two synced events and UI stuff.
    Cantorsdust's Freeze Time Inside Mod: Good mod to see how the time system works
    Cantorsdust's All Crops All Seasons Mod: This mod is good to see location-events
    Cantorsdust's Time Speed Mod: Mod that slows (or speed-up) the in-game clock speed
     

      Attached Files:

      Last edited: Mar 17, 2016
      curi0, Axiam, Bouhm and 4 others like this.
    • exect3r

      exect3r Space Hobo

      Very well done!
      But it would be much better if you explained what each element does.
      Something like:
      Game1:
      currentSeason : a string variable that stores the season (duh).​
      Utility:
      getAllCharacters() : returns a List of all NPCs in the game.
      getTodaysBirthdayNPC(string season, int day) : returns an NPC that is born on that date.
      It'll obviously be a lot of work. And if you like my idea, I suggest you do it with Storm when it comes out, not with SMAPI.
       
      • OrSpeeder

        OrSpeeder Phantasmal Quasar

        I know, the problem this is internal CA code, not SMAPI.

        With Storm you are not even allowed to do that sort of stuff (ie: access StardewValley code directly), thus documenting it for Storm would also be kinda pointless.
         
        • InfinitySamurai

          InfinitySamurai Space Hobo

          That kind of feature is on it's way for Storm, don't you worry
           
          • jivex5k

            jivex5k Intergalactic Tourist

            Wait, so SMAPI can access the objects coded into the exe? Going to check this out, maybe I can access the object class to add new Keg interactions.
             
            • OrSpeeder

              OrSpeeder Phantasmal Quasar

              Sadly, that part is not easy :/

              I started a Keg mod on Storm, and then also tried on SMAPI, could not go very far, it requires heavy injection use because of how kegs were coded in the game (they are kinda... hackish, for example the game runs a very long function, compares objects names with strings, and act as appropriate per object).
               
              • jivex5k

                jivex5k Intergalactic Tourist

                Damn...
                I found the functions using ILSpy thanks to a tip from another member here. Made a thread about getting help recompiling the exe but got no response. There's some objects that ILSpy can't figure out or something, I'm not too familiar with reverse engineering. Might try a couple other decompilers to see if I can fill in the gaps.

                I just want to make mead damnit! Hahaha.
                 
                • OrSpeeder

                  OrSpeeder Phantasmal Quasar

                  I wanted to mod in the game Mead, Bourbon, some other beverages, and make Kegs and Jelly machines give quality of the item equal to the input (example: gold star hop would result in gold star ale...)
                   
                  • jivex5k

                    jivex5k Intergalactic Tourist

                    Yeah that sounds awesome.

                    Another line of thought to accomplish something similar would be creating a new item that acts like the kitchen, except it has homebrew recipes. Could call it a homebrew kit or something and add specific grains and hop varieties to make different beers.
                    Would have to duplicate the kitchen crafting interface. Maybe I'll start tinkering with the kitchen recipes if I can't get this exe to recompile.
                     
                    • OrSpeeder

                      OrSpeeder Phantasmal Quasar

                      I added another mod of mine to the sources, anyone that makes a mod that is "example worthy" and has sources available and want it on the original post, just tell me and I will add it :)
                       
                      • Bouhm

                        Bouhm Big Damn Hero

                        Hey, I'm completely new to modding and I wanted to thank you for making this guide to help newbies who are interested in getting started!

                        I'm pretty unfamiliar with C# but managing fine with a background in OOP. But I'm still pretty lost as to what I'm doing, I admit.
                        I just wanted to learn step by step, first by trying to place a sprite on the game screen but couldn't manage that. Looking at the XNA guides and documentation added to my confusion. I just have
                        Code:
                        public override void Entry(params object[] objects)
                                {
                                    KeyboardInput.KeyDown += drawNPCs;
                                }
                        
                        private void drawNPCs(object sender, KeyEventArgs e)
                                {
                                    if (e.KeyCode.ToString().Equals("N") && Game1.hasLoadedGame && Game1.activeClickableMenu == null)
                                    {
                                        Game1.spriteBatch.Begin();
                                        foreach (NPC npc in Utility.getAllCharacters())
                                            if (npc.Schedule != null || npc.name.Equals("Kent"))
                                            {
                                                Rectangle markerLocation = new Rectangle(Game1.graphics.GraphicsDevice.Viewport.Width / 2, Game1.graphics.GraphicsDevice.Viewport.Height / 2, 200, 200);
                                                Game1.spriteBatch.Draw(npc.sprite.Texture, markerLocation, Color.White);
                                            }
                                        Game1.spriteBatch.End();
                                    }
                                }
                        
                        I just put in an abitrary x and y position for the sake of testing. This snippet of code is in a function that's called in the Entry function, but doesn't seem to draw anything despite the function being called.
                         
                          Last edited: Mar 16, 2016
                        • OrSpeeder

                          OrSpeeder Phantasmal Quasar

                          You are trying to draw in a input event... this happen after the game stopped drawing the last frame, and before it draws the new frame, thus the end result is just you drawing stuff, and then the game rendering over whatever you drew, so it is never shown.

                          Your code is right, but running at the wrong time, look into my chest label mod to see the correct way, you need one callback for logic, and another separate callback for drawing.
                           
                          • Bouhm

                            Bouhm Big Damn Hero

                            Ah, I see. I'm pretty clueless about the order of events when the game runs, but this is a good learning process. Thank you very much for the quick reply, and thank you for sharing the examples! Much appreciated.
                             
                            • OrSpeeder

                              OrSpeeder Phantasmal Quasar

                              I added some Cantorsdust's mods with his permisson to the sources list :)
                               
                              • Bouhm

                                Bouhm Big Damn Hero

                                So after many hours of trial and error I learned some things but I can't seem to figure out how to make actions happen only once. I was hoping someone could point me in the right direction.

                                The mod I'm trying to make is basically putting NPC markers on the locations of the villagers on the map, and the way I have it set up, it draws sprites on the map every tick, and it's creating new sprites, rectangles, and SpriteBatches every tick which is horrible. I can't seem to create a condition for "draw once while map is opened." I spent a really long time trying to figure out how to get an if condition to know when the map is opened since IClickableMenu only recognizes GameMenu and not when each individual tab is opened, but finally found a way by passing the GameMenu as a parameter in a private method. Honestly all the different parameters of objects and class instances really confuse me.

                                Code:
                                       public override void Entry(params object[] objects)
                                        {
                                            GameEvents.UpdateTick += createMarkers;
                                            if (markersDrawn < numVillagers )
                                            {
                                                GraphicsEvents.DrawTick += updateMap;
                                            }
                                
                                        private SerializableDictionary<Rectangle, Texture2D> npcMarkers;
                                        private int markersDrawn;
                                        private int numVillagers; // Should be a constant but could change
                                
                                        private void createMarkers(object sender, EventArgs e)
                                        {
                                            if (!(Game1.activeClickableMenu is GameMenu)) return;
                                
                                            foreach (NPC npc in Utility.getAllCharacters())
                                            {
                                                if (npc.Schedule != null || npc.name.Equals("Kent"))
                                                {
                                                    npcMarkers.Add(new Rectangle((int)npc.Position.X, (int)npc.Position.Y, 40, 40),
                                                                   Game1.content.Load<Texture2D>("Characters\\NPCMarkers\\" + npc.name + "Marker"));
                                                }
                                                numVillagers++;
                                            }
                                        }
                                
                                        private void updateMap(object sender, EventArgs e)
                                        {
                                            if (Game1.hasLoadedGame)
                                            {
                                               drawMarkers((GameMenu) Game1.activeClickableMenu);
                                            }
                                        }
                                
                                        private void drawMarkers(GameMenu menu)
                                        {
                                            if (menu.currentTab == 3)
                                            {
                                                Game1.spriteBatch.Begin();
                                                foreach (KeyValuePair<Rectangle, Texture2D> npcMarker in npcMarkers)
                                                {
                                                    Game1.spriteBatch.Draw(npcMarker.Value, npcMarker.Key, Color.White);
                                                    markersDrawn++;
                                                }
                                                Game1.spriteBatch.End();
                                            }
                                            else
                                            {
                                                markersDrawn = 0;
                                            }
                                        }
                                
                                I know this code doesn't work, this is just to try to motivate the logic.

                                So the things I discovered while debugging is that only static methods will run in the Entry method (so the code currently doesn't do anything since they're all non-static methods), which means I can't update things like the dictionary of Rectangles and Textures. I wanted to store data in the dictionary so that I would only have to create Rectangles, Textures, and SpriteBatches only once and then access it to draw, which is where I'm currently stuck at.

                                Instead of trying to draw while checking conditions every tick, I thought of doing something similar to the SocialEditor mod and just create a new IClickableMenu of the map with the markers on them that opens with a Key press or Click, but that doesn't seem very intuitive for this mod.

                                I tried to follow the ChestLabel mod (and various other examples) but having a real hard time following because I don't really understand the context without further documentation of the API and game programming in general. Thanks again in advance.
                                 
                                  Last edited: Mar 17, 2016
                                • bgy36ww

                                  bgy36ww Space Hobo

                                  Hi, I am new here.
                                  I don't have any knowledge on C# as well, but I have lot of Java and C++ experience. I want to know if it's possible to use this api to create new object? Such as new kind of seeds?
                                  Sincerely
                                   
                                  • OrSpeeder

                                    OrSpeeder Phantasmal Quasar

                                    Not really, sadly :(

                                    I am waiting on another API that has been in testing phase, to see if I can help them implement the possibility to create new seeds, crops, machines, etc.
                                     
                                    • Jinxiewinxie

                                      Jinxiewinxie Farmer Fashionista

                                    • OrSpeeder

                                      OrSpeeder Phantasmal Quasar

                                      Jinxiewinxie likes this.
                                    • Jinxiewinxie

                                      Jinxiewinxie Farmer Fashionista

                                      Breaking things isn't good =P I'm still learning so forgive me for silly questions, please!
                                       

                                      Share This Page