Modding:SaveGame Data Storage

From Vintage Story Wiki
Revision as of 19:28, 14 April 2020 by Tylermcwilliams (talk | contribs) (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...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search


Before starting, you should have a development environment set up. If you don't have one already you should read the tutorial Modding: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

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 data is stored as an array of bytes to save space, but we can easily convert any valid C# type to byte[] with a very convenient class in the VintageStory API: SerializerUtil. This class provides us with two static methods that allow us to quickly Serialize into byte arrays, and Deserialize<T> them into C# types.

You can check out the methods for storing and retrieving these arrays of bytes for Chunks and for SaveGame

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 the VintageStory.ServerMods namespace. Additionally, we'll declare our class for the mod which will inheret 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;
        public override void StartServerSide(ICoreServerAPI api)
        {
            serverApi = api;

            api.RegisterCommand("lfg", "List or join the lfg list", "[list|join|leave]", 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.

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 /lfg with the arguments list, join or leave.

Let's create the delegate OnLfg that will be called when a player enters this command.

Storing and Retrieving the List

Right after our StartServerSide override, let's create our /lfg command delegate:

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

        }

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 if (args.Length < 1). 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 WorldManager which we use to access everything World related, including our SaveGame.

        private void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
        {
            if (args.Length < 1)...

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

            List<string> players = 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 identifier, 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 data type we actually store on the SaveGame. Let's convert it to a List of strings:

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

Here we use a ternary operator to assign our players list 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 our list of players "lfg", let's handle the possible arguments of the command.

        private void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
        {
            if (args.Length < 1)...

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

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

            string cmd = args.PopWord();

            switch (cmd)
            {
                case "join":
                    break;

                case "leave":
                    break;

                case "list":
                    break;

                default:
                    player.SendMessage(groupId, "/lfg [list|join|leave]", EnumChatType.CommandError);
                    break;
            }
        }

We use the PopWord of our CmdArgs 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.

Let's handle each of these:

                case "join":
                    if (players.Contains(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're already in this list!", EnumChatType.Notification);
                    }
                    else
                    {
                        players.Add(player.PlayerUID);
                        data = SerializerUtil.Serialize(players);

                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);

                        player.SendMessage(groupId, "Successfully joined!", EnumChatType.Notification);
                    }
                    break;

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 use the Serialize method to turn the updated players List<string> back to an array of byte.

After, we use the StoreData 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.

Now let's handle /lfg leave:

                case "leave":
                    if (!players.Remove(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're not in the list!", EnumChatType.Notification);
                    }
                    else
                    {
                        data = SerializerUtil.Serialize(players);

                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);

                        player.SendMessage(groupId, "Successfully left!", EnumChatType.Notification);
                    }
                    break;

The Remove 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 SaveGame, letting the player know his request was successful.

Finally, we handle /lfg list:

                case "list":
                    if (players.Count == 0)
                    {
                        player.SendMessage(groupId, "Noone is looking for group!", EnumChatType.Notification);
                        break;
                    }

                    string lfgList = "Players looking for group:";
                    players.ForEach((playerUid) =>
                    {
                        lfgList += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                    });

                    player.SendMessage(groupId, lfgList, EnumChatType.Notification);
                    break;

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!

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;
        public override void StartServerSide(ICoreServerAPI api)
        {
            serverApi = api;

            api.RegisterCommand("lfg", "List or join the lfg list", "[list|join|leave]", OnLfg);
        }

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

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

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

            string cmd = args.PopWord();

            switch (cmd)
            {
                case "join":
                    if (players.Contains(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're already in this list!", EnumChatType.Notification);
                    }
                    else
                    {
                        players.Add(player.PlayerUID);
                        data = SerializerUtil.Serialize(players);

                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);

                        player.SendMessage(groupId, "Successfully joined!", EnumChatType.Notification);
                    }
                    break;

                case "leave":
                    if (!players.Remove(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're not in the list!", EnumChatType.Notification);
                    }
                    else
                    {
                        data = SerializerUtil.Serialize(players);

                        serverApi.WorldManager.SaveGame.StoreData("lfg", data);

                        player.SendMessage(groupId, "Successfully left!", EnumChatType.Notification);
                    }
                    break;

                case "list":
                    if (players.Count == 0)
                    {
                        player.SendMessage(groupId, "Noone is looking for group!", EnumChatType.Notification);
                        break;
                    }

                    string lfgList = "Players looking for group:";
                    players.ForEach((playerUid) =>
                    {
                        lfgList += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                    });

                    player.SendMessage(groupId, lfgList, EnumChatType.Notification);
                    break;

                default:
                    player.SendMessage(groupId, "/lfg [list|join|leave]", EnumChatType.CommandError);
                    break;
            }
        }
    }
}

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!

Distribution

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: