This document is about: SERVER 4
SWITCH TO

Photon Plugins Manual

Photon Plugins are available only for Enterprise Cloud or self-hosted Photon Server v4.

With Photon 4 plugins are introduced as the new way to extend the Game/Room behavior replacing what in Photon 3 used to be done through inheritance.

For best practices and frequently asked questions see the Photon Plugins FAQ.

Introduction

Over the years Photon Loadbalancing has evolved as a platform for room based games in both environments "as-is" (no server side custom code) in the cloud and self-hosted extended with custom behavior.
And although the feature set over the years has been changing the core flow between client and server (operations create game, join, leave, etc. and their events) has remained remarkably stable - allowing to keep backwards compatibility.

Customizing up to Photon 3 was based on the source code (Lite & Loadbalancing) and typically inheriting from the Game class.
This was flexible but somewhat brittle / more complex in development since the flow between Client, GameServer and Master could break unexpectedly, for example a client getting "stuck" waiting for a response or an event.

The Plugin API has been designed close to the core flow (create room, join, leave, etc.)

  1. keeping a high degree of flexibility: allowing to hook into the core flow before and after its processing.
  2. minimizing chances to break the flow: failing fast providing errors on client and server.
  3. allowing to use lock free code: plugin instances will never get more than one call at the time and the framework provides a HTTP client and timers integrated in the underlying Photon message passing architecture (fibers).
  4. lowering complexity and increasing ease of use: see "Minimal Plugin".

Concept

To add custom server logic, you inject your code into predefined Photon server hooks.
Currently Photon server supports GameServer plugins only as hooks are triggered by room events.

By definition, a Photon plugin has a unique name and implements those event callbacks.
Custom plugins are compiled into a DLL file called plugins assembly.
Then, the required files are "deployed" on your Photon server or uploaded to your Enterprise Cloud.

The configured plugins assembly is dynamically loaded at runtime with each room creation.
A plugin instance is then created based on a factory pattern.

The room which triggers the plugin creation is called the "host" game.
The latter can be directly accessed from plugins and they both share the same lifecycle.

Webhooks is a good example of Photon plugins.
Source code of Webhooks 1.2 is available in the plugins SDK.
We encourage you to dig into it.

Basic Flow

The Photon hooking mechanism relies on a 6 steps flow:

  1. Intercept the hook call
    When the callback is triggered, the host transfers control to the plugin.
  2. [optional] Alter call info
    Access and modify request sent by client/server, before it is processed.
  3. [optional] Inject custom code
    Interact with the host before processing call (e.g. issue HTTP request, query room/actor, set timer, etc.)
  4. Process hook call
    Decide how and process the request (see "ICallInfo Processing Methods")
  5. [optional] Inject custom code
    Once processed, the request sent by client/server is useful "as read-only".
    However, plugin can still interact with host even after processing.
  6. Return
    Plugin returns control to host.

Getting Started

Minimal Plugin

A "Step by Step Guide" is available for beginners.

The Plugin

The recommended and easy way of making plugins is to extend PluginBase class instead of implementing all IGamePlugin methods directly.
Then you can override just the ones you need.

To obtain a minimal plugin implementation, you only need to override the PluginBase.Name property. It is what identifies a plugin.

"Default" and "ErrorPlugin" are reserved names used internally and should not be used as a custom plugin name.

C#

namespace MyCompany.MyProject.HivePlugin
{
  public class CustomPlugin : PluginBase
  {
    public override string Name
    {
        get
        {
            return "CustomPlugin"; // anything other than "Default" or "ErrorPlugin"
        }
    }
  }
} 

The Factory

A plugin factory class is expected to be implemented as part of the plugins assembly.
It is responsible for the creation of a plugin instance per room.
For the sake of simplicity, in the following snippet the factory returns an instance of CustomPlugin by default without checking the plugin name requested by client.

The room which triggers plugin instantiation is passed as a IPluginHost parameter to the IPluginFactory.Create method.
The same parameter should be passed to the IGamePlugin.SetupInstance in order to keep a reference to the game inside the plugin itself.
IPluginHost provides access to the room data and operations.

C#

namespace MyCompany.MyProject.HivePlugin
{
  public class PluginFactory : IPluginFactory
  {
    public IGamePlugin Create(
          IPluginHost gameHost,
          string pluginName, // name of plugin requested by client
          Dictionary<string, string> config, // plugin settings
          out string errorMsg)
    {
        var plugin = new CustomPlugin();
        if (plugin.SetupInstance(gameHost, config, out errorMsg))
        {
            return plugin;
        }
        return null;
    }
  }
} 

Configuration

Enterprise Cloud

