Photon Plugins Manual
To discover what's new in Photon Server Plugins SDK v5 see here.
For best practices and frequently asked questions see the Photon Plugins FAQ.
Introduction
The Plugins API has been designed close to the core flow (create room, join, leave, etc.)
- keeping a high degree of flexibility: allowing to hook into the core flow before and after its processing.
- minimizing chances to break the flow: failing fast providing errors on client and server.
- 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).
- 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:
- Intercept the hook call
When the callback is triggered, the host transfers control to the plugin. - [optional] Alter call info
Access and modify request sent by client/server, before it is processed. - [optional] Inject custom code
Interact with the host before processing call (e.g. issue HTTP request, query room/actor, set timer, etc.) - Process hook call
Decide how and process the request (see "ICallInfo Processing Methods") - [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. - 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.
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.
C#
namespace MyCompany.MyProject.HivePlugin
{
public class PluginFactory : PluginFactoryBase
{
public override IGamePlugin Create(string pluginName)
{
return new CustomPlugin();
}
}
}
Configuration
Enterprise Cloud Configuration
To add a new plugin:
Go to the dashboard of one of the supported Photon product types.
Go to the management page of one of the Photon applications listed there.
Click on the "Create a new Plugin" button on bottom of the page.
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 thePluginFactory
class to be used. It has the following format: "{plugins namespace}.{pluginfactory class name}".
Self-Hosted Server Configuration
Add or modify the following XML node in the "plugin.config" file of your GameServer application ("deploy\LoadBalancing\GameServer\bin\plugin.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
<Configuration>
<PluginSettings Enabled="true">
<Plugins>
<Plugin
Name="{pluginName}"
Version="{pluginVersion}"
AssemblyName="{pluginDllFileName}.dll"
Type="{pluginNameSpace}.{pluginFactoryClassName}"
/>
</Plugins>
</PluginSettings>
</Configuration>
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 serve multiple 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.
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#
using System.Collections.Generic;
public class PluginFactory : IPluginFactory
{
public IGamePlugin Create(IPluginHost gameHost, string pluginName, Dictionary<string, string> config, out string errorMsg)
{
IGamePlugin 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:
Continue()
: Used to resume the default Photon processing.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.
- Called inside
Fail(string msg, Dictionary<byte,object> errorData)
: Used to cancel further processing returning an error response to client.
From client, you can getmsg
parameter inOperationResponse.DebugMessage
anderrorData
inOperationResponse.Parameters
.
Notes:
- Plugins should have strict mode enabled by default (
UseStrictMode = true
).
Strict mode means that a call to one of theICallInfo
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 ofIGamePlugin
call{ICallInfo}.Continue()
. Continue()
,Fail()
andCancel()
are expected to be called only once. Calling any of them again throws an exception.Cancel()
can only be called insideOnRaiseEvent
orBeforeSetProperties
.- 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
(orRequest
) property. - All classes that implement
ICallInfo
include helper properties to inform you of the processingCallStatus
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
orCancel
orFail
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).IsPaused
: Indicates if the request was paused internally (e.g. Synchronous HTTP request sent).IsDeferred
: Indicates if the request was deferred internally (e.g. Asynchronous HTTP request sent).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 |
|
Fail |
CreateGame operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError . |
Cancel |
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 callingIPluginHost.SetGameState
. - Before processing request, any call to
PluginHost.SetProperties
orPluginHost.BroadcastEvent
will be ignored. - You can use
ICreateGameCallInfo.IsJoin
andICreateGameCallInfo.CreateIfNotExists
to know the type of the operation request.
- Before processing request, the room state is not initialized and contains default values.
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 |
- Notes:
- Before processing the
IBeforeJoinGameCallInfo
, if you callPluginHost.BroadcastEvent
do not expect the joining actor to receive the event unless you cache it.
- Before processing the
OnJoin(IJoinGameCallInfo info)
Precondition: IBeforeJoinGameCallInfo.Continue()
is called in BeforeJoin
.
Processing Method | Processing Result |
---|---|
Continue |
|
Fail |
|
Cancel |
N/A |
OnLeave(ILeaveGameCallInfo info)
Precondition: client called OpLeave
, peer disconnects or PlayerTTL
elapses. (see Actors life cycle).
Processing Method | Processing Result |
---|---|
Continue |
|
Fail |
If triggered by OpLeave operation, its response is sent to the client with ReturnCode == ErrorCode.PluginReportedError . |
Cancel |
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 |
|
Fail |
RaiseEvent operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError . |
Cancel |
silently skips processing. |
- Notes:
- If the
IRaiseEventCallInfo
is successfully processed, no operation response is sent back to the client.
- If the
BeforeSetProperties(IBeforeSetPropertiesCallInfo info)
Precondition: client calls OpSetProperties
.
Processing Method | Processing Result |
---|---|
Continue |
|
Fail |
SetProperties operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError . |
Cancel |
silently skips processing. |
- 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
withIBeforeSetPropertiesCallInfo.ActorNr
.
The latter refers to the actor making the operation request.
- In order to know if the properties to be changed belong to the room or to an actor, you can check the value of
OnSetProperties(ISetPropertiesCallInfo info)
Precondition: IBeforeSetPropertiesCallInfo.Continue()
is called in BeforeSetProperties
.
Processing Method | Processing Result |
---|---|
Continue |
nil. |
Fail |
only logs failure. |
Cancel |
N/A |
BeforeCloseGame(IBeforeCloseGameCallInfo info)
Precondition: all peers disconnected.
Processing Method | Processing Result |
---|---|
Continue |
triggers OnCloseGame . |
Fail |
only logs failure. |
Cancel |
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 |
- 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.
- Before processing the
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 tosenderActor
.
targetGroup
parameter is ignored.
IfsenderActor
is equal to0
, the behaviour is equivalent to the one wheretarget
is set to0
(ReciverGroup.All
).2
(ReciverGroup.Group
): only active actors subscribed to the interest group specified usingtargetGroup
argument.
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.
The IPluginHost.Broadcastevent
methods do not support all cache operations though.
All cacheOp
values higher than 6 are not accepted and the BroadcastEvent
call will fail.
Use IPluginHost.ExecuteCacheOperation
method in general for interacting with the room's events cache.
Read more about "Photon Events Caching".
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.
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)
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);
}
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.
-
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.
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, ICallInfo info)
.
Asynchornous HTTP requests
You can delay processing of a hook until a HTTP response is received:
C#
HttpRequest request = new HttpRequest()
{
Callback = OnHttpResponse,
Url = yourCustomUrl,
Async = true,
UserObject = yourOptionalUserObject
};
PluginHost.HttpRequest(request, info);
When the response is received you should decide how you want to process the ICallInfo
if needed.
C#
private void OnHttpResponse(IHttpResponse response, object userState)
{
ICallInfo info = response.CallInfo;
ProcessCallInfoIfNeeded(info);
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 theHttpRequestQueue
.
A timer starts when a request is enqueued. It times out when the previous queries take too much time.Offline (3)
: The application's respectiveHttpRequestQueue
is in offline mode.
NoHttpRequest
should be made during 10 seconds which is the time theHttpRequestQueue
takes to reconnect.QueueFull (4)
: TheHttpRequestQueue
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 whenIHttpResponse.Status
is equal toHttpRequestQueueResult.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 way to schedule or delay code execution from a plugin.
StopTimer
for every timer object you create no matter its type.
The last good timing to clean up those timers is BeforeCloseGame
.
It's also recommended to not keep reference to timers after they are stopped so you may set them to null
.
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 method:
object CreateOneTimeTimer(ICallInfo callInfo, Action callback, int dueTimeMs);
To stop the timer call void IPluginHost.StopTimer(object timer)
.
If the timer is stopped before it's elapsed no action will be called.
However, we still recommend to stop the timer even after the scheduled action has been executed.
Example: delaying SetProperties
C#
public override void BeforeSetProperties(IBeforeSetPropertiesCallInfo info)
{
object oneTimeTimer = null;
ontTimeTimer = PluginHost.CreateOneTimeTimer(
info,
() => {
info.Continue();
PluginHost.StopTimer(oneTimeTimer);
oneTimeTimer = null;
},
1000);
}
Example 2: scheduled single time event
C#
private object ontTimeTimer;
public override void OnCreateGame(ICreateGameCallInfo info)
{
info.Continue();
ontTimeTimer = PluginHost.CreateOneTimeTimer(
info,
ScheduledOneTimeEvent,
1000);
}
private void ScheduledOneTimeEvent(){
// ...
}
public override void BeforeCloseGame(IBeforeCloseGameCallInfo info)
{
PluginHost.StopTimer(ontTimeTimer);
oneTimeTimer = null;
info.Continue();
}
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 method:
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 still ongoing).
It can be stopped at any time using void IPluginHost.StopTimer(object timer)
.
Example: scheduled events
C#
private object repeatingTimer;
public override void OnCreateGame(ICreateGameCallInfo info)
{
info.Continue();
repeatingTimer = PluginHost.CreateTimer(
ScheduledEvent,
1000,
2000);
}
private void ScheduledEvent()
{
BroadcastEvent(1, new Dictionary<byte, string>() { { 245, "Time is up" } });
}
public override void BeforeCloseGame(IBeforeCloseGameCallInfo info)
{
PluginHost.StopTimer(repeatingTimer);
repeatingTimer = null;
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 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
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:
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
Enterprise Cloud Logging Configuration
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.
Self-Hosted Server Logging Configuration
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: new way, the new loggers have prefix "Plugin" -->
<logger name="Plugin" additivity="false">
<level value="INFO" />
<appender-ref ref="PluginLogFileAppender" />
</logger>
<!-- CUSTOM PLUGINS: old way -->
<logger name="Photon.Hive.HiveGame.HiveHostGame.Plugin" additivity="false">
<level value="DEBUG" />
<appender-ref ref="PluginLogFileAppender" />
</logger>
Plugins Versioning in Enterprise Cloud
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
- Upload new version of plugins assembly.
- On a staging AppId: test to verify that new version works as expected. (recommended)
- Update production AppId configuration to use new plugins assembly version.
B. "Incompatible" plugins deploy: requires new client version
- Upload new version of plugins assembly.
- Setup a new production AppId.
- 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 implementIGamePlugin.OnUnknownType
orIGamePlugin.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 implementOnRaiseEvent
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.To get PUN's
PhotonNetwork.ServerTimestamp
in a Photon Plugin, useEnvironment.TickCount
.
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, inOnBeforeCloseGame
andOnCloseGame
,IBeforeCloseGameCallInfo.AuthCookie
andICloseGameCallInfo.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>; }
Update the secure authentication cookie per actor using
void IActor.UpdateSecure(string key, object value)