Modding:SaveGame ModData: Difference between revisions

From Vintage Story Wiki
(Created page with "__FORCETOC__ Before starting, you should have a development environment set up. If you don't have one already you should read the tutorial Setting up your Development Envir...")
 
(Link to the doc about all serialization formats)
 
(30 intermediate revisions by 8 users not shown)
Line 1: Line 1:
__FORCETOC__
__FORCETOC__
 
{{GameVersion|1.19.3}}
<languages/><translate>
<!--T:1-->
Before starting, you should have a development environment set up. If you don't have one already you should read the tutorial [[Setting up your Development Environment]]. Furthermore, we assume that you have a basic understanding of the C# language and Object Oriented Programming. Let's get started!
Before starting, you should have a development environment set up. If you don't have one already you should read the tutorial [[Setting up your Development Environment]]. Furthermore, we assume that you have a basic understanding of the C# language and Object Oriented Programming. Let's get started!


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


VintageStory allows you to set and retrieve custom data to the world's game save as well as individual chunk. The way it works is actually quite simple!
Custom mod data can be attached to the top level [http://apidocs.vintagestory.at/api/Vintagestory.API.Server.ISaveGame.html#methods SaveGame]. Alternatively, mods can store custom [[Modding:Chunk_Moddata|chunk mod data]] on specific chunks.


Custom data is stored as an array of bytes to save space, but we can easily convert any valid C# type to <code>byte[]</code> with a very convenient class in the VintageStory API: [http://apidocs.vintagestory.at/api/Vintagestory.API.Util.SerializerUtil.html <code>SerializerUtil</code>]. This class provides us with two static methods that allow us to quickly <code>Serialize</code> into byte arrays, and <code>Deserialize<T></code> them into C# types.
This tutorial goes over the following storage related methods and fields:
; [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.ISaveGame.html#Vintagestory_API_Server_ISaveGame_StoreData__1_System_String___0_ ISaveGame.StoreData]
: Update the mod data to store in the save game.
; [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.ISaveGame.html#Vintagestory_API_Server_ISaveGame_GetData__1_System_String___0_ ISaveGame.GetData]
: Retrieve mod data from the save game.
; [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerEventAPI.html?q=SaveGameLoaded#Vintagestory_API_Server_IServerEventAPI_GameWorldSave IServerEventAPI.GameWorldSave]
: Register a delegate to call whenever a new save is about to be created. Here is where the mod should call '''ISaveGame.StoreData''' to copy the latest state from memory to the save game.
; [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerEventAPI.html?q=SaveGameLoaded#Vintagestory_API_Server_IServerEventAPI_SaveGameLoaded IServerEventAPI.SaveGameLoaded]
: Register a delegate to call whenever a save game is loaded from disk. Here is where the mod should call '''ISaveGame.GetData''' to read the saved state from disk and copy it to memory.


You can check out the methods for storing and retrieving these arrays of bytes for [http://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerChunk.html Chunks] and for [http://apidocs.vintagestory.at/api/Vintagestory.API.Server.ISaveGame.html#methods SaveGame]
<!--T:4-->
Internally, the custom data is stored as an array of bytes to save space. Converting C# objects to/from an array of bytes is called serialization/deserialization. Serialization methods can either be invoked directly on [[Modding:Serialization_Formats|<code>SerializerUtil</code>]], or the generalized versions of '''ISaveGame.StoreData<T>''' and '''ISaveGame.GetData<T>''' can be used which implicitly use '''SerializerUtil'''.


== Custom Data in SaveGame ==
== Custom Data in SaveGame == <!--T:6-->


<!--T:7-->
Let us show you this powerful set of tools by making an example mod in which we implement an open list of players currently looking for a group to play with ingame.
Let us show you this powerful set of tools by making an example mod in which we implement an open list of players currently looking for a group to play with ingame.


<!--T:8-->
This list will be a <code>List<string></code> of players which will be stored to the world's <code>SaveGame</code>. Anyone can join it, leave it, or see who needs a group. We'll store this list as an array of bytes, and it can be accessed by the key "lfg".
This list will be a <code>List<string></code> of players which will be stored to the world's <code>SaveGame</code>. Anyone can join it, leave it, or see who needs a group. We'll store this list as an array of bytes, and it can be accessed by the key "lfg".


== Preparation ==
== Preparation == <!--T:9-->


Let's start by creating a new .cs file for this mod, and adding our imports and the the <code>VintageStory.ServerMods</code> namespace. Additionally, we'll declare our class for the mod which will inheret from <code>ModSystem</code> which is the base class for mod systems. You can read more about this [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.ModSystem.html here].
<!--T:10-->
Let's start by creating a new .cs file for this mod, and adding our imports and the <code>VintageStory.ServerMods</code> namespace to wrap our class in. Additionally, we'll declare the class <code>LookingForGroup</code> that will inherit from <code>ModSystem</code> which is the base class for mod systems. You can read more about this [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.ModSystem.html here].
</translate>


<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
Line 32: Line 47:
     {
     {
         ICoreServerAPI serverApi;
         ICoreServerAPI serverApi;
        List<string> lfgList;
         public override void StartServerSide(ICoreServerAPI api)
         public override void StartServerSide(ICoreServerAPI api)
         {
         {
             serverApi = api;
             serverApi = api;


             api.RegisterCommand("lfg", "List or join the lfg list", "[list|join|leave]", OnLfg);
             api.Event.SaveGameLoaded += OnSaveGameLoading;
            api.Event.GameWorldSave += OnSaveGameSaving;
 
            api.ChatCommands.Create("lfg")
                .WithDescription("List or join the lfg list")
                .RequiresPrivilege(Privilege.chat)
                .RequiresPlayer()
                .WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" }))
                .HandleWith(new OnCommandDelegate(OnLfg));
         }
         }
     }
     }
Line 44: Line 69:
In this class, we create an override for the <code>StartServerSide</code> method. This method of the <code>ModSystem</code> is called for all mods on the Server side by the game.
In this class, we create an override for the <code>StartServerSide</code> method. This method of the <code>ModSystem</code> is called for all mods on the Server side by the game.


When the Server side starts, we register a command that players will use to access our list of those looking for group. This will be <code>/lfg</code> with the arguments list, join or leave.
The Core Server API contains an additional API for <code>Event</code> registration; this, in turn, contains two very important events (among others): <code>SaveGameLoaded</code> and <code>GameWorldSave</code>. These events are fired when the game is loaded, and when the game saves, respectively. We can assign delegates that will be called when the events fire by assigning these to the event with the <code>+=</code> operator.  


Let's create the delegate <code>OnLfg</code> that will be called when a player enters this command.
When the Server side starts, we add two event delegates that will retrieve our list from the <code>SaveGame</code> when the we game loads, and that will save the list when the game saves. We also register a command that players will use to access our list of those looking for group. This will be <code>/lfg</code> with the arguments list, join or leave.


== Storing and Retrieving the List ==
Let us now define the event delegates!


Right after our <code>StartServerSide</code> override, let's create our <code>/lfg</code> command delegate:
== Retrieving and Storing the List ==


<syntaxhighlight lang="c#">
When the game loads, <code>OnSaveGameLoading</code> gets called and attempts to get our list of players looking for group. The Core Server Api has a member called <code>WorldManager</code> which we use to access everything World related, including our <code>SaveGame</code>.
        private void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
        {
            if (args.Length < 1)
            {
                player.SendMessage(groupId, "/lfg [list|join|leave]", EnumChatType.CommandError);
                return;
            }
 
        }
</syntaxhighlight>
 
The chat command delegate has three parameters: the player issuing the command, the group in which this command was entered, and the arguments sent for the command.
 
The first thing we do is check that there is at least an argument given, with <code>if (args.Length < 1)</code>. If there isn't, we show the player the list of possible arguments and we go no further.
 
Next, we'll attempt to get our list of players looking for group. The Core Server Api has a member called <code>WorldManager</code> which we use to access everything World related, including our <code>SaveGame</code>.


<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
         private void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
         private void OnSaveGameLoading()
         {
         {
            if (args.Length < 1)...
             byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");
             byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");


             List<string> players = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
             lfgList = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
 
         }
         }
</syntaxhighlight>
</syntaxhighlight>
Line 84: Line 90:
We attempt to get our list by exposing the <code>GetData</code> method and passing it an identifier for our custom data, in this case our "lfg" list.
We attempt to get our list by exposing the <code>GetData</code> method and passing it an identifier for our custom data, in this case our "lfg" list.


'''Hint''': If nothing is found stored under the identifier, the <code>GetData</code> method returns null. In our mod example, this will happen until at least one player enters the list!
'''Hint''': If nothing is found stored under the key we provide, the <code>GetData</code> method returns null. In our mod example, this will happen until at least one player enters the list!


<code>byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");</code>
<code>byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");</code>


As you can see, we're retrieving an array of bytes, which is data type we actually store on the SaveGame. Let's convert it to a <code>List</code> of strings:
As you can see, we're retrieving an array of bytes, which is the data type we actually store on the <code>SaveGame</code>. Let's convert it to a <code>List</code> of strings:


List<string> players = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
<code>lfgList = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);</code>


Here we use a ternary operator to assign our <code>players</code> list a new <code>List<string></code> if there was nothing stored under the "lfg" key.
Here we use a ternary operator to assign our <code>lfgList</code> field a new <code>List<string></code> if there was nothing stored under the "lfg" key.


If <code>data</code> is not null, we expose the <code>Deserialize<T></code> method of the <code>SerializerUtil</code> class. This method will deserialize an array of bytes into an instance of the type argument we pass it.
If <code>data</code> is not null, we expose the <code>Deserialize<T></code> method of the <code>SerializerUtil</code> class. This method will deserialize an array of bytes into an instance of the type argument we pass it. Now that we have a delegate that fetches our list of players "lfg", let's create the delegate that stores this list when the Game World is saved:
 
Now that we have our list of players "lfg", let's handle the possible arguments of the command.


<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
         private void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
         private void OnSaveGameSaving()
         {
         {
             if (args.Length < 1)...
             serverApi.WorldManager.SaveGame.StoreData("lfg", SerializerUtil.Serialize(lfgList));
        }
</syntaxhighlight>


            byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");
Here we call the <code>StoreData</code> method to save our list under the "lfg" key for later retrieval! Because we can only store data in the form of <code>byte[]</code>, we use the <code>Serialize</code> method of the <code>SerializerUtil</code> class to turn <code>lfgList</code> back to an array of bytes, which we pass as the second argument.  
 
We now have implemented a way to assign our list from storageto <code>lfgList</code> when the <code>SaveGame</code> is loaded, and a way to store this list once the game is saved. In between these two events, we want players to be added or removed from <code>lfgList</code> via our command. Let's create our command delegate!


            List<string> players = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
== Handling the Command ==


            string cmd = args.PopWord();
Server chat commands have three parameters: the player issuing the command, the group in which this command was entered, and the arguments sent for the command.


<syntaxhighlight lang="c#">
        private TextCommandResult OnLfg(TextCommandCallingArgs args)
        {
            string cmd = args[0] as String;
             switch (cmd)
             switch (cmd)
             {
             {
Line 121: Line 133:


                 default:
                 default:
                     player.SendMessage(groupId, "/lfg [list|join|leave]", EnumChatType.CommandError);
                     return TextCommandResult.Error("/lfg [list|join|leave]");
                    break;
             }
             }
         }
         }
</syntaxhighlight>
</syntaxhighlight>


We use the <code>PopWord</code> of our <code>CmdArgs</code> parameter to collect the first argument passed (ignoring anything subsequent). We then start a switch statement for our valid arguments, and default to showing these to the player if none of them match.
We use <code>args[0]</code> to collect the first argument passed (ignoring anything subsequent). We then start a switch statement for our valid arguments, and default to showing these to the player if none of them match or <code>cmd</code> is null.


Let's handle each of these:
Let's handle each of these:
Line 133: Line 144:
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
                 case "join":
                 case "join":
                     if (players.Contains(player.PlayerUID))
                     if (lfgList.Contains(args.Caller.Player.PlayerUID))
                     {
                     {
                         player.SendMessage(groupId, "You're already in this list!", EnumChatType.Notification);
                         return TextCommandResult.Error("You're already in the list!");
                     }
                     }
                     else
                     else
                     {
                     {
                         players.Add(player.PlayerUID);
                         lfgList.Add(args.Caller.Player.PlayerUID);
                        data = SerializerUtil.Serialize(players);
                         return TextCommandResult.Success("Successfully joined.");
 
                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);
 
                         player.SendMessage(groupId, "Successfully joined!", EnumChatType.Notification);
                     }
                     }
                    break;
</syntaxhighlight>
</syntaxhighlight>


If <code>/lfg join</code> was entered, we'll first check if the player is already on the "lfg" list, letting the player know if so. Alternatively, we add the player's unique identifier to the list, and use the <code>Serialize</code> method to turn the updated players <code>List<string></code> back to an array of byte.
If <code>/lfg join</code> was entered, we'll first check if the player is already on the "lfg" list, letting the player know if so. Alternatively, we add the player's unique identifier to the list and show a success message.  


After, we use the <code>StoreData</code> method to save our new serialized list under the "lfg" key for later retrieval! Upon completion, we let the player know that he is now in the list.
'''Hint:''' We do not want to store player names directly. This is because player names may change, therefore our list could become inaccurate.


Now let's handle <code>/lfg leave</code>:
Next we handle <code>/lfg leave</code>:


<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
                 case "leave":
                 case "leave":
                     if (!players.Remove(player.PlayerUID))
                     if (!lfgList.Remove(args.Caller.Player.PlayerUID))
                     {
                     {
                         player.SendMessage(groupId, "You're not in the list!", EnumChatType.Notification);
                         return TextCommandResult.Error("You're not in the list!");
                     }
                     }
                     else
                     else
                     {
                     {
                         data = SerializerUtil.Serialize(players);
                         return TextCommandResult.Success("Successfully left.");
 
                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);
 
                        player.SendMessage(groupId, "Successfully left!", EnumChatType.Notification);
                     }
                     }
                    break;
</syntaxhighlight>
</syntaxhighlight>


The <code>Remove</code> method returns false if nothing matching the argument passed to it was found to be removed, and true if it was. If the player was in the list, we serialize the updated list, and store the data to the <code>SaveGame</code>, letting the player know his request was successful.
The <code>Remove</code> method returns false if nothing matching the argument passed to it was found and removed, and true if it was. We let the player know if they were not on the list, or if they were and got successfully taken out of it.


Finally, we handle <code>/lfg list</code>:
Finally, we handle <code>/lfg list</code>:
Line 178: Line 179:
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
                 case "list":
                 case "list":
                     if (players.Count == 0)
                     if (lfgList.Count == 0)
                     {
                     {
                         player.SendMessage(groupId, "Noone is looking for group!", EnumChatType.Notification);
                         return TextCommandResult.Success("No one is looking for a group.");
                        break;
                     }
                     }
 
                     else
                     string lfgList = "Players looking for group:";
                    players.ForEach((playerUid) =>
                     {
                     {
                         lfgList += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                        string response = "Players looking for group:";
                    });
                         lfgList.ForEach((playerUid) =>
                        {
                            response += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                        });


                    player.SendMessage(groupId, lfgList, EnumChatType.Notification);
                        return TextCommandResult.Success(response);
                     break;
                     }
</syntaxhighlight>
</syntaxhighlight>


In this case we simply let the player know if are no players in the "lfg" list, and if there are then we build a string with all the player names on the list!
In this case we simply let the player know if there is noone on the list, but if there are we build a string with all the player names on the list.


== Conclusion ==
== Conclusion ==
Line 211: Line 212:
     {
     {
         ICoreServerAPI serverApi;
         ICoreServerAPI serverApi;
        List<string> lfgList;
         public override void StartServerSide(ICoreServerAPI api)
         public override void StartServerSide(ICoreServerAPI api)
         {
         {
             serverApi = api;
             serverApi = api;


             api.RegisterCommand("lfg", "List or join the lfg list", "[list|join|leave]", OnLfg);
             api.Event.SaveGameLoaded += OnSaveGameLoading;
            api.Event.GameWorldSave += OnSaveGameSaving;
 
            api.ChatCommands.Create("lfg")
                .WithDescription("List or join the lfg list")
                .RequiresPrivilege(Privilege.chat)
                .RequiresPlayer()
                .WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" }))
                .HandleWith(new OnCommandDelegate(OnLfg));
         }
         }


         private void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
         private void OnSaveGameLoading()
         {
         {
            if (args.Length < 1)
            {
                player.SendMessage(groupId, "/lfg [list|join|leave]", EnumChatType.CommandError);
                return;
            }
             byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");
             byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");


             List<string> players = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
             lfgList = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
 
        }
             string cmd = args.PopWord();
        private void OnSaveGameSaving()
        {
             serverApi.WorldManager.SaveGame.StoreData("lfg", SerializerUtil.Serialize(lfgList));
        }


        private TextCommandResult OnLfg(TextCommandCallingArgs args)
        {
            string cmd = args[0] as String;
             switch (cmd)
             switch (cmd)
             {
             {
                 case "join":
                 case "join":
                     if (players.Contains(player.PlayerUID))
                     if (lfgList.Contains(args.Caller.Player.PlayerUID))
                     {
                     {
                         player.SendMessage(groupId, "You're already in this list!", EnumChatType.Notification);
                         return TextCommandResult.Error("You're already in the list!");
                     }
                     }
                     else
                     else
                     {
                     {
                         players.Add(player.PlayerUID);
                         lfgList.Add(args.Caller.Player.PlayerUID);
                        data = SerializerUtil.Serialize(players);
                         return TextCommandResult.Success("Successfully joined.");
 
                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);
 
                         player.SendMessage(groupId, "Successfully joined!", EnumChatType.Notification);
                     }
                     }
                    break;


                 case "leave":
                 case "leave":
                     if (!players.Remove(player.PlayerUID))
                     if (!lfgList.Remove(args.Caller.Player.PlayerUID))
                     {
                     {
                         player.SendMessage(groupId, "You're not in the list!", EnumChatType.Notification);
                         return TextCommandResult.Error("You're not in the list!");
                     }
                     }
                     else
                     else
                     {
                     {
                         data = SerializerUtil.Serialize(players);
                         return TextCommandResult.Success("Successfully left.");
 
                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);
 
                        player.SendMessage(groupId, "Successfully left!", EnumChatType.Notification);
                     }
                     }
                    break;


                 case "list":
                 case "list":
                     if (players.Count == 0)
                     if (lfgList.Count == 0)
                     {
                     {
                         player.SendMessage(groupId, "Noone is looking for group!", EnumChatType.Notification);
                         return TextCommandResult.Success("No one is looking for a group.");
                        break;
                     }
                     }
 
                     else
                     string lfgList = "Players looking for group:";
                    players.ForEach((playerUid) =>
                     {
                     {
                         lfgList += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                        string response = "Players looking for group:";
                    });
                         lfgList.ForEach((playerUid) =>
                        {
                            response += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                        });


                    player.SendMessage(groupId, lfgList, EnumChatType.Notification);
                        return TextCommandResult.Success(response);
                     break;
                     }


                 default:
                 default:
                     player.SendMessage(groupId, "/lfg [list|join|leave]", EnumChatType.CommandError);
                     return TextCommandResult.Error("/lfg [list|join|leave]");
                    break;
             }
             }
         }
         }
     }
     }
}
}
</syntaxhighlight>
</syntaxhighlight>