To add a new plugin:

  1. Go to the dashboard of one of the supported Photon product types.
  2. Go to the management page of one of the Photon applications listed there.
  3. Click on the "Create a new Plugin" button on bottom of the page.
  4. Configure the plugin by adding key/value entries of type string.
    Configuration is done by defining key/value pairs of strings.
    The maximum length allowed for each string is 256 characters. The required settings are:
  • AssemblyName: Full name of the DLL file uploaded containing the plugins.
  • Version: Version of the plugins. The same version string used or returned when uploading the plugins' files using the PowerShell script provided by Exit Games.
  • Path: Path to the assembly file. It should have the following format: "{customerName}\{pluginName}".
  • Type: Full name of the PluginFactory class to be used. It has the following format: "{plugins namespace}.{pluginfactory class name}".

On-Premises

Add or modify the following XML node in the "Photon.LoadBalancing.dll.config" file of your GameServer application ("deploy\LoadBalancing\GameServer\bin\Photon.LoadBalancing.dll.config").
The default attributes of <Plugin.../> element are presented in the example below.
Only "Version" is not required.
You can add other optional configuration key/value pairs that are passed to the plugin code.
You can easily activate or deactivate plugins by changing Enabled attribute value in <PluginSettings> element.

XML

<PluginSettings Enabled="true">
    <Plugins>
      <Plugin
          Name="{pluginName}"
          Version="{pluginVersion}"
          AssemblyName="{pluginDllFileName}.dll"
          Type="{pluginNameSpace}.{pluginFactoryClassName}"
      />
    </Plugins>
 </PluginSettings>

The plugins DLL, its dependencies and other files required by the build must be placed in the folder "deploy\plugins\{pluginName}\{pluginVersion}\bin\" according to the configured value of plugin name.

At least these two files should there:

  • "deploy\plugins\{pluginName}\{pluginVersion}\bin\{pluginDllFileName}.dll"
  • "deploy\plugins\{pluginName}\{pluginVersion}\bin\PhotonHivePlugin.dll"

If the "Version" configuration key is not used or its value is kept empty, the path will be "deploy\plugins\{pluginName}\\bin\".
And since "\\" is treated as "\" the expected path is "deploy\plugins\{pluginName}\bin\".

So at least these two files should there:

  • "deploy\plugins\{pluginName}\bin\{pluginDllFileName}.dll"
  • "deploy\plugins\{pluginName}\bin\PhotonHivePlugin.dll"

Plugin Factory

Our plugin model uses a factory design pattern. Plugins are instantiated by name on demand.

A single Photon plugins assembly may contain multiple plugin classes and one active "PluginFactory".
Although it is possible, there is no particular use of making more than one "PluginFactory".
On the contrary, writing multiple plugin classes can be very useful.
For instance, you can have a plugin per game type (or mode or difficulty, etc.).
The factory could also be used to server mutliple plugin versions. Read more about this use case.

Client creates rooms requesting a plugin setup using roomOptions.Plugins.
roomOptions.Plugins is of type string[] where the first string (roomOptions.Plugins[0]) should be a plugin name that will be passed to the factory.

Examples:
roomOptions.Plugins = new string[] { "NameOfYourPlugin" };
or
roomOptions.Plugins = new string[] { "NameOfOtherPlugin" };

If the client doesn't send anything, the server will either use the default (when nothing is configured) or whatever the plugin factory returns on create if it is configured.

If the name of the returned plugin in PluginFactory.Create does not match the one requested by client, then the plugin will be unloaded and the client's create or join operation will fail with PluginMismatch (32757) error.
Also, currently, roomOptions.Plugins should contain one element (plugin name string) at most. If more than one element is sent (roomOptions.Plugins.Length > 1) then a PluginMismatch error will be received and room join or creation will fail.

In the factory just use the name to load the corresponding plugin as follows:

C#

public class PluginFactory : IPluginFactory 
{
    public IGamePlugin Create(IPluginHost gameHost, string pluginName, Dictionary<string, string> config, out string errorMsg) 
    {
        PluginBase plugin = new DefaultPlugin(); // default
        switch(pluginName)
        { 
            case "Default":
                // name not allowed, throw error
            break;
            case "NameOfYourPlugin":
                plugin = new NameOfYourPlugin();
            break;
            case "NameOfOtherPlugin":
                plugin = new NameOfOtherPlugin();
            break;
            default:
                //plugin = new DefaultPlugin();
            break;
        }
        if (plugin.SetupInstance(gameHost, config, out errorMsg)) 
        {
            return plugin;
        }
        return null;
    }
}

ICallInfo Processing Methods

