Modding:Chunk Moddata: Difference between revisions

From Vintage Story Wiki
m (Bluelightning32 moved page Modding:Chunk Data Storage to Modding:Chunk Moddata without leaving a redirect: Part of translatable page "Modding:Chunk Data Storage")
(→‎Distribution: Show the code navbox instead of the generic modding navbox)
(8 intermediate revisions by the same user not shown)
Line 1: Line 1:
__FORCETOC__
__FORCETOC__
{{GameVersion|1.12}}
{{GameVersion|1.19.8}}
<languages/><translate>
<languages/><translate>
<!--T:1-->
<!--T:1-->
Line 8: Line 8:


<!--T:3-->
<!--T:3-->
VintageStory allows mods to set and retrieve custom data for their mod on individual chunks. The way it works is actually quite simple! The custom data is set with [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerChunk.html#Vintagestory_API_Server_IServerChunk_SetServerModdata_System_String_System_Byte___ IServerChunk.SetServerModdata] and read with [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerChunk.html#Vintagestory_API_Server_IServerChunk_GetServerModdata_System_String_ GetServerModdata]. The first parameter is key of type string to identify the custom data. The intention is for the modid to be used as the key, but really the methods will accept any string as the key.
Custom mod data can be attached to individual [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerChunk.html chunks]. Alternatively, mods can store custom [[Modding:SaveGame_ModData|save game mod data]] that applies to the entire save game.
 
Each chunk has two dictionaries where custom mod data can be attached. The shared mod data dictionary and the server mod data dictionary. Both of these dictionaries have a string key and a byte array value. Logically '''LiveModData''' is another accessor for the shared mod dictionary, but technically it is third dictionary. Every time the chunk is serialized, all of the entries in '''LiveModData''' are serialized into byte arrays and stored in the shared mod data dictionary, overriding any existing entries with the same key.
<table class="wikitable">
  <tr style="background-color: rgba(0,0,0,0.2);">
    <th style="background-color: rgba(0,0,0,0.2);">Dictionary</th>
    <th style="background-color: rgba(0,0,0,0.2);">Readable on</th>
    <th style="background-color: rgba(0,0,0,0.2);">Writable on</th>
    <th style="background-color: rgba(0,0,0,0.2);">Serialized to</th>
    <th style="background-color: rgba(0,0,0,0.2);">Accessors</th>
  </tr>
  <tr>
    <td>shared mod data</td>
    <td>client and server</td>
    <td>server</td>
    <td>
* save game on disk
* client over network
    </td>
    <td>
* [https://apidocs.vintagestory.at/api/Vintagestory.API.Common.IWorldChunk.html#Vintagestory_API_Common_IWorldChunk_LiveModData IWorldChunk.LiveModData]
* [https://apidocs.vintagestory.at/api/Vintagestory.API.Common.IWorldChunk.html#Vintagestory_API_Common_IWorldChunk_SetModdata_System_String_System_Byte___ IWorldChunk.SetModdata] - has race serialization race conditions. Use '''LiveModData''' instead.
* [https://apidocs.vintagestory.at/api/Vintagestory.API.Common.IWorldChunk.html#Vintagestory_API_Common_IWorldChunk_GetModdata_System_String_ IWorldChunk.GetModdata]
    </td>
  </tr>
  <tr>
    <td>server mod data</td>
    <td>server</td>
    <td>server</td>
    <td>
* save game on disk
    </td>
    <td>
* [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerChunk.html#Vintagestory_API_Server_IServerChunk_GetServerModdata_System_String_ IServerChunk.GetServerModdata]
* [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerChunk.html#Vintagestory_API_Server_IServerChunk_SetServerModdata_System_String_System_Byte___ IServerChunk.SetServerModdata]
    </td>
  </tr>
</table>


<!--T:4-->
<!--T:4-->
The dictionary values are byte arrays, but there is a utility function to convert any [https://loyc.net/2013/protobuf-net-unofficial-manual.html protobuf annotated] C# type to a <code>byte[]</code> 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 other types.
Both the server mod data dictionary and shared mod data dictionary are have string keys and byte array values. '''IWorldChunk.LiveModData''' internally uses [http://apidocs.vintagestory.at/api/Vintagestory.API.Util.SerializerUtil.html <code>SerializerUtil</code>] to convert rich types into byte arrays. '''SetServerModdata''' does not have such a convenience wrapper. So developers need to manually use a serializer such as '''SerializerUtil.Serialize''' to convert a rich object into byte array to give '''SetServerModdata'''.


<!--T:5-->
Implementation-wise any string can be used as a key in the mod data dictionaries, but the best practice is to use a string that starts with the mod identifier, which is "haunting" in this tutorial.
In addition to attaching custom data to chunks, custom data can be attached to the world. See [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.ISaveGame.html#Vintagestory_API_Server_ISaveGame_GetData_System_String_ ISaveGame.GetData] and [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.ISaveGame.html#Vintagestory_API_Server_ISaveGame_StoreData_System_String_System_Byte___ ISaveGame.StoreData].


==Custom Chunk Data== <!--T:6-->
==Custom Chunk Data== <!--T:6-->


<!--T:7-->
<!--T:7-->
In this example mod we'll show you how to store custom data to chunks represented on the server side. We'll create a mod that keeps track of how many times players have died on a given chunk, and this sum of total deaths on the chunk will be used to inflict damage onto players sleeping in it! Spooky!
In this example mod we'll show you how to store custom data to chunks represented on the server side. We'll create a mod that keeps track of how many times players have died on a given chunk. Every time a player tries to sleep in a bed, the sum of total deaths on the chunk will be used to inflict damage onto the player! Spooky!


Logically the count of deaths in a chunk is an integer. However, as described earlier, for serialization purposes, that int needs to be stored as a byte array on the chunk. But, an int is more convenient for processing outside serialization purposes. So we'll use a <code>Dictionary<IServerChunk, int></code> to map chunks to the amount of player deaths that occurred on them (which we'll call haunting level). That dictionary will be updated when a player dies, and that dictionary will be read when a player uses a bed.
Logically the count of deaths in a chunk is an integer. However, as described earlier, for serialization purposes, that int needs to be stored as a byte array on the chunk. But, an int is more convenient for processing outside serialization purposes. So we'll wrap the integer in a '''HauntingChunkData''' class. The '''[ProtoContract]''' and lets the serializer know that it is allowed to serialize the class. The '''[ProtoMember(1)]''' assigns an identifier to the '''Deaths''' field. Any previously unused identifier can be used. The identifier is used to track the field in case the field is later renamed, removed, or new fields are added. Since this is the first version of the mod, such versioning concerns do not apply.
<syntaxhighlight lang='c#'>
[ProtoContract]
public class HauntingChunkData {
[ProtoMember(1)]
  public int Deaths;
}
</syntaxhighlight>
 
Since all of the death tracking and bed tracking logic runs on the server side, the '''HauntingChunkData''' will be stored in the server mod data instead of the shared mod data.
 
Every time a bed is used, this mod will find the chunk containing the block, then look up the number of deaths in the chunk. Each time a bed is used, it would be possible to deserialize the '''HauntingChunkData''' directly from the server mod data in the chunk. Using a bed occurs rarely enough that the deserialization cost of this approach would be acceptable. However, for pedagogical purposes, this mod uses a more CPU efficient approach: the mod adds a separate dictionary to directly look up the deaths for cached chunks. Later this tutorial will explain how to synchronize '''_hauntedChunks''' with the server mod data dictionary.
<syntaxhighlight lang='c#'>
  private readonly ConditionalWeakTable<IServerChunk, HauntingChunkData> _hauntedChunks = new();
</syntaxhighlight>
 
'''LiveModData''' contains unserialized objects. This example mod does not store the death count in '''LiveModData''', because '''LiveModData''' serializes its entries to the shared mod data dictionary. However, for mods that use the shared mod data, the performance of directly reading from '''LiveModData''' is lower than directly reading from the server mod data, because reading from '''LiveModData''' skips the deserialization stage for already loaded chunks. However, a mod specific cached chunk dictionary is still faster, because it can skip the string hash call necessary to look up an entry in '''LiveModData''' (in exchange for a cheap '''IServerChunk''' hash call).


<!--T:8-->
<!--T:8-->
When the game saves, we iterate through our <code>Dictionary<IServerChunk, int></code>, convert the number of death integers into byte arrays, and attach each byte array to its corresponding chunk. Vintage Story will take care of saving that mod data in the game save file.
When the game saves, we iterate through our <code>Dictionary<IServerChunk, int></code>, convert the number of death integers into byte arrays, and attach each byte array to its corresponding chunk. Vintage Story will take care of saving that mod data in the game save file.
==Chunk Serialization Triggers==
When a chunk is marked as modified, it is eventually serialized into the save game on disk. A chunk is marked as modified in many conditions, such as when an entity is created in it, or a block is modified. However, when storing custom mod data in the chunk, the safest option is to directly marked as modified by calling [https://apidocs.vintagestory.at/api/Vintagestory.API.Common.IWorldChunk.html#Vintagestory_API_Common_IWorldChunk_MarkModified IWorldChunk.MarkModified]. This example mod relies on the player dying to mark the chunk as modified.
Modified chunks will get serialized by the next save game event. They will get serialized sooner if the chunk is unloaded. Both of these operations run on the chunk thread (not the main thread). Care must be taken with updating the mod data dictionaries at the right time. The dictionaries should be updated right before they are serialized. If the dictionaries are updated too early before the chunk is locked, then the main thread can further modify the chunk before the serialization. Then the chunk will be in an inconsistent state. Maybe the compressed blocks will be newer than the mod data. Although for most mods, small race conditions like this would be unnoticeable.
'''LiveModData''' serializes its fields at exactly the right time: on the chunk thread while the chunk is locked for serialization. There are no other events one can hook into to trigger serialization at the correct time. This is why '''LiveModData''' is strongly recommended over [https://apidocs.vintagestory.at/api/Vintagestory.API.Common.IWorldChunk.html#Vintagestory_API_Common_IWorldChunk_SetModdata_System_String_System_Byte___ IWorldChunk.SetModdata].
There is no equivalent to '''LiveModData''' for the server mod data. So instead this example mod stores a fake entry in '''LiveModData''' with a '''[ProtoBeforeSerialization]''' callback. It uses that callback to update the server mod data dictionary right before the chunk is serialized.
<syntaxhighlight lang='c#'>
[ProtoContract]
public class SerializationCallback {
  public delegate void OnSerializationDelegate(IServerChunk chunk);
  readonly private IServerChunk _chunk;
  public OnSerializationDelegate OnSerialization;
  public SerializationCallback(IServerChunk chunk) {
    _chunk = chunk;
  }
  [ProtoBeforeSerialization]
  private void BeforeSerialization() {
    OnSerialization(_chunk);
  }
}
...
    if (!chunk.LiveModData.TryGetValue("haunting", out object serializerObj) || serializerObj is not SerializationCallback serializer) {
      serializer = new(chunk);
      chunk.LiveModData["haunting"] = serializer;
    }
    serializer.OnSerialization += chunk => chunk.SetServerModdata("haunting", SerializerUtil.Serialize(chunkData));
</syntaxhighlight>
An earlier version of haunting mod relied the [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerEventAPI.html#Vintagestory_API_Server_IServerEventAPI_GameWorldSave IServerEventAPI.GameWorldSave] event. However, that event is not called before chunks are unloaded. It is also called too early in the save game process, such that the chunk can be modified again before it is serialized.
In addition to saving chunks to disk, nearby chunks are sent to clients when they first join the server, or when they walk into range of the chunk. When using shared mod data, the data may need to be sent more often by calling [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IWorldManagerAPI.html#Vintagestory_API_Server_IWorldManagerAPI_BroadcastChunk_System_Int32_System_Int32_System_Int32_System_Boolean_ IWorldManagerAPI.BroadcastChunk]. Note that marking a chunk as modified only triggers saving it to disk; it does not trigger sending the modified chunk to clients.


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


<!--T:10-->
<!--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>ChunkDataStorage</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].
Let's start by creating a new .cs file for this mod, and adding our imports and the <code>HauntedChunks</code> namespace to wrap our class in. Additionally, we'll declare the class <code>HauntedChunks</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>
</translate>


Line 39: Line 128:
using Vintagestory.API.Util;
using Vintagestory.API.Util;


namespace Vintagestory.ServerMods
namespace HauntedChunks;
{
    internal class ChunkDataStorage : ModSystem
    {
        private ICoreServerAPI serverApi;
        private Dictionary<IServerChunk, int> hauntedChunks;
 
        public override void StartServerSide(ICoreServerAPI api)
        {
            serverApi = api;


            api.Event.SaveGameLoaded += OnSaveGameLoading;
public class HauntedChunks : ModSystem {
            api.Event.GameWorldSave += OnSaveGameSaving;
  private ICoreServerAPI _serverApi;


            api.Event.PlayerDeath += OnPlayerDeath;
  public override void StartServerSide(ICoreServerAPI api) {
            api.Event.DidUseBlock += OnUseBlock;
    _serverApi = api;
        }
  }
    }
}
}
</syntaxhighlight>
</syntaxhighlight>


In this class, we declare the field <code>serverApi</code> for the server api, and <code>hauntedChunks</code> which will be a list of hunated chunks and their haunting level. We also create an override for the <code>StartServerSide</code> method, which is a member of <code>ModSystem</code> and is called for all mods on the Server side by the game.
The '''StartServerSide''' method is a member of '''ModSystem''' and is called for all mods on the Server side by the game. It is given a '''ICoreServerAPI''', which the mod will need later. So the mod overrides the method and saves it in '''_serverApi'''. '''hauntedChunks''' will be a list of hunated chunks and their haunting level.


The Core Server API contains an additional API for <code>Event</code> 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 <code>+=</code> 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:
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 two separate events, each with their own delegates that we'll define later. The purpose of each is as follows:


* <code>api.Event.SaveGameLoaded += OnSaveGameLoading;</code> We create a blank, new instance of a <code>hauntedChunks</code> when the game loads.
* '''[https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerEventAPI.html#Vintagestory_API_Server_IServerEventAPI_PlayerDeath api.Event.PlayerDeath] += OnPlayerDeath;''' When a player dies, we want to increase haunting level on the chunk they died in by 1.
* <code>api.Event.GameWorldSave += OnSaveGameSaving;</code> We'll store each chunk's haunting level as custom chunk data.
* '''[https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerEventAPI.html#Vintagestory_API_Server_IServerEventAPI_DidUseBlock 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. Note that '''DidUseBlock''' is called any time any block is right clicked. A more efficient -- but complicated -- approach would be to create a behavior that intercepts [[Modding:Adding_Block_Behavior|OnBlockInteractStart]], and install it on just the bed block.
* <code>api.Event.PlayerDeath += OnPlayerDeath;</code> When a player dies, we want to increase haunting level on the chunk they died in by 1.
* <code>api.Event.DidUseBlock += OnUseBlock;</code> 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!
Let us now define these event delegates!


===Load and Save delegates===
===Player death and block use delegates===


Let's begin by defining the delegates that fire when the game loads and when the game saves:
<syntaxhighlight lang='c#'>
  private void OnPlayerDeath(IServerPlayer byPlayer,
                            DamageSource damageSource) {
    IServerChunk chunk =
        _serverApi.WorldManager.GetChunk(byPlayer.Entity.ServerPos.AsBlockPos);


<syntaxhighlight lang='c#'>
    if (!_hauntedChunks.TryGetValue(chunk, out HauntingChunkData chunkData)) {
        private void OnSaveGameLoading()
      chunkData = AddChunkToDictionary(chunk, true);
        {
    }
            hauntedChunks = new Dictionary<IServerChunk, int>();
        }


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


With <code>OnSaveGameLoading</code> we simply create a new instance of a <code>Dictionary</code> in which we hold all the haunted chunks as <code>IServerChunk</code> for the key, and the chunk's haunting level as <code>int</code> for the value.
The '''OnPlayerDeath''' delegate that we used to listen for player death events, will first retrieve the chunk the player died on, by calling [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IWorldManagerAPI.html#Vintagestory_API_Server_IWorldManagerAPI_GetChunk_Vintagestory_API_MathTools_BlockPos_ IWorldManager.GetChunk]. Once we have the chunk's instance as [https://apidocs.vintagestory.at/api/Vintagestory.API.Server.IServerChunk.html 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:
 
<code>OnSaveGameSaving</code> has the purpose of going through every chunk we've collected from the last time the server launched, storing the haunting level as <code>byte[]</code>. We do this with a <code>foreach</code> loop that iterates through all the entries of <code>hauntedChunks</code> and sets custom data for each entry's chunk according to its haunting level. The <code>SetServerModdata</code> method's first argument is an arbitrary <code>string</code> key, which we choose to name "haunting"; the second argument passed is an array of <code>byte</code>, which we get by using the <code>Serialize</code> method of the <code>SerializerUtil</code> class. This simply turns data (in our case an <code>int</code>) into a <code>byte[]</code>.
 
Let's now define the other two delegates!
 
===Player death and block use delegates===


<syntaxhighlight lang='c#'>
<syntaxhighlight lang='c#'>
        private void OnPlayerDeath(IServerPlayer byPlayer, DamageSource damageSource)
  private HauntingChunkData AddChunkToDictionary(IServerChunk chunk, bool create) {
        {
    HauntingChunkData chunkData;
            IServerChunk chunk = serverApi.WorldManager.GetChunk(byPlayer.Entity.ServerPos.AsBlockPos);
    byte[] data = chunk.GetServerModdata("haunting");
    if (data != null) {
      chunkData = SerializerUtil.Deserialize<HauntingChunkData>(data);
    } else if(!create) {
      return null;
    } else {
      chunkData = new();
    }


            if (!hauntedChunks.ContainsKey(chunk))
    _hauntedChunks.Add(chunk, chunkData);
            {
    if (!chunk.LiveModData.TryGetValue("haunting", out object serializerObj) || serializerObj is not SerializationCallback serializer) {
                AddChunkToDictionary(chunk);
      serializer = new(chunk);
            }
      chunk.LiveModData["haunting"] = serializer;
 
    }
            hauntedChunks[chunk]++;
    serializer.OnSerialization += chunk => chunk.SetServerModdata("haunting", SerializerUtil.Serialize(chunkData));
        }
    return chunkData;
  }
</syntaxhighlight>
</syntaxhighlight>


The <code>OnPlayerDeath</code> delegate that we used to listen for player death events, will first retrieve the chunk the player died on, by calling the <code>GetChunk</code> method of the server's <code>WorldManager</code>, 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 [http://apidocs.vintagestory.at/api/Vintagestory.API.Server.IWorldManagerAPI.html here]. Once we have the chunk's instance as <code>IServerChunk</code>, we check to see if we're not already holding this chunk in memory by seeing if <code>hauntedChunks</code> contains it. If it doesn't we'll add it with a method we'll call <code>AddChunkToDictionary</code>, and this method should look like this:
'''_hauntedChunks''' acts like a cache. If the cache does not have the entry, maybe it already exists on disk. '''GetServerModdata''' is called to read the entry from disk (technically it is already in memory but not deserialized into '''_hauntedChunks''' yet). '''GetServerModdata''' returns a '''byte[]'''.
 
<syntaxhighlight lang='c#'>
        private void (IServerChunk chunk)
        {
            byte[] data = chunk.GetServerModdata("haunting");
            int haunting = data == null ? 0 : SerializerUtil.Deserialize<int>(data);


            hauntedChunks.Add(chunk, haunting);
If '''GetServerModdata''' returned non-null, then it deserializes the previous haunting level from disk into a '''HauntingChunkData''' object. If '''GetServerModdata''' returned null, then that means the mod has never attached a haunting level to the chunk. In this case, the '''create''' parameter indicates whether a new default '''HauntingChunkData''' should be attached to the chunk, or whether it should return null to indicate no data is attached.
        }
</syntaxhighlight>


Here we simply call <code>GetServerModdata</code>, which returns the data stored in the exposing chunk in the form of a <code>byte[]</code>, or null if nothing is stored under the given <code>string</code> identifier. In our case we pass it "haunting" which is what we chose to save our haunting level under. We then declare a new <code>int</code> called haunting, and use a ternary operator to assign 0 if data is null and if not we use the <code>Deserialize<T></code> method to convert data into type <code>T</code> (in our case, an integer).
As described earlier, a fake entry is added to '''LiveModData''' just to get a notification when the chunk is serialized. The mod responds to that notification by calling '''SetServerModdata''' to set the server mod data for the chunk. Note that the fake entry in '''LiveModData''' will still get serialized to disk (shown later in the tutorial), but the small overhead is worth getting notified at the correct time.


'''Hint''': Holding custom data as <code>byte[]</code> 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.
'''Hint''': Holding custom data as <code>byte[]</code> 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.
Line 132: Line 199:


<syntaxhighlight lang='c#'>
<syntaxhighlight lang='c#'>
        private void OnUseBlock(IServerPlayer byPlayer, BlockSelection blockSel)
  private void OnUseBlock(IServerPlayer byPlayer, BlockSelection blockSel) {
        {
    if (_serverApi.World.BlockAccessor.GetBlock(blockSel.Position).GetType() !=
            if (serverApi.World.BlockAccessor.GetBlock(blockSel.Position).GetType() != typeof(BlockBed))
        typeof(BlockBed)) {
            {
      return;
                return;
    }
            }


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


            if (!hauntedChunks.ContainsKey(chunk))
    if (!_hauntedChunks.TryGetValue(chunk, out HauntingChunkData chunkData)) {
            {
      chunkData = AddChunkToDictionary(chunk, false);
                AddChunkToDictionary(chunk);
    }
            }


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


We begin by checking whether or not the block used is of type <code>BlockBed</code>, 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 <code>GetBlock</code> method of the World's <code>BlockAccessor</code> object which holds a lot of ingame block functionality, such as getting, removing and replacing blocks. Check out the [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.IBlockAccessor.html 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.
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 in-game block functionality, such as getting, removing and replacing blocks. Check out the [http://apidocs.vintagestory.at/api/Vintagestory.API.Common.IBlockAccessor.html 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 <code>BlockAccessor</code> is part of the <code>World</code> object, which is not to be confused with the aforementioned <code>WorldManager</code> object. The main difference being that WorldManager is only Server sided, while <code>World</code> is on both Client and Server side, and contains functionality to access more general aspects of the game world and has a broader scope.
'''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 <code>GetChunk</code>, but this time we pass it the position of the bed block. We then check if the <code>hauntedChunks</code> list contains the chunk, if not we add it with <code>AddChunkToDictionary</code>.
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 then call '''AddChunkToDictionary''' to possibly add it. The second argument to '''AddChunkToDictionary''' is false, to indicate that it should only load the haunting data if it already exists in the chunk in the save file. If it does not exist, then '''AddChunkToDictionary''' returns null. The later <code>chunkData?.Deaths ?? 0</code> converts a null into a 0.


Finally, we check if the chunk's haunting level is above 0, and if so we damage the player by using the <code>ReceiveDamage</code> method of the player's <code>Entity</code>. This method takes two arguments. The first one is the <code>DamageSource</code> which we pass in as newly created instance. The <code>Source</code> and <code>Type</code> fields we populate determine how the player's entity will handle this damage. For instance, damage of type <code>Heal</code> 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.
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!
The mod is now complete!
Line 178: Line 241:
using Vintagestory.API.Server;
using Vintagestory.API.Server;
using Vintagestory.API.Util;
using Vintagestory.API.Util;
using ProtoBuf;
using System.Runtime.CompilerServices;
namespace HauntedChunks;
[ProtoContract]
public class SerializationCallback {
  public delegate void OnSerializationDelegate(IServerChunk chunk);


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


        public override void StartServerSide(ICoreServerAPI api)
  public SerializationCallback(IServerChunk chunk) {
        {
    _chunk = chunk;
            serverApi = api;
  }
 
[ProtoBeforeSerialization]
  private void BeforeSerialization() {
    OnSerialization(_chunk);
  }
}


            api.Event.SaveGameLoaded += OnSaveGameLoading;
[ProtoContract]
            api.Event.GameWorldSave += OnSaveGameSaving;
public class HauntingChunkData {
[ProtoMember(1)]
  public int Deaths;
}


            api.Event.PlayerDeath += OnPlayerDeath;
public class HauntedChunks : ModSystem {
            api.Event.DidUseBlock += OnUseBlock;
  private ICoreServerAPI _serverApi;
        }
  private readonly ConditionalWeakTable<IServerChunk, HauntingChunkData> _hauntedChunks = new();


        private void OnSaveGameLoading()
  public override void StartServerSide(ICoreServerAPI api) {
        {
    _serverApi = api;
            hauntedChunks = new Dictionary<IServerChunk, int>();
        }


        private void OnSaveGameSaving()
    api.Event.PlayerDeath += OnPlayerDeath;
        {
    api.Event.DidUseBlock += OnUseBlock;
            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)
  private void OnPlayerDeath(IServerPlayer byPlayer,
        {
                            DamageSource damageSource) {
            IServerChunk chunk = serverApi.WorldManager.GetChunk(byPlayer.Entity.ServerPos.AsBlockPos);
    IServerChunk chunk =
        _serverApi.WorldManager.GetChunk(byPlayer.Entity.ServerPos.AsBlockPos);


            if (!hauntedChunks.ContainsKey(chunk))
    if (!_hauntedChunks.TryGetValue(chunk, out HauntingChunkData chunkData)) {
            {
      chunkData = AddChunkToDictionary(chunk, true);
                AddChunkToDictionary(chunk);
    }
            }


            hauntedChunks[chunk]++;
    chunkData.Deaths++;
        }
  }


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


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


            if (!hauntedChunks.ContainsKey(chunk))
    if (!_hauntedChunks.TryGetValue(chunk, out HauntingChunkData chunkData)) {
            {
      chunkData = AddChunkToDictionary(chunk, false);
                AddChunkToDictionary(chunk);
    }
            }


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


        private void AddChunkToDictionary(IServerChunk chunk)
  private HauntingChunkData AddChunkToDictionary(IServerChunk chunk, bool create) {
        {
    HauntingChunkData chunkData;
            byte[] data = chunk.GetServerModdata("haunting");
    byte[] data = chunk.GetServerModdata("haunting");
            int haunting = data == null ? 0 : SerializerUtil.Deserialize<int>(data);
    if (data != null) {
      chunkData = SerializerUtil.Deserialize<HauntingChunkData>(data);
    } else if(!create) {
      return null;
    } else {
      chunkData = new();
    }


            hauntedChunks.Add(chunk, haunting);
    _hauntedChunks.Add(chunk, chunkData);
        }
    if (!chunk.LiveModData.TryGetValue("haunting", out object serializerObj) || serializerObj is not SerializationCallback serializer) {
      serializer = new(chunk);
      chunk.LiveModData["haunting"] = serializer;
     }
     }
    serializer.OnSerialization += chunk => chunk.SetServerModdata("haunting", SerializerUtil.Serialize(chunkData));
    return chunkData;
  }
}
}
</syntaxhighlight>
</syntaxhighlight>
Line 268: Line 344:


Here are the official versions:
Here are the official versions:
* for VS v1.12.14: [https://wiki.vintagestory.at/index.php?title=File:HauntedChunks_vs1.12.14_v1.0.0.zip HauntedChunks_vs1.12.14_v1.0.0.zip]
* for VS v1.19.8: [[File:HauntedChunks.zip]]


{{Navbox/modding|Vintage Story}}
{{Navbox/codemodding}}

Revision as of 07:58, 15 July 2024

This page was last verified for Vintage Story version 1.19.8.

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 individual chunks. Alternatively, mods can store custom save game mod data that applies to the entire save game.

Each chunk has two dictionaries where custom mod data can be attached. The shared mod data dictionary and the server mod data dictionary. Both of these dictionaries have a string key and a byte array value. Logically LiveModData is another accessor for the shared mod dictionary, but technically it is third dictionary. Every time the chunk is serialized, all of the entries in LiveModData are serialized into byte arrays and stored in the shared mod data dictionary, overriding any existing entries with the same key.

Dictionary Readable on Writable on Serialized to Accessors
shared mod data client and server server
  • save game on disk
  • client over network
server mod data server server
  • save game on disk

Both the server mod data dictionary and shared mod data dictionary are have string keys and byte array values. IWorldChunk.LiveModData internally uses SerializerUtil to convert rich types into byte arrays. SetServerModdata does not have such a convenience wrapper. So developers need to manually use a serializer such as SerializerUtil.Serialize to convert a rich object into byte array to give SetServerModdata.

Implementation-wise any string can be used as a key in the mod data dictionaries, but the best practice is to use a string that starts with the mod identifier, which is "haunting" in this tutorial.

Custom Chunk Data

In this example mod we'll show you how to store custom data to chunks represented on the server side. We'll create a mod that keeps track of how many times players have died on a given chunk. Every time a player tries to sleep in a bed, the sum of total deaths on the chunk will be used to inflict damage onto the player! Spooky!

Logically the count of deaths in a chunk is an integer. However, as described earlier, for serialization purposes, that int needs to be stored as a byte array on the chunk. But, an int is more convenient for processing outside serialization purposes. So we'll wrap the integer in a HauntingChunkData class. The [ProtoContract] and lets the serializer know that it is allowed to serialize the class. The [ProtoMember(1)] assigns an identifier to the Deaths field. Any previously unused identifier can be used. The identifier is used to track the field in case the field is later renamed, removed, or new fields are added. Since this is the first version of the mod, such versioning concerns do not apply.

[ProtoContract]
public class HauntingChunkData {
	[ProtoMember(1)]
  public int Deaths;
}

Since all of the death tracking and bed tracking logic runs on the server side, the HauntingChunkData will be stored in the server mod data instead of the shared mod data.

Every time a bed is used, this mod will find the chunk containing the block, then look up the number of deaths in the chunk. Each time a bed is used, it would be possible to deserialize the HauntingChunkData directly from the server mod data in the chunk. Using a bed occurs rarely enough that the deserialization cost of this approach would be acceptable. However, for pedagogical purposes, this mod uses a more CPU efficient approach: the mod adds a separate dictionary to directly look up the deaths for cached chunks. Later this tutorial will explain how to synchronize _hauntedChunks with the server mod data dictionary.

  private readonly ConditionalWeakTable<IServerChunk, HauntingChunkData> _hauntedChunks = new();

LiveModData contains unserialized objects. This example mod does not store the death count in LiveModData, because LiveModData serializes its entries to the shared mod data dictionary. However, for mods that use the shared mod data, the performance of directly reading from LiveModData is lower than directly reading from the server mod data, because reading from LiveModData skips the deserialization stage for already loaded chunks. However, a mod specific cached chunk dictionary is still faster, because it can skip the string hash call necessary to look up an entry in LiveModData (in exchange for a cheap IServerChunk hash call).

When the game saves, we iterate through our Dictionary<IServerChunk, int>, convert the number of death integers into byte arrays, and attach each byte array to its corresponding chunk. Vintage Story will take care of saving that mod data in the game save file.

Chunk Serialization Triggers

When a chunk is marked as modified, it is eventually serialized into the save game on disk. A chunk is marked as modified in many conditions, such as when an entity is created in it, or a block is modified. However, when storing custom mod data in the chunk, the safest option is to directly marked as modified by calling IWorldChunk.MarkModified. This example mod relies on the player dying to mark the chunk as modified.

Modified chunks will get serialized by the next save game event. They will get serialized sooner if the chunk is unloaded. Both of these operations run on the chunk thread (not the main thread). Care must be taken with updating the mod data dictionaries at the right time. The dictionaries should be updated right before they are serialized. If the dictionaries are updated too early before the chunk is locked, then the main thread can further modify the chunk before the serialization. Then the chunk will be in an inconsistent state. Maybe the compressed blocks will be newer than the mod data. Although for most mods, small race conditions like this would be unnoticeable.

LiveModData serializes its fields at exactly the right time: on the chunk thread while the chunk is locked for serialization. There are no other events one can hook into to trigger serialization at the correct time. This is why LiveModData is strongly recommended over IWorldChunk.SetModdata.

There is no equivalent to LiveModData for the server mod data. So instead this example mod stores a fake entry in LiveModData with a [ProtoBeforeSerialization] callback. It uses that callback to update the server mod data dictionary right before the chunk is serialized.

[ProtoContract]
public class SerializationCallback {
  public delegate void OnSerializationDelegate(IServerChunk chunk);

  readonly private IServerChunk _chunk;
  public OnSerializationDelegate OnSerialization;

  public SerializationCallback(IServerChunk chunk) {
    _chunk = chunk;
  }

  [ProtoBeforeSerialization]
  private void BeforeSerialization() {
    OnSerialization(_chunk);
  }
}
...
    if (!chunk.LiveModData.TryGetValue("haunting", out object serializerObj) || serializerObj is not SerializationCallback serializer) {
      serializer = new(chunk);
      chunk.LiveModData["haunting"] = serializer;
    }
    serializer.OnSerialization += chunk => chunk.SetServerModdata("haunting", SerializerUtil.Serialize(chunkData));

An earlier version of haunting mod relied the IServerEventAPI.GameWorldSave event. However, that event is not called before chunks are unloaded. It is also called too early in the save game process, such that the chunk can be modified again before it is serialized.

In addition to saving chunks to disk, nearby chunks are sent to clients when they first join the server, or when they walk into range of the chunk. When using shared mod data, the data may need to be sent more often by calling IWorldManagerAPI.BroadcastChunk. Note that marking a chunk as modified only triggers saving it to disk; it does not trigger sending the modified chunk to clients.

Preparation

Let's start by creating a new .cs file for this mod, and adding our imports and the HauntedChunks namespace to wrap our class in. Additionally, we'll declare the class HauntedChunks 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.GameContent;
using Vintagestory.API.Common;
using Vintagestory.API.Server;
using Vintagestory.API.Util;

namespace HauntedChunks;

public class HauntedChunks : ModSystem {
  private ICoreServerAPI _serverApi;

  public override void StartServerSide(ICoreServerAPI api) {
    _serverApi = api;
  }
}

The StartServerSide method is a member of ModSystem and is called for all mods on the Server side by the game. It is given a ICoreServerAPI, which the mod will need later. So the mod overrides the method and saves it in _serverApi. hauntedChunks will be a list of hunated chunks and their haunting level.

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 two separate events, each with their own delegates that we'll define later. The purpose of each is as follows:

  • 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. Note that DidUseBlock is called any time any block is right clicked. A more efficient -- but complicated -- approach would be to create a behavior that intercepts OnBlockInteractStart, and install it on just the bed block.

Let us now define these event 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.TryGetValue(chunk, out HauntingChunkData chunkData)) {
      chunkData = AddChunkToDictionary(chunk, true);
    }

    chunkData.Deaths++;
  }

The OnPlayerDeath delegate that we used to listen for player death events, will first retrieve the chunk the player died on, by calling IWorldManager.GetChunk. 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 HauntingChunkData AddChunkToDictionary(IServerChunk chunk, bool create) {
    HauntingChunkData chunkData;
    byte[] data = chunk.GetServerModdata("haunting");
    if (data != null) {
      chunkData = SerializerUtil.Deserialize<HauntingChunkData>(data);
    } else if(!create) {
      return null;
    } else {
      chunkData = new();
    }

    _hauntedChunks.Add(chunk, chunkData);
    if (!chunk.LiveModData.TryGetValue("haunting", out object serializerObj) || serializerObj is not SerializationCallback serializer) {
      serializer = new(chunk);
      chunk.LiveModData["haunting"] = serializer;
    }
    serializer.OnSerialization += chunk => chunk.SetServerModdata("haunting", SerializerUtil.Serialize(chunkData));
    return chunkData;
  }

_hauntedChunks acts like a cache. If the cache does not have the entry, maybe it already exists on disk. GetServerModdata is called to read the entry from disk (technically it is already in memory but not deserialized into _hauntedChunks yet). GetServerModdata returns a byte[].

If GetServerModdata returned non-null, then it deserializes the previous haunting level from disk into a HauntingChunkData object. If GetServerModdata returned null, then that means the mod has never attached a haunting level to the chunk. In this case, the create parameter indicates whether a new default HauntingChunkData should be attached to the chunk, or whether it should return null to indicate no data is attached.

As described earlier, a fake entry is added to LiveModData just to get a notification when the chunk is serialized. The mod responds to that notification by calling SetServerModdata to set the server mod data for the chunk. Note that the fake entry in LiveModData will still get serialized to disk (shown later in the tutorial), but the small overhead is worth getting notified at the correct time.

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.TryGetValue(chunk, out HauntingChunkData chunkData)) {
      chunkData = AddChunkToDictionary(chunk, false);
    }

    int haunting = chunkData?.Deaths ?? 0;
    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 in-game 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 then call AddChunkToDictionary to possibly add it. The second argument to AddChunkToDictionary is false, to indicate that it should only load the haunting data if it already exists in the chunk in the save file. If it does not exist, then AddChunkToDictionary returns null. The later chunkData?.Deaths ?? 0 converts a null into a 0.

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;
using ProtoBuf;
using System.Runtime.CompilerServices;

namespace HauntedChunks;

[ProtoContract]
public class SerializationCallback {
  public delegate void OnSerializationDelegate(IServerChunk chunk);

  readonly private IServerChunk _chunk;
  public OnSerializationDelegate OnSerialization;

  public SerializationCallback(IServerChunk chunk) {
    _chunk = chunk;
  }

	[ProtoBeforeSerialization]
  private void BeforeSerialization() {
    OnSerialization(_chunk);
  }
}

[ProtoContract]
public class HauntingChunkData {
	[ProtoMember(1)]
  public int Deaths;
}

public class HauntedChunks : ModSystem {
  private ICoreServerAPI _serverApi;
  private readonly ConditionalWeakTable<IServerChunk, HauntingChunkData> _hauntedChunks = new();

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

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

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

    if (!_hauntedChunks.TryGetValue(chunk, out HauntingChunkData chunkData)) {
      chunkData = AddChunkToDictionary(chunk, true);
    }

    chunkData.Deaths++;
  }

  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.TryGetValue(chunk, out HauntingChunkData chunkData)) {
      chunkData = AddChunkToDictionary(chunk, false);
    }

    int haunting = chunkData?.Deaths ?? 0;
    if (haunting > 0) {
      byPlayer.Entity.ReceiveDamage(
          new DamageSource() { Source = EnumDamageSource.Void,
                               Type = EnumDamageType.BluntAttack },
          haunting);
    }
  }

  private HauntingChunkData AddChunkToDictionary(IServerChunk chunk, bool create) {
    HauntingChunkData chunkData;
    byte[] data = chunk.GetServerModdata("haunting");
    if (data != null) {
      chunkData = SerializerUtil.Deserialize<HauntingChunkData>(data);
    } else if(!create) {
      return null;
    } else {
      chunkData = new();
    }

    _hauntedChunks.Add(chunk, chunkData);
    if (!chunk.LiveModData.TryGetValue("haunting", out object serializerObj) || serializerObj is not SerializationCallback serializer) {
      serializer = new(chunk);
      chunk.LiveModData["haunting"] = serializer;
    }
    serializer.OnSerialization += chunk => chunk.SetServerModdata("haunting", SerializerUtil.Serialize(chunkData));
    return chunkData;
  }
}

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:


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