Skip to content

Plugin Configuration

Every RocketMod plugin that needs user-configurable settings uses a configuration system built on XML serialization. The framework provides two primary mechanisms: the IRocketPluginConfiguration interface for standard plugins and XMLFileAsset<T> for standalone configuration files that plugins can read and write independently. Understanding how RocketMod serializes, loads, and saves these configuration files is necessary for any plugin developer who wants configurable commands, adjustable gameplay parameters, or persistent state between server restarts.

This article covers the full configuration pipeline from defining a configuration class through runtime modification to multi-file configuration management. It explains the serialization contract, the file layout, and the edge cases that cause production issues.

57 Studios maintains a suite of RocketMod plugins for the Horizon Life RP community. The configuration patterns documented here are drawn from production plugin authoring experience.

Prerequisites

  • A working RocketMod installation on an Unturned dedicated server. See RocketMod and OpenMod Plugin Basics for setup.
  • Visual Studio with C# development workload.
  • Familiarity with C# class definitions and XML serialization concepts.
  • Access to the server's Rocket/Plugins/ directory via FTP or local file system.

What you'll learn

  • How to define a configuration class using IRocketPluginConfiguration.
  • How RocketMod's XML serialization works under the hood.
  • How to use XMLFileAsset<T> for standalone config files.
  • How to save and reload configuration at runtime.
  • How WebXMLFileAsset<T> works for remote configuration loading.
  • Common configuration pitfalls and how to avoid them.

How RocketMod configuration works

RocketMod binds each plugin to a configuration type through the generic parameter of RocketPlugin<TConfiguration>. When the plugin loads, RocketMod looks for an XML file at Rocket/Plugins/<PluginName>/<PluginName>.configuration.xml. If the file exists, it deserializes the XML into an instance of TConfiguration. If the file does not exist, RocketMod calls LoadDefaults() on a new instance and saves it to disk.

The configuration instance is accessible through Configuration.Instance inside the plugin class.

The configuration class

A configuration class implements IRocketPluginConfiguration, which requires a single method:

csharp
public interface IRocketPluginConfiguration
{
    void LoadDefaults();
}

RocketMod uses .NET's XmlSerializer under the hood. Public fields and properties with public setters are serialized. Private fields and read-only properties are ignored.

csharp
using Rocket.API;

namespace MyConfigurablePlugin
{
    public class MyPluginConfiguration : IRocketPluginConfiguration
    {
        public string WelcomeMessage;
        public int MaxPlayersPerTeam;
        public bool EnablePvP;
        public float RespawnDelaySeconds;

        public void LoadDefaults()
        {
            WelcomeMessage = "Welcome to the server!";
            MaxPlayersPerTeam = 4;
            EnablePvP = true;
            RespawnDelaySeconds = 15f;
        }
    }
}

The generated XML

When RocketMod saves this configuration, it produces an XML file:

xml
<?xml version="1.0" encoding="utf-8"?>
<MyPluginConfiguration>
  <WelcomeMessage>Welcome to the server!</WelcomeMessage>
  <MaxPlayersPerTeam>4</MaxPlayersPerTeam>
  <EnablePvP>true</EnablePvP>
  <RespawnDelaySeconds>15</RespawnDelaySeconds>
</MyPluginConfiguration>

XML declaration is required

RocketMod's XML deserializer relies on the <?xml?> declaration to detect the file encoding. If your configuration file is missing the declaration, or if it uses an encoding attribute other than encoding="utf-8", the deserializer may read the file with the wrong character encoding. This causes special characters — accented letters, emoji, non-ASCII symbols — to appear garbled or throw a deserialization exception.

Always ensure your configuration XML files include this exact declaration on the first line:

xml
<?xml version="1.0" encoding="utf-8"?>

Without it, the server's regional codepage can corrupt non-ASCII values. This applies to all RocketMod XML files including permissions, translations, and plugin configuration files.

XMLFileAsset for standalone config files