The idea of the plugin is that you hook into the "normal" Photon flow before or after the server processes the incoming request.
The developer needs to decide what to do using one of the 4 available methods depending on the request type:

  1. Continue(): Used to resume the default Photon processing.
  2. Cancel(): Used to silently cancel processing, i.e. without any error or other notification to the clients.
    It is equivalent to skipping further processing:
    • Called inside OnRaiseEvent will ignore the incoming event.
    • Called inside BeforeSetProperties will cancel properties change.
  3. Fail(string msg, Dictionary<byte,object> errorData): Used to cancel further processing returning an error response to client.
    From client, you can get msg parameter in OperationResponse.DebugMessage and errorData in OperationResponse.Parameters.
  4. Defer(): Photon expects that one of the processing methods is called before control is returned.
    This method is used to defer processing. It can be used to allow continuing at a later point in time,
    e.g. in a callback of a timer or when making an asynchronous HTTP request. Read more about this use case in Outbound HTTP section.

Notes:

  • Plugins should have strict mode enabled by default (UseStrictMode = true).
    Strict mode means that a call to one of the ICallInfo processing methods is expected in each plugin callback.
    Not calling any of the available methods will throw an exception.
    Read more here.
  • All callbacks of the PluginBase implementation of IGamePlugin call {ICallInfo}.Continue().
  • Continue(), Fail() and Cancel() are expected to be called only once. Calling any of them again throws an exception.
  • Defer() and Cancel() can only be called inside OnRaiseEvent or BeforeSetProperties.
  • All classes that implement ICallInfo expose the client's original operation request when available.
    You can get the operation code and parameters from the {ICallInfo}.OperationRequest (or Request) property.
  • All classes that implement ICallInfo include helper properties to inform you of the processing CallStatus of the operation request:
    • IsNew: Indicates if the request was not processed nor deferred.
    • IsProcessed: Indicates if the request has already been processed (i.e. Continue or Cancel or Fail method was called).
    • IsSucceeded: Indicates if the request was successfully processed (i.e. Continue method was called).
    • IsCanceled: Indicates if the request was canceled (i.e. Cancel method was called).
    • IsDeferred: Indicates if the request is deferred (i.e. Defer method was called).
    • IsFailed: Indicates if the request "has failed" (i.e. Fail method was called).

Plugin Callbacks

Photon Server has 9 predefined hooks. You don't need to explicitly register for those hooks in the code.
By default, any plugin class can intercept all 9 events.
However, you should implement the ones you need. We recommend extending PluginBase and overriding the required callbacks.

All core event callbacks have a specific ICallInfo contract.
Most callbacks are directly triggered by client actions.
The operation request sent by the client is provided in ICallInfo.Request where available.

OnCreateGame(ICreateGameCallInfo info)

Precondition: client called OpCreateRoom or OpJoinOrCreateRoom or OpJoinRoom and room cannot be found in Photon Servers memory.

Processing Method Processing Result
Continue
  • CreateGame operation response is sent to the client with ReturnCode == ErrorCode.Ok.
  • Join event is sent back to client unless SuppressRoomEvents == false.
  • If ICreateGameCallInfo.BroadcastActorProperties == true player custom properties, if any, will be included in the event parameters.
  • If initialized for the first time; room options and initial properties are assigned to the room state, ActorList should contain first actor with its default properties (UserId and NickName). If request contains custom actor properties they should also be added to the actor entry in the list.
  • If loaded, room state should be the same as it was before last removal from Photon Servers memory unless it was altered.
Fail CreateGame operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel N/A
Defer N/A
  • Notes:
    • Before processing request, the room state is not initialized and contains default values.
      This is the only situation where the room state can be loaded from external source, parsed and assigned to the room by calling IPluginHost.SetGameState.
    • Before processing request, any call to PluginHost.SetProperties or PluginHost.BroadcastEvent will be ignored.
    • You can use ICreateGameCallInfo.IsJoin and ICreateGameCallInfo.CreateIfNotExists to know the type of the operation request.
Operation method IsJoin CreateIfNotExist
OpCreateRoom false false
OpJoinRoom true false
OpJoinOrCreateRoom true true

BeforeJoin(IBeforeJoinGameCallInfo info)

Precondition:
client called OpJoinRoom or OpJoinOrCreateRoom or OpJoinRandomRoom and room is in Photon Servers memory.

Processing Method Processing Result
Continue triggers the OnJoin callback.
Fail JoinGame operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel N/A
Defer N/A
  • Notes:
    • Before processing the IBeforeJoinGameCallInfo, if you call PluginHost.BroadcastEvent do not expect the joining actor to receive the event unless you cache it.

OnJoin(IJoinGameCallInfo info)

Precondition: IBeforeJoinGameCallInfo.Continue() is called in BeforeJoin.

Processing Method Processing Result
Continue
  • If the join is allowed, joining actor is added to ActorList with its default properties (UserId and NickName).
  • If request contains custom actor properties they should be set also.
  • JoinGame operation response is sent back to the client with ReturnCode == ErrorCode.Ok.
  • If IJoinGameCallInfo.PublishUserId == true, UserId of other actors will be sent back to client in the operation response.
  • Join event is broadcasted unless SuppressRoomEvents == false.
  • If IJoinGameCallInfo.BroadcastActorProperties == true player custom properties, if any, will be included in the event parameters.
