This document is about: SERVER 4
SWITCH TO

Photon Plugins FAQs

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

Configuration

How to configure custom plugins for Photon Server?

You should add the Plugin XML node in the app.config.

Here are the minimal required elements:

XML

    <PluginSettings Enabled="true"> 
        <Plugins> 
          <Plugin
              AssemblyName="{filename}.dll"
              Version=""
              Type="{namespace}.PluginFactory" />
        </Plugins>
     </PluginSettings>

Here is an example of configuration for WebHooks plugin:

XML

<PluginSettings Enabled="true">
    <Plugins>
        <Plugin
          Name="WebHooks"
          Version=""
          AssemblyName="PhotonHive.WebhooksPlugin.dll"
          Type="Photon.Hive.Plugin.WebHooks.PluginFactory"
          BaseUrl="<custom webhooks base url>"
          IsPersistent="true"
          HasErrorInfo="true"
          PathClose="GameClose"
          PathCreate="GameCreate"
          PathEvent="GameEvent"
          PathGameProperties="GameProperties"
          PathJoin="GameJoin"
          PathLeave="GameLeave"
          PathLoad="GameCreate" />
    </Plugins>
</PluginSettings>

For more information, read the "Configuration" section in the Plugins Manual.

Does Photon support multiple plugins?

Only one plugins assembly (DLL) or plugins factory can be configured at a time per application.
In this DLL you can have as many plugins as you want.
Only one plugin will be loaded and instantiated upon room creation.
There is a 1 to 1 relationship between room and instance of a plugin - so each room has its own instance of a plugin.

The following configuration is not allowed: ```xml ```

How to choose which plugin to use when creating a room?

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

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.

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) 
    {
        var 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;
    }
}

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.

I'd like to read files from disk on runtime. Do plugins have access to the file system? Do I need to download the files from an external server?

You can upload extra files along with the ones essential to the plugin.
The path to the files can then be retrieved using typeof(yourplugin).Assembly.Location.

Do dependency modules (DLLs) get automatically loaded with the main plugins DLL? Why am I getting a System.TypeLoadException?

Using external modules should work by referencing the DLLs or projects in the plugins solution.
You need to make sure all referenced dependencies' modules get deployed to the same directory as the plugin DLL in order for them to get loaded and linked.

Callbacks

What methods are called when a player tries to enter a room?

In order to answer this question, we need to understand that there are four different scenarios that involves a player trying to enter a room either by creating, joining or rejoining it.

In general the structures of JoinRoom and CreateRoom operations are very similar.
It might help to see them as one logical Join operation with different JoinMode values:

  1. Create: OpCreateRoom: error is returned if the room already exist
  2. Join: OpJoinRoom (JoinMode.Default or not set), OpJoinRandomRoom: error is returned if the room doesn't exist
  3. CreateIfNotExists: OpJoinRoom (JoinMode.CreateIfNotExist): create room if it doesn't exist
  4. RejoinOnly: OpJoinRoom (JoinMode.RejoinOnly): error if actor does not exist in room already

If the room is in memory 2) to 4) will trigger a BeforeJoin and assuming you call {ICallInfo}.Continue(), OnJoin will be called.

OnCreateGame is called right after the room creation, once the plugin is setup and just before the actor is added to the room.
This can be triggered by 1), 3) and 4).
The latter can happen only if saving and loading the state is being handled properly.
This happens when a player asks to rejoin and the room that was removed from servers memory.
Photon will create it anyway and setup the plugin.
The plugin is then expected to retrieve the serialized state from a database or an external service and call SetSerializedGameState.
The state will contain a list of actors, all in inactive state.
The rejoin reactivates the rejoining actor.

How to get the actor number in plugin callbacks?

Here is how to get ActorNr in plugins hooks:

1. OnCreateGame, info.IsJoin == false:

ActorNr = 1: The first actor of the game, the one who creates it, will always have the actor number set to 1.

2. OnCreateGame, info.IsJoin == true:
a. before info.Continue();

