Modding:SaveGame ModData: Difference between revisions
Mirotworez (talk | contribs) (Marked this version for translation) |
m (Updated to 1.19.3. Now using new command api.) |
||
Line 1: | Line 1: | ||
__FORCETOC__ | __FORCETOC__ | ||
{{GameVersion|1. | {{GameVersion|1.19.3}} | ||
<languages/><translate> | <languages/><translate> | ||
<!--T:1--> | <!--T:1--> | ||
Line 50: | Line 50: | ||
api.Event.GameWorldSave += OnSaveGameSaving; | api.Event.GameWorldSave += OnSaveGameSaving; | ||
api. | api.ChatCommands.Create("lfg") | ||
.WithDescription("List or join the lfg list") | |||
.RequiresPrivilege(Privilege.chat) | |||
.RequiresPlayer() | |||
.WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" })) | |||
.HandleWith(new OnCommandDelegate(OnLfg)); | |||
} | } | ||
} | } | ||
Line 107: | Line 112: | ||
<syntaxhighlight lang="c#"> | <syntaxhighlight lang="c#"> | ||
private | private TextCommandResult OnLfg(TextCommandCallingArgs args) | ||
{ | { | ||
string cmd = args | string cmd = args[0] as String; | ||
switch (cmd) | switch (cmd) | ||
{ | { | ||
Line 122: | Line 127: | ||
default: | default: | ||
return TextCommandResult.Error("/lfg [list|join|leave]"); | |||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
We use | We use <code>args[0]</code> 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 <code>cmd</code> is null. | ||
Let's handle each of these: | Let's handle each of these: | ||
Line 134: | Line 138: | ||
<syntaxhighlight lang="c#"> | <syntaxhighlight lang="c#"> | ||
case "join": | case "join": | ||
if (lfgList.Contains( | if (lfgList.Contains(args.Caller.Player.PlayerUID)) | ||
{ | { | ||
return TextCommandResult.Error("You're already in the list!"); | |||
} | } | ||
else | else | ||
{ | { | ||
lfgList.Add( | lfgList.Add(args.Caller.Player.PlayerUID); | ||
return TextCommandResult.Success("Successfully joined."); | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 154: | Line 157: | ||
<syntaxhighlight lang="c#"> | <syntaxhighlight lang="c#"> | ||
case "leave": | case "leave": | ||
if (!lfgList.Remove( | if (!lfgList.Remove(args.Caller.Player.PlayerUID)) | ||
{ | { | ||
return TextCommandResult.Error("You're not in the list!"); | |||
} | } | ||
else | else | ||
{ | { | ||
return TextCommandResult.Success("Successfully left."); | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 173: | Line 175: | ||
if (lfgList.Count == 0) | if (lfgList.Count == 0) | ||
{ | { | ||
return TextCommandResult.Success("No one is looking for a group."); | |||
} | } | ||
else | else | ||
Line 183: | Line 185: | ||
}); | }); | ||
return TextCommandResult.Success(response); | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 213: | Line 214: | ||
api.Event.GameWorldSave += OnSaveGameSaving; | api.Event.GameWorldSave += OnSaveGameSaving; | ||
api. | api.ChatCommands.Create("lfg") | ||
.WithDescription("List or join the lfg list") | |||
.RequiresPrivilege(Privilege.chat) | |||
.RequiresPlayer() | |||
.WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" })) | |||
.HandleWith(new OnCommandDelegate(OnLfg)); | |||
} | } | ||
Line 227: | Line 233: | ||
} | } | ||
private | private TextCommandResult OnLfg(TextCommandCallingArgs args) | ||
{ | { | ||
string cmd = args | string cmd = args[0] as String; | ||
switch (cmd) | switch (cmd) | ||
{ | { | ||
case "join": | case "join": | ||
if (lfgList.Contains( | if (lfgList.Contains(args.Caller.Player.PlayerUID)) | ||
{ | { | ||
return TextCommandResult.Error("You're already in the list!"); | |||
} | } | ||
else | else | ||
{ | { | ||
lfgList.Add( | lfgList.Add(args.Caller.Player.PlayerUID); | ||
return TextCommandResult.Success("Successfully joined."); | |||
} | } | ||
case "leave": | case "leave": | ||
if (!lfgList.Remove( | if (!lfgList.Remove(args.Caller.Player.PlayerUID)) | ||
{ | { | ||
return TextCommandResult.Error("You're not in the list!"); | |||
} | } | ||
else | else | ||
{ | { | ||
return TextCommandResult.Success("Successfully left."); | |||
} | } | ||
case "list": | case "list": | ||
if (lfgList.Count == 0) | if (lfgList.Count == 0) | ||
{ | { | ||
return TextCommandResult.Success("No one is looking for a group."); | |||
} | } | ||
else | else | ||
Line 268: | Line 272: | ||
}); | }); | ||
return TextCommandResult.Success(response); | |||
} | } | ||
default: | default: | ||
return TextCommandResult.Error("/lfg [list|join|leave]"); | |||
} | } | ||
} | } | ||
Line 286: | Line 287: | ||
Let's test our mod. Once you're ingame, try entering <code>/lfg join</code>. Now quit the game and join back in. Upon entering <code>/lfg list</code>, you should see your name on the list; this means that your custom data has persisted in the <code>SaveGame</code>! | Let's test our mod. Once you're ingame, try entering <code>/lfg join</code>. Now quit the game and join back in. Upon entering <code>/lfg list</code>, you should see your name on the list; this means that your custom data has persisted in the <code>SaveGame</code>! | ||
{{Navbox/modding|Vintage Story}} | {{Navbox/modding|Vintage Story}} |
Revision as of 16:28, 26 February 2024
This page was last verified for Vintage Story version 1.19.3.
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
VintageStory allows you to set and retrieve custom data to the world's game save as well as individual chunks. The way it works is actually quite simple!
Custom data is stored as an array of bytes to save space, but we can easily convert any valid C# type to byte[]
with a very convenient class in the VintageStory API: SerializerUtil
. This class provides us with two static methods that allow us to quickly Serialize
into byte arrays, and Deserialize<T>
them into other types.
You can check out the methods for storing and retrieving these arrays of bytes for Chunks and for SaveGame
Custom Data in SaveGame
Let us show you this powerful set of tools by making an example mod in which we implement an open list of players currently looking for a group to play with ingame.
This list will be a List<string>
of players which will be stored to the world's SaveGame
. Anyone can join it, leave it, or see who needs a group. We'll store this list as an array of bytes, and it can be accessed by the key "lfg".
Preparation
Let's start by creating a new .cs file for this mod, and adding our imports and the VintageStory.ServerMods
namespace to wrap our class in. Additionally, we'll declare the class LookingForGroup
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.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.ChatCommands.Create("lfg")
.WithDescription("List or join the lfg list")
.RequiresPrivilege(Privilege.chat)
.RequiresPlayer()
.WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" }))
.HandleWith(new OnCommandDelegate(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 TextCommandResult OnLfg(TextCommandCallingArgs args)
{
string cmd = args[0] as String;
switch (cmd)
{
case "join":
break;
case "leave":
break;
case "list":
break;
default:
return TextCommandResult.Error("/lfg [list|join|leave]");
}
}
We use args[0]
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(args.Caller.Player.PlayerUID))
{
return TextCommandResult.Error("You're already in the list!");
}
else
{
lfgList.Add(args.Caller.Player.PlayerUID);
return TextCommandResult.Success("Successfully joined.");
}
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(args.Caller.Player.PlayerUID))
{
return TextCommandResult.Error("You're not in the list!");
}
else
{
return TextCommandResult.Success("Successfully left.");
}
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)
{
return TextCommandResult.Success("No one is looking for a group.");
}
else
{
string response = "Players looking for group:";
lfgList.ForEach((playerUid) =>
{
response += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
});
return TextCommandResult.Success(response);
}
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.ChatCommands.Create("lfg")
.WithDescription("List or join the lfg list")
.RequiresPrivilege(Privilege.chat)
.RequiresPlayer()
.WithArgs(api.ChatCommands.Parsers.Word("cmd", new string[] { "list", "join", "leave" }))
.HandleWith(new OnCommandDelegate(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 TextCommandResult OnLfg(TextCommandCallingArgs args)
{
string cmd = args[0] as String;
switch (cmd)
{
case "join":
if (lfgList.Contains(args.Caller.Player.PlayerUID))
{
return TextCommandResult.Error("You're already in the list!");
}
else
{
lfgList.Add(args.Caller.Player.PlayerUID);
return TextCommandResult.Success("Successfully joined.");
}
case "leave":
if (!lfgList.Remove(args.Caller.Player.PlayerUID))
{
return TextCommandResult.Error("You're not in the list!");
}
else
{
return TextCommandResult.Success("Successfully left.");
}
case "list":
if (lfgList.Count == 0)
{
return TextCommandResult.Success("No one is looking for a group.");
}
else
{
string response = "Players looking for group:";
lfgList.ForEach((playerUid) =>
{
response += "\n" + serverApi.World.PlayerByUid(playerUid).PlayerName;
});
return TextCommandResult.Success(response);
}
default:
return TextCommandResult.Error("/lfg [list|join|leave]");
}
}
}
}
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
!
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 • Entity Behaviors • Block • Block Behaviors • Block Classes • Block Entities • Block Entity Behaviors • Collectible 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 |