Skip to content

Project layout and plugin.json

Homes/
├── Homes.csproj class library, net10.0, references the Ramjet.Api package (compile-time only, nothing copied)
├── PluginMain.cs the plugin class: derives from Plugin, [PluginInfo] attribute, OnEnable / OnDisable
├── plugin.json manifest: id, version, API version, dependencies, permissions
├── AGENTS.md rules and API cheat sheet for AI agents working in this project
├── CLAUDE.md one line: @AGENTS.md (Claude Code reads CLAUDE.md)
├── .claude/skills/ramjet-plugin/ the Claude Code skill for plugin development
└── .gitignore
PluginMain.cs
using Mc.Scripting;
namespace Homes;
[PluginInfo("Homes", Version = "1.0.0", Author = "Ahmed", Description = "Set and teleport to homes")]
public sealed class PluginMain : Plugin
{
protected override void OnEnable()
{
Events.On((ref PlayerJoinEvent e) => Log.Info($"{e.Username.ToString()} joined"));
}
protected override void OnDisable() { }
}

The server creates one instance of the plugin class. It is the class named by main in plugin.json, or, without that, the first public, non-abstract type that implements IPlugin and has a parameterless constructor (Plugin does). Keep exactly one per project. [PluginInfo] is optional metadata and is used only when there is no manifest; without either, the plugin is named after its assembly. The name prefixes every log line ([Homes] ...).

Three things matter and the template sets them for you:

  • EnableDynamicLoading makes the build usable in an unloadable, collectible load context (this is what makes hot reload possible);
  • the Ramjet.Api package is a reference assembly: it adds no code to your output, and the server supplies the real API. Do not copy an API dll next to your plugin;
  • the dependencies you add yourself (NuGet packages) are copied next to your dll and loaded privately into your plugin’s context.

The package also imports the global usings of the API namespaces (Mc.Scripting, Mc.Scripting.Commands, Mc.Scripting.Configuration, Mc.Scripting.Services, Mc.Scripting.Events, Mc.Scripting.Permissions, Mc.Scripting.Text, Mc.Scripting.Worlds, Mc.Scripting.Nbt, Mc.Scripting.Inventories, Mc.Scripting.Content). Set RamjetGlobalUsings to false in the project if you want to write them yourself.

Pass --plugins <dir> to the server. It looks for:

Layout Used for
plugins/Name.dll a single-file plugin
plugins/Name/Name.dll a plugin with private dependencies: every other dll inside plugins/Name/ is loaded into the plugin’s own context; changing any dll in that folder reloads the plugin
plugins/Name.cs, or a folder plugins/Name/ of .cs files a source plugin, compiled by the server when it starts (plugins.allow_source, on by default)

The directory is watched, so adding, replacing or deleting a dll loads, hot-reloads or unloads the plugin. dotnet build with RAMJET_PLUGINS set writes the folder layout.

Plugin.DataFolder is plugins/<id>/data/, where <id> is the plugin’s id from its manifest (homes for the example above). Plugins run in a separate, sandboxed worker process (scripting.mode, remote by default; see ramjet.toml), and that process can write only inside its own data folder. Keep every file the plugin writes there. Config.Load, Config.ReadText and Config.WriteText work on files in it.

The server reads plugin.json before it loads any assembly. It gives the plugin’s id, display name, version, the API version it was built for, the class to load, its dependencies and its permission nodes. The template creates this one:

{
"id": "homes",
"name": "Homes",
"version": "1.0.0",
"api": "1.0",
"main": "Homes.PluginMain",
"authors": ["Ahmed"],
"description": "Set and teleport to homes",
"depends": [],
"softDepends": [],
"permissions": []
}

The manifest also orders the plugins. The server builds the dependency order from the manifests, so it can skip a plugin whose hard dependency is missing or too old and say why, instead of failing with a stack trace. When a manifest is present, it takes precedence over [PluginInfo]. See the plugin.json reference for every field.

Next: build and hot reload.