Skip to content

Config and data

Every plugin gets its own data folder, and a typed JSON config file inside it. You describe the config as a plain class, and the server writes the defaults on first run, reads your edits, and keeps the last good values if an edit is broken.

plugin.DataFolder (the same as plugin.Config.DataFolder) is the plugin’s folder: plugins/<id>/data/ for a plugin running in the plugin worker, which is how plugins run. It is created the first time you use it. Put every file the plugin writes there.

Hot reload watches the plugin’s assembly, not the data folder, so changing a file in it never reloads the plugin.

Describe the settings as a class with defaults, and register it with System.Text.Json source generation (no reflection at run time, AOT-friendly):

HomesConfig.cs
using System.Text.Json.Serialization;
public sealed class HomesConfig
{
public int MaxHomes { get; set; } = 3;
public int CooldownSeconds { get; set; } = 5;
public string Greeting { get; set; } = "Welcome home!";
}
[JsonSerializable(typeof(HomesConfig))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class MyJson : JsonSerializerContext { }

Load it in OnEnable:

HomesConfigPlugin.cs
[PluginInfo("HomesConfig", Version = "1.0.0")]
public sealed class HomesConfigPlugin : Plugin
{
private Config<HomesConfig> _config = null!;
protected override void OnEnable()
{
_config = Config.Load(MyJson.Default.HomesConfig); // plugins/<id>/data/config.json
int max = _config.Value.MaxHomes; // an immutable snapshot
_config.Changed += config => Log.Info($"config reloaded: {config.MaxHomes} homes");
_config.Watch = true; // pick up edits made on disk
}
}

Config.Load returns a Config<T>:

Member Behaviour
Config.Load(typeInfo, fileName) reads config.json (or fileName) in the data folder; if it does not exist, the defaults (new T(), including property initialisers) are written first
Value the current values: an immutable snapshot, replaced as a whole on reload; safe to read from any thread
Reload() reads the file again; returns false, and keeps the old values, when the file is invalid
Save() writes the current value, pretty-printed
Update(value) replaces the value and saves it, then raises Changed
Changed an event with the new value, raised on the tick thread after a reload that produced a value
Watch when true, edits on disk are picked up (debounced) and reported through Changed
Path the file’s full path

Comments and trailing commas are allowed when the file is read, so admins can annotate it. A parse error keeps the previous values (or the defaults on the first load) and logs the file and the line. The broken file is never overwritten after an error.

For data that is not configuration (homes per player, a ban list), plugin.Config offers two helpers for files in the data folder:

  • ReadText(fileName) returns the text, or null when the file does not exist.
  • WriteText(fileName, text) writes atomically (a temp file, then a rename), so a crash never leaves half a file.

Names are relative to the data folder: a rooted path, a .. or a reserved character throws ArgumentException. Serialise your own types with the same source-generated context:

if (Config.ReadText("homes.json") is { } json)
homes = JsonSerializer.Deserialize(json, MyJson.Default.HomesData) ?? new HomesData();
Config.WriteText("homes.json", JsonSerializer.Serialize(homes, MyJson.Default.HomesData));

Do the disk work off the tick thread for anything big (see threading).

Your plugin’s fields do not survive a hot reload; the ECS component values of the entities do, when the component’s key and layout are unchanged (see hot reload). State that must outlive a reload belongs in components or on disk.