Fail
  • JoinGame operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
  • Adding of actor is reverted.
Cancel N/A
Defer N/A

OnLeave(ILeaveGameCallInfo info)

Precondition: client called OpLeave, peer disconnects or PlayerTTL elapses. (see Actors life cycle).

Processing Method Processing Result
Continue
  • If triggered by OpLeave operation, its response is sent to the client with ReturnCode == ErrorCode.Ok.
  • Leave event is sent to any other actor unless SuppressRoomEvents == false.
  • If ILeaveGameCallInfo.IsInactive == true:
    • The actor is marked as inactive.
    • DeactivationTime is added to its properties.
  • If ILeaveGameCallInfo.IsInactive == false:
    • The actor and its properties are removed from ActorList.
    • The relative cached events could also be removed if DeleteCacheOnLeave == true.
    • If the ActiveActorList becomes empty, a BeforeCloseGame call will follow after EmptyRoomTTL milliseconds.
Fail If triggered by OpLeave operation, its response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel N/A
Defer N/A
  • Notes:
    • PlayerTTL can be set during room creation.
    • If you call PluginHost.BroadcastEvent do not expect the leaving actor to receive the event.

OnRaiseEvent(IRaiseEventCallInfo info)

Precondition: client calls OpRaiseEvent.

Processing Method Processing Result
Continue
  • The room state's events cache could be updated if a caching option is used.
  • The custom event is sent according to its parameters.
Fail RaiseEvent operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel silently skips processing.
Defer processing is deferred.
  • Notes:
    • If the IRaiseEventCallInfo is successfully processed, no operation response is sent back to the client.

BeforeSetProperties(IBeforeSetPropertiesCallInfo info)

Precondition: client calls OpSetProperties.

Processing Method Processing Result
Continue
  • Room or actor properties are updated.
  • SetProperties operation response is sent back to the client with ReturnCode == ErrorCode.Ok.
  • PropertiesChanged event is broadcasted.
  • triggers OnSetProperties.
Fail SetProperties operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel silently skips processing.
Defer processing is deferred.
  • Notes:
    • In order to know if the properties to be changed belong to the room or to an actor, you can check the value of IBeforeSetPropertiesCallInfo.Request.ActorNumber.
      If it's 0 than the room's properties are about to be updated. Otherwise it's the target actor number whose properties need to be updated.
    • Be careful not to mix the previously mentioned ActorNumber with IBeforeSetPropertiesCallInfo.ActorNr.
      The latter refers to the actor making the operation request.

#### `OnSetProperties(ISetPropertiesCallInfo info)`

Precondition: IBeforeSetPropertiesCallInfo.Continue() is called in BeforeSetProperties.

Processing Method Processing Result
Continue nil.
Fail only logs failure.
Cancel N/A
Defer N/A

BeforeCloseGame(IBeforeCloseGameCallInfo info)

Precondition: all peers disconnected.

Processing Method Processing Result
Continue triggers OnCloseGame.
Fail only logs failure.
Cancel N/A
Defer N/A
  • Notes:
    • EmptyRoomTTL can be set by clients during room creation.
    • Any call to PluginHost.BroadcastEvent will be ignored unless it changes the room events cache.

#### `OnCloseGame(ICloseGameCallInfo info)`

Precondition: IBeforeCloseGameCallInfo.Continue() is called in BeforeCloseGame and the EmptyRoomTTL elapses.

Processing Method Processing Result
Continue the room is removed from Photon Servers memory and plugin instance is unloaded.
Fail only logs failure.
Cancel N/A
Defer N/A
  • Notes:
    • Before processing the ICloseGameCallInfo, you can choose to save room state or lose it forever.
      In webhooks, this can be done when there is still at least one inactive actor in the room.

Advanced Concepts

Actors Life Cycle

Peer <-> Actor <-> Room

An actor is a player inside a room.
Once a player enters a room, either by creating or joining it, he/she will be represented by an actor.
The actor is defined first by its ActorNr then using UserId and NickName.
It can also have custom properties.
If the player enters the room for the first time, he/she will get an actor number inside that room that no other player can claim.
Also for each new player an actor will be added to the room's ActorsList.

If a player leaves the room for good, the respective actor will be removed from that list.
However, if the room options permit it, a player can leave a room and come back later.
The corresponding actor in this case will be marked inactive when the player leaves.
The timestamp of the event will be saved in DeactivationTime actor property.

