Server Plugin
The Casual MMO Server Plugin is a worked example of a custom Fusion Shared Authority server plugin. It validates and controls key game logic (damage, movement, progression and object spawning) that would otherwise be left to the clients.
The plugin is built on the Fusion Plugin SDK. This page focuses on how the MMO sample uses the SDK - it is not a full API reference. For the generic workflow, attributes and APIs, see the SDK's Setup and Features pages.
Creating a Photon Enterprise Cloud involves additional costs. Please contact us for more information.
What the MMO plugin demonstrates:
- Spawning and protecting plugin-owned objects (the tornado).
- Processing hits server-side: tracking damage history and clamping per-source damage.
- Plugin-authoritative
[Networked]properties (player level and XP). - Intercepting and re-firing RPCs from the plugin.
- Tolerance-based speed-hack detection that flags rather than corrects.
- Sharing C# files and baked per-object data between Unity and the plugin.
Running a local Photon Server is supported only on Windows.
Getting the Plugin
Installation
(Windows only) Unblock the downloaded
.zipviaright-click > Properties > Unblockbefore unzipping it.Unzip the archive into a
Fusion.Pluginfolder in the root of the Unity project.Plain Old Text
MMO ├─Assets ├─Library ├─.. └─Fusion.PluginPlace your Photon Server
.licensefile (from the Photon dashboard) intoFusion.Plugin\Fusion.Plugin.Custom\Config. Without it, the server logs a license error and refuses to start games.
The MMO sample already ships with the FusionPluginProjectSettings asset configured (Assets\Settings\FusionPluginSettings.asset), with its Plugin SDK Path set to Fusion.Plugin, so no extra wiring is needed after unzipping into that folder.
Running the Server Plugin
- Open the Casual MMO Unity project.
- Select
Assets\Photon\Fusion\Resources\PhotonAppSettings.assetand set the server address to127.0.0.1. - Select the plugin settings asset
Assets\Settings\FusionPluginSettings.assetand pressExport. - On the same asset, press
Build and Run. This builds the plugin (Release) and starts the localPhotonServerwith the plugin loaded in a new terminal window. - Wait until the server is ready, usually within several seconds.
- Enter play mode in the Unity Editor and connect to the local server.
- The game server log is at
Fusion.Plugin\Photon.Server\log\GSGame.log. Entries from the custom plugin are tagged[Plugin.Custom].
To work on and debug the plugin code directly, press Open Plugin Project (or open Fusion.Plugin\Fusion.Plugin.Custom.sln) and run/debug from your IDE - see SDK Setup → IDE Setup for the full walkthrough.
The server can also be launched manually:
Fusion.Plugin\Photon.Server\bin\PhotonServer.exe /run LoadBalancing
Press Export again whenever you change code or objects that the plugin needs to see - the plugin works against a snapshot taken at export time.
How the MMO Plugin Is Organized
The plugin is a C# solution (Fusion.Plugin.Custom.sln) split across two projects:
Fusion.Plugin.Custom- the plugin lifecycle and server-level logic.Core/holds the SDK'sCustomPlugin,CustomPluginFactoryandCustomServerpartials; the MMO's server-callback overrides (spawning the tornado, gating object destruction) live inCustomServer.User.cs, alongsideCore/.Config/holdsplugin.config,log4net.configand your.license.Fusion.Plugin.Types- the export target and where most MMO plugin logic lives.Generated/contains the exporter's output (UserTypes.<Assembly>.csand theDB.Prefab.*.json/DB.Scene.*.json/DB.ScriptableObject.*.jsondatabases). The MMO'spartialextensions of exported components live underMMO/Components/(Health.cs,Player.cs,Chest.cs,Tornado.cs,SpeedValidation.cs, …), alongside the generated partials they extend.
The compiled plugin is written to Fusion.Plugin\Photon.Server\Plugins\Fusion.Plugin.Custom\bin and zipped to Fusion.Plugin\Photon.Server\Plugins\Fusion.Plugin.Custom.zip - that zip is what runs locally and what you upload to the cloud, see Enterprise Plugin Setup.
For the full SDK directory layout, the Lib/ runtime binaries and the local Photon.Server, see SDK Setup → SDK structure.
MMO Plugin Walkthrough
The following sections map MMO features to the plugin techniques behind them. Each technique is documented in full in the Plugin SDK Features reference; here we show how the sample applies it.
Authoritative Damage (Health)
Damage is forwarded to the plugin, which validates and clamps it before any client applies it.
The same code runs with or without a plugin via Runner.HasCustomPlugin.
To export a private field for the plugin, pair [PluginCodeExportSettings(PluginExportOptions.Export)] with [SerializeField] (see Attribute Annotations).
Unity project (Assets/Scripts/Components/Health.cs):
C#
public class Health : NetworkBehaviour, IInterestEnter
{
// Exported so the plugin can compute MaxHealth. Private fields need [SerializeField] to be exported.
[SerializeField, PluginCodeExportSettings(PluginExportOptions.Export)]
private int _baseHealth = 100;
[Networked]
private int _currentHealth { get; set; }
// Damage is forwarded to the plugin first.
[Rpc(RpcSources.All, RpcTargets.StateAuthority, InvokeLocalMode = RpcInvokeLocalMode.ForwardToPlugin)]
private void RPC_TakeHit(int damage, NetworkId instigator, RpcInfo info = default)
{
if (Runner.HasCustomPlugin == true)
{
// The plugin already validated and applied the damage - just play the hit reaction.
HitReceived?.Invoke(instigator);
return;
}
// No plugin: state authority applies the damage itself.
_currentHealth -= damage;
// ...
}
}
Server plugin (Fusion.Plugin.Types/MMO/Components/Health.cs):
C#
public partial class Health
{
private const int MAX_DAMAGE = 50; // clamp cumulative damage from a single source per second
partial void RPC_TakeHit(int damage, NetworkId instigator, ref RpcInfo info)
{
// Cancel the incoming RPC so it is not forwarded to clients as-is.
info.Cancel();
// ApplyDamage clamps cumulative per-source damage and tracks a damage history.
if (ApplyDamage(damage, instigator, info.Source) == false)
return;
// Re-fire the RPC so clients get a hit reaction only for damage we accepted.
RPC_TakeHit(damage, instigator);
}
}
This pattern - ForwardToPlugin + info.Cancel() + re-fire - is the SDK's standard way to take over an RPC; see Intercepting RPCs.
Plugin-Authoritative Progression (Player)
Level and XP are owned exclusively by the plugin, so clients cannot cheat them.
They are marked [Networked(PluginAuthority = true, AllowPrediction = false)] - readable on clients, writable only by the plugin (see Networked Properties - Plugin Authority).
Unity project (Assets/Scripts/Components/Player.cs):
C#
public sealed partial class Player : Agent, IStateMachineOwner, IInterestEnter
{
[Networked(PluginAuthority = true, AllowPrediction = false)]
private int _level { get; set; }
[Networked(PluginAuthority = true, AllowPrediction = false)]
private int _xp { get; set; }
// Even on the state authority, the write must go through the plugin.
[Rpc(RpcSources.StateAuthority, RpcTargets.StateAuthority, InvokeLocalMode = RpcInvokeLocalMode.ForwardToPlugin)]
private void RPC_LevelUp()
{
++_level; // applied here only when no plugin is present
}
}
Server plugin (Fusion.Plugin.Types/MMO/Components/Player.cs):
C#
partial class Player
{
partial void RPC_LevelUp(ref RpcInfo info)
{
info.Cancel();
++_level; // only the plugin can write a PluginAuthority property
}
}
Plugin-Driven Objects (Tornado)
The tornado is spawned, moved and made to deal damage entirely by the plugin; clients only render it.
The plugin spawns it by prefab name with Runner.Spawn and refuses any client attempt to despawn it via the CanPlayerDestroyObject authority hook (see Plugin Side Spawning and Server-side hooks → Authority & validation).
Server plugin (Fusion.Plugin.Custom/CustomServer.User.cs):
C#
partial class CustomServer
{
public override void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
base.OnPlayerJoined(runner, player);
// "Tornado" is the name of a Unity prefab with a NetworkObject.
NetworkObject tornadoObject = runner.Spawn("Tornado");
if (tornadoObject != null)
{
tornadoObject.GetBehaviour<Tornado>().SetTarget(player);
}
}
public override bool CanPlayerDestroyObject(NetworkRunner runner, PlayerRef player, NetworkObjectMeta meta)
{
if (runner.TryFindObject(meta.Id, out NetworkObject obj) && obj.TryGetBehaviour(out Tornado tornado))
{
Log.Warn($"{player} is trying to despawn {tornado.GetType().Name} ({obj.Id}). This is not allowed!");
return false;
}
return base.CanPlayerDestroyObject(runner, player, meta);
}
}
The tornado's movement and damage logic lives in Fusion.Plugin.Types/MMO/Components/Tornado.cs, driven from FixedUpdateNetwork.
Baked Data + RPC Interception (Chest)
When a chest is opened, the plugin spawns the loot authoritatively.
To place items correctly, the chest bakes its transform into the export via IPluginBakedDataProvider<T>; the plugin reads it back as BakedPluginData (see Baked per-object data).
Unity project (Assets/Scripts/Interactables/Chest.cs):
C#
public class Chest : NetworkBehaviour, IInteractable, IPluginBakedDataProvider<Chest.PluginData>
{
[SerializeField, PluginCodeExportSettings(PluginExportOptions.Export)]
private RollTable[] _rollTables;
[SerializeField, PluginCodeExportSettings(PluginExportOptions.Export)]
private float _closeAfterTime = 40f;
// Captured at export time and surfaced plugin-side as BakedPluginData.
PluginData IPluginBakedDataProvider<PluginData>.Bake(in PluginBakedDataContext context)
{
PluginData pluginData = new PluginData();
transform.GetPositionAndRotation(out pluginData.Position, out pluginData.Rotation);
return pluginData;
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority, InvokeLocalMode = RpcInvokeLocalMode.ForwardToPlugin)]
private void RPC_Open(RpcInfo info = default)
{
// Client-side open: spawn loot (runs only without a plugin).
}
// The data type must be visible to the plugin, so mirror it with the attribute.
[PluginCodeExportSettings(PluginExportOptions.Export)]
public sealed class PluginData
{
[PluginCodeExportSettings(PluginExportOptions.Export)]
public Vector3 Position;
[PluginCodeExportSettings(PluginExportOptions.Export)]
public Quaternion Rotation;
}
}
Server plugin (Fusion.Plugin.Types/MMO/Components/Chest.cs):
C#
partial class Chest
{
partial void RPC_Open(ref RpcInfo info)
{
// Cancel the RPC - the plugin spawns the loot authoritatively.
info.Cancel();
if (IsOpen)
return;
_openCooldown = TickTimer.CreateFromSeconds(Runner, _closeAfterTime);
// Use the baked transform to place loot in front of the chest.
Vector3 position = BakedPluginData.Position + BakedPluginData.Rotation * Vector3.forward * 1.5f;
// ... roll the tables and Runner.Spawn(rollItem, position, BakedPluginData.Rotation);
}
}
Speed-Hack Detection (SpeedValidation)
SpeedValidation (plugin-side, in Fusion.Plugin.Types/MMO/Components/SpeedValidation.cs) compares per-tick movement against an exported MaxSpeed.
It intentionally does not clamp speed - sustained violations are logged (and could be reported to flag a cheater) without tipping the player off.
Server plugin (Fusion.Plugin.Types/MMO/Components/SpeedValidation.cs):
C#
partial class SpeedValidation
{
public override void FixedUpdateNetwork()
{
// ... compute speed from position delta over elapsed ticks
if (speed > MaxSpeed)
++_fasterTicks;
else
_fasterTicks = default;
if (_fasterTicks > 8)
Log.Warn($"Speed of {Object.Id} ({speed:F1}m/s) is over threshold ({MaxSpeed:F1}m/s) for {_fasterTicks} ticks in a row!");
}
}
Sharing Code Between Unity and the Plugin
Some logic (e.g. loot roll tables, player progress data) must run identically on both sides.
The MMO keeps these in Assets/Scripts/PluginShared (RollTable.cs, ProgressData.cs, Player.Shared.cs), which is registered in the settings asset's IncludePaths so the whole folder compiles into the plugin.
Individual files can instead be tagged with the FusionPluginInclude asset label.
See Code Sharing and The FusionPluginInclude Asset Label.
Press Export after changing IncludePaths (or any exported code/objects) so the changes land in the plugin project.
General Recommendations
- The plugin runs in a multi-threaded environment - avoid mutable
staticstate, see Plugin Threading. - Don't chase 100% cheat-proof gameplay. The MMO clamps obvious damage cheats but only detects speed hacks. Flag cheaters on repeated patterns and segregate them into their own matchmaking pool rather than punishing single events.
- You can run different plugin logic per session type (e.g. casual vs. ranked) by branching on dashboard config or session properties.
See Common patterns for more.
Further Reading
- Plugin SDK - Setup - installation, first run, IDE debugging, SDK structure.
- Plugin SDK - Features - full reference for exports, attributes, RPC interception, plugin authority, server-side hooks and spawning.
- Plugin SDK - Enterprise Plugin Setup - uploading the plugin to the Photon Enterprise Cloud.
- Photon-Server V5 Step by Step Guide - the underlying Photon Server plugin model.
Last updated on
Back to top- Getting the Plugin
- Installation
- Running the Server Plugin
- How the MMO Plugin Is Organized
- MMO Plugin Walkthrough
- Authoritative Damage (Health)
- Plugin-Authoritative Progression (Player)
- Plugin-Driven Objects (Tornado)
- Baked Data + RPC Interception (Chest)
- Speed-Hack Detection (SpeedValidation)
- Sharing Code Between Unity and the Plugin
- General Recommendations
- Further Reading