Not every configuration file belongs to a single plugin. Some plugins manage multiple configuration files — one for gameplay settings, one for map-specific overrides, one for integration with external services. RocketMod provides XMLFileAsset<T> for loading and saving arbitrary XML configuration files from any path on the server.

csharp
using Rocket.API;
using Rocket.Unturned;

public class ExternalConfig
{
    public string ApiEndpoint;
    public int SyncIntervalSeconds;
    public bool LogRequests;

    public ExternalConfig()
    {
        ApiEndpoint = "http://localhost:5000/api";
        SyncIntervalSeconds = 60;
        LogRequests = false;
    }
}

Loading a config file

csharp
var configFile = new XMLFileAsset<ExternalConfig>(
    path: "Rocket/Plugins/MyPlugin/external-settings.xml",
    load: true
);

// Access the deserialized data
string endpoint = configFile.Instance.ApiEndpoint;

The constructor accepts three parameters:

ParameterTypePurpose
pathstringAbsolute or relative path to the XML file
loadboolIf true, load from disk immediately. If false, create default instance without loading.
rootstringOptional. The XML root element name. Defaults to the type name.

Saving a config file

Call Save() to write changes back to disk:

csharp
configFile.Instance.SyncIntervalSeconds = 120;
configFile.Instance.LogRequests = true;
configFile.Save();

::: caution Save() writes UTF-8 with BOM When you call IAsset<T>.Save(), RocketMod writes the file using UTF-8 encoding with a Byte Order Mark (BOM) — the three-byte sequence EF BB BF at the start of the file. This is important to know because:

  1. If you edit the file in Notepad++ and save it as UTF-8 without BOM, Save() will re-add the BOM on the next write.
  2. If you process the file with tools that do not expect a BOM (such as certain web servers or script parsers that read configuration files), the BOM can cause parsing errors.
  3. The BOM is invisible in most editors, so you may not realize it is there until an external tool fails.

If you need to produce files without a BOM, you must post-process the output or use a custom serialization method outside of XMLFileAsset<T>. The built-in Save() always writes the BOM. :::

When to use XMLFileAsset over IRocketPluginConfiguration

ScenarioRecommended approach
Single configuration per pluginIRocketPluginConfiguration on the main plugin class
Multiple configuration files per pluginXMLFileAsset<T> for each additional file
Shared configuration across pluginsXMLFileAsset<T> with a shared path
Configuration loaded from external sourceWebXMLFileAsset<T>
Runtime-generated configuration dataXMLFileAsset<T> with load: false

WebXMLFileAsset for remote configuration

Plugins that need configuration from a remote source — a shared settings server, a Discord bot's configuration endpoint, a CDN-hosted JSON-to-XML bridge — use WebXMLFileAsset<T>. This variant downloads the XML from a URL, deserializes it, and caches it to a local file as a fallback.

csharp
using Rocket.API;

public class RemoteConfig
{
    public string ServerDisplayName;
    public int MaxPlayers;

    public RemoteConfig()
    {
        ServerDisplayName = "57 Studios RP";
        MaxPlayers = 24;
    }
}

// In Load():
var remoteConfig = new WebXMLFileAsset<RemoteConfig>(
    url: "https://config.example.com/myplugin/settings.xml",
    localPath: "Rocket/Plugins/MyPlugin/remote-settings-cache.xml",
    load: true
);

Behavior

On load: true, WebXMLFileAsset tries to download the XML from the URL. If the download succeeds, it deserializes and caches the result to localPath. If the download fails (server offline, network timeout, DNS error), it falls back to reading the cached local file if it exists, or creates a default instance if neither remote nor local file is available.

Forcing a refresh

WebXMLFileAsset does not automatically refresh. To re-download the remote configuration, create a new instance:

csharp
remoteConfig = new WebXMLFileAsset<RemoteConfig>(
    url: "https://config.example.com/myplugin/settings.xml",
    localPath: "Rocket/Plugins/MyPlugin/remote-settings-cache.xml",
    load: true
);

Consider adding a /myplugin:reloadconfig command that triggers this refresh so server admins can update settings without a full plugin reload.

Runtime configuration patterns

Reading configuration in commands