Get ActorNr by UserId from list of inactive actors: In case the room is being "recreated" by loading its game state.
Then you need to find the actor number in your game state by looping over ActorList from the loaded room state and comparing with the UserId.
(UserId may not be available which will make rejoin fail, this requires creating rooms with CheckUserOnJoin and unique UserId per Actor)

C#

// load State from webservice or database or re-construct it
if (this.PluginHost.SetGameState(state)) 
{
    int actorNr = 0;
    foreach (var actor in PluginHost.GameActorsInactive)
    {
        if (actor.UserId == info.UserId)
        {
            actorNr = actor.ActorNr;
            break;
        }
     }
     if (actorNr == 0) 
     {
         if (!asyncJoin) 
         {
             // error, join will fail with 
             // ErrorCode.JoinFailedWithRejoinerNotFound = 32748, // 0x7FFF - 19,
         } 
         else 
         {
             actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
         }
   }
}

Otherwise, if you send the "correct" ActorNr from client in JoinRoom operation, you can get it from info.Request.ActorNr.

b. after info.Continue();

Get ActorNr by UserId from list of active actors.

C#

int actorNr;
foreach (var actor in PluginHost.GameActorsActive)
{
    if (actor.UserId == info.UserId)
    {
        actorNr = actor.ActorNr;
        break;
    }
}
3. BeforeJoin
a. before info.Continue();

C#

int actorNr = 0;
switch (info.Request.JoinMode)
{
    case JoinModeConstants.JoinOnly:
    case JoinModeConstants.CreateIfNotExists:
        actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
        break;
    case JoinModeConstants.RejoinOnly:
        foreach (var actor in PluginHost.GameActorsInactive)
        {
            if (actor.UserId == info.UserId)
            {
                actorNr = actor.ActorNr;
                break;
             }
         }
         if (actorNr == 0)
         {
             // error, join will fail with 
             // ErrorCode.JoinFailedWithRejoinerNotFound = 32748, // 0x7FFF - 19,
          }
          break;
      case JoinModeConstants.RejoinOrJoin:
          foreach (var actor in PluginHost.GameActorsInactive)
          {
              if (actor.UserId == info.UserId)
              {
                  actorNr = actor.ActorNr;
                  break;
              }
           }
           if (actorNr == 0)
           {
               actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
           }
           break;
}

Otherwise, if you send the "correct" ActorNr from client in JoinRoom operation, you can get it from info.Request.ActorNr.

b. after info.Continue();

Get ActorNr by UserId from list of active actors.

C#

int actorNr;
foreach (var actor in this.PluginHost.GameActorsActive)
{
    if (actor.UserId == info.UserId)
    {
         actorNr = actor.ActorNr;
         break;
    }
}
4. OnJoin, OnRaiseEvent, BeforeSetProperties, OnSetProperties, OnLeave:

Use the available info.ActorNr.

5. BeforeCloseGame, OnCloseGame:

No way to get ActorNr as the hook is triggered by server and not by client operation.

Is it possible to create rooms from the server?

No it is not possible to do so.

Is it possible to remove rooms from the server?

No it is not possible to do so.

What is the best way to know what data is available in the plugin events?

In general, the ICallInfo callback parameter of the event should expose what it is needed.
For most cases, actual operation request parameters can be retrieved through the {ICallInfo}.OperationRequest (or Request) property.

What does UseStrictMode do?

The idea of the plugin is that you hook into the "normal" Photon flow before we process the incoming request or after.

Initially we did not enforce that you had to call anything. Which essentially was the same as canceling default processing of received request.
This makes the developer responsible of implementing whatever it is required from scratch without making use of any of the default callbacks logic.
This has caused some unexpected issues. Canceling is not always what the developer wanted to do.
So we introduced the strict mode - which now expects a decision by the developer.
Now you are expected to call Continue, Fail or Cancel only once (any of them). There is also the Defer method which delays the process.

When overriding callback methods of PluginBase, do I need to call base.XXX()?