You can limit the amount of time actors can stay inactive inside rooms.
This duration can be defined when creating rooms by setting the PlayerTTL option to the value needed in milliseconds.
If the value is negative or equal to maximum int value, actors can stay inactive indefinitely.
Otherwise, inactive actors will be removed from the room once PlayerTTL is elapsed after their DeactivationTime.
The player can rejoin the room until then.
And if he/she decides to temporarily leave the room again, a new DeactivationTime will be calculated and countdown will be reset.
So there is no restriction on the number of rejoins.

Photon plugins SDK offers a way to get all actors at any given moment using one of the following properties:

  • IPluginHost.GameActors contains all actors inside a room (active and inactive).
  • IPluginHost.GameActorsActive contains all actors currently joined to the room.
  • IPluginHost.GameActorsInActive contains all actors who left the room (without abandoning).

Sending Events From Plugins

You can send custom events inside the room using the Photon plugins SDK.
Custom events' type and content should be defined by their codes.
Your event codes should stay below 200.

There are two overload methods for this.
Although the name BroadcastEvent suggests that events will be broadcasted,
you can do a multicast based on filters or even send to a single actor:

  • Send to a group of actors:

C#

void IPluginHost.BroadcastEvent(byte target, int senderActor, byte targetGroup, byte evCode, Dictionary<byte, object> data, byte cacheOp, SendParameters sendParameters = null);

You can set the target argument to the following values:

  • 0 (ReciverGroup.All): all active actors.
    targetGroup parameter is ignored.
  • 1 (ReciverGroup.Others): all active actors except the actor with actor number equal to senderActor.
    targetGroup parameter is ignored.
    If senderActor is equal to 0, the behaviour is equivalent to the one where target is set to 0 (ReciverGroup.All).
  • 2 (ReciverGroup.Group): only active actors subscribed to the interest group specified using targetGroup argument.
The ReciverGroup enum and values should not be confused with the ReceiverGroup enum and values in Photon's C# client SDKs including PUN.
  • Send to a specific list of actors using their actor numbers:

C#

void IPluginHost.BroadcastEvent(IList<int> recieverActors, int senderActor, byte evCode, Dictionary<byte, object> data, byte cacheOp, SendParameters sendParameters = null);

It is also possible to use either methods to update room events cache.
You can define the caching option using the cacheOp parameter.
Read more about "Photon Events Caching".

The plugins API do not support all cache operations. All cacheOp values higher than 6 are not accepted and the BroadcastEvent call will fail.

And since Photon events require an actor number as the origin of the event (sender), you have two options:

  • Impersonate an actor: set the senderActor argument to an actor number of an actor joined to the room (must be an active actor).
  • Send an "authoritative" or "global" room event: set the senderActor argument to 0.
    Since the actor number 0 is never assigned to a player.
    So it may be used to indicate that the event origin is not a client.
You can't combine senderActor set to 0 with a cacheOp value other than 0 or 6.

If you choose to extend your plugin class from PluginBase, which is what we recommend, you may want to use the following helper method to broadcast event to all actors joined in the room:

C#

protected void BroadcastEvent(byte code, Dictionary<byte, object> data)
In order to be able to retrieve event data sent from plugins and the sender actor number from clients without changing client code, please send the data in the expected events structure as follows new Dictionary<byte, object>(){{245,eventData },{254,senderActorNr}} instead of (Dictionary<byte, object>)eventData. You could use one of the following helper or wrapper methods:

C#

public void RaiseEvent(byte eventCode, object eventData, 
    byte receiverGroup = ReciverGroup.All, 
    int senderActorNumber = 0, 
    byte cachingOption = CacheOperations.DoNotCache, 
    byte interestGroup = 0, 
    SendParameters sendParams = default(SendParameters))
{
    Dictionary<byte, object> parameters = new Dictionary<byte, object>();
    parameters.Add(245, eventData);
    parameters.Add(254, senderActorNumber);
    PluginHost.BroadcastEvent(receiverGroup, senderActorNumber, interestGroup, eventCode, parameters, cachingOption, sendParams);
}

