Modding:Chunk Moddata: Difference between revisions
(Give an overview of the tutorial for the new version of the example mod) |
(Give more information about the accessors) |
||
Line 10: | Line 10: | ||
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. | 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. | |||
; [https://apidocs.vintagestory.at/api/Vintagestory.API.Common.IWorldChunk.html#Vintagestory_API_Common_IWorldChunk_LiveModData IWorldChunk.LiveModData] | <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--> | ||
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'''. | 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'''. | ||
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== <!--T:6--> | ==Custom Chunk Data== <!--T:6--> |
Revision as of 06:13, 15 July 2024
This page was last verified for Vintage Story version 1.12.
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 |
|
|
server mod data | server | server |
|
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, and this sum of total deaths on the chunk will be used to inflict damage onto players sleeping in it! 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 Dictionary<IServerChunk, int>
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.
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.
Preparation
Let's start by creating a new .cs file for this mod, and adding our imports and the VintageStory.ServerMods
namespace to wrap our class in. Additionally, we'll declare the class ChunkDataStorage
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 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 ahauntedChunks
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:
- for VS v1.19.8: File:HauntedChunks.zip
![Icon Sign.png](/images/thumb/1/15/Icon_Sign.png/32px-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 | Item • Entity • Block • Block Behaviors • Block Classes • Block Entities • Block Entity Behaviors • World properties |
Workflows & Infrastructure | Modding Efficiency Tips • Mod-engine compatibility • Mod Extensibility • VS Engine |
Additional Resources | Community Resources • Modding API Updates • Programming Languages • List of server commands • List of client commands • Client startup parameters • Server startup parameters Example Mods • API Docs • GitHub Repository |