This document is about: SERVER 5
SWITCH TO

What's New

Plugins API Version Upgrade

Plugins API assembly (PhotonHivePlugin.dll) version is updated from 1.0.x to 1.1.0.

This means that if you are building a custom plugin using a Photon Server Plugins SDK (v5 RC1 or newer) that contains PhotonHivePlugin.dll 1.1.x and want to deploy it and run it on your server, the server binaries need to match that plugins API version.

For Enterprise customer this means that their private cloud needs to be updated to at least Photon Server v5 RC1. If you try to upload the plugin to an older private cloud you will get an error when trying to load the plugin when creating new rooms.

New Separate Configuration File

Plugins are now configured separately on their own file. The file is "plugin.config" in the same folder as the previously used configuration file. The configuration syntax is still the same though.

New Logging Interface

We have added new improved logging interface accessible from plugins.

Old Deprecated Way

In v4 we used to call the IPluginHost logging methods directly.

C#

private IPluginHost pluginHost;

public bool SetupInstance(IPluginHost host, Dictionary<string, string> config, out string errorMsg)
{
    pluginHost = host;
}

///...

this.pluginHost.LogDebug("debug");
this.pluginHost.LogWarning("warning");
this.pluginHost.LogError("error");
this.pluginHost.LogFatal("fatal");

New Way

Now instead of calling the IPluginHost logging methods, you should create a new IPluginLogger object per plugin instance and use it to log everything from your plugin:

C#

public const string PluginName = "MyPlugin";

private IPluginLogger pluginLogger;

public override bool SetupInstance(IPluginHost host, Dictionary<string, string> config, out string errorMsg)
{
    pluginLogger = host.CreateLogger(PluginName);
    // ...
}

// ...

this.pluginLogger.LogDebug("debug");
this.pluginLogger.LogWarning("warning");
this.pluginLogger.LogError("error");
this.pluginLogger.LogFatal("fatal");

When setting the name of the logger (passed to IPluginHost.CreateLogger(loggerName)), we prepend the logger name with Plugin.. For example, if you set MyPlugin.MyClass, it will be logged as Plugin.MyPlugin.MyClass. The code snippet above will generate these log entries in case the log level is set to maximum level:

Unknown

2020-01-31 17:10:07,394 [1] DEBUG  Plugin.MyPLugin - debug
2020-01-31 17:10:07,901 [1] WARN  Plugin.MyPLugin - warning
2020-01-31 17:10:08,152 [1] ERROR  Plugin.MyPLugin - error
2020-01-31 17:10:08,724 [1] FATAL  Plugin.MyPLugin - fatal

Improved Plugin Hook Processing

The ICallInfo is the argument we get inside the plugin hook methods which represents the context of the method call. This context object needs to be processed within a limited time after the hook call to guarantee the normal workflow inside the room.

Anonymous Functions

If you were using anonymous functions (lambda expression or anonymous method) as callbacks for HTTP requests (see examples below), you can keep doing so.

Example of HTTP callback as anonymous method:

C#