csharp
public class GreetCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        var config = MyPlugin.Instance.Configuration.Instance;
        UnturnedChat.Say(caller, config.WelcomeMessage, Color.green);
    }
}

Modifying configuration at runtime

Some plugins allow in-game operators to change settings without editing XML files directly:

csharp
[RocketCommand("setwelcome", "Set the welcome message.", "/setwelcome <message>", AllowedCaller = AllowedCaller.Console)]
public class SetWelcomeCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        if (command.Length < 1)
        {
            UnturnedChat.Say(caller, "Usage: /setwelcome <message>");
            return;
        }

        var config = MyPlugin.Instance.Configuration.Instance;
        config.WelcomeMessage = string.Join(" ", command);
        Configuration.Instance.Save();

        UnturnedChat.Say(caller, $"Welcome message updated to: {config.WelcomeMessage}", Color.yellow);
    }
}

Save mutation before calling Save()

A common mistake is mutating the config instance after calling Save(). Because XmlSerializer writes the current state of the object, any mutation after Save() is lost. Always call Save() as the last step after all mutations are complete.

Reloading configuration from disk

RocketMod does not have a built-in /rocket reloadconfig command. To reload configuration from disk without reloading the entire plugin, read the file manually:

csharp
private void ReloadConfig()
{
    var newConfig = new XMLFileAsset<MyPluginConfiguration>(
        path: "Rocket/Plugins/MyPlugin/MyPlugin.configuration.xml",
        load: true
    );
    // Swap the old config for the new one
    Configuration.Instance = newConfig.Instance;
}

This pattern avoids the full plugin reload cycle while still picking up file changes.

Complex configuration types

Nested configuration classes

Configuration classes can contain nested objects and collections:

csharp
public class GameSettings
{
    public string ModeName;
    public bool Hardcore;
    public WeatherConfig Weather;
    public List<KitEntry> StarterKits;
}

public class WeatherConfig
{
    public bool Rain;
    public bool Fog;
    public float RainChance;
}

public class KitEntry
{
    public string Name;
    public List<ushort> ItemIds;
}

Serialized XML:

xml
<?xml version="1.0" encoding="utf-8"?>
<GameSettings>
  <ModeName>Survival</ModeName>
  <Hardcore>false</Hardcore>
  <Weather>
    <Rain>true</Rain>
    <Fog>false</Fog>
    <RainChance>0.3</RainChance>
  </Weather>
  <StarterKits>
    <KitEntry>
      <Name>Basic</Name>
      <ItemIds>
        <ushort>1</ushort>
        <ushort>2</ushort>
        <ushort>3</ushort>
      </ItemIds>
    </KitEntry>
  </StarterKits>
</GameSettings>

Arrays vs Lists

XmlSerializer handles both arrays and List<T>:

Collection typeXML outputDeserialization
string[]<ArrayOfString>Requires exact type match
List<string><ListOfString>More flexible
string[] Items with [XmlArray]Custom element nameRequires attribute

For plugin configuration, List<T> is preferred because it handles serialization more predictably across .NET Framework versions.

Dictionaries

XmlSerializer does not natively support Dictionary<TKey, TValue>. RocketMod plugins that need key-value configuration should use a List of custom key-value pair objects:

csharp
public class ConfigEntry
{
    public string Key;
    public string Value;
}

public class AdvancedConfig
{
    public List<ConfigEntry> Overrides;
}

Configuration file management

Multi-environment setups

Some server operators run multiple instances of the same plugin with different settings — development, staging, and production. A common pattern is to use environment-specific configuration files:

Rocket/Plugins/MyPlugin/
├── MyPlugin.configuration.xml          ← production (default)
├── MyPlugin.configuration.dev.xml      ← development overrides
└── MyPlugin.configuration.staging.xml  ← staging overrides

The plugin loads the base config, then checks for an environment override:

csharp
protected override void Load()
{
    string environment = Environment.GetEnvironmentVariable("UNTURNED_ENV") ?? "production";
    string overridePath = $"Rocket/Plugins/MyPlugin/MyPlugin.configuration.{environment}.xml";

    if (File.Exists(overridePath))
    {
        var overrideAsset = new XMLFileAsset<MyPluginConfiguration>(overridePath, true);
        // Apply overrides to base config
        Configuration.Instance = overrideAsset.Instance;
    }

    Rocket.Core.Logging.Logger.Log($"MyPlugin loaded in {environment} mode.");
}

Hot-reloading from file watcher

Advanced setups use FileSystemWatcher to detect config file changes and reload automatically:

csharp
private FileSystemWatcher _configWatcher;

protected override void Load()
{
    string configDir = Directory.GetParent(ConfigurationFilePath).FullName;
    string configFile = Path.GetFileName(ConfigurationFilePath);

    _configWatcher = new FileSystemWatcher(configDir, configFile)
    {
        NotifyFilter = NotifyFilters.LastWrite,
        EnableRaisingEvents = true
    };
    _configWatcher.Changed += OnConfigFileChanged;
}

private void OnConfigFileChanged(object sender, FileSystemEventArgs e)
{
    // Debounce — WriteEnd triggers Changed multiple times
    System.Threading.Thread.Sleep(500);
    ReloadConfig();
    Rocket.Core.Logging.Logger.Log("Configuration auto-reloaded from file change.");
}

FileSystemWatcher on Windows servers

FileSystemWatcher works reliably on Windows but may not fire on all hosting providers that use network-mounted drives. Test this pattern on your target hosting platform before deploying to production. Always include a debounce delay (500ms) because the Changed event fires multiple times per save.

Common configuration pitfalls

Field name changes break existing configs

Changing a public field name in the configuration class breaks deserialization of existing files. RocketMod's XmlSerializer matches element names to field names. A renamed field becomes a missing element, which causes the field to keep its default value from LoadDefaults().

Example: Renaming WelcomeMessage to GreetingMessage means existing config files still have <WelcomeMessage> but the class now expects <GreetingMessage>. The old value is silently lost.

Solution: When renaming fields, inform server operators to regenerate config files by deleting the old ones. Alternatively, use [XmlElement("oldName")] to maintain backward compatibility:

csharp
using System.Xml.Serialization;

public class MyPluginConfiguration : IRocketPluginConfiguration
{
    [XmlElement("WelcomeMessage")]
    public string GreetingMessage;

    public void LoadDefaults()
    {
        GreetingMessage = "Welcome to the server!";
    }
}

Case sensitivity

XML element names are case-sensitive. XmlSerializer matches the element name to the field name exactly. A config file with <Welcomemessage> (lowercase 'm') will not deserialize into a field named WelcomeMessage. The field keeps its default value.

Boolean serialization

XmlSerializer writes bool values as lowercase true and false. XML parsers are case-sensitive for these values:

xml
<EnablePvP>true</EnablePvP>

If a server operator manually edits the file and writes True or TRUE, deserialization throws an exception. Always document in your plugin's readme that boolean values must be lowercase.

Float culture invariance

In .NET Framework 4.7.2, XmlSerializer uses the invariant culture for float and double serialization. This means decimal values use a period (.) as the decimal separator regardless of the server's regional settings:

xml
<RespawnDelaySeconds>15.5</RespawnDelaySeconds>

If an operator on a German-language server edits the file and writes 15,5 with a comma, deserialization will fail with a FormatException. Document this in your plugin's configuration instructions.

Default value trap

Fields that are not present in the XML file at all (missing elements) silently take their default value. If LoadDefaults() sets RespawnDelaySeconds = 15f and the operator deletes the <RespawnDelaySeconds> element from the XML, the value reverts to 15 without warning. There is no validation or error message.

This is by design — RocketMod uses LoadDefaults() to fill in missing elements so that adding a new config field does not break existing installations. But it also means that accidentally deleting an element causes a silent fallback, not an error.

Configuration best practices

Validate on load

Consider adding a validation step in Load() that checks configuration values and warns if they are out of expected range:

csharp
protected override void Load()
{
    var config = Configuration.Instance;

    if (config.MaxPlayersPerTeam < 1 || config.MaxPlayersPerTeam > 32)
    {
        Rocket.Core.Logging.Logger.LogWarning(
            $"MaxPlayersPerTeam ({config.MaxPlayersPerTeam}) is out of range. Expected 1-32."
        );
    }

    if (string.IsNullOrWhiteSpace(config.WelcomeMessage))
    {
        Rocket.Core.Logging.Logger.LogWarning("WelcomeMessage is empty. Using default.");
        config.WelcomeMessage = "Welcome!";
        Configuration.Save();
    }
}

Use sensible defaults

Every configuration field should have a reasonable default in LoadDefaults(). If a field has no reasonable default, document the expected value range in your plugin's readme and add a validation warning as shown above.

Document each config field

RocketMod does not support XML comments or descriptions in configuration files. The best practice is to maintain a documentation file alongside the plugin that lists each field, its type, its default value, and its purpose.

Version your config

When your plugin's configuration schema changes (fields added, removed, or renamed), increment an internal version number:

csharp
public class MyPluginConfiguration : IRocketPluginConfiguration
{
    public int ConfigVersion;
    public string WelcomeMessage;

    public void LoadDefaults()
    {
        ConfigVersion = 2;
        WelcomeMessage = "Welcome!";
    }
}

In Load(), check the version and apply migrations:

csharp
protected override void Load()
{
    var config = Configuration.Instance;

    if (config.ConfigVersion < 2)
    {
        // Migrate from version 1 to 2
        // ...
        config.ConfigVersion = 2;
        Configuration.Save();
    }
}

Complete working example

The following example demonstrates a complete plugin with multi-file configuration, runtime reload, and validation.

PluginConfiguration.cs:

csharp
using Rocket.API;
using System.Collections.Generic;

namespace AdvancedConfigPlugin
{
    public class PluginConfiguration : IRocketPluginConfiguration
    {
        public int ConfigVersion;
        public string ServerTag;
        public bool EnableAutoRestart;
        public float RestartIntervalHours;
        public List<string> AllowedCommands;
        public GameplaySettings Gameplay;
        public List<RewardEntry> DailyRewards;

        public void LoadDefaults()
        {
            ConfigVersion = 1;
            ServerTag = "[57 Studios]";
            EnableAutoRestart = false;
            RestartIntervalHours = 6f;
            AllowedCommands = new List<string> { "help", "rules", "discord" };
            Gameplay = new GameplaySettings
            {
                PvPMode = "full",
                SafeZoneRadius = 50f,
                EnableDrops = true
            };
            DailyRewards = new List<RewardEntry>();
        }
    }

    public class GameplaySettings
    {
        public string PvPMode;
        public float SafeZoneRadius;
        public bool EnableDrops;
    }

    public class RewardEntry
    {
        public string Name;
        public ushort ItemId;
        public byte Amount;
    }
}

AdvancedConfigPlugin.cs:

csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System;
using System.IO;
using UnityEngine;

namespace AdvancedConfigPlugin
{
    public class AdvancedConfigPlugin : RocketPlugin<PluginConfiguration>
    {
        public static AdvancedConfigPlugin Instance { get; private set; }
        private XMLFileAsset<PluginConfiguration> _overrideConfig;

        protected override void Load()
        {
            Instance = this;

            // Check for environment-specific overrides
            string serverName = Environment.GetEnvironmentVariable("UNTURNED_SERVER_NAME") ?? "production";
            string overridePath = $"Rocket/Plugins/AdvancedConfigPlugin/config.{serverName}.xml";

            if (File.Exists(overridePath))
            {
                _overrideConfig = new XMLFileAsset<PluginConfiguration>(overridePath, true);

                // Apply overrides on top of base config
                if (_overrideConfig.Instance.EnableAutoRestart)
                    Configuration.Instance.EnableAutoRestart = true;

                if (_overrideConfig.Instance.RestartIntervalHours > 0)
                    Configuration.Instance.RestartIntervalHours = _overrideConfig.Instance.RestartIntervalHours;

                Rocket.Core.Logging.Logger.Log($"Applied overrides from {overridePath}");
            }

            ValidateConfiguration();

            UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
            Rocket.Core.Logging.Logger.Log("AdvancedConfigPlugin loaded.");
        }