All callback methods inside PluginBase include a call to Continue() at the end.
The idea is that by inheriting from the PluginBase you only have to override the methods you are interested in.
Sometimes you just need to add extra code after or before base.XXX() and sometimes you need to change the default behavior completely.
In the first case, you should not add any call to the ICallInfo processing methods.
In the second case, you should not call base.XXX(), make your own implementation of the method from scratch then add a call to one of the available ICallInfo processing methods.
There is one exception though, which is PluginBase.OnLeave.
This method will handle MasterClient change in case the current one is leaving.

Events

What does the HttpForward property and WebFlags class do?

These are WebHooks related features.
WebFlags are introduced in WebHooks v1.2 plugin. Go to this page to read more about WebFlags.
HttpForward is a property that indicates the value of the corresponding webflag.

Although they are initially made for WebHooks and WebRPC, you may want to use them in your plugin.

How to send events from plugins?

PluginHost.BroadcastEvent should be used for this purpose.
It works in the same way as if you're sending event from client with the main difference,
that you can set the sender actor number to 0 to represent the server.

C#

this.PluginHost.BroadcastEvent(
            recieverActors: new int[] { targetPlayerNr },
            senderActor: 0,
            data: new Dictionary<byte, object>() { { 1, data } },
            evCode: (byte)code,
            cacheOp: 0,
            sendParameters: new SendParameters() { Unreliable = false });

For more information, read the "Sending Events from Plugins" section in the Plugins Manual.

Why does PluginHost.BroadcastEvent never succeed in sending events to client when called inside OnJoin callback unlike when done from OnRaiseEvent?

In OnRaiseEvent the client is already joined so it's able to receive events.
In OnJoin, the client is not fully joined unless the request is processed using {ICallInfo}.Continue().

So if PluginHost.BroadcastEvent is called after {ICallInfo}.Continue(), the event should be received by the target clients.

The plugin hooks work in this way: you trigger Photon's normal processing with the call to {ICallInfo}.Continue().
In this case sending an event after the join is fully completed makes more sense.

Why clients can't receive event data sent from plugins?

This is a known issue. Clients expect events to have a certain predefined structure with well known key codes.
245 for event data and 254 for actor number.
To fix this, simply do a minor change in the way you send events data from plugins:
instead of (Dictionary<byte,object>)eventData send new Dictionary<byte,object>(){{245,eventData},{254,senderActorNr}}.

Does plugins support custom operations?

No, currently plugins do not support custom operations.
Photon plugins offer callbacks only for a set of native operations ("Create", "Join", "SetProperties", "RaiseEvent" and "Leave").
You can't intercept custom operations (added in a self-hosted Photon Server) or extend new ones using Plugins SDK.

However you can achieve the same results by exchanging 2-ways events:

  • From client to plugins by calling LoadBalancingClient.OpRaiseEvent.
  • From plugins to client by calling PluginHost.BroadcastEvent.

Game State

What distinguishes an "active" user and an "inactive" user?

Once a player disconnects photon usually cleans up and the actor is removed, but you can define a PlayerTTL in CreateOptions on the client when creating the room.
If it is strictly positive, Photon waits for that amount of time (which is defined in milliseconds) before cleaning up.
Meanwhile, the actor is considered inactive and can rejoin the game. If that is successfully done, the player is
active again.

The feature is useful if you want to save the game state and allow players to continue or for instance RTS games where players can get disconnected due to bad connectivity but it's OK for a short time (minutes) to allow them to return.

PluginHost.GameActorsActive contains all actors inside a room (joined) and PluginHost.GameActorsInActive contains all actors who left the room (without abandoning).

Is it possible to make an actor leave a room from a plugin? If so how?

Yes that is possible. From the plugin class, you should call PluginHost.RemoveActor(int actorNr, string reasonDetails).
You can set the reason by calling the method overload that accepts three parameters:
PluginHost.RemoveActor(int actorNr, byte reason, string reasonDetails).

