Appearance
Custom Server Logic
This page demonstrates how to add a custom server-only entity to the backend.
Appearance
This page demonstrates how to add a custom server-only entity to the backend.
Overview of Metaplay's server programming model - In this page we'll axpand upon the Entities, Models, and Actors introduced in Introduction to Game Server Programming
In this guide we'll focus on extending existing Actor code by creating new server actions. Although you can implement much of the game's functionality in shared code, some processes are better left to the server. Logic that you wish to keep secret, like chest randomization or rewards, will be inaccessible even to hacked clients if you run them only on the server. This page will teach you how to transmit these results to the player model.
As the player model is updated by the client first and only later by the server, any modifications originating on the server require some extra steps to avoid causing Desyncs. There are three approaches to server-side player model updates:
PlayerModel DirectlyYou can also use a hybrid approach to implement a queue pattern. Both synchronized and unsynchronized server actions run at a later timestamp than they are initiated, which allows for a little less control than might be necessary in some cases. Using a queue pattern allows you to store some pending operations on the client that can be initiated in client-side code, thus taking effect for the player instantly.
The SDK computes checksums of models based on all members not marked with the [NoChecksum] attribute. This checksum is calculated on both the server and client to ensure the client's calculations have the same results as the authoritative execution on the server. When the client and server's checksums differ, a Desync is triggered.
To successfully modify a checksummed member without causing a Desync, the action must execute simultaneously on both the client and the server. You can achieve this by issuing a SynchronizedServerAction, which is first sent to the client to determine the shared timeline position and executed only at a later, shared time. You can use this approach to modify any member.
Race condition warning
As the actions need to be sent to the client before execution, the time from the action issue to the execution can grow large, especially if the client app has been put into the background or if there are poor network conditions. You should be careful to check that any preconditions still hold by the time the action is executed.
For example, if we wanted to give a player some extra gold to celebrate their birthday, we could do the following:
class PlayerModel
{
....
[MetaMember(103)] public int NumGold;
}[ModelAction(101)]
public class PlayerAddGold : PlayerSynchronizedServerActionCore<PlayerModel>
{
public int NumToAdd { get; private set; }
PlayerAddGold() { }
public PlayerAddGold(int numToAdd) { NumToAdd = numToAdd; }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
{
player.NumGold += NumToAdd;
}
return MetaActionResult.Success;
}
}On the server, we use the EnqueueServerAction method instead of ExecuteAction. This happens since the action doesn't execute instantly and needs to be put in a queue.
void OnHappyBirthday()
{
EnqueueServerAction(new PlayerAddGold(100));
}Pro tip!
The synchronized actions are not required to inherit from the helper PlayerSynchronizedServerActionCore<> class. Instead, the Action is only required to have the [ModelActionExecuteFlags(ModelActionExecuteFlags.FollowerSynchronized)] flag defined. This can be useful for reusing actions for multiple modes by adding different ModelActionExecuteFlags.
Besides synchronized actions, you can also use UnsynchronizedServerAction. The server will perform the action immediately, and the client will do it as soon as possible. We call this an "unsynchronized action" as the client may already have advanced on the timeline, and hence, the action would get executed later on the client than on the server. As the execution time may vary, the action may only modify a [ServerOnly] or [NoChecksum] member, or risk causing a Desync. Editing other members will raise a warning if debug checks are enabled.
For example, let's say we have a tip-of-the-day string on the player model that we'd like to update and have the client be able to see it:
class PlayerModel
{
...
// Using [NoChecksum] to allow modifications by the server without Desyncs.
[MetaMember(102), NoChecksum] public string TipOfTheDay;
}In this case, we can create an UnsynchronizedServerAction to update it.
[ModelAction(101)]
public class PlayerUpdateTotd : PlayerUnsynchronizedServerActionCore<PlayerModel>
{
public string TipOfTheDay { get; private set; }
PlayerUpdateTotd() { }
public PlayerUpdateTotd(string tipOfTheDay) { TipOfTheDay = tipOfTheDay; }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
{
player.TipOfTheDay = TipOfTheDay;
}
return MetaActionResult.Success;
}
}To run the action, we use ExecuteServerActionImmediately on the PlayerActor.cs script.
void OnLoginOrAtMidnight()
{
string tipOfTheDay = TipGenerator.Instance.GetTipForDay(DateTime.UtcNow.TotalDays);
ExecuteServerActionImmediately(new PlayerUpdateTotd(tipOfTheDay));
}Pro tip!
Like the synchronized actions, unsynchronized actions are not required to inherit from the helper PlayerUnsynchronizedServerActionCore<> class. Instead, the SDK only requires the action to have a [ModelActionExecuteFlags(ModelActionExecuteFlags.FollowerUnsynchronized)] flag defined. You can use this to repurpose the actions for multiple execution modes.
Lastly, you can update the PlayerModel state from the server by modifying it directly. The main drawbacks are:
[NoChecksum] fields.For example, if we add a member to the player model that tracks the number of times a player has logged in:
class PlayerModel
{
....
[MetaMember(101)] public int NumTimesLoggedIn;
}We could do the following to update it:
void OnSessionStart()
{
// In this case, even though NumTimesLoggedIn is a checksummed field,
// the change doesn't trigger a desync as this method is only called
// when a player is logging in.
Model.NumTimesLoggedIn++;
}The client, however, does not automatically become aware of such direct modifications. If there is an ongoing session, only the old value will be available to the client until the start of the next session. As such, a direct modification to any member not marked as [ServerOnly] or [NoChecksum] will cause a Desync. Therefore, you should only update such members when the client is known to be offline, such as in login hooks like the previous example, or when updating server-only members for internal bookkeeping.
You can terminate a player's session manually for rare one-off operations. The values will then be updated when the client reconnects.
If you need to kick a player from the game for any reason, you can use the KickPlayerIfConnected method:
KickPlayerIfConnected(PlayerForceKickOwnerReason.AdminAction);After kicking a player, you can safely modify their player model.
Sometimes, just adding new functionality to existing Entities is not enough. In the next guide, Custom Entities, we'll create an Entity on the server that tracks which players were the first to log in that day. This is of course only an example, but hopefully it can get you a good understanding of how and why you can add your own Entities and Actors to the server.