Моддинг:Хранение данных SaveGame

From Vintage Story Wiki
This page is a translated version of the page Modding:SaveGame Data Storage and the translation is 100% complete.

This page was last verified for Vintage Story version 1.15.

Other languages:

Перед запуском у вас должна быть настроена среда разработки. Если у вас его еще нет, вам следует прочитать руководство Настройка среды разработки. Кроме того, мы предполагаем, что у вас есть базовые знания языка C# и объектно-ориентированного программирования. Давайте начнем!

Введение

VintageStory позволяет вам устанавливать и извлекать пользовательские данные для сохранения игры в мире, а также для отдельных фрагментов. На самом деле это работает очень просто!

Пользовательские данные хранятся в виде массива байтов для экономии места, но мы можем легко преобразовать любой допустимый тип C# в byte[] с помощью очень удобного класса в API VintageStory: vintagestory.at/api/Vintagestory.API.Util.SerializerUtil.html SerializerUtil. Этот класс предоставляет нам два статических метода, которые позволяют нам быстро Serialize в байтовые массивы и Deserialize<T> их в другие типы.

Вы можете ознакомиться с методами хранения и извлечения этих массивов байтов для фрагментов и для at/api/Vintagestory.API.Server.ISaveGame.html#methods SaveGame

Пользовательские данные в SaveGame

Давайте покажем вам этот мощный набор инструментов, создав пример мода, в котором мы реализуем открытый список игроков, которые в настоящее время ищут группу для игры в игре.

Этот список будет List<string> игроков, который будет сохранен в SaveGame мира. Любой может присоединиться к ней, выйти из нее или посмотреть, кому нужна группа. Мы сохраним этот список в виде массива байтов, доступ к которому можно получить по ключу «lfg».

Подготовка

Давайте начнем с создания нового файла .cs для этого мода и добавления нашего импорта и пространства имен VintageStory.ServerMods, чтобы обернуть наш класс. Кроме того, мы объявим класс LookingForGroup</ code>, который будет унаследован от ModSystem, который является базовым классом для систем модов. Подробнее об этом можно прочитать здесь.

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

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 void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
        {
            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 or cmd is null.

Let's handle each of these:

                case "join":
                    if (lfgList.Contains(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're already in the list!", EnumChatType.CommandError);
                    }
                    else
                    {
                        lfgList.Add(player.PlayerUID);
                        player.SendMessage(groupId, "Successfully joined.", EnumChatType.CommandSuccess);
                    }
                    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 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(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're not in the list!", EnumChatType.CommandError);
                    }
                    else
                    {
                        player.SendMessage(groupId, "Successfully left.", EnumChatType.CommandSuccess);
                    }
                    break;

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)
                    {
                        player.SendMessage(groupId, "Noone is looking for a group.", EnumChatType.CommandSuccess);
                    }
                    else
                    {
                        string response = "Players looking for group:";
                        lfgList.ForEach((playerUid) =>
                        {
                            response += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
                        });

                        player.SendMessage(groupId, response, EnumChatType.CommandSuccess);
                    }
                    break;

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.RegisterCommand("lfg", "Access the lfg list.", "[list|join|leave]", 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 void OnLfg(IServerPlayer player, int groupId, CmdArgs args)
        {
            string cmd = args.PopWord();
            switch (cmd)
            {
                case "join":
                    if (lfgList.Contains(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're already in the list!", EnumChatType.CommandError);
                    }
                    else
                    {
                        lfgList.Add(player.PlayerUID);
                        player.SendMessage(groupId, "Successfully joined.", EnumChatType.CommandSuccess);
                    }
                    break;

                case "leave":
                    if (!lfgList.Remove(player.PlayerUID))
                    {
                        player.SendMessage(groupId, "You're not in the list!", EnumChatType.CommandError);
                    }
                    else
                    {
                        player.SendMessage(groupId, "Successfully left.", EnumChatType.CommandSuccess);
                    }
                    break;

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

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

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:


Modding
Green Items require C# coding
Basics

Getting Started | Mod Types | Simple Examples | Theme Pack

Asset System | Textures | Items | Recipes | Blocks | Entities | Model Creator | Animation Basics | VTML & Icons | Mod Packaging & Release | Modinfo | Debugging

Advanced

JSON Patching | Advanced JSON Items | The Remapper | Server-Client Considerations | Compatibility with other mods

Setting Up Your Development Environment (General - Windows - Linux)

Advanced Blocks | Advanced Items | Item-Block Interactions | Block Behavior | Block Entities | Particle Effects | World Access | Inventory Handling | Chat Commands | GUIs | Server-Client Networking | Monkey patching (Harmony)

Data Management

Savegame Data Storage | ModConfig File | Chunk Data Storage | Tree Attribute

Worldgen

WorldGen Concepts | Terrain | Ores | Trees | WorldGen API

Rendering

Shaders and Renderers

Property Overview

Item | Entity | Block | Block Behaviors | Block Classes | Block Entities | Block Entity Behaviors

Workflows & Infrastructure

Modding Efficiency Tips | Mod-engine compatibility | Mod Extensibility | Load Order

Additional Resources

List of server commands | List of client commands | Client startup parameters | Creative Starter Guide | ServerBlockTicking | Bot System | WorldEdit | Cinematic Camera

Example Mods | API Docs | GitHub Repository