Modular Plugins
Photon Plugins are available only for Enterprise Cloud or self-hosted Photon Server.
The modular plugin framework is an optional layer on top of the plugins API described in the Plugins Manual.
Instead of writing one plugin class that handles every concern, you split the behavior into independent modules and stack them.
For every room callback (OnCreateGame, OnJoin, OnRaiseEvent, ...) the modules are called one after the other, in the order you chose.
Each module decides whether the call goes on to the next module or stops there.
Use it when:
- your plugin layers several concerns (security checks, forwarding to a web service, voice handling, your own game logic) and you want each of them separated and reusable;
- you want to use the modules shipped with the Plugins SDK (Security, Voice and WebHooks) in any combination, and add your own logic on top.
If your plugin is small, extending PluginBase as described in the manual is simpler and perfectly fine.
Concepts
The framework lives in the PhotonHivePlugin.Modules assembly (namespace Photon.Hive.Plugin.Modules).
It builds on PhotonHivePlugin.dll and replaces PluginBase and PluginFactoryBase by the following classes.
| You write | Base class | Purpose |
|---|---|---|
| Module | PluginModuleBase |
Handles room callbacks for one concern. One instance per room. |
| Module factory | PluginModuleFactoryBase |
Creates the module of each room. Once per server it can also create state shared by all rooms (workers, caches, ...). |
| Plugin | ModularPlugin |
The plugin instance of a room. Holds the ordered list of modules. Your own logic goes into its OnXxxCore methods. |
| Plugin factory | ModularPluginFactory |
The factory class referenced from plugin.config. Declares the module factories and creates the plugin. |
Other types you will meet:
| Type | Purpose |
|---|---|
ChainStep |
Passed to each module callback. Calling Next() hands the call over to the next module. |
EventScope |
Lives for the duration of one callback dispatch. Lets modules publish and consume the results of asynchronous work. |
PendingResult<T> |
A handle on the result of an asynchronous operation published in an EventScope. |
IModuleContext, FactoryServices |
Registries to share objects between modules (per room and per factory respectively). |
Getting Started
The following steps build a plugin that has one module which logs every join.
The Module
C#
using System.Collections.Generic;
using Photon.Hive.Plugin;
using Photon.Hive.Plugin.Modules;
public class JoinLoggerModule : PluginModuleBase
{
// Prefix of this module's configuration keys, e.g. "jl_SomeSetting".
public override string Identifier => "jl";
private IPluginLogger log;
public override bool Setup(IPluginHost host, Dictionary<string, string> config, IModuleContext context, out string errorMsg)
{
this.log = host.CreateLogger("JoinLogger");
errorMsg = string.Empty;
return true;
}
public override void OnJoin(IJoinGameCallInfo info, ChainStep step)
{
this.log.InfoFormat("User {0} joined {1}", info.UserId, info.Request.GameId);
step.Next(); // hand over to the next module
}
}
All the other callbacks of PluginModuleBase do step.Next() by default, so a module only overrides what it needs.
Note that a module does not call info.Continue(): when the last module has called step.Next(), the plugin's OnJoinCore method runs and, by default, continues the call (see How Calls Flow Through Modules).
The Module Factory
C#
public class JoinLoggerModuleFactory : PluginModuleFactoryBase
{
public override PluginModuleBase CreateModule(IPluginHost gameHost, Dictionary<string, string> gameConfig)
{
return new JoinLoggerModule();
}
}
The Plugin and the Plugin Factory
C#
public class GamePlugin : ModularPlugin
{
// Must be the name the clients request in roomOptions.Plugins[0].
public override string Name => "GamePlugin";
}
public class GamePluginFactory : ModularPluginFactory
{
public override IGamePlugin CreatePlugin(string pluginName)
{
return new GamePlugin();
}
protected override IEnumerable<PluginModuleFactoryBase> CreateModuleFactories()
{
yield return new JoinLoggerModuleFactory();
}
}
ModularPlugin is abstract, so you always need a subclass, even if it has no overrides.
Override Name.
By default the name of a ModularPlugin is the list of its modules' names joined by +, and the names of some modules depend on their settings (for example "SecurityChecksV1.2+VoiceGGWP+Webhooks").
If clients request a plugin by name using roomOptions.Plugins, the name must match, otherwise the create or join operation fails with PluginMismatch.
See Plugin Factory.
Configuration and Deployment
Configure GamePluginFactory as the Type of the plugin as described in the Plugins Manual.
Your project references PhotonHivePlugin.dll and PhotonHivePlugin.Modules.dll.
Both come with the Plugins SDK, in the same folders: use the ones for the runtime of your server (net471 for Photon Server 5.x, netcoreapp3.1 for 6.x, see Photon Server 5.x and 6.x).
Deploy PhotonHivePlugin.Modules.dll together with your plugin assembly: unlike PhotonHivePlugin.dll, the server does not provide it.
The framework (PhotonHivePlugin.Modules.dll) is a binary, but the shipped Security, Voice and WebHooks modules are distributed as source code.
Each of them comes with a .props file (e.g. PhotonHivePlugin.Modules.Security.props) that you import in your plugin's project file.
It adds the module's source files to your assembly, so these modules are part of your plugin DLL and need no extra deployment.
They need Newtonsoft.Json, which you have to reference and deploy.
The "HivePlugin.Modules.TestPlugin" sample project in the Plugins SDK does this for all three modules.
How Calls Flow Through Modules
ModularPlugin implements all the plugin callbacks of IGamePlugin for you.
When the server calls one, for example OnJoin(IJoinGameCallInfo):
- the active modules are called one after the other, in the order they were added to the plugin, with the same call info and a
ChainStep; - when a module calls
step.Next(), the next module is called; - when the last module has called
step.Next(), the matchingOnXxxCoremethod of your plugin class runs. Its default implementation callsinfo.Continue(). This is where you put your own logic, and it is the only place that has to decide how to process the call info (Continue,FailorCancel) if every module forwarded it.
| Module callback | Plugin terminal (override it in your ModularPlugin subclass) |
|---|---|
OnCreateGame(info, step) |
OnCreateGameCore(info) |
BeforeJoin(info, step) |
BeforeJoinCore(info) |
OnJoin(info, step) |
OnJoinCore(info) |
OnLeave(info, step) |
OnLeaveCore(info) and OnLeaveMasterClientChanged(oldId, newId) |
OnRaiseEvent(info, step) |
OnRaiseEventCore(info) |
BeforeSetProperties(info, step) |
BeforeSetPropertiesCore(info) |
OnSetProperties(info, step) |
OnSetPropertiesCore(info) |
BeforeCloseGame(info, step) |
BeforeCloseGameCore(info) |
OnCloseGame(info, step) |
OnCloseGameCore(info) |
OnRawMessage(info, step) |
OnRawMessageCore(info) |
OnUnknownType(type, ref value) |
OnUnknownTypeCore(type, ref value) |
ReportError(code, exception, state) |
ReportError(code, exception, state) (override and call base) |
The OnXxx methods of ModularPlugin itself are sealed.
Override the OnXxxCore methods instead.
The call info rules of the manual still apply: exactly one of Continue, Fail or Cancel must be called for each call info, either by a module, which then does not call step.Next(), or by the terminal.
A module has three options in each callback:
- Forward: call
step.Next()before returning. - Forward later: keep the
ChainStepand callNext()from an asynchronous continuation, such as the callback of an HTTP request, or a timer. The chain is paused until then. - Stop: do not call
Next(). The module is then responsible for processing the call info itself, usually withinfo.Fail(...)orinfo.Cancel(). Modules after it, and the terminal, are not called. This is how the Security module rejects calls and the Voice module consumes voice events.
Call Next() at most once per callback.
A second call throws an InvalidOperationException, also if the first one happened synchronously and the second one from a callback.
If a module neither calls Next() nor processes the call info (and has not started asynchronous work that will do it later), strict mode fails the call.
Two callbacks are not chained:
OnUnknownType: the modules are asked in order and the first one to returntruewins. If none does,OnUnknownTypeCoreis called.ReportError: every active module is told, an exception thrown by a module is swallowed, and then the base implementation runs.
Writing Modules
Module Properties
| Member | Meaning |
|---|---|
Identifier |
Prefix of the module's configuration keys (<Identifier>_<key>). Defaults to the class name; override it with a short string. |
Name, Version |
Reported in diagnostics. A ModularPlugin joins the names (and versions) of its modules with + as its own name (and version) unless you override them. |
IsPersistent |
true if the module needs the room state to be saved and loaded. The plugin is persistent if any module is. |
IsActive |
Switch to disable the module for a room. See Inactive Modules. |
Scope |
The EventScope of the current callback. Valid only while a module callback is running. |
Module Lifecycle
Every room goes through the following steps, in this order:
Register(IModuleContext)of every module: publish the services this module provides.Setup(host, config, context, out errorMsg)of every module, in order: read the configuration, resolve services, create loggers. Returningfalsefails the creation of the plugin (and hence of the room) with an error message "Module '{name}' setup failed: ...".SetupInstanceCoreof your plugin: your own initialization.- The room callbacks.
The config passed to Setup is the same dictionary as the one passed to IGamePlugin.SetupInstance: all the key/value attributes of the plugin in plugin.config.
Modules read their keys directly from it, using their identifier as the prefix.
Module Factories
A module factory has two methods:
ConfigureFactory(IFactoryHost host, FactoryServices services, Dictionary<string, string> factoryConfig)is called once when the plugin factory is initialized, for each module factory in declaration order. Create here what all the rooms share: background workers (host.CreateWorkerHost), random number generators, log guards. Keep them in fields of the module factory. Optionally publish them inservicesso that other modules can look them up.CreateModule(IPluginHost gameHost, Dictionary<string, string> gameConfig)is called for each room. Create the module and give it what it needs from the fields.
C#
public class StatsModuleFactory : PluginModuleFactoryBase
{
private StatsCollector stats;
public override void ConfigureFactory(IFactoryHost host, FactoryServices services, Dictionary<string, string> factoryConfig)
{
this.stats = new StatsCollector();
services.Provide(this.stats); // optional: lets other modules call context.Get<StatsCollector>()
}
public override PluginModuleBase CreateModule(IPluginHost gameHost, Dictionary<string, string> gameConfig)
{
return new StatsModule(this.stats);
}
}
Sharing Services Between Modules
Modules can share objects without knowing each other, using a registry:
IModuleContext.Provide<T>(instance)publishes an object for the room.IModuleContext.Get<T>()andTryGet<T>(out instance)look it up. Lookups search the room's services first, then the factory-wide services published withFactoryServices.Provide<T>. Types are matched exactly: the type you ask for must be the type that was provided.- In your plugin class use
Get<T>()andTryGet<T>()(available once the setup is done).
Publish in Register, and look up in Setup:
C#
public class CacheModule : PluginModuleBase
{
public override string Identifier => "cm";
public override void Register(IModuleContext context)
{
context.Provide(new SharedCache()); // available to every module's Setup
}
}
public class StatsModule : PluginModuleBase
{
public override string Identifier => "st";
private SharedCache cache;
public override bool Setup(IPluginHost host, Dictionary<string, string> config, IModuleContext context, out string errorMsg)
{
this.cache = context.Get<SharedCache>(); // every module's Register is done by now
errorMsg = string.Empty;
return true;
}
}
Because all the Register methods run before the first Setup, the order of the modules does not matter for this.
Do not call Get or TryGet inside Register: it throws, as other modules may not have published yet.
Composing Modules
Declaring and Ordering Modules
By default the modules of the module factories declared in CreateModuleFactories() are added to the plugin in declaration order, which is also their dispatch order.
Order matters because a module can stop the chain: put the modules that reject calls first.
For example, if the Voice module is before your module, your module never sees the voice events that the Voice module consumes.
C#
public class GamePluginFactory : ModularPluginFactory
{
public override IGamePlugin CreatePlugin(string pluginName)
{
return new GamePlugin();
}
protected override IEnumerable<PluginModuleFactoryBase> CreateModuleFactories()
{
yield return new SecurityModuleFactory(); // first: rejects invalid calls
yield return new VoiceModuleFactory();
yield return new WebHooksModuleFactory();
}
}
You can also add a module directly, with ModularPlugin.Use(module).
Use returns the plugin so that calls can be chained.
Modules added in CreatePlugin come before the ones of the module factories:
C#
public override IGamePlugin CreatePlugin(string pluginName)
{
return new GamePlugin().Use(new RateLimitModule()); // runs before all module factory modules
}
To put a module between modules of module factories, override ConfigurePlugin and add the modules yourself in the order you want.
The module factories are available in Factories, in declaration order:
C#
protected override void ConfigurePlugin(ModularPlugin plugin, IPluginHost gameHost, Dictionary<string, string> gameConfig)
{
plugin.Use(this.Factories[0].CreateModule(gameHost, gameConfig)); // Security
plugin.Use(new JoinAuditModule()); // your module
plugin.Use(this.Factories[1].CreateModule(gameHost, gameConfig)); // Voice
}
Rules for Use:
- It must be called before the plugin is set up (in
CreatePlugin,ConfigurePluginor the constructor). - A plugin can have only one module of each concrete type. If you need the same behavior twice, derive a second class.
Your Own Logic on Top of Modules
Override the OnXxxCore methods in your ModularPlugin subclass.
They run after every module has forwarded the call.
In addition:
SetupInstanceCore(host, config, out errorMsg)is your initialization hook.SetupInstanceis sealed inModularPlugin. It runs after all modules are registered and set up, soGet<T>()andGetModule<T>()work. Returningfalsefails the creation of the plugin.ModularPluginFactory.OnFactoryHostSetCore(FactoryParams)replacesOnFactoryHostSet, which is sealed. It runs after all module factories are configured.OnLeaveMasterClientChanged(oldId, newId)replacesOnChangeMasterClientId.
The following example uses the result of the WebHooks module (see Asynchronous Work):
C#
public class GamePlugin : ModularPlugin
{
public override string Name => "GamePlugin";
protected override void OnRaiseEventCore(IRaiseEventCallInfo info)
{
var result = this.Scope.Get<WebHookResult>("webhooks");
if (result == null)
{
info.Continue();
return;
}
result.OnComplete(r =>
{
if (r.Success) { info.Continue(); }
else { info.Fail(r.ErrorMessage); }
});
}
}
Inactive Modules
A module can be added to the plugin but not called for room callbacks: it is inactive.
An inactive module still goes through Register and Setup (so the services it depends on are in place) but the chain skips it, and it does not receive OnUnknownType and ReportError either.
This is useful when your plugin wants to call the module's functionality itself, at times you choose, for example under your own event codes.
There are four ways to make a module inactive. The last one is applied last and wins:
- the module constructor:
PluginModuleBasehas a constructor parameterisActive, and the shipped modules expose it, for examplenew SecurityModule(isActive: false); - the property: set
module.IsActive = falsebefore the plugin is set up; - a factory flag:
VoiceModuleFactoryoffers one,new VoiceModuleFactory { ModuleStartsActive = false }(for the Security and WebHooks modules use the constructor, the property or the configuration key); - the configuration key
<Identifier>_IsActiveset tofalsein the plugin's configuration, e.g.vm_IsActive.
Get hold of the module with GetModule<T>() (it throws if there is no such module) or TryGetModule<T>(out module) and call its public methods from your OnXxxCore methods:
C#
public class VoicePlugin : ModularPlugin
{
public override string Name => "VoicePlugin";
protected override void OnRaiseEventCore(IRaiseEventCallInfo info)
{
var voice = this.GetModule<VoiceModule>();
switch (voice.ProcessRaiseEvent(info)) // does the work, does not process the call info
{
case VoiceModule.RaiseEventResult.Continue: info.Continue(); return;
case VoiceModule.RaiseEventResult.Cancel: info.Cancel(); return;
}
// not a voice event: your own handling
info.Continue();
}
}
Note that GetModule<T>() looks for modules in the plugin, while Get<T>() looks for services in the registry.
A module designed for this use (like the Voice module) offers, next to each OnXxx callback, a ProcessXxx method that does the work but leaves the call info for you to process (see VoiceModule).
The OnXxx callback is a thin wrapper around it.
Asynchronous Work
A module often has to wait for something, usually an HTTP response.
Two things help: the ChainStep can be kept and used later, and the EventScope lets modules publish the outcome for the modules after them and for your terminal methods.
Event Scope and Pending Results
this.Scope returns the EventScope of the current dispatch.
It is available in module callbacks and in the OnXxxCore methods, and only there.
It stays valid across an asynchronous resume of the chain, but do not capture the scope itself in a callback: keep the PendingResult<T> you published (or ChainStep), or look the result up again with Scope.Get<T>(tag) when the chain resumes.
EventScope member |
Meaning |
|---|---|
BeginAsync<T>(tag) |
Publish a new pending result of type T with a string tag. Returns a PendingResult<T>. |
Get<T>(tag) |
Look up a published pending result by type and tag. Returns null if there is none. |
WhenAllComplete(action) |
Run action when all the results published so far are complete (immediately if they already are). |
PendingResult<T> member |
Meaning |
|---|---|
Tag |
The tag it was published with. |
IsCompleted, Result |
Whether the publisher has completed it, and its value. |
Complete(value) |
Called once by the publisher when the operation is done. Calling it twice throws. Subscribers run on the calling thread. |
OnComplete(action) |
Subscribe to the result. If it is already complete the action runs immediately. |
HasSubscribers, IfNoSubscribers(action) |
Used by publishers to find out whether a downstream consumer took over the continuation. |
Pattern 1: Forward Immediately
Send the request, keep the chain going, and let whoever is interested react later.
C#
public override void OnJoin(IJoinGameCallInfo info, ChainStep step)
{
var token = this.Scope.BeginAsync<bool>("telemetry");
var request = new HttpRequest
{
Url = this.telemetryUrl,
Method = "POST",
Async = true, // the room keeps processing messages meanwhile
Callback = (response, userState) => token.Complete(response.Status == HttpRequestQueueResult.Success)
};
this.host.HttpRequest(request, info);
step.Next(); // do not wait for the response
}
Pattern 2: Wait for the HTTP Response
Send the request and do not call Next().
The callback decides what happens with the call.
C#
public override void BeforeJoin(IBeforeJoinGameCallInfo info, ChainStep step)
{
var token = this.Scope.BeginAsync<bool>("validation");
var request = new HttpRequest
{
Url = this.validationUrl,
Method = "POST",
Async = false, // the room is paused until the response arrives
Callback = (response, userState) =>
{
var ok = response.Status == HttpRequestQueueResult.Success;
token.Complete(ok);
if (ok) { step.Next(); }
else { info.Fail("validation failed"); }
}
};
this.host.HttpRequest(request, info);
// return without calling step.Next(): the chain is paused
}
Later modules and your terminal method find the result with this.Scope.Get<bool>("validation").
The rules of outbound HTTP calls of the manual apply: the callback always runs on the room's fiber, so it can safely access room data.
Never call step.Next() both synchronously and from the callback.
Shipped Modules
The Plugins SDK includes three ready-made modules.
They all take their settings from the key/value attributes of the plugin in plugin.config (or the plugin's key/value settings in the Enterprise Cloud dashboard), using their identifier as the key prefix.
They can also be used from a standalone, non-modular plugin: the SDK keeps the original plugin implementations ("SecurityPlugin", "VoicePlugin" and "WebHooks1.2"), which share the source files with the modules.
All the values are strings. The Enterprise Cloud dashboard limits the length of each value (see Enterprise Cloud Configuration). Keep this in mind for the keys that take a JSON document.
SecurityModule
Identifier sp.
Class SecurityModule, factory SecurityModuleFactory.
It checks operations against a declarative policy: it rejects the operations that violate it, corrects some others (for example it limits MaxPlayers), and lets the rest through to the next modules.
Put it first in the chain.
It processes BeforeSetProperties, OnCreateGame and OnRaiseEvent (rejecting with Fail or Cancel) and observes the other callbacks for its analytics.
The module does nothing unless sp_version is set.
| Key | Meaning |
|---|---|
sp_version |
1.0, 1.1 or 1.2. Selects the set of checks. Missing or unknown: no checks at all. |
sp_maxPlayers |
MaxPlayers of the rooms. One number: every room gets this value. Two numbers separated by , or ; (min,max): the value requested by the client is raised or lowered to fit into that range. Default: 10. |
sp_cacheDeletion |
Who may remove events from the cache. Default: AllowOwner,AllowMaster. |
sp_playerPropsChange |
Who may change actor properties. Default: AllowOwner. |
sp_customGamePropsChange |
Who may change custom room properties. Default: AllowMaster. |
sp_wellKnownGamePropsChange |
Who may change well-known room properties. Default: AllowMaster. |
sp_blockEvents |
Event codes that clients can't raise (the call fails): a list of numbers separated by , or ;. Default: 207. |
sp_blockMasterClientChange |
true (default) rejects a client's attempt to make itself the master client (and a master client value that is not an integer). Other changes are subject to sp_wellKnownGamePropsChange. |
sp_disableCache |
true ignores the caching option of raised events: they are sent but not cached. Default: false. |
sp_enablePropIsOpen, sp_enablePropIsVisible, sp_enablePropMaxPlayers, sp_enablePropPlayerTTL, sp_enablePropEmptyRoomTTL, sp_enablePropExpectedUsers, sp_enablePropLobbyProp |
When a client may set the given well-known room property. One or more of Disable, AllowOnCreate, AllowChangeMaster, AllowChangeUser, AllowAll, separated by ,, \| or ;. |
sp_customGameProperties |
Version 1.1 and higher. A JSON object mapping custom room property names to the same values as above. |
sp_eventLimits |
Version 1.2 only. A JSON document with limits on the events clients raise, see below. |
sp_analytics |
A percentage (1-100) of rooms for which message statistics are recorded, or a JSON object with percent, reportIntervalSeconds, maxShapesPerSender and maxMembershipChanges. Works independently of sp_version. |
The "Who" values of the Allow... settings are combinations of AllowNobody, AllowOwner, AllowMaster and AllowAll, separated by ,, | or ;.
Event limits (sp_eventLimits, version 1.2) limit, per user and per room, how often an event code may be raised (rate per second, or total for the life of the room) and how big the request may be (maxSize in bytes).
Rules are given per event code, or * for one shared budget of all the codes without a rule.
action is Fail (default, the client gets an error), Cancel (silently dropped) or Log (only logged, to try out a policy).
Rules can be refined by looking at the data of the event.
A malformed document never prevents the plugin from loading: the problem is logged and the limits stay off.
JSON
{
"maxTrackedUsers": 1000,
"raiseEvent": {
"1": { "rate": 10 },
"100": { "total": 2 },
"202": { "rate": 200, "maxSize": 512, "action": "Cancel" },
"*": { "rate": 50 }
}
}
maxSize is not enforced for clients using the GpBinary 1.6 or 1.7 protocol, as the request size is not available for them.
Violations are logged by the SecurityChecks.EventLimits logger, and analytics records (JSON lines) by SecurityChecks.Analytics.
Both are children of the SecurityChecks logger, so you can route them to their own log file with log4net.
VoiceModule
Identifier vm.
Class VoiceModule, factory VoiceModuleFactory.
It is the module version of the voice moderation plugin: it handles the voice and voice command events of Photon Voice (mute and proximity lists, voice frames) and forwards voice to a voice moderation provider.
How the voice moderation plugin works, its settings and the client commands for mute and proximity are described in Voice Moderation with Photon Voice.
Voice events are consumed by the module: the modules after it do not see them.
Voice moderation is opt-in with vm_Provider.
| Key | Meaning |
|---|---|
vm_Provider |
The voice moderation provider: ToxMod, GGWP or VoicePatrol. Without it voice moderation is off. |
vm_url |
URL of the provider's service. |
vm_ForwardPercentage |
ForwardPercentage of the voice moderation page: percentage of rooms that are forwarded. |
vm_ForwardPrivateRooms |
ForwardPrivateRooms: whether private rooms are forwarded too. |
vm_PrivateRoomSuffix |
PrivateRoomSuffix: suffix that makes a room private. |
vm_MutePollUrl, vm_MutePollInterval |
MutePollUrl and MutePollInterval: the service that provides the list of globally muted users, and how often (in milliseconds) it is polled. |
vm_IsActive |
false makes the module inactive. |
The settings of the individual providers (ApiKey, AccountId, UseAnonIDs, SingleTenantPrefix, ...) are described on the voice moderation page.
The provider settings are read with the vm_ prefix (vm_ApiKey); the bare name (ApiKey) is accepted as well, for compatibility with the standalone voice plugin.
The keys in the table above, however, must always have the prefix: for them there is no fallback to the bare names.
VoiceModuleFactory has two properties:
ModuleStartsActive(defaulttrue):falseregisters the module inactive.EnableMutePoller(defaulttrue): the mute poller is a worker shared by all rooms. Disable it if you don't use globally muted users.
For plugins that drive the module themselves the module offers methods that do the work but do not process the call info: ProcessCreateGame, ProcessJoin, ProcessLeave, ProcessBeforeSetProperties, ProcessCloseGame, ProcessRaiseEvent and ProcessVoiceFrame, plus methods to manage users, mutes and proximity.
ProcessRaiseEvent returns RaiseEventResult.Continue, Cancel or NotHandled, and ProcessCloseGame returns false if the module will resume the call by itself later, in which case you must not.
WebHooksModule
Identifier wh.
Class WebHooksModule, factory WebHooksModuleFactory.
Sends room events to a web service, like the WebHooks plugin.
See the WebHooks documentation for the requests and responses of the web service.
The module is persistent if configured so.
| Key | Meaning |
|---|---|
wh_BaseUrl |
Required. Base URL of the web service. Without it the plugin can't be created. |
wh_PathCreate, wh_PathClose, wh_PathBeforeJoin, wh_PathJoin, wh_PathLeave, wh_PathEvent, wh_PathGameProperties, wh_PathLoad |
Path appended to the base URL for each kind of request. |
wh_IsPersistent |
true to save and load the room state with the web service. Only effective if the create and close paths are set. Also set the attribute IsPersistent="true" on the plugin in plugin.config: the server reads that one to decide whether rooms that are not in memory are created when clients rejoin or join them. If the two differ, a warning is logged. |
wh_HasErrorInfo |
Whether error information events are sent to clients. |
wh_CustomHttpHeaders |
A JSON object with additional HTTP headers to send with each request. A malformed value fails the plugin creation. |
The module publishes a PendingResult<WebHookResult> with the tag "webhooks" for the calls it sends (WebHookResult has Success, HttpCode, ResponseText and ErrorMessage).
Calls that need the response before the room can go on (loading the state of a persistent room, BeforeJoin, closing the room) pause the chain until it arrives.
The notifications (join, leave, properties, events) are sent without waiting: the chain goes on and the result completes later.
Modules after it and your terminal methods can read the result with Scope.Get<WebHookResult>("webhooks") and OnComplete.
Gotchas
- Override
Nameof your plugin class so that it matches the name requested by clients. - Do not call
Next()twice, and never let a call info hang: a module that does not callNext()has to process the call info. - Order matters. A module that stops the chain hides the call from the modules after it.
- Publish in
Register, look up inSetup.GetandTryGetthrow inRegister. Usebefore setup, one module per concrete type.SetupInstanceandOnFactoryHostSetare sealed. UseSetupInstanceCoreandOnFactoryHostSetCore.- Inactive modules still run
RegisterandSetup. Only the callbacks are skipped. Scopeis only valid inside a callback. Outside it throws.- Configuration is flat.
All the modules share the same dictionary and only the identifier prefixes keep their keys apart.
The prefixes of the shipped modules are
sp,vmandwh: do not use them for your own modules. - Keys are not entirely uniform.
WebHooks accepts only the
wh_prefixed keys, while the Voice providers also accept the bare names of the standalone plugin. - Deploy
PhotonHivePlugin.Modules.dllnext to your plugin assembly.
Last updated on
Back to top