        protected override void Unload()
        {
            UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
            Instance = null;
            Rocket.Core.Logging.Logger.Log("AdvancedConfigPlugin unloaded.");
        }

        private void OnPlayerConnected(UnturnedPlayer player)
        {
            var config = Configuration.Instance;

            if (!string.IsNullOrEmpty(config.ServerTag))
            {
                player.SendChat($"{config.ServerTag} Welcome to the server!", Color.cyan);
            }
        }

        private void ValidateConfiguration()
        {
            var config = Configuration.Instance;

            if (config.RestartIntervalHours <= 0 && config.EnableAutoRestart)
            {
                Rocket.Core.Logging.Logger.LogWarning(
                    "EnableAutoRestart is true but RestartIntervalHours is not positive. " +
                    "Auto-restart will not fire."
                );
            }

            if (string.IsNullOrEmpty(config.ServerTag))
            {
                config.ServerTag = "[57 Studios]";
                Configuration.Save();
            }
        }

        public void ReloadConfiguration()
        {
            string configPath = $"Rocket/Plugins/AdvancedConfigPlugin/AdvancedConfigPlugin.configuration.xml";
            var freshConfig = new XMLFileAsset<PluginConfiguration>(configPath, true);
            Configuration.Instance = freshConfig.Instance;
            ValidateConfiguration();
            Rocket.Core.Logging.Logger.Log("Configuration reloaded from disk.");
        }
    }
}

ReloadCommand.cs:

csharp
using Rocket.API;
using System.Collections.Generic;

namespace AdvancedConfigPlugin.Commands
{
    public class ReloadConfigCommand : IRocketCommand
    {
        public string Name => "acreload";
        public string Help => "Reload the plugin configuration from disk.";
        public string Syntax => "/acreload";
        public List<string> Aliases => new List<string>();
        public List<string> Permissions => new List<string> { "advancedconfigplugin.reload" };
        public AllowedCaller AllowedCaller => AllowedCaller.Console;

        public void Execute(IRocketPlayer caller, string[] command)
        {
            AdvancedConfigPlugin.Instance.ReloadConfiguration();
        }
    }
}

The XMLFileAsset<T> approach shown here allows the plugin to maintain a clean base configuration with optional per-environment overrides while keeping the core plugin configuration managed by RocketMod's standard lifecycle.

Frequently asked questions

Why does my configuration keep resetting to defaults?

The configuration file is missing or RocketMod cannot deserialize it. Check that the XML file is valid, that element names match field names exactly (case-sensitive), and that there are no encoding issues. If the file fails to deserialize, RocketMod silently falls back to defaults and overwrites the file.

Does RocketMod support JSON configuration?

No. RocketMod uses XmlSerializer exclusively. There is no built-in JSON configuration support. If your plugin needs JSON, you must add a third-party JSON library (such as Newtonsoft.Json) and manage the file serialization yourself.

How do I share configuration between two plugins?

Both plugins can reference the same XMLFileAsset<T> path. Create a shared library project that defines the configuration class and have both plugins reference it. Each plugin creates its own XMLFileAsset<T> pointing at the same file path.

What happens if the XML file is corrupted?

XmlSerializer throws an InvalidOperationException. RocketMod catches this during plugin initialization and falls back to defaults. The corrupted file is overwritten with default values. If you need to preserve the corrupted file for debugging, take a backup before the server starts.

Can I use XMLFileAsset in OpenMod?

No. XMLFileAsset<T> is a RocketMod class. OpenMod has its own configuration system based on IConfiguration with YAML files. See the OpenMod documentation for its configuration API.

Cross-references

Document history

VersionDateAuthorNotes
1.02026-07-2757 StudiosInitial publication. Coverage of IRocketPluginConfiguration, XMLFileAsset, WebXMLFileAsset, runtime patterns, best practices.