Моддинг: Хранение чанков данных

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

This page was last verified for Vintage Story version ?.??.

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

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

В этом примере мода мы покажем вам, как хранить пользовательские данные в чанках, представленных на стороне сервера. Мы создадим мод, который отслеживает, сколько раз игроки умерли на данном чанке, и эта сумма общего количества смертей на чанке будет использоваться для нанесения урона спящим в нем игрокам! Пугающий!

Мы будем использовать Dictionary<IServerChunk, int> для хранения списка фрагментов и количества смертей игроков, произошедших на них (этот уровень мы будем называть преследующим уровнем). Этот список будет обновляться в игре по мере того, как игроки умирают или используют кровати, и уровень привидения каждого фрагмента будет извлекаться из пользовательских данных, если фрагмент когда-либо имел такое значение. Когда игра сохраняется, мы перебираем наш Dictionary<IServerChunk, int> и сохраняем уровень привидения для каждого фрагмента в виде постоянных пользовательских данных!

Подготовка

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

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

namespace Vintagestory.ServerMods
{
    internal class ChunkDataStorage : ModSystem
    {
        private ICoreServerAPI serverApi;
        private Dictionary<IServerChunk, int> hauntedChunks;

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

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

            api.Event.PlayerDeath += OnPlayerDeath;
            api.Event.DidUseBlock += OnUseBlock;
        }
    }
}

In this class, we declare the field serverApi for the server api, and hauntedChunks which will be a list of hunated chunks and their haunting level. We also create an override for the StartServerSide method, which is a member of ModSystem and is called for all mods on the Server side by the game.

The Core Server API contains an additional API for Event registration; this object contains a list of server sided events that we can attach our delegates to, which will be invoked every time the event fires. We do this by assigning a delegate to an exposed event with the += operator. In our case, we register to four seperate events, each with their own delegates that we'll define later. The purpose of each is as follows:

  • api.Event.SaveGameLoaded += OnSaveGameLoading; We create a blank, new instance of a hauntedChunks when the game loads.
  • api.Event.GameWorldSave += OnSaveGameSaving; We'll store each chunk's haunting level as custom chunk data.
  • api.Event.PlayerDeath += OnPlayerDeath; When a player dies, we want to increase haunting level on the chunk they died in by 1.
  • api.Event.DidUseBlock += OnUseBlock; We check if the player used a bed, if so we try damaging them according to how haunted the chunk is.

Let us now define these event delegates!

Load and Save delegates

Let's begin by defining the delegates that fire when the game loads and when the game saves:

        private void OnSaveGameLoading()
        {
            hauntedChunks = new Dictionary<IServerChunk, int>();
        }

        private void OnSaveGameSaving()
        {
            foreach (KeyValuePair<IServerChunk, int> chunk in hauntedChunks)
            {
                if(chunk.Value == 0) continue;
                chunk.Key.SetServerModdata("haunting", SerializerUtil.Serialize(chunk.Value));
            }
        }

With OnSaveGameLoading we simply create a new instance of a Dictionary in which we hold all the haunted chunks as IServerChunk for the key, and the chunk's haunting level as int for the value.

OnSaveGameSaving has the purpose of going through every chunk we've collected from the last time the server launched, storing the haunting level as byte[]. We do this with a foreach loop that iterates through all the entries of hauntedChunks and sets custom data for each entry's chunk according to its haunting level. The SetServerModdata method's first argument is an arbitrary string key, which we choose to name "haunting"; the second argument passed is an array of byte, which we get by using the Serialize method of the SerializerUtil class. This simply turns data (in our case an int) into a byte[].

Let's now define the other two delegates!

Player death and block use delegates

        private void OnPlayerDeath(IServerPlayer byPlayer, DamageSource damageSource)
        {
            IServerChunk chunk = serverApi.WorldManager.GetChunk(byPlayer.Entity.ServerPos.AsBlockPos);

            if (!hauntedChunks.ContainsKey(chunk))
            {
                AddChunkToDictionary(chunk);
            }

            hauntedChunks[chunk]++;
        }

The OnPlayerDeath delegate that we used to listen for player death events, will first retrieve the chunk the player died on, by calling the GetChunk method of the server's WorldManager, an object that is used for a myriad of game world functionality such as accessors for Blocks, Chunks and more. For more information, check out the documentation here. Once we have the chunk's instance as IServerChunk, we check to see if we're not already holding this chunk in memory by seeing if hauntedChunks contains it. If it doesn't we'll add it with a method we'll call AddChunkToDictionary, and this method should look like this:

        private void (IServerChunk chunk)
        {
            byte[] data = chunk.GetServerModdata("haunting");
            int haunting = data == null ? 0 : SerializerUtil.Deserialize<int>(data);

            hauntedChunks.Add(chunk, haunting);
        }