How to persist the room state?

To save a room state:

  1. call PluginHost.GetGameState and get a SerializableGamestate.
  2. serialize the state (JSON for instance).
  3. save the state to your data store.

To load a room state:

  1. retrieve the state from your data store.
  2. desrialize your state.
  3. call PluginHost.SetGameState.

Note: you are only allowed to call PluginHost.SetGameState in OnCreateGame before calling {ICallInfo}.Continue().

I can't find some custom room properties inside the SerializableGameState? Only lobby properties why is that?

In the serializable game state, we don't provide access to all custom properties by design.
Only those you share with the Lobby are exposed and it is only for "viewing" purposes.
All properties combined are contained in binary arrays.
This allows to serialize to JSON and not lose the type information when deserializing back.
This feature was designed mainly for the Save/Load scenario.
We may change this behaviour in the future.

How to access room properties (MaxPlayers, IsVisible, IsOpen, etc.) from plugins?

All room properties can be accessed from PluginBase.PluginHost.GameProperties which is of type Hashtable.
Those properties include the "native" or "well-known" ones.
They have byte keys that refers to values that can be retrieved from the LoadBalancing.GamePropertyKey either from LoadBalancing client SDK
or from the respective API reference page.

Just make sure that you do cast keys to byte from int (e.g. MaxPlayers = (byte)255).
We'll provide a more convenient way to get those values in the future.

PluginHost.GameProperties also contains custom room properties.
The developer is responsible of handling those key/values.

On the other hand, only the custom properties visible to the lobby are stored in PluginBase.PluginHost.CustomGameProperties which is a Dictionary<string, object>.

You can access (read and write) room and actor properties from plugins as follows:

Reading examples:

C#

PluginHost.GameProperties.ContainsKey("map");
PluginHost.GameActors[1].Properties["health"];

Writing examples:

C#

PluginHost.SetProperties(actorNr: 0, properties: new Hashtable() { { "map", "america" } }, expected: null, broadcast: false); // actor=0 for Room properties
PluginHost.SetProperties(actorNr: 1, properties: new Hashtable() { { "health", 100 } }, expected: null, broadcast: true);

Threading

More details about the threading requirements of the .NET Plugin component

How many threads are running on a single Photon server?

The use of threads is split:

  1. Native - 9 threads
  2. Managed - based on .Net ThreadPool which uses .NET default settings

This setup has been tested quite intensively and works very well for a great variety of load profiles.

The use of managed threads (as reported by the .NET Windows Performance counters) varies:

a) ~12 on a typical Photon cloud RealTime load
b) 35 (and more) as an example of a customer cloud running a plugin with inter-plugin communication (locking), causing some higher contention (as opposed to our code)

Note: If necessary, .Net ThreadPool settings can be adjusted.
So far we've had good results with the defaults, although they may be different for each version.

Is the Photon host free threaded? Can any thread access any room at any time?

We have a message passing architecture: your plugin will be called only by one thread at a time.
However, the thread may not be the same in each call since we use a thread pool.

Are there any thread safety concerns with writing a plugin?

In general it's safe to assume all calls into the plugin are serialized (virtually on one thread / not necessarily on the same physical thread).

Enterprise Cloud

Questions related to the runtime environment of the Enterprise Cloud.

How to configure plugins?

For Photon Enterprise Cloud:
You should add a new plugin by going to the Photon dashboard, then the management page of the application.
Once there you should click on the "Create a new Plugin" button on bottom of the page.
Now you can configure the plugin by adding key/value entries.
AssemblyName, Version, Path and Type are required ones.

What is the pipeline process in making Photon plugins?

The pipeline process of making Photon plugins is simple:

  1. Download the required SDKs and server binaries.
  2. Code and build a plugin assembly.
  3. Deploy and test it.
  4. Upload it.
  5. Configure it.

The environment setup for Photon plugins could be:

  • Development: local machine.
  • Test: in your local network.
  • Staging: separate AppId in our cloud.
  • Production: live AppId in our cloud.