Line 294: Line 294:
Let's test our mod. Once you're ingame, try entering <code>/lfg join</code>. Now quit the game and join back in. Upon entering <code>/lfg list</code>, you should see your name on the list; this means that your custom data has persisted in the <code>SaveGame</code>!
Let's test our mod. Once you're ingame, try entering <code>/lfg join</code>. Now quit the game and join back in. Upon entering <code>/lfg list</code>, you should see your name on the list; this means that your custom data has persisted in the <code>SaveGame</code>!


== Distribution ==
{{Navbox/codemodding}}
 
To distribute this mod, you may run the following command in the modtools cli pack <your mod id>, then copy the .zip file into your VintageStory mods folder.
 
Here are the official versions:
 
* for VS v1.12.4: [https://wiki.vintagestory.at/images/8/84/LookingForGroup_vs1.12.4_v1.0.0.zip LookingForGroup_vs1.12.4_v1.0.0.zip]
* for VS v1.6: [https://wiki.vintagestory.at/images/f/f0/LookingForGroup_vs1.6_v1.0.0.zip LookingForGroup_vs1.6_v1.0.0.zip]

Latest revision as of 01:31, 24 October 2024

This page was last verified for Vintage Story version 1.19.3.

Other languages:

Before starting, you should have a development environment set up. If you don't have one already you should read the tutorial Setting up your Development Environment. Furthermore, we assume that you have a basic understanding of the C# language and Object Oriented Programming. Let's get started!

Introduction

Custom mod data can be attached to the top level SaveGame. Alternatively, mods can store custom chunk mod data on specific chunks.

This tutorial goes over the following storage related methods and fields:

ISaveGame.StoreData
Update the mod data to store in the save game.
ISaveGame.GetData
Retrieve mod data from the save game.
IServerEventAPI.GameWorldSave
Register a delegate to call whenever a new save is about to be created. Here is where the mod should call ISaveGame.StoreData to copy the latest state from memory to the save game.
IServerEventAPI.SaveGameLoaded
Register a delegate to call whenever a save game is loaded from disk. Here is where the mod should call ISaveGame.GetData to read the saved state from disk and copy it to memory.

Internally, the custom data is stored as an array of bytes to save space. Converting C# objects to/from an array of bytes is called serialization/deserialization. Serialization methods can either be invoked directly on SerializerUtil, or the generalized versions of ISaveGame.StoreData<T> and ISaveGame.GetData<T> can be used which implicitly use SerializerUtil.

Custom Data in SaveGame

Let us show you this powerful set of tools by making an example mod in which we implement an open list of players currently looking for a group to play with ingame.

This list will be a List<string> of players which will be stored to the world's SaveGame. Anyone can join it, leave it, or see who needs a group. We'll store this list as an array of bytes, and it can be accessed by the key "lfg".

Preparation

Let's start by creating a new .cs file for this mod, and adding our imports and the VintageStory.ServerMods namespace to wrap our class in. Additionally, we'll declare the class LookingForGroup that will inherit from ModSystem which is the base class for mod systems. You can read more about this here.

using System.Collections.Generic;
using Vintagestory.API.Common;
using Vintagestory.API.Server;
using Vintagestory.API.Util;

namespace Vintagestory.ServerMods
{
    class LookingForGroup : ModSystem
    {
        ICoreServerAPI serverApi;
        List<string> lfgList;

        public override void StartServerSide(ICoreServerAPI api)
        {
            serverApi = api;

            api.Event.SaveGameLoaded += OnSaveGameLoading;
            api.Event.GameWorldSave += OnSaveGameSaving;

            api.ChatCommands.Create("lfg")
                .WithDescription("List or join the lfg list")
                .RequiresPrivilege(Privilege.chat)
                .RequiresPlayer()
                .WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" }))
                .HandleWith(new OnCommandDelegate(OnLfg));
        }
    }
}

In this class, we create an override for the StartServerSide method. This method of the ModSystem is called for all mods on the Server side by the game.

The Core Server API contains an additional API for Event registration; this, in turn, contains two very important events (among others): SaveGameLoaded and GameWorldSave. These events are fired when the game is loaded, and when the game saves, respectively. We can assign delegates that will be called when the events fire by assigning these to the event with the += operator.

When the Server side starts, we add two event delegates that will retrieve our list from the SaveGame when the we game loads, and that will save the list when the game saves. We also register a command that players will use to access our list of those looking for group. This will be /lfg with the arguments list, join or leave.

Let us now define the event delegates!

Retrieving and Storing the List

When the game loads, OnSaveGameLoading gets called and attempts to get our list of players looking for group. The Core Server Api has a member called WorldManager which we use to access everything World related, including our SaveGame.

        private void OnSaveGameLoading()
        {
            byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");

            lfgList = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
        }

We attempt to get our list by exposing the GetData method and passing it an identifier for our custom data, in this case our "lfg" list.

Hint: If nothing is found stored under the key we provide, the GetData method returns null. In our mod example, this will happen until at least one player enters the list!

byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");

As you can see, we're retrieving an array of bytes, which is the data type we actually store on the SaveGame. Let's convert it to a List of strings:

lfgList = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);

Here we use a ternary operator to assign our lfgList field a new List<string> if there was nothing stored under the "lfg" key.

If data is not null, we expose the Deserialize<T> method of the SerializerUtil class. This method will deserialize an array of bytes into an instance of the type argument we pass it. Now that we have a delegate that fetches our list of players "lfg", let's create the delegate that stores this list when the Game World is saved:

        private void OnSaveGameSaving()
        {
            serverApi.WorldManager.SaveGame.StoreData("lfg", SerializerUtil.Serialize(lfgList));
        }

Here we call the StoreData method to save our list under the "lfg" key for later retrieval! Because we can only store data in the form of byte[], we use the Serialize method of the SerializerUtil class to turn lfgList back to an array of bytes, which we pass as the second argument.

We now have implemented a way to assign our list from storageto lfgList when the SaveGame is loaded, and a way to store this list once the game is saved. In between these two events, we want players to be added or removed from lfgList via our command. Let's create our command delegate!

Handling the Command

Server chat commands have three parameters: the player issuing the command, the group in which this command was entered, and the arguments sent for the command.

        private TextCommandResult OnLfg(TextCommandCallingArgs args)
        {
            string cmd = args[0] as String;
            switch (cmd)
            {
                case "join":
                    break;

                case "leave":
                    break;

                case "list":
                    break;

                default:
                    return TextCommandResult.Error("/lfg [list|join|leave]");
            }
        }

We use args[0] to collect the first argument passed (ignoring anything subsequent). We then start a switch statement for our valid arguments, and default to showing these to the player if none of them match or cmd is null.

Let's handle each of these:

                case "join":
                    if (lfgList.Contains(args.Caller.Player.PlayerUID))
                    {
                        return TextCommandResult.Error("You're already in the list!");
                    }
                    else
                    {
                        lfgList.Add(args.Caller.Player.PlayerUID);
                        return TextCommandResult.Success("Successfully joined.");
                    }

If /lfg join was entered, we'll first check if the player is already on the "lfg" list, letting the player know if so. Alternatively, we add the player's unique identifier to the list and show a success message.

Hint: We do not want to store player names directly. This is because player names may change, therefore our list could become inaccurate.

Next we handle /lfg leave:

                case "leave":
                    if (!lfgList.Remove(args.Caller.Player.PlayerUID))
                    {
                        return TextCommandResult.Error("You're not in the list!");
                    }
                    else
                    {
                        return TextCommandResult.Success("Successfully left.");
                    }

The Remove method returns false if nothing matching the argument passed to it was found and removed, and true if it was. We let the player know if they were not on the list, or if they were and got successfully taken out of it.

Finally, we handle /lfg list:

                case "list":
                    if (lfgList.Count == 0)
                    {
                        return TextCommandResult.Success("No one is looking for a group.");
                    }
                    else
                    {
                        string response = "Players looking for group:";
                        lfgList.ForEach((playerUid) =>
                        {
                            response += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                        });

                        return TextCommandResult.Success(response);
                    }

In this case we simply let the player know if there is noone on the list, but if there are we build a string with all the player names on the list.

Conclusion

If you followed the steps correctly, you should have the following code:

using System.Collections.Generic;
using Vintagestory.API.Common;
using Vintagestory.API.Server;
using Vintagestory.API.Util;

namespace Vintagestory.ServerMods
{
    class LookingForGroup : ModSystem
    {
        ICoreServerAPI serverApi;
        List<string> lfgList;
        public override void StartServerSide(ICoreServerAPI api)
        {
            serverApi = api;

            api.Event.SaveGameLoaded += OnSaveGameLoading;
            api.Event.GameWorldSave += OnSaveGameSaving;

            api.ChatCommands.Create("lfg")
                .WithDescription("List or join the lfg list")
                .RequiresPrivilege(Privilege.chat)
                .RequiresPlayer()
                .WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" }))
                .HandleWith(new OnCommandDelegate(OnLfg));
        }

        private void OnSaveGameLoading()
        {
            byte[] data = serverApi.WorldManager.SaveGame.GetData("lfg");

            lfgList = data == null ? new List<string>() : SerializerUtil.Deserialize<List<string>>(data);
        }
        private void OnSaveGameSaving()
        {
            serverApi.WorldManager.SaveGame.StoreData("lfg", SerializerUtil.Serialize(lfgList));
        }

        private TextCommandResult OnLfg(TextCommandCallingArgs args)
        {
            string cmd = args[0] as String;
            switch (cmd)
            {
                case "join":
                    if (lfgList.Contains(args.Caller.Player.PlayerUID))
                    {
                        return TextCommandResult.Error("You're already in the list!");
                    }
                    else
                    {
                        lfgList.Add(args.Caller.Player.PlayerUID);
                        return TextCommandResult.Success("Successfully joined.");
                    }

                case "leave":
                    if (!lfgList.Remove(args.Caller.Player.PlayerUID))
                    {
                        return TextCommandResult.Error("You're not in the list!");
                    }
                    else
                    {
                        return TextCommandResult.Success("Successfully left.");
                    }

                case "list":
                    if (lfgList.Count == 0)
                    {
                        return TextCommandResult.Success("No one is looking for a group.");
                    }
                    else
                    {
                        string response = "Players looking for group:";
                        lfgList.ForEach((playerUid) =>
                        {
                            response += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                        });

                        return TextCommandResult.Success(response);
                    }

                default:
                    return TextCommandResult.Error("/lfg [list|join|leave]");
            }
        }
    }
}

Testing

Let's test our mod. Once you're ingame, try entering /lfg join. Now quit the game and join back in. Upon entering /lfg list, you should see your name on the list; this means that your custom data has persisted in the SaveGame!


Code Modding
Basics Code Mods Preparing For Code Mods Creating A Code Mod
Tutorials
Advanced Server-Client Considerations Setting up your Development Environment Advanced Blocks Advanced Items Block and Item Interactions Block Behavior Block Entity Particle Effects World Access Inventory Handling Commands GUIs Network API Monkey patching (Harmony)
Data Management VCDBS format Savegame Moddata ModConfig File Chunk Moddata Serialization Formats TreeAttribute
Worldgen WorldGen API NatFloat EvolvingNatFloat
Rendering Shaders and Renderers
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 ItemEntityEntity BehaviorsBlockBlock BehaviorsBlock ClassesBlock EntitiesBlock Entity BehaviorsCollectible 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