Here we simply call GetServerModdata, which returns the data stored in the exposing chunk in the form of a byte[], or null if nothing is stored under the given string identifier. In our case we pass it "haunting" which is what we chose to save our haunting level under. We then declare a new int called haunting, and use a ternary operator to assign 0 if data is null and if not we use the Deserialize<T> method to convert data into type T (in our case, an integer).

Hint: Holding custom data as byte[] means we don't know know the type that is stored. This warrants extra attention when using it, and we should make sure we're always storing and retrieving the same type under for a given key.

Now let's handle our final delegate.

        private void OnUseBlock(IServerPlayer byPlayer, BlockSelection blockSel)
        {
            if (serverApi.World.BlockAccessor.GetBlock(blockSel.Position).GetType() != typeof(BlockBed))
            {
                return;
            }

            IServerChunk chunk = serverApi.WorldManager.GetChunk(blockSel.Position);

            if (!hauntedChunks.ContainsKey(chunk))
            {
                AddChunkToDictionary(chunk);
            }

            int haunting = hauntedChunks[chunk];
            if (haunting > 0)
            {
                byPlayer.Entity.ReceiveDamage(new DamageSource()
                {
                    Source = EnumDamageSource.Void,
                    Type = EnumDamageType.BluntAttack
                }, haunting);
            }
        }

We begin by checking whether or not the block used is of type BlockBed, which is a block class defined in the game's Survival mod that corresponds to all beds in the base game. We find the block the player is using by using the GetBlock method of the World's BlockAccessor object which holds a lot of ingame block functionality, such as getting, removing and replacing blocks. Check out the documentation for more info. If the block being used by the player is not a bed, there's no need to go further so we return.

Hint: Note that the BlockAccessor is part of the World object, which is not to be confused with the aforementioned WorldManager object. The main difference being that WorldManager is only Server sided, while World is on both Client and Server side, and contains functionality to access more general aspects of the game world and has a broader scope.

We then get the chunk, the same way we got it previously by exposing GetChunk, but this time we pass it the position of the bed block. We then check if the hauntedChunks list contains the chunk, if not we add it with AddChunkToDictionary.

Finally, we check if the chunk's haunting level is above 0, and if so we damage the player by using the ReceiveDamage method of the player's Entity. This method takes two arguments. The first one is the DamageSource which we pass in as newly created instance. The Source and Type fields we populate determine how the player's entity will handle this damage. For instance, damage of type Heal will increase the entity's health points! The second argument passed is the amount of damage we want the entity to receive, in our case this number will be the haunting level of this chunk, aka the amount of player deaths that happened on the chunk.

The mod is now complete!

Conclusion

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

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

namespace Vintagestory.ServerMods
{
    internal class ChunkDataStorage : ModSystem
    {
        private ICoreServerAPI serverApi;
        private Dictionary<IServerChunk, int> hauntedChunks;

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

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

            api.Event.PlayerDeath += OnPlayerDeath;
            api.Event.DidUseBlock += OnUseBlock;
        }

        private void OnSaveGameLoading()
        {
            hauntedChunks = new Dictionary<IServerChunk, int>();
        }

        private void OnSaveGameSaving()
        {
            foreach (KeyValuePair<IServerChunk, int> chunk in hauntedChunks)
            {
                if(chunk.Value == 0) continue;
                chunk.Key.SetServerModdata("haunting", SerializerUtil.Serialize(chunk.Value));
            }
        }

        private void OnPlayerDeath(IServerPlayer byPlayer, DamageSource damageSource)
        {
            IServerChunk chunk = serverApi.WorldManager.GetChunk(byPlayer.Entity.ServerPos.AsBlockPos);

            if (!hauntedChunks.ContainsKey(chunk))
            {
                AddChunkToDictionary(chunk);
            }

            hauntedChunks[chunk]++;
        }

        private void OnUseBlock(IServerPlayer byPlayer, BlockSelection blockSel)
        {
            if (serverApi.World.BlockAccessor.GetBlock(blockSel.Position).GetType() != typeof(BlockBed))
            {
                return;
            }

            IServerChunk chunk = serverApi.WorldManager.GetChunk(blockSel.Position);

            if (!hauntedChunks.ContainsKey(chunk))
            {
                AddChunkToDictionary(chunk);
            }

            int haunting = hauntedChunks[chunk];
            if (haunting > 0)
            {
                byPlayer.Entity.ReceiveDamage(new DamageSource()
                {
                    Source = EnumDamageSource.Void,
                    Type = EnumDamageType.BluntAttack
                }, haunting);
            }
        }

        private void AddChunkToDictionary(IServerChunk chunk)
        {
            byte[] data = chunk.GetServerModdata("haunting");
            int haunting = data == null ? 0 : SerializerUtil.Deserialize<int>(data);

            hauntedChunks.Add(chunk, haunting);
        }
    }
}

Testing

Let's run the mod now! Once you're ingame, enter the command /kill. From now on, you will receive 1 damage every time you sleep in the chunk you died in.

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