Modding:Moddable Mod: Difference between revisions

From Vintage Story Wiki
mNo edit summary
m (Updated to 1.19.4. Using new command api.)
 
(13 intermediate revisions by 4 users not shown)
Line 1: Line 1:
 
<languages/><translate>
- THIS IS CURRENTLY A DRAFT -
<!--T:1-->
 
{{GameVersion|1.19.4}}
__FORCETOC__
This article requires a setup development environment. If you don't have one, read the tutorial
This article requires a setup development environment. If you don't have one, read the tutorial
at 'Setting up your Development Environment'. For brevity, this article presupposes some familiarity with
on [https://wiki.vintagestory.at/index.php?title=Modding:Setting_up_your_Development_Environment setting up your development environment.] For brevity, this article presupposes some familiarity with
VintageStory code modding, primarily how to package a mod (https://wiki.vintagestory.at/index.php?title=Modding:Mod_Packaging) and familiarity with what a ModSystem is.  
VintageStory code modding, primarily [https://wiki.vintagestory.at/index.php?title=Modding:Mod_Packaging how mods are packaged] and [https://wiki.vintagestory.at/index.php?title=Modding:Setting_up_your_Development_Environment#Project_Setup_.28Compiled_Zip.29 how to setup a compiled mod project.]
If you are looking to create your first mod, we recommend starting here =Link to first code mod sample=.
If you are looking to create your first code mod, we recommend starting [https://wiki.vintagestory.at/index.php?title=Modding:Advanced_Blocks here] instead.


== Introduction ==
== Introduction == <!--T:2-->


VintageStory makes all loaded mods available through the API.
<!--T:3-->
This makes it possible for code mods to interact with eachother and access eachother's public properties.
VintageStory makes all loaded mods available through it's expansive API, which you can explore in [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.ICoreAPI.html the API docs.]
This makes it possible for code mods to interact with each other and access each other's public properties. It does, however, make whatever mod is including another mod dependent on that mod. If the mod being included is not present or disabled, the including mod will not work. 


The loading and retrieving of mods is done through the =ModLoader=, which is accessible via the API.  
<!--T:4-->
We can retrieve both the full =Mod= with all it's useful data, as well as it's contained ModSystems.
The loading and retrieving of mods is done through the [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.IModLoader.html <code>IModLoader</code>], which we retrieve from [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.ICoreAPI.html <code>ICoreAPI</code>]. We can retrieve both the full [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.Mod.html <code>Mod</code>], with all it's useful data, as well as it's contained [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.ModSystem.html <code>ModSystems</code>].


<!--T:5-->
This is superbly useful in multiple scenarios, for example -
This is superbly useful in multiple scenarios, for example -
* Splitting up a large mod into smaller logical parts that communicate - a modpack.
* Splitting up a large mod into smaller logical parts that communicate - a modpack.
* Extendable mods, for example - the creative tool for growing trees, which lets you register new tree types.
* Extendable mods, for example - the creative GUI tool for growing trees, which lets you register new tree types and easily test them.


<!--T:6-->
In this article, we will be creating two mods. Our first mod will provide a method for our second mod to interact with.
In this article, we will be creating two mods. Our first mod will provide a method for our second mod to interact with.
We will demonstrate the use of the =ModLoader= and also explain how to reference an external mod and have that reference
We will demonstrate the use of the <code>ModLoader</code> and also explain how to reference an external mod and have that reference
resolve at game runtime. We'll also take note of execution order to make sure our moddable mod is loaded by the ModLoader before
resolve at game runtime.  
we try to access it.


Note that, excluding the use of reflection or the use of an intermediary library, which is outside the scope of this article,
<!--T:7-->
<b>only compiled mods will be able to resolve eachother's references</b>.
<small>Note that, excluding the use of reflection or the use of an intermediary library, which is outside the scope of this article,
<b>only compiled mods can interact with eachother</b>.</small>


== TipMod & SpawnMod ==
== TipMod & SpawnMod == <!--T:8-->


<!--T:9-->
The two mods we will be creating are named "TipMod" and "SpawnMod".
The two mods we will be creating are named "TipMod" and "SpawnMod".
* "TipMod" will store useful gameplay tips and periodically print them in chat. It will also provide a method which other mods can use to add their own tips.
* "SpawnMod" will register a command which teleports the player to his set or default spawnpoint. It will also access "TipMod" and add it's own tips about the command.


"TipMod" will store useful gameplay tips and periodically print them in chat. It will also provide a method which other mods
== Preparation == <!--T:10-->
can use to add their own tips.
 
"SpawnMod" will register a command which teleports the player to his set or default spawnpoint. It will also
access "TipMod" and add it's own tips about the command.
 
== Preperation ==
We start by setting up two empty mods, "TipMod" and "SpawnMod".
We start by setting up two empty mods, "TipMod" and "SpawnMod".
In each mod, we add a new *.cs source file - we will be naming these files TipMod.cs and SpawnMod.cs respectively.
In each mod, we add a new *.cs source file - we will be naming these files <code>TipMod.cs</code> and <code>SpawnMod.cs</code> respectively.
In our .cs files, we do the following.
In our .cs files, we do the following:
* Add the necessary using directives.
* Add the necessary <code>using</code> directives.
* Create a class named after the mod it's contained it.
* Create a class named after the mod it's contained in - <code>TipMod</code> and <code>SpawnMod</code>.
* Have our classes inherit ModSystem - the base for any VintageStory code mod.
* Have our classes inherit <code>ModSystem</code> - the base for any VintageStory code mod.
* Override the StartServerSide method to access the API.
* Override the <code>StartServerSide</code> method to access [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.ICoreServerAPI.html <code>ICoreServerAPI</code>].
* Make the TipMod class public so that it's accessible by other mods.
* Make the <code>TipMod</code> class <code>public</code> so that it's accessible by other mods.
 
- img of folders -


<!--T:11-->
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
// In TipMod.cs
// In TipMod.cs
Line 59: Line 59:
using Vintagestory.API.Server;
using Vintagestory.API.Server;


<!--T:12-->
namespace tipmod.src
namespace tipmod.src
{
{
Line 71: Line 72:
</syntaxhighlight>
</syntaxhighlight>


<!--T:13-->
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
// In SpawnMod.cs
// In SpawnMod.cs
Line 81: Line 83:
using Vintagestory.API.Server;
using Vintagestory.API.Server;


<!--T:14-->
namespace spawnmod.src
namespace spawnmod.src
{
{
Line 92: Line 95:
}
}
</syntaxhighlight>
</syntaxhighlight>
This concludes setting up the basis of our mods. Lastly, in TipMod, we create an additional file called Tip.cs,  
This concludes setting up the basis of our mods. Lastly, in "TipMod", we create an additional file called <code>Tip.cs</code>,  
where we will define the data structure for tips.
where we will define the data structure for tips, called <code>Tip</code>.


<!--T:15-->
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
// In Tip.cs
// In Tip.cs
Line 105: Line 109:
using System.Threading.Tasks;
using System.Threading.Tasks;


<!--T:16-->
namespace tipmod.src
namespace tipmod.src
{
{
Line 112: Line 117:
         string tip;
         string tip;


         public Tip(string author, string tip)
         <!--T:17-->
public Tip(string author, string tip)
         {
         {
             this.author = author;
             this.author = author;
Line 118: Line 124:
         }
         }


         public override string ToString() {
         <!--T:18-->
public override string ToString() {
             return "Tip by \"" + author + "\": " + tip;  
             return "Tip by \"" + author + "\": " + tip;  
         }
         }
Line 124: Line 131:
}
}
</syntaxhighlight>
</syntaxhighlight>
We are now ready to start adding the functionality of our mods. We will start with TipMod, as that will be our
Our resulting folder structure looks like this.
point of interaction.
[[File:ModdableModFolderStructure.png|none]]
 
We are now ready to extend our mods, starting with "TipMod" - our moddable mod.
== Setting up TipMod ==


For TipMod, we do the following.
== Setting up TipMod == <!--T:19-->
* Store the API object in a variable so that we can access it throughout our class.
For "TipMod", we do the following:
* Define a List that stores tips.
* Store the <code>ICoreServerAPI</code> object as a field so that we can access it throughout our class.
* Create a method that selects a random tip and prints it in chat for all players, printing a default message if there are no existing tips.
* Define a <code>List</code> that stores <code>Tip</code> objects.
* Register that method to be called at set intervals by the timer, which is available in the event API.
* Define a method that selects a random <code>Tip</code> from the <code>List</code> and prints it in chat for all players, printing a default message if the <code>List</code> is empty.
* Define a public method for adding new Tips.
* Register that method to be called at set intervals by the [http://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerEventAPI.html#Vintagestory_API_Server_IServerEventAPI_Timer_Vintagestory_API_Common_Action_System_Double_ <code>Timer</code>], which is available in the [http://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerEventAPI.html <code>IServerEventAPI</code>].
* Define a <code>public</code> method for adding new <code>Tip</code> objects.


<!--T:20-->
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
     // In TipMod.cs
     // In TipMod.cs
Line 142: Line 150:
         ICoreServerAPI api;
         ICoreServerAPI api;


         List<Tip> tips = new List<Tip>();
         <!--T:21-->
List<Tip> tips = new List<Tip>();
         double tipInterval = 10;
         double tipInterval = 10;
         public override void StartServerSide(ICoreServerAPI api)
 
         <!--T:22-->
public override void StartServerSide(ICoreServerAPI api)
         {
         {
             base.StartServerSide(api);
             base.StartServerSide(api);
             this.api = api;
             this.api = api;


             api.Event.Timer(OnInterval, tipInterval);
             <!--T:23-->
api.Event.Timer(OnInterval, tipInterval);
         }
         }


         private void OnInterval()
         <!--T:24-->
private void OnInterval()
         {
         {
             int tipCount = tips.Count();
             int tipCount = tips.Count();
             string message = "There aren't any listed tips";
             string message = "There aren't any listed tips";


             if (tipCount > 0)
             <!--T:25-->
if (tipCount > 0)
             {
             {
                 // Select a random number in the range of [0-1]
                 // Select a random number in the range of [0-1]
Line 164: Line 178:
                 int randomTip = (int)Math.Floor(random * tipCount);
                 int randomTip = (int)Math.Floor(random * tipCount);


                 Tip tip = tips[randomTip];
                 <!--T:26-->
Tip tip = tips[randomTip];


                 message = tip.ToString();
                 <!--T:27-->
message = tip.ToString();
             }  
             }  




             api.SendMessageToGroup(GlobalConstants.GeneralChatGroup, message, EnumChatType.AllGroups);
             <!--T:28-->
api.SendMessageToGroup(GlobalConstants.GeneralChatGroup, message, EnumChatType.AllGroups);
         }
         }


         public void AddTip(Tip tip)
         <!--T:29-->
public void AddTip(Tip tip)
         {
         {
             tips.Add(tip);
             tips.Add(tip);
Line 181: Line 199:
For testing purposes, we set the interval to be very short (10 seconds). Feel free to change this accordingly.
For testing purposes, we set the interval to be very short (10 seconds). Feel free to change this accordingly.


Remember, both <b>classes and methods which other mods will interact with have to be declared public.</b>
<!--T:30-->
In C#, classes and methods are not public by default. You can read more about this here (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers)
Remember, both <b>classes and methods which other mods will interact with have to be declared <code>public</code>.</b>
In C#, classes and methods are not public by default. You can [https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers read more about this here.]


We can now compile our mod, add it to our VintageStory mod folder and test it ingame. If the there are occasional chat messages
We can now compile our mod, add it to our VintageStory mod folder and test it ingame. If the there are occasional chat messages saying "There aren't any listed tips", our mod is working. We are ready to move on to the next step - setting up "SpawnMod" and then having it interact with "TipMod".
claiming that no tips are listed, our mod is working. We are ready to move on to the next step - setting up SpawnMod and then having it interact with TipMod.
== Setting up SpawnMod == <!--T:31-->
== Setting up SpawnMod ==
Let's first setup our mods functionality. We register a command which teleports the player to spawn. Conveniently, the [http://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerPlayer.html <code>IServerPlayer</code>] object stores it's spawn position, as well as the [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.IPlayer.html#Vintagestory_API_Common_IPlayer_Entity <code>IPlayerEntity</code>], which defines a method for teleporting itself to a location.  
Let's first setup our mods functionality, We register a command which teleports the player to spawn. Conveniently the Player object holds it's entity, which defines a method for teleporting itself to a location.  
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
    // In SpawnMod.cs
     class SpawnMod : ModSystem
     class SpawnMod : ModSystem
     {
     {
Line 195: Line 214:
             base.StartServerSide(api);
             base.StartServerSide(api);


             api.RegisterCommand("spawn", "Teleport to spawn", "", OnCmdSpawn);
             api.ChatCommands.Create("spawn")
                .WithDescription("Teleport to spawn")
                .RequiresPlayer()
                .RequiresPrivilege(Privilege.chat)
                .HandleWith(OnCmdSpawn);
         }
         }


         private void OnCmdSpawn(IServerPlayer player, int groupId, CmdArgs args)
        <!--T:33-->
         private TextCommandResult OnCmdSpawn(TextCommandCallingArgs args)
         {
         {
             player.Entity.TeleportTo(player.SpawnPosition);
            //Spawn position is stored on the server - Our command is called on the server so we can turn our IPlayer into IServerPlayer.
            IServerPlayer player = args.Caller.Player as IServerPlayer;
             player.Entity.TeleportTo(player.GetSpawnPosition(false));
            return TextCommandResult.Success();
         }
         }
     }
     }
</syntaxhighlight>
</syntaxhighlight>
SpawnMod is ready, let's test it. The command /spawn should now teleport us to spawn.
"SpawnMod" is ready. Let's test it. The command <code>/spawn</code> should now teleport us to spawn.
We've setup the base of our mods, now let's make SpawnMod interact with TipMod.
If our mods are working, we're ready to make "SpawnMod" interact with "TipMod".


== Mods interacting ==
== Mods interacting == <!--T:34-->
Before we get started, we have to add a reference in SpawnMod to TipMod. If you are using Visual Studio, in your Solution Explorer, directly under your project, right-click "References" and select "Add Reference".
Before we get started, we have to add a reference in "SpawnMod" to "TipMod".  
=== Adding a reference to an external mod ===
If you are using Visual Studio, in your Solution Explorer, directly under your project, right-click "References" and select "Add Reference".
[[File:ADDINGAREFERENCERIGHTCLICK.png|none]]


- img -
<!--T:35-->
Here we locate either the "TipMod" project or the compiled .dll file, and add it.
[[File:FindingReference.png|none]]


Here we locate either the TipMod project or the compiled .dll file and add it.
<!--T:36-->
After adding the reference, make sure it is not copying the file to the output folder when compiling -
After adding the reference, make sure it is not copying the file to the output folder when compiling -
having multiple .dll files with ModSystems in them will break your mod.
having multiple .dll files with <code>ModSystems</code> in them will break your mod.
 
[[File:SettingCopyLocalToFalse.png|none]]
- img -


Now we're ready to have SpawnMod add tips. Let's go ahead and do the following.
=== Accessing the ModLoader === <!--T:37-->
* Add a using directive for tipmod.src.
Now we're ready to have "SpawnMod" add tips. Let's go ahead and do the following.
* Retrieve TipMod through the ModLoader by passing it's type.
* Add a <code>using</code> directive for the namespace <code>tipmod.src</code>.
* Call the method provided by TipMod and add a variety of tips.
* Retrieve <code>TipMod</code> through the <code>ModLoader</code> by passing it's type.
* Call the <code>AddTip</code> method provided by <code>TipMod</code> and add a variety of tips.


<!--T:38-->
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
         public override void StartServerSide(ICoreServerAPI api)
         public override void StartServerSide(ICoreServerAPI api)
Line 228: Line 261:
             base.StartServerSide(api);
             base.StartServerSide(api);


             api.RegisterCommand("spawn", "Teleport to spawn", "", OnCmdSpawn);
             api.ChatCommands.Create("spawn")
 
                .WithDescription("Teleport to spawn")
                .RequiresPlayer()
                .RequiresPrivilege(Privilege.chat)
                .HandleWith(OnCmdSpawn);
           
             using tipmod.src;
             using tipmod.src;
             TipMod tipMod = api.ModLoader.GetModSystem<TipMod>();
             TipMod tipMod = api.ModLoader.GetModSystem<TipMod>();
Line 238: Line 275:
</syntaxhighlight>
</syntaxhighlight>


== Conclusion ==
== Conclusion == <!--T:41-->


<!--T:42-->
Following the steps in this article, this is what our resulting code looks like, spread across two mods.
Following the steps in this article, this is what our resulting code looks like, spread across two mods.


<!--T:43-->
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
// In TipMod.cs
// In TipMod.cs
Line 253: Line 292:
using Vintagestory.API.Server;
using Vintagestory.API.Server;


<!--T:44-->
namespace tipmod.src
namespace tipmod.src
{
{
Line 259: Line 299:
         ICoreServerAPI api;
         ICoreServerAPI api;


         List<Tip> tips = new List<Tip>();
         <!--T:45-->
List<Tip> tips = new List<Tip>();
         double tipInterval = 10;
         double tipInterval = 10;


         public override void StartServerSide(ICoreServerAPI api)
         <!--T:46-->
public override void StartServerSide(ICoreServerAPI api)
         {
         {
             base.StartServerSide(api);
             base.StartServerSide(api);
             this.api = api;
             this.api = api;


             api.Event.Timer(OnInterval, tipInterval);
             <!--T:47-->
api.Event.Timer(OnInterval, tipInterval);
         }
         }


         private void OnInterval()
         <!--T:48-->
private void OnInterval()
         {
         {
             int tipCount = tips.Count();
             int tipCount = tips.Count();
             string message = "There aren't any listed tips";
             string message = "There aren't any listed tips";


             if (tipCount > 0)
             <!--T:49-->
if (tipCount > 0)
             {
             {
                 // Select a random number in the range of [0-1]
                 // Select a random number in the range of [0-1]
Line 282: Line 327:
                 int randomTip = (int)Math.Floor(random * tipCount);
                 int randomTip = (int)Math.Floor(random * tipCount);


                 Tip tip = tips[randomTip];
                 <!--T:50-->
Tip tip = tips[randomTip];


                 message = tip.ToString();
                 <!--T:51-->
message = tip.ToString();
             }  
             }  




             api.SendMessageToGroup(GlobalConstants.GeneralChatGroup, message, EnumChatType.AllGroups);
             <!--T:52-->
api.SendMessageToGroup(GlobalConstants.GeneralChatGroup, message, EnumChatType.AllGroups);
         }
         }


         public void AddTip(Tip tip)
         <!--T:53-->
public void AddTip(Tip tip)
         {
         {
             tips.Add(tip);
             tips.Add(tip);
Line 308: Line 357:
using System.Threading.Tasks;
using System.Threading.Tasks;


<!--T:54-->
namespace tipmod.src
namespace tipmod.src
{
{
Line 315: Line 365:
         string tip;
         string tip;


         public Tip(string author, string tip)
         <!--T:55-->
public Tip(string author, string tip)
         {
         {
             this.author = author;
             this.author = author;
Line 321: Line 372:
         }
         }


         public override string ToString() {
         <!--T:56-->
public override string ToString() {
             return "Tip by \"" + author + "\": " + tip;  
             return "Tip by \"" + author + "\": " + tip;  
         }
         }
Line 338: Line 390:
using tipmod.src;
using tipmod.src;


<!--T:57-->
namespace spawnmod.src
namespace spawnmod.src
{
{
Line 346: Line 399:
             base.StartServerSide(api);
             base.StartServerSide(api);


             api.RegisterCommand("spawn", "Teleport to spawn", "", OnCmdSpawn);
             api.ChatCommands.Create("spawn")
                .WithDescription("Teleport to spawn")
                .RequiresPlayer()
                .RequiresPrivilege(Privilege.chat)
                .HandleWith(OnCmdSpawn);


             TipMod tipMod = api.ModLoader.GetModSystem<TipMod>();
             TipMod tipMod = api.ModLoader.GetModSystem<TipMod>();
             tipMod.AddTip(new Tip("codemeister32", "To quickly return to spawn, type /spawn"));
             tipMod.AddTip(new Tip("codemeister32", "To quickly return to spawn, type /spawn"));
             tipMod.AddTip(new Tip("codemeister32", "Can't find your way home? Type /spawn"));
             tipMod.AddTip(new Tip("codemeister32", "Can't find your way home? Type /spawn"));
             tipMod.AddTip(new Tip("codemeister32", "Being chased by wolves? Quick, type /spawn"));
             tipMod.AddTip(new Tip("codemeister32", "Being chased by wolves? Quick, type /spawn"));
         }
         }


         private void OnCmdSpawn(IServerPlayer player, int groupId, CmdArgs args)
         private TextCommandResult OnCmdSpawn(TextCommandCallingArgs args)
         {
         {
             player.Entity.TeleportTo(player.SpawnPosition);
            //Spawn position is stored on the server - Our command is called on the server so we can turn our IPlayer into IServerPlayer.
            IServerPlayer player = args.Caller.Player as IServerPlayer;
             player.Entity.TeleportTo(player.GetSpawnPosition(false));
            return TextCommandResult.Success();
         }
         }
     }
     }
Line 363: Line 423:
</syntaxhighlight>
</syntaxhighlight>


We're now ready to test our mods to see if they're interacting successfully. After placing both our compiled mods in the mods folder, we should see a random tip pop up in the chat every 10 seconds. If so, well done! You've successfully followed the article and have created a moddable mod.
<!--T:62-->
We're now ready to test our mods to see if they're interacting successfully. After placing both our compiled mods in the mods folder and booting into a world, we should see a random tip pop up in the chat every 10 seconds.
[[File:ModdableModSuccess.png|none]]
If so, well done! You've successfully followed the article and have created a moddable mod.


- img -
== Troubleshooting == <!--T:63-->


Congratulations!
<!--T:64-->
 
If you've ran into problems setting up either mod, check the error logs located at <code>VintagestoryData/Logs</code> in <code>server-main.txt</code>.
== Troubleshooting ==
Possible causes for either mod not working:
* It's a .cs (source) mod and not a .dll (compiled) mod. Refer here [https://wiki.vintagestory.at/index.php?title=Modding:Setting_up_your_Development_Environment#Set_Mod for how to set a compiled mod.]
* It's missing <code>modinfo.json</code>.
* Your version of VintageStory is outdated.
* You have multiple .dll files containing <code>ModSystems</code> in your mod folder.
* The referenced mod, "TipMod", is not present in the mod folder.


If you've ran into problems setting up either mod, check the error logs located at VintagestoryData/Logs in server-main.txt.
== Distribution == <!--T:65-->
Possible causes for either mod not working are as follows.
To distribute this mod, run the following command in the vsmodtools cli <code>pack <your mod id></code>, then copy the .zip file into your VintageStory mods folder.
* It's a .cs mod and not a .dll (compiled) mod.
* It's missing modinfo.json.
* Your version of VintageStory is outdated.
* You have multiple .dll files containing ModSystems in your mod folder.
* The referenced mod is not present in the mod folder.


== Distribution ==
<!--T:66-->
Here are the official versions:
* for VS v1.13.2: [https://wiki.vintagestory.at/index.php?title=File:TipMod_vs1.13.2_v1.0.0.zip TipMod_vs1.13.2_v1.0.0.zip] and [https://wiki.vintagestory.at/index.php?title=File:SpawnMod_vs1.13.2_v1.0.0.zip SpawnMod_vs1.13.2_v1.0.0.zip].


Here are the mod versions:
<!--T:67-->
-Mod Download here-
{{Navbox/modding|Vintage Story}}
</translate>

Latest revision as of 14:29, 6 March 2024

Other languages:

This page was last verified for Vintage Story version 1.19.4.


This article requires a setup development environment. If you don't have one, read the tutorial on setting up your development environment. For brevity, this article presupposes some familiarity with VintageStory code modding, primarily how mods are packaged and how to setup a compiled mod project. If you are looking to create your first code mod, we recommend starting here instead.

Introduction

VintageStory makes all loaded mods available through it's expansive API, which you can explore in the API docs. This makes it possible for code mods to interact with each other and access each other's public properties. It does, however, make whatever mod is including another mod dependent on that mod. If the mod being included is not present or disabled, the including mod will not work.

The loading and retrieving of mods is done through the IModLoader, which we retrieve from ICoreAPI. We can retrieve both the full Mod, with all it's useful data, as well as it's contained ModSystems.

This is superbly useful in multiple scenarios, for example -

  • Splitting up a large mod into smaller logical parts that communicate - a modpack.
  • Extendable mods, for example - the creative GUI tool for growing trees, which lets you register new tree types and easily test them.

In this article, we will be creating two mods. Our first mod will provide a method for our second mod to interact with. We will demonstrate the use of the ModLoader and also explain how to reference an external mod and have that reference resolve at game runtime.

Note that, excluding the use of reflection or the use of an intermediary library, which is outside the scope of this article, only compiled mods can interact with eachother.

TipMod & SpawnMod

The two mods we will be creating are named "TipMod" and "SpawnMod".

  • "TipMod" will store useful gameplay tips and periodically print them in chat. It will also provide a method which other mods can use to add their own tips.
  • "SpawnMod" will register a command which teleports the player to his set or default spawnpoint. It will also access "TipMod" and add it's own tips about the command.

Preparation

We start by setting up two empty mods, "TipMod" and "SpawnMod". In each mod, we add a new *.cs source file - we will be naming these files TipMod.cs and SpawnMod.cs respectively. In our .cs files, we do the following:

  • Add the necessary using directives.
  • Create a class named after the mod it's contained in - TipMod and SpawnMod.
  • Have our classes inherit ModSystem - the base for any VintageStory code mod.
  • Override the StartServerSide method to access ICoreServerAPI.
  • Make the TipMod class public so that it's accessible by other mods.
// In TipMod.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vintagestory.API.Common;
using Vintagestory.API.Server;

namespace tipmod.src
{
    public class TipMod : ModSystem
    {
        public override void StartServerSide(ICoreServerAPI api)
        {
            base.StartServerSide(api);
        }
    }
}
// In SpawnMod.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vintagestory.API.Common;
using Vintagestory.API.Server;

namespace spawnmod.src
{
    class SpawnMod : ModSystem
    {
        public override void StartServerSide(ICoreServerAPI api)
        {
            base.StartServerSide(api);
        }
    }
}

This concludes setting up the basis of our mods. Lastly, in "TipMod", we create an additional file called Tip.cs, where we will define the data structure for tips, called Tip.

// In Tip.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace tipmod.src
{
    public class Tip
    {
        string author;
        string tip;

        public Tip(string author, string tip)
        {
            this.author = author;
            this.tip = tip;
        }

        public override string ToString() {
            return "Tip by \"" + author + "\": " + tip; 
        }
    }
}

Our resulting folder structure looks like this.

ModdableModFolderStructure.png

We are now ready to extend our mods, starting with "TipMod" - our moddable mod.

Setting up TipMod

For "TipMod", we do the following:

  • Store the ICoreServerAPI object as a field so that we can access it throughout our class.
  • Define a List that stores Tip objects.
  • Define a method that selects a random Tip from the List and prints it in chat for all players, printing a default message if the List is empty.
  • Register that method to be called at set intervals by the Timer, which is available in the IServerEventAPI.
  • Define a public method for adding new Tip objects.
    // In TipMod.cs
    public class TipMod : ModSystem
    {
        ICoreServerAPI api;

        List<Tip> tips = new List<Tip>();
        double tipInterval = 10;

        public override void StartServerSide(ICoreServerAPI api)
        {
            base.StartServerSide(api);
            this.api = api;

            api.Event.Timer(OnInterval, tipInterval);
        }

        private void OnInterval()
        {
            int tipCount = tips.Count();
            string message = "There aren't any listed tips";

            if (tipCount > 0)
            {
                // Select a random number in the range of [0-1]
                double random = api.World.Rand.NextDouble();
                // Make the number relative to the size of our tips list
                int randomTip = (int)Math.Floor(random * tipCount);

                Tip tip = tips[randomTip];

                message = tip.ToString();
            } 


            api.SendMessageToGroup(GlobalConstants.GeneralChatGroup, message, EnumChatType.AllGroups);
        }

        public void AddTip(Tip tip)
        {
            tips.Add(tip);
        }
    }

For testing purposes, we set the interval to be very short (10 seconds). Feel free to change this accordingly.

Remember, both classes and methods which other mods will interact with have to be declared public. In C#, classes and methods are not public by default. You can read more about this here.

We can now compile our mod, add it to our VintageStory mod folder and test it ingame. If the there are occasional chat messages saying "There aren't any listed tips", our mod is working. We are ready to move on to the next step - setting up "SpawnMod" and then having it interact with "TipMod".

Setting up SpawnMod

Let's first setup our mods functionality. We register a command which teleports the player to spawn. Conveniently, the IServerPlayer object stores it's spawn position, as well as the IPlayerEntity, which defines a method for teleporting itself to a location.

    // In SpawnMod.cs
    class SpawnMod : ModSystem
    {
        public override void StartServerSide(ICoreServerAPI api)
        {
            base.StartServerSide(api);

            api.ChatCommands.Create("spawn")
                .WithDescription("Teleport to spawn")
                .RequiresPlayer()
                .RequiresPrivilege(Privilege.chat)
                .HandleWith(OnCmdSpawn);
        }

                private TextCommandResult OnCmdSpawn(TextCommandCallingArgs args)
        {
            //Spawn position is stored on the server - Our command is called on the server so we can turn our IPlayer into IServerPlayer.
            IServerPlayer player = args.Caller.Player as IServerPlayer;
            player.Entity.TeleportTo(player.GetSpawnPosition(false));
            return TextCommandResult.Success();
        }
    }

"SpawnMod" is ready. Let's test it. The command /spawn should now teleport us to spawn. If our mods are working, we're ready to make "SpawnMod" interact with "TipMod".

Mods interacting

Before we get started, we have to add a reference in "SpawnMod" to "TipMod".

Adding a reference to an external mod

If you are using Visual Studio, in your Solution Explorer, directly under your project, right-click "References" and select "Add Reference".

ADDINGAREFERENCERIGHTCLICK.png

Here we locate either the "TipMod" project or the compiled .dll file, and add it.

FindingReference.png

After adding the reference, make sure it is not copying the file to the output folder when compiling - having multiple .dll files with ModSystems in them will break your mod.

SettingCopyLocalToFalse.png

Accessing the ModLoader

Now we're ready to have "SpawnMod" add tips. Let's go ahead and do the following.

  • Add a using directive for the namespace tipmod.src.
  • Retrieve TipMod through the ModLoader by passing it's type.
  • Call the AddTip method provided by TipMod and add a variety of tips.
        public override void StartServerSide(ICoreServerAPI api)
        {
            base.StartServerSide(api);

            api.ChatCommands.Create("spawn")
                .WithDescription("Teleport to spawn")
                .RequiresPlayer()
                .RequiresPrivilege(Privilege.chat)
                .HandleWith(OnCmdSpawn);
            
            using tipmod.src;
            TipMod tipMod = api.ModLoader.GetModSystem<TipMod>();
            tipMod.AddTip(new Tip("codemeister32", "To quickly return to spawn, type /spawn"));
            tipMod.AddTip(new Tip("codemeister32", "Can't find your way home? Type /spawn"));
            tipMod.AddTip(new Tip("codemeister32", "Being chased by wolves? Quick, type /spawn"));
        }

Conclusion

Following the steps in this article, this is what our resulting code looks like, spread across two mods.

// In TipMod.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vintagestory.API.Common;
using Vintagestory.API.Config;
using Vintagestory.API.Server;

namespace tipmod.src
{
    public class TipMod : ModSystem
    {
        ICoreServerAPI api;

        List<Tip> tips = new List<Tip>();
        double tipInterval = 10;

        public override void StartServerSide(ICoreServerAPI api)
        {
            base.StartServerSide(api);
            this.api = api;

            api.Event.Timer(OnInterval, tipInterval);
        }

        private void OnInterval()
        {
            int tipCount = tips.Count();
            string message = "There aren't any listed tips";

            if (tipCount > 0)
            {
                // Select a random number in the range of [0-1]
                double random = api.World.Rand.NextDouble();
                // Make the number relative to the size of our tips list
                int randomTip = (int)Math.Floor(random * tipCount);

                Tip tip = tips[randomTip];

                message = tip.ToString();
            } 


            api.SendMessageToGroup(GlobalConstants.GeneralChatGroup, message, EnumChatType.AllGroups);
        }

        public void AddTip(Tip tip)
        {
            tips.Add(tip);
        }
    }
}
// In Tip.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace tipmod.src
{
    public class Tip
    {
        string author;
        string tip;

        public Tip(string author, string tip)
        {
            this.author = author;
            this.tip = tip;
        }

        public override string ToString() {
            return "Tip by \"" + author + "\": " + tip; 
        }
    }
}
// In SpawnMod.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vintagestory.API.Common;
using Vintagestory.API.Server;
using tipmod.src;

namespace spawnmod.src
{
    class SpawnMod : ModSystem
    {
        public override void StartServerSide(ICoreServerAPI api)
        {
            base.StartServerSide(api);

            api.ChatCommands.Create("spawn")
                .WithDescription("Teleport to spawn")
                .RequiresPlayer()
                .RequiresPrivilege(Privilege.chat)
                .HandleWith(OnCmdSpawn);

            TipMod tipMod = api.ModLoader.GetModSystem<TipMod>();
            tipMod.AddTip(new Tip("codemeister32", "To quickly return to spawn, type /spawn"));
            tipMod.AddTip(new Tip("codemeister32", "Can't find your way home? Type /spawn"));
            tipMod.AddTip(new Tip("codemeister32", "Being chased by wolves? Quick, type /spawn"));

        }

        private TextCommandResult OnCmdSpawn(TextCommandCallingArgs args)
        {
            //Spawn position is stored on the server - Our command is called on the server so we can turn our IPlayer into IServerPlayer.
            IServerPlayer player = args.Caller.Player as IServerPlayer;
            player.Entity.TeleportTo(player.GetSpawnPosition(false));
            return TextCommandResult.Success();
        }
    }
}

We're now ready to test our mods to see if they're interacting successfully. After placing both our compiled mods in the mods folder and booting into a world, we should see a random tip pop up in the chat every 10 seconds.

ModdableModSuccess.png

If so, well done! You've successfully followed the article and have created a moddable mod.

Troubleshooting

If you've ran into problems setting up either mod, check the error logs located at VintagestoryData/Logs in server-main.txt. Possible causes for either mod not working:

  • It's a .cs (source) mod and not a .dll (compiled) mod. Refer here for how to set a compiled mod.
  • It's missing modinfo.json.
  • Your version of VintageStory is outdated.
  • You have multiple .dll files containing ModSystems in your mod folder.
  • The referenced mod, "TipMod", is not present in the mod folder.

Distribution

To distribute this mod, run the following command in the vsmodtools cli pack <your mod id>, then copy the .zip file into your VintageStory mods folder.

Here are the official versions:

Icon Sign.png

Wondering where some links have gone?
The modding navbox is going through some changes! Check out Navigation Box Updates for more info and help finding specific pages.

Modding
Modding Introduction Getting Started Theme Pack
Content Modding Content Mods Developing a Content Mod Basic Tutorials Intermediate Tutorials Advanced Tutorials Content Mod Concepts
Code Modding Code Mods Setting up your Development Environment
Property Overview ItemEntityBlockBlock BehaviorsBlock ClassesBlock EntitiesBlock Entity BehaviorsWorld properties
Workflows & Infrastructure Modding Efficiency TipsMod-engine compatibilityMod ExtensibilityVS Engine
Additional Resources Community Resources Modding API Updates Programming Languages List of server commandsList of client commandsClient startup parametersServer startup parameters
Example ModsAPI DocsGitHub Repository