private void SendRequest(ICallInfo callInfo)
{
    HttpRequest request = new HttpRequest
    {
        Callback = delegate
                   {
                       ProcessCallIfNeeded(callInfo);
                   },

Example of HTTP callback as lambda expression:

C#

private void SendRequest(ICallInfo callInfo)
{
    HttpRequest request = new HttpRequest()
    {
        Callback = (response, userObject) => ProcessCallIfNeeded(callInfo),

It is recommended however to replace obsolete HTTP method calls:

C#

PluginHost.HttpRequest(request);

with the new overload method that takes two arguments:

C#

PluginHost.HttpRequest(request, callInfo);

Old Way

Previously we had to use a workaround to get the ICallInfo object inside the HTTP response callback. We used the custom optional HttpRequest.UserState to store the ICallInfo object.

Example:

C#

private void CallbackOldLogic(IHttpResponse response, object userObject)
{
    ICallInfo callInfo = userObject as ICallInfo;
    ProcessCallIfNeeded(callInfo);
}

private void SendRequest(string url, ICallInfo callInfo)
{
    HttpRequest request = new HttpRequest
    {
        Url       = url,
        Callback  = CallbackOldLogic,
        UserState = callInfo
    }
    this.PluginHost.HttpRequest(request);

New Way

We now pass the plugin hook context (ICallInfo) inside a new PluginHost.HttpRequest overload method. Then we can retrieve it from the IHttpResponse.

Example:

C#

private void CallbackNewLogic(IHttpResponse response, object userObject)
{
    ICallInfo callInfo = response.CallInfo;
    ProcessCall(callInfo);
}

private void SendRequest(string url, ICallInfo callInfo, object userObject = null)
{
    HttpRequest request = new HttpRequest
    {
        Url       = url,
        Callback  = CallbackNewLogic,
        UserState = userObject,
        CallInfo  = callInfo
    };
    this.PluginHost.HttpRequest(request, callInfo);

New Async Behaviour

Old Deprecated Way

In v4, you had to call ICallInfo.Defer for asynchronous HTTP requests or timers when you want to delay processing for a plugin hook.

Example with HTTP request:

C#

private void SendRequest(string url, ICallInfo callInfo)
{
    HttpRequest request = new HttpRequest
    {
        Url       = url,
        Async     = true,
        Callback  = delegate { ProcessCallIfNeeded(callInfo); },
    };
    this.PluginHost.HttpRequest(request);
    callInfo.Defer();
}

Example with timer:

C#

private void DelayLogic(ICallInfo callInfo, Action logic, int delayMs)
{
    PluginHost.CreateOneTimeTimer(
                () =>
                {
                    logic();
                    ProcessCallIfNeeded(callInfo);
                },
                delayMs);
    callInfo.Defer();
}

New Way

Now ICallInfo.Defer is removed and instead we internally defer the hook context call for you automatically if:

  • you are sending a HttpRequest with httpRequest.Async = true
  • you are creating timers

Example with HTTP request:

C#

private void SendRequest(string url, ICallInfo callInfo)
{
    HttpRequest request = new HttpRequest
    {
        Url       = url,
        Async     = true,
        Callback  = delegate { ProcessCallIfNeeded(callInfo); },
        CallInfo  = callInfo
    };
    this.PluginHost.HttpRequest(callInfo, request);
}

Example with timer:

C#

private void DelayLogic(ICallInfo callInfo, Action logic, int delayMs)
{
    PluginHost.CreateOneTimeTimer(
                callInfo,
                () =>
                {
                    logic();
                    ProcessCallIfNeeded(callInfo);
                },
                delayMs);
}

Fibers Access

A fiber is a list of tasks which are executed one by one sequentially in FIFO manner. This does not mean that they are executed in one thread. Actually, they are executed in many threads, but one by one. So the first task may be executed in thread A, when it finishes, the second task may be executed in thread B and so on. But at every given moment just one thread accesses room data. If many fibers try to access the same data we use locks.

To get a room fiber from plugins:

C#

IPluginFiber fiber = this.PluginHost.GetRoomFiber();

Enqueue

You could enqueue an action to the room's fiber using:

C#

PluginHost.Enqueue(someAction);

Or you could use the fiber you got above:

C#

int r = fiber.Enqueue(someAction);
if (r == 0)
{
   // success
}
else
{
   // error
}

Timers

IPluginFiber share same methods as IPluginHost except it does not require ICallInfo and could be executed with thread safety outside hooks context:

C#

object timer = fiber.CreateOneTimeTimer(someAction, executeAfterMs);
\\ ...
fiber.StopTimer(timer); // in case you want to abort before delay is expired and someAction is executed

C#

object timer = fiber.CreateTimer(someAction, firstExecutionAfterMs, repeatEachMs);
fiber.StopTimer(timer);

IFactoryHost

There is also a new support for lock-less processing outside rooms, that can be used for application-wide services (ie. anticheat service, broadcasting, etc.). This processing is built fully using IPluginFiber mechanism. Plugin fibers are fibers similar to room fibers, but operating outside rooms. You can Enqueue to them from anywhere and operations are executed based on a server time. To get a IPluginFiber, you need an instance of IFactoryHost, which is provided by IFactoryPlugin2. IFactoryPlugin2 is an interface extending standard IPluginFactory with one method:

C#

void SetFactoryHost(IFactoryHost factoryHost, FactoryParams factoryParams);

This method is called once during the plugin lifetime, right after the plugin factory is instantiated and provides an IFactoryHost instance, which provides plugin fibers and plugin logger:

C#

IPluginFiber CreateFiber();

IPluginLogger CreateLogger(string loggerName);

Fibers created by FactoryHost can be used both outside rooms (i.e. when creating a new room) and inside rooms to enqueue or timer operations outside of a room context. Similarly, loggers created by FactoryHost are to be used inside these operations.

New Sync Behaviour

For "synchronous HTTP requests" (HttpRequest sent with httpRequest.Async = false) we now pause the room fiber (i.e. no room message will be processed) until you get the response or a timeout.

The old IPluginHost.HttpRequest method is blocking: it does not return until the HTTP response is received or a timeout. The main issue with this approach is that the thread is blocked and the callback execution can be pushed to the queue of this thread. So, this callback cannot be executed, because the thread is blocked.. As a result we get a timeout. The main difference with the new approach is the non-blocking nature of the new IPluginHost.HttpRequest method. It will never block.. This means that the user code should be rewritten - both the plugin hook where the call is made and the HTTP request callback used need to be updated. The plugin hook should be aware that the new IPluginHost.HttpRequest call will return right after pushing request to HTTP queue. Inside the callback the ICallInfo object should be processed (call callInfo.Continue or Fail or Cancel to continue or abort operation) if not done yet. Now instead of blocking thread we block the room fiber. So, no new tasks will be processed until we get a response to our request or a timeout.

Global Ban

When you pass the new RemoveActorReason.GlobalBanned (2), the master server will keep the UserId in memory for up to twice the duration authentication token expiration (default is 2 * 1 hour) so the globally banned user cannot join any new room for that duration. You can reject the user in custom authentication afterwards. Also, this has the same effect on the current room as RemoveActorReason.Banned: the UserId will be added to the room state's ExcludedUsers and the user can't rejoin it.

Updating AuthCookie

You can now update the secure authentication cookie per actor using this method:

void IActor.UpdateSecure(string key, object value)

Support for All Cache Operations

You can now execure all cache operations. For all cache operations that do not involve sending events to clients use the new methods:

bool IPluginHost.ExecuteCacheOperation(CacheOp operation, out string errorMsg)

More flexible & powerful HTTP Requests API

  • Added support for PUT Method. Other HTTP verbs should be supported as well.
  • You can set some restricted HTTP request headers using HttpRequest properties (Accept, ContentType).
  • You can get HTTP response headers using IHttpResponse.Headers.
Back to top