public void RaiseEvent(byte eventCode, object eventData, IList<int> targetActorsNumbers, 
    int senderActorNumber = 0, 
    byte cachingOption = CacheOperations.DoNotCache, 
    SendParameters sendParams = default(SendParameters))
{
    Dictionary<byte, object> parameters = new Dictionary<byte, object>();
    parameters.Add(245, eventData);
    parameters.Add(254, senderActorNumber);
    PluginHost.BroadcastEvent(targetActorsNumbers, senderActorNumber, eventCode, parameters, cachingOption, sendParams);
}
```</div>

<a id="http"></a>
### Outbound HTTP Calls

`HttpRequest` is a helper class to construct HTTP requests. 
Using this class, you can set the URL and the HTTP method (default is "GET"), the required `Accept` and `ContentType` headers. 
The values of these properties should be supported by [HttpWebRequest](https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.110).aspx).
Additionally, you can specify other custom HTTP headers as a `IDictionary<string, string>` and assign it to `HttpRequest.CustomHeaders`.
You may also add request data by converting it into a `MemoryStream` object first then assigning it to `HttpRequest.DataStream`.
See the [Post JSON](#json) example for more information on how to do this.

<div class="alert alert-info">
What's worth noting, is that there are two properties that are exclusive to Photon and important in the plugins logic:  
<ul>
<li> <code>Async</code>: a flag that indicates if the normal processing of the room logic should be interrupted until the response is received or not. 
This should be set to false if the room logic depends on the HTTP response. 
<li> <code>UserState</code>: an object that will be saved by Photon Server for each request and sent back in the response's callback. 
One example use case is to store the deferred <code>ICallInfo</code> (in <code>OnRaiseEvent</code> or <code>BeforeSetProperties</code>) to be able process it later (call <code>Continue()</code>).
</ul>
</div>

The `HttpRequest` class also should hold reference to the response callback which should have the following signature:
`public delegate void HttpRequestCallback(IHttpResponse response, object userState)`.

Once the request object is setup, you can send it by calling `IPluginHost.HttpRequest(HttpRequest request)`.

#### Use Case Examples:

**Example: making use of Defer and Async in OnRaiseEvent**

In this basic example we demonstrate how to delay `RaiseEvent` operation process until a HTTP response is received.
First we should send the HTTP request in `OnRaiseEvent` callback and then defer the `ICallInfo`.


```csharp
public override void OnRaiseEvent(IRaiseEventCallInfo info) 
{
    HttpRequest request = new HttpRequest() 
    {
        Callback = OnHttpResponse,
        Url = "https://requestb.in/<token>", // change URL
        Async = !WebFlags.ShouldSendSync(info.Request.WebFlags),
        UserState = info
    };
    // here you can modify the request to suit your needs
    PluginHost.HttpRequest(request);
    info.Defer();
}

When the response is received you should decide if you want to continue processing RaiseEvent operation normally or abort it.

C#

private void OnHttpResponse(IHttpResponse response, object userState) 
{
    ICallInfo info = userState as ICallInfo;
    // here you can make an extra check to resume or cancel the RaiseEvent operation
    if (info.IsDeferred) 
    {
        info.Continue();
    }
}


Example: Sending JSON<!--

C#

void PostJson(string url, HttpRequestCallback callback, string json) 
{
    var stream = new MemoryStream();
    var data = Encoding.UTF8.GetBytes(json);
    stream.Write(data, 0, data.Length);
    HttpRequest request = new HttpRequest() 
    {
        Callback = callback,
        Url = url,
        DataStream = stream,
        Method = "POST",
        ContentType = "application/json"
    };
    // here you can modify the request to suit your needs
    PluginHost.HttpRequest(request);
}

Example: Sending querystring<!--

C#

void HttpGet(string url, HttpRequestCallback callback, Dictionary<string, object> getParams) 
{
    StringBuilder sb = new StringBuilder();
    sb.AppendFormat("{0}?", url);
    foreach(var p in getParams)
    {
        sb.AppendFormat("{0}={1}&", p.Key, p.Value);
    }
    HttpRequest request = new HttpRequest() 
    {
        Callback = callback,
        Url = sb.ToString().TrimEnd('&'),
        Method = "GET"
    };
    // here you can modify the request to suit your needs
    PluginHost.HttpRequest(request);
} 

Handling HTTP Response

In the response callback, the first thing to check is IHttpResponse.Status.
It can take one of the following HttpRequestQueueResult values:

  • Success (0): The endpoint returned a successful HTTP status code. (i.e. 2xx codes).
  • RequestTimeout (1): The endpoint did not return a response in a timely manner.
  • QueueTimeout (2): The request timed out inside the HttpRequestQueue.
    A timer starts when a request is enqueued. It times out when the previous queries take too much time.
  • Offline (3): The application's respective HttpRequestQueue is in offline mode.
    No HttpRequest should be made during 10 seconds which is the time the HttpRequestQueue takes to reconnect.
  • QueueFull (4): The HttpRequestQueue has reached a certain threshold for the respective application.
  • Error (5): The request's URL couldn't be parsed or the hostname couldn't be resolved or the endpoint is unreachable.
    Also this may happen if the endpoint returns an error HTTP status code. (e.g. 400:BAD REQUEST)

If the result is not Success (0) you can get more details about what went wrong using the following properties:

  • Reason: readable form of the error. Useful when IHttpResponse.Status is equal to HttpRequestQueueResult.Error.
  • WebStatus: contains the code of the WebExceptionStatus
    that indicates any eventual WebException.
  • HttpCode: contains the returned HTTP status code.

Here is an example on how to do this in code:

C#

private void OnHttpResponse(IHttpResponse response, object userState) 
{
    switch(response.Status)
    {
        case HttpRequestQueueResult.Success:
            // on success logic
            break;
        case HttpRequestQueueResult.Error:
            if (response.HttpCode <= 0) 
            {
                PluginHost.BroadcastErrorInfoEvent(
                    string.Format("Error on web service level: WebExceptionStatus={0} Reason={1}", 
                    (WebExceptionStatus)response.WebStatus, response.Reason));
            } 
            else 
            {
                PluginHost.BroadcastErrorInfoEvent(
                    string.Format("Error on endpoint level: HttpCode={0} Reason={1}", 
                    response.HttpCode, response.Reason));
            }
            break;
        default:
            PluginHost.BroadcastErrorInfoEvent(
                string.Format("Error on HttpQueue level: {0}", response.Status));
            break;
    }
}

For convenience and ease-of-use, Photon plugins SDK offer two ways of getting data from the HTTP response.
Two properties are exposed in the class that implements IHttpResponse:

  • ResponseData: byte array of the response body. It can be useful if the received data is not textual.
  • ResponseText: UTF8 string version of the response body. It can be useful if the received data is textual.

The response class also holds reference to the corresponding HttpRequest in case you need it later.
It is available in IHttpResponse.Request.

Timers

Timers are objects that can be setup with a purpose of calling a method after a specific period of time.
Countdown starts automatically once the timer is created.
It is the best out-of-box way to schedule or delay code execution from a plugin.

Photon plugins SDK offer two different variants of timers depending on the use case:

One-time Timers

One-time timers are meant to trigger a method once after a due time.
In order to create such timer, you need to use the following overload method that takes 2 arguments only:
object CreateOneTimeTimer(Action callback, int dueTimeMs);
You don't need to stop this kind of timer unless you want to cancel the scheduled action before it happens.
If that's the case you should use void IPluginHost.StopTimer(object timer).

Example: delaying SetProperties<!--

C#

public override void BeforeSetProperties(IBeforeSetPropertiesCallInfo info)
{
    PluginHost.CreateOneTimeTimer(
                () => info.Continue(), 
                1000);
    info.Defer();
}

Repeating Timers

A repeating timer periodically invokes a method.
You can define the time of execution of the first callback and the interval between the following successive executions.
In order to create such timer, you need to use the following overload method that takes 3 arguments:
object CreateTimer(Action callback, int dueTimeMs, int intervalMs);
This kind of timer will keep calling the corresponding method as long as it is running and the plugin is loaded (the room is not closed).
It can be stopped at any time using void IPluginHost.StopTimer(object timer).

Example: scheduled events<!--

C#

private object timer;
public override void OnCreateGame(ICreateGameCallInfo info)
{
    info.Continue();
    timer = PluginHost.CreateTimer(
                ScheduledEvent,
                1000,
                2000);
}
private void ScheduledEvent()
{
    BroadcastEvent(1, new Dictionary<byte, string>() { { (byte)245, "Time is up" } });
}
public override void BeforeCloseGame(IBeforeCloseGameCallInfo info)
{
    PluginHost.StopTimer(timer);
    info.Continue();
}

Custom Types

If you want Photon to support serialization of your custom classes then you need to register their types.
You need to manually assign a code (byte) for each type and provide the serialization and deserilization methods of the fields and properties of the class.
The same code for registering new types should be used in client also.
Then to complete registration you need to call the following method:

C#

bool IPluginHost.TryRegisterType(Type type, byte typeCode, Func<object, byte[]> serializeFunction, Func<byte[], object> deserializeFunction);

Example: Registering CustomPluginType

Here is the example of custom type class to register:

C#

class CustomPluginType
{
    public int intField;
    public byte byteField;
    public string stringField;
}
The registration of custom types should be done by both ends. Meaning the Photon client should also register the custom type with the same code and serialization methods.

The serialization method should transform the object of custom type to a byte array.
Note that you should cast the object to the expected type (CustomPluginType) first.

C#

private byte[] SerializeCustomPluginType(object o)
{
    CustomPluginType customObject = o as CustomPluginType;
    if (customObject == null) { return null; }
    using (var s = new MemoryStream())
    {
        using (var bw = new BinaryWriter(s))
        {
            bw.Write(customObject.intField);
            bw.Write(customObject.byteField);
            bw.Write(customObject.stringField);
            return s.ToArray();
        }
    }
}

The deserilization method should do the opposite.
It constructs the custom type object back from a byte array.

C#

private object DeserializeCustomPluginType(byte[] bytes)
{
    CustomPluginType customObject = new CustomPluginType();
    using (var s = new MemoryStream(bytes))
    {
        using (var br = new BinaryReader(s))
        {
            customObject.intField = br.ReadInt32();
            customObject.byteField = br.ReadByte();
            customObject.stringField = br.ReadString();
        }
    }
    return customobject;
}

Finally, we need to register the CustomPluginType.
We can do this as soon as the plugin is initialized in the SetupInstance:

C#

public override bool SetupInstance(IPluginHost host, Dictionary<string, string> config, out string errorMsg)
{
    host.TryRegisterType(typeof(CustomPluginType), 1, 
        SerializeCustomPluginType, 
        DeserializeCustomPluginType);
    return base.SetupInstance(host, config, out errorMsg);
}

Logging

To log from plugins, use one of the PluginHost.LogXXX methods.

Enterprise Cloud

Since access to log files on our servers is not granted an external service should be used for logs or alerts.
We recommend using Logentries or Papertrail.
So we ask our Enterprise Customers to get in touch with us to configure their private cloud with the logging service of their choice.
If you want to use Logentries provide the configured log token.
If you want to use Papertrail provide your custom URL with the port.

On-Premises

By default log output can be retrieved in "GSGame.log" file along with GameServer log entries.
To use a separate log file, add the following snippet to the log4net configuration file ("log4net.config") of the GameServer:

XML

<!-- "plugin" log file appender -->
<appender name="PluginLogFileAppender" type="log4net.Appender.RollingFileAppender">
    <file type="log4net.Util.PatternString" value="%property{Photon:ApplicationLogPath}\\Plugins.log" />
    <param name="AppendToFile" value="true" />
    <param name="MaxSizeRollBackups" value="20" />
    <param name="MaximumFileSize" value="10MB" />
    <param name="RollingStyle" value="Size" />
    <param name="LockingModel" type="log4net.Appender.FileAppender+MinimalLock" />
    <layout type="log4net.Layout.PatternLayout">
        <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
    </layout>
</appender>
<!-- CUSTOM PLUGINS:  --> 
<logger name="Photon.Hive.HiveGame.HiveHostGame.Plugin" additivity="false">
    <level value="DEBUG" />
    <appender-ref ref="PluginLogFileAppender" />
</logger>

Versioning

This section is relevant for Photon Enterprise Cloud customers only.

Currently Photon Plugins support only side-by-side assembly versioning: one plugin DLL version per AppId.

Here are two methods we recommend for rolling out new plugins versions:

A. "Compatible" plugins deploy: does not require new client version

  1. Upload new version of plugins assembly.
  2. On a staging AppId: test to verify that new version works as expected. (recommended)
  3. Update production AppId configuration to use new plugins assembly version.

B. "Incompatible" plugins deploy: requires new client version

  1. Upload new version of plugins assembly.
  2. Setup a new production AppId.
  3. Configure the new production AppId to use new plugins assembly version.

PUN Specific Plugins

If you use PUN as a client SDK and want to implement a server side plugin that works with it, you should know the following:

  • PUN registers some extra custom types mainly Unity basic classes.
    If you want to handle those from the plugin you should register the same custom types from the plugin as well.
    You can find all those custom types and how to register them in "CustomTypes.cs" class inside PUN package.
    Failure to register some custom types may result in errors or unexpected behavior.
    You should implement IGamePlugin.OnUnknownType or IGamePlugin.ReportError to be notified of such issues.

  • All PUN's high level and unique features use RaiseEvent under the hood.
    Each feature uses one or more event codes and special event data structure.
    To get the list of events reserved by PUN, take a look at "PunEvent" class from "PunClasses.cs" file inside PUN package.
    For example to intercept OnSerializeView calls, you need to implement OnRaiseEvent callback and catch event codes with the corresponding type, in this case "SendSerialize = 201".
    To know each event's expected content, take a look at how its event data is constructed in PUN's code or inspect it from the incoming event inside the plugin.

AuthCookie

Also called secure data, is an optional JSON object returned by the web service set up as authentication provider.
This object will not be accessible from client side.
For more information please visit the custom authentication documentation page.

From Plugins you could access the AuthCookie as follows:

  • ICallInfo.AuthCookie: to get the AuthCookie of the current actor triggering the hook.
    However, in OnBeforeCloseGame and OnCloseGame, IBeforeCloseGameCallInfo.AuthCookie and ICloseGameCallInfo.AuthCookie respectively won't have any value since those are triggered outside of a user context.
    e.g.

    C#

    public void OnCreateGame(ICreateGameCallInfo info)
    {
        Dictionary<string, object> authCookie = info.AuthCookie;
    
  • IActor.Secure: to get the AuthCookie for any active actor. e.g.

    C#

    foreach (var actor in this.PluginHost.GameActorsActive)
    {
        var authCookie = actor.Secure as Dictionary<string, object>;
    }
    
Back to top