Show Sidebar

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.

Content

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

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" should not be used as a plugin name.

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.

Configuration

Photon 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 Photon.
  • 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}".

Photon Server:

Add the following XML node in the "Photon.LoadBalancing.dll.config" file of your GameServer application. The required attributes of <Plugin.../> element are presented in the example. Any other additional attribute are optional. You can easily activate or deactivate plugins by changing Enabled attribute value in <PluginSettings> element.

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.).

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:

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.
  • 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).
    • IsCanceld: 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:
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 his/her 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:
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:
Continue
  • If the join is allowed, joining actor is added to ActorList and with his/her 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:
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 his/her properties are removed from ActorList.
    • The relative cached events could also be removed if DeleteCacheOnLeave == true.
    • If the ActorList 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 by clients through RoomOptions in OpCreateRoom or OpJoinOrCreate.
    • If you call PluginHost.BroadcastEvent do not expect the leaving actor to receive the event.

OnRaiseEvent(IRaiseEventCallInfo info)

  • Precondition: client calls OpRaiseEvent.
  • Processing:
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:
Continue
  • Room or actor properties are updated.
  • SetProperties operation response is sent back to the client with ReturnCode == ErrorCode.Ok.
  • PropertiesChanged event is broadcasted.
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:
Continue nil.
Fail only logs failure.
Cancel N/A
Defer N/A

BeforeCloseGame(IBeforeCloseGameCallInfo info)

  • Precondition: all peers disconnected and the EmptyRoomTTL elapses.
  • Processing:
Continue triggers OnCloseGame.
Fail only logs failure.
Cancel N/A
Defer N/A
  • Notes:
    • EmptyRoomTTL can be set by clients through RoomOptions in OpCreateRoom or OpJoinOrCreate.
    • 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.
  • Processing:
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.GameActorsActive 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 specific group of actors:

You can set the target argument to one of the "well-known" groups:

  • 0: ReceiverGroup.Others
  • 1: ReceiverGroup.All
  • 2: ReceiverGroup.MasterClient

You can also filter receiving actors by "Interest Group" using targetGroup argument. The event will be sent only to the actors registered to that "Interest Group". By default all actors are registered to "Interest Group" 0.

  • Send to a specific list of actors:

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 actorNumber of an actor joined to the room (active or inactive).
  • Send an authoritative 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.

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".

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:

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.

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. 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 example for more information on how to do this.

What's worth noting, is that there are two properties that are exclusive to Photon and important in the plugins logic:
  • Async: 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.
  • UserState: 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 ICallInfo (in OnRaiseEvent or BeforeSetProperties) to be able process it later (call Continue()).

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.

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

Example: Sending JSON

Example: Sending querystring

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:

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

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

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:

Example: Registering CustomPluginType

Here is the example of custom type class to register:

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.

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

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

Logging

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

Enterprise Customers

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. So we ask our Enterprise Customers to get in touch with us to configure their private cloud with the logging service of their choice.

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:

 To Document Top