Is the plugin upload automated?

Yes, we provide PowerShell scripts for Enterprise customers to help them manage their private cloud.
Check out the plugins upload online guide for full details.

How to monitor performance of plugins?

We track a bunch of counters that are available in our Dashboard.
Additionally, we can add custom counters for you or you could integrate an external tool for counters as well (e.g. New Relic)
If you require those services, a consulting agreement needs to be arranged.

Is there a way to have any plugins' logs?

Access to log files on our servers is not granted.
So 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.

How do I get the Logentries token?

Create a Logentries account and follow these steps:

  1. Select "Logs"/"Add New Log".
  2. Select "Libraries"/".NET".
  3. Enter a name for your log set.
  4. Click "Create Log Token".
  5. Click "Finish & View Log".
  6. Select your new log set and choose the "Setting" tab. You can view the token now.
  7. Send us that token via email.

How do I get the Papertrail URL?

Create a Papertrail account and follow these steps:

  1. Add "System"
  2. On top of the next page you will see something like "Your logs will go to logs6.papertrailapp.com:12345 and appear in Events."
  3. Send us that URL via email.

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.

Another possible yet more advanced technique:

You could build a plugin DLL that has the core server update loop while the actual game logic is from another DLL that will be loaded explicitly.
The core plugin DLL should not be updated while the game logic should have more than one DLL per version.
The core plugin DLL will load the appropriate game logic DLL based on the client version.
It is like mapping server side code with client side code for a full compatibility.
This allows for a version compatible updates of plugins:
This way you do not need to force client updates, when a new plugin version is rolled out.
You could put the game logic DLLs in separate folders per version or inside the same folder but with different names per version.

Can we use static fields inside a plugin?

Same plugins assembly will be shared across rooms and applications.
Static fields of same plugin class will be shared as well.
If you cannot avoid using static fields, here is what you could do to avoid using same plugins assembly in two applications:

  1. Upload same plugin files under two different plugin names:

    a- Upload plugin archive with name X
    b- Upload plugin archive with name Y

  2. Same configuration for two apps except "Path":

    a- Configure app A to use plugin X: "Path": "{customerName}\X"
    b- Configure app B to use plugin Y: "Path": "{customerName}\Y"

Miscellaneous

How to retrieve reason given when the plugin executes RemoveActor?

Currently, we don't send the reason (code byte or message string) to the client.
You can use a custom event to inform the client before disconnecting it.
For instance, you could send a custom event with the reason and schedule the RemoveActor 200ms later using a timer.

You could make use of these helper methods:

C#

private const int RemoveActorEventCode = 199;
private const int RemoveActorTimerDelay = 200;

private void RemoveActor(int actorNr, string reason)
{
    this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode, 
        new Dictionary<byte, object> { { 254, 0 }, { 245, reason }}, 0);
    this.PluginHost.CreateOneTimeTimer(() => this.PluginHost.RemoveActor(actorNr, reason),
        RemoveActorTimerDelay);
}

private void RemoveActor(int actorNr, byte reasonCode, string reason)
{
    this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode, 
        new Dictionary<byte, object> { { 254, 0 }, { 245, new { reasonCode, reason } }}, 0);
    this.PluginHost.CreateOneTimeTimer(() => this.PluginHost.RemoveActor(actorNr, reason),
        RemoveActorTimerDelay);
}

Client SDKs support extending serialization by registering custom types. How would I deserialize those types on the server?

You can register custom types just like in client SDKs, as follows:

C#

PluginHost.TryRegisterType(type: typeof (CustomPluginType), typeCode: 1, serializeFunction: SerializeFunction, deserializeFunction: DeserializeFunction);

For more information please read the "Custom Type" section in the Plugins Manual.

Is there a limit on the file size of the plugins .DLL?

No, but we think it should not be that big.

How to get PUN's PhotonNetwork.ServerTimestamp in a Photon Plugin?

Use Environment.TickCount.

Back to top