# ramjet plugin development: complete documentation Generated at build time from the site sources (42 pages plus the generated API reference). Paths like /concepts/events/ refer to pages of the documentation site. The same content as structured data is in /api.json. ## Contents - Write a plugin in an hour (/getting-started/) - Install .NET and the server (/getting-started/install/) - Create a plugin from the template (/getting-started/create-a-plugin/) - Project layout and plugin.json (/getting-started/project-layout/) - Build, run and hot reload (/getting-started/build-and-hot-reload/) - Plugin lifecycle (/concepts/lifecycle/) - The threading model (/concepts/threading/) - Entities, handles and the ECS (/concepts/entities-and-ecs/) - Events (/concepts/events/) - Commands (/concepts/commands/) - Permissions (/concepts/permissions/) - Config and data (/concepts/config-and-data/) - The scheduler (/concepts/scheduler/) - Systems and tick phases (/concepts/systems/) - Hot reload (/concepts/hot-reload/) - Performance rules (/concepts/performance/) - Your first plugin (/guides/first-plugin/) - Commands with arguments and suggestions (/guides/commands-with-arguments/) - A homes plugin with config and permissions (/guides/homes-plugin/) - A scoreboard sidebar (/guides/scoreboard-sidebar/) - Boss bars, titles and action bars (/guides/boss-bars-and-titles/) - Custom GUIs (menus) (/guides/custom-guis/) - Inter-plugin services (/guides/inter-plugin-services/) - Migrating from Paper / Bukkit (/guides/migrating-from-paper/) - plugin.json reference (/reference/plugin-json/) - API status (/reference/status/) - Event catalogue (/reference/events/) - Command argument types (/reference/command-arguments/) - Native systems (/reference/native-systems/) - BossBarTimer (/examples/bossbartimer/) - DiscordWebhook (/examples/discordwebhook/) - Greeter (/examples/greeter/) - HelloPlugin (/examples/helloplugin/) - Homes (/examples/homes/) - Kits (/examples/kits/) - PermissionAdmin (/examples/permissionadmin/) - RubyOre (/examples/rubyore/) - RubyWand (/examples/rubywand/) - ShopGui (/examples/shopgui/) - SidebarStats (/examples/sidebarstats/) - SpawnProtect (/examples/spawnprotect/) - WorldTools (/examples/worldtools/) - API reference (generated) --- # Write a plugin in an hour Page: /getting-started/ From zero to a hot-reloading C# plugin on a Ramjet, and what to read next. Written for people who know Paper/Bukkit. Ramjet plugins are plain C# class libraries (.NET 10) loaded into the running server. They compile against the **`Ramjet.Api`** NuGet package, subscribe to events, register commands, components and per-tick systems, and are **hot reloaded every time you rebuild**: no server restart, no lost state in the ECS. If you have written Paper plugins you already know the shapes: a plugin class with enable/disable hooks, event listeners with priorities and cancellation, commands, permissions, a scheduler. The differences that matter are in how you write them, and they are explained in [Concepts](/concepts/lifecycle/) and in the [migration table](/guides/migrating-from-paper/). ## The one-hour path | Step | Time | Page | |---|---|---| | 1. Install the .NET 10 SDK and build the server | 15 min | [Install](/getting-started/install/) | | 2. Create a project from the template | 5 min | [Create a plugin](/getting-started/create-a-plugin/) | | 3. Understand the layout and `plugin.json` | 5 min | [Project layout](/getting-started/project-layout/) | | 4. Build, drop into `plugins/`, edit, watch it reload | 10 min | [Build and hot reload](/getting-started/build-and-hot-reload/) | | 5. Write something real | 25 min | [Your first plugin](/guides/first-plugin/) | Then read the five concept pages that carry the most weight: [threading](/concepts/threading/), [entities and the ECS](/concepts/entities-and-ecs/), [events](/concepts/events/), [hot reload](/concepts/hot-reload/) and [performance](/concepts/performance/). > **Check what is wired** > > The API is still growing. The [API status page](/reference/status/) lists which events the server fires and which host services are wired to the game, and is generated from the repository on every build. Read it before you build a feature on a call that may only log. ## Where things are - [Concepts](/concepts/lifecycle/): how plugins work. - [Guides](/guides/first-plugin/): full, working code. - [Examples](/examples/): the sample plugins of the repository, with their source read at build time. - [API reference](/reference/api/): generated from the C# XML documentation. - [For AI agents](/ai-agents/): `llms.txt`, `llms-full.txt`, `api.json`, a Claude Code skill and an `AGENTS.md` in the template. --- # Install .NET and the server Page: /getting-started/install/ Install the .NET 10 SDK, build Ramjet with scripting enabled and start it with a plugins folder. ## 1. The .NET 10 SDK Plugins target `net10.0`, and the server embeds the .NET 10 runtime (CoreCLR through `hostfxr`), so you need the **.NET 10 SDK** on the machine that builds the plugin and the same major runtime on the machine that runs the server. ```bash dotnet --list-sdks # must list a 10.x entry ``` If it does not, install it from [dotnet.microsoft.com](https://dotnet.microsoft.com/download/dotnet/10.0) (Windows installer, `winget install Microsoft.DotNet.SDK.10`, or your Linux package manager). The server finds the runtime itself (`DOTNET_ROOT`, the Windows registry, `/etc/dotnet/install_location`, the default install paths); you do not need to configure anything. ## 2. The server There is no binary release yet: you build Ramjet from the repository. You need CMake, Ninja and GCC (MinGW-w64 on Windows), see Install and run for the full toolchain. ```bash cmake -S . -B build -G Ninja -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ cmake --build build ``` When the configure step finds a .NET 10 SDK it turns `MC_BUILD_SCRIPTING` on and also builds the managed side next to the server: ``` build/bin/ramjet(.exe) build/bin/dotnet/Mc.Scripting.Api.dll <- the plugin API the server loads (plugins compile against the Ramjet.Api package) build/bin/dotnet/Mc.Scripting.Runtime.dll <- plugin host, loaded by the server build/bin/dotnet/samples/HelloPlugin/ <- the sample plugin ``` If the configure step printed `scripting: no .NET 10+ SDK found`, install the SDK and configure again. You do not need this build to write a plugin. Plugin projects compile against the `Ramjet.Api` NuGet package (see [create a plugin](/getting-started/create-a-plugin/)); the server needs its own build to run them. ## 3. Run it with plugins Create a folder to run in (it holds `server.properties`, the world and the plugins) and start the server with `--plugins`: ```bash mkdir run && cd run ../build/bin/ramjet --data-dir ../data --plugins plugins ``` - `--plugins ` turns scripting on: the folder is created if missing, every plugin in it is loaded, and the folder is **watched** for changes (hot reload). Without the flag, no .NET runtime is started at all. - `--data-dir` points at the vanilla data of the repository (`data/`, containing `vanilla/`). - The server binds `127.0.0.1` by default and listens on **25567** unless you set `server-port` in `server.properties` or pass `--port`. See [server.properties](/admin/server-properties/). - Console commands: `stop`, `list`, `help`. Look for these lines in the log: ``` scripting: .NET runtime started (.../build/bin/dotnet) [HelloPlugin] enabled v1.0.0 (id 1) ``` To try the sample, copy `build/bin/dotnet/samples/HelloPlugin/HelloPlugin.dll` into `run/plugins/`. Next: [create your own plugin from the template](/getting-started/create-a-plugin/). --- # Create a plugin from the template Page: /getting-started/create-a-plugin/ Install the ramjet-plugin dotnet template, create a project, point it at your server and build it. The repository ships a `dotnet new` template in `templates/ramjet-plugin/`. It creates a ready-to-build class library with the right project settings, a small plugin class, a `plugin.json`, and an `AGENTS.md` for AI coding agents. ## Install the template locally From a checkout of the repository: ```bash # Windows powershell -ExecutionPolicy Bypass -File templates/install.ps1 # Linux / macOS sh templates/install.sh ``` Both run `dotnet new install templates/ramjet-plugin --force`, so there is nothing to pack or publish. To remove it again, pass `-Uninstall` / `--uninstall`. If you prefer a NuGet package (to hand the template to others), pack it and install the `.nupkg`: ```bash dotnet pack templates -c Release -o artifacts dotnet new install artifacts/Ramjet.Templates.1.0.0.nupkg ``` ## Create the project ```bash dotnet new ramjet-plugin -n Homes --Author "Ahmed" --PluginDescription "Set and teleport to homes" cd Homes ``` | Option | Default | Effect | |---|---|---| | `-n`, `--name` | `MyPlugin` | assembly, namespace and plugin name; also the lower-cased `id` in `plugin.json` | | `--Author` | `Your Name` | `[PluginInfo(Author = ...)]` and `plugin.json` | | `--PluginDescription` | `A plugin for ramjet.` | `[PluginInfo(Description = ...)]` and `plugin.json` | | `--RamjetApiVersion` | `1.0.0-preview.1` | version of the `Ramjet.Api` package the project references | ## The API package The project references **`Ramjet.Api`**, the plugin API as a NuGet package. It holds a reference assembly: every public type and member of the API, with its documentation (IntelliSense), and no code. Your plugin compiles against it, and the server supplies the real API when it loads the plugin, so nothing from the package is copied next to your dll. You don't need a server build to compile a plugin. The package also sets up the project: - `EnableDynamicLoading` and implicit usings, so each plugin loads into its own unloadable context; - global usings for the API namespaces (`Mc.Scripting`, `Mc.Scripting.Commands`, `Mc.Scripting.Events`, ...). Set `RamjetGlobalUsings` to `false` to turn them off; - the deploy step described below. Until the package is on nuget.org, add the folder that holds `Ramjet.Api..nupkg` as a package source, once: ```bash dotnet nuget add source /path/to/folder-with-the-nupkg -n ramjet # from a checkout of the repository, build that folder first: dotnet pack sdk/Ramjet.Api -o artifacts/sdk ``` Optional: point the build at a server's plugins folder, and every build copies the plugin there, which triggers the server's hot reload: ```bash export RAMJET_PLUGINS=/path/to/run/plugins ``` PowerShell: `$env:RAMJET_PLUGINS = 'C:\path\to\run\plugins'`, or per build: `-p:RamjetPluginsDir=...`. ## Build ```bash dotnet build -c Release ``` ``` Homes -> .../bin/Release/net10.0/Homes.dll Homes deployed to /path/to/run/plugins/Homes/ ``` The second line appears only when `RAMJET_PLUGINS` (or `RamjetPluginsDir`) is set. Continue with the [project layout](/getting-started/project-layout/) to see what the template produced, or jump to [build and hot reload](/getting-started/build-and-hot-reload/) to watch the plugin load. --- # Project layout and plugin.json Page: /getting-started/project-layout/ What the template creates, how plugins are discovered, where a plugin keeps its files and what plugin.json is for. ## What the template creates ``` 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 ``` ### The plugin class **PluginMain.cs** ```csharp 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] ...`). ### The project file 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. ## How plugins are discovered Pass `--plugins ` 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. ## Data folder `Plugin.DataFolder` is `plugins//data/`, where `` 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](/admin/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. ## plugin.json 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: ```json { "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](/reference/plugin-json/) for every field. Next: [build and hot reload](/getting-started/build-and-hot-reload/). --- # Build, run and hot reload Page: /getting-started/build-and-hot-reload/ The edit-build-reload loop of a Ramjet plugin, plugins added while the server runs, and how to test against a scratch server. ## The loop 1. Start the server once, with a plugins folder, and leave it running: ```bash cd run ../build/bin/ramjet --data-dir ../data --plugins plugins ``` 2. In your plugin project, with `RAMJET_PLUGINS` set (see [create a plugin](/getting-started/create-a-plugin/)), build: ```bash dotnet build -c Release ``` 3. The build copies `Homes.dll`, `Homes.pdb` and `plugin.json` into `run/plugins/Homes/`. The server notices, waits 250 ms for the write to finish, and **reloads the plugin**: ``` [INFO] hot reloading Homes.dll [INFO] [Homes] enabled v1.0.1 (id 1) [INFO] [Homes] disabled ``` The new version is enabled **before** the old one is released, so there is no window in which your commands or event handlers are missing. Edit, build, test again: a few seconds per cycle. If you do not set `RAMJET_PLUGINS`, copy `bin/Release/net10.0/Homes.dll`, `Homes.pdb` and `plugin.json` into `plugins/Homes/` yourself. ## What survives a reload - **ECS components with an unchanged layout keep their values** on every entity. Change a field type or order and the values are dropped (and the component recreated), but only if the reloading plugin is the only one bound to the same component key; otherwise the registration fails and the log says why. - **Entities** are not owned by plugins, they survive. - **Files** in the plugin's data folder (`plugins/homes/data/`) survive. - **Plugin-owned objects** (your fields, caches, timers) are gone: the plugin is a new instance. Keep state that must outlive a reload in components or in the data folder. See [hot reload](/concepts/hot-reload/) for the full rules. ## A failed build changes nothing The new assembly is loaded first and the old one is unloaded only if that worked. A compile error never produces a dll; an exception in the new `OnEnable` is logged and the previous version keeps running: ``` [ERROR] failed to load plugin .../Homes.dll ... [WARN] reload of .../Homes.dll failed (code -3); keeping the previous version ``` ## Plugins added while the server runs A plugin folder copied into `plugins/` while the server runs is picked up without a restart. The plugin worker has to grant a new plugin its data folder before the plugin can load, so the server then restarts the worker: every plugin in it is disabled and loaded again, and plugin fields are reset as they are on any reload. `/plugins` shows the state of the plugin worker. `/plugins reload` restarts it on demand; it needs operator level 4. ## Debug logging ```csharp Log.Debug("..."); // visible when the server runs with --verbose Log.Info($"..."); ``` Plugin log lines carry the plugin name: `[Homes] ...`. ## Test against a scratch server Do not use a server people play on. Start a throw-away one on a free port and bind loopback only, which is the default: ```properties # run/server.properties server-ip=127.0.0.1 server-port=25640 online-mode=false level-type=flat # instant superflat world, nothing saved: ideal for plugin tests view-distance=4 simulation-distance=4 ``` `level-type=flat` skips world generation and persistence. Startup then spends its time loading the datapack and recipes, which takes anywhere from seconds to a couple of minutes depending on the machine. To get a player into the server without a Minecraft client, use the repository's headless load-test client; it speaks the real protocol: ```bash build/bin/botswarm --port 25640 --bots 1 --duration 20 --behaviour chat:100 --chat-interval 5 ``` The bot joins, so your `PlayerJoinEvent` handler runs, and `chat` bots send chat messages, which exercises `PlayerChatEvent`. The repository also contains an automated version of this walk-through: `docs-site/scripts/verify-template.mjs` instantiates the template, builds it, starts a scratch server, rebuilds the plugin to check the hot reload and joins a bot (`npm run verify:template`). ## Stop the server Type `stop` in the server console. On shutdown every plugin gets `OnDisable` in turn and its registrations are released. --- # Plugin lifecycle Page: /concepts/lifecycle/ How the server finds, loads, enables, disables and unloads a plugin, and what the context owns. ## From file to running plugin ``` plugins/Homes/Homes.dll (+ plugin.json beside it, if any) │ 1. plugin.json is read first: id, name, version, main, depends, ... │ 2. the assembly is loaded from bytes into a new collectible AssemblyLoadContext (the file is never locked) │ 3. the plugin class is found: "main" from plugin.json, else the first public IPlugin with a parameterless constructor │ 4. a PluginContext is created (name and version from plugin.json or [PluginInfo], else the assembly) ▼ OnEnable() register events, commands, systems, components, queries, tasks │ ... the plugin runs ... ▼ OnDisable() optional cleanup of your own state │ 5. everything registered through the context is released, in reverse order │ 6. native leftovers are dropped by owner tag (events, commands, systems, queries, components) │ 7. the load context is unloaded and garbage collected ``` Plugins run in a separate, sandboxed **worker process**, not inside the server. The worker loads every plugin from the plugins folder, and all of the steps above run on the worker's tick thread, in step with the server's ticks. Plugin code never runs on the server's own threads. The worker is started with the server and restarted when it needs to be (see [hot reload](/concepts/hot-reload/)). On shutdown and on `/plugins reload`, plugins are disabled first (`OnDisable` runs, so they can save) and then the worker exits. After a crash, the server starts a new worker and loads every plugin again from disk, so state that is not in files or components is lost. ## The plugin class **PluginMain.cs** ```csharp [PluginInfo("Homes", Version = "1.0.0", Author = "you")] public sealed class PluginMain : Plugin { protected override void OnEnable() { Log.Info("homes enabled"); } protected override void OnDisable() { Log.Info("homes disabled"); } } ``` `Plugin` exposes the plugin-scoped services as properties: `Log`, `Events`, `Ecs`, `Systems`, `Commands`, `Scheduler`, `Permissions`, `Config`, `DataFolder`, `Services`, `Info`, `Channels` and the raw `Context` (a [`PluginContext`](/reference/api/plugins/plugincontext/)). Everything you register through them is tracked by the context, so **you never unregister by hand**; unloading does it. - Do no work in the constructor. Use `OnEnable`. - You can implement [`IPlugin`](/reference/api/plugins/iplugin/) directly if you do not want the base class: `Enable(PluginContext)` receives the context, and `Disable()` is called on unload. - Name and version come from `plugin.json` when there is one. Without a manifest, [`[PluginInfo]`](/reference/api/plugins/plugininfoattribute/) names the plugin. Without either, the plugin is named after its assembly, with the assembly's version. ## The manifest A `plugin.json` next to the dll is read **before the assembly is loaded**. It declares what the loader needs to decide whether and in what order to load the plugin. The fields are described in the [plugin.json reference](/reference/plugin-json/); the ones that matter for the lifecycle: ```json { "id": "homes", "name": "Homes", "version": "1.0.0", "main": "PluginMain", "depends": ["economy >=1.2"], "softDepends": ["placeholders"], "loadBefore": [] } ``` - `depends`: must be loaded first. A missing one, or one with a version that does not satisfy the constraint, keeps this plugin from loading. - `softDepends`: loaded first when present. The plugin must cope with their absence (check [`Plugins.IsLoaded`](/reference/api/plugins/plugins/) or a service). - `loadBefore`: this plugin is loaded before the listed ones. - `api`: the plugin API version the plugin was written for. A server that does not provide it rejects the plugin. [`Plugin.Info`](/reference/api/plugins/pluginmetadata/) gives you the same data at runtime. ## Load order The loader sorts the plugins with a topological sort of `depends`, `softDepends` and `loadBefore`, and breaks ties by id, so the order is deterministic. Unloading runs in reverse order: a plugin's dependents are unloaded before it, because they hold its types. [`Plugins`](/reference/api/plugins/plugins/) is a read-only view of what is loaded: `Plugins.All`, `Plugins.Get(id)`, `Plugins.IsLoaded(id)` and `Plugins.Problems` (one sentence per plugin that did not load, and why). Use `PluginEnableEvent` and `PluginDisableEvent` to react when another plugin comes or goes; both fire after the plugin's registrations are set up or released. ## What happens on errors | Situation | Result | |---|---| | No public `IPlugin` with a parameterless constructor (or the `main` class is not one) | `no IPlugin implementation with a parameterless constructor` is logged, nothing is loaded | | A dependency is missing, too old, or part of a cycle, or the `api` version is not supported | the plugin is not loaded, with a message such as `plugin 'homes' requires 'economy >=1.2' which is not installed`; plugins that depend on it are not loaded either | | `OnEnable` throws | the exception is logged, every registration made so far is released, the plugin is **not** loaded, and `Plugins.Problems` names the cause; other plugins are unaffected | | `OnDisable` throws | logged (`error in OnDisable`), unloading continues | | A handler, task or system throws | caught at the boundary and logged with the plugin name; the server keeps running and the next call goes through | ## What the context owns Everything you create through the plugin's services belongs to the plugin and goes away with it: | Registered through | Released on unload | |---|---| | `Events.On` | the subscription | | `Commands.Register` | the command | | `Systems.Add/Register/After/Before/Replace` | the system (a `Replace`d system is restored) | | `Ecs.Query<...>()` | the query | | `Ecs.RegisterComponent` | the type binding (values on entities survive a reload, see [hot reload](/concepts/hot-reload/)) | | `Scheduler.RunLater/RunEvery` | the pending scheduled task; queued `Post`, `RunSync` and `ToTick` work of the plugin is dropped | | `BossBars`, `Holograms`, `Npcs`, `Sidebars`, `ChunkTickets`, `Menus`, `Channels`, `Permissions` | the plugin's boss bars, holograms, NPCs, sidebars, chunk tickets, menus, channel listeners and permission nodes | | `Context.Track(resource)` | anything implementing [`IPluginResource`](/reference/api/plugins/ipluginresource/) | Entities are **not** owned: they live in the world, not in the plugin. Destroy the ones you created in `OnDisable` if they should not outlive it. ## Enable and disable events `PluginEnableEvent` and `PluginDisableEvent` are fired when a plugin enables or disables, including on hot reloads. A disable event fires after the plugin's registrations are already released. --- # The threading model Page: /concepts/threading/ The tick thread owns the world. What runs where, what is thread-safe, and how to do async work. ## One rule **All plugin code runs on one thread, the tick thread, and the game-facing API must be called from it.** Event handlers, command handlers, systems, scheduled tasks, `OnEnable` and `OnDisable` are all invoked there. There is no per-plugin thread and no lock to take: while your handler runs, nothing else in your plugin runs. Plugins run in the plugin worker process, and the server drives that process one tick at a time. The worker's tick thread runs your code inside those ticks, so the rule is the same as in a single-threaded game loop. ``` Server Plugin worker process IO threads recv / decrypt / compress / write (no network, no world access) Tick thread 1. drain inbound packets 2. engine systems and events ◀───▶ your handlers, tasks and systems 3. build and flush outbound packets Worker pool chunk load + generation, lighting ``` The consequences are the same as in any single-threaded game loop: - **A slow handler delays the tick for every player.** A tick has 50 ms, and the engine's own work uses most of that on a busy server. Keep handlers in the microsecond range. The worker's budgets, described below, bound the wait for your answers, but they do not make slow code free. - **Never block** the tick thread: no `Thread.Sleep`, no `Task.Wait()` / `.Result`, no synchronous HTTP or disk reads of unbounded size. - **No synchronisation is needed** for state that only your handlers touch: they never run concurrently. ## What the tick waits for Most events are only observed, and the server does not wait for your answer. An **awaited** event is one your handler can change: a cancellable event, or one with fields you can write. For those the server waits for your handler's answer: - The wait is at most `scripting.ipc.event_budget_us` (5 ms by default). When it runs out, the event goes ahead with its default: it is not cancelled and the fields keep their values. The engine logs a warning that names the slow plugin. - Repeated overruns mute the plugin's event handlers until it is reloaded. - The whole tick has its own budget, `scripting.ipc.tick_budget_us` (20 ms by default). Once it is spent, awaited events take their default without a round trip. - The first time an event type reaches your plugin, the server may wait longer (`scripting.ipc.warmup_budget_us`), because your code is loaded and compiled then. ## Thread-safe members The thread-safe members are the ones you can call from any thread: - [`Scheduler.Post`](/reference/api/scheduler/scheduler/), `Scheduler.RunSync`, `Scheduler.RunAsync` and the awaitables `Scheduler.ToTick()` and `Scheduler.ToAsync()`. - `Log`. Logging from any thread is fine. - `Server.IsTickThread` (true only on the tick thread) and `Server.CurrentTick` (the engine's tick count). - `Http.Client`, the shared `HttpClient` for outgoing requests. Requests may be started from any thread. - `Config.Value`: the current values are an immutable snapshot, replaced as a whole on reload. - Completing a login deferral (see below). Everything else (`Events`, `Commands`, `Ecs`, `Entity`, `Server`, `Systems`, `Players`) must be called on the tick thread. ## Posting work back [`Scheduler.Post`](/reference/api/scheduler/scheduler/) queues an action to run on the tick thread at the start of the next tick. Use it to apply the result of slow work started elsewhere: **ProfilePlugin.cs** ```csharp [PluginInfo("ProfileDemo", Version = "1.0.0")] public sealed class ProfilePlugin : Plugin { protected override void OnEnable() { Events.On((ref PlayerJoinEvent e) => { string name = e.Username.ToString(); // copy: the string is only valid during the dispatch Player player = e.Player; _ = Task.Run(async () => { string profile = await Http.Client.GetStringAsync($"https://example.org/profile/{name}"); Scheduler.Post(() => // back on the tick thread { if (player.IsOnline) Log.Info($"loaded profile of {name}: {profile.Length} bytes"); }); }); }); } } ``` Rules of thumb for async work: 1. **Copy everything you need before leaving the tick thread**: strings (`Utf8String.ToString()`), numbers, the `Player` or `Entity` handle. Event structs and their borrowed fields are invalid after the handler returns. 2. **Re-validate on return**: the player may have left (`Player.IsOnline`), the entity may be dead (`Entity.IsAlive`). 3. **Do not touch the game API from `Task.Run`, `async` continuations, `System.Threading.Timer` or `FileSystemWatcher` callbacks.** An `await` resumes on the thread pool unless you hop back: use `Scheduler.Post`, `Scheduler.RunSync` or `await Scheduler.ToTick()`. 4. Avoid `async void` handlers: an exception on a pool thread has no plugin to be attributed to. ## Hopping between threads Inside an `async` method you can switch threads with awaitables: ```csharp private async Task SaveAsync(string path, string text) { await Scheduler.ToAsync(); // leave the tick thread before blocking work await File.WriteAllTextAsync(path, text); await Scheduler.ToTick(); // back on the tick thread at the next tick Log.Info($"saved {path}"); } ``` - `Scheduler.RunAsync(Action)` and `RunAsync(Func)` run work on the thread pool and return a `Task`. The work must not touch the game API. - `Scheduler.RunSync(Action)` and `RunSync(Func)` queue work for the tick thread and return a `Task` that completes after it ran (FIFO, next tick boundary). - A continuation waiting in `ToTick()` is owned by the plugin: if the plugin unloads first, it is dropped. If a posted task or scheduled callback throws, the exception is logged with your plugin's name and the server carries on. A task posted by a plugin that has unloaded in the meantime is dropped. ## Thread checks Game-facing members check the thread they are called on. Calling one from another thread throws [`WrongThreadException`](/reference/api/scheduler/wrongthreadexception/), for example `Events.On`, `Commands.Register`, `Ecs.RegisterComponent`, `Entity.Get` and `Plugins.All`. Use `Server.IsTickThread` when you need to know which thread you are on. Not every call is checked. In the worker, a host call from another thread that the API does not check is refused: it returns the default value and writes one line to the log. Do not rely on either behaviour; keep the rule. ## Login decisions that take time [`PlayerPreLoginEvent`](/reference/api/event-types/playerpreloginevent/) is the one event you can answer later. Call `Defer()` on the tick thread to hold the login, then complete the returned `LoginDeferral` from any thread, for example after a database lookup started with `Scheduler.RunAsync`. Completing a deferral is thread-safe and happens once. The server waits up to 30 seconds for the answer. ## Systems and queries Systems you register run on the tick thread, in the engine's pipeline phases (see [systems](/concepts/systems/)). Managed systems run one after another, never at the same time as each other or as your handlers, so it is safe to write to your own state in a system and in an event handler without locks. ## What about the engine's other threads? The IO threads and the chunk worker pool never call into plugin code. Results that come back from workers (a chunk finished generating) are integrated by the tick thread in the `AsyncIntegration` phase, so by the time you see a chunk or a block, it is in a consistent state. --- # Entities, handles and the ECS Page: /concepts/entities-and-ecs/ Why an Entity is a handle and not an object, how components, queries and systems work, and the reference-invalidation rule. If you come from Paper, this is the biggest mental shift. In Bukkit a `Player` or `Zombie` is an object with methods and a lifetime managed by the JVM. In Ramjet the world is an **ECS** (flecs): an entity is only an id, its data lives in **components** stored in flat arrays, and behaviour lives in **systems** that run over those arrays. That layout is why the server can keep thousands of players on one process, and it shapes the plugin API. Your plugin reaches the ECS through `Ecs` (`plugin.Ecs`): your own components, queries and bare entities. Game objects have typed handles of their own: [`Player`](/reference/api/players/player/), [`World`](/reference/api/worlds/world/) and [`Entity`](/reference/api/ecs/entity/). ## Entities are handles [`Entity`](/reference/api/ecs/entity/) is a 64-bit id (`ulong Id`) wrapped in a struct. It is blittable, so it can sit in an event, a component or a collection at no cost, and **holding one never keeps anything alive**. ```csharp Entity e = player.Entity; // a Player wraps its entity if (e.IsNull) return; // default / "no entity" (e.g. no attacker for fall damage) if (!e.IsAlive) return; // destroyed? (one native call) e.Set(new Mana { Value = 20 }); e.Destroy(); // only for entities made with Ecs.Spawn(); after this the handle is stale ``` A handle stays valid across component changes (the id never changes when you add or remove components). When the entity is destroyed the handle goes stale: `Has()` is false and `Get()` throws, rather than touching the wrong entity, because ids carry a generation. A `Player` handle that goes stale is safer still: its reads return defaults and its actions do nothing (`player.IsOnline` is false). A player who rejoins gets a new handle. | Paper / Bukkit | Ramjet | |---|---| | `Player player` object, `player.getHealth()` | `Player` handle: `player.Health`, `player.Position`, `player.SendMessage(...)` | | `player.setMetadata("mana", ...)` / `PersistentDataContainer` | your own component: `player.Set(new Mana { ... })` | | `UUID` lookups | `player.Uuid`; `Server.GetPlayer(Uuid)` | | `player.isOnline()` | `player.IsOnline`; for any entity, `entity.IsAlive` | ## Components: plain data you own A component is an **unmanaged struct** (no references, no `string`, no arrays) that you declare and register under a global key: ```csharp public struct Mana { public int Value; public int Max; } protected override void OnEnable() { Ecs.RegisterComponent("mymana:mana"); // key: "plugin:name" } ``` - The key is global and stable. Registering the same key with the **same layout** (field types and offsets) after a hot reload keeps every entity's values. A changed layout drops the values, and is refused while another plugin is also bound to the key (see [hot reload](/concepts/hot-reload/)). - **A component type must be owned by one plugin.** The API caches one id per CLR type for the whole process, so do not register a shared type (such as `Vec3d`) under different keys from different plugins; a second binding throws. - Strings and variable data do not fit in an unmanaged struct. Keep them in a side dictionary keyed by `Entity` (clean it up when the entity dies) or write them to disk, and keep only a numeric id in the component. - Tags are empty structs: `public struct Frozen { }`, `entity.Set(new Frozen())`, `entity.Has()`. - Components belong to their entity. Entities made with `Ecs.Spawn()` survive a reload of your plugin; the component bindings and queries do not. Reading and writing: | Call | Meaning | |---|---| | `entity.Set(new T { ... })` | add or overwrite a copy | | `entity.Has()` | present? | | `entity.TryGet(out var copy)` | copy out; false when missing | | `ref var c = ref entity.Get()` | in-place access; **throws** when missing | | `ref var c = ref entity.GetOrAdd()` | in-place access, adding a zeroed component first | | `ref var c = ref entity.TryGetRef()` | a null ref when absent: test with `Unsafe.IsNullRef(ref c)` | | `entity.Remove()` | remove | `Player` has the same members (`Get`, `TryGet`, `Has`, `Set`, `Remove`) for your components. A component that the engine registers under its own name can also be bound, if the layout matches. Writes through that binding change only a copy, so engine state is changed through the typed API instead. ## The rule that bites: refs die on structural change A `ref T` from `Get`, `GetOrAdd`, `TryGetRef` or a query column points **into the table that stores the entity**. It is valid only until the next *structural change touching that table*: adding or removing a component on **any** entity of the same archetype, destroying one, or creating one that lands there. Other code (a system, an event handler, another plugin) can make such a change between two lines of your code, so a ref you keep can dangle. ```csharp // WRONG: the ref may dangle after Set moves the entity to another table ref var mana = ref player.Get(); player.Set(new Frozen()); mana.Value--; // undefined // RIGHT: copy what you need, or re-fetch after the structural change int value = player.Get().Value; player.Set(new Frozen()); player.Get().Value = value - 1; ``` Practical habits: 1. Keep the **`Entity`** (or `Player`) in your data structures, never the `ref`; re-fetch when you need it. 2. Take the value out first when you must call other APIs in between. 3. Do not add, remove or destroy components of the entities you are iterating in a query loop. Collect the entities first and change them after the loop. 4. Register components in `OnEnable` and add them at spawn or join time, so a later `Set` does not move the entity. ## Queries A query is a cached, indexed view over all entities that have a set of components. Create it once with `Ecs.Query<...>()` and iterate it every tick. The query belongs to your plugin and is disposed when the plugin unloads. **ManaPlugin.cs** ```csharp public struct Mana { public int Value; public int Max; } [PluginInfo("ManaPlugin", Version = "1.0.0")] public sealed class ManaPlugin : Plugin { private Query _mana = null!; protected override void OnEnable() { Ecs.RegisterComponent("mymana:mana"); _mana = Ecs.Query(); // up to three components: Query Systems.Add("regen", Phase.Tick, _ => Regen()); } private void Regen() { foreach (var chunk in _mana) // one chunk per archetype: a Span per component { var mana = chunk.Field(0); // term index = order of the type arguments for (int i = 0; i < mana.Length; i++) if (mana[i].Value < mana[i].Max) mana[i].Value++; } } } ``` `chunk.Entities` gives the entity handles in the same order as the columns, and `chunk.Count` the number of rows. Leaving the `foreach` early releases the native iterator. For the convenient form, a little slower: ```csharp _mana.Each((Entity entity, ref Mana m) => { if (m.Value > m.Max) m.Value = m.Max; }); ``` `Count()` iterates the query and returns the number of matching entities. Bare entities that are not game objects come from `Ecs.Spawn()` (no position, never sent to clients). `Ecs.GetEntity(ulong id)` wraps an id you stored. ## Systems A system is a callback the engine runs once per tick in a chosen phase: `Systems.Add(name, Phase.Tick, callback)`. See [systems and phases](/concepts/systems/). ## Game entities Players, mobs, items and projectiles are also entities. For a game entity `Entity` offers: - `TypeKey` (`"minecraft:zombie"`), `IsGameEntity`, and `TryGetLocation(out Location)` or `Location`; - `Teleport(Location)` for mobs, items and the like (players use `Player.Teleport`, which fires their teleport event); - `Despawn()` (no death logic) and `Kill()` (the `/kill` path, with death events and drops); - `GetNbt()` and `SetNbt(data, merge)`, the data `/data get entity` shows. `Destroy()` is only for entities you made yourself. On a game entity it is ignored with a warning. Players cannot be despawned: use `Player.Kick`. ## What is not exposed Engine state (a player's health, position, inventory) is not a set of raw components you can query. It is read and changed through typed handles: `Player` (`Health`, `Position`, `GameMode`, `Inventory`, ...) and `World` (blocks, entities, time and weather). Those members look the entity up on each call, so a plugin never holds a pointer into an engine table. The [entity reference](/reference/api/ecs/entity/) and the [player reference](/reference/api/players/player/) list every member. --- # Events Page: /concepts/events/ Subscribing, priorities, cancellation, mutable fields, borrowed strings and custom events. Events are how the engine tells a plugin that something happened, and how a plugin changes the outcome. They are **synchronous**, run on the tick thread, and use **blittable structs passed by `ref`**: no boxing, no allocation, no GC pressure. **EventsDemo.cs** ```csharp [PluginInfo("EventsDemo", Version = "1.0.0")] public sealed class EventsDemoPlugin : Plugin { protected override void OnEnable() { // cancel chat that contains "spam": the engine drops the message Events.On((ref PlayerChatEvent e) => { if (e.Message.Contains("spam", StringComparison.OrdinalIgnoreCase)) e.Cancelled = true; }); // observe the final result, whoever cancelled it Events.On((ref PlayerChatEvent e) => Log.Info(e.Cancelled ? "blocked" : "delivered"), EventPriority.Monitor, receiveCancelled: true); // halve all damage dealt or taken Events.On((ref EntityDamageEvent e) => e.Amount *= 0.5f); } } ``` Events are reached through `plugin.Events` (an [`EventRegistry`](/reference/api/events/eventregistry/)) inside a `Plugin` subclass. ## Subscribing [`Events.On(listener, priority, receiveCancelled)`](/reference/api/events/eventregistry/) returns a [`Subscription`](/reference/api/events/subscription/). You rarely keep it: unloading the plugin unsubscribes everything. Dispose it to unsubscribe earlier, and check `IsActive` if you need to know. ```csharp Subscription sub = Events.On((ref PlayerJoinEvent e) => { ... }); sub.Dispose(); // optional ``` The handler is a plain delegate, `EventListener(ref T ev)`. There is no reflection-based listener scanning and no `@EventHandler` annotation. `Events.HasSubscribers()` tells you whether anyone listens, so you can skip building an expensive event. ## Priorities Lower runs **first**, and native and managed handlers share one ordering. The conventional values are [`EventPriority`](/reference/api/events/eventpriority/): | Priority | Value | Use it to | |---|---:|---| | `Lowest` | -200 | see the event before anyone changes it | | `Low` | -100 | | | `Normal` | 0 | the default | | `High` | 100 | override other plugins; act on a nearly final state | | `Highest` | 200 | last chance to change the outcome | | `Monitor` | 1000 | **observe** the final outcome; must not modify the event | The order is the same as Paper's (`LOWEST` first, `MONITOR` last). Any `int` is allowed (`(EventPriority)150`). ## Cancellation A catalogue event is either cancellable or not (`IsCancellable`, shown in the [catalogue](/reference/events/)). On a cancellable event a handler sets `e.Cancelled = true`: - later handlers are **skipped**, unless they subscribed with `receiveCancelled: true`; - the code that fired the event sees the result and acts on it (`PlayerChat` cancelled: the message is dropped; `PlayerMove` cancelled: the player is teleported back); - setting `Cancelled` on a non-cancellable event does nothing. The `Monitor` handler above is the usual way to react to the final outcome. It must not change the event. ## Mutable fields Most event fields are `readonly`. A few are **mutable**: a handler may change them and the engine reads the new value back after the dispatch. Numbers, booleans and enums are assigned directly (`PlayerMoveEvent.To`, `EntityDamageEvent.Amount`, `BlockBreakEvent.DropItems`). Strings and rich text are changed through `Set` methods, because the engine copies them into a per-tick buffer: `SetMessage(string)` on chat, `SetJoinMessage(Component)` on join, `SetCommand(string)` on a command, `SetMotd(Component)` on the server list ping. The [catalogue](/reference/events/) and the [event explorer](/reference/event-explorer/) show which fields are mutable. ```csharp Events.On((ref PlayerJoinEvent e) => e.SetJoinMessage(Component.Text($"{e.Username.ToString()} arrived"))); ``` Text fields you only read (`Username`, `Message` ...) are [`Utf8String`](/reference/api/primitives/utf8string/): see below. ## Borrowed strings and bytes Text fields such as `Message`, `Username` and `Command` are [`Utf8String`](/reference/api/primitives/utf8string/): a view of native memory that is **only valid during the dispatch**. Reading it allocates nothing until you ask for a string. **BorrowedText.cs** ```csharp [PluginInfo("BorrowedText", Version = "1.0.0")] public sealed class BorrowedTextPlugin : Plugin { protected override void OnEnable() { Events.On((ref PlayerChatEvent e) => { if (e.Message.IsEmpty) return; if (e.Message.EqualsAscii("!help"u8)) { /* byte compare, no allocation */ } string text = e.Message.ToString(); // allocates; safe to keep after the handler returns Log.Debug(text); }); } } ``` `Utf8String` offers `Bytes` (a `ReadOnlySpan`), `Length`, `IsEmpty`, `Contains(text, comparison)`, `EqualsAscii(bytes)` and `ToString()`. Never store the `Utf8String`, the `NativeBytes` or the `ref` event itself: the compiler refuses to capture a `ref` in a lambda, and the other two are `ref struct`s too. Copy first. ## Holding a login `PlayerPreLoginEvent` is the one event that may be answered later. Call `Defer()` in the handler, which returns a `LoginDeferral`, then call `Allow()` or `Deny(reason)` from any thread. The login waits for every deferral (up to 30 seconds) and is refused as soon as one denies it. A plugin that unloads releases its pending deferrals as allowed. ## Which events exist, and which fire The catalogue has 78 events. They are generated from one source of truth that the engine's C++ headers and the C# structs share, and each has a firing site in the engine today (see the generated [status page](/reference/status/), the full [catalogue](/reference/events/) and the [event explorer](/reference/event-explorer/)). Some events from Paper's list are **not declared yet**, because the engine feature behind them does not exist: `BlockExplode`, `EntityExplode`, `EntityTarget`, `EntityPickupItem`, `EntityTame`, `EntityBreed`, `EntityTransform`, `StructureGrow` and `LightningStrike`. A plugin cannot subscribe to them until they are declared. Everything that is declared fires. ## Custom events A plugin can define its own events and fire them on the same bus, so other plugins (and native code) can listen: **HomeSetEvent.cs** ```csharp public struct HomeSetEvent : IEvent { public EventHeader Header; // must be the first field public Entity Player; public int HomeCount; public static string Key => "homes:set"; // "plugin:name" public static bool IsCancellable => true; } ``` **HomeEvents.cs** ```csharp [PluginInfo("HomeEvents", Version = "1.0.0")] public sealed class HomeEventsPlugin : Plugin { protected override void OnEnable() { // another plugin (or this one) can cancel a home that would be the eleventh Events.On((ref HomeSetEvent e) => { if (e.HomeCount > 10) e.Header.Cancelled = true; }); } private void SetHome(Player player, int count) { var ev = new HomeSetEvent { Player = player, HomeCount = count }; if (!Events.Fire(ref ev)) return; // false: a handler cancelled it // store the home } } ``` - The struct must be blittable (no references or strings). Ids from 512 are assigned at runtime by key. - Registering the same key with a different size or cancellability while someone listens is rejected. - Native and managed handlers interleave by priority, exactly as for catalogue events. - `Events.Fire` must be called on the tick thread. ## Errors An exception that escapes a handler is caught at the native boundary and logged with the plugin's name and the stack trace, and dispatching continues with the next handler. A bug in one plugin cannot take the server down. A handler that keeps throwing (more than 100 exceptions in 20 seconds) is switched off until the plugin is reloaded, and the log says so. ## Cost Firing an event nobody listens to costs about 0.4 ns (one bit test); an event with a C# handler costs more, and more again when plugins run in the separate worker process (the numbers are on the [performance](/concepts/performance/) page). A handler that allocates is where the real cost is; see [performance](/concepts/performance/). A handler that blocks the tick thread for more than 50 ms is reported in the log, so move slow work to `Scheduler.RunAsync` (see [threading](/concepts/threading/)). Subscribe to the events you need, not to `PlayerMoveEvent` for a rarely needed feature. --- # Commands Page: /concepts/commands/ Command trees on the server's real dispatcher: typed arguments, suggestions, permissions, failures, and what a plugin cannot do with commands yet. The server has a complete **Brigadier-compatible command engine** (a port of `com.mojang.brigadier` and vanilla's `net.minecraft.commands`): the full vanilla command tree, suggestions, and the Commands packet the client uses for tab completion and syntax colouring. The [commands page](/admin/commands/) browses the vanilla tree. Plugin commands are **nodes of that same tree**. They show up in the client's command tree with typed arguments, tab-complete on the server, and run through the same dispatcher as vanilla commands. You build a tree with `Command.Literal` and `Command.Argument` and register it through `plugin.Commands`. ## A first command **HomeCommands.cs** ```csharp [PluginInfo("HomeCommands", Version = "1.0.0")] public sealed class HomeCommandsPlugin : Plugin { private readonly Dictionary _homes = []; protected override void OnEnable() { Permissions.Register("homes.use", PermissionDefault.True, "Use /sethome and /home"); Commands.Register(Command.Literal("sethome").Permission("homes.use") .Executes(ctx => SetHome(ctx)), description: "Remember this place as your home"); Commands.Register(Command.Literal("home").Permission("homes.use") .Executes(ctx => GoHome(ctx.RequirePlayer())) .Then(Command.Literal("of").OpLevel(2) .Then(Command.Argument("player", Arg.Player()) .Executes(ctx => GoHome(ctx.GetPlayer("player"))))), aliases: ["h"], description: "Teleport to your home"); } private int SetHome(CommandContext ctx) { var player = ctx.RequirePlayer(); _homes[player.Uuid] = player.Location; ctx.Reply("Home set"); return 1; } private int GoHome(Player player) { if (!_homes.TryGetValue(player.Uuid, out var home)) throw new CommandFailure($"{player.Name} has no home. Use /sethome"); player.Teleport(home); return 1; } } ``` - `Commands.Register(root, aliases, description)` returns a `RegisteredCommand`. Dispose it to unregister; unloading the plugin does that for you. - The root must be a literal. Each alias runs the same tree, and the client shows it too. - A name that the server or another plugin already owns throws `InvalidOperationException`. A server operator can let a plugin take over a vanilla root with `commands.override` in the server configuration; the groups are `tp` (with `teleport`) and `msg` (with `tell` and `w`). - `Commands.Register(name, handler, description)` is sugar for a `name` literal followed by a greedy `args` argument. Use it for quick commands that parse their own text: **FlatHello.cs** ```csharp [PluginInfo("FlatHello", Version = "1.0.0")] public sealed class FlatHelloPlugin : Plugin { protected override void OnEnable() { Commands.Register("hello", ctx => { ctx.Reply($"Hello, {(ctx.Args.Length > 0 ? ctx.Args[0] : "world")}!"); ctx.Result = ctx.Args.Length; // the command's numeric result }, "Says hello"); } } ``` To see every command a player types, including vanilla ones, subscribe to [`PlayerCommandEvent`](/reference/api/event-types/playercommandevent/). Its `Command` field is the line without the slash, and cancelling it suppresses the command. ## The context An executor gets a [`CommandContext`](/reference/api/commands/commandcontext/): | Member | Meaning | |---|---| | `Source` | who runs it: a [`CommandSource`](/reference/api/commands/commandsource/) | | `Sender`, `Player`, `RequirePlayer()` | `Sender` is the executing entity (`Entity.Null` for the console); `Player` is the executing player, or `null`; `RequirePlayer()` returns the player and throws `CommandFailure` for the console | | `Name`, `Input`, `RawArgs`, `Args` | the root word, the whole line, the text after the root, and the words of it | | `Has(name)` | whether an optional argument was given | | `GetInt`, `GetLong`, `GetFloat`, `GetDouble`, `GetBool`, `GetString` | typed values of the argument with that name | | `GetPlayer`, `GetPlayers`, `GetEntities` | selectors, resolved by the engine (`@a`, `@e[...]`) | | `GetBlockPos`, `GetVec3`, `GetBlockState` | positions (relative coordinates resolved against the source) and block states | | `GetText` | the textual form of any argument, for types without a typed getter (items, components, NBT, uuids) | | `Reply(string)`, `Reply(Component)` | answer whoever ran the command | | `Result` | the command result, the number `/execute store` sees (default 1) | | `Detach()` | a copy of what a deferred reply needs, valid after the executor returns | [`CommandSource`](/reference/api/commands/commandsource/) has `Entity`, `Player`, `IsConsole`, `PermissionLevel`, `Position`, `World`, and feedback: `Success(message, broadcastToOps)` and `Failure(message)`. Feedback is sent without aborting the command. An executor returns an `int`, or is an `Action` whose result is `Result`. `1` means success. ## Argument types Each [`Arg`](/reference/api/commands/arg/) factory creates the argument type the client shows: `Arg.Int(min, max)`, `Arg.Long`, `Arg.Float`, `Arg.Double`, `Arg.Bool()`, `Arg.Word()`, `Arg.String()` (one phrase), `Arg.GreedyString()` (the rest of the line), `Arg.Player()`, `Arg.Players()`, `Arg.Entity()`, `Arg.Entities()`, `Arg.Vec3()`, `Arg.BlockPos()`, `Arg.BlockState()`, `Arg.Item()`, `Arg.Component()`, `Arg.Uuid()`, `Arg.Time(min)`, `Arg.Dimension()`, `Arg.GameMode()`, `Arg.Resource("registry")` and many more, down to the vanilla particle, loot and feature arguments. The full list, with the client-side properties, is on [command argument types](/reference/command-arguments/). There is no enum factory yet. Use `Arg.Word()` with a `Suggests` list and check the value in the executor. ## Suggestions Tab completion is computed on the server. A provider can return plain words: ```csharp Command.Argument("name", Arg.Word()) .Suggests(ctx => HomeNames(ctx.Player)) // Func> ``` or fill a builder, with tooltips and an integer form: ```csharp Command.Argument("count", Arg.Int(1, 64)) .Suggests((ctx, builder) => builder.Suggest("8", "a stack of eight")) // SuggestionProvider ``` The engine filters the candidates by what the player typed, the way vanilla does. ## Requirements and permissions A node can require the sender to have a permission node, a vanilla operator level, or any condition: - `.Permission("homes.use")`: the sender needs the node (see [permissions](/concepts/permissions/)). A player without it does not see the node in the client's tree. - `.OpLevel(2)`: a vanilla operator level, 0 to 4. - `.Requires(source => source.PermissionLevel >= 2)`: any condition on the [`CommandSource`](/reference/api/commands/commandsource/). The console always passes a `Permission` check. ## Failures Throw [`CommandFailure`](/reference/api/commands/commandfailure/) to fail the command with a red message. The result is 0, and nothing is written to the log. Any other exception shows the sender a generic error and logs the stack trace. `ctx.Source.Failure(message)` shows a red message without ending the command. ```csharp throw new CommandFailure("You have no home called 'base'"); ``` ## Ownership and reload Everything you register is removed when the plugin unloads. Clients get their trees refreshed once per tick, not once per change. The `CommandsRegistered` event fires after the tree has changed (see [events](/concepts/events/)). ## What is not there yet - **Running a command line from a plugin.** No API runs a command as a player or the console yet. Put the shared code in a method and call that instead. - **Async suggestions in the plugin worker.** `SuggestsAsync` exists, but plugins run in the plugin worker, which does not start those providers yet: the client gets no suggestions from them. Use `Suggests` until that changes. - **Typed getters for `Uuid` and enums.** Use `GetText` and parse it, and `Arg.Word()` with a suggestion list. --- # Permissions Page: /concepts/permissions/ Permission nodes, wildcards, defaults, groups, the provider interface, and how vanilla ops map onto them. Permissions decide who may use a command or a feature. A permission is a dotted **node** that your plugin declares with a default. The server ships with a file-based backend (groups and per-player grants in `permissions.json`), and a plugin can replace it with its own. ## Nodes You declare the nodes your plugin checks, with a default and a description, so admins can discover them: **PermissionsDemo.cs** ```csharp [PluginInfo("PermissionsDemo", Version = "1.0.0")] public sealed class PermissionsDemoPlugin : Plugin { protected override void OnEnable() { Permissions.Register("homes.use", PermissionDefault.True, "Use /home"); Permissions.Register("homes.set", PermissionDefault.True, "Use /sethome"); Permissions.Register(new Permission("homes.admin", PermissionDefault.Op, "Teleport to other players' homes")); Events.On((ref PermissionChangedEvent e) => Log.Info(e.AllPlayers ? "permissions changed for everybody" : $"permissions changed for {e.Player}")); } private void CheckHomes(Player player) { if (!player.HasPermission("homes.set")) { player.SendMessage(Component.Text("No permission.")); return; } // three-valued: True, False or Unset (no entry: the node's default decides) PermissionValue admin = PermissionManager.GetExplicit(player.Uuid, "homes.admin"); } } ``` `plugin.Permissions.Register` is removed again when the plugin unloads. A node can also be listed in the `permissions` array of [`plugin.json`](/reference/plugin-json/), so it exists before your plugin loads. A node name is dotted, has no spaces, and does not start or end with a dot. To check a node, call `player.HasPermission("homes.set")`, or `plugin.Permissions.Has(player, node)` (also takes a `Uuid`, so it works for offline players). [`PermissionManager.GetExplicit`](/reference/api/permissions/permissionmanager/) returns the tri-state [`PermissionValue`](/reference/api/permissions/permissionvalue/) without applying defaults. A command can check the same node for you with `.Permission("homes.use")` (see [commands](/concepts/commands/)). ## Defaults The default is what a node is worth when no entry decides: | Default | Meaning | |---|---| | `PermissionDefault.True` | everybody | | `PermissionDefault.False` | nobody, unless granted | | `PermissionDefault.Op` | vanilla operator level 2 or higher | | `PermissionDefault.OpLevel(n)` | vanilla operator level `n` or higher (1 = bypass spawn protection, 2 = gamemaster, 3 = admin, 4 = owner) | | `PermissionDefault.NotOp` | only players who are not operators | An undeclared node resolves to denied. The console has every permission. ## Wildcards and precedence A grant of `homes.*` matches every node under `homes.`, and `*` matches everything. The answer is found in layers, and the first layer with an opinion wins: 1. the player's own entries; 2. the groups the player is in, highest weight first (see below); 3. the implicit `default` group that everybody is in; 4. the node's default. Inside one layer the most specific entry wins: an exact node beats the longest matching wildcard, which beats `*`. A `false` entry is a denial. Try it with the rules of one layer: _(An interactive PermissionTester component is on the site page /concepts/permissions/.)_ ## Groups and per-player overrides The built-in backend stores **groups** in `permissions.json` in the server directory. A group has a weight, optional parents (`inherits`), a set of nodes with true or false values, and optional prefix and suffix text for chat plugins. A **per-player override**, keyed by UUID, adds nodes and group memberships. The file is plain JSON. Comments and trailing commas are allowed, and changes are written to disk shortly after they happen and at shutdown. `PermissionManager.Reload()` (or `/perm reload`) reads it again. `PermissionManager.BuiltIn` gives the file backend's administration methods, such as `SetPlayerPermission` and `SetGroupPermission`. `GetPrefix(uuid)` returns a player's group prefix. ## Vanilla ops Operators keep working as before. The operator level of a player (from `ops.json`) is the default value source for `Op` and `OpLevel`. Every vanilla command also has a node, `minecraft.command.`, whose default is that command's operator level, so a plugin can check it with `HasPermission`. An explicit value for such a node overrides the level check. The server setting `permissions.enforce_vanilla_nodes` (`off`, `warn` or `on`; the default `warn` only logs the difference) decides whether those explicit values also gate the vanilla command, once a permissions plugin has claimed the engine's permission table. Until then, vanilla commands are gated by their operator level alone. ## Replacing the backend `plugin.Permissions.SetProvider(provider)` replaces the answers for as long as your plugin is loaded. The provider implements [`IPermissionProvider`](/reference/api/permissions/ipermissionprovider/): **StaticPermissionProvider.cs** ```csharp public sealed class StaticPermissionProvider : IPermissionProvider { public string Name => "static example"; public event Action? Changed; public PermissionValue Get(Uuid player, string node) => node.StartsWith("homes.", StringComparison.Ordinal) ? PermissionValue.True : PermissionValue.Unset; public IReadOnlyCollection Groups(Uuid player) => []; // call this when the data behind the provider changed public void Refresh() => Changed?.Invoke(null); } ``` - `Get` returns `Unset` when the provider has no opinion, so the node's default decides. Wildcards and groups are the provider's business. - `Groups` returns the group names of a player, for display and admin commands. - `Changed` tells the server that answers changed: `null` means everything, a `Uuid` means one player. The manager caches answers until then, so a database-backed provider must raise it when its data changes. The provider is removed when its plugin unloads, and the built-in backend takes over again. ## Events - [`PermissionChangedEvent`](/reference/api/event-types/permissionchangedevent/) carries the affected player's `Player` (a `Uuid`) and `AllPlayers`, which is true when a group or the whole backend changed. The command trees of the players are refreshed after it. - [`PermissionsReloadedEvent`](/reference/api/event-types/permissionsreloadedevent/) fires when the built-in file has been read again. ## Admin commands The `PermissionAdmin` sample adds `/perm` (alias `/permission`, node `permissions.admin`, default operator level 3) on top of this API. It covers `reload`, `list`, `user set|unset|check `, `user group add|remove `, `user info`, and `group create|delete|set|unset|parent|list`. A replacement backend can reuse the same command layout. See the [homes guide](/guides/homes-plugin/) for permissions in a complete plugin. --- # Config and data Page: /concepts/config-and-data/ Typed JSON config with defaults, the plugin data folder, atomic writes and live reload. 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. ## The data folder `plugin.DataFolder` (the same as `plugin.Config.DataFolder`) is the plugin's folder: `plugins//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. ## Typed config 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** ```csharp 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** ```csharp [PluginInfo("HomesConfig", Version = "1.0.0")] public sealed class HomesConfigPlugin : Plugin { private Config _config = null!; protected override void OnEnable() { _config = Config.Load(MyJson.Default.HomesConfig); // plugins//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`](/reference/api/config/pluginconfig/) returns a [`Config`](/reference/api/config/config-1/): | 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. ## Your own files 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: ```csharp 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](/concepts/threading/)). ## What survives a reload 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](/concepts/hot-reload/)). State that must outlive a reload belongs in components or on disk. --- # The scheduler Page: /concepts/scheduler/ Tick-based delayed and repeating tasks, posting work back to the tick thread, and how tasks are owned and ordered. The scheduler is tick-based: **20 ticks are one second**, and a scheduled task always runs on the tick thread. There is no separate "async task" concept: for slow work use the thread pool ([`Scheduler.RunAsync`](/reference/api/scheduler/scheduler/)) and get back with [`Scheduler.Post`](/reference/api/scheduler/scheduler/) or `RunSync` (see [threading](/concepts/threading/)). **SchedulerDemo.cs** ```csharp [PluginInfo("SchedulerDemo", Version = "1.0.0")] public sealed class SchedulerDemoPlugin : Plugin { private ScheduledTask? _autosave; protected override void OnEnable() { // once, 5 seconds from now Scheduler.RunLater(100, () => Server.Broadcast("five seconds later")); // every minute, first run after 10 ticks _autosave = Scheduler.RunEvery(20 * 60, () => Log.Info("autosave"), initialDelayTicks: 10); } protected override void OnDisable() { _autosave?.Cancel(); // optional: unloading cancels it too } } ``` ```csharp // from another thread: run on the tick thread at the start of the next tick Scheduler.Post(() => Apply(result)); ``` | Member | Behaviour | |---|---| | `RunLater(delayTicks, action)` | runs once. The delay is at least 1 tick (a smaller value is raised to 1) | | `RunEvery(periodTicks, action, initialDelayTicks = 0)` | repeats. `periodTicks` must be positive (`ArgumentOutOfRangeException` otherwise); the first run is after `max(1, initialDelayTicks)` ticks | | `Post(action)` | **thread-safe**. Runs at the start of the next tick, FIFO, before due scheduled tasks | | `RunSync(action)` / `RunSync(func)` | **thread-safe**. Runs on the tick thread at the next tick boundary and returns a `Task` (with the result for `RunSync`). The task faults if the action throws, and never completes if the plugin unloads first. Its continuations run on the thread pool, so use `ToTick()` to return to the tick thread | | `RunAsync(action)` / `RunAsync(func)` | runs on the thread pool and returns a `Task`. It must not touch the game API | | `ToTick()` / `ToAsync()` | awaitables: `await Scheduler.ToTick();` resumes on the tick thread at the next tick, `await Scheduler.ToAsync();` on a thread-pool thread. A continuation of an unloaded plugin is dropped | | `Delay(ticks)` | returns a `Task` that completes after `ticks` ticks (at least 1). Its continuations run on the thread pool, as for `RunSync`; `await Scheduler.ToTick()` to return | | `ScheduledTask.Cancel()` / `Dispose()` | stop it; `IsCancelled` tells. Safe to call twice | ## How it runs Every tick, the scheduler runs in this order: 1. **posted** actions (those queued before the tick started), oldest first, and the `RunSync` work with them; 2. every scheduled task whose due tick has arrived, ordered by due tick and then by creation order. A repeating task is rescheduled for `now + period` **after it ran**, so a slow task drifts later instead of piling up. If an action throws, the exception is logged with the plugin's name and the task keeps its schedule (a repeating task runs again; a one-shot is finished anyway). In the plugin worker, scheduled tasks run when the tick's window opens, before the engine's own systems of that tick. In-process they ran after the engine's pipeline. Do not depend on the order between your tasks and engine systems. ## Ownership Tasks belong to the plugin that created them. When the plugin unloads (or hot reloads) its pending scheduled and posted tasks are discarded, so no delegate from your collectible assembly survives. A repeating task from the old version does not carry over to the new one: re-create it in `OnEnable`. ## Tips - **Counting time**: derive durations from tick counts, not wall clocks. A lagging server stretches a tick, and your timers should stretch with it (a 5 second cooldown is `5 * 20` ticks). `Server.CurrentTick` is the engine's tick count and is thread-safe, so you can compare it with a stored value. - **Do not allocate per run** in a frequently repeating task: hoist delegates into fields, avoid LINQ and string building (see [performance](/concepts/performance/)). - **Heavy work** (disk, network) goes to the thread pool and returns with `Post` or `RunSync`; do not run it inside a scheduled task. - For per-tick work over many entities prefer a [system](/concepts/systems/) with a query over a scheduled task that loops. --- # Systems and tick phases Page: /concepts/systems/ Per-tick systems in the engine's pipeline, ordering against native systems, replacing and disabling vanilla behaviour. A **system** is a function the engine calls once per tick, in a chosen phase, with the tick's delta time in seconds. Systems are how you run per-tick logic over many entities (with a [query](/concepts/entities-and-ecs/#queries)), and how you hook, reorder, replace or switch off the server's own behaviour. Your systems live in the **engine's own system registry**: they run in the same pipelines as native systems, are ordered against them by name, and show up with timings in the engine's system list. **ManaPlugin.cs** ```csharp [PluginInfo("ManaDemo", Version = "1.0.0")] public sealed class ManaPlugin : Plugin { private Query _mana = null!; protected override void OnEnable() { Ecs.RegisterComponent("manademo:mana"); _mana = Ecs.Query(); Systems.Add("regen", Phase.Tick, dt => { foreach (var chunk in _mana) { var mana = chunk.Field(0); for (int i = 0; i < mana.Length; i++) mana[i].Value += 1; } }); } } public struct Mana { public int Value; } ``` ## Phases [`Phase`](/reference/api/systems/phase/) values, in the order they run each tick: | Phase | What happens there | |---|---| | `NetworkReceive` | drain inbound packets, apply them to entities | | `PreTick` | timers, input integration | | `Tick` | game logic: entities, blocks ... (the default for `Add`) | | `PostTick` | removals, collision resolution, derived state | | `Tracking` | spatial index update, view diffs | | `NetworkSend` | build and encode outbound packets | | `AsyncIntegration` | integrate worker results (chunk loads, lighting) | Deferred changes (entity creation, component add/set/remove) made in one phase are merged at the phase boundary and are visible to every **later** phase of the same tick. A `Tick` system sees an entity a native `NetworkReceive` system created in the same tick; the other way round only from the next phase on. ## Ordering against other systems ```csharp Systems.Register(Phase.Tick, "regen", dt => ...); // end of the phase (same as Add) Systems.After("vanilla:living_physics", "after_physics", dt => ...); // right behind a native or scripted system Systems.Before("mobs:ai", "before_ai", dt => ...); // right in front of one var h = Systems.Replace("mobs:ai", "my_ai", dt => ...); // disables mobs:ai, runs in its place h.Dispose(); // ... and restores mobs:ai Systems.SetEnabled("mobs:spawner", false); // re-enabled when the plugin unloads ``` The names you can anchor on are listed on the generated [native systems](/reference/native-systems/) page. - **Names** are the engine registry's names and are unique. Yours are namespaced by the plugin: `Add("regen", ...)` in a plugin called `Hello` registers `Hello:regen` (a name that already starts with `Hello:` is kept). Registering a name that a native system or another plugin holds throws. `SystemHandle.Name` returns the effective name. - **Anchors** for `After`/`Before`/`Replace` are full registry names (not namespaced), and the new system takes the anchor's phase. `Register` with a placement other than `Append` needs an anchor. - **`Replace` is reversible by design**: the original is disabled and yours runs right after it. Dispose the handle or unload the plugin and the original comes back. Two plugins replacing or disabling the same system do not fight: it is re-enabled only when the last holder goes. - **Registry changes made from inside a system** (`Register`, `Dispose`, `SetEnabled`) are queued and applied right after the tick; the handle is valid immediately. - **`SetEnabled(name, bool)`** works on native and scripted systems, returns `false` for an unknown name, and is undone for a system your plugin disabled when it unloads. ## Systems in the plugin worker Your systems run in the plugin worker, inside the window of the phase they were registered for: after the engine's systems of earlier phases and before the later ones. A system positioned next to an engine system (`After`, `Before`, `Replace`) runs where the engine's registry puts it, through a proxy. The `dt` a system receives is the engine's frame delta. Two things differ from in-process plugins: - A component your plugin defines lives in the worker. Engine systems and engine queries do not see it. - An engine component can be read from the worker, but not written: a write is ignored with a warning. ## Hot reload and system names A reloaded plugin registers its successor **before** the predecessor is released. For that moment the new system runs as `Hello:regen#2`, and is renamed back to `Hello:regen` (at the same position) once the old one is gone. ## Systems versus events versus the scheduler | You want | Use | |---|---| | react to something that happened | an [event](/concepts/events/) | | run logic every tick over many entities | a system and a query | | run something later or periodically, not per entity | the [scheduler](/concepts/scheduler/) | | change how vanilla behaves | `Replace`, `Before`, `After` or `SetEnabled` on a [native system](/reference/native-systems/) | Managed systems are not multithreaded: they run on the tick thread, one after another, never at the same time as each other or as your event handlers. --- # Hot reload Page: /concepts/hot-reload/ How plugins are reloaded without a restart, what survives, what does not, and the rules that keep reload leak-free. Hot reload is the default workflow, not an afterthought: rebuilding a plugin replaces it in the running server (the delay is the 250 ms debounce plus load time), and **ECS component values survive it**. It is on by default (`plugins.hot_reload`). ## What the server does 1. A file watcher on the plugins folder sees a changed `*.dll` (any dll inside `plugins/Name/` counts, so a private dependency change reloads `Name` too) and waits **250 ms** after the last write. 2. It checks the file can be opened (a half-written dll is retried later), then **loads the new version first**: a new collectible `AssemblyLoadContext`, assemblies read from bytes, so files are never locked and a rebuild can overwrite them while the server runs. 3. The new instance's `OnEnable` runs **while the old one is still registered**. Names the old version holds (commands, systems) are taken over by the successor, and components or events it re-declares are re-bound. 4. Only if that succeeded is the old version unloaded: `OnDisable`, every tracked registration released in reverse order, native leftovers dropped by owner tag, the old context unloaded and collected. 5. If the new version fails to load, **nothing changes**: the old one keeps running and the log says why. `Mc.Scripting.Api` always resolves to the server's own copy, in every plugin context, so types unify across plugins and across reloads. ## What survives | | Survives a reload? | |---|---| | ECS component values with an unchanged layout and key | **yes** | | Entities | yes (they are not owned by plugins) | | Your plugin object, its fields, static fields, caches, delegates | **no**: it is a new instance in a new load context | | Subscriptions, commands, systems, queries, scheduled and posted tasks | released, then re-registered by the new `OnEnable` | | Files on disk, config | yes | A component keeps its values when its **size, alignment and field layout** (types and offsets, computed from the struct) and its key stay the same. Change a field type or order, and the values are dropped and the component recreated, but only if the reloading plugin (or the one it replaces) is the only one bound to the key; otherwise registration fails with a message in the log. Renaming a field does not change the layout. Because plugin state in fields is lost, design for it: keep what must survive in **components or on disk**, and rebuild caches in `OnEnable`. ## Restarts of the plugin worker Some operations restart the plugin worker process rather than reloading one plugin. A restart starts from nothing: every plugin is loaded again from disk, and the component values and the entities the worker created are gone. - **`/plugins reload`** restarts the worker on request. Every plugin is disabled first (`OnDisable` runs and they save), then the worker is started again and loads them all. - **A crash** of the worker (a plugin that brings the process down) is handled the same way, automatically. - **A plugin added while the server runs** is picked up by the watcher. Its data folder may not be writable until the worker restarts, in which case the plugin waits and the server restarts the worker for it. That restart reloads every plugin. `/plugins` shows the state of the worker. ## Rules to keep reload leak-free Unloading a collectible context only completes when **nothing references it**. The runtime checks this after every unload (the tests assert zero leaked contexts), and logs ` was unloaded but is still referenced (leak)` if you break it. The ways to break it: 1. **Do not store anything from your assembly in a static field of another assembly** (a framework or API class, a `ThreadLocal`, `AppDomain` data, a `ConditionalWeakTable` of your types...). That pins your context. 2. **Do not start threads or timers that outlive `OnDisable`**: a `Task.Run` loop, a `System.Threading.Timer`, a `FileSystemWatcher` of your own. Stop them in `OnDisable`, or use the [scheduler](/concepts/scheduler/), which is cancelled for you. 3. **Register through the plugin context** (`Events`, `Commands`, `Systems`, `Scheduler`, `Ecs`) rather than a global registry, so the context can release it. Anything custom: implement `IPluginResource` and call `Context.Track(...)`. 4. **Do not keep `Entity` refs (`ref T`) or event structs across ticks**: see [entities](/concepts/entities-and-ecs/). 5. A plugin's own dependencies (NuGet packages) are loaded privately into its context. Share *types between plugins* only through the API assembly or a shared assembly that both plugins reference with `Private=false` and the server can resolve. 6. After a failed reload, if the failed version already re-declared a component or event layout, that declaration stays. Fix and reload again. ## Triggering reloads - Build into the watched folder: set `RAMJET_PLUGINS` to the server's plugins folder (see [create a plugin](/getting-started/create-a-plugin/)). The template copies dependencies and the pdb first and the main dll last, because the dll is what the watcher reacts to. - Copying a dll over the old one works the same way; deleting it unloads the plugin; adding a new `Name.dll` or `Name/Name.dll` loads it. ## Plugins that depend on the reloaded one A reload also reloads the plugins that hard-depend on the one being replaced (see `depends` in [the lifecycle](/concepts/lifecycle/#the-manifest)), so that their types bind to the new version. Plugins that only soft-depend on it keep running, and can see the service they used go away and come back through `Services`. --- # Performance rules Page: /concepts/performance/ Allocation-free hot paths, what each call costs, and why the rules exist. The server is built to carry far more players per process than a JVM server, and the plugin layer is built not to give that back. Most of the cost of a plugin is **what your handlers allocate and how often they run**, not the cost of calling them. ## Why allocation is the thing to avoid - A tick has **50 ms**, and the tick thread does *all* the work of the server's tick: networking glue, entities, blocks, your handlers. There is no spare thread to absorb a pause. - .NET uses a **generational GC**. Short-lived garbage is cheap to collect, but every allocation on a path that runs thousands of times per tick (movement, damage, block updates, chat of a busy server) adds up to collections whose pauses land **in the tick**. Concurrent GC is on, which helps, but a gen0 collection still stops the world briefly. - Your plugin runs in the worker process, and the worker's own garbage is collected there, not in the server. But a handler that allocates still makes the worker's tick slower, and the server waits for the worker's answer on awaited events (see [threading](/concepts/threading/)). ## The rules 1. **Hot-path handlers must not allocate.** Hot = anything that fires per tick, per move packet, per block or per entity: `PlayerMoveEvent`, `TickStartEvent` / `TickEndEvent`, `EntityDamageEvent`, `BlockPlaceEvent` / `BlockBreakEvent` on a busy world, systems, scheduled tasks that repeat often. 2. **No LINQ, no `string` building, no `$"..."` interpolation, no `params` calls, no boxing, no new lambdas** on those paths. A lambda that captures variables allocates a closure each time it is created: create delegates once (in `OnEnable`, or as `static` lambdas) and reuse them. 3. **Read `Utf8String` in place.** Compare bytes (`EqualsAscii("..."u8)`) and only call `ToString()` when you really need a managed string (and then once, not per event). 4. **Allocate once, reuse**: `static readonly` tables, pooled arrays (`ArrayPool`), `Span` / `stackalloc` for small temporaries, `StringBuilder` fields cleared between uses. 5. **Use queries for bulk work.** A per-entity `Get` / `Set` in a loop costs a call into the engine each time (about 6.5 ns for `Get` in-process). Iterate query chunks instead: about 2.7 ns per entity in-process. In the worker, a read of an engine component is a round trip to the engine, so batch them with queries. 6. **Subscribe to what you need.** `PlayerMoveEvent` fires per move packet; do not subscribe to it for a feature that needs it rarely. Unsubscribe (`Subscription.Dispose`) when a feature is turned off. If you only care about coarse movement, raise `Server.PlayerMoveThreshold`: at 0, the default, a moving player produces about 20 events a second. 7. **Check cheaply, then work**: `if (!HasWork) return;` first; `Events.HasSubscribers()` before building an event that costs something; compare the cheap field before the expensive one. In the worker, `HasSubscribers` only sees your plugin's own listeners, not those of the engine or other plugins, so do not use it to skip firing an event that someone else may handle. 8. **Do heavy work off the tick thread** and come back with `Scheduler.Post` or `RunSync` (see [threading](/concepts/threading/)); never block. 9. **Do not allocate in `foreach` over interfaces**: iterate arrays and `List` (struct enumerators), not `IEnumerable`. ```csharp // Bad on a hot path: allocates a list, a closure and an iterator on every move Events.On((ref PlayerMoveEvent e) => { var nearby = _zones.Where(z => z.Contains(e.To)).ToList(); Log.Debug($"{e.Player.Name} moved to {e.To}"); }); // Good: no allocation, no work unless the player left the zone grid cell private static readonly Zone[] s_zones = ...; private void OnMove(ref PlayerMoveEvent e) { foreach (ref readonly var z in s_zones.AsSpan()) // Span enumeration: no allocation if (z.Contains(e.To)) { /* ... */ } } // in OnEnable: Events.On(OnMove); // one delegate, created once ``` (`Events.On(OnMove)` with a method group allocates its delegate once, at registration.) ## What things cost The figures below were measured on the reference machine (GCC 16 -O3, .NET 10.0.5, Windows 11) with the scripting runtime's call-overhead test. They are for the **in-process** runtime, the plumbing between native code and managed code, and they vary with machine load. | Path | Cost | |---|---| | fire an event, no subscribers | about 0.4 ns | | fire, one native handler | about 4-8 ns | | native to managed noop call | about 3.3 ns | | native fire to one C# handler (ref struct, mutate a field) | about 22 ns, with the thread checks and error bookkeeping included | | managed to native call (e.g. `EntityAlive`) | about 3.4 ns | | C# `entity.Get()` | about 6.5 ns | | C# `EventBus.Fire` to a C# handler | about 12-15 ns | | C# chunk query pass, 10 000 entities, one component | about 2.7 ns per entity | The numbers are for the plumbing. They say a handler that does nothing is free, and that what you do inside it is what you pay for. ## Costs in the plugin worker Plugins run in the worker process, so a few costs are different from the table above: - **Events are copies.** The server encodes each event for the worker, and your handler gets a decoded copy, not a view of the engine's own struct. Changes to mutable fields are sent back with the answer. - **Every host call is a round trip** between the two processes, inside the tick. No per-call figure is published for the worker, so measure your own plugin. - **Observed events are batched.** An event nobody needs to answer is queued and sent with the rest of the tick in one message. Awaited events (cancellable or with mutable fields) wait for your answer, up to `scripting.ipc.event_budget_us` (5 ms by default). See [threading](/concepts/threading/). - **Plugin-defined components stay in the worker**, so reading them does not cross the process boundary. Engine components are read through the engine, one request at a time. - **Logging is rate-limited**: 200 lines a second by default (`scripting.ipc.log_per_second`). Lines past that are dropped, so do not log per event. ## Measure your own plugin - To check a handler for allocations in a test or while developing, wrap it: ```csharp long before = GC.GetAllocatedBytesForCurrentThread(); Handle(ref e); long allocated = GC.GetAllocatedBytesForCurrentThread() - before; // should be 0 ``` - `/tick query` prints the vanilla-style tick timing, so you can compare the server with and without your plugin. - The engine's profiler charges each plugin's time to its own group, `plugin:`, split into events, commands, tasks, reloads and systems. Look there first when the tick gets slower. - Wrap heavy logic in a system so it shows up by name. In-process each system has its own row; in the worker a plugin's systems are one row for the plugin. --- # Your first plugin Page: /guides/first-plugin/ A complete plugin with a component, three events, a system, a scheduled task and a command, built, run and hot-reloaded on a scratch server. You will build **VisitTracker**: it counts how often each player joined (as a component, so the counts survive hot reloads), blocks chat messages with banned words, answers `/visits`, and logs a summary every minute. The code blocks marked `compile` are built against the real API by the docs build (`npm run verify:snippets`). ## 1. Create the project ```bash dotnet new ramjet-plugin -n VisitTracker cd VisitTracker ``` (Set `RAMJET_PLUGINS` as described in [create a plugin](/getting-started/create-a-plugin/).) Replace the generated `PluginMain.cs` with this: **PluginMain.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Commands; namespace VisitTracker; /// Per-player data stored in the ECS: a plain unmanaged struct. Values survive hot reloads. public struct Visits { public int Count; public long LastJoinTick; } [PluginInfo("VisitTracker", Version = "1.0.0", Author = "you", Description = "Counts joins, filters chat, answers /visits")] public sealed class PluginMain : Plugin { private static readonly string[] BlockedWords = ["spam", "free-diamonds"]; private Query _visits = null!; private long _tick; private int _joinsSinceEnable; protected override void OnEnable() { // 1. A component: register the type under a global key, create a query over it once. Ecs.RegisterComponent("visittracker:visits"); _visits = Ecs.Query(); // 2. Join: add or update the component on the player's entity. Events.On((ref PlayerJoinEvent e) => { Entity player = e.Player; // Player has Get/Set/TryGet; GetOrAdd lives on Entity ref var visits = ref player.GetOrAdd(); // a ref into ECS storage: use it right away visits.Count++; visits.LastJoinTick = _tick; _joinsSinceEnable++; Log.Info($"{e.Username.ToString()} joined (visit #{visits.Count})"); // joins are rare: allocating is fine }); // 3. Chat: cancel messages with a banned word. High priority runs after Normal handlers. Events.On((ref PlayerChatEvent e) => { foreach (string word in BlockedWords) { if (e.Message.Contains(word, StringComparison.OrdinalIgnoreCase)) { e.Cancelled = true; return; } } }, EventPriority.High); // 4. A slash command: the name is matched for you, the body runs for the player who typed it. Commands.Register(Command.Literal("visits") .Executes(ctx => { Player player = ctx.RequirePlayer(); // the console has no visits: fails with a message int count = player.TryGet(out var visits) ? visits.Count : 0; ctx.Reply($"You have joined {count} time(s) since tracking started."); return 1; }), description: "Show how often you joined"); // 5. A system: runs every tick. Here it only counts ticks, so we can timestamp joins. Systems.Add("tick_counter", Phase.PreTick, _ => _tick++); // 6. A repeating task: once a minute (20 ticks = 1 second). Scheduler.RunEvery(20 * 60, () => Log.Info($"{_visits.Count()} tracked player(s), {_joinsSinceEnable} joins since this version was enabled")); Log.Info("enabled"); } protected override void OnDisable() => Log.Info("disabled"); } ``` ## 2. Build and run ```bash dotnet build -c Release ``` Start a scratch server (see [build and hot reload](/getting-started/build-and-hot-reload/#test-against-a-scratch-server)) and watch the log. Loading the plugin prints: ``` [INFO] [VisitTracker] enabled ``` Join with a bot from the repository's headless load-test client (the same section explains how to start it) to see the event handler run: ``` [INFO] [VisitTracker] Bot0 joined (visit #1) ``` Type `/visits` in a game client connected to the server and the reply is the count from the component. ## 3. What each part does - **`Ecs.RegisterComponent("visittracker:visits")`** binds your struct to a global key and creates the flecs component. The struct must be unmanaged (no strings or arrays) and owned by this plugin. See [entities and the ECS](/concepts/entities-and-ecs/). - **`player.GetOrAdd()`** (on the `Entity` behind the player) returns a `ref` into storage. It is only valid until the next structural change, so it is used immediately and never stored. `Player` itself offers `Get`, `TryGet`, `Has`, `Set` and `Remove`. - **`Events.On((ref PlayerChatEvent e) => ...)`**: events are structs passed by `ref`. `e.Cancelled = true` is how a handler vetoes. `EventPriority.High` runs after `Normal`. A handler does not run for an event that an earlier handler cancelled, unless you pass `receiveCancelled: true`. - **`e.Username.ToString()`**: the event's strings are borrowed `Utf8String` values. Call `ToString()` before you interpolate one (see [Utf8String](/reference/api/primitives/utf8string/)). `e.Message.Contains(...)` works on it directly. - **`Commands.Register(Command.Literal("visits")...)`** registers a command node in the server's command tree. `ctx.Reply` answers the player who typed it. See [commands](/concepts/commands/), and [commands with arguments](/guides/commands-with-arguments/) for typed arguments. - **`Systems.Add("tick_counter", Phase.PreTick, ...)`** registers a system in the engine's pipeline as `VisitTracker:tick_counter`. See [systems](/concepts/systems/). - **`Scheduler.RunEvery`** schedules the log line; it is cancelled automatically on unload. See [scheduler](/concepts/scheduler/). - **`Log`** prefixes every line with the plugin name. ## 4. Hot reload it Keep a bot connected for a while, then, with the server still running, change `Log.Info("enabled")` to say something else (for example `"enabled (v2)"`) and run `dotnet build -c Release` again. The log shows the new version enabled and the old one disabled: ``` [INFO] hot reloading VisitTracker.dll [INFO] [VisitTracker] enabled (v2) [INFO] [VisitTracker] disabled ``` When the next minute log line comes, it reads: ``` [INFO] [VisitTracker] 1 tracked player(s), 0 joins since this version was enabled ``` The player who joined before the reload is still tracked: the `Visits` component sits on its entity in the ECS and its layout did not change. `_joinsSinceEnable` is back to 0, because it is a field of the plugin object, which is a new instance after every reload. That difference, *state in components survives, state in fields does not*, is the most important thing to internalise about [hot reload](/concepts/hot-reload/). (A player who disconnects loses their entity and the component with it; to keep counts across sessions, write them to disk keyed by the player's UUID, see [config and data](/concepts/config-and-data/).) ## 5. Try next - Make the banned words configurable: [config and data](/concepts/config-and-data/). - Add arguments and tab completion: [commands with arguments](/guides/commands-with-arguments/). - A bigger plugin with config, permissions and commands: the [homes plugin](/guides/homes-plugin/). - Coming from Paper? The [migration table](/guides/migrating-from-paper/) maps every concept. --- # Commands with arguments and suggestions Page: /guides/commands-with-arguments/ A /calc command written twice. First by parsing the command line from PlayerCommandEvent, then as a typed Brigadier tree with suggestions and a permission. We build `/calc <+|-|*|/> `. Part 1 parses the raw command line yourself, from `PlayerCommandEvent`. Part 2 is the same command as a node of the server's Brigadier command tree, with typed arguments, tab completion and a permission. Use Part 2 for your own commands; Part 1 shows what the engine hands you underneath. ## Part 1: parse the line yourself The engine fires `PlayerCommandEvent` for every command a player types (the line without the leading slash). A small router turns that into registered commands with split arguments and a usage message. It is the lowest-level way to see a command, and it has no tab completion or permission integration. **CommandRouter.cs** ```csharp using Mc.Scripting; namespace Calc; /// Handler of one command. are the words after the command name. public delegate void CommandAction(Entity sender, ReadOnlySpan args); /// Routes player commands (PlayerCommandEvent) to registered handlers. public sealed class CommandRouter { private readonly Dictionary _commands = new(StringComparer.OrdinalIgnoreCase); public void Add(string name, CommandAction action) => _commands[name] = action; public void Attach(EventRegistry events) { events.On((ref PlayerCommandEvent e) => { // commands are rare compared to ticks: one string and one split per command is fine string[] words = e.Command.ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries); if (words.Length == 0 || !_commands.TryGetValue(words[0], out var action)) return; // not ours: leave it alone e.Cancelled = true; // handled: the engine must not answer "Unknown command" action(e.Player, words.AsSpan(1)); }); } } ``` **PluginMain.cs** ```csharp using System.Globalization; using Mc.Scripting; namespace Calc; [PluginInfo("Calc", Version = "1.0.0")] public sealed class PluginMain : Plugin { private readonly CommandRouter _router = new(); protected override void OnEnable() { _router.Add("calc", Calc); _router.Attach(Events); } private static void Calc(Entity sender, ReadOnlySpan args) { if (args.Length != 3 || !double.TryParse(args[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double a) || !double.TryParse(args[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double b) || args[1].Length != 1) { Server.SendMessage(sender, "usage: /calc <+|-|*|/> "); return; } double? result = args[1][0] switch { '+' => a + b, '-' => a - b, '*' => a * b, '/' when b != 0 => a / b, _ => null, }; Server.SendMessage(sender, result is { } r ? $"{a} {args[1]} {b} = {r.ToString(CultureInfo.InvariantCulture)}" : "cannot calculate that (unknown operator or division by zero)"); } } ``` What you do not get here: no tab completion, no typed argument checks in the client, no permission integration. `Server.SendMessage` sends the text to the player (`Entity.Null` sends it to the console). Part 2 adds the rest. ## Part 2: a typed Brigadier tree This is the code to use in place of Part 1. Save it as `PluginMain.cs` in your project and delete `CommandRouter.cs`. The namespace is `Calc.Tree` only so that the docs build can compile both versions side by side. **PluginMainTree.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Commands; namespace Calc.Tree; public sealed class PluginMain : Plugin { private static readonly string[] Operators = ["+", "-", "*", "/"]; protected override void OnEnable() { Commands.Register( Command.Literal("calc") .Permission("calc.use") // hides the node and blocks it .Then(Command.Argument("a", Arg.Double()) .Then(Command.Argument("op", Arg.Word()) .Suggests(ctx => Operators) // tab completion .Then(Command.Argument("b", Arg.Double()) .Executes(Calculate)))), description: "Calculate with two numbers"); } private static int Calculate(CommandContext ctx) { double a = ctx.GetDouble("a"), b = ctx.GetDouble("b"); string op = ctx.GetString("op"); double result = op switch { "+" => a + b, "-" => a - b, "*" => a * b, "/" when b != 0 => a / b, "/" => throw new CommandFailure("Division by zero"), // red message, result 0, no log _ => throw new CommandFailure($"Unknown operator '{op}'"), }; ctx.Reply($"{a} {op} {b} = {result}"); return (int)result; // the command result (usable by /execute store) } } ``` What changed compared to Part 1: - The tree is sent to the client: `/calc ` shows `` in the usage hint, number arguments are validated by the client and server (`Arg.Double()` rejects `abc` before your code runs), `op` completes to `+ - * /`. - `.Permission("calc.use")` makes the command invisible and non-executable for players without the node. The console always passes ([permissions](/concepts/permissions/)). - Errors are thrown as `CommandFailure`: the player sees red text, the result is 0 and nothing is logged. Any other exception shows "An unexpected error occurred" to the player and logs a stack trace. - The return value is the command's result, exactly like vanilla's (the number `/execute store result` captures). - Unloading the plugin removes the nodes, and the command trees of the online players are sent again at the end of the tick. ### Subcommands, optional arguments and aliases This fragment shows the shape of a larger tree. `Teleport`, `SetHome` and `HomeNames` are your own methods: ```csharp Commands.Register( Command.Literal("home") .Executes(ctx => Teleport(ctx.RequirePlayer(), "home")) // /home .Then(Command.Argument("name", Arg.Word()) // /home .Suggests(ctx => HomeNames(ctx.Player)) .Executes(ctx => Teleport(ctx.RequirePlayer(), ctx.GetString("name")))) .Then(Command.Literal("set") // /home set .Then(Command.Argument("name", Arg.Word()).Executes(SetHome))), aliases: ["h"], description: "Teleport to a home"); ``` An argument node with an executor is an optional last argument; a literal child is a subcommand. See the [homes guide](/guides/homes-plugin/) for the complete plugin and the [command argument types](/reference/command-arguments/) for everything `Arg` can produce. --- # A homes plugin with config and permissions Page: /guides/homes-plugin/ A complete /home /sethome /delhome /homes plugin with typed config, permissions, a data file, tab completion and a cooldown. Plugin: **Homes**. Players save named homes and teleport back to them. | Command | Does | Permission | |---|---|---| | `/sethome [name]` | saves the current position as `name` (default `home`) | `homes.set` | | `/home [name]` | teleports to a home, with a cooldown | `homes.use` | | `/delhome ` | deletes a home | `homes.set` | | `/homes` | lists your homes | `homes.use` | Limit: `MaxHomes` per player from config; the permission `homes.unlimited` (default: operators) lifts it. ## Manifest **plugin.json** ```json { "id": "homes", "name": "Homes", "version": "1.0.0", "api": "1.0", "main": "Homes.PluginMain", "authors": ["you"], "description": "Set and teleport to homes.", "permissions": [ { "node": "homes.use", "default": "true", "description": "Use /home and /homes" }, { "node": "homes.set", "default": "true", "description": "Use /sethome and /delhome" }, { "node": "homes.unlimited", "default": "op", "description": "No limit on the number of homes" } ] } ``` The loader registers the manifest's permissions with their defaults and descriptions before `OnEnable` runs, so a permissions plugin can list and grant them from the start. ## Config and data types **HomesConfig.cs** ```csharp using System.Text.Json.Serialization; namespace Homes; /// plugins/homes/config.json: written with these defaults on first start, comments allowed. public sealed class HomesConfig { public int MaxHomes { get; set; } = 3; public int CooldownSeconds { get; set; } = 5; } /// One saved location. public sealed class Home { public string World { get; set; } = "minecraft:overworld"; public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public float Yaw { get; set; } public float Pitch { get; set; } } /// homes.json: player uuid to home name to home. public sealed class HomesData { public Dictionary> Players { get; set; } = []; } // System.Text.Json source generation: no reflection at run time [JsonSerializable(typeof(HomesConfig))] [JsonSerializable(typeof(HomesData))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class HomesJson : JsonSerializerContext { } ``` ## The plugin **PluginMain.cs** ```csharp using System.Text.Json; namespace Homes; public sealed class PluginMain : Plugin { private Config _config = null!; private HomesData _data = new(); private readonly Dictionary _lastTeleport = new(); // Environment.TickCount64 values (milliseconds) protected override void OnEnable() { _config = Config.Load(HomesJson.Default.HomesConfig); // creates config.json with the defaults _data = LoadData(); Commands.Register(Command.Literal("sethome") .Permission("homes.set") .Executes(ctx => SetHome(ctx, "home")) .Then(Command.Argument("name", Arg.Word()) .Executes(ctx => SetHome(ctx, ctx.GetString("name")))), description: "Save your position as a home"); Commands.Register(Command.Literal("home") .Permission("homes.use") .Executes(ctx => GoHome(ctx, "home")) .Then(Command.Argument("name", Arg.Word()) .Suggests(OwnHomes) // tab completion of the player's own homes .Executes(ctx => GoHome(ctx, ctx.GetString("name")))), aliases: ["h"], description: "Teleport to a home"); Commands.Register(Command.Literal("delhome") .Permission("homes.set") .Then(Command.Argument("name", Arg.Word()) .Suggests(OwnHomes) .Executes(DeleteHome)), description: "Delete a home"); Commands.Register(Command.Literal("homes") .Permission("homes.use") .Executes(ListHomes), description: "List your homes"); // a player who leaves starts with a fresh cooldown next time Events.On((ref PlayerQuitEvent e) => _lastTeleport.Remove(e.Uuid)); Log.Info($"enabled, limit {_config.Value.MaxHomes} homes"); } // ---- commands ----------------------------------------------------------------------------------------- private int SetHome(CommandContext ctx, string name) { Player player = ctx.RequirePlayer(); var homes = HomesOf(player, create: true)!; bool exists = homes.ContainsKey(name); int limit = _config.Value.MaxHomes; if (!exists && homes.Count >= limit && !player.HasPermission("homes.unlimited")) throw new CommandFailure($"You already have {homes.Count} homes (limit {limit}). Delete one with /delhome."); Location at = player.Location; homes[name] = new Home { World = at.World.Key, X = at.X, Y = at.Y, Z = at.Z, Yaw = at.Yaw, Pitch = at.Pitch }; Save(); ctx.Reply(exists ? $"Home '{name}' moved." : $"Home '{name}' saved."); return homes.Count; } private int GoHome(CommandContext ctx, string name) { Player player = ctx.RequirePlayer(); var homes = HomesOf(player, create: false); if (homes is null || !homes.TryGetValue(name, out var home)) throw new CommandFailure($"You have no home called '{name}'. Use /homes to list them."); long now = Environment.TickCount64; long cooldownMs = _config.Value.CooldownSeconds * 1000L; if (_lastTeleport.TryGetValue(player.Uuid, out long last) && now - last < cooldownMs) throw new CommandFailure($"Wait {(cooldownMs - (now - last) + 999) / 1000} more seconds."); World world = Server.GetWorld(home.World) ?? throw new CommandFailure($"The world '{home.World}' is not loaded."); // a Location carries its dimension: Teleport also moves the player to another world if (!player.Teleport(new Location(world, home.X, home.Y, home.Z, home.Yaw, home.Pitch))) throw new CommandFailure("The teleport was cancelled."); _lastTeleport[player.Uuid] = now; ctx.Reply($"Welcome home, '{name}'."); return 1; } private int DeleteHome(CommandContext ctx) { Player player = ctx.RequirePlayer(); string name = ctx.GetString("name"); var homes = HomesOf(player, create: false); if (homes is null || !homes.Remove(name)) throw new CommandFailure($"You have no home called '{name}'."); Save(); ctx.Reply($"Home '{name}' deleted."); return 1; } private int ListHomes(CommandContext ctx) { Player player = ctx.RequirePlayer(); var homes = HomesOf(player, create: false); if (homes is null || homes.Count == 0) throw new CommandFailure("You have no homes. Use /sethome."); ctx.Reply($"Homes ({homes.Count}): {string.Join(", ", homes.Keys.Order())}"); return homes.Count; } private IEnumerable OwnHomes(CommandContext ctx) => ctx.Player is { } p && HomesOf(p, create: false) is { } homes ? homes.Keys.Order() : []; // ---- data ----------------------------------------------------------------------------------------------- private Dictionary? HomesOf(Player player, bool create) { string key = player.Uuid.ToString(); if (_data.Players.TryGetValue(key, out var homes)) return homes; if (!create) return null; homes = new Dictionary(StringComparer.OrdinalIgnoreCase); _data.Players[key] = homes; return homes; } private HomesData LoadData() { string? json = Config.ReadText("homes.json"); if (json is null) return new HomesData(); try { var data = JsonSerializer.Deserialize(json, HomesJson.Default.HomesData) ?? new HomesData(); // home names compare without regard to case: /home Base and /home base are the same home foreach (var key in data.Players.Keys.ToList()) data.Players[key] = new Dictionary(data.Players[key], StringComparer.OrdinalIgnoreCase); return data; } catch (JsonException e) { string path = Path.Combine(DataFolder, "homes.json"); File.Move(path, path + ".bad", overwrite: true); // keep the damaged file for the admin Log.Error("homes.json is damaged, starting with no homes (the file is kept as homes.json.bad)", e); return new HomesData(); } } private void Save() => Config.WriteText("homes.json", JsonSerializer.Serialize(_data, HomesJson.Default.HomesData)); // atomic: temp file + rename } ``` ## The command tree This is the tree the four commands describe (nodes in italics are arguments; the orange chips are the permission each node requires). Switch to *usage lines* for the forms players see. _(An interactive CommandTree component is on the site page /guides/homes-plugin/.)_ ## How it behaves - **Permissions**: with no permissions plugin, defaults apply: everyone can use `/home` and `/sethome`; only operators have `homes.unlimited`. A permissions backend can grant or deny any node per player or group, and the commands appear or disappear in the client's command tree accordingly. - **Config**: edit `plugins/homes/config.json`. Call `_config.Reload()` (or set `_config.Watch = true` in `OnEnable`) to pick up edits without a restart; a parse error keeps the previous values and logs the line. Values are read through `_config.Value` at the time of use. - **Hot reload**: rebuild the plugin and the commands and permissions are registered again by the new `OnEnable`; the homes are re-read from disk; the cooldown dictionary starts empty (it is not worth persisting). Nothing is lost because `Save()` writes through on every change. - **Why milliseconds for the cooldown**: `Environment.TickCount64` measures wall-clock time, so the cooldown lasts the configured seconds even when the server runs behind. Use `Server.CurrentTick` instead if the cooldown should follow the server's tick rate. - **Why `Dictionary` keyed by the UUID string**: JSON object keys are strings; the UUID is the stable identity of a player (names change). ## Next - Add `/sethome` confirmation as a boss bar or title: [boss bars and titles](/guides/boss-bars-and-titles/). - Show homes in a sidebar: [scoreboard sidebar](/guides/scoreboard-sidebar/). - Expose `GetHomes(player)` to other plugins: [inter-plugin services](/guides/inter-plugin-services/). --- # A scoreboard sidebar Page: /guides/scoreboard-sidebar/ Show live lines in the sidebar with Sidebars and Sidebar, shared by everyone or one per player. The sidebar is the right-hand scoreboard. In vanilla you drive it with a scoreboard objective and `/scoreboard objectives setdisplay sidebar`. The API hides that: a `Sidebar` is a **plugin-owned objective** sent only to its viewers, shown **per viewer**, and removed automatically when your plugin unloads. Different players can see different lines at the same time. ## The API `Sidebars` is a property of your plugin: `Sidebars.Create(...)` makes a sidebar. | Member | Meaning | |---|---| | `Sidebars.Create(Component title)` | creates a sidebar nobody sees yet | | `sidebar.Title` | get/set the title | | `sidebar.SetLine(int index, Component? text)` | sets or replaces a line; `null` removes it | | `sidebar.SetLines(IReadOnlyList lines)` | replaces all lines at once | | `sidebar.Clear()` | removes every line | | `sidebar.Show(Player)` / `sidebar.Hide(Player)` | per-viewer visibility | | `sidebar.IsShownTo(Player)` / `sidebar.Viewers` | who sees it | | `sidebar.Remove()` | hides it from everybody and releases it | Text is a `Component`: a plain `string` converts to **literal** text, so a player name can never inject formatting. For colours use `Component.Legacy("&aGreen")` or `Component.Parse("Green")`, or build one with `Component.Text("Green").Color(TextColor.Green)`. A `Sidebar` has one set of lines, shown to every player you `Show` it to. Use one shared sidebar for global information and one sidebar per player for personal information. ## A shared sidebar Shows the player count and the server tick, refreshed once a second. **SharedBoard.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Text; namespace StatsBoard; public sealed class PluginMain : Plugin { private Sidebar _board = null!; protected override void OnEnable() { _board = Sidebars.Create(Component.Text("My Server").Color(TextColor.Gold).Bold()); foreach (Player player in Server.Players) _board.Show(player); Events.On((ref PlayerJoinEvent e) => _board.Show(e.Player)); Scheduler.RunEvery(20, () => { _board.SetLine(2, Component.Legacy($"&7Online: &f{Server.PlayerCount}")); _board.SetLine(1, Component.Legacy($"&7Tick: &f{Server.CurrentTick}")); }); } } ``` ## One sidebar per player For text that differs per viewer, keep one `Sidebar` per player. The name is written as literal text, not as a legacy format string, so it cannot inject colour codes. **PersonalBoards.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Text; namespace PersonalBoards; public sealed class PluginMain : Plugin { private readonly Dictionary _boards = new(); protected override void OnEnable() { Events.On((ref PlayerJoinEvent e) => { Sidebar board = Sidebars.Create(Component.Text(e.Username.ToString()).Color(TextColor.Aqua)); board.Show(e.Player); _boards[e.Uuid] = board; }); Events.On((ref PlayerQuitEvent e) => { if (_boards.Remove(e.Uuid, out Sidebar? board)) board.Remove(); }); Scheduler.RunEvery(20, () => { foreach (Player p in Server.Players) { if (!_boards.TryGetValue(p.Uuid, out Sidebar? board)) continue; board.SetLine(2, Component.Legacy($"&7Health: &c{p.Health:0.#}")); board.SetLine(1, Component.Legacy($"&7XP level: &a{p.ExperienceLevel}")); } }); } } ``` ## Notes - **Lifetime**: a sidebar is owned by your plugin; on unload or hot reload it is hidden from every viewer, so a reloaded plugin starts from a clean scoreboard and re-creates its sidebars in `OnEnable`. - **Performance**: `SetLine` sends packets to the viewers of that sidebar, and it does nothing when the text is unchanged. Setting the same text every tick costs only a comparison. Spread large numbers of per-player sidebars over several ticks if needed. - **Limits**: a sidebar shows at most `Sidebar.MaxLines` (15) lines. An index outside 0 to 14 throws `ArgumentOutOfRangeException`. - **Layering with the server sidebar**: a plugin sidebar shown to a player covers the server's own sidebar on that player's screen. Hiding it shows the next plugin sidebar, or the server's one. - **Server-wide scores**: `Scoreboard.CreateObjective(...)` makes a server objective that every player sees and that is saved with the world, and `Scoreboard.SetDisplay(DisplaySlot.Sidebar, objective)` puts it in the sidebar slot. Use those for shared scores that `/scoreboard` also edits, and `Sidebar` for per-player content. - **Players who leave**: a handle goes stale when the player leaves (`IsOnline` is false). Dropping the dictionary entry and removing the sidebar on `PlayerQuitEvent` is enough. --- # Boss bars, titles and action bars Page: /guides/boss-bars-and-titles/ Show a countdown boss bar, a welcome title and an action bar message. ## Titles and action bars Titles and action bars are **fire and forget**: nothing to create, nothing to release. **Welcome.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Text; namespace Welcome; public sealed class PluginMain : Plugin { protected override void OnEnable() { Events.On((ref PlayerJoinEvent e) => { e.Player.ShowTitle( Component.Text("Welcome").Color(TextColor.Gold), Component.Text("to the server").Color(TextColor.Gray), new TitleTimes(FadeIn: 10, Stay: 60, FadeOut: 20)); // ticks: 20 = 1 second e.Player.SendActionBar(Component.Legacy("&aType /help to get started")); }); } } ``` - `ShowTitle(title, subtitle, times)` takes a `Component` for each line. The subtitle is optional (`null` for none). `times` is a `TitleTimes` in ticks (fade in, stay, fade out); `null` keeps the client's current timings, which default to 10, 70 and 20. `TitleTimes.From(fadeIn, stay, fadeOut)` takes `TimeSpan` values instead. - `ShowSubtitle(subtitle)` sets a subtitle on its own; it appears with the next title. - `player.ClearTitle()` hides a title that is showing. `ClearTitle(resetTimes: true)` also restores the default timings. - The action bar is the single line above the hotbar. Sending again replaces it; it fades by itself after a few seconds, so resend it from a repeating task if you want it to stay. - Text is a `Component`. A plain `string` is literal text; use `Component.Legacy("&a...")` or `Component.Parse("...")` explicitly for formatting, so user-controlled strings never inject formatting. ## Boss bars A `BossBar` is an **owned resource**: create it with `BossBars.Create(...)` on your plugin. It is removed from every viewer when you call `Remove()` (or `Dispose()`) and when your plugin unloads. | Member | Meaning | |---|---| | `BossBars.Create(string name, Component title, BossBarColor color, BossBarStyle style, float progress)` | creates a bar with no viewers yet; `name` is unique within the plugin (letters, digits, `_ - . /`) | | `bar.Progress` | 0.0 to 1.0 (values outside are clamped) | | `bar.Title`, `bar.Color`, `bar.Style` | change while it is shown | | `bar.Flags` | `BossBarFlags`: darken the sky, play boss music, create fog | | `bar.Visible` | hide and show the bar without losing its viewers | | `bar.AddViewer(Player)` / `bar.RemoveViewer(Player)` | who sees it (both also take a profile id, `Uuid`) | | `bar.Remove()` | removes it from every viewer | `BossBarColor` is `Pink`, `Blue`, `Red`, `Green`, `Yellow`, `Purple` or `White`. `BossBarStyle` is `Solid`, `Segmented6`, `Segmented10`, `Segmented12` or `Segmented20`. ### A countdown A repeating task shrinks the bar; at zero the bar is removed and a message is sent. `/countdown` is a Brigadier command; see [commands with arguments](/guides/commands-with-arguments/). **Countdown.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Text; namespace Countdown; public sealed class PluginMain : Plugin { private BossBar? _bar; private ScheduledTask? _task; protected override void OnEnable() { Commands.Register(Command.Literal("countdown") .Permission("countdown.use") .Then(Command.Argument("seconds", Arg.Int(1, 3600)) .Executes(ctx => Start(ctx.GetInt("seconds"))))); Events.On((ref PlayerJoinEvent e) => _bar?.AddViewer(e.Player)); } private int Start(int seconds) { Stop(); int total = seconds * 20, left = total; BossBar bar = BossBars.Create("countdown", Component.Legacy($"&eRestart in {seconds}s"), BossBarColor.Yellow, BossBarStyle.Solid, 1f); foreach (Player p in Server.Players) bar.AddViewer(p); _bar = bar; _task = Scheduler.RunEvery(20, () => { left -= 20; if (left <= 0) { Server.Broadcast("Time is up!"); Stop(); return; } bar.Progress = (float)left / total; bar.Title = Component.Legacy($"&eRestart in {left / 20}s"); if (left <= 100) bar.Color = BossBarColor.Red; }); return seconds; } private void Stop() { _task?.Cancel(); _bar?.Remove(); _task = null; _bar = null; } protected override void OnDisable() => Stop(); // not required: unload removes the bar and cancels the task } ``` ### Notes - **Viewers who join later**: a viewer is kept by profile id, and `AddViewer` also accepts the id of an offline player (the bar appears when he joins). The countdown adds joining players on `PlayerJoinEvent`, so new players see the bar. - **One bar per purpose**: boss bars stack at the top of the screen (the client shows several). Reuse one bar and change its title and progress rather than creating a bar per update. `BossBars.Create` throws `InvalidOperationException` when the name is already in use, so `Remove()` the old bar first. - **Updating** `Progress`, `Title` or `Color` sends a small packet to the viewers; set them only when the value changes. - **Persistence**: bars are not saved with the world. - **Hot reload**: a new version that creates a bar with the same name takes over the old one, with its viewers. A bar the new version does not create is removed when the old version unloads. --- # Custom GUIs (menus) Page: /guides/custom-guis/ Chest-style menus with clickable icons: a warp selector with click and close handlers, and the rules that keep a menu locked. ## The model A menu is a server-authoritative chest-style GUI owned by the plugin. Every plugin has `Menus` (`plugin.Menus`) to create one: `Chest(rows, title)` for one to six rows of nine slots, `Hopper(title)` for five slots and `Dispenser(title)` for a 3 x 3 grid. The menu is a display: a click never moves an item in or out unless your handler allows it. | Type | What it is for | |---|---| | `Menus` | creates menus owned by your plugin: `Chest`, `Hopper`, `Dispenser`, `Create(type, title, rows)` | | `Menu` | the GUI: `Slot(slot, icon, onClick)`, `FillEmpty`, `OnClick`, `OnClose`, `Open(player)`, `Close(player)`, `Viewers`, `Title`, `Refresh()`, `Dispose()` | | `MenuClick` | one click: `Player`, `Slot`, `Type` (a `MenuClickType`), `Clicked`, `Cursor`, `Allow()`, `Cancel()`, `Close()` | | `MenuCloseReason` | why a menu closed: `Player`, `Plugin`, `Unload`, `Quit`, `Replaced`, `Invalid` | | `ItemStack` | an immutable item: `new ItemStack("minecraft:diamond_sword", 1)`, with `WithName`, `WithLore`, `WithEnchantment`, `WithCount` and more | Rules: - A click on one of the menu's own slots is **cancelled by default**. `MenuClick.Allow()` lets it through, so the player can take the stack out or put one in, just as in a chest. `Cancel()` denies it for good. - A click that only touches the player's own inventory passes unless a handler cancels it. Set `Menu.LockPlayerInventory` to deny those too. - Handlers run on the tick thread, like every other callback. - A menu is **closed for all viewers** when its plugin unloads or reloads, and its handlers are dropped. - `ItemStack` is a value: `WithName(...)` returns a new stack. ## A warp selector A menu with one icon per warp. Each icon's click handler teleports the player. **Warps.cs** ```csharp using Mc.Scripting.Inventories; using Mc.Scripting.Text; namespace Warps; public sealed class PluginMain : Plugin { private Menu _menu = null!; protected override void OnEnable() { _menu = Menus.Chest(3, Component.Parse("Warps")) .FillEmpty(new ItemStack("minecraft:gray_stained_glass_pane").WithName(Component.Text(" "))) .Slot(11, new ItemStack("minecraft:grass_block").WithName(Component.Legacy("&aSpawn")), click => Go(click.Player, "minecraft:overworld", 0, 80, 0)) .Slot(13, new ItemStack("minecraft:netherrack").WithName(Component.Legacy("&cNether hub")), click => Go(click.Player, "minecraft:the_nether", 0, 70, 0)) .Slot(15, new ItemStack("minecraft:end_stone").WithName(Component.Legacy("&5The End")), click => Go(click.Player, "minecraft:the_end", 100, 50, 0)) .OnClose(player => Log.Debug($"{player.Name} closed the warp menu")); Commands.Register(Command.Literal("warps") .Executes(ctx => { _menu.Open(ctx.RequirePlayer()); return 1; }), description: "Open the warp menu"); } private static void Go(Player player, string world, double x, double y, double z) { if (Server.GetWorld(world) is not { } target) return; // the dimension is not loaded player.Teleport(new Location(target, x, y, z)); } } ``` The menu does not depend on the command: call `Open` from any event handler and it works the same way. `Open` returns `false` when a handler cancelled the opening or the player is offline. `Close(player)` closes the menu for one viewer, and `Close()` closes it for everybody. ## Handling clicks and closes - `OnClick(handler)` runs for every click in the menu, before the slot's own handler. Use it for a rule that covers the whole menu, such as logging clicks or cancelling every click when the player is not allowed to use it. - A slot's handler runs only for a click the menu did not cancel. Its `MenuClick.Clicked` and `Cursor` are the stacks before the click. - `OnClose` has two forms: `OnClose(player => ...)` for the closing player, and `OnClose((player, reason) => ...)` to also get the `MenuCloseReason`. The cursor stack has already been given back when it runs. - `Menu.Title` can be changed at any time. The viewers' screens are reopened with the same contents. The engine also raises `MenuClickEvent` and `MenuCloseEvent` for every menu, not only yours. They carry the menu's native id instead of a `Menu` object, so use them to observe menus you do not own. ## Next - Items you put in a menu are ordinary stacks: `Get`, `Set`, `AddItem` and `RemoveItem` on the menu read and write them. - Shared state between plugins: [inter-plugin services](/guides/inter-plugin-services/). - Plain events and commands: [first plugin](/guides/first-plugin/). --- # Inter-plugin services Page: /guides/inter-plugin-services/ Let one plugin offer an API to another with the ServiceManager: an economy provider, a shop that buys from it, and hot reload. Paper's equivalent is the `ServicesManager` (Vault-style). The idea: a plugin **registers an interface implementation** as a service; other plugins **look it up by interface** without knowing which plugin provides it. Every plugin has `Services`, a `ServiceManager`: | Member | What it does | |---|---| | `Register(instance, priority)` | offers an implementation; the registration belongs to your plugin and is removed when it unloads | | `Unregister()` | removes your registration of `T` | | `TryGet()` | the best provider, or `null` when there is none | | `Get()` | the best provider; throws `ServiceUnavailableException` when there is none | | `GetAll()` | every registration, best first, as `ServiceRegistration` (`Instance`, `Provider`, `Priority`) | | `Changed(Action)` | runs when the set of providers of `T` changes (a registration, an unregistration, or a provider unloading) | `ServicePriority` is `Lowest`, `Low`, `Normal`, `High` or `Highest`. The highest priority wins; among equal priorities the first registration wins. A plugin replaces another provider by registering at a higher priority. ## 1. The contract The API already ships some service interfaces in `Mc.Scripting.Services`, so a consumer needs nothing but the API to see them: | Interface | What it is | Built-in provider | |---|---|---| | `IEconomy` | balances and transfers, in minor units (`long`), with a `Currency` | none: without a provider, money rules do nothing | | `IBanList` | the server's ban lists | built in (`Lowest`) | | `IPlaceholders` | `%token%` values for text | built in (`Normal`), one shared registry | | `IPlayerMeta` | per-player values and affixes (text such as a prefix) | built in (`Lowest`), no meta | This page uses `IEconomy`. The provider below implements it in memory; the shop buys from whichever economy is registered. For an interface of your own, put it in a type both plugins can see: a plugin that lists another in `depends` gets that plugin's assembly, so both sides share the same type. Keep such interfaces **small and stable**: adding a member breaks every provider and consumer compiled against the old shape. Add a new interface (`IWarpDirectoryV2`) instead. ## 2. The provider **Bank/plugin.json** ```json { "id": "bank", "name": "Bank", "version": "1.0.0", "api": "1.0", "main": "Bank.PluginMain", "description": "An in-memory economy" } ``` **Bank.cs** ```csharp using Mc.Scripting.Services; namespace Bank; public sealed class PluginMain : Plugin { protected override void OnEnable() { Services.Register(new BankEconomy(), ServicePriority.Normal); } } /// An in-memory economy with one currency of two fraction digits (1050 is 10.50). Nothing is saved to disk. internal sealed class BankEconomy : IEconomy { private readonly Dictionary _balances = new(); public Currency Currency { get; } = new("dollar", "$", 2, "dollars"); public event Action? Changed; public bool HasAccount(Uuid player) => _balances.ContainsKey(player); public long Balance(Uuid player) => _balances.GetValueOrDefault(player); public bool Has(Uuid player, long minor) => Balance(player) >= minor; public EconomyResult Deposit(Uuid player, long minor, string reason) { if (minor <= 0) return EconomyResult.Refused; _balances[player] = Balance(player) + minor; Changed?.Invoke(player); return EconomyResult.Ok; } public EconomyResult Withdraw(Uuid player, long minor, string reason) { if (minor <= 0) return EconomyResult.Refused; if (!Has(player, minor)) return EconomyResult.NotEnough; _balances[player] -= minor; Changed?.Invoke(player); return EconomyResult.Ok; } public EconomyResult Transfer(Uuid from, Uuid to, long minor, string reason) { if (minor <= 0) return EconomyResult.Refused; if (!Has(from, minor)) return EconomyResult.NotEnough; _balances[from] -= minor; _balances[to] = Balance(to) + minor; Changed?.Invoke(null); // two accounts changed: null means "many" return EconomyResult.Ok; } } ``` `Currency` and `EconomyResult` are in `Mc.Scripting.Services` next to `IEconomy`. Amounts are always `long` minor units, so no floating point is involved. ## 3. The consumer **Shop/plugin.json** ```json { "id": "shop", "name": "Shop", "version": "1.0.0", "api": "1.0", "main": "Shop.PluginMain", "description": "Sell things for money", "softDepends": ["bank >=1.0"] } ``` `softDepends` loads `bank` first when it is installed, and the shop still loads without it. Use `depends` only when the consumer cannot work at all without the provider's types. The built-in `IEconomy` needs neither, because the API is always there. **Shop.cs** ```csharp using Mc.Scripting.Services; namespace Shop; public sealed class PluginMain : Plugin { private IEconomy? _economy; protected override void OnEnable() { _economy = Services.TryGet(); // the best provider, null when none is registered // re-resolve whenever the set of providers changes (also when a provider unloads) Services.Changed(() => _economy = Services.TryGet()); Commands.Register(Command.Literal("buy") .Then(Command.Argument("item", Arg.Word()) .Executes(ctx => Buy(ctx.RequirePlayer(), ctx.GetString("item"))))); } private int Buy(Player player, string item) { if (_economy is null) throw new CommandFailure("The shop is closed (no economy plugin)."); const long price = 1000; // minor units: 10.00 with two fraction digits if (_economy.Withdraw(player.Uuid, price, $"buy {item}") != EconomyResult.Ok) throw new CommandFailure($"Not enough money ({_economy.Currency.Format(price)} needed)."); player.SendMessage($"Bought {item} for {_economy.Currency.Format(price)}."); return 1; } } ``` The shop keeps its reference in a field and refreshes it in the `Changed` callback, so it never holds a provider that has gone away. ## Hot reload and unloading - **Registrations come and go** with their plugin. Unloading a provider, or registering a replacement at a higher priority, fires `Changed` in every plugin that listens. An object a consumer still holds from before the change belongs to the old provider, so drop it: the `Changed` callback above is the signal. Never cache a service in a static field. - **`Get` versus `TryGet`**: use `Get()` when the provider is required and a missing one is a bug (it throws `ServiceUnavailableException`). Use `TryGet()` when no provider is a normal state, as in the shop. - **Unloading or reloading a provider** also unloads or reloads the plugins that list it in `depends` or `softDepends`, because they may hold its types. They are unloaded before the provider and loaded again after it. ## When not to use services For fire-and-forget notifications between plugins ("a player bought something"), define a [custom event](/concepts/events/#custom-events) and let others subscribe. Services are for request/response APIs where the caller needs an answer. --- # Migrating from Paper / Bukkit Page: /guides/migrating-from-paper/ A concept-by-concept mapping from the Bukkit API to the Ramjet C# plugin API, with side-by-side code and the surprises to expect. If you have written Paper plugins you already know most of the concepts. This page maps them to the Ramjet names, says what is **different on purpose**, and lists the surprises to expect. Every Ramjet name below exists in the API. The [API reference](/reference/api/) has the exact signatures. ## Concept mapping ### Plugin model | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `JavaPlugin` | `Plugin` (derive; `[PluginInfo]` for name, version, author) | | | `onEnable()` / `onDisable()` | `OnEnable()` / `OnDisable()` | Register everything in `OnEnable`. There is no `onLoad()`. | | `plugin.yml` | [`plugin.json`](/reference/plugin-json/) | `depends`, `softDepends`, `loadBefore`, `api` | | `depend` / `softdepend` / `loadbefore` | `depends` / `softDepends` / `loadBefore` | Entries can carry a version constraint, e.g. `"economy >=1.2"` | | `api-version` | `"api": "1.0"` | Checked against the server's API version | | `getLogger()` | `Log` (on `Plugin`; `Info`, `Warn`, `Error`, `Debug`) | Every line carries the plugin name | | `getDataFolder()` | `DataFolder` | `plugins//`, created on first use | | `saveDefaultConfig()`, `getConfig()` | `Config.Load(MyJsonContext.Default.MyConfig)` returns `Config` | Typed and source-generated JSON; `Value`, `Reload()`, `Save()`, `Watch` | | `getResource`, own data files | `Config.ReadText(file)`, `Config.WriteText(file, text)` | | | Maven/Gradle, shading | `dotnet build` | One assembly per plugin; its own NuGet dependencies load privately into its load context | | PlugMan / `/reload` | hot reload on every build | Built in; see [hot reload](/concepts/hot-reload/) | | `Bukkit.getServer()` | the static `Server` class | | ### Events | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `class X implements Listener` + `@EventHandler` methods | `Events.On((ref FooEvent e) => ...)` | A delegate; no reflection scanning | | `registerEvents(...)` | `Events.On(...)` returns a `Subscription` | Dispose it to unsubscribe | | `EventPriority.LOWEST ... MONITOR` | `EventPriority.Lowest ... Monitor` | Lowest runs first, as in Paper | | `@EventHandler(ignoreCancelled = true)` | the **default** | A cancelled event skips your handler | | `ignoreCancelled = false` | `receiveCancelled: true` | | | `event.setCancelled(true)` | `e.Cancelled = true` | Cancellable events only | | `event.getPlayer()` | `e.Player` | A `Player` handle | | custom `Event` + `HandlerList` | a blittable struct implementing `IEvent` | A `Key` such as `"myplugin:thing"`, starting with an `EventHeader` field; `Events.Fire(ref e)` returns false if it was cancelled | | `AsyncPlayerChatEvent` | `PlayerChatEvent` | One synchronous event, on the tick thread | | `PlayerJoinEvent#setJoinMessage` | `e.SetJoinMessage(Component)` | | | the event catalogue | 78 events, see the [event reference](/reference/events/) | | ### Scheduling and threads | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `runTaskLater(plugin, r, delay)` | `Scheduler.RunLater(delayTicks, action)` | Returns a `ScheduledTask` | | `runTaskTimer(plugin, r, delay, period)` | `Scheduler.RunEvery(periodTicks, action, initialDelayTicks)` | The period comes first | | `BukkitTask#cancel()` | `ScheduledTask.Cancel()` or `Dispose()` | Tasks are cancelled on unload | | `runTask` from async code | `Scheduler.Post(action)` | Thread-safe; runs on the tick thread at the next tick | | `runTask` with a result | `Scheduler.RunSync(action)` or `RunSync(func)` | Returns a `Task` | | `runTaskAsynchronously` | `Scheduler.RunAsync(action)` | Thread pool; must not touch game APIs | | `await` back to the main thread | `await Scheduler.ToTick()` | `await Scheduler.ToAsync()` goes the other way | | `Bukkit.isPrimaryThread()` | `Server.IsTickThread` | | | main thread rule | the tick thread owns the game | Game calls from another thread throw `WrongThreadException` | ### Commands and permissions | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `commands:` in `plugin.yml` + `onCommand(...)` | `Commands.Register(Command.Literal("name")...)` | A command tree node; see [commands](/concepts/commands/) | | `TabCompleter` | `.Suggests(ctx => ...)` on an argument node | | | manual `args[]` parsing | typed arguments: `Arg.Int()`, `Arg.Double()`, `Arg.Word()`, `Arg.Player()`, `Arg.BlockPos()`, ... | Checked by the client and the server | | `sender instanceof Player` | `ctx.Player` (null for the console) or `ctx.RequirePlayer()` | | | `return false` (show usage) | `throw new CommandFailure("...")` | Red text for the sender, nothing logged | | `player.hasPermission("x")` | `player.HasPermission("x")`, or `.Permission("x")` on the node | The console always passes a node | | operator level checks | `.OpLevel(n)` on the node, `player.PermissionLevel` | | | `permissions:` in `plugin.yml` with `default: true / false / op / not op` | `Permissions.Register("node", PermissionDefault.True, "description")` | `PermissionDefault.False`, `Op`, `NotOp`, `OpLevel(n)` | | Vault-style permission plugins | `Permissions.SetProvider(IPermissionProvider)` | | ### Players and entities | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `Entity` / `Player` objects | `Entity` / `Player` **handles** (structs) | The data lives in ECS components | | `entity.isValid()` | `entity.IsAlive` | | | `player.isOnline()` | `player.IsOnline` | | | `Bukkit.getPlayer(name)` / `getPlayer(uuid)` | `Server.GetPlayer(name)` / `Server.GetPlayer(uuid)` | Null when offline | | `Bukkit.getOnlinePlayers()` | `Server.Players` | A snapshot | | `Bukkit.getOfflinePlayer(name)` | `OfflinePlayers.Find(name)`, `OfflinePlayers.Get(uuid)` | | | `player.getUniqueId()` | `player.Uuid` | | | `player.getLocation()` | `player.Location`, `player.Position` | `Location` holds the world, position and rotation | | `player.teleport(location)` | `player.Teleport(location)` | | | `player.sendMessage(...)` | `player.SendMessage(Component)` | A string converts to a component | | `sendActionBar`, `sendTitle` | `player.SendActionBar(...)`, `player.ShowTitle(title, subtitle, times)` | | | `playSound`, `spawnParticle` | `player.PlaySound(...)`, `player.SpawnParticle(...)` | | | `player.kickPlayer(message)` | `player.Kick(Component)` | Fires `PlayerKickEvent` first | | `setGameMode`, `setHealth`, `setFoodLevel` | `player.GameMode`, `player.Health`, `player.FoodLevel` | Settable properties | | `setDisplayName`, `setPlayerListName`, tab header/footer | `player.DisplayName`, `player.SetListName(...)`, `player.SetTabList(header, footer)` | | | `getInventory()` | `player.Inventory` (a `PlayerInventory`) | `player.HeldItem` is the item in the main hand | | `PersistentDataContainer`, `setMetadata` | your own component: `entity.Set(new Mana { ... })` | See [entities and ECS](/concepts/entities-and-ecs/) | | `entity.remove()` | `entity.Despawn()` for game entities, `entity.Destroy()` for your own ECS entities | Players are removed with `Kick` | | `world.getEntities()` | `World.Entities` | | ### World and blocks | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `World`, `Bukkit.getWorld(name)` | `World` handle; `Server.GetWorld(key)`, `Server.Overworld` | `World.Nether`, `World.End` | | `world.getBlockAt(x, y, z)` | `world.GetBlock(new BlockPos(x, y, z))` | Returns a `BlockState` value | | `block.setType(...)` | `world.SetBlock(pos, state)` | | | `Block#breakNaturally` | `world.DestroyBlock(pos, dropItems)` | | | block entity data | `world.GetBlockEntityData(pos)`, `SetBlockEntityData(...)` | Returns NBT | | `Location`, `Vector` | `Location`, `Vec3d`, `BlockPos` | | | `ItemStack` | `ItemStack` (a readonly struct) | | | `ChunkTickets` / forced chunk loading | `plugin.ChunkTickets` | Released on unload | ### Scoreboard, boss bars and titles | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `Scoreboard`, `Objective`, `setDisplaySlot` | `Scoreboard.CreateObjective(name, title, criteria)`, `objective.SetScore(holder, value)`, `Scoreboard.SetDisplay(DisplaySlot, objective)` | | | per-player sidebar | `Sidebars.Create(title)`, then `sidebar.SetLine(index, text)` and `sidebar.Show(player)` | [scoreboard sidebar](/guides/scoreboard-sidebar/) | | `BossBar` | `BossBars.Create(name, title, color, style, progress)`, then `AddViewer(player)`, `Progress`, `Remove()` | [boss bars and titles](/guides/boss-bars-and-titles/) | | titles and subtitles | `player.ShowTitle(...)`, `player.ShowSubtitle(...)`, `player.ClearTitle()` | | ### Menus and inventories | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `Bukkit.createInventory` + `InventoryClickEvent` | `Menus.Chest(rows, title)`, then `menu.Slot(slot, icon, onClick)` | Plugin property `Menus`; see [custom GUIs](/guides/custom-guis/) | | `player.openInventory(menu)` | `menu.Open(player)` | `menu.Close(player)` closes it | | `InventoryCloseEvent` | `menu.OnClose(...)` | | | clicks on your own menus | `menu.OnClick(...)`, `MenuClickEvent` | Clicks on the engine's own inventories use `InventoryClickEvent` | | `inventory.addItem`, `removeItem`, `setItem` | `menu.AddItem`, `RemoveItem`, `Set` | The same methods exist on `PlayerInventory` | ### Chat and text | Paper / Bukkit | Ramjet | Notes | |---|---|---| | Adventure `Component`, MiniMessage | `Mc.Scripting.Text.Component` | `Component.Text(...)`, `Component.Legacy("&a...")`, `Component.Parse("...")` for MiniMessage | | `Bukkit.broadcastMessage(...)` | `Server.Broadcast(string)` or `Server.Broadcast(Component)` | | | `AsyncPlayerChatEvent#setFormat` | `PlayerChatEvent`: `SetRendered(...)`, `SetDecoration(...)`, `SetSenderName(...)` | | | chat ban or mute | `ChatMute.Set(uuid, expires, reason)`, `ChatMute.Clear(uuid)` | | | `Chat.sendMessage` to a set of players | `Chat.SendTo(recipients, kind, name, target, message)` | | ### Services and plugin messaging | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `ServicesManager` (register, get) | `Services.Register(instance, priority)`, `Services.Get()`, `Services.TryGet()`, `Services.Changed(...)` | [inter-plugin services](/guides/inter-plugin-services/) | | plugin messaging channels | `Channels.On("namespace:channel", (player, data) => ...)`, `player.SendPluginMessage(channel, bytes)` | | ### Packets and NBT | Paper / Bukkit | Ramjet | Notes | |---|---|---| | ProtocolLib packet listeners | `Packets.On(handler, mode, priority)` | `PacketMode.Observe` gets a copy after the fact; `PacketMode.Intercept` may change or cancel the packet (`PacketEvent.Cancel()`) | | sending a packet | `Packets.Send(player, packet)`, or `player.SendPacket(packet)` | | | NBT (`CompoundTag`, `NBTTagCompound`) | `NbtCompound`, `NbtList`, `NbtTag.Parse(snbt)`, `NbtTag.FromBytes(bytes)` | | | entity data (`/data get entity`) | `entity.GetNbt()`, `entity.SetNbt(data)` | | ### Holograms, NPCs and fake blocks | Paper / Bukkit | Ramjet | Notes | |---|---|---| | hologram (armor stand or text display) | `Holograms.Create(world, position, text, options)`, then `ShowToAll()` | Removed on unload | | NPC (Citizens and similar) | `Npcs.Create(world, position, name, skin)`, then `ShowToAll()`, `SetEquipment(...)` | `NpcInteractEvent` fires on clicks | | fake blocks for one player | `player.ShowBlock(pos, state)`, `player.ClearFakeBlock(pos)` | | ### HTTP and content | Paper / Bukkit | Ramjet | Notes | |---|---|---| | `HttpURLConnection`, your own HTTP client | `Http.Client`, a shared `HttpClient` | The server applies its own limits to plugin requests | | resource pack tricks for custom items and blocks | `CustomItem`, `CustomBlock`, `Content.Items.Register(item)`, `Content.Blocks.Register(block)` | `ResourcePackBuilder` generates the pack | ## Side by side ### A listener ```java // Paper public final class ChatListener implements Listener { @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onChat(AsyncPlayerChatEvent e) { if (e.getMessage().toLowerCase().contains("spam")) e.setCancelled(true); } } // in onEnable getServer().getPluginManager().registerEvents(new ChatListener(), this); ``` **ChatFilter.cs** ```csharp using Mc.Scripting; namespace ChatFilterSample; public sealed class ChatFilterPlugin : Plugin { protected override void OnEnable() { // A cancelled event skips this handler by default, which is Paper's ignoreCancelled = true. Events.On((ref PlayerChatEvent e) => { if (e.Message.Contains("spam", StringComparison.OrdinalIgnoreCase)) e.Cancelled = true; }, EventPriority.High); } } ``` ### A scheduler task ```java // Paper new BukkitRunnable() { public void run() { getLogger().info("tick"); } }.runTaskTimer(this, 0L, 20L * 60); ``` **RepeatingLog.cs** ```csharp using Mc.Scripting; namespace RepeatingLogSample; public sealed class RepeatingLogPlugin : Plugin { protected override void OnEnable() { Scheduler.RunEvery(20 * 60, () => Log.Info("tick")); } } ``` ### A command ```java // Paper: plugin.yml declares "sethome", then @Override public boolean onCommand(CommandSender s, Command c, String l, String[] a) { if (!(s instanceof Player p)) { s.sendMessage("Players only."); return true; } if (a.length != 1) return false; homes.put(p.getUniqueId(), a[0], p.getLocation()); return true; } ``` **SetHomeCommand.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Commands; namespace SetHomeSample; public sealed class SetHomePlugin : Plugin { private readonly Dictionary<(Uuid Owner, string Name), Vec3d> _homes = new(); protected override void OnEnable() { Commands.Register(Command.Literal("sethome") .Permission("homes.set") .Then(Command.Argument("name", Arg.Word()) .Executes(ctx => { Player p = ctx.RequirePlayer(); // the console gets a clear message instead string name = ctx.GetString("name"); _homes[(p.Uuid, name)] = p.Position; ctx.Reply($"Home {name} set."); return 1; }))); } } ``` ## What surprises Paper developers 1. **Events are structs passed by `ref`.** You cannot capture the event in a lambda or keep it after the handler returns. Copy what you need. String fields are borrowed UTF-8 views (`Utf8String`): call `ToString()` to keep one. 2. **Allocation matters.** The tick thread has no spare time for GC pauses; hot-path handlers must not allocate. See [performance](/concepts/performance/). 3. **State in fields does not survive a reload; state in components does.** Plugins are rebuilt and reloaded constantly; design for it ([hot reload](/concepts/hot-reload/)). Avoid the `getInstance()` static singleton: statics die with the plugin's load context, and a static in another assembly that points into your plugin leaks it. 4. **There is no `Player` object to hold on to.** A `Player` is a handle. Check `IsOnline` when you come back to it later: a stale handle reads defaults and its actions do nothing. 5. **Ticks, not milliseconds.** Durations are ticks (20 per second), unless the method takes a `TimeSpan`. 6. **Commands are Brigadier-native.** There is no `plugin.yml` command section, no `label`, and no hand-written tab completion for the basics. 7. **No reflection in the API.** No `@EventHandler` scanning and no annotation processing: registrations are explicit calls, which is also what makes ownership and unloading exact. 8. **Argument order differs in places.** `RunEvery(periodTicks, action, initialDelayTicks)` takes the period first, where Paper's `runTaskTimer` takes the delay first. Check the signature in the [API reference](/reference/api/) before you port a call. --- # plugin.json reference Page: /reference/plugin-json/ Every field of the plugin manifest, where it lives, how dependencies and versions are resolved. ## Location | Layout | Manifest | |---|---| | `plugins//.dll` | `plugins//plugin.json` | | `plugins/.dll` (loose) | `plugins/.plugin.json`, if present | | source folder `plugins//` | `plugins//plugin.json`, else the `[assembly: McPlugin]` attribute | | source file `plugins/.cs` | `plugins/.plugin.json`, else the `[assembly: McPlugin]` attribute | | no manifest | the `[PluginInfo]` attribute; the id is the lower-cased assembly name, with every character outside `a-z 0-9 _ -` replaced by `_` (at most 32) | The manifest is read **before any assembly is loaded**: the server builds the dependency graph from manifests alone, then loads in order. ## Example ```json { "id": "homes", "name": "Homes", "version": "1.2.0", "api": "1.0", "main": "Homes.HomesPlugin", "authors": ["Ahmed"], "description": "Set and teleport to homes.", "website": "https://example.org", "depends": ["economy >=1.2"], "softDepends": ["permissions-ui"], "loadBefore": ["chat-format"], "permissions": [ { "node": "homes.use", "default": "true", "description": "Use /home" } ] } ``` ## Fields | Field | Type | Required | Meaning | |---|---|---|---| | `id` | string | yes | stable plugin id: lower-case `[a-z0-9_-]`, 1 to 32 characters. Used for the data folder (`plugins//data/`), dependencies and logs. A second plugin with the same id is reported and skipped | | `name` | string | no | display name, also the log prefix (defaults to the id) | | `version` | string | no | plugin version, compared against dependants' constraints (defaults to `0.0.0`) | | `api` | string | no | the API version the plugin was built for, `"major.minor"`. When missing the server assumes `1.0` | | `main` | string | no | full name of the plugin class. Default: the first public, non-abstract `IPlugin` type with a parameterless constructor (a `Plugin` subclass) | | `authors` | string[] | no | | | `description` | string | no | one line | | `website` | string | no | | | `depends` | string[] | no | **hard** dependencies: the plugin is skipped if one is missing or too old | | `softDepends` | string[] | no | loaded first when present; the plugin works without them | | `loadBefore` | string[] | no | an edge from this plugin to the named one: this plugin loads first | | `permissions` | object[] | no | each permission node the plugin checks: `node`, `default` and `description` | The `default` of a permission is `true`, `false`, `op` (the default when absent), `op:N` (operator level N) or `not_op` (`notop` is also accepted). Unknown fields are ignored (forward compatible). Malformed JSON, a missing or invalid `id`, an invalid `api` or permission `default` are reported with the file path and the line or key. An `api` the server cannot provide is refused. ### Version constraints A `depends` / `softDepends` entry may carry a constraint after the id: `"economy >=1.2"`. Supported operators: `>=`, `>`, `=`, `<`, `<=` and `^1.2` (caret: same major, at least that version). ### API version The server declares an API version (`ApiVersion.Current`, currently `1.0`): minor = additive, major = breaking. A plugin loads when its `major` equals the server's and its `minor` is at most the server's, and is refused otherwise with a message such as `plugin 'homes' needs API 1.4, this server provides 1.2`. Within a major, no member is removed and no signature changes; new overloads are added instead of optional parameters, and events only gain fields. ## Source plugins A source plugin is a `.cs` file or a folder of `.cs` files that the server compiles when it starts (`plugins.allow_source`, on by default). Its metadata comes from a `plugin.json` next to the sources, or else from an assembly attribute in them. The attribute carries the same information as the manifest: ```csharp [assembly: McPlugin("homes", version: "1.2.0", depends: ["economy >=1.0"])] ``` ## Load order and failures Plugins are discovered in `plugins/` and ordered by a topological sort of `depends`, `softDepends` (only if present) and `loadBefore`, ties broken by id so the order is deterministic. | Situation | Result | |---|---| | missing hard dependency | the plugin and everything depending on it is skipped: `plugin 'homes' requires 'economy >=1.2' which is not installed` | | hard dependency too old or too new | the same: `plugin 'homes' requires 'economy >=1.2' but version 1.1.0 of 'economy' is installed` | | dependency cycle | every plugin on the cycle is skipped and the cycle is printed: `dependency cycle: a -> b -> c -> a` | | a plugin whose `OnEnable` throws | reported as `failed to enable`; its dependents are skipped with that cause | | unload | reverse load order; hot reload of a plugin also reloads its hard dependents | --- # API status Page: /reference/status/ What the plugin API can actually do in this release: wired events and host services, generated from the sources. This page is generated on every build from the server sources, so it always matches the release the docs were built from. It answers "will my plugin actually see this?" for every event and host service. ## Events the engine fires 78 of 78 catalogue events have a firing site in the engine. | Event | Cancellable | Fired from | |---|---|---| | [`ServerStartEvent`](/reference/api/event-types/serverstartevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp`, `src/server/main.cpp` | | [`ServerStopEvent`](/reference/api/event-types/serverstopevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp`, `src/server/main.cpp` | | [`TickStartEvent`](/reference/api/event-types/tickstartevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`TickEndEvent`](/reference/api/event-types/tickendevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`ServerListPingEvent`](/reference/api/event-types/serverlistpingevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`CommandsRegisteredEvent`](/reference/api/event-types/commandsregisteredevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PluginEnableEvent`](/reference/api/event-types/pluginenableevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PluginDisableEvent`](/reference/api/event-types/plugindisableevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PermissionChangedEvent`](/reference/api/event-types/permissionchangedevent/) | no | `src/game/scriptbridge/bridge_permissions.cpp`, `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PermissionsReloadedEvent`](/reference/api/event-types/permissionsreloadedevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PlayerPreLoginEvent`](/reference/api/event-types/playerpreloginevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PlayerJoinEvent`](/reference/api/event-types/playerjoinevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PlayerQuitEvent`](/reference/api/event-types/playerquitevent/) | no | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PlayerMoveEvent`](/reference/api/event-types/playermoveevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerChatEvent`](/reference/api/event-types/playerchatevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PrivateMessageEvent`](/reference/api/event-types/privatemessageevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerCommandEvent`](/reference/api/event-types/playercommandevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerInteractBlockEvent`](/reference/api/event-types/playerinteractblockevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerKickEvent`](/reference/api/event-types/playerkickevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp`, `src/scripting/ipc/session.cpp` | | [`PlayerRespawnEvent`](/reference/api/event-types/playerrespawnevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerDeathEvent`](/reference/api/event-types/playerdeathevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerCreativeSlotEvent`](/reference/api/event-types/playercreativeslotevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerItemHeldEvent`](/reference/api/event-types/playeritemheldevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerSwapHandsEvent`](/reference/api/event-types/playerswaphandsevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerDropItemEvent`](/reference/api/event-types/playerdropitemevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerPickupItemEvent`](/reference/api/event-types/playerpickupitemevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerConsumeEvent`](/reference/api/event-types/playerconsumeevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerInteractEvent`](/reference/api/event-types/playerinteractevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerInteractEntityEvent`](/reference/api/event-types/playerinteractentityevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerToggleSneakEvent`](/reference/api/event-types/playertogglesneakevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerToggleSprintEvent`](/reference/api/event-types/playertogglesprintevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerToggleFlightEvent`](/reference/api/event-types/playertoggleflightevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerGameModeChangeEvent`](/reference/api/event-types/playergamemodechangeevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerTeleportEvent`](/reference/api/event-types/playerteleportevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerPortalEvent`](/reference/api/event-types/playerportalevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerChangedWorldEvent`](/reference/api/event-types/playerchangedworldevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerResourcePackStatusEvent`](/reference/api/event-types/playerresourcepackstatusevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`PlayerLevelChangeEvent`](/reference/api/event-types/playerlevelchangeevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockBreakEvent`](/reference/api/event-types/blockbreakevent/) | yes | `src/scripting/event_bus.hpp`, `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockPlaceEvent`](/reference/api/event-types/blockplaceevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockDamageEvent`](/reference/api/event-types/blockdamageevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockIgniteEvent`](/reference/api/event-types/blockigniteevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockBurnEvent`](/reference/api/event-types/blockburnevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockSpreadEvent`](/reference/api/event-types/blockspreadevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockFadeEvent`](/reference/api/event-types/blockfadeevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockFormEvent`](/reference/api/event-types/blockformevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockGrowEvent`](/reference/api/event-types/blockgrowevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockPistonExtendEvent`](/reference/api/event-types/blockpistonextendevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockPistonRetractEvent`](/reference/api/event-types/blockpistonretractevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`BlockRedstoneEvent`](/reference/api/event-types/blockredstoneevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`EntitySpawnEvent`](/reference/api/event-types/entityspawnevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`EntityDamageEvent`](/reference/api/event-types/entitydamageevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`EntityDeathEvent`](/reference/api/event-types/entitydeathevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`EntityCombustEvent`](/reference/api/event-types/entitycombustevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`InventoryOpenEvent`](/reference/api/event-types/inventoryopenevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`InventoryCloseEvent`](/reference/api/event-types/inventorycloseevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`InventoryClickEvent`](/reference/api/event-types/inventoryclickevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`InventoryDragEvent`](/reference/api/event-types/inventorydragevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`CraftItemEvent`](/reference/api/event-types/craftitemevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`PrepareCraftEvent`](/reference/api/event-types/preparecraftevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`FurnaceSmeltEvent`](/reference/api/event-types/furnacesmeltevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`FurnaceBurnEvent`](/reference/api/event-types/furnaceburnevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`EnchantItemEvent`](/reference/api/event-types/enchantitemevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PrepareAnvilEvent`](/reference/api/event-types/prepareanvilevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PacketReceiveEvent`](/reference/api/event-types/packetreceiveevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`PacketSendEvent`](/reference/api/event-types/packetsendevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`ChunkLoadEvent`](/reference/api/event-types/chunkloadevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`ChunkUnloadEvent`](/reference/api/event-types/chunkunloadevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`TimeSkipEvent`](/reference/api/event-types/timeskipevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`WeatherChangeEvent`](/reference/api/event-types/weatherchangeevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`PortalCreateEvent`](/reference/api/event-types/portalcreateevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`MenuClickEvent`](/reference/api/event-types/menuclickevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`MenuCloseEvent`](/reference/api/event-types/menucloseevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | | [`CustomItemUseEvent`](/reference/api/event-types/customitemuseevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`CustomBlockPlaceEvent`](/reference/api/event-types/customblockplaceevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`CustomBlockBreakEvent`](/reference/api/event-types/customblockbreakevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`CustomBlockInteractEvent`](/reference/api/event-types/customblockinteractevent/) | yes | `src/scripting/ipc/ipc_events.g.hpp` | | [`NpcInteractEvent`](/reference/api/event-types/npcinteractevent/) | no | `src/scripting/ipc/ipc_events.g.hpp` | Declared in the catalogue but **not fired yet** (you can subscribe; nothing raises them): . ## Host services | Service | Wired to the game | What a plugin sees while it is not wired | |---|---|---| | `Server.SendMessage` | yes | - | | `Server.Broadcast` | yes | - | | `Server.CurrentTick` | yes | - | | `Commands.Register` commands run from in-game chat (nodes of the server command tree) | yes | - | | `plugin.json` manifest read by the loader | yes | - | ## Always available - ECS: `plugin.Ecs` (`RegisterComponent`, `Spawn`, `Query<...>`), `Entity`, `Systems` (phases, ordering, replace), `Scheduler`. - The event bus: subscribe, priorities, cancellation, mutable fields, custom events, `HasSubscribers`. - Hot reload with ownership tracking (everything registered through the plugin context is released on unload). - Logging through `Log` / `Logger`. --- # Event catalogue Page: /reference/events/ Every event of the ramjet plugin API with its fields, mutability and cancellability. 78 events are in the catalogue (ids 1-511 are stable ABI). Plugins can add their own events with ids from 512 (see [Events](/concepts/events/)). **Mutable** fields can be changed by handlers and are read back by the code that fired the event; all others are `readonly` in C#. **Engine** says whether the engine fires the event in this release; an event marked "not yet" can be subscribed to, but nothing fires it until the gameplay code gets wired up. Try the [event explorer](/reference/event-explorer/) to filter this list. ## Server and tick ### ServerStartEvent Fired once after the server finished starting, before the first tick. `mc:server_start` · id 1 · not cancellable · engine: fired · [type reference](/reference/api/event-types/serverstartevent/) ### ServerStopEvent Fired once when the server begins shutting down. `mc:server_stop` · id 2 · not cancellable · engine: fired · [type reference](/reference/api/event-types/serverstopevent/) ### TickStartEvent Fired at the start of every server tick. `mc:tick_start` · id 3 · not cancellable · engine: fired · [type reference](/reference/api/event-types/tickstartevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Tick` | `ulong` | no | The tick number. | ### TickEndEvent Fired at the end of every server tick. `mc:tick_end` · id 4 · not cancellable · engine: fired · [type reference](/reference/api/event-types/tickendevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Tick` | `ulong` | no | The tick number. | | `TickNanos` | `long` | no | Wall time spent in this tick. | ## Other ### ServerListPingEvent A client asked for the server list status. Change the MOTD, the player counts or hide the player sample. `mc:server_list_ping` · id 100 · not cancellable · engine: fired · [type reference](/reference/api/event-types/serverlistpingevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Connection` | `ulong` | no | Connection id. | | `Address` | `Utf8String` | no | Client address. | | `ProtocolVersion` | `int` | no | Protocol version the client announced. | | `Motd` | `component` | **yes** | The message of the day. | | `MaxPlayers` | `int` | **yes** | Maximum player count shown. | | `OnlinePlayers` | `int` | **yes** | Online player count shown. | | `HidePlayers` | `bool` | **yes** | Do not send the player sample. | ### CommandsRegisteredEvent The command tree changed (a plugin registered or removed commands, or a permission changed). Fired once per tick at most, before the trees are resent to the players. `mc:commands_registered` · id 101 · not cancellable · engine: fired · [type reference](/reference/api/event-types/commandsregisteredevent/) ### PluginEnableEvent A plugin finished enabling. `mc:plugin_enable` · id 102 · not cancellable · engine: fired · [type reference](/reference/api/event-types/pluginenableevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `PluginId` | `Utf8String` | no | The plugin id. | | `PluginName` | `Utf8String` | no | The plugin name. | | `Version` | `Utf8String` | no | The plugin version. | ### PluginDisableEvent A plugin was disabled (unload or hot reload); its registrations are already released. `mc:plugin_disable` · id 103 · not cancellable · engine: fired · [type reference](/reference/api/event-types/plugindisableevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `PluginId` | `Utf8String` | no | The plugin id. | | `PluginName` | `Utf8String` | no | The plugin name. | | `Version` | `Utf8String` | no | The plugin version. | ### PermissionChangedEvent A permission entry of a player changed (or of a group or everything: AllPlayers is true). Fired after the change; the command trees of the players are resent in the same tick. `mc:permission_changed` · id 104 · not cancellable · engine: fired · [type reference](/reference/api/event-types/permissionchangedevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `Uuid` | no | The profile whose permissions changed (zero when AllPlayers). | | `AllPlayers` | `bool` | no | A group, the default group or the whole backend changed. | ### PermissionsReloadedEvent The permission backend was reloaded from disk (/perm reload or Permissions.Reload). `mc:permissions_reloaded` · id 105 · not cancellable · engine: fired · [type reference](/reference/api/event-types/permissionsreloadedevent/) ### PrivateMessageEvent A player sent a private message (/msg, /tell, /w) to one player. Cancel to drop the copy for that target. Changing the message sends it unsigned; a changed decoration keeps it signed. Console senders do not fire it. `mc:player_private_message` · id 305 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/privatemessageevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Sender` | `player` | no | The player who typed the command. | | `Target` | `player` | no | The recipient. A /msg with several targets fires once per target. | | `Message` | `Utf8String` | **yes** | The typed text. Changing it sends the message unsigned. | | `Decoration` | `component` | **yes** | The body as the line shows it (unsigned content). Equal to the typed text means none. Keeps the message signed. | ### InventoryOpenEvent A player is about to open a container (chest, furnace, crafting table, ...). Cancel to keep it closed. `mc:inventory_open` · id 260 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/inventoryopenevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Kind` | `enum:InventoryKind` | no | The container kind. | | `HasPos` | `bool` | no | Whether the container is a block. | | `Pos` | `BlockPos` | no | The block position. | ### InventoryCloseEvent A player closed a container. `mc:inventory_close` · id 261 · not cancellable · engine: fired · [type reference](/reference/api/event-types/inventorycloseevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Kind` | `enum:InventoryKind` | no | The container kind. | ### InventoryClickEvent A player clicked a slot of an open container (also the player inventory). Cancel to undo the click (the client is resynced). `mc:inventory_click` · id 262 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/inventoryclickevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Kind` | `enum:InventoryKind` | no | The container kind. | | `Slot` | `int` | no | The clicked slot index of the container (-999 outside the window). | | `ClickType` | `enum:ClickType` | no | How it was clicked. | | `Button` | `int` | no | Raw button of the packet. | | `Clicked` | `item` | no | The item in the clicked slot. | | `Cursor` | `item` | no | The item on the cursor. | ### InventoryDragEvent A player finished dragging a stack over several slots. Cancel to undo. `mc:inventory_drag` · id 263 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/inventorydragevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Kind` | `enum:InventoryKind` | no | The container kind. | | `DragType` | `enum:DragType` | no | The drag mode. | | `Slots` | `NativeBytes` | no | The dragged slots as packed little-endian int32 values. | | `Cursor` | `item` | no | The cursor stack being distributed. | ### CraftItemEvent A player took the result of a crafting recipe (cancel the InventoryClick event of the result slot to prevent crafting). `mc:craft_item` · id 264 · not cancellable · engine: fired · [type reference](/reference/api/event-types/craftitemevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Recipe` | `Utf8String` | no | Recipe id. | | `Result` | `item` | no | The crafted item. | | `ShiftClick` | `bool` | no | Whether it is a quick-craft. | ### PrepareCraftEvent The crafting grid matched a recipe. Cancel to show no result. `mc:prepare_craft` · id 265 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/preparecraftevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Recipe` | `Utf8String` | no | Recipe id. | | `Result` | `item` | no | The result that would be shown. | ### FurnaceSmeltEvent A furnace, smoker or blast furnace finished smelting an item. Cancel to keep the input unsmelted. `mc:furnace_smelt` · id 266 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/furnacesmeltevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The furnace. | | `Source` | `item` | no | The ingredient. | | `Result` | `item` | no | The product. | ### FurnaceBurnEvent A furnace is about to consume fuel. Cancel to not burn it; the burn time may be changed. `mc:furnace_burn` · id 267 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/furnaceburnevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The furnace. | | `Fuel` | `item` | no | The fuel. | | `BurnTicks` | `int` | **yes** | Burn time in ticks. | ### EnchantItemEvent A player is enchanting an item at an enchanting table. Cancel to refuse. `mc:enchant_item` · id 268 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/enchantitemevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Item` | `item` | no | The item being enchanted. | | `Option` | `int` | no | The chosen option (0-2). | | `Cost` | `int` | no | Levels charged. | ### PrepareAnvilEvent The anvil inputs changed and a result was computed. Cancel to show no result; the cost may be changed. `mc:prepare_anvil` · id 269 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/prepareanvilevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `First` | `item` | no | Left input. | | `Second` | `item` | no | Right input. | | `Result` | `item` | no | The computed result. | | `RepairCost` | `int` | **yes** | Levels the player pays. | | `RenameText` | `Utf8String` | no | The text typed in the rename field. | ### TimeSkipEvent The day time jumped (sleeping through the night, /time set or /time add). `mc:time_skip` · id 300 · not cancellable · engine: fired · [type reference](/reference/api/event-types/timeskipevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Reason` | `enum:TimeSkipReason` | no | Why. | | `SkipAmount` | `long` | no | Ticks the clock advanced. | ### WeatherChangeEvent The rain started or stopped (overworld weather). `mc:weather_change` · id 301 · not cancellable · engine: fired · [type reference](/reference/api/event-types/weatherchangeevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Raining` | `bool` | no | Whether it will be raining. | | `Thundering` | `bool` | no | Whether it will be thundering. | ### PortalCreateEvent A nether portal frame is about to be filled or a destination portal built. Cancel to prevent it. `mc:portal_create` · id 303 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/portalcreateevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Reason` | `enum:PortalCreateReason` | no | Why. | | `BlockCount` | `int` | no | Portal blocks created. | ### MenuClickEvent A player clicked in a custom GUI (Menu) of a plugin, after InventoryClick. Menus are locked: the click is denied (the client is resynced) unless a handler sets Allow, and a cancel always denies it. A click that could move items out of or into the menu (a slot of the menu, shift click, double click, drag, number key, off hand key) is delivered here with the slot of the menu's own numbering (top slots first, then the player's 27 + 9 slots). `mc:menu_click` · id 270 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/menuclickevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Menu` | `int` | no | The native id of the menu (what Menu uses to route the click). | | `Slot` | `int` | no | The clicked slot, -999 outside the window. | | `ClickType` | `enum:ClickType` | no | How it was clicked. | | `Button` | `int` | no | Raw button of the packet. | | `Clicked` | `item` | no | The item in the clicked slot. | | `Cursor` | `item` | no | The item on the cursor. | | `Allow` | `bool` | **yes** | Set by a handler to let the click change items like in a chest (default false: the menu is a display). | ### MenuCloseEvent A plugin menu was closed for a player (for any reason). The player's cursor stack has already been given back. `mc:menu_close` · id 271 · not cancellable · engine: fired · [type reference](/reference/api/event-types/menucloseevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Menu` | `int` | no | The native id of the menu. | | `Reason` | `enum:MenuCloseReason` | no | Why it closed. | ### CustomItemUseEvent A player used a custom item (right click in the air or on a block). Fired by the content layer after PlayerInteract. Cancel to stop the item's own handler and the default action. `mc:custom_item_use` · id 272 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/customitemuseevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `CustomId` | `Utf8String` | no | The id of the custom item, "myplugin:ruby". | | `Hand` | `enum:Hand` | no | The hand. | | `Item` | `item` | no | The item stack. | | `HasBlock` | `bool` | no | Whether the click was on a block. | | `Pos` | `BlockPos` | no | The clicked block (only when has_block). | | `Face` | `enum:BlockFace` | no | The clicked face (only when has_block). | ### CustomBlockPlaceEvent A player is placing a custom block with a custom item. Cancel to refuse. `mc:custom_block_place` · id 273 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/customblockplaceevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `CustomId` | `Utf8String` | no | The id of the custom block. | | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | Where it is placed. | | `Against` | `BlockPos` | no | The block that was clicked. | | `Hand` | `enum:Hand` | no | The hand. | ### CustomBlockBreakEvent A player is breaking a custom block (fired by the content layer after BlockBreak). Cancel to keep the block. `mc:custom_block_break` · id 274 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/customblockbreakevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `CustomId` | `Utf8String` | no | The id of the custom block. | | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The block. | | `DropItems` | `bool` | **yes** | Whether the block's drops are spawned. | ### CustomBlockInteractEvent A player right-clicked a custom block (fired by the content layer after PlayerInteractBlock). Cancel to stop the block handler and the item use. `mc:custom_block_interact` · id 275 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/customblockinteractevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `CustomId` | `Utf8String` | no | The id of the custom block. | | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The block. | | `Face` | `enum:BlockFace` | no | The clicked face. | | `Hand` | `enum:Hand` | no | The hand. | | `Item` | `item` | no | The item in that hand. | ### NpcInteractEvent A player clicked an NPC made with Npcs.Create (a left click is an attack, a right click an interaction). Observed only: the click has no other effect. `mc:npc_interact` · id 304 · not cancellable · engine: fired · [type reference](/reference/api/event-types/npcinteractevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Npc` | `ulong` | no | The NPC's handle. | | `Action` | `enum:NpcClick` | no | The click. | | `Hand` | `enum:Hand` | no | The hand (a left click is the main hand). | | `Sneaking` | `bool` | no | Whether the player was sneaking. | ## Player ### PlayerPreLoginEvent A client finished authentication and is about to enter the configuration phase. Handlers run on the TICK thread: a blocking call (a database or web lookup) freezes the whole server, so do not look anything up here. Cancel to refuse the login (set the kick message first), or call Defer() and answer later from Scheduler.RunAsync: the login waits up to 30 seconds, continues when every deferral allowed it and is refused as soon as one denies. A plugin that unloads releases its pending deferrals (counted as allowed). `mc:player_pre_login` · id 10 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerpreloginevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Connection` | `ulong` | no | Connection id. | | `Uuid` | `Uuid` | no | The authenticated profile id. | | `Username` | `Utf8String` | no | The profile name. | | `Address` | `Utf8String` | no | Client address. | | `KickMessage` | `component` | **yes** | Shown to the client when the login is cancelled (default: a generic message). | | `Deferrals` | `int` | no | How many plugins hold the login (each Defer() adds one): it continues when all of them allowed it, and any denial refuses it at once. | ### PlayerJoinEvent A player entity entered the play state. The join message is broadcast after the handlers ran. `mc:player_join` · id 11 · not cancellable · engine: fired · [type reference](/reference/api/event-types/playerjoinevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Uuid` | `Uuid` | no | The profile id. | | `Username` | `Utf8String` | no | The profile name. | | `JoinMessage` | `component` | **yes** | The message broadcast to everybody (set to an empty component to suppress it). | ### PlayerQuitEvent A player disconnected (the entity is still alive during dispatch). `mc:player_quit` · id 12 · not cancellable · engine: fired · [type reference](/reference/api/event-types/playerquitevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Uuid` | `Uuid` | no | The profile id. | | `Username` | `Utf8String` | no | The profile name. | | `Reason` | `Utf8String` | no | Why the connection ended: disconnected, kicked, timeout, protocol_error, connection_lost or server_shutdown. | | `QuitMessage` | `component` | **yes** | The message broadcast to everybody (set to an empty component to suppress it). | ### PlayerMoveEvent A player moved or rotated. Cancel to teleport the player back; 'to', 'yaw' and 'pitch' may be rewritten. Fires for every accepted movement packet: keep handlers cheap. `mc:player_move` · id 13 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playermoveevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `From` | `Vec3d` | no | Position before the move. | | `To` | `Vec3d` | **yes** | Position after the move. | | `Yaw` | `float` | **yes** | Yaw after the move. | | `Pitch` | `float` | **yes** | Pitch after the move. | | `OnGround` | `bool` | no | Whether the client reports being on the ground. | ### PlayerChatEvent A player sent a chat message. Cancel to drop it. Changing Message or Rendered sends the line as an unsigned server message instead of the player's signed one. SenderName, ChatType, Decoration and the filter fields restyle the signed line without unsigning it: the signature covers only the typed text. `mc:player_chat` · id 14 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerchatevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The sender. | | `Message` | `Utf8String` | **yes** | What the player typed. Changing it re-renders the line with the default format. | | `Rendered` | `component` | **yes** | The complete line shown to everybody (default: message). Replace it to change the format; takes precedence over Message. | | `SenderName` | `component` | **yes** | The sender name as the line shows it (hover, click and insertion included). Changing it keeps the message signed. | | `ChatType` | `enum:ChatKind` | **yes** | The chat type of the line: Chat, EmoteCommand or SayCommand. The message kinds need a target and are refused here (Chat is used). | | `Decoration` | `component` | **yes** | The body as the line shows it (unsigned content: colours, highlights, glyphs). Equal to the typed text means none. Keeps the message signed. | | `FilterKind` | `enum:ChatFilterKind` | **yes** | How the recipients who filter text see the message (vanilla FilterMask). Any other value fully filters the message. | | `FilterBits` | `Utf8String` | **yes** | For PartiallyFiltered: one 0 or 1 per UTF-16 unit of the typed text, 1 hides the unit. A different length or character fully filters the message. | ### PlayerCommandEvent A player ran a command (without the leading slash). Cancel to suppress it; the command line may be rewritten. `mc:player_command` · id 15 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playercommandevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Command` | `Utf8String` | **yes** | The command line without the slash. | ### PlayerInteractBlockEvent A player right-clicked a block (fired after PlayerInteract). Cancel to stop the block interaction and item use. `mc:player_interact_block` · id 16 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerinteractblockevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Pos` | `BlockPos` | no | The clicked block. | | `Face` | `byte` | no | The clicked face (BlockFace). | | `Hand` | `byte` | no | The hand (Hand). | | `Item` | `item` | no | The item in that hand. | ### PlayerKickEvent A player is about to be kicked by a command or plugin. Cancel to keep the player. Not fired for protocol errors or disconnects. `mc:player_kick` · id 110 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerkickevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Reason` | `component` | **yes** | The disconnect screen text. | | `LeaveMessage` | `component` | **yes** | The message broadcast to everybody (empty = the default quit message). | ### PlayerRespawnEvent A player is respawning. The destination may be changed. `mc:player_respawn` · id 111 · not cancellable · engine: fired · [type reference](/reference/api/event-types/playerrespawnevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Reason` | `enum:RespawnReason` | no | Why. | | `Position` | `Vec3d` | **yes** | Where the player appears. | | `World` | `world` | **yes** | The dimension the player appears in. | | `IsSpawnPoint` | `bool` | no | True when a bed or respawn anchor of the player is used, false for the world spawn. | ### PlayerDeathEvent A player died. The death message, inventory/experience drops and level keeping can be changed. `mc:player_death` · id 112 · not cancellable · engine: fired · [type reference](/reference/api/event-types/playerdeathevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Killer` | `Entity` | no | The entity responsible (none if there is none). | | `Cause` | `enum:DamageCause` | no | The damage cause. | | `DeathMessage` | `component` | **yes** | Broadcast to everybody (empty = none). | | `DropInventory` | `bool` | **yes** | Drop the items (false keeps them, like keepInventory). Only the items: the experience is controlled by the two fields below. | | `KeepLevel` | `bool` | **yes** | Keep the experience level through the respawn (default: the keepInventory rule). True drops no experience orbs. | | `DropExperience` | `int` | **yes** | Experience points dropped as orbs at the death position (default the vanilla amount min(level*7, 100), 0 with keepInventory); independent of drop_inventory. | ### PlayerCreativeSlotEvent A creative-mode player put a stack into an inventory slot from the creative menu (slot -1: threw one out of the menu). Cancel to refuse; the client is resynced. `mc:player_creative_slot` · id 129 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playercreativeslotevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Slot` | `int` | no | The menu slot (1-45), or -1 for a drop. | | `Item` | `item` | no | The stack the client wants. | ### PlayerItemHeldEvent A player selected another hotbar slot. Cancel to revert the selection. `mc:player_item_held` · id 113 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playeritemheldevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `PreviousSlot` | `int` | no | Hotbar slot before (0-8). | | `NewSlot` | `int` | no | Hotbar slot selected (0-8). | ### PlayerSwapHandsEvent A player swapped the main hand and off hand items (F key). Cancel to keep them. `mc:player_swap_hands` · id 114 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerswaphandsevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `MainHand` | `item` | no | The item that moves to the off hand. | | `OffHand` | `item` | no | The item that moves to the main hand. | ### PlayerDropItemEvent A player drops an item from the hotbar or inventory (drop key). Cancel to keep it. `mc:player_drop_item` · id 115 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerdropitemevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Item` | `item` | no | The item (a single one, or the whole stack for the control-drop). | | `WholeStack` | `bool` | no | Control-drop of the whole stack. | ### PlayerPickupItemEvent A player is about to pick up an item entity. Cancel to leave it on the ground. `mc:player_pickup_item` · id 116 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerpickupitemevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `ItemEntity` | `Entity` | no | The item entity. | | `Item` | `item` | no | The stack on the ground. | ### PlayerConsumeEvent A player finished eating or drinking. Cancel to prevent the effects and the item consumption. `mc:player_consume` · id 117 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerconsumeevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Item` | `item` | no | The consumed item. | | `Hand` | `enum:Hand` | no | The hand. | ### PlayerInteractEvent A player clicked (left or right, air or block). Cancel to stop the default action: block breaking start, block use, item use. `mc:player_interact` · id 118 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerinteractevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Action` | `enum:InteractAction` | no | What was clicked. | | `Hand` | `enum:Hand` | no | The hand. | | `Item` | `item` | no | The item in that hand. | | `Pos` | `BlockPos` | no | The clicked block (only for block actions). | | `Face` | `enum:BlockFace` | no | The clicked face (only for block actions). | | `HasBlock` | `bool` | no | Whether pos/face are valid. | ### PlayerInteractEntityEvent A player right-clicked an entity. Cancel to stop the interaction. `mc:player_interact_entity` · id 119 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerinteractentityevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Target` | `Entity` | no | The clicked entity. | | `Hand` | `enum:Hand` | no | The hand. | | `Item` | `item` | no | The item in that hand. | ### PlayerToggleSneakEvent A player started or stopped sneaking. Cancel to ignore the change. `mc:player_toggle_sneak` · id 120 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playertogglesneakevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Sneaking` | `bool` | no | The new sneaking state. | ### PlayerToggleSprintEvent A player started or stopped sprinting. Cancel to ignore the change. `mc:player_toggle_sprint` · id 121 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playertogglesprintevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Sprinting` | `bool` | no | The new sprinting state. | ### PlayerToggleFlightEvent A player started or stopped flying (creative / may-fly). Cancel to ignore the change. `mc:player_toggle_flight` · id 122 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playertoggleflightevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Flying` | `bool` | no | The new flying state. | ### PlayerGameModeChangeEvent A player's game mode is about to change. Cancel to keep the old one. `mc:player_game_mode_change` · id 123 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playergamemodechangeevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `OldMode` | `enum:GameMode` | no | The current mode. | | `NewMode` | `enum:GameMode` | no | The new mode. | | `Cause` | `enum:GameModeChangeCause` | no | Why. | ### PlayerTeleportEvent A player is about to be teleported (command, plugin, ender pearl, ...). Cancel to stay; the destination may be changed. `mc:player_teleport` · id 124 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerteleportevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `From` | `Vec3d` | no | Current position. | | `To` | `Vec3d` | **yes** | Destination position. | | `FromWorld` | `world` | no | Current dimension. | | `ToWorld` | `world` | no | Destination dimension. | | `Cause` | `enum:TeleportCause` | no | Why. | ### PlayerPortalEvent A player is about to travel through a portal (nether portal, end portal, end gateway). Cancel to stay. `mc:player_portal` · id 125 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/playerportalevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `From` | `Vec3d` | no | Current position. | | `FromWorld` | `world` | no | Current dimension. | | `ToWorld` | `world` | no | Destination dimension. | | `Cause` | `enum:TeleportCause` | no | Which portal. | ### PlayerChangedWorldEvent A player arrived in another dimension. `mc:player_changed_world` · id 126 · not cancellable · engine: fired · [type reference](/reference/api/event-types/playerchangedworldevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `FromWorld` | `world` | no | The dimension left. | | `ToWorld` | `world` | no | The dimension entered. | ### PlayerResourcePackStatusEvent A client reported the state of a pushed resource pack. `mc:player_resource_pack_status` · id 127 · not cancellable · engine: fired · [type reference](/reference/api/event-types/playerresourcepackstatusevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `PackId` | `Uuid` | no | The pack id given in the push. | | `Status` | `enum:ResourcePackStatus` | no | The reported state. | ### PlayerLevelChangeEvent A player's experience level changed. `mc:player_level_change` · id 128 · not cancellable · engine: fired · [type reference](/reference/api/event-types/playerlevelchangeevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `OldLevel` | `int` | no | Level before. | | `NewLevel` | `int` | no | Level after. | ## Block ### BlockBreakEvent A player is about to break a block. Cancel to keep the block (the client is resynced). `mc:block_break` · id 20 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockbreakevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Pos` | `BlockPos` | no | The block. | | `BlockState` | `blockstate` | no | The block state about to be broken. | | `DropItems` | `bool` | **yes** | Whether the block drops its loot (the experience is controlled by experience). | | `Experience` | `int` | **yes** | The experience orbs dropped by the block, pre-filled with what vanilla would drop (ores, spawners, sculk; silk touch applied). Set 0 to drop none or any amount to drop that many; independent of drop_items. | | `Tool` | `item` | no | The item in the main hand. | ### BlockPlaceEvent A player is about to place a block. Cancel to refuse; 'block_state' may be rewritten. `mc:block_place` · id 21 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockplaceevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Pos` | `BlockPos` | no | Where the block goes. | | `Against` | `BlockPos` | no | The clicked block. | | `BlockState` | `blockstate` | **yes** | The state that will be placed. | | `Hand` | `byte` | no | The hand (Hand). | | `Item` | `item` | no | The block item used. | ### BlockDamageEvent A player started digging a block. Cancel to stop the dig. Set InstaBreak to break it at once. `mc:block_damage` · id 180 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockdamageevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Player` | `player` | no | The player. | | `Pos` | `BlockPos` | no | The block. | | `Face` | `enum:BlockFace` | no | The face being dug. | | `Tool` | `item` | no | The item in the main hand. | | `InstaBreak` | `bool` | **yes** | Break the block immediately (as in creative mode). | ### BlockIgniteEvent Fire is about to be placed in the world. Cancel to prevent it. `mc:block_ignite` · id 181 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockigniteevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | Where the fire goes. | | `Cause` | `enum:IgniteCause` | no | What ignites it. | | `IgnitingEntity` | `Entity` | no | The player or entity responsible, if any. | ### BlockBurnEvent Fire is about to destroy a block. Cancel to keep it. `mc:block_burn` · id 182 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockburnevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The burning block. | | `BlockState` | `blockstate` | no | Its state. | ### BlockSpreadEvent A block is spreading to a neighbour (grass, mycelium, vines, fire). Cancel to stop it. `mc:block_spread` · id 183 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockspreadevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The target position. | | `Source` | `BlockPos` | no | The spreading block. | | `NewState` | `blockstate` | no | The state that would be placed. | ### BlockFadeEvent A block is melting, drying or fading (ice, snow, farmland, fire). Cancel to keep it. `mc:block_fade` · id 184 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockfadeevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The block. | | `BlockState` | `blockstate` | no | The current state. | | `NewState` | `blockstate` | no | The state it fades to. | ### BlockFormEvent A block is forming through the environment (snow, ice, concrete, obsidian ...). Cancel to stop it. `mc:block_form` · id 185 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockformevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The position. | | `BlockState` | `blockstate` | no | The current state. | | `NewState` | `blockstate` | no | The state that forms. | ### BlockGrowEvent A plant is growing (crops, stems, cactus, sugar cane, ...). Cancel to stop it. `mc:block_grow` · id 186 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockgrowevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The block. | | `BlockState` | `blockstate` | no | The current state. | | `NewState` | `blockstate` | no | The state after growth. | ### BlockPistonExtendEvent A piston is about to extend. Cancel to keep it retracted. `mc:block_piston_extend` · id 187 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockpistonextendevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The piston. | | `Direction` | `enum:BlockFace` | no | The push direction. | | `Sticky` | `bool` | no | Whether it is a sticky piston. | | `MovedBlocks` | `int` | no | Number of blocks that move. | ### BlockPistonRetractEvent A piston is about to retract. Cancel to keep it extended. `mc:block_piston_retract` · id 188 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/blockpistonretractevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The piston. | | `Direction` | `enum:BlockFace` | no | The push direction. | | `Sticky` | `bool` | no | Whether it is a sticky piston. | | `MovedBlocks` | `int` | no | Number of blocks that move. | ### BlockRedstoneEvent A redstone component's output power changes. The new power may be changed. `mc:block_redstone` · id 189 · not cancellable · engine: fired · [type reference](/reference/api/event-types/blockredstoneevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `World` | `world` | no | The dimension. | | `Pos` | `BlockPos` | no | The block. | | `OldPower` | `int` | no | Power before (0-15). | | `NewPower` | `int` | **yes** | Power after (0-15). | ## Entity ### EntitySpawnEvent An entity is about to be added to the world. Cancel to prevent it. `mc:entity_spawn` · id 30 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/entityspawnevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Entity` | `Entity` | no | The entity (already created). | | `EntityType` | `key.entity_type` | no | The entity type key, e.g. minecraft:zombie. | | `Position` | `Vec3d` | no | Where it appears. | | `Reason` | `enum:SpawnReason` | no | Why it spawns. | ### EntityDamageEvent A living entity is about to take damage. Cancel to negate it. 'amount' (the damage after armor and enchantments, before absorption) may be rewritten. `mc:entity_damage` · id 31 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/entitydamageevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Target` | `Entity` | no | The damaged entity. | | `Attacker` | `Entity` | no | The responsible entity, none when there is no attacking entity. | | `DamageType` | `key.damage_type` | no | The damage type key, e.g. minecraft:fall. | | `Amount` | `float` | **yes** | Final damage after armor and enchantments. | | `Cause` | `enum:DamageCause` | no | The cause derived from the damage type. | | `RawAmount` | `float` | no | Damage before armor and enchantments. | | `BlockSource` | `BlockPos` | no | Position of the block that caused the damage. | | `HasBlockSource` | `bool` | no | Whether block_source is valid (cactus, magma, fire, ...). | ### EntityDeathEvent A living entity died. `mc:entity_death` · id 32 · not cancellable · engine: fired · [type reference](/reference/api/event-types/entitydeathevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Entity` | `Entity` | no | The entity. | | `Killer` | `Entity` | no | The responsible entity (none if there is none). | | `DamageType` | `key.damage_type` | no | The damage type key, e.g. minecraft:player_attack. | | `Cause` | `enum:DamageCause` | no | The cause derived from the damage type. | ### EntityCombustEvent An entity is about to be set on fire. Cancel to prevent it; the duration may be changed. `mc:entity_combust` · id 221 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/entitycombustevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Entity` | `Entity` | no | The entity. | | `DurationTicks` | `int` | **yes** | Burn time in ticks. | | `Source` | `Entity` | no | The entity that ignites it, if any. | ## Network ### PacketReceiveEvent A decoded-frame packet arrived (payload excludes the packet id). Cancel to drop it. `mc:packet_receive` · id 40 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/packetreceiveevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Connection` | `ulong` | no | Connection id. | | `Player` | `player` | no | The player entity (none before play). | | `State` | `byte` | no | Connection state. | | `PacketId` | `int` | no | Packet id. | | `Payload` | `NativeBytes` | no | Packet body (borrowed). | ### PacketSendEvent RESERVED: not fired yet (the per-connection send path encodes shared frames). A packet is about to be sent to one connection. `mc:packet_send` · id 41 · **cancellable** · engine: fired · [type reference](/reference/api/event-types/packetsendevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Connection` | `ulong` | no | Connection id. | | `Player` | `player` | no | The player entity (none before play). | | `State` | `byte` | no | Connection state. | | `PacketId` | `int` | no | Packet id. | | `Payload` | `NativeBytes` | no | Packet body (borrowed). | ## World ### ChunkLoadEvent A chunk became loaded. `mc:chunk_load` · id 50 · not cancellable · engine: fired · [type reference](/reference/api/event-types/chunkloadevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Dimension` | `int` | no | Dimension index. | | `X` | `int` | no | Chunk x. | | `Z` | `int` | no | Chunk z. | | `NewlyGenerated` | `bool` | no | True when the chunk was generated, false when loaded from disk. | ### ChunkUnloadEvent A chunk is about to be unloaded. `mc:chunk_unload` · id 51 · not cancellable · engine: fired · [type reference](/reference/api/event-types/chunkunloadevent/) | Field | Type | Mutable | Description | |---|---|---|---| | `Dimension` | `int` | no | Dimension index. | | `X` | `int` | no | Chunk x. | | `Z` | `int` | no | Chunk z. | --- # Command argument types Page: /reference/command-arguments/ The Brigadier argument types available to commands. The 62 argument types of the vanilla 26.3 `command_argument_type` registry (protocol 777), as the server's Brigadier engine implements them (`src/game/commands`). Plugin commands built with the v1 Brigadier API use these types; the current flat `Commands.Register` API gives you the raw argument strings instead. | Wire id | Type | Description | |---:|---|---| | 0 | `brigadier:bool` | true or false. | | 1 | `brigadier:float` | Single-precision number, optional min/max. | | 2 | `brigadier:double` | Double-precision number, optional min/max. | | 3 | `brigadier:integer` | Signed 32-bit integer, optional min/max. | | 4 | `brigadier:long` | Signed 64-bit integer, optional min/max. | | 5 | `brigadier:string` | A word, a quoted phrase, or the rest of the line (single word / quotable phrase / greedy phrase). | | 6 | `minecraft:entity` | An entity selector or player name; single or multiple, players only or any entity. | | 7 | `minecraft:game_profile` | A player profile: a name, a selector or a UUID (may name offline players). | | 8 | `minecraft:block_pos` | Integer block coordinates; supports ~ relative and ^ local coordinates. | | 9 | `minecraft:column_pos` | X and Z block coordinates. | | 10 | `minecraft:vec3` | Three double coordinates (x y z), with ~ and ^. | | 11 | `minecraft:vec2` | Two double coordinates (x z). | | 12 | `minecraft:block_state` | A block id with optional [properties] and {nbt}. | | 13 | `minecraft:block_predicate` | A block state or #tag with optional [properties] and {nbt}. | | 14 | `minecraft:item_stack` | An item id with optional [component=value] list. | | 15 | `minecraft:item_predicate` | An item id, #tag or * with component tests. | | 16 | `minecraft:team_color` | | | 17 | `minecraft:hex_color` | An RRGGBB hex colour. | | 18 | `minecraft:component` | A text component as SNBT or JSON. | | 19 | `minecraft:style` | A text component style. | | 20 | `minecraft:message` | The rest of the line, with entity selectors expanded. | | 21 | `minecraft:nbt_compound_tag` | A compound tag in SNBT. | | 22 | `minecraft:nbt_tag` | Any NBT tag in SNBT. | | 23 | `minecraft:nbt_path` | A path into an NBT structure. | | 24 | `minecraft:objective` | The name of a scoreboard objective. | | 25 | `minecraft:objective_criteria` | A scoreboard criterion (dummy, health, stat types ...). | | 26 | `minecraft:operation` | A scoreboard operation (=, +=, <, > ...). | | 27 | `minecraft:particle` | A particle id with its options. | | 28 | `minecraft:angle` | A yaw angle, with ~ for relative. | | 29 | `minecraft:rotation` | Yaw and pitch (two angles). | | 30 | `minecraft:scoreboard_slot` | A scoreboard display slot. | | 31 | `minecraft:score_holder` | An entity, selector, name or * that holds scores. | | 32 | `minecraft:swizzle` | A combination of x, y and z axes. | | 33 | `minecraft:team` | The name of a team. | | 34 | `minecraft:item_slot` | One inventory slot name (container.0, armor.head ...). | | 35 | `minecraft:item_slots` | A slot range name such as container.* or hotbar.*. | | 36 | `minecraft:resource_location` | A namespaced id such as minecraft:stone. | | 37 | `minecraft:function` | A function id or #tag. | | 38 | `minecraft:entity_anchor` | feet or eyes. | | 39 | `minecraft:int_range` | An integer range such as 1..5. | | 40 | `minecraft:float_range` | A decimal range such as 0.5..2. | | 41 | `minecraft:dimension` | A dimension id. | | 42 | `minecraft:gamemode` | survival, creative, adventure or spectator. | | 43 | `minecraft:time` | A duration with an optional unit (t, s, d). | | 44 | `minecraft:resource_or_tag` | A registry entry id or #tag. | | 45 | `minecraft:resource_or_tag_key` | | | 46 | `minecraft:resource` | A registry entry id. | | 47 | `minecraft:resource_key` | | | 48 | `minecraft:resource_selector` | | | 49 | `minecraft:template_mirror` | none, left_right or front_back. | | 50 | `minecraft:template_rotation` | none, clockwise_90, 180 or counterclockwise_90. | | 51 | `minecraft:heightmap` | A heightmap type. | | 52 | `minecraft:loot_table` | A loot table id or inline definition. | | 53 | `minecraft:loot_predicate` | | | 54 | `minecraft:loot_modifier` | | | 55 | `minecraft:context_float_provider` | | | 56 | `minecraft:context_int_provider` | | | 57 | `minecraft:slot_source` | | | 58 | `minecraft:dialog` | | | 59 | `minecraft:feature` | | | 60 | `minecraft:swing_animation` | | | 61 | `minecraft:uuid` | A UUID. | --- # Native systems Page: /reference/native-systems/ Engine systems a plugin can order against, replace or disable, generated from the sources. Scripted systems are ordered against the engine's own systems **by name** (see [systems](/concepts/systems/)). This list is generated by scanning the engine sources for `add_system` calls, so it matches the code the docs were built from. It shows the systems registered with a literal name; a few are registered from tables (for example the physics and per-dimension systems) and do not appear here, but the engine's system list prints every name at run time. | System | Phase | Placement | Registered in | |---|---|---|---| | `dragon:removals` | PreTick | end of phase | `src/game/dragon/system.cpp` | | `dragon:tick` | anchor's phase | after `mobs:post` | `src/game/dragon/system.cpp` | | `dragon:tracking` | Tracking | before `ramjet:mob_tracking` | `src/game/dragon/system.cpp` | | `dragon:untrack_removed` | PreTick | before `mobs:removals` | `src/game/dragon/system.cpp` | | `entities:removals` | PreTick | end of phase | `src/game/entities/system.cpp` | | `entities:thunder` | anchor's phase | after `entities:tick` | `src/game/entities/system.cpp` | | `entities:tick` | anchor's phase | after `mobs:post` | `src/game/entities/system.cpp` | | `entities:tracking` | Tracking | before `ramjet:mob_tracking` | `src/game/entities/system.cpp` | | `entities:untrack_removed` | PreTick | before `mobs:removals` | `src/game/entities/system.cpp` | | `gamecmd:locator_bar` | PostTick | end of phase | `src/game/gamecmd/waypoints.cpp` | | `gamecmd:profiler` | PreTick | end of phase | `src/game/gamecmd/cmd_profiler.cpp` | | `gamecmd:profiling` | PreTick | end of phase | `src/game/gamecmd/cmd_profiling.cpp` | | `gamecmd:schedule` | PreTick | end of phase | `src/game/gamecmd/functions.cpp` | | `gamecmd:store_pool` | PostTick | end of phase | `src/game/gamecmd/cmd_execute.cpp` | | `gamecmd:world_border` | PreTick | end of phase | `src/game/gamecmd/cmd_worldborder.cpp` | | `inventory:item_removals` | PreTick | end of phase | `src/game/inventory/item_entity.cpp` | | `inventory:items` | Tick | end of phase | `src/game/inventory/item_entity.cpp` | | `mobs:ai` | anchor's phase | before `vanilla:living_physics` | `src/game/mobs/service.cpp` | | `mobs:autosave` | PostTick | end of phase | `src/game/mobs/system.cpp` | | `mobs:boss_bars` | PostTick | end of phase | `src/game/mobs/system.cpp` | | `mobs:carriers` | anchor's phase | before `mobs:prepare` | `src/game/mobs/service.cpp` | | `mobs:dormancy` | PreTick | end of phase | `src/game/mobs/system.cpp` | | `mobs:entity_io` | PreTick | end of phase | `src/game/mobs/system.cpp` | | `mobs:eyes_of_ender` | anchor's phase | after `mobs:falling_blocks` | `src/game/mobs/eye_of_ender.cpp` | | `mobs:falling_blocks` | anchor's phase | after `mobs:post` | `src/game/mobs/falling.cpp` | | `mobs:game_events` | PostTick | end of phase | `src/game/mobs/system.cpp` | | `mobs:post` | anchor's phase | after `vanilla:living_physics` | `src/game/mobs/service.cpp` | | `mobs:prepare` | anchor's phase | before `vanilla:living_physics` | `src/game/mobs/service.cpp` | | `mobs:push` | anchor's phase | after `mobs:ai` | `src/game/mobs/service.cpp` | | `mobs:removals` | PreTick | end of phase | `src/game/mobs/service.cpp` | | `mobs:riders` | anchor's phase | after `mobs:post` | `src/game/mobs/service.cpp` | | `mobs:spawner` | anchor's phase | before `mobs:ai` | `src/game/mobs/spawner.cpp` | | `mobs:status` | PostTick | end of phase | `src/game/mobs/system.cpp` | | `mobs:untrack_removed` | PreTick | before `mobs:removals` | `src/game/mobs/system.cpp` | | `ramjet:advancements` | PostTick | end of phase | `src/game/advancements/service.cpp` | | `ramjet:block_entities[per dimension]` | Tick | end of phase | `src/server/main.cpp` | | `ramjet:chunk_integration` | AsyncIntegration | end of phase | `src/game/session/game_server.cpp` | | `ramjet:chunk_profile` | PostTick | end of phase | `src/server/main.cpp` | | `ramjet:chunk_pump` | PreTick | end of phase | `src/server/main.cpp` | | `ramjet:combat` | Tick | end of phase | `src/game/combat/server_glue.cpp` | | `ramjet:entity_tracking` | Tracking | end of phase | `src/game/tracking/player_tracking.cpp` | | `ramjet:level_time` | PreTick | end of phase | `src/game/session/game_server.cpp` | | `ramjet:memory_governor` | PreTick | end of phase | `src/server/main.cpp` | | `ramjet:memory_report` | PostTick | end of phase | `src/server/main.cpp` | | `ramjet:mob_tracking` | Tracking | end of phase | `src/game/mobs/system.cpp` | | `ramjet:network_receive` | NetworkReceive | end of phase | `src/game/session/game_server.cpp` | | `ramjet:network_send` | NetworkSend | end of phase | `src/game/session/game_server.cpp` | | `ramjet:orb_tracking` | Tracking | end of phase | `src/game/session/game_server.cpp` | | `ramjet:player_sync` | PostTick | end of phase | `src/game/session/game_server.cpp` | | `ramjet:portals` | PostTick | end of phase | `src/game/dimension/service.cpp` | | `ramjet:script_ban_flush` | NetworkSend | end of phase | `src/game/scriptbridge/bridge.cpp` | | `ramjet:script_menu_events` | AsyncIntegration | end of phase | `src/game/scriptbridge/bridge.cpp` | | `ramjet:script_sidebar_guard` | AsyncIntegration | end of phase | `src/game/scriptbridge/bridge.cpp` | | `ramjet:script_tick_end` | AsyncIntegration | end of phase | `src/game/scriptbridge/bridge.cpp` | | `ramjet:script_tick_start` | NetworkReceive | before `ramjet:network_receive` | `src/game/scriptbridge/bridge.cpp` | | `ramjet:sleep` | Tick | end of phase | `src/game/sleep/sleep_system.cpp` | | `ramjet:spawner_anim` | PreTick | end of phase | `src/game/blockentity/spawner_glue.cpp` | | `ramjet:spectator_camera` | PostTick | end of phase | `src/game/session/game_server.cpp` | | `ramjet:stats` | PostTick | end of phase | `src/game/stats/stats_service.cpp` | | `ramjet:system_profile` | PreTick | end of phase | `src/server/main.cpp` | | `vehicles:removals` | PreTick | end of phase | `src/game/vehicles/system.cpp` | | `vehicles:snapshot` | anchor's phase | before `mobs:prepare` | `src/game/vehicles/system.cpp` | | `vehicles:tick` | anchor's phase | after `mobs:post` | `src/game/vehicles/system.cpp` | | `vehicles:tracking` | Tracking | before `ramjet:mob_tracking` | `src/game/vehicles/system.cpp` | | `vehicles:untrack_removed` | PreTick | before `mobs:removals` | `src/game/vehicles/system.cpp` | | `village:tick` | anchor's phase | after `mobs:riders` | `src/game/village/service.cpp` | | `xp:orb_removals` | PreTick | end of phase | `src/game/xp/service.cpp` | | `xp:orbs` | Tick | end of phase | `src/game/xp/service.cpp` | | `xp:players` | PostTick | end of phase | `src/game/xp/service.cpp` | Names starting `vanilla:` are anchors that other systems order against (they are registered by tables the scanner does not follow). --- # BossBarTimer Page: /examples/bossbartimer/ Countdown timers on a boss bar Countdown timers on a boss bar Source: `src/scripting/managed/samples/BossBarTimer/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - event subscriptions - commands - Brigadier command trees (v1) - the scheduler - permissions (v1) - players API: boss bars, sidebar, titles (v1) Events used: [`PlayerJoinEvent`](/reference/api/event-types/playerjoinevent/) ## Build and run ```bash dotnet build src/scripting/managed/samples/BossBarTimer -c Release # the output lands in build/bin/dotnet/samples/BossBarTimer/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### BossBarTimer.cs **BossBarTimer.cs** ```csharp namespace BossBarTimer; /// Sample: /timer <seconds> <title> shows a countdown boss bar to every player (including those who join meanwhile), turns it red in the last ten seconds, /// plays a tick sound and announces the end with a title. The bar belongs to the plugin: it vanishes if the plugin unloads while it runs. [PluginInfo("BossBarTimer", Version = "1.0.0", Author = "Ramjet", Description = "Countdown timers on a boss bar")] public sealed class BossBarTimerPlugin : Plugin { private BossBar? _bar; private ScheduledTask? _task; private int _total; private int _left; protected override void OnEnable() { Permissions.Register("bossbartimer.use", PermissionDefault.Op, "Start and stop countdown timers"); Commands.Register(Command.Literal("timer").Permission("bossbartimer.use") .Then(Command.Literal("stop").Executes(ctx => { Stop(); ctx.Reply("Timer stopped"); return 1; })) .Then(Command.Argument("seconds", Arg.Int(1, 3600)) .Executes(ctx => Start(ctx.GetInt("seconds"), "Time left")) .Then(Command.Argument("title", Arg.GreedyString()).Executes(ctx => Start(ctx.GetInt("seconds"), ctx.GetString("title")))))); Events.On((ref PlayerJoinEvent e) => _bar?.AddViewer(e.Player)); } private int Start(int seconds, string title) { Stop(); _total = _left = seconds; _bar = BossBars.Create("countdown", Component.Text(title), BossBarColor.Green, BossBarStyle.Segmented10, 1f); foreach (var player in Server.Players) _bar.AddViewer(player); _task = Scheduler.RunEvery(20, Tick, 20); Update(title); return seconds; } private void Tick() { if (_bar is null) return; _left--; if (_left <= 0) { var done = _bar.Title; Stop(); foreach (var player in Server.Players) { player.ShowTitle(Component.Text("Time is up!").Color(TextColor.Red).Bold(), done, new TitleTimes(5, 40, 10)); player.PlaySound("minecraft:entity.player.levelup", SoundCategory.Master, 1f, 0.8f); } return; } Update(null); if (_left <= 10) foreach (var player in Server.Players) player.PlaySound("minecraft:block.note_block.hat", SoundCategory.Master, 0.6f, 1.2f); } private void Update(string? title) { if (_bar is null) return; _bar.Progress = (float)_left / _total; _bar.Color = _left <= 10 ? BossBarColor.Red : _left <= _total / 2 ? BossBarColor.Yellow : BossBarColor.Green; if (title is not null) _bar.Title = Component.Text(title); } private void Stop() { _task?.Cancel(); _task = null; _bar?.Remove(); _bar = null; } } ``` ### BossBarTimer.csproj **BossBarTimer.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/BossBarTimer/ ``` --- # DiscordWebhook Page: /examples/discordwebhook/ Join and quit messages to a Discord webhook Join and quit messages to a Discord webhook Source: `src/scripting/managed/samples/DiscordWebhook/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - event subscriptions - config (v1) Events used: [`PlayerJoinEvent`](/reference/api/event-types/playerjoinevent/), [`PlayerQuitEvent`](/reference/api/event-types/playerquitevent/) ## Build and run ```bash dotnet build src/scripting/managed/samples/DiscordWebhook -c Release # the output lands in build/bin/dotnet/samples/DiscordWebhook/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### DiscordWebhook.cs **DiscordWebhook.cs** ```csharp using System.Text; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace DiscordWebhook; /// The settings of the DiscordWebhook plugin (plugins/discordwebhook/config.json, written with these defaults on the first run). /// The webhook URL is a secret: anyone who has it can post to the channel. public sealed class DiscordWebhookConfig { /// The Discord webhook URL (https://discord.com/api/webhooks/...). Empty = the plugin posts nothing. public string WebhookUrl { get; set; } = ""; /// The line posted when a player joins ({player} is the name). Empty = no line. public string JoinMessage { get; set; } = "{player} joined the server"; /// The line posted when a player leaves ({player} is the name). Empty = no line. public string QuitMessage { get; set; } = "{player} left the server"; /// The name the webhook posts under. public string Username { get; set; } = "Ramjet"; } [JsonSerializable(typeof(DiscordWebhookConfig))] internal sealed partial class DiscordWebhookJson : JsonSerializerContext; /// Sample: posts join and quit messages to a Discord webhook. It shows the plugin HTTP client: Http.Client sends the request /// through the server (the server resolves the name, refuses private, loopback and metadata addresses, and applies the limits of /// scripting.http; see docs/agents/http-egress.md). The answer returns on the tick thread; nothing here blocks the tick. [PluginInfo("DiscordWebhook", Version = "1.0.0", Author = "Ramjet", Description = "Join and quit messages to a Discord webhook")] public sealed class DiscordWebhookPlugin : Plugin { private Config _config = null!; protected override void OnEnable() { _config = Config.Load(DiscordWebhookJson.Default.DiscordWebhookConfig); Events.On((ref PlayerJoinEvent e) => Post(_config.Value.JoinMessage, e.Username.ToString())); Events.On((ref PlayerQuitEvent e) => Post(_config.Value.QuitMessage, e.Username.ToString())); } /// Starts one post. Fire and forget: the task is not awaited by the tick, and its failure is logged by SendAsync. private void Post(string template, string player) { var config = _config.Value; if (config.WebhookUrl.Length == 0 || template.Length == 0) return; var payload = new JsonObject { ["content"] = template.Replace("{player}", player), ["username"] = config.Username, }; _ = SendAsync(config.WebhookUrl, payload.ToJsonString()); } private async Task SendAsync(string url, string json) { try { using var content = new StringContent(json, Encoding.UTF8, "application/json"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); using HttpResponseMessage response = await Http.Client.PostAsync(url, content, cts.Token); // the webhook URL is not logged (it is the secret); the status is enough to diagnose if (!response.IsSuccessStatusCode) Log.Warn($"the Discord webhook answered {(int)response.StatusCode}"); } catch (Exception e) { Log.Warn($"the Discord webhook post failed: {e.Message}"); } } } ``` ### DiscordWebhook.csproj **DiscordWebhook.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/DiscordWebhook/ ``` ### plugin.json **plugin.json** ```json { "id": "discordwebhook", "name": "DiscordWebhook", "version": "1.0.0", "api": "1.0", "authors": ["Ramjet"], "description": "Join and quit messages to a Discord webhook (set the URL in config.json)" } ``` --- # Greeter Page: /examples/greeter/ Join/quit messages and a server list MOTD Join/quit messages and a server list MOTD Source: `src/scripting/managed/samples/Greeter/` · version 1.1.0. The code below is read from the repository at build time. ## What it shows - event subscriptions - commands - Brigadier command trees (v1) - config (v1) Events used: [`PlayerJoinEvent`](/reference/api/event-types/playerjoinevent/), [`PlayerQuitEvent`](/reference/api/event-types/playerquitevent/), [`ServerListPingEvent`](/reference/api/event-types/serverlistpingevent/) ## Build and run ```bash dotnet build src/scripting/managed/samples/Greeter -c Release # the output lands in build/bin/dotnet/samples/Greeter/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### Greeter.cs **Greeter.cs** ```csharp using System.Text.Json.Serialization; namespace Greeter; /// The settings of the Greeter plugin. The defaults below are written to plugins/greeter/config.json the first time the plugin runs; edit the file and /// run /greeter reload to apply changes. Colours use legacy & codes. public sealed class GreeterConfig { /// The line everybody sees when a player joins ({player} is the name). public string JoinMessage { get; set; } = "&8[&a+&8] &e{player}"; /// The line everybody sees when a player leaves. public string QuitMessage { get; set; } = "&8[&c-&8] &7{player}"; /// A private message sent to the joining player; empty to disable. public string Welcome { get; set; } = "&6Welcome to the server, &e{player}&6! Have fun."; /// The server list description (two lines separated by \n); empty keeps the server's own. public string Motd { get; set; } = "&a&lA C# powered server\n&7plugins are the whole ecosystem"; } [JsonSerializable(typeof(GreeterConfig))] internal sealed partial class GreeterJson : JsonSerializerContext; /// Sample: greets players on join with a coloured broadcast and a private message, says goodbye on quit and /// customises the server list entry. Everything is configurable in config.json (typed , defaults written on first run, live reload). [PluginInfo("Greeter", Version = "1.1.0", Author = "Ramjet", Description = "Join/quit messages and a server list MOTD")] public sealed class GreeterPlugin : Plugin { private Config _config = null!; protected override void OnEnable() { _config = Config.Load(GreeterJson.Default.GreeterConfig); _config.Watch = true; // edits of config.json are picked up without a command Commands.Register(Command.Literal("greeter") .Then(Command.Literal("reload").Permission("greeter.reload").Executes(ctx => { bool ok = _config.Reload(); ctx.Reply(ok ? "Greeter configuration reloaded" : "config.json is invalid, see the server log (the previous values stay active)"); return ok ? 1 : 0; }))); Events.On((ref PlayerJoinEvent e) => { string name = e.Username.ToString(); var config = _config.Value; // the message everybody sees (replaces " joined the game") if (config.JoinMessage.Length == 0) e.SetJoinMessage(Component.Empty); else e.SetJoinMessage(Component.Legacy(config.JoinMessage.Replace("{player}", name))); // a private welcome, sent right away if (config.Welcome.Length > 0) e.Player.SendMessage(Component.Legacy(config.Welcome.Replace("{player}", name))); }); Events.On((ref PlayerQuitEvent e) => { var config = _config.Value; e.SetQuitMessage(config.QuitMessage.Length == 0 ? Component.Empty : Component.Legacy(config.QuitMessage.Replace("{player}", e.Username.ToString()))); }); Events.On((ref ServerListPingEvent e) => { if (_config.Value.Motd.Length > 0) e.SetMotd(Component.Legacy(_config.Value.Motd.Replace("\\n", "\n"))); Log.Debug($"ping from {e.Address.ToString()}"); }); } } ``` ### Greeter.csproj **Greeter.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/Greeter/ ``` ### plugin.json **plugin.json** ```json { "id": "greeter", "name": "Greeter", "version": "1.1.0", "api": "1.0", "authors": ["Ramjet"], "description": "Join/quit messages and a server list MOTD", "permissions": [ { "node": "greeter.reload", "default": "op:3", "description": "Reload the Greeter configuration" } ] } ``` --- # HelloPlugin Page: /examples/helloplugin/ Sample plugin: events (cancellation and mutation), a component, a per-tick system, a command and the scheduler. Sample plugin: events (cancellation and mutation), a component, a per-tick system, a command and the scheduler. Source: `src/scripting/managed/samples/HelloPlugin/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - event subscriptions - cancelling events - event priorities - ECS components - ECS queries - per-tick systems - commands - the scheduler Events used: [`PlayerJoinEvent`](/reference/api/event-types/playerjoinevent/), [`PlayerChatEvent`](/reference/api/event-types/playerchatevent/), [`EntityDamageEvent`](/reference/api/event-types/entitydamageevent/) ## Build and run ```bash dotnet build src/scripting/managed/samples/HelloPlugin -c Release # the output lands in build/bin/dotnet/samples/HelloPlugin/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### HelloPlugin.cs **HelloPlugin.cs** ```csharp using Mc.Scripting; using Mc.Scripting.Events; namespace HelloPlugin; /// Per-player tick counter (plain data, stored in flecs). public struct JoinTicks { public long Ticks; } /// Sample plugin: events (cancellation and mutation), a component, a per-tick system, /// a command and the scheduler. [PluginInfo("HelloPlugin", Version = "1.0.0", Author = "Ramjet")] public sealed class HelloPlugin : Plugin { private Query _players = null!; private int _chatSeen; protected override void OnEnable() { Ecs.RegisterComponent("hello:join_ticks"); _players = Ecs.Query(); Events.On((ref PlayerJoinEvent e) => { Log.Info($"welcome {e.Username.ToString()}!"); e.Player.Set(new JoinTicks()); }); // Cancel chat containing "spam". High priority = runs late, after Normal handlers. Events.On((ref PlayerChatEvent e) => { _chatSeen++; if (e.Message.Contains("spam", StringComparison.OrdinalIgnoreCase)) e.Cancelled = true; }, EventPriority.High); // Halve damage dealt by entities. Events.On((ref EntityDamageEvent e) => { if (!e.Attacker.IsNull) e.Amount *= 0.5f; }); // Count ticks for every player that joined. Systems.Add("hello:count_ticks", Phase.Tick, _ => { foreach (var chunk in _players) { var ticks = chunk.Field(0); for (int i = 0; i < ticks.Length; i++) ticks[i].Ticks++; } }); Commands.Register("hello", ctx => { ctx.Reply($"Hello, {(ctx.Args.Length > 0 ? ctx.Args[0] : "world")}! (chat seen: {_chatSeen})"); ctx.Result = ctx.Args.Length; }, "Says hello"); Scheduler.RunEvery(20 * 60, () => Log.Info("still alive")); } protected override void OnDisable() => Log.Info("goodbye"); } ``` ### HelloPlugin.csproj **HelloPlugin.csproj** ```xml HelloPlugin true $(McOutDir)samples/HelloPlugin/ ``` --- # Homes Page: /examples/homes/ Set homes and teleport back to them Set homes and teleport back to them Source: `src/scripting/managed/samples/Homes/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - commands - Brigadier command trees (v1) - config (v1) ## Build and run ```bash dotnet build src/scripting/managed/samples/Homes -c Release # the output lands in build/bin/dotnet/samples/Homes/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### Homes.cs **Homes.cs** ```csharp using System.Text.Json; using System.Text.Json.Serialization; namespace Homes; /// Settings of the Homes plugin (plugins/homes/config.json, written with these defaults on first run). public sealed class HomesConfig { /// How many homes a player may set (players with homes.unlimited are not limited). public int MaxHomes { get; set; } = 3; /// Seconds a player must wait between two teleports (players with homes.cooldown.bypass do not wait). public int CooldownSeconds { get; set; } = 10; /// The sound played to the player after teleporting; empty for none. public string TeleportSound { get; set; } = "minecraft:entity.enderman.teleport"; } /// One saved home. public sealed class SavedHome { /// The dimension key. public string World { get; set; } = "minecraft:overworld"; /// X. public double X { get; set; } /// Y. public double Y { get; set; } /// Z. public double Z { get; set; } /// Yaw. public float Yaw { get; set; } /// Pitch. public float Pitch { get; set; } } /// The saved homes of every player (homes.json): profile id to home name to home. public sealed class HomesData { /// Homes per player. public Dictionary> Players { get; set; } = []; } [JsonSerializable(typeof(HomesConfig))] [JsonSerializable(typeof(HomesData))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class HomesJson : JsonSerializerContext; /// Sample: /sethome, /home, /delhome, /homes with a typed config, permission nodes declared in plugin.json, a data file in the plugin's /// data folder, tab completion of the player's own home names, a cooldown, cross-dimension teleporting and feedback with sound and titles. [PluginInfo("Homes", Version = "1.0.0", Author = "Ramjet", Description = "Set homes and teleport back to them")] public sealed class HomesPlugin : Plugin { private Config _config = null!; private HomesData _data = new(); private readonly Dictionary _lastTeleport = []; protected override void OnEnable() { _config = Config.Load(HomesJson.Default.HomesConfig); if (Config.ReadText("homes.json") is { } json) { try { _data = JsonSerializer.Deserialize(json, HomesJson.Default.HomesData) ?? new HomesData(); // System.Text.Json builds ordinal dictionaries: home names compare without regard to case, as /home Base and /home base should foreach (var key in _data.Players.Keys.ToList()) _data.Players[key] = new Dictionary(_data.Players[key], StringComparer.OrdinalIgnoreCase); } catch (JsonException e) { Log.Error("homes.json is invalid, starting with no homes (the file is kept)", e); } } Commands.Register(Command.Literal("sethome").Permission("homes.use") .Executes(ctx => SetHome(ctx, "home")) .Then(Command.Argument("name", Arg.Word()).Suggests(OwnHomes).Executes(ctx => SetHome(ctx, ctx.GetString("name"))))); Commands.Register(Command.Literal("home").Permission("homes.use") .Executes(ctx => GoHome(ctx, "home")) .Then(Command.Argument("name", Arg.Word()).Suggests(OwnHomes).Executes(ctx => GoHome(ctx, ctx.GetString("name")))), description: "Teleport to one of your homes"); Commands.Register(Command.Literal("delhome").Permission("homes.use") .Then(Command.Argument("name", Arg.Word()).Suggests(OwnHomes).Executes(ctx => { var player = ctx.RequirePlayer(); var homes = HomesOf(player, create: false); if (homes is null || !homes.Remove(ctx.GetString("name"))) throw new CommandFailure($"You have no home called '{ctx.GetString("name")}'"); Save(); ctx.Reply($"Deleted home '{ctx.GetString("name")}'"); return 1; }))); Commands.Register(Command.Literal("homes").Permission("homes.use").Executes(ctx => { var player = ctx.RequirePlayer(); var homes = HomesOf(player, create: false); if (homes is null || homes.Count == 0) throw new CommandFailure("You have no homes. Use /sethome "); foreach (var (name, home) in homes.OrderBy(h => h.Key)) ctx.Reply(Component.Text($"{name}: ").Color(TextColor.Gold).Append(Component.Text($"{home.X:0} {home.Y:0} {home.Z:0} ({home.World})").Color(TextColor.Gray))); return homes.Count; })); } private IEnumerable OwnHomes(CommandContext ctx) => ctx.Player is { } p && HomesOf(p, create: false) is { } homes ? homes.Keys.Order() : []; private Dictionary? HomesOf(Player player, bool create) { string key = player.Uuid.ToString(); if (_data.Players.TryGetValue(key, out var homes)) return homes; return create ? _data.Players[key] = new Dictionary(StringComparer.OrdinalIgnoreCase) : null; } private void Save() => Config.WriteText("homes.json", JsonSerializer.Serialize(_data, HomesJson.Default.HomesData)); private int SetHome(CommandContext ctx, string name) { var player = ctx.RequirePlayer(); var homes = HomesOf(player, create: true)!; bool exists = homes.ContainsKey(name); if (!exists && homes.Count >= _config.Value.MaxHomes && !player.HasPermission("homes.unlimited")) throw new CommandFailure($"You can have at most {_config.Value.MaxHomes} homes. Delete one with /delhome "); var at = player.Location; homes[name] = new SavedHome { World = at.World.Key, X = at.X, Y = at.Y, Z = at.Z, Yaw = at.Yaw, Pitch = at.Pitch }; Save(); ctx.Reply(Component.Text(exists ? "Moved home " : "Set home ").Color(TextColor.Green).Append(Component.Text(name).Color(TextColor.Yellow))); return 1; } private int GoHome(CommandContext ctx, string name) { var player = ctx.RequirePlayer(); var homes = HomesOf(player, create: false); if (homes is null || !homes.TryGetValue(name, out var home)) throw new CommandFailure(name == "home" ? "You have no home yet. Use /sethome" : $"You have no home called '{name}'"); ulong now = Server.CurrentTick; if (!player.HasPermission("homes.cooldown.bypass") && _lastTeleport.TryGetValue(player.Uuid, out ulong last)) { long waitTicks = _config.Value.CooldownSeconds * 20L - (long)(now - last); if (waitTicks > 0) throw new CommandFailure($"Wait {(waitTicks + 19) / 20} more seconds before teleporting again"); } var world = Server.GetWorld(home.World) ?? player.World; if (!player.Teleport(new Location(world, home.X, home.Y, home.Z, home.Yaw, home.Pitch))) throw new CommandFailure("The teleport was cancelled"); _lastTeleport[player.Uuid] = now; if (_config.Value.TeleportSound.Length > 0) player.PlaySound(_config.Value.TeleportSound, SoundCategory.Players); player.ShowTitle(Component.Text("Home").Color(TextColor.Green), Component.Text(name).Color(TextColor.Yellow), TitleTimes.Default); return 1; } } ``` ### Homes.csproj **Homes.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/Homes/ ``` ### plugin.json **plugin.json** ```json { "id": "homes", "name": "Homes", "version": "1.0.0", "api": "1.0", "authors": ["Ramjet"], "description": "Set homes and teleport back to them", "permissions": [ { "node": "homes.use", "default": "true", "description": "Use /sethome, /home, /delhome and /homes" }, { "node": "homes.unlimited", "default": "op:3", "description": "Set more homes than the configured maximum" }, { "node": "homes.cooldown.bypass", "default": "op:3", "description": "Teleport without waiting for the cooldown" } ] } ``` --- # Kits Page: /examples/kits/ Claimable kits with cooldowns Claimable kits with cooldowns Source: `src/scripting/managed/samples/Kits/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - commands - Brigadier command trees (v1) - config (v1) - permissions (v1) ## Build and run ```bash dotnet build src/scripting/managed/samples/Kits -c Release # the output lands in build/bin/dotnet/samples/Kits/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### Kits.cs **Kits.cs** ```csharp using System.Text.Json.Serialization; namespace Kits; /// A status effect a kit grants. public sealed class KitEffect { /// The effect key, e.g. minecraft:strength. public string Effect { get; set; } = "minecraft:regeneration"; /// How long it lasts in seconds. public int Seconds { get; set; } = 30; /// The level minus one. public int Amplifier { get; set; } } /// One kit. public sealed class KitConfig { /// Seconds a player must wait before claiming this kit again. public int CooldownSeconds { get; set; } = 600; /// Experience levels granted. public int ExperienceLevels { get; set; } /// Food level restored (0 leaves hunger alone). public int Food { get; set; } /// Status effects granted. public List Effects { get; set; } = []; /// The message shown when the kit is claimed. public string Message { get; set; } = "Kit claimed!"; } /// The settings of the Kits plugin: the kits by name. Item kits arrive with the inventory API; this version grants effects, experience and food. public sealed class KitsConfig { /// The kits. public Dictionary Kits { get; set; } = new() { ["starter"] = new KitConfig { CooldownSeconds = 3600, Food = 20, ExperienceLevels = 3, Effects = [new KitEffect { Effect = "minecraft:regeneration", Seconds = 20 }, new KitEffect { Effect = "minecraft:speed", Seconds = 60 }], Message = "Welcome kit: food, three levels and a short boost", }, ["warrior"] = new KitConfig { CooldownSeconds = 1800, Effects = [new KitEffect { Effect = "minecraft:strength", Seconds = 120, Amplifier = 1 }, new KitEffect { Effect = "minecraft:resistance", Seconds = 120 }], Message = "Warrior kit: strength and resistance for two minutes", }, }; } [JsonSerializable(typeof(KitsConfig))] internal sealed partial class KitsJson : JsonSerializerContext; /// Sample: /kit <name> with kits defined in the config, per-kit permission nodes declared at runtime from the config (kits.kit.<name>), cooldowns /// measured in server ticks, tab completion that only offers the kits the player may claim, and a live config reload. [PluginInfo("Kits", Version = "1.0.0", Author = "Ramjet", Description = "Claimable kits with cooldowns")] public sealed class KitsPlugin : Plugin { private Config _config = null!; private readonly Dictionary<(Uuid, string), ulong> _claimed = []; protected override void OnEnable() { _config = Config.Load(KitsJson.Default.KitsConfig); DeclarePermissions(); _config.Changed += _ => DeclarePermissions(); // new kits get their node without a restart _config.Watch = true; Permissions.Register("kits.reload", PermissionDefault.OpLevel(3), "Reload the kits configuration"); Commands.Register(Command.Literal("kit") .Then(Command.Argument("name", Arg.Word()).Suggests(ctx => ctx.Player is { } p ? Available(p) : _config.Value.Kits.Keys).Executes(Claim)) .Then(Command.Literal("reload").Permission("kits.reload").Executes(ctx => { ctx.Reply(_config.Reload() ? "Kits reloaded" : "kits config is invalid, see the log"); return 1; })), description: "Claim a kit"); Commands.Register(Command.Literal("kits").Executes(ctx => { var player = ctx.RequirePlayer(); var names = Available(player).Order().ToList(); ctx.Reply(names.Count == 0 ? "No kits available" : "Kits: " + string.Join(", ", names)); return names.Count; })); } private void DeclarePermissions() { foreach (string name in _config.Value.Kits.Keys) Permissions.Register($"kits.kit.{name}", PermissionDefault.True, $"Claim the {name} kit"); } private IEnumerable Available(Player player) => _config.Value.Kits.Keys.Where(k => player.HasPermission($"kits.kit.{k}")); private int Claim(CommandContext ctx) { var player = ctx.RequirePlayer(); string name = ctx.GetString("name"); if (!_config.Value.Kits.TryGetValue(name, out var kit)) throw new CommandFailure($"There is no kit called '{name}'"); if (!player.HasPermission($"kits.kit.{name}")) throw new CommandFailure($"You may not use the {name} kit"); ulong now = Server.CurrentTick; if (_claimed.TryGetValue((player.Uuid, name), out ulong last)) { long wait = kit.CooldownSeconds * 20L - (long)(now - last); if (wait > 0) throw new CommandFailure($"The {name} kit is ready again in {(wait + 19) / 20} seconds"); } _claimed[(player.Uuid, name)] = now; if (kit.Food > 0) { player.FoodLevel = Math.Max(player.FoodLevel, kit.Food); player.Saturation = Math.Max(player.Saturation, kit.Food); } if (kit.ExperienceLevels > 0) player.GiveExperienceLevels(kit.ExperienceLevels); foreach (var effect in kit.Effects) player.AddEffect(new PotionEffect(effect.Effect, effect.Seconds * 20, effect.Amplifier)); player.PlaySound("minecraft:entity.player.levelup", SoundCategory.Players, 0.8f); player.SendActionBar(Component.Text(kit.Message).Color(TextColor.Green)); return 1; } } ``` ### Kits.csproj **Kits.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/Kits/ ``` --- # PermissionAdmin Page: /examples/permissionadmin/ /perm: manage groups and player permissions of the built-in backend /perm: manage groups and player permissions of the built-in backend Source: `src/scripting/managed/samples/PermissionAdmin/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - commands - Brigadier command trees (v1) - permissions (v1) ## Build and run ```bash dotnet build src/scripting/managed/samples/PermissionAdmin -c Release # the output lands in build/bin/dotnet/samples/PermissionAdmin/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### PermissionAdmin.cs **PermissionAdmin.cs** ```csharp namespace PermissionAdmin; /// Sample: the /perm admin command for the built-in permission backend (groups, per-player entries, reload). It shows how a plugin declares permission nodes, /// protects commands with them and talks to ; a plugin that replaces the backend can reuse the same command layout. /// /// /perm user <player> set|unset|check <node> [true|false] /// /perm user <player> group add|remove <group> /// /perm group <group> create [weight] | delete | set <node> [true|false] | unset <node> | parent <group> | list /// /perm list | /perm reload /// [PluginInfo("PermissionAdmin", Version = "1.0.0", Author = "Ramjet", Description = "/perm: manage groups and player permissions of the built-in backend")] public sealed class PermissionAdminPlugin : Plugin { private const string Admin = "permissions.admin"; protected override void OnEnable() { Permissions.Register(Admin, PermissionDefault.OpLevel(3), "Use /perm to edit permissions"); var node = Command.Literal("perm").Permission(Admin) .Then(Command.Literal("reload").Executes(ctx => { PermissionManager.Reload(); ctx.Reply("Permissions reloaded"); return 1; })) .Then(Command.Literal("list").Executes(ctx => { foreach (var p in PermissionManager.Registered) ctx.Reply($"{p.Node} (default {p.Default}): {p.Description}"); return PermissionManager.Registered.Count; })) .Then(Command.Literal("user").Then(Command.Argument("player", Arg.Player()) .Then(Command.Literal("set").Then(Command.Argument("node", Arg.GreedyString()).Executes(SetUser))) .Then(Command.Literal("unset").Then(Command.Argument("node", Arg.GreedyString()).Executes(ctx => { var player = ctx.GetPlayer("player"); Backend().SetPlayerPermission(player.Uuid, ctx.GetString("node"), null, player.Name); ctx.Reply($"Removed {ctx.GetString("node")} from {player.Name}"); return 1; }))) .Then(Command.Literal("check").Then(Command.Argument("node", Arg.GreedyString()).Executes(ctx => { var player = ctx.GetPlayer("player"); string node = ctx.GetString("node"); bool has = player.HasPermission(node); var explicitValue = PermissionManager.GetExplicit(player.Uuid, node); ctx.Reply($"{player.Name} {(has ? "has" : "does not have")} {node} (explicit: {explicitValue})"); return has ? 1 : 0; }))) .Then(Command.Literal("group") .Then(Command.Literal("add").Then(Command.Argument("group", Arg.Word()).Suggests(GroupNames).Executes(ctx => { var player = ctx.GetPlayer("player"); Backend().AddToGroup(player.Uuid, Group(ctx), player.Name); ctx.Reply($"{player.Name} is now in {Group(ctx)}"); return 1; }))) .Then(Command.Literal("remove").Then(Command.Argument("group", Arg.Word()).Suggests(GroupNames).Executes(ctx => { var player = ctx.GetPlayer("player"); bool removed = Backend().RemoveFromGroup(player.Uuid, Group(ctx)); ctx.Reply(removed ? $"{player.Name} left {Group(ctx)}" : $"{player.Name} was not in {Group(ctx)}"); return removed ? 1 : 0; })))) .Then(Command.Literal("info").Executes(ctx => { var player = ctx.GetPlayer("player"); ctx.Reply($"{player.Name}: op level {player.PermissionLevel}, groups [{string.Join(", ", PermissionManager.Provider.Groups(player.Uuid))}]"); foreach (var (n, v) in Backend().PlayerEntries(player.Uuid)) ctx.Reply($" {n} = {v}"); return 1; })))) .Then(Command.Literal("group").Then(Command.Argument("group", Arg.Word()).Suggests(GroupNames) .Then(Command.Literal("create").Executes(ctx => CreateGroup(ctx, 0)) .Then(Command.Argument("weight", Arg.Int(0, 10000)).Executes(ctx => CreateGroup(ctx, ctx.GetInt("weight"))))) .Then(Command.Literal("delete").Executes(ctx => { bool ok = Backend().DeleteGroup(Group(ctx)); ctx.Reply(ok ? $"Deleted {Group(ctx)}" : $"No group {Group(ctx)} (the default group cannot be deleted)"); return ok ? 1 : 0; })) .Then(Command.Literal("set").Then(Command.Argument("node", Arg.GreedyString()).Executes(SetGroup))) .Then(Command.Literal("unset").Then(Command.Argument("node", Arg.GreedyString()).Executes(ctx => { Backend().SetGroupPermission(Group(ctx), ctx.GetString("node"), null); ctx.Reply($"Removed {ctx.GetString("node")} from {Group(ctx)}"); return 1; }))) .Then(Command.Literal("parent").Then(Command.Argument("parent", Arg.Word()).Suggests(GroupNames).Executes(ctx => { Backend().SetGroupParents(Group(ctx), [ctx.GetString("parent")]); ctx.Reply($"{Group(ctx)} inherits from {ctx.GetString("parent")}"); return 1; }))) .Then(Command.Literal("list").Executes(ctx => { var entries = Backend().GroupEntries(Group(ctx)); foreach (var (n, v) in entries) ctx.Reply($" {n} = {v}"); return entries.Count; })))); Commands.Register(node, aliases: ["permission"], description: "Manage permissions"); } private static FilePermissionProvider Backend() => PermissionManager.Provider as FilePermissionProvider ?? throw new CommandFailure($"The permission backend is {PermissionManager.Provider.Name}; manage permissions with its own commands"); private static string Group(CommandContext ctx) => ctx.GetString("group"); private static IEnumerable GroupNames(CommandContext ctx) => PermissionManager.Provider is FilePermissionProvider file ? file.GroupNames : []; // "node [true|false]": the node comes from a greedy string because a wildcard ('homes.*', '*') is not valid in an unquoted word private static (string Node, bool Value) NodeAndValue(CommandContext ctx) { string text = ctx.GetString("node").Trim(); int space = text.LastIndexOf(' '); if (space > 0 && bool.TryParse(text[(space + 1)..], out bool value)) return (text[..space].Trim(), value); return (text, true); } private static int SetUser(CommandContext ctx) { var player = ctx.GetPlayer("player"); var (node, value) = NodeAndValue(ctx); Backend().SetPlayerPermission(player.Uuid, node, value, player.Name); ctx.Reply($"{player.Name}: {node} = {value}"); return 1; } private static int SetGroup(CommandContext ctx) { var (node, value) = NodeAndValue(ctx); Backend().SetGroupPermission(Group(ctx), node, value); ctx.Reply($"{Group(ctx)}: {node} = {value}"); return 1; } private static int CreateGroup(CommandContext ctx, int weight) { Backend().CreateGroup(Group(ctx), weight); ctx.Reply($"Group {Group(ctx)} (weight {weight}) ready"); return 1; } } ``` ### PermissionAdmin.csproj **PermissionAdmin.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/PermissionAdmin/ ``` --- # RubyOre Page: /examples/rubyore/ A custom block and the item it drops A custom block and the item it drops Source: `src/scripting/managed/samples/RubyOre/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - cancelling events - commands - Brigadier command trees (v1) - permissions (v1) ## Build and run ```bash dotnet build src/scripting/managed/samples/RubyOre -c Release # the output lands in build/bin/dotnet/samples/RubyOre/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### RubyOre.cs **RubyOre.cs** ```csharp namespace RubyOre; /// Sample: a custom block. rubyore:ruby_ore lives in a free state of the note block (the world stores plain vanilla block states, a server without the plugin shows a note /// block), mines in a second, drops one to three rubyore:ruby items and answers a right click. /rubyore give hands out the ore as an item that places the block, /// /rubyore pack sends the resource pack that gives the block its texture (see the RubyWand sample for the pack host). [PluginInfo("RubyOre", Version = "1.0.0", Author = "Ramjet", Description = "A custom block and the item it drops")] public sealed class RubyOrePlugin : Plugin { private CustomItem _ruby = null!; private CustomItem _oreItem = null!; private CustomBlock _ore = null!; protected override void OnEnable() { Permissions.Register("rubyore.give", PermissionDefault.OpLevel(2), "Give out ruby ore and send the pack"); _ruby = new CustomItem("rubyore:ruby", "minecraft:paper") .Model("rubyore:ruby") .Name(Component.Text("Ruby").Color(TextColor.Red)) .WithRarity(ItemRarity.Rare); Content.Items.Register(_ruby); _ore = new CustomBlock("rubyore:ruby_ore") .Hardness(0.8f) .Model("rubyore:block/ruby_ore") .Drops(_ => [_ruby.CreateStack(Random.Shared.Next(1, 4))]) .OnInteract(ctx => { ctx.Player.SendActionBar(Component.Text("A vein of rubies").Color(TextColor.Red)); ctx.Cancelled = false; // let the held block be placed against it like against any block }); Content.Blocks.Register(_ore); _oreItem = new CustomItem("rubyore:ruby_ore_item", "minecraft:amethyst_shard") .Model("rubyore:ruby_ore") .Name(Component.Text("Ruby Ore").Color(TextColor.Red)) .Places(_ore); Content.Items.Register(_oreItem); Commands.Register(Command.Literal("rubyore") .Permission("rubyore.give") .Then(Command.Literal("give").Executes(ctx => { var player = ctx.RequirePlayer(); player.Inventory.Give(_oreItem.CreateStack(16)); ctx.Reply("Here is your ruby ore"); return 1; })) .Then(Command.Literal("pack").Executes(ctx => { var player = ctx.RequirePlayer(); try { ResourcePackBuilder.Create("rubyore") .Description("Ruby ore") .AddItem(_ruby, PackImage.Solid(0xFFE0115F)) .AddItem(_oreItem, PackImage.Checker(0xFF5A5A5A, 0xFFE0115F, cell: 4)) .AddCustomBlock(_ore, PackImage.Checker(0xFF5A5A5A, 0xFFE0115F, cell: 4)) .Build() .SendTo(player, required: false, Component.Text("Ruby ore textures")); ctx.Reply("Sent the resource pack"); return 1; } catch (InvalidOperationException e) { throw new CommandFailure(e.Message); } })), description: "Ruby ore tools"); } } ``` ### RubyOre.csproj **RubyOre.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/RubyOre/ ``` --- # RubyWand Page: /examples/rubywand/ A custom item with a right-click ability A custom item with a right-click ability Source: `src/scripting/managed/samples/RubyWand/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - cancelling events - commands - Brigadier command trees (v1) - permissions (v1) ## Build and run ```bash dotnet build src/scripting/managed/samples/RubyWand -c Release # the output lands in build/bin/dotnet/samples/RubyWand/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### RubyWand.cs **RubyWand.cs** ```csharp namespace RubyWand; /// Sample: a custom item with a right-click ability and a resource pack for its look. The wand is a stick with data components (custom_data holding its id, an /// item_model, a name, lore, rarity and the enchantment glint), so no client mod is needed. Right-clicking it heals the player and sprays particles (with a two second /// cooldown the content layer enforces); /rubywand give hands one out and /rubywand pack sends the generated resource pack that gives it a ruby texture. /// The pack is served by the server's built-in pack host: set content.pack_host_port in Ramjet.toml (and pack_host_bind / pack_host_url for players on other /// machines), or host the zip yourself and use pack.ToResourcePack(url). [PluginInfo("RubyWand", Version = "1.0.0", Author = "Ramjet", Description = "A custom item with a right-click ability")] public sealed class RubyWandPlugin : Plugin { private CustomItem _wand = null!; protected override void OnEnable() { Permissions.Register("rubywand.use", PermissionDefault.True, "Use the ruby wand"); Permissions.Register("rubywand.give", PermissionDefault.OpLevel(2), "Give out ruby wands and send the pack"); _wand = new CustomItem("rubywand:wand", "minecraft:stick") .Model("rubywand:wand") .Name(Component.Text("Ruby Wand").Color(TextColor.Red)) .Lore(Component.Text("Right click to feel better").Color(TextColor.Gray).Italic(false)) .MaxStack(1) .WithRarity(ItemRarity.Epic) .Glint() .Cooldown(40) .OnUse(Zap); Content.Items.Register(_wand); Commands.Register(Command.Literal("rubywand") .Permission("rubywand.give") .Then(Command.Literal("give").Executes(ctx => { var player = ctx.RequirePlayer(); player.Inventory.Give(_wand.CreateStack()); ctx.Reply("Here is your ruby wand"); return 1; })) .Then(Command.Literal("pack").Executes(ctx => { var player = ctx.RequirePlayer(); try { BuildPack().SendTo(player, required: false, Component.Text("Ruby wand textures")); ctx.Reply("Sent the resource pack"); return 1; } catch (InvalidOperationException e) { throw new CommandFailure(e.Message); } })), description: "Ruby wand tools"); } private void Zap(ItemUseContext ctx) { var player = ctx.Player; if (!player.HasPermission("rubywand.use")) { ctx.Cancelled = true; return; } player.Health = Math.Min(player.Health + 4, player.MaxHealth); var at = player.Position + new Vec3d(0, 1, 0); player.World.SpawnParticle("heart", at, count: 6, offset: new Vec3d(0.4, 0.4, 0.4), speed: 0.05f); player.PlaySound("minecraft:entity.player.levelup", SoundCategory.Players, 0.6f, 1.6f); player.SendActionBar(Component.Text("The wand hums").Color(TextColor.Red)); } private ResourcePackFile BuildPack() => ResourcePackBuilder.Create("rubywand") .Description("Ruby wand") .AddItem(_wand, PackImage.Checker(0xFFE0115F, 0xFF9B0F44, size: 16, cell: 2)) .Build(); } ``` ### RubyWand.csproj **RubyWand.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/RubyWand/ ``` --- # ShopGui Page: /examples/shopgui/ A shop in a custom GUI A shop in a custom GUI Source: `src/scripting/managed/samples/ShopGui/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - commands - Brigadier command trees (v1) - config (v1) - permissions (v1) ## Build and run ```bash dotnet build src/scripting/managed/samples/ShopGui -c Release # the output lands in build/bin/dotnet/samples/ShopGui/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### ShopGui.cs **ShopGui.cs** ```csharp using System.Text.Json.Serialization; namespace ShopGui; /// One thing the shop sells. public sealed class ShopItem { /// The item key, e.g. minecraft:bread. public string Item { get; set; } = "minecraft:bread"; /// How many the player gets for one purchase. public int Count { get; set; } = 1; /// The price in units of the currency item. public int Price { get; set; } = 1; } /// The settings of the ShopGui plugin. public sealed class ShopConfig { /// The item players pay with. public string Currency { get; set; } = "minecraft:gold_ingot"; /// The title of the shop window. public string Title { get; set; } = "Shop"; /// What the shop sells, in slot order (at most 21 entries: three rows of seven). public List Items { get; set; } = [ new() { Item = "minecraft:bread", Count = 8, Price = 1 }, new() { Item = "minecraft:arrow", Count = 16, Price = 2 }, new() { Item = "minecraft:iron_pickaxe", Count = 1, Price = 6 }, new() { Item = "minecraft:golden_apple", Count = 1, Price = 12 }, new() { Item = "minecraft:diamond", Count = 1, Price = 20 }, ]; } [JsonSerializable(typeof(ShopConfig))] internal sealed partial class ShopJson : JsonSerializerContext; /// Sample: /shop opens a custom chest GUI. Every product is an icon with a price in its lore; clicking it pays with a currency item from the player's inventory /// () and hands over the product (). The menu is locked by the engine: nothing in it can be taken out, shift clicked, /// dragged or swapped away, whatever a handler does or forgets to do, so a shop has no duplication holes. It also shows OnClose and the config file with its live reload. [PluginInfo("ShopGui", Version = "1.0.0", Author = "Ramjet", Description = "A shop in a custom GUI")] public sealed class ShopGuiPlugin : Plugin { private Config _config = null!; private Menu? _menu; protected override void OnEnable() { _config = Config.Load(ShopJson.Default.ShopConfig); _config.Watch = true; _config.Changed += _ => Rebuild(); Permissions.Register("shop.use", PermissionDefault.True, "Open the shop"); Rebuild(); Commands.Register(Command.Literal("shop").Permission("shop.use").Executes(ctx => { var player = ctx.RequirePlayer(); _menu!.Open(player); return 1; }), description: "Open the shop"); } // The menu is rebuilt when the config changes: the old one is closed for its viewers and the new one is opened for them again. private void Rebuild() { var viewers = _menu?.Viewers.ToList() ?? []; _menu?.Dispose(); var config = _config.Value; var menu = Menus.Chest(3, Component.Text(config.Title).Color(TextColor.DarkGreen)); menu.FillEmpty(new ItemStack("minecraft:gray_stained_glass_pane").WithName(Component.Text(" "))); for (int i = 0; i < Math.Min(config.Items.Count, 21); i++) { var product = config.Items[i]; int slot = (i / 7) * 9 + 1 + (i % 7); // three rows, columns 1-7 var stack = new ItemStack(product.Item, product.Count); var icon = stack.WithLore( Component.Text($"Price: {product.Price} x {Pretty(config.Currency)}").Color(TextColor.Gold).Italic(false), Component.Text("Click to buy").Color(TextColor.Gray).Italic(false)); menu.Slot(slot, icon, click => Buy(click, product, stack)); } menu.OnClose((player, reason) => Log.Info($"{player.Name} left the shop ({reason})")); _menu = menu; foreach (var viewer in viewers) if (viewer.IsOnline) menu.Open(viewer); } private void Buy(MenuClick click, ShopItem product, ItemStack stack) { var player = click.Player; var inventory = player.Inventory; var price = new ItemStack(_config.Value.Currency, Math.Max(product.Price, 1)); if (inventory.Count(price) < price.Count) { player.SendActionBar(Component.Text($"You need {price.Count} x {Pretty(price.Type)}").Color(TextColor.Red)); player.PlaySound("minecraft:entity.villager.no", SoundCategory.Players); return; } inventory.RemoveItem(price); int dropped = inventory.Give(stack); player.SendActionBar(Component.Text($"Bought {stack.Count} x {Pretty(stack.Type)}" + (dropped > 0 ? " (your inventory was full: it is at your feet)" : "")).Color(TextColor.Green)); player.PlaySound("minecraft:entity.villager.yes", SoundCategory.Players); } private static string Pretty(string key) => key[(key.IndexOf(':') + 1)..].Replace('_', ' '); } ``` ### ShopGui.csproj **ShopGui.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/ShopGui/ ``` --- # SidebarStats Page: /examples/sidebarstats/ Live personal sidebar scoreboard Live personal sidebar scoreboard Source: `src/scripting/managed/samples/SidebarStats/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - event subscriptions - commands - Brigadier command trees (v1) - the scheduler - players API: boss bars, sidebar, titles (v1) Events used: [`PlayerJoinEvent`](/reference/api/event-types/playerjoinevent/), [`PlayerQuitEvent`](/reference/api/event-types/playerquitevent/), [`PlayerDeathEvent`](/reference/api/event-types/playerdeathevent/) ## Build and run ```bash dotnet build src/scripting/managed/samples/SidebarStats -c Release # the output lands in build/bin/dotnet/samples/SidebarStats/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### SidebarStats.cs **SidebarStats.cs** ```csharp namespace SidebarStats; /// Sample: a personal sidebar for every player with live lines (health, food, latency, position, kills and deaths), each refreshed once a second; the players are spread over /// the twenty ticks of that second (a slice every tick) so that a big server does not rebuild every sidebar in one tick. /// /stats toggles it. Each player has his own , so the lines differ per player; the sidebar objects are owned by the plugin and disappear when it unloads. [PluginInfo("SidebarStats", Version = "1.0.0", Author = "Ramjet", Description = "Live personal sidebar scoreboard")] public sealed class SidebarStatsPlugin : Plugin { private readonly Dictionary _sidebars = []; private readonly HashSet _hidden = []; private readonly Dictionary _counts = []; protected override void OnEnable() { foreach (var player in Server.Players) Attach(player); Events.On((ref PlayerJoinEvent e) => Attach(e.Player)); Events.On((ref PlayerQuitEvent e) => { if (_sidebars.Remove(e.Player.Entity, out var sidebar)) sidebar.Remove(); }); Events.On((ref PlayerDeathEvent e) => { Count(e.Player.Uuid, deaths: 1); var killer = new Player(e.Killer); if (killer.IsOnline && killer != e.Player) Count(killer.Uuid, kills: 1); }); Commands.Register(Command.Literal("stats").Executes(ctx => { var player = ctx.RequirePlayer(); if (!_hidden.Add(player.Uuid)) { _hidden.Remove(player.Uuid); Attach(player); ctx.Reply("Stats sidebar on"); } else { if (_sidebars.TryGetValue(player.Entity, out var sidebar)) sidebar.Hide(player); ctx.Reply("Stats sidebar off"); } return 1; })); Scheduler.RunEvery(1, Refresh); } private void Count(Uuid player, int kills = 0, int deaths = 0) { var (k, d) = _counts.GetValueOrDefault(player); _counts[player] = (k + kills, d + deaths); } private void Attach(Player player) { if (_hidden.Contains(player.Uuid)) return; if (!_sidebars.TryGetValue(player.Entity, out var sidebar)) _sidebars[player.Entity] = sidebar = Sidebars.Create(Component.Text("Stats").Color(TextColor.Gold).Bold()); Fill(sidebar, player); sidebar.Show(player); } // Every tick the players whose slot (entity index modulo 20) is the tick's slot: one twentieth of the sidebars, each of them once a second private void Refresh() { ulong slot = Server.CurrentTick % 20; foreach (var (entity, sidebar) in _sidebars) { if ((entity.Id & 0xFFFFFFFF) % 20 != slot) continue; var player = new Player(entity); if (player.IsOnline && sidebar.IsShownTo(player)) Fill(sidebar, player); } } private void Fill(Sidebar sidebar, Player player) { var (kills, deaths) = _counts.GetValueOrDefault(player.Uuid); var at = player.Position; sidebar.SetLines( [ Line("Health", $"{player.Health:0.#}/{player.MaxHealth:0}", TextColor.Red), Line("Food", player.FoodLevel.ToString(), TextColor.Gold), Line("Ping", $"{player.Latency} ms", TextColor.Green), Line("XYZ", $"{at.X:0} {at.Y:0} {at.Z:0}", TextColor.Aqua), Component.Empty, Line("Kills", kills.ToString(), TextColor.White), Line("Deaths", deaths.ToString(), TextColor.White), ]); } private static Component Line(string label, string value, TextColor color) => Component.Text(label + ": ").Color(TextColor.Gray).Append(Component.Text(value).Color(color)); } ``` ### SidebarStats.csproj **SidebarStats.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/SidebarStats/ ``` --- # SpawnProtect Page: /examples/spawnprotect/ Blocks building near the world spawn Blocks building near the world spawn Source: `src/scripting/managed/samples/SpawnProtect/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - event subscriptions - cancelling events Events used: [`BlockBreakEvent`](/reference/api/event-types/blockbreakevent/), [`BlockPlaceEvent`](/reference/api/event-types/blockplaceevent/), [`BlockIgniteEvent`](/reference/api/event-types/blockigniteevent/), [`BlockBurnEvent`](/reference/api/event-types/blockburnevent/), [`EntityDamageEvent`](/reference/api/event-types/entitydamageevent/) ## Build and run ```bash dotnet build src/scripting/managed/samples/SpawnProtect -c Release # the output lands in build/bin/dotnet/samples/SpawnProtect/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### SpawnProtect.cs **SpawnProtect.cs** ```csharp namespace SpawnProtect; /// Sample: protects the area around the world spawn from breaking, placing and fire, and makes it pvp-free. /// Shows cancellation, enum fields and per-event filtering. [PluginInfo("SpawnProtect", Version = "1.0.0", Author = "Ramjet", Description = "Blocks building near the world spawn")] public sealed class SpawnProtectPlugin : Plugin { private const int Radius = 16; private static bool Protected(BlockPos pos) => Math.Abs(pos.X) <= Radius && Math.Abs(pos.Z) <= Radius; protected override void OnEnable() { Events.On((ref BlockBreakEvent e) => { if (!Protected(e.Pos)) return; e.Cancelled = true; e.Player.SendActionBar(Component.Text("The spawn area is protected").Color(TextColor.Red)); }); Events.On((ref BlockPlaceEvent e) => { if (!Protected(e.Pos)) return; e.Cancelled = true; e.Player.SendActionBar(Component.Text("You cannot build here").Color(TextColor.Red)); }); // fire must not spread (or be lit) inside the protected area either Events.On((ref BlockIgniteEvent e) => { if (Protected(e.Pos)) e.Cancelled = true; }); Events.On((ref BlockBurnEvent e) => { if (Protected(e.Pos)) e.Cancelled = true; }); // players cannot be hurt by other players inside the area (pvp-free spawn) Events.On((ref EntityDamageEvent e) => { var victim = new Player(e.Target); if (e.Cause == DamageCause.EntityAttack && !e.Attacker.IsNull && victim.IsOnline) { var pos = victim.Position; if (Math.Abs(pos.X) <= Radius && Math.Abs(pos.Z) <= Radius) e.Cancelled = true; } }); } } ``` ### SpawnProtect.csproj **SpawnProtect.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/SpawnProtect/ ``` --- # WorldTools Page: /examples/worldtools/ World API sample: blocks, signs, mobs, explosions, time, weather, chunk tickets World API sample: blocks, signs, mobs, explosions, time, weather, chunk tickets Source: `src/scripting/managed/samples/WorldTools/` · version 1.0.0. The code below is read from the repository at build time. ## What it shows - commands - Brigadier command trees (v1) - permissions (v1) ## Build and run ```bash dotnet build src/scripting/managed/samples/WorldTools -c Release # the output lands in build/bin/dotnet/samples/WorldTools/ when built through CMake (MC_BUILD_SCRIPTING); # copy the dll into your plugins folder and start the server with --plugins ``` ## Source ### WorldTools.cs **WorldTools.cs** ```csharp namespace WorldTools; /// Sample of the world API: /wt subcommands that change blocks with update flags, edit a sign through its NBT, spawn mobs with NBT, make explosions, change the /// time and weather, keep chunks loaded with tickets and print an entity's data. Each subcommand is a few lines: this is the plugin to copy snippets from. [PluginInfo("WorldTools", Version = "1.0.0", Author = "Ramjet", Description = "World API sample: blocks, signs, mobs, explosions, time, weather, chunk tickets")] public sealed class WorldToolsPlugin : Plugin { private readonly List _tickets = []; protected override void OnEnable() { Permissions.Register("worldtools.use", PermissionDefault.OpLevel(2), "Use /wt"); Commands.Register(Command.Literal("wt").Permission("worldtools.use") // /wt sphere : a hollow sphere around the player, written without neighbour updates (fast) and with clients told .Then(Command.Literal("sphere").Then(Command.Argument("block", Arg.BlockState()).Then(Command.Argument("radius", Arg.Int(1, 24)).Executes(Sphere)))) // /wt sign : puts a sign in front of the player and writes its text through the block entity data .Then(Command.Literal("sign").Then(Command.Argument("text", Arg.GreedyString()).Executes(Sign))) // /wt mob : spawns mobs around the player with NBT (a custom name, no drops) .Then(Command.Literal("mob").Then(Command.Argument("type", Arg.Word()).Then(Command.Argument("count", Arg.Int(1, 20)).Executes(Mob)))) // /wt boom [terrain]: an explosion at the player's feet that may leave the terrain alone .Then(Command.Literal("boom").Then(Command.Argument("power", Arg.Float(0.5f, 10f)).Then(Command.Argument("terrain", Arg.Bool()).Executes(Boom)))) // /wt time and /wt storm .Then(Command.Literal("time").Then(Command.Argument("ticks", Arg.Int(0)).Executes(ctx => { var world = ctx.RequirePlayer().World; if (!world.HasDayTime) throw new CommandFailure($"{world.Key} has no day/night cycle"); world.DayTime = ctx.GetInt("ticks"); ctx.Reply($"It is {world.DayTime % 24000} ticks into the day"); return 1; }))) .Then(Command.Literal("storm").Executes(ctx => { var world = ctx.RequirePlayer().World; bool ok = world.SetWeather(WeatherType.Thunder, durationTicks: 6000); ctx.Reply(ok ? "Thunderstorm for five minutes" : $"{world.Key} has no weather"); return ok ? 1 : 0; })) // /wt keep : keeps the chunks around the player loaded until /wt release (or until the plugin unloads) .Then(Command.Literal("keep").Then(Command.Argument("radius", Arg.Int(0, 4)).Executes(ctx => { var player = ctx.RequirePlayer(); var pos = player.Location.BlockPos; _tickets.AddRange(ChunkTickets.AddArea(player.World, pos.X >> 4, pos.Z >> 4, ctx.GetInt("radius"))); ctx.Reply($"Holding {_tickets.Count} chunks"); return _tickets.Count; }))) .Then(Command.Literal("release").Executes(ctx => { int n = _tickets.Count; foreach (var t in _tickets) t.Release(); _tickets.Clear(); ctx.Reply($"Released {n} chunks"); return n; })) // /wt nearby: the entities within 8 blocks with their type and health (read from their NBT) .Then(Command.Literal("nearby").Executes(Nearby)), description: "World API examples"); } private static int Sphere(CommandContext ctx) { var player = ctx.RequirePlayer(); var world = player.World; var state = ctx.GetBlockState("block"); int r = ctx.GetInt("radius"); var center = player.Location.BlockPos; int changed = 0; for (int x = -r; x <= r; x++) for (int y = -r; y <= r; y++) for (int z = -r; z <= r; z++) { double d = Math.Sqrt(x * x + y * y + z * z); if (d > r || d < r - 1) continue; // the shell only var pos = new BlockPos(center.X + x, center.Y + y, center.Z + z); if (!world.TryGetBlock(pos, out var old) || !old.IsAir) continue; // unloaded chunk or not free // Clients only: no neighbour updates, so redstone and plants around do not react to every block of the shell if (world.SetBlock(pos, state, BlockUpdateFlags.Clients)) changed++; } ctx.Reply($"{changed} blocks placed"); return changed; } private static int Sign(CommandContext ctx) { var player = ctx.RequirePlayer(); var world = player.World; var pos = player.Location.BlockPos; if (!world.SetBlock(pos, "minecraft:oak_sign[rotation=0]")) throw new CommandFailure("Cannot place a sign here"); // The sign's text lives in its block entity: four JSON components for the front side var messages = new NbtList(); string text = ctx.GetString("text"); for (int line = 0; line < 4; line++) messages.Add(new NbtString(line == 0 ? $"\"{text.Replace("\"", "\\\"")}\"" : "\"\"")); var data = new NbtCompound().Put("front_text", new NbtCompound().Put("messages", messages)); if (!world.SetBlockEntityData(pos, data)) throw new CommandFailure("The sign has no block entity"); ctx.Reply("Sign placed: " + world.GetBlockEntityData(pos)!.GetCompound("front_text")!.GetList("messages")![0]); return 1; } private static int Mob(CommandContext ctx) { var player = ctx.RequirePlayer(); var world = player.World; int spawned = 0; for (int i = 0; i < ctx.GetInt("count"); i++) { var offset = new Vec3d(Random.Shared.NextDouble() * 6 - 3, 0, Random.Shared.NextDouble() * 6 - 3); // NBT is applied as given: here a custom name and a health value; leaving the compound out rolls the vanilla random attributes instead var nbt = new NbtCompound().PutString("CustomName", "Sample " + (i + 1)).PutFloat("Health", 10f); var mob = world.SpawnEntity(ctx.GetString("type"), player.Position + offset, nbt); if (!mob.IsNull) spawned++; } ctx.Reply($"{spawned} spawned"); return spawned; } private static int Boom(CommandContext ctx) { var player = ctx.RequirePlayer(); bool ok = player.World.Explode(player.Position, ctx.GetFloat("power"), setFire: false, breakBlocks: ctx.GetBool("terrain"), source: player.Entity); return ok ? 1 : 0; } private static int Nearby(CommandContext ctx) { var player = ctx.RequirePlayer(); int count = 0; foreach (var e in player.World.Entities) { if (e == player.Entity || !e.TryGetLocation(out var where)) continue; if ((where.Position - player.Position).Length > 8) continue; double health = e.GetNbt()?.GetDouble("Health", -1) ?? -1; ctx.Reply($"{e.TypeKey} at {where.BlockPos.X},{where.BlockPos.Y},{where.BlockPos.Z}" + (health >= 0 ? $" health {health}" : "")); count++; } return count; } } ``` ### WorldTools.csproj **WorldTools.csproj** ```xml $(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj $(McOutDir)samples/WorldTools/ ``` --- # API reference (Mc.Scripting.Api) Namespace `Mc.Scripting`. One entry per public type; event structs are listed in the event catalogue above. ## ApiVersion (struct, plugins) ```csharp public readonly struct ApiVersion : IComparable, IEquatable ``` The version of the plugin API (semantic: a minor increase is additive, a major increase is breaking). - `public ApiVersion(int Major, int Minor)`: The version of the plugin API (semantic: a minor increase is additive, a major increase is breaking). - `public int Major { get; init; }`: Breaking changes. - `public int Minor { get; init; }`: Additive changes. - `public static ApiVersion Current { get; }`: The API version this server provides. - `public int CompareTo(ApiVersion other)` - `public bool Satisfies(ApiVersion required)`: Whether a plugin that needs `required` can run on this server version. - `public override string ToString()` - `public static bool TryParse(string? text, out ApiVersion version)`: Parses "1.0", "1" or "1.x". ## Belowname (class, display) ```csharp public sealed class Belowname ``` Text (and optionally a number) shown under a player's name, per viewer. It uses the engine's below-name objective, which is defined while a player has one. Reach it as `plugin.Belowname`. Call from the tick thread. What a plugin sets is removed when the plugin unloads. - `public void Clear(Player viewer, Player target)`: Removes the text under one player's name for one viewer. - `public void Set(Player viewer, Player target, Component text, int? number)`: Shows text under one player's name for one viewer, with a number when given (hidden when null). ## BlockFace (enum, worlds) ```csharp public enum BlockFace : byte ``` A block face / direction (vanilla Direction order). - `Down` = 0: Negative Y. - `Up` = 1: Positive Y. - `North` = 2: Negative Z. - `South` = 3: Positive Z. - `West` = 4: Negative X. - `East` = 5: Positive X. ## BlockPos (struct, primitives) ```csharp public struct BlockPos : IEquatable ``` Integer block coordinates. - `public BlockPos(int x, int y, int z)`: Creates a block position. - `public int X;`: Components. - `public int Y;`: Components. - `public int Z;`: Components. - `public bool Equals(BlockPos o)`: Equality. - `public override bool Equals(object? obj)`: Equality. - `public override int GetHashCode()`: Hash code. - `public override string ToString()`: Text form. ## BossBar (class, display) ```csharp public sealed class BossBar : IDisposable, IPluginResource ``` A boss bar at the top of the screen, shown to the players who were added as viewers. Create it with `BossBars.Create`; it disappears from every viewer when the plugin unloads (or when `BossBar.Remove` is called). Bars are not saved with the world. - `public BossBarColor Color { get; set; }`: The colour. - `public BossBarFlags Flags { get; set; }`: Darkening, music and fog. - `public bool IsActive { get; }`: Whether the bar still exists (false after `BossBar.Remove` or the plugin unloading). - `public string Key { get; }`: The full key of the bar ("pluginid:name"); also its id in `/bossbar`. - `public float Progress { get; set; }`: How full the bar is, 0..1. - `public BossBarStyle Style { get; set; }`: The segmentation. - `public Component Title { get; set; }`: The text above the bar. - `public IReadOnlyCollection Viewers { get; }`: The profile ids of the players who see (or will see, when they join) the bar. - `public bool Visible { get; set; }`: Whether the viewers currently see the bar (hiding keeps the viewer list). - `public BossBar AddViewer(Player player)`: Shows the bar to a player until `BossBar.RemoveViewer` or until the player leaves the server. - `public BossBar AddViewer(Uuid player)`: Shows the bar to a player (by profile id, also an offline one: the bar appears when he joins). - `public void Dispose()` - `public void Remove()`: Removes the bar from every viewer and deletes it. - `public BossBar RemoveViewer(Player player)`: Hides the bar from a player. - `public BossBar RemoveViewer(Uuid player)`: Hides the bar from a player (by profile id). ## BossBarColor (enum, display) ```csharp public enum BossBarColor : int ``` The colour of a boss bar. - `Pink` = 0: Pink. - `Blue` = 1: Blue. - `Red` = 2: Red. - `Green` = 3: Green. - `Yellow` = 4: Yellow. - `Purple` = 5: Purple. - `White` = 6: White. ## BossBarFlags (enum, display) ```csharp public enum BossBarFlags : int ``` Extra behaviour of a boss bar. - `None` = 0: Nothing special. - `DarkenScreen` = 1: Darkens the sky. - `PlayMusic` = 2: Plays the boss music. - `CreateFog` = 4: Creates fog. ## BossBarStyle (enum, display) ```csharp public enum BossBarStyle : int ``` How a boss bar is divided. - `Solid` = 0: A plain bar. - `Segmented6` = 1: Six segments. - `Segmented10` = 2: Ten segments. - `Segmented12` = 3: Twelve segments. - `Segmented20` = 4: Twenty segments. ## BossBars (class, display) ```csharp public sealed class BossBars ``` Creates boss bars owned by a plugin (`plugin.BossBars`). - `public BossBar Create(string name, Component title, BossBarColor color = BossBarColor.Pink, BossBarStyle style = BossBarStyle.Solid, float progress = 1f)`: Creates a boss bar with nobody watching; add viewers to show it. ## ChannelRegistry (class, chat) ```csharp public sealed class ChannelRegistry ``` Plugin messaging channels (Paper's Messenger, sendPluginMessage). `ChannelRegistry.On` listens on a channel: the messages that the clients send on it arrive in the handler. The engine tells the clients which channels are listened on (minecraft:register). Send with {{cref:M:Mc.Scripting.Player.SendPluginMessage(System.String,System.ReadOnlySpan{System.Byte})}}. Refused: the minecraft:* channels (a client's own registrations are read through `Player.Channels`), velocity:* and bungeecord:*. Messages arrive in the tick window, in order per player, within the budgets of scripting.channels (a message beyond them is dropped). A listener ends with its plugin (unload or reload). Tick thread only. The worker delivers the messages; the in-process runtime does not. - `public ChannelSubscription On(string channel, PluginMessageHandler handler)`: Listens on a channel. The first listener of a channel makes the engine announce it to the clients (minecraft:register). ## ChannelSubscription (class, chat) ```csharp public sealed class ChannelSubscription : IDisposable, IPluginResource ``` A listener on a plugin messaging channel (`ChannelRegistry.On`). Dispose it to stop listening. - `public string Channel { get; }`: The channel the subscription listens on. - `public bool IsActive { get; }`: Whether the subscription still listens (false after Dispose). - `public void Dispose()`: Stops listening. When it was the last listener of its channel, the engine stops announcing the channel to the clients. ## Chat (class, chat) ```csharp public static class Chat ``` Chat lines that no player signed: the disguised chat the console's `/say` sends. The chat type picks the vanilla format and the name and message are shown in it. Use it for lines that are not a player's own words (a notice in a staff channel, a relayed message). The line is unsigned and has no signature marker, so it is never shown as a player's verified chat; for a player's own words use the chat event instead. - `public static int SendTo(IEnumerable recipients, ChatKind kind, Component name, Component? target, Component message)`: Sends an unsigned chat line to some players. The line is formatted from `kind` and `name` (the sender's name as the line shows it) and `message`. The message kinds (`ChatKind.MsgCommandIncoming`, `ChatKind.MsgCommandOutgoing`, `ChatKind.TeamMsgCommandIncoming`, `ChatKind.TeamMsgCommandOutgoing`) need `target` (the other side's name, as the vanilla format shows it); without one the call returns -1. Players who do not accept chat are skipped. ## ChatFilterKind (enum, chat) ```csharp public enum ChatFilterKind : byte ``` How the recipients who filter text see a chat message (vanilla FilterMask). - `PassThrough` = 0: The text as typed. - `FullyFiltered` = 1: Filtering recipients get nothing of the message. - `PartiallyFiltered` = 2: Filtering recipients see the text with the characters of filter_bits hidden. ## ChatKind (enum, chat) ```csharp public enum ChatKind : byte ``` The chat type of a line: the vanilla minecraft:chat_type entries the server uses. - `Chat` = 0: A player's chat line (minecraft:chat). - `EmoteCommand` = 1: The /me line (minecraft:emote_command). - `SayCommand` = 2: The /say line (minecraft:say_command). - `MsgCommandIncoming` = 3: A private message received (minecraft:msg_command_incoming). Needs a target. - `MsgCommandOutgoing` = 4: A private message sent (minecraft:msg_command_outgoing). Needs a target. - `TeamMsgCommandIncoming` = 5: A team message received (minecraft:team_msg_command_incoming). Needs a target. - `TeamMsgCommandOutgoing` = 6: A team message sent (minecraft:team_msg_command_outgoing). Needs a target. ## ChatMute (class, chat) ```csharp public static class ChatMute ``` Chat mutes. The engine keeps the mute table itself (it survives plugin reloads and server restarts) and refuses a muted player's chat lines, `/say`, `/me` and private messages before a plugin sees them, so a plugin does not check mutes on the message path. Commands and other actions are not affected. The reason is what the engine's mute notice (`chat.mute_notice`) can show the player. Thread: tick thread. - `public static bool Clear(Uuid player)`: Lifts a player's chat mute. - `public static ChatMuteEntry? Get(Uuid player)`: The player's active chat mute, null when the player is not muted (an expired mute is not active). - `public static IReadOnlyList List()`: All active chat mutes, in profile id order: a snapshot, read from the engine in pages when this is called. - `public static bool Set(Uuid player, DateTimeOffset? expires, string? reason)`: Mutes a player's chat. A mute already in force is replaced by this one; the same mute again changes nothing. ## ChatMuteEntry (class, chat) ```csharp public sealed class ChatMuteEntry : IEquatable ``` An active chat mute: the muted player, when the mute ends and the reason it was given. - `public ChatMuteEntry(Uuid Player, DateTimeOffset? Expires, string? Reason)`: An active chat mute: the muted player, when the mute ends and the reason it was given. - `public DateTimeOffset? Expires { get; init; }`: When the mute ends; null = forever. - `public Uuid Player { get; init; }`: The muted player's profile id. - `public string? Reason { get; init; }`: The reason the mute was given; null when none. ## ChatRoute (class, chat) ```csharp public static class ChatRoute ``` Restricts who gets a chat message while its handler runs. Signed chat stays signed: a restricted line is sent to the listed players only, as the engine's own private messages are. - `public static int Restrict(IEnumerable recipients)`: Restricts the recipients of the chat message of the running handler. Only the listed players get the line. Works in a `PlayerChatEvent` or a `PrivateMessageEvent` handler; the restriction applies to that one message only and ends when the dispatch is over. A second call replaces the list. An empty list means nobody gets the line. Offline handles are left out. ## ClickType (enum, inventories) ```csharp public enum ClickType : byte ``` How an inventory slot was clicked (vanilla ContainerInput folded with the button). - `Left` = 0: Left click. - `ShiftLeft` = 1: Shift + left click (quick move). - `Right` = 2: Right click. - `ShiftRight` = 3: Shift + right click (quick move). - `Middle` = 4: Middle click (clone, creative). - `NumberKey` = 5: A hotbar number key (swap). - `DoubleClick` = 6: Double click (collect to cursor). - `Drop` = 7: Drop key over a slot. - `ControlDrop` = 8: Control + drop key over a slot (whole stack). - `WindowBorderLeft` = 9: Left click outside the window (drops the cursor stack). - `WindowBorderRight` = 10: Right click outside the window (drops one). - `SwapOffhand` = 11: Swap with the off hand (F key). - `Unknown` = 12: Anything else. - `Drag` = 13: The end of a drag over several slots (only delivered to MenuClick, which has no InventoryDrag counterpart for locked menus). ## Arg (class, commands) ```csharp public static class Arg ``` Factories for the argument types of the vanilla command engine. Values are read in the executor with the typed getters of `CommandContext` (`GetInt`, `GetPlayer`, `GetBlockPos`, ...); types without a typed getter are read as text with `CommandContext.GetText`. - `public static ArgType Angle()`: A yaw angle, absolute or relative (`90`, `~-10`). - `public static ArgType BlockPos()`: A block position (`1 64 1`, `~ ~1 ~`, `^ ^ ^2`). - `public static ArgType BlockPredicate()`: A block predicate (state, tag, block entity data). - `public static ArgType BlockState()`: A block state such as `minecraft:oak_stairs[facing=north]` (read with `CommandContext.GetBlockState`). - `public static ArgType Bool()`: true / false. - `public static ArgType Color()`: A named chat colour (`red`, `gold`, ...) or `reset`. - `public static ArgType ColumnPos()`: An x/z column position. - `public static ArgType Component()`: A text component in JSON/SNBT form (read with `CommandContext.GetText`). - `public static ArgType ContextFloatProvider()`: A float provider as the loot / worldgen data writes it (a constant, `{type:"minecraft:uniform",min_inclusive:0,max_exclusive:1}`, ...). - `public static ArgType ContextIntProvider()`: An int provider as the loot / worldgen data writes it. - `public static ArgType Dialog()`: A dialog: an id of the `dialog` registry or an inline definition. - `public static ArgType Dimension()`: A dimension key. - `public static ArgType Double(double min = -1.7976931348623157E+308, double max = 1.7976931348623157E+308)`: A double-precision number, optionally limited. - `public static ArgType Entities()`: Any number of entities. - `public static ArgType Entity()`: Exactly one entity. - `public static ArgType EntityAnchor()`: An entity anchor: `feet` or `eyes`. - `public static ArgType Feature()`: A configured worldgen feature (an id of the `worldgen/configured_feature` registry). - `public static ArgType Float(float min = -3.4028235E+38f, float max = 3.4028235E+38f)`: A single-precision number, optionally limited. - `public static ArgType FloatRange()`: An inclusive decimal range. - `public static ArgType Function()`: A data pack function id or tag. - `public static ArgType GameMode()`: A game mode name. - `public static ArgType GameProfile()`: A player profile (also offline players: names and uuids). - `public static ArgType GreedyString()`: The rest of the line. - `public static ArgType Heightmap()`: A heightmap name (`world_surface`, `motion_blocking`, ...). - `public static ArgType HexColor()`: A colour as `#RRGGBB`. - `public static ArgType Int(int min = -2147483648, int max = 2147483647)`: A 32-bit integer, optionally limited. - `public static ArgType IntRange()`: An inclusive integer range (`1..5`, `..3`). - `public static ArgType Item()`: An item with components, e.g. `minecraft:diamond_sword[damage=5]`. - `public static ArgType ItemPredicate()`: An item predicate. - `public static ArgType ItemSlot()`: One inventory slot name (`container.5`, `weapon.mainhand`). - `public static ArgType ItemSlots()`: A group of inventory slots (`container.*`). - `public static ArgType Long(long min = -9223372036854775808, long max = 9223372036854775807)`: A 64-bit integer, optionally limited. - `public static ArgType LootModifier()`: A loot modifier. - `public static ArgType LootPredicate()`: A loot predicate. - `public static ArgType LootTable()`: A loot table. - `public static ArgType Message()`: A chat message that may contain selectors. - `public static ArgType NbtCompound()`: An SNBT compound. - `public static ArgType NbtPath()`: An NBT path. - `public static ArgType NbtTag()`: Any SNBT tag. - `public static ArgType Objective()`: A scoreboard objective name. - `public static ArgType ObjectiveCriteria()`: A scoreboard criterion (`dummy`, `health`, `minecraft.mined:minecraft.stone`). - `public static ArgType Operation()`: A scoreboard operation (`+=`, `%=`, `><`). - `public static ArgType Particle()`: A particle with its parameters (`minecraft:dust{color:[1,0,0],scale:1}`). - `public static ArgType Player()`: One online player (name, uuid or a selector that matches exactly one player). - `public static ArgType Players()`: Any number of online players (names or selectors such as `@a`). - `public static ArgType Resource(string registry)`: An entry of a registry ("minecraft:enchantment", "minecraft:sound_event", ...), suggested by the engine. - `public static ArgType ResourceKey(string registry)`: A registry key (not the entry), e.g. `Arg.ResourceKey("minecraft:worldgen/biome")`. - `public static ArgType ResourceLocation()`: A resource location, `namespace:path`. - `public static ArgType ResourceOrTag(string registry)`: An entry of a registry or a `#tag` of it. - `public static ArgType ResourceOrTagKey(string registry)`: A registry key or a `#tag` of keys. - `public static ArgType ResourceSelector(string registry)`: A selector over the entries of a registry (`*`, `minecraft:*`, tags). - `public static ArgType Rotation()`: A rotation (yaw pitch). - `public static ArgType ScoreHolder(bool multiple = false)`: A score holder (names, selectors, `*`). - `public static ArgType ScoreboardSlot()`: A scoreboard display slot (`sidebar`, `list`, `below_name`, `sidebar.team.red`). - `public static ArgType SlotSource()`: A slot source of the item modifier data (`minecraft:slot_source`). - `public static ArgType String()`: A word or a quoted phrase. - `public static ArgType Style()`: A text style in JSON/SNBT form. - `public static ArgType SwingAnimation()`: A swing animation type (`none`, `stab`, `whack`). - `public static ArgType Swizzle()`: A set of axes (`xz`, `xyz`). - `public static ArgType Team()`: A team name. - `public static ArgType TemplateMirror()`: A structure mirror (`none`, `left_right`, `front_back`). - `public static ArgType TemplateRotation()`: A structure rotation (`none`, `clockwise_90`, `180`, `counterclockwise_90`). - `public static ArgType Time(int minTicks = 0)`: A duration such as `10s`, `5d`, `20t` (parsed to ticks). - `public static ArgType Uuid()`: A uuid (read with `CommandContext.GetText`). - `public static ArgType Vec2()`: A 2D position. - `public static ArgType Vec3()`: A position with decimals. - `public static ArgType Word()`: A single word (letters, digits, `_ - . +`). ## ArgType (class, commands) ```csharp public sealed class ArgType ``` The type of a command argument: what the client's command tree shows (parser id + properties) and what the server parses. Create with the `Arg` factories. - `public string RegistryName { get; }`: The vanilla argument type id, e.g. "brigadier:integer" or "minecraft:entity". - `public override string ToString()` ## AsyncSuggestionProvider (delegate, commands) ```csharp public delegate Task> AsyncSuggestionProvider(SuggestionContext context, string typed) ``` An asynchronous suggestion provider (the list is produced off the tick thread, e.g. from a database). ## Command (class, commands) ```csharp public static class Command ``` Entry point for building command trees: `Command.Literal("home").Then(Command.Argument("name", Arg.Word()).Executes(...))`. - `public static CommandNode Argument(string name, ArgType type)`: A typed argument. - `public static CommandNode Literal(string name)`: A literal word. ## CommandContext (class, commands) ```csharp public sealed class CommandContext ``` The parsed invocation of a command: the source and the argument values. Valid only while the executor / provider runs. - `public string[] Args { get; }`: The words after the command name (flat-command style access: split on spaces). - `public string Input { get; }`: The whole command line without the leading slash. - `public string Name { get; }`: The name of the executing command node chain's root literal (the word the player typed). - `public Player? Player { get; }`: The sending player, null for the console. - `public string RawArgs { get; }`: The text after the command name. - `public int Result { get; set; }`: The command result the executor of an `Action`-style handler returns (default 1). Set it to change the number `/execute store` sees. - `public Entity Sender { get; }`: The executing entity (shortcut for `Source.Entity`; `Entity.Null` for the console). - `public CommandSource Source { get; }`: Who runs the command. - `public DetachedCommand Detach()`: Copies what a command needs after its executor returned: who ran it, the input, the permission level and the position. Use it for deferred work (`Scheduler.RunLater`, a task, a callback): the context itself must not be used after the executor returned. - `public BlockPos GetBlockPos(string name)`: The block position of a `Arg.BlockPos` argument (relative coordinates resolved against the source). - `public BlockState GetBlockState(string name)`: The block state of a `Arg.BlockState` argument. - `public bool GetBool(string name)`: The value of a boolean argument. - `public double GetDouble(string name)`: The value of a double argument. - `public IReadOnlyList GetEntities(string name)`: The entities a selector argument matches (an empty list when none). - `public float GetFloat(string name)`: The value of a float argument. - `public int GetInt(string name)`: The value of an integer argument. - `public long GetLong(string name)`: The value of a long argument. - `public Player GetPlayer(string name)`: The single player a `Arg.Player` argument names. Throws `CommandFailure` when none matched. - `public IReadOnlyList GetPlayers(string name)`: The players a selector argument matches. - `public string GetString(string name)`: The value of a word / phrase / greedy string argument. - `public string GetText(string name)`: The text of any argument as typed (block states, items, components, nbt, uuids, resource ids, ...). - `public Vec3d GetVec3(string name)`: The position of a `Arg.Vec3` argument (relative coordinates resolved against the source). - `public bool Has(string name)`: Whether an optional argument was given. - `public void Reply(string message)`: Sends a plain message to whoever ran the command. - `public void Reply(Component message)`: Sends a rich message to whoever ran the command. - `public Player RequirePlayer()`: The sending player, or throws `CommandFailure` ("Only players can use this command") for the console. ## CommandExecutor (delegate, commands) ```csharp public delegate int CommandExecutor(CommandContext context) ``` A command executor: runs the command and returns its result (vanilla semantics: the number `/execute store` sees; 1 = success). ## CommandFailure (class, commands) ```csharp public sealed class CommandFailure : Exception ``` Thrown by an executor to fail the command with a message (red text, no stack trace in the log). The command result is 0. - `public CommandFailure(string message)`: Creates a failure with a plain message. - `public CommandFailure(string translationKey, string message)`: Creates a failure with a translation key (client-translated) and an English fallback message. - `public string? TranslationKey { get; }`: The translation key, if any. ## CommandHandler (delegate, commands) ```csharp public delegate void CommandHandler(CommandContext context) ``` A flat command body (`Commands.Register("name", ctx => ...)`): reads `CommandContext.Args` and may set `CommandContext.Result`. ## CommandNode (class, commands) ```csharp public sealed class CommandNode ``` A node of a command tree under construction: a literal word or a typed argument, with children, an executor, requirements and suggestions. Build trees with `Command.Literal` / `Command.Argument` and register them with `CommandRegistry.Register`. - `public bool IsLiteral { get; }`: Whether this is a literal word. - `public string Name { get; }`: The literal word or the argument name. - `public ArgType? Type { get; }`: The argument type, null for a literal. - `public CommandNode Executes(CommandExecutor executor)`: Runs `executor` when the command line ends at this node. - `public CommandNode Executes(Action action)`: Runs `action` when the command line ends at this node; the result is `CommandContext.Result` (default 1). - `public CommandNode OpLevel(int level)`: Requires a vanilla operator level (0-4) for this node. - `public CommandNode Permission(string node)`: Requires a permission node (see `Mc.Scripting.Permissions`) to see and use this node. Players without it do not get the node in their client's tree. The console always passes. - `public CommandNode Requires(Func requirement)`: Requires an arbitrary condition of the command source. A node with a condition is encoded per player in the client's command tree. - `public CommandNode Suggests(SuggestionProvider provider)`: Provides tab-completion suggestions for this argument (server-side, on the tick thread). - `public CommandNode Suggests(Func> provider)`: Provides suggestions from a list computed on the tick thread. - `public CommandNode SuggestsAsync(AsyncSuggestionProvider provider)`: Provides suggestions asynchronously (the client waits until the task completes). - `public CommandNode Then(CommandNode child)`: Adds a child node. ## CommandRegistry (class, commands) ```csharp public sealed class CommandRegistry ``` Command registration, scoped to a plugin (`plugin.Commands`). Plugin commands are nodes of the server's real command tree: they appear in the client's command tree with argument types and suggestions, and run through the same dispatcher as vanilla commands. Everything registered here is removed again when the plugin unloads (the clients' trees are refreshed once per tick). Two forms. The tree form `Register(root, aliases, description)` takes a literal built with `Command.Literal` and `Command.Argument`: typed arguments, suggestions, permissions and aliases, shown in the client's command tree. The flat form `Register(name, handler, description)` is a shortcut for a `name` literal followed by a greedy text argument; the handler reads `CommandContext.Args`. Plugin commands are removed when the plugin unloads. See [Commands](/concepts/commands/). - `public RegisteredCommand Register(CommandNode root, string[]? aliases = null, string description = "")`: Registers a command tree. - `public RegisteredCommand Register(string name, CommandHandler handler, string description = "")`: Registers a flat command: the word `name` followed by anything. The handler reads `CommandContext.Args`. Prefer `CommandRegistry.Register` for typed arguments, suggestions and permissions. ```csharp Commands.Register("hello", ctx => { ctx.Reply($"Hello, {(ctx.Args.Length > 0 ? ctx.Args[0] : "world")}!"); ctx.Result = ctx.Args.Length; }, "Says hello"); ``` ```csharp Commands.Register(Command.Literal("greet").Permission("hello.greet") .Then(Command.Argument("name", Arg.Word()) .Suggests(ctx => new[] { "world", "steve" }) .Executes(ctx => { ctx.Reply($"Hello, {ctx.GetString("name")}!"); return 1; })), description: "Greets someone"); ``` ## CommandSource (class, commands) ```csharp public sealed class CommandSource ``` Who runs a command: a player or the console, with a permission level, a position and a dimension. - `public Entity Entity { get; }`: The executing entity (`Entity.Null` for the console and command blocks). - `public bool IsConsole { get; }`: Whether the command runs from the server console. - `public int PermissionLevel { get; }`: The vanilla permission level of the source (0-4). - `public Player? Player { get; }`: The executing player, null when the console or another entity runs the command. - `public Vec3d Position { get; }`: The position the command runs at (the entity's position for players; the origin for the console). - `public World World { get; }`: The dimension the command runs in. - `public void Failure(string message)`: Sends a red failure message to the source (the command is not aborted). - `public void Success(string message, bool broadcastToOps = false)`: Sends feedback to the source (and, like vanilla, to operators when `broadcastToOps` is set). ## DetachedCommand (class, commands) ```csharp public sealed class DetachedCommand ``` What a command needed, copied out of a `CommandContext` so that it can be used after the executor returned (`CommandContext.Detach`). - `public string Input { get; }`: The whole command line without the leading slash. - `public string Name { get; }`: The command word. - `public int PermissionLevel { get; }`: The vanilla permission level of the source when the command ran. - `public Player? Player { get; }`: The executing player, null for the console. - `public Vec3d Position { get; }`: The position the command ran at. - `public Entity Sender { get; }`: The executing entity (`Entity.Null` for the console). - `public void Reply(string message)`: Sends a plain message to whoever ran the command (nothing happens when the player left; call on the tick thread). - `public void Reply(Component message)`: Sends a rich message to whoever ran the command. ## RegisteredCommand (class, commands) ```csharp public sealed class RegisteredCommand : IDisposable, IPluginResource ``` A command registered through `CommandRegistry`. Dispose to unregister (plugin unload does it automatically). - `public IReadOnlyList Names { get; }`: The names the command answers to (the main name first, then the aliases). - `public void Dispose()`: Unregisters the command. Calling it again does nothing. ## SuggestionContext (class, commands) ```csharp public sealed class SuggestionContext ``` What an asynchronous suggestion provider gets: a copy of the request (the native command context does not outlive the call that started the provider). - `public string Input { get; }`: The command line typed so far (without the leading slash). - `public int PermissionLevel { get; }`: The permission level of the asker. - `public Player? Player { get; }`: The player asking, null for the console. - `public Entity Sender { get; }`: The player (or entity) asking; `Entity.Null` for the console. - `public string Typed { get; }`: The text typed so far for the argument being completed. ## SuggestionProvider (delegate, commands) ```csharp public delegate void SuggestionProvider(CommandContext context, SuggestionsBuilder builder) ``` A suggestion provider: fills the builder with completions for the argument being typed. ## SuggestionsBuilder (class, commands) ```csharp public sealed class SuggestionsBuilder ``` Collects the suggestions of one argument. - `public string Remaining { get; }`: The text typed so far for the argument. - `public void Suggest(int value)`: Offers an integer completion. - `public void Suggest(string text, string? tooltip = null)`: Offers a completion. Like vanilla's `matchesSubStr`, completions that do not match `SuggestionsBuilder.Remaining` are dropped: the typed text must be a case-insensitive prefix of the completion or of a part of it after a `.`, `_` or `/`. ## Component (class, ecs) ```csharp public static class Component where T : unmanaged ``` Per-type component id cache. Set by `Ecs.RegisterComponent`. The cache is one slot per CLR type for the whole process, so a component type must be owned by a single plugin (declare your own struct; do not register a shared type such as `Vec3d` under different keys from different plugins). Registering a type that is already bound to another key throws. The binding is cleared when the registering plugin unloads. You rarely use this type directly: `Ecs.RegisterComponent(key)` fills it, and `Entity.Get()` and friends read the cached id from it. - `public static ulong Id { get; }`: ECS component id, 0 if not registered. - `public static string? Key { get; }`: The key the component type is registered under, null if unregistered. ## Config (class, config) ```csharp public sealed class Config : IDisposable, IPluginResource where T : class, new() ``` A typed, JSON-backed configuration file of a plugin (`plugins//config.json` by default). Defaults (the values of a freshly constructed `T`, including property initialisers) are written on first run; the file is read with comments and trailing commas allowed. Describe `T` with System.Text.Json source generation so loading needs no reflection. - `public string Path { get; }`: The path of the file. - `public T Value { get; }`: The current values: an immutable snapshot replaced as a whole on `Config.Reload` (safe to read from any thread). - `public bool Watch { get; set; }`: Whether the file is watched: edits on disk are picked up (debounced) and reported through `Config.Changed`. - `public void Dispose()` - `public bool Reload()`: Re-reads the file. A parse error keeps the previous value, logs the file and line, and never overwrites the file. - `public void Save()`: Writes the current value to the file (pretty-printed). Use it after changing a copy of `Config.Value` with `Config.Update`. - `public void Update(T value)`: Replaces the value (and saves it). - `public event Action? Changed;`: Raised after a reload that produced a new value (also when the file changed on disk with `Config.Watch` on), on the tick thread. ```csharp public sealed class HomesConfig { public int MaxHomes { get; set; } = 3; public int CooldownSeconds { get; set; } = 5; } [JsonSerializable(typeof(HomesConfig))] partial class Json : JsonSerializerContext; var config = Config.Load(Json.Default.HomesConfig); // config.Value.MaxHomes ``` ## PluginConfig (class, config) ```csharp public sealed class PluginConfig ``` Plugin-scoped access to configuration files (`plugin.Config`) and the data folder. - `public string DataFolder { get; }`: The plugin's data folder (`plugins//`), created on first access. Put every file the plugin writes here. - `public Config Load(JsonTypeInfo typeInfo, string fileName = "config.json") where T : class, new()`: Loads (and creates, with defaults, when missing) a JSON config file in the data folder. - `public string? ReadText(string fileName)`: Reads a text file of the data folder, null when it does not exist. - `public void WriteText(string fileName, string text)`: Writes a text file of the data folder atomically (temp file + rename). ## BlockBreakContext (class, content) ```csharp public sealed class BlockBreakContext ``` What a custom block's break handler gets. Valid during the handler only. - `public CustomBlock Block { get; }`: The block definition. - `public bool Cancelled { get; set; }`: Keeps the block (nothing breaks). - `public bool DropItems { get; set; }`: Whether the drops of the block are spawned (default true; nothing drops in creative mode anyway). - `public Player Player { get; }`: The player breaking it. - `public BlockPos Position { get; }`: The position. - `public ItemStack Tool { get; }`: The tool in the player's hand. - `public World World { get; }`: The dimension. ## BlockInteractContext (class, content) ```csharp public sealed class BlockInteractContext ``` What a custom block's interact handler gets. Valid during the handler only. - `public CustomBlock Block { get; }`: The block definition. - `public bool Cancelled { get; set; }`: Stops the item use after the block (placing a block against it, the held item's own use). - `public BlockFace Face { get; }`: The clicked face. - `public Hand Hand { get; }`: The hand. - `public ItemStack Item { get; }`: The stack in that hand. - `public Player Player { get; }`: The player. - `public BlockPos Position { get; }`: The position of the block. - `public World World { get; }`: The dimension. ## BlockPlaceContext (class, content) ```csharp public sealed class BlockPlaceContext ``` What a custom block's place handler gets. Valid during the handler only. - `public BlockPos Against { get; }`: The block that was clicked. - `public CustomBlock Block { get; }`: The block definition. - `public bool Cancelled { get; set; }`: Refuses the placement. - `public Hand Hand { get; }`: The hand. - `public Player Player { get; }`: The player. - `public BlockPos Position { get; }`: Where the block is placed. - `public World World { get; }`: The dimension. ## BlockRegistry (class, content) ```csharp public sealed class BlockRegistry ``` Registers custom blocks. - `public IReadOnlyList All { get; }`: Every registered block (a snapshot). - `public CustomBlock? At(World world, BlockPos pos)`: The custom block a world position holds. - `public CustomBlock? Get(string id)`: The registered block with this id. - `public CustomBlock? Of(BlockState state)`: The custom block a block state stands for. - `public void Register(CustomBlock block)`: Registers a block for the plugin whose code is running: picks its block state (the same one after a restart), marks it custom in the engine and starts listening for its breaking and clicking. Released when the plugin unloads. ## Content (class, content) ```csharp public static class Content ``` Entry point of the custom content API: `Content.Items` and `Content.Blocks` (and `ResourcePackBuilder`, `ResourcePackHost`). - `public static BlockRegistry Blocks { get; }`: Custom blocks. - `public static ItemRegistry Items { get; }`: Custom items. ## CustomBlock (class, content) ```csharp public sealed class CustomBlock ``` A custom block: a state of a vanilla block that vanilla play seldom produces (and never turns a block into: the engine guards it), with its own hardness, drops and behaviour. The world stores a plain vanilla block state (so a server without the plugin shows a note block, never a corrupt chunk); a resource pack gives that state its own model (`ResourcePackBuilder.AddCustomBlock`). **Which state.** Custom blocks live in the note block's states with a mob head instrument (the ones nobody gets without a head above the block), 175 of them, handed out in registration order and remembered in the file named by `content.state_file` in Ramjet.toml, so the same id gets the same state after a restart. `CustomBlock.BaseBlock` names the family: only `minecraft:note_block` exists in this version. While a block is registered the vanilla note block behaviour (tuning, notes, the instrument following the block below) is suppressed for its state. **Mining.** The server decides how long a block takes to break, from `CustomBlock.HardnessValue`; the client predicts it from the note block (hardness 0.8), so a block that takes much longer than that appears to break on the client and comes back (the server refuses to finish it early): keep custom hardness near 0.8 or lower for smooth mining. - `public CustomBlock(string id, string baseBlock = "minecraft:note_block")`: Creates a definition. - `public string BaseBlock { get; }`: The vanilla block whose states host it. - `public float HardnessValue { get; }`: How long it takes to mine (vanilla hardness; negative = unbreakable). - `public string Id { get; }`: The id. - `public string? ModelId { get; }`: The model id of the block in the resource pack (`assets/myplugin/models/block/ruby_ore.json` is `"myplugin:block/ruby_ore"`), or null. - `public BlockState State { get; }`: The block state that stands for the block in the world (valid after registration). - `public CustomBlock Drops(Func> drops)`: Sets what the block drops when a player breaks it (nothing drops in creative mode, and nothing by default). - `public CustomBlock DropsItem(CustomItem item, int count = 1)`: The block drops this item (a registered custom item or any stack) when broken. - `public CustomBlock Hardness(float hardness)`: Sets the hardness. - `public CustomBlock Model(string modelId)`: Sets the model the resource pack gives the block (used by `ResourcePackBuilder.AddCustomBlock`). - `public CustomBlock OnBreak(Action handler)`: Sets what happens when a player breaks the block (before the drops; cancel the context to keep the block). - `public CustomBlock OnInteract(Action handler)`: Sets what happens when a player right-clicks the block (sneaking with an item in hand skips it, like vanilla blocks). - `public CustomBlock OnPlace(Action handler)`: Sets what happens when a player places the block with a custom item (`CustomItem.Places`); cancel the context to refuse. - `public bool Place(World world, BlockPos pos)`: Puts the block into the world (registered blocks only). ## CustomItem (class, content) ```csharp public sealed class CustomItem ``` A custom item: a vanilla item the client already knows (the base item decides the id the client sees and the behaviour class) carrying data components that make it a different item: `custom_data` (the id, found again with `ItemStack.CustomId`), `item_model` (the look, from a resource pack), a name, lore, a stack size and a rarity. No client mod is needed. Register it with `ItemRegistry.Register`, hand out stacks with `CustomItem.CreateStack`. Choose an inert base item (`minecraft:paper`, `minecraft:stick`, `minecraft:amethyst_shard`): the vanilla behaviour of the base item still happens unless a use handler cancels it. Stacks keep their id when the plugin is unloaded; they behave like the base item until it is back. - `public CustomItem(string id, string baseItem)`: Creates a definition. - `public string BaseItem { get; }`: The vanilla base item. - `public int CooldownTicks { get; }`: Ticks a player waits between two uses (0 for none). - `public Component? DisplayName { get; }`: The display name. - `public bool? HasGlint { get; }`: The enchantment glint (null: the item's own). - `public string Id { get; }`: The id. - `public IReadOnlyList LoreLines { get; }`: The lore lines. - `public int MaxStackSize { get; }`: The maximum stack size, 0 for the base item's own. - `public string? ModelId { get; }`: The item model id (an item definition of the resource pack), or null for the base item's own. - `public string? PlacesBlock { get; }`: The id of the custom block the item places when used on a block, or null. - `public ItemRarity? Rarity { get; }`: The rarity, null for the base item's own. - `public Action? UseHandler { get; }`: The handler called when a player uses the item in the air or on a block. - `public Action? UseOnHandler { get; }`: The handler called when a player uses the item on a block. - `public CustomItem Cooldown(int ticks)`: Makes a player wait between two uses: further uses are ignored (cancelled) during the cooldown. - `public ItemStack CreateStack(int count = 1)`: A stack of the item with every component of the definition. - `public CustomItem Glint(bool glint = true)`: Forces the enchantment glint on or off. - `public CustomItem Lore(params Component[] lines)`: Sets the lore lines. - `public bool Matches(ItemStack stack)`: Whether a stack is an instance of this item (its `ItemStack.CustomId` is the id). - `public CustomItem MaxStack(int max)`: Sets the maximum stack size. - `public CustomItem Model(string modelId)`: Sets the item model (`minecraft:item_model`). - `public CustomItem Name(Component name)`: Sets the display name (`minecraft:item_name`: not italic, replaced by an anvil rename). - `public CustomItem OnUse(Action handler)`: Sets what happens when a player uses the item (right click in the air or on a block). Cancel the context to stop the vanilla behaviour of the base item. - `public CustomItem OnUseOn(Action handler)`: Sets what happens when a player uses the item on a block (after {{cref:M:Mc.Scripting.Content.CustomItem.OnUse(System.Action{Mc.Scripting.Content.ItemUseContext})}}). - `public CustomItem Places(CustomBlock block)`: Makes the item place a custom block against the clicked face (and use up one item). - `public CustomItem WithRarity(ItemRarity rarity)`: Sets the rarity. ## ItemRarity (enum, content) ```csharp public enum ItemRarity : int ``` How rare an item is (the colour of its name). - `Common` = 0: White. - `Uncommon` = 1: Yellow. - `Rare` = 2: Aqua. - `Epic` = 3: Light purple. ## ItemRegistry (class, content) ```csharp public sealed class ItemRegistry ``` Registers custom items. - `public IReadOnlyList All { get; }`: Every registered item (a snapshot). - `public ItemStack Create(string id, int count = 1)`: A stack of a registered item. - `public CustomItem? Get(string id)`: The registered item with this id. - `public CustomItem? Of(ItemStack stack)`: The definition of a stack that is a registered custom item. - `public void Register(CustomItem item)`: Registers an item for the plugin whose code is running (released when the plugin unloads). ## ItemUseContext (class, content) ```csharp public sealed class ItemUseContext ``` What a custom item's use handler gets. Valid during the handler only. - `public bool Cancelled { get; set; }`: Stops the vanilla behaviour of the base item and of the clicked block. - `public CustomItem Definition { get; }`: The definition of the item. - `public BlockFace Face { get; }`: The clicked face (only when `ItemUseContext.HasBlock`). - `public Hand Hand { get; }`: The hand holding the item. - `public bool HasBlock { get; }`: Whether the click was on a block. - `public ItemStack Item { get; }`: The stack that was used. - `public Player Player { get; }`: The player. - `public BlockPos Position { get; }`: The clicked block (only when `ItemUseContext.HasBlock`). - `public World World { get; }`: The dimension the player is in. - `public void Consume(int count = 1)`: Uses up items of the stack that was used (nothing happens in creative mode). ## PackImage (class, content) ```csharp public static class PackImage ``` Small generated textures for packs and tests: 8-bit RGBA PNG files made without any imaging library. - `public static byte[] Checker(uint a, uint b, int size = 16, int cell = 4)`: A PNG of a two colour checker board. - `public static byte[] Create(int width, int height, Func pixel)`: A PNG from a pixel function. - `public static byte[] Solid(uint argb, int size = 16)`: A PNG of one colour. ## ResourcePackBuilder (class, content) ```csharp public sealed class ResourcePackBuilder ``` Builds the resource pack zip that goes with custom content: item definitions, models, textures, language files and the block state remap of custom blocks. The output is deterministic (sorted entries, fixed timestamps), so the same input gives the same SHA-1 and clients keep their cached copy until something changes. - `public string Name { get; }`: The pack name. - `public ResourcePackBuilder AddCustomBlock(CustomBlock block, byte[] png)`: Gives the block state of a registered custom block a cube model with one texture. Writes the block state file of the note block with every variant: the custom ones point at the new model, all others keep the vanilla model. - `public ResourcePackBuilder AddCustomBlockModel(CustomBlock block, string modelId)`: Points the block state of a registered custom block at a model of the pack (written by `ResourcePackBuilder.AddModel` or `ResourcePackBuilder.AddFile`). - `public ResourcePackBuilder AddFile(string path, byte[] content)`: Adds (or replaces) a file of the pack. - `public ResourcePackBuilder AddFile(string path, string text)`: Adds (or replaces) a text file of the pack. - `public ResourcePackBuilder AddItem(string itemModelId, byte[] png)`: Adds a flat item: the item definition, a `item/generated` model and the texture, all named after the item model id. - `public ResourcePackBuilder AddItem(CustomItem item, byte[] png)`: Adds a flat item for a custom item (its `CustomItem.ModelId` names the model). - `public ResourcePackBuilder AddItemDefinition(string itemModelId, string json)`: Adds an item definition (`assets//items/.json`): what a stack with this `item_model` looks like. - `public ResourcePackBuilder AddLang(string locale, IReadOnlyDictionary entries, string? namespaceId = null)`: Adds a language file (`assets//lang/.json`). - `public ResourcePackBuilder AddModel(string modelId, string json)`: Adds a model (`assets//models/.json`). - `public ResourcePackBuilder AddTexture(string textureId, byte[] png)`: Adds a texture (`assets//textures/.png`). - `public ResourcePackFile Build()`: Builds the zip and its SHA-1. - `public ResourcePackBuilder Description(string description)`: Sets the description shown in the pack list. - `public ResourcePackBuilder PackFormat(int format)`: Sets the resource pack format number written to `pack.mcmeta` (97 for 26.3). - `public static ResourcePackBuilder Create(string name)`: Starts a pack. ```csharp var pack = ResourcePackBuilder.Create("myplugin") .Description("My plugin") .AddItem(ruby, PackImage.Solid(0xFFE0115F)) .AddCustomBlock(rubyOre, PackImage.Checker(0xFF8B0A3C, 0xFFE0115F)) .AddLang("en_us", new Dictionary { ["item.myplugin.ruby"] = "Ruby" }) .Build(); pack.SendTo(player); // hosted by ResourcePackHost (content.pack_host_port), or pack.ToResourcePack(yourUrl) ``` ## ResourcePackFile (class, content) ```csharp public sealed class ResourcePackFile ``` A built resource pack: the zip, its SHA-1 and the id clients know it by. - `public byte[] Bytes { get; }`: The zip. - `public Uuid Id { get; }`: The pack id (derived from the name: a pack of the same name replaces the older one on the client). - `public string Name { get; }`: The pack name. - `public string Sha1 { get; }`: The SHA-1 of the zip (40 lower-case hex characters). - `public int Size { get; }`: The size of the zip in bytes. - `public ResourcePack Publish(bool required = false, Component? prompt = null)`: Hosts the zip with `ResourcePackHost` and returns the description a client needs. - `public void SendTo(Player player, bool required = false, Component? prompt = null)`: Hosts the pack (see `ResourcePackFile.Publish`) and asks a player to download it. - `public ResourcePack ToResourcePack(string url, bool required = false, Component? prompt = null)`: The pack as the packet a client needs, for a URL you host yourself. - `public void WriteTo(string path)`: Writes the zip to a file. ## ResourcePackHost (class, content) ```csharp public static class ResourcePackHost ``` A small built-in web server that hands out the resource packs plugins publish, so a plugin needs no web space of its own. It is off until it is configured (`content.pack_host_port` in Ramjet.toml, with `pack_host_bind` and the public `pack_host_url`) or started by code with `ResourcePackHost.Start`; it listens on 127.0.0.1 unless told otherwise, answers only `GET` / `HEAD` of `/.zip` for packs that were published, and keeps nothing of the plugins that published them (a pack is removed when its plugin unloads). - `public static string? BaseUrl { get; }`: The base URL the clients use (`http://host:port`), or null while the server is off. - `public static bool IsRunning { get; }`: Whether the server is listening. - `public static int Port { get; }`: The port the server listens on, 0 while it is off. - `public static string Publish(ResourcePackFile pack)`: Publishes a pack and returns the URL it is downloadable from. The pack stays available until the plugin that published it unloads (or `ResourcePackHost.Unpublish`). - `public static string Start(string bind = "127.0.0.1", int port = 0, string? publicUrl = null)`: Starts the server (does nothing when it already runs). - `public static void Stop()`: Takes back the packs the calling plugin published, and stops the server when it was started by code (`ResourcePackHost.Start`) and no published pack of any plugin is left. Packs of other plugins stay available; a server started by the configuration keeps running. - `public static void Unpublish(ResourcePackFile pack)`: Stops handing out a pack the calling plugin published (it stays available while another plugin publishes the same pack). ## DamageCause (enum, events) ```csharp public enum DamageCause : byte ``` Why an entity took damage (derived from the vanilla damage type). - `Other` = 0: Any damage type without a dedicated cause. - `Contact` = 1: Cactus, stalagmite, sweet berry bush. - `EntityAttack` = 2: Melee attack by an entity. - `Projectile` = 3: Arrow, trident, fireball, thrown item, spit, wind charge ... - `Suffocation` = 4: Inside a wall. - `Fall` = 5: Fall damage. - `Fire` = 6: Standing in fire. - `FireTick` = 7: Burning. - `Lava` = 8: Lava. - `Drowning` = 9: Out of air. - `Explosion` = 10: An explosion. - `Void` = 11: Below the world. - `Lightning` = 12: Lightning bolt. - `Starvation` = 13: Starvation. - `Magic` = 14: Magic, poison, instant damage. - `Wither` = 15: The wither effect or a wither. - `FallingBlock` = 16: A falling block or anvil. - `Thorns` = 17: Thorns enchantment. - `FlyIntoWall` = 18: Elytra kinetic damage. - `HotFloor` = 19: Magma block. - `Cramming` = 20: Entity cramming. - `Dryout` = 21: Water animals out of water. - `Freeze` = 22: Powder snow. - `Kill` = 23: /kill or the generic kill damage. - `Border` = 24: Outside the world border. - `Campfire` = 25: Campfire. - `MaceSmash` = 26: Mace smash attack. - `Custom` = 27: Damage dealt by a plugin. ## DisplaySlot (enum, display) ```csharp public enum DisplaySlot : int ``` Where an objective is displayed. - `List` = 0: The player list (Tab), next to each name. - `Sidebar` = 1: The sidebar at the right of the screen. - `BelowName` = 2: Below the name tags of players. ## DragType (enum, inventories) ```csharp public enum DragType : byte ``` Kind of inventory drag. - `Even` = 0: Left drag: split the cursor stack evenly. - `Single` = 1: Right drag: one item per slot. - `Clone` = 2: Middle drag (creative). ## Ecs (class, ecs) ```csharp public sealed class Ecs ``` ECS access scoped to a plugin (`plugin.Ecs`): custom components, queries and raw entities. Entities are not owned (they survive plugin reloads); queries are, and are destroyed on unload. Game worlds (dimensions) are `World`. Game worlds (dimensions) are a different type, `Mc.Scripting.Worlds.World`, reached through `Server.Worlds` and `Server.Overworld`. - `public Entity GetEntity(ulong id)`: Wraps an entity id. - `public Query Query() where T1 : unmanaged`: Creates a cached query over one component type. - `public Query Query() where T1 : unmanaged where T2 : unmanaged`: Creates a cached query over two component types. - `public Query Query() where T1 : unmanaged where T2 : unmanaged where T3 : unmanaged`: Creates a cached query over three component types. - `public ulong RegisterComponent(string key) where T : unmanaged`: Registers (or re-binds) a plain-data component backed by a flecs component. Keys are global ("myplugin:health"); registering the same key with the same layout (field types and offsets) after a hot reload keeps existing component values. A changed layout drops the values, but is refused while another plugin is also bound to the key. Engine components can be bound by their registered name as long as the layout matches. - `public Entity Spawn()`: Creates an empty ECS entity (not a game entity: it has no position and is never sent to clients). ```csharp public struct Mana { public int Value; } private Query _mana = null!; protected override void OnEnable() { Ecs.RegisterComponent("hello:mana"); _mana = Ecs.Query(); Systems.Add("regen", Phase.Tick, _ => { foreach (var chunk in _mana) foreach (ref var m in chunk.Field(0)) m.Value++; }); } ``` ## Entity (struct, ecs) ```csharp public readonly struct Entity : IEquatable ``` Handle to an ECS entity (flecs id). Blittable: usable directly in events and components. Refs returned by `Entity.Get` point into ECS storage and are invalidated by structural changes (adding/removing components, destroying entities). Don't keep them. A handle stays valid as a value forever; whether the entity behind it is alive is a separate question (`IsAlive`). Operations on a dead handle fail (`Get` throws, `Has` is false). - `public Entity(ulong id)`: Wraps an entity id. - `public readonly ulong Id;`: The flecs entity id (index and generation); 0 is the null entity. - `public bool IsAlive { get; }`: Whether the entity exists. A destroyed entity's id goes stale (the generation no longer matches). - `public bool IsGameEntity { get; }`: Whether this is an entity of the game world (a player, mob, item, orb, projectile ...) as opposed to a bare ECS entity. - `public bool IsNull { get; }`: Whether this is the null entity. - `public Location Location { get; }`: The location of a game entity. - `public string TypeKey { get; }`: The entity type key of a game entity (`"minecraft:zombie"`, `"minecraft:item"`, `"minecraft:player"`), empty for a stale handle or an entity that is not a game entity (one created with `Entity.Spawn`). - `public static Entity Null { get; }`: The null entity. - `public bool Despawn()`: Removes a game entity from the world without any death logic (no drops, no death event): items, orbs, mobs, projectiles. Players cannot be removed (use `Player.Kick`). - `public void Destroy()`: Destroys an entity created with `Entity.Spawn`. Does nothing for the null entity or a stale handle. Entities of the game (players, mobs, items, anything an event or a handle hands out) are not the plugin's to destroy: the call is ignored with a warning (use the game APIs, e.g. `Player.Kick`). - `public bool Equals(Entity other)`: Same entity id. - `public override bool Equals(object? obj)`: Same entity id. - `public ref T Get() where T : unmanaged`: Reference to component `T`; throws if the entity lacks it. For a component the game registered itself (bound by its engine name) the reference points at a COPY: engine components are read only for plugins, writing through it changes nothing. - `public override int GetHashCode()`: Hash of the id. - `public NbtCompound? GetNbt()`: The data of a game entity as `/data get entity` shows it (position, motion, health, name, item, ...). - `public ref T GetOrAdd() where T : unmanaged`: Reference to the component, adding it (zero-initialised) when missing. - `public bool Has() where T : unmanaged`: Whether the entity has component `T`. - `public bool Kill()`: Kills a living entity the way the `/kill` command does (generic kill damage: death events, drops and the death animation); other entities are removed. - `public Entity Remove() where T : unmanaged`: Removes a component. - `public Entity Set(in T value) where T : unmanaged`: Sets (adds or overwrites) a component value. - `public bool SetGlowingFor(Player viewer, bool glowing)`: Makes this entity (a player or a mob) glow for one viewer: the viewer sees the glowing outline on it, the other players do not. The colour is the vanilla white (no team colour). The glow ends with `glowing = false`, with the entity, or with the plugin. - `public bool SetNbt(NbtCompound data, bool merge = true)`: Changes the data of a game entity like `/data merge entity`. Players cannot be edited this way (use the player API). - `public bool Teleport(Location location)`: Moves a game entity. Players go through `Player.Teleport` (with its event); mobs, items and the like move silently, within their dimension. - `public override string ToString()`: Debug text. - `public bool TryGet(out T value) where T : unmanaged`: Copies the component if present. - `public bool TryGetLocation(out Location location)`: Where a game entity is. - `public ref T TryGetRef() where T : unmanaged`: Pointer-style access: a null ref (check with Unsafe.IsNullRef) when absent. - `public static Entity Spawn()`: Creates an empty ECS entity (not a game entity). - `public static bool operator !=(Entity a, Entity b)`: Different entity id. - `public static bool operator ==(Entity a, Entity b)`: Same entity id. ```csharp public struct Mana { public int Value; } // in OnEnable: Ecs.RegisterComponent("hello:mana"); // in an event handler, where e is a PlayerJoinEvent: Entity player = e.Player; // a Player converts to its Entity player.Set(new Mana { Value = 10 }); // add or overwrite if (player.Has()) player.Get().Value--; // in-place edit through a ref if (player.TryGet(out var copy)) Log.Info($"mana {copy.Value}"); player.Remove(); ``` ## EventBus (class, events) ```csharp public static class EventBus ``` Owner-less event firing helpers. - `public static bool Fire(ref T ev) where T : struct, IEvent`: Fires an event through the native bus; returns false if it was cancelled. - `public static bool HasSubscribers() where T : struct, IEvent`: Whether anybody (native or managed) listens to `T`. ## EventHeader (struct, events) ```csharp public struct EventHeader ``` First field of every event struct (McEventHeader). The first field of every event struct. Handlers use `Cancelled`; the other fields are managed by the runtime. - `public byte Flags;`: Flag bits (bit 0 = cancellable). - `public ushort Id;`: Event type id. - `public uint Size;`: Size of the full event struct. - `public bool Cancelled { get; set; }`: Whether a handler cancelled the event (ignored if not cancellable). - `public bool IsCancellable { get; }`: Whether the event type can be cancelled. - `public static EventHeader For() where T : struct, IEvent`: Creates the header for event type `T`. ## EventListener (delegate, events) ```csharp public delegate void EventListener(ref T ev) where T : struct, IEvent ``` Event handler; gets the event by reference (mutable fields and Cancelled are writable). ## EventPriority (enum, events) ```csharp public enum EventPriority : int ``` Lower runs first. Any int is allowed; these are conventional values. - `Lowest` = -200: Runs first. - `Low` = -100: Runs early. - `Normal` = 0: Default. - `High` = 100: Runs late. - `Highest` = 200: Runs last before monitors. - `Monitor` = 1000: Observe the final outcome; must not modify the event. ## EventRegistry (class, events) ```csharp public sealed class EventRegistry ``` Event subscription and firing, scoped to a plugin (everything is released on unload). - `public bool Fire(ref T ev) where T : struct, IEvent`: Fires an event through the native bus (native and managed handlers, by priority). Returns false if it was cancelled. - `public bool HasSubscribers() where T : struct, IEvent`: Whether anybody (native or managed) listens to `T`. - `public Subscription On(EventListener listener, EventPriority priority = EventPriority.Normal, bool receiveCancelled = false) where T : struct, IEvent`: Subscribes `listener` to events of type `T`. ```csharp // Block spam (High runs after Normal, so other plugins' edits are already applied) Events.On((ref PlayerChatEvent e) => { if (e.Message.Contains("spam", StringComparison.OrdinalIgnoreCase)) e.Cancelled = true; }, EventPriority.High); // Rewrite a mutable field Events.On((ref EntityDamageEvent e) => e.Amount *= 0.5f); // Observe the final outcome without changing it Events.On((ref PlayerChatEvent e) => Log.Info(e.Cancelled ? "chat was blocked" : "chat delivered"), EventPriority.Monitor, receiveCancelled: true); ``` ## EventType (class, events) ```csharp public static class EventType where T : struct, IEvent ``` Resolved runtime id per event type. - `public static readonly ushort Id;`: The runtime event id. ## LoginDeferral (class, events) ```csharp public sealed class LoginDeferral : IDisposable, IPluginResource ``` A login held by a plugin (see `PlayerPreLoginEvent.Defer`). Completing it is thread-safe and happens once. - `public bool IsCompleted { get; }`: Whether the deferral was already completed. - `public static Component UnloadDenyMessage { get; set; }`: What a player sees when the plugin holding his login unloaded before it answered (the safe default: a check that never finished is not a pass). - `public void Allow()`: Lets the login continue. Callable from any thread. - `public void Deny(Component? reason = null)`: Refuses the login. Callable from any thread. - `public void Dispose()`: The plugin unloaded with the deferral pending: the login is refused with `LoginDeferral.UnloadDenyMessage`. A check that never answered (a ban list lookup) must not turn into a pass; the player just reconnects. Complete the deferral yourself with `LoginDeferral.Allow` to decide otherwise. ## Facing (enum, worlds) ```csharp public enum Facing : int ``` Values of the protocol enum Facing (the wire carries the ordinal). - `Down` = 0: Facing down (ordinal 0). - `Up` = 1: Facing up (ordinal 1). - `North` = 2: Facing north (ordinal 2). - `South` = 3: Facing south (ordinal 3). - `West` = 4: Facing west (ordinal 4). - `East` = 5: Facing east (ordinal 5). ## GameMode (enum, players) ```csharp public enum GameMode : byte ``` Vanilla game mode (the numbers are the protocol ids). - `Survival` = 0: Survival mode. - `Creative` = 1: Creative mode. - `Adventure` = 2: Adventure mode. - `Spectator` = 3: Spectator mode. ## GameModeChangeCause (enum, events) ```csharp public enum GameModeChangeCause : byte ``` Why a game mode changed. - `Unknown` = 0: Unknown cause. - `Command` = 1: /gamemode or /defaultgamemode. - `Plugin` = 2: A plugin set the game mode. - `DefaultGameMode` = 3: The server default game mode was applied. ## Hand (enum, players) ```csharp public enum Hand : byte ``` The hand an action was performed with. - `MainHand` = 0: The main hand. - `OffHand` = 1: The off hand. ## Hologram (class, visuals) ```csharp public sealed class Hologram : IDisposable, IPluginResource ``` Text that floats in the world. The players see it (it is an entity on their side only: the server has none), and it is not saved with the world. Create it with `Holograms.Create`. It disappears for every viewer when the plugin unloads or when `Hologram.Remove` is called. Holograms are shown to everybody until `Hologram.HideFromAll` or `Hologram.ShowTo`. - `public bool IsActive { get; }`: Whether the hologram still exists. - `public HologramOptions Options { get; set; }`: The look of the hologram. Assign a new `HologramOptions` to change it. - `public Vec3d Position { get; }`: Where it is. - `public Component Text { get; set; }`: The text (a component; a newline makes a line break). - `public void Dispose()` - `public Hologram HideFrom(Player player)`: Hides the hologram from one player (while it is shown to everybody). - `public Hologram HideFromAll()`: Hides the hologram from everybody (clears the list of players it is shown to or hidden from). - `public Hologram Move(Vec3d position)`: Moves the hologram inside its world. - `public void Remove()`: Removes the hologram from every viewer and deletes it. - `public Hologram ShowTo(Player player)`: Shows the hologram to one player (after `Hologram.HideFromAll`), or hides it from one player while it is shown to everybody. - `public Hologram ShowToAll()`: Shows the hologram to everybody again (clears the list of players it is shown to or hidden from). ## HologramAlign (enum, visuals) ```csharp public enum HologramAlign : int ``` How the lines of a hologram are aligned. - `Center` = 0: Centred. - `Left` = 8: Left aligned. - `Right` = 16: Right aligned. ## HologramBillboard (enum, visuals) ```csharp public enum HologramBillboard : int ``` How a hologram turns towards the players. - `Fixed` = 0: Fixed in the world. - `Vertical` = 1: Turns around the vertical axis. - `Horizontal` = 2: Turns around the horizontal axis. - `Center` = 3: Always faces the player. ## HologramOptions (class, visuals) ```csharp public sealed class HologramOptions ``` The look of a hologram (the fields of the client's text display; the defaults are vanilla's). - `public HologramOptions()` - `public HologramAlign Align { get; set; }`: The alignment of the lines. - `public int Background { get; set; }`: The background colour, ARGB (0x40000000 is vanilla's translucent black). - `public HologramBillboard Billboard { get; set; }`: How the text turns towards the players. - `public bool DefaultBackground { get; set; }`: Uses the client's default background instead of `HologramOptions.Background`. - `public int LineWidth { get; set; }`: The wrap width in pixels (1..1024). - `public float Scale { get; set; }`: The uniform scale (0 .. 64]. - `public bool SeeThrough { get; set; }`: The text shows through blocks. - `public bool Shadow { get; set; }`: Draws a shadow under the text. - `public float ShadowRadius { get; set; }`: The shadow radius (0..16). - `public float ShadowStrength { get; set; }`: The shadow strength (0..1). - `public int? TextOpacity { get; set; }`: The text opacity 0..255; null for the default. - `public float ViewRange { get; set; }`: The view distance multiplier (0..64). ## Holograms (class, visuals) ```csharp public sealed class Holograms ``` Creates holograms for this plugin. Everything made here is removed when the plugin unloads. - `public Hologram Create(World world, Vec3d position, Component text, HologramOptions? options = null)`: Creates a hologram that everybody sees. - `public Hologram Create(World world, Vec3d position, IEnumerable lines, HologramOptions? options = null)`: Creates a hologram from plain lines (one per line of text). ## Http (class, server) ```csharp public static class Http ``` HTTP(S) for plugins. In the plugin worker the requests are performed by the server: its HTTP client resolves each name, refuses loopback, private, link-local and metadata addresses, limits request and response sizes and the rate, and enforces the timeouts (see docs/agents/http-egress.md, scripting.http.* in Ramjet.toml). In an in-process host (development only) requests go out directly. Use `Http.Client`, or `new HttpClient(Http.Handler)` for your own timeouts and headers. Do not dispose either: they are shared by every plugin. - `public static HttpClient Client { get; }`: A shared client over `Http.Handler`. Set the time limit per request with a `CancellationToken`; the server applies its own total timeout as well. Do not dispose it. - `public static HttpMessageHandler Handler { get; }`: The message handler behind every request. Share it; do not dispose it. ## IEvent (interface, events) ```csharp public interface IEvent ``` An event type. Catalogue events are `ref struct`s: a handler gets the event by reference and can neither store it nor capture it in a closure or an async method, so the borrowed native data it carries (strings, byte buffers) cannot outlive the dispatch. Plugin-defined events are ordinary blittable structs that start with an `EventHeader` field (or ref structs of the same layout). Catalogue events are generated from src/scripting/events.json. Plugins can declare their own events: implement `IEvent.Key` (e.g. "myplugin:thing") and leave `IEvent.CatalogueId` at 0; an id is assigned at runtime. - `static virtual ushort CatalogueId { get; }`: Catalogue id for built-in events, 0 for plugin-defined events. - `static virtual bool IsCancellable { get; }`: Whether handlers can cancel the event. - `static abstract string Key { get; }`: Registry key ("plugin:name" for plugin events). ```csharp public struct HomeSetEvent : IEvent { public EventHeader Header; // must come first public Entity Player; public BlockPos Position; public static string Key => "homes:set"; public static bool IsCancellable => true; } // fire it from your plugin var ev = new HomeSetEvent { Player = p, Position = pos }; if (Events.Fire(ref ev)) { /* nobody cancelled */ } ``` ## IPlugin (interface, plugins) ```csharp public interface IPlugin ``` Plugin entry point. One public, non-abstract type with a parameterless constructor per plugin assembly. Most plugins derive from `Plugin` instead. - `void Disable()`: Called when the plugin is unloaded; registrations are released afterwards. - `void Enable(PluginContext context)`: Called once after load; register everything here. ## IPluginResource (interface, plugins) ```csharp public interface IPluginResource : IDisposable ``` Something owned by a plugin that must be released when it unloads. Implement it for your own objects and call `PluginContext.Track` so they are disposed on unload. ## IgniteCause (enum, events) ```csharp public enum IgniteCause : byte ``` What set a block on fire. - `FlintAndSteel` = 0: A player used flint and steel. - `Lava` = 1: Lava next to a flammable block. - `Lightning` = 2: A lightning strike. - `Spread` = 3: Fire spreading from another fire. - `FireCharge` = 4: A fire charge. - `Explosion` = 5: An explosion. - `Arrow` = 6: A flaming arrow. - `Plugin` = 7: A plugin. - `Other` = 8: Anything else. ## InteractAction (enum, events) ```csharp public enum InteractAction : byte ``` What a player clicked. - `LeftClickAir` = 0: Left click without a block target. - `LeftClickBlock` = 1: Left click (start digging) on a block. - `RightClickAir` = 2: Right click without a block target. - `RightClickBlock` = 3: Right click on a block. ## InteractionHand (enum, players) ```csharp public enum InteractionHand : int ``` Values of the protocol enum InteractionHand (the wire carries the ordinal). - `MainHand` = 0: InteractionHand main_hand (ordinal 0). - `OffHand` = 1: InteractionHand off_hand (ordinal 1). ## BlockInventory (class, inventories) ```csharp public sealed class BlockInventory : EngineInventory ``` The inventory of a block entity by position: chests, trapped chests, barrels, furnaces, blast furnaces, smokers, hoppers, droppers, dispensers, brewing stands and shulker boxes. A double chest is two inventories of 27 slots (one per half). The handle holds a position, not the block entity: it is empty when the block goes away. - `public BlockPos Position { get; }`: The block position. - `public World World { get; }`: The dimension. - `public static BlockInventory? At(World world, BlockPos pos)`: The inventory of the block entity at a position. ## EngineInventory (class, inventories) ```csharp public abstract class EngineInventory : IInventory ``` Shared implementation of the inventories the engine owns: one native call per operation, the inventory is addressed by (kind, world, reference) every time, so a handle never keeps anything alive and a block that was destroyed meanwhile simply reads as an empty inventory. - `public IEnumerable Contents { get; }` - `public bool Exists { get; }`: Whether the inventory still exists (a destroyed block entity or a closed player has none). - `public virtual int Size { get; }` - `public virtual int AddItem(ItemStack stack)` - `public virtual void Clear()` - `public bool Contains(ItemStack stack)`: Whether the inventory holds at least `stack`.Count items of its type. - `public int Count(ItemStack stack)`: How many items of this type (and components) the inventory holds in total. - `public virtual ItemStack Get(int slot)` - `public virtual int RemoveItem(ItemStack stack)` - `public virtual void Set(int slot, ItemStack stack)` ## IInventory (interface, inventories) ```csharp public interface IInventory ``` A list of item slots: a player's inventory, a chest, a plugin menu. Every read returns a copy of the stack in the slot and every write replaces the stack: no object aliases the engine's storage, so a stack can be kept and a slot can change under it without harm. Slots count from 0. Tick thread only. - `IEnumerable Contents { get; }`: Every slot in order (a snapshot). - `int Size { get; }`: The number of slots. - `int AddItem(ItemStack stack)`: Adds a stack, merging into existing stacks first and then filling free slots. - `void Clear()`: Empties every slot. - `ItemStack Get(int slot)`: Reads a slot. - `int RemoveItem(ItemStack stack)`: Removes items of the stack's type (and components) from the slots. - `void Set(int slot, ItemStack stack)`: Replaces a slot. ## ItemStack (struct, inventories) ```csharp public readonly struct ItemStack : IEquatable ``` An immutable item stack value: an item type, a count and data components. Every `With...` method returns a new stack; the value can be kept, compared and put into inventories, menus and the world (`IInventory.Set`, {{cref:M:Mc.Scripting.Inventories.Menu.Slot(System.Int32,Mc.Scripting.Inventories.ItemStack,System.Action{Mc.Scripting.Inventories.MenuClick})}}, `World.DropItem`). Components are addressed by their registry key (`"minecraft:custom_name"`) and carry NBT values. The ones the server converts are given in a plugin friendly form: text components (`custom_name`, `item_name`, `lore` lines) as JSON strings (what `Component.ToJson` makes), integers (`damage`, `max_damage`, `max_stack_size`, `repair_cost`, `map_id`, `enchantable`, `dyed_color`), `rarity` as `"common"` .. `"epic"`, `enchantment_glint_override` as a byte, `unbreakable` / `glider` as an empty compound, `custom_data` as a compound, `item_model` and `tooltip_style` as identifier strings, `custom_model_data` as `{floats, flags, strings, colors}`, `enchantments` and `stored_enchantments` as `{"minecraft:sharpness": 3}`, `tooltip_display` as `{hide_tooltip, hidden_components}`, `use_cooldown` as `{seconds, group?}` and `food` as `{nutrition, saturation, can_always_eat}`. Every other component the server knows is read as an opaque byte array holding its network encoding: it survives copying a stack but cannot be edited. A component key starting with `!` removes a component of the item's prototype (`ItemStack.WithoutDefault`). A stack that no inventory accepts (unknown item, a count above the stack size, a component whose value does not convert) is refused by the call that receives it with an `ArgumentException`. - `public ItemStack(string type, int count = 1)`: Creates a stack. - `public IEnumerable ComponentKeys { get; }`: The keys of the components added to the stack (and `!key` entries for removed defaults). - `public int Count { get; }`: The stack size (0 for an empty stack). - `public NbtCompound? CustomData { get; }`: The plugin data of the stack (`minecraft:custom_data`), or null. - `public string? CustomId { get; }`: The id of the custom item this stack is (`custom_data.mc.id`, written by the content layer), or null for an ordinary stack. - `public bool IsEmpty { get; }`: Whether the stack holds nothing. - `public int MaxStackSize { get; }`: The largest size a stack like this can have (the item's own, or its `max_stack_size` component). Asks the server. - `public string Type { get; }`: The item key (`"minecraft:air"` for an empty stack). - `public static ItemStack Empty { get; }`: No item. - `public bool Equals(ItemStack other)` - `public override bool Equals(object? obj)` - `public NbtTag? GetComponent(string key)`: Reads a data component. - `public override int GetHashCode()` - `public bool HasComponent(string key)`: Whether the stack carries a component that was added to it. - `public NbtCompound ToNbt()`: The stack in its NBT form (`{id:"minecraft:stick",count:2,components:{...}}`), the form the server exchanges. - `public override string ToString()` - `public ItemStack WithComponent(string key, NbtTag value)`: The same stack with a data component set. - `public ItemStack WithCount(int count)`: The same stack with another size. - `public ItemStack WithCustomData(NbtCompound data)`: The same stack with plugin data (`minecraft:custom_data`) replaced. - `public ItemStack WithCustomId(string customId)`: The same stack marked as a custom item (`custom_data.mc.id`); other plugin data is kept. - `public ItemStack WithCustomModelData(float value)`: The same stack with one float of `minecraft:custom_model_data` (what `range_dispatch` model definitions of a resource pack read). - `public ItemStack WithDamage(int damage)`: The same stack with durability damage (`minecraft:damage`). - `public ItemStack WithEnchantment(string enchantment, int level)`: The same stack with an enchantment level (`minecraft:enchantments`). - `public ItemStack WithItemModel(string modelId)`: The same stack with another model (`minecraft:item_model`): the id of an item definition of a resource pack (`assets//items/.json`). - `public ItemStack WithItemName(Component name)`: The same stack with an item name (`minecraft:item_name`: the base name of the item, not italic, replaced by a rename in an anvil). - `public ItemStack WithLore(params Component[] lines)`: The same stack with lore lines (`minecraft:lore`). - `public ItemStack WithMaxStackSize(int max)`: The same stack with another maximum stack size (`minecraft:max_stack_size`, 1 to 99). - `public ItemStack WithName(Component name)`: The same stack with a custom name (`minecraft:custom_name`: shown in italics like a renamed item). - `public ItemStack WithUnbreakable()`: The same stack that cannot take durability damage (`minecraft:unbreakable`). - `public ItemStack WithoutComponent(string key)`: The same stack without a component that was added to it (the item's own default of it, if it has one, stays). - `public ItemStack WithoutDefault(string key)`: The same stack without the default value of a component of its item (`minecraft:max_damage` of a tool, for example). - `public static ItemStack FromNbt(NbtCompound nbt)`: Reads a stack from its NBT form. - `public static bool IsSimilar(ItemStack a, ItemStack b)`: Whether two stacks are the same item with the same components, whatever their sizes (the test of merging stacks). - `public static bool operator !=(ItemStack a, ItemStack b)`: Inequality. - `public static bool operator ==(ItemStack a, ItemStack b)`: Equality. ## ItemView (struct, inventories) ```csharp public readonly struct ItemView ``` A read-only view of an item stack inside an event: the item and the count. Blittable. Valid only during the dispatch it came from. `ItemView.ToStack` turns it into a full `ItemStack` (components, names, lore) that can be kept. - `public int Count { get; }`: Stack size (0 when empty). - `public string? CustomId { get; }`: The id of the custom item this is (`ItemStack.CustomId`), or null. Costs nothing for a stack without components. - `public bool HasComponents { get; }`: Whether the engine has the data components of this stack (false for plain stacks). - `public bool IsEmpty { get; }`: Whether the stack is empty (no item, air or count 0). - `public int ItemId { get; }`: Item registry id (0 for air / nothing). - `public string Key { get; }`: The item key, e.g. "minecraft:diamond_sword" (empty for an empty stack). - `public ItemStack ToStack()`: A copy of the stack with all its data components (names, lore, custom data ...) that stays valid after the event. A view of a stack without components needs no call to the engine. - `public override string ToString()` ## Menu (class, inventories) ```csharp public sealed class Menu : IDisposable, IInventory, IPluginResource ``` A custom GUI: a container with icons and click handlers that the client shows like a chest, a hopper or a dispenser. The menu is owned by the plugin and closed for every viewer when the plugin unloads or reloads. **A menu is locked.** The engine keeps it a display: a click that could take an icon out or put an item in is denied (and the whole menu resynced to the client) unless a handler allows it with `MenuClick.Allow`; drags over its slots, shift clicks, double clicks, number keys, the off hand key and the drop key cannot move items out of it or into it; the creative clone click gives no copy of an icon. Whatever a handler decides, the server is authoritative: it only performs what vanilla would perform for the click, and a cancelled click changes nothing. Create menus with `Menus.Chest` (`plugin.Menus`) or the static factories, which belong to the plugin whose code is running (OnEnable, an event handler, a command, a scheduled task); items in the slots are ordinary stacks, so the menu doubles as a small shared inventory (`Menu.Get`, `Menu.Set`). - `public IEnumerable Contents { get; }` - `public bool IsDisposed { get; }`: Whether the menu was disposed (or its plugin unloaded). - `public bool LockPlayerInventory { get; set; }`: Whether every click in the menu is denied unless allowed, also the ones that only touch the player's own inventory (false by default: the player can rearrange his own slots). - `public int Rows { get; }`: The number of rows (1 for a hopper, 3 for a dispenser). - `public int Size { get; }`: The number of slots of the menu itself (the player's own slots are not counted). - `public Component Title { get; set; }`: The title shown above the slots; changing it re-opens the screen of the viewers with the same contents. - `public MenuType Type { get; }`: The vanilla menu type. - `public IReadOnlySet Viewers { get; }`: The players who have the menu open (a snapshot). - `public int AddItem(ItemStack stack)` - `public void Clear()` - `public int Close()`: Closes the menu for every viewer. - `public bool Close(Player player)`: Closes the menu for one viewer (his cursor stack goes back to him). - `public void Dispose()`: Closes the menu for everybody and frees it (what unloading the plugin does). - `public Menu FillEmpty(ItemStack icon)`: Sets the icon of every empty slot (a border or a background). - `public ItemStack Get(int slot)` - `public ItemStack Icon(int slot)`: The icon the plugin set for a slot with {{cref:M:Mc.Scripting.Inventories.Menu.Slot(System.Int32,Mc.Scripting.Inventories.ItemStack,System.Action{Mc.Scripting.Inventories.MenuClick})}} (the stack in the slot now is `Menu.Get`). - `public bool IsOpenFor(Player player)`: Whether a player has this menu open. - `public Menu OnClick(Action handler)`: Called for every click in the menu (the menu's own slots and the player's), before the handler of the slot. - `public Menu OnClose(Action handler)`: Sets what happens when a player closes the menu (or it is closed for him). - `public Menu OnClose(Action handler)`: Sets what happens when a player closes the menu, with the reason. - `public bool Open(Player player)`: Opens the menu for a player, closing whatever container he has open. `InventoryOpenEvent` (kind `Custom`) fires first and can cancel it. - `public void Refresh()`: Sends the changed slots to the viewers now. (They are synced every tick anyway; this is for code that needs the packets within the current tick.) - `public int RemoveItem(ItemStack stack)` - `public void Set(int slot, ItemStack stack)` - `public Menu Slot(int slot, ItemStack icon, Action? onClick = null)`: Sets the icon and the click handler of a slot. - `public static Menu Chest(int rows, Component title)`: Creates a menu of 9-wide rows for the plugin whose code is running. - `public static Menu Dispenser(Component title)`: Creates a dispenser-style menu (3 x 3 slots) for the plugin whose code is running. - `public static Menu Hopper(Component title)`: Creates a hopper menu (5 slots) for the plugin whose code is running. ## MenuClick (class, inventories) ```csharp public sealed class MenuClick ``` A click in a `MenuClick.Menu`. A menu is locked: a click that could move items out of the menu or into it (a slot of the menu, shift click, double click, number key, the off hand key, the drop key, a middle click) is denied and the client is resynced unless the handler calls `MenuClick.Allow`; `MenuClick.Cancel` always denies. Clicks that only touch the player's own inventory slots pass unless a handler cancels them (or the menu locks the player inventory too). The object is valid during the handler only. - `public bool Allowed { get; }`: Whether the click will be processed (set by `MenuClick.Allow`; false by default for a click on the menu's own slots). - `public int Button { get; }`: The raw button of the click packet. - `public bool Cancelled { get; set; }`: Whether the click is cancelled for good (`MenuClick.Cancel` or setting this). - `public ItemStack Clicked { get; }`: The stack in the clicked slot (before the click). - `public ItemStack Cursor { get; }`: The stack the player holds on the cursor (before the click). - `public bool IsTopInventory { get; }`: Whether the click is on one of the menu's own slots. - `public Menu Menu { get; }`: The menu. - `public Player Player { get; }`: The player who clicked. - `public int PlayerSlot { get; }`: For a click on the player's own slots: the slot in `PlayerInventory` numbering; otherwise -1. - `public int Slot { get; }`: The clicked slot in the menu's numbering: 0 to `Menu.Size` - 1 are the menu's own slots, then the player's 27 main and 9 hotbar slots; -999 outside the window. - `public MenuClickType Type { get; }`: How the player clicked. - `public void Allow()`: Lets the click change items like it would in a chest (take the stack out of the menu, put one in, shift click ...). The server only does what vanilla does. - `public void Cancel()`: Denies the click: nothing changes and the client is resynced (also for clicks that would pass by default). - `public void Close()`: Closes the menu for the player after the click. - `public void SetClicked(ItemStack stack)`: Replaces the stack in the clicked slot (a slot of the menu or one of the player's). - `public void SetCursor(ItemStack stack)`: Replaces the stack on the player's cursor (the client is resynced after the click). ## MenuClickType (enum, inventories) ```csharp public enum MenuClickType : int ``` The kind of a click in a menu. - `Left` = 0: A plain left click. - `Right` = 1: A plain right click. - `ShiftClick` = 2: Shift plus click. - `NumberKey` = 3: A number key swap. - `DoubleClick` = 4: A double click. - `Drop` = 5: A drop key. - `Drag` = 6: The end of a drag over the player's own slots (`MenuClick.Slot` is the first dragged slot; a drag over one of the menu's own slots is denied by the engine before any handler sees it). - `Middle` = 7: A middle click (the creative clone click). - `SwapOffhand` = 8: The off hand key over a slot. - `ControlDrop` = 9: Control plus the drop key (the whole stack). - `Outside` = 10: A click outside the window (drops the cursor stack). - `Unknown` = 11: Anything else. ## MenuType (enum, inventories) ```csharp public enum MenuType : int ``` The vanilla menu type a `Menu` uses (what the client draws). - `Chest` = 0: A chest: 1 to 6 rows of 9 slots. - `Hopper` = 1: A hopper: 5 slots in a row. - `Dispenser` = 2: A dispenser or dropper: 3 x 3 slots. ## Menus (class, inventories) ```csharp public sealed class Menus ``` Creates menus owned by a plugin (`plugin.Menus`). - `public Menu Chest(int rows, Component title)`: Creates a chest-style menu of 9-wide rows. - `public Menu Create(MenuType type, Component title, int rows = 3)`: Creates a menu. - `public Menu Dispenser(Component title)`: Creates a dispenser-style menu (3 x 3 slots). - `public Menu Hopper(Component title)`: Creates a hopper menu (5 slots). ## PlayerInventory (class, inventories) ```csharp public sealed class PlayerInventory : EngineInventory ``` The inventory of a player: 41 slots numbered like the game's own inventory container, plus the cursor. Slots 0-8 are the hotbar, 9-35 the main inventory, 36 to 39 the armor (boots, leggings, chestplate, helmet) and 40 the off hand. A stale player (one who left) has no slots: reads are empty and writes are refused. - `public const int BootsSlot = 36;`: The boots slot. - `public const int ChestplateSlot = 38;`: The chestplate slot. - `public const int FirstMainSlot = 9;`: First slot of the main inventory (below the hotbar). - `public const int HelmetSlot = 39;`: The helmet slot. - `public const int LeggingsSlot = 37;`: The leggings slot. - `public const int OffHandSlot = 40;`: The off hand slot. - `public const int SlotCount = 41;`: Number of slots. - `public ItemStack Boots { get; set; }`: The boots. - `public ItemStack Chestplate { get; set; }`: The chestplate. - `public ItemStack Cursor { get; set; }`: The stack the player carries on the cursor while a container or his inventory screen is open. Setting it resyncs the client. - `public ItemStack Helmet { get; set; }`: The helmet. - `public ItemStack Leggings { get; set; }`: The leggings. - `public ItemStack MainHand { get; set; }`: The stack in the main hand (the selected hotbar slot). - `public ItemStack OffHand { get; set; }`: The stack in the off hand. - `public Player Player { get; }`: The player. - `public int SelectedSlot { get; set; }`: The selected hotbar slot (0-8); setting it tells the client. - `public override int Size { get; }` - `public int Give(ItemStack stack)`: Gives a stack to the player: what does not fit into the inventory is dropped at his feet (like picking up with a full inventory). - `public ItemStack Hotbar(int index)`: The stack in a hotbar slot. - `public static PlayerInventory Of(Player player)`: The inventory of a player. ## InventoryKind (enum, inventories) ```csharp public enum InventoryKind : byte ``` The kind of container a player has open. - `Chest` = 0: Chest / ender chest / generic rows menu. - `Dispenser` = 1: Dispenser or dropper (3x3). - `Furnace` = 2: Furnace. - `BlastFurnace` = 3: Blast furnace. - `Smoker` = 4: Smoker. - `Workbench` = 5: Crafting table. - `Enchanting` = 6: Enchanting table. - `Brewing` = 7: Brewing stand. - `Hopper` = 8: Hopper. - `Anvil` = 9: Anvil. - `Grindstone` = 10: Grindstone. - `Stonecutter` = 11: Stonecutter. - `Smithing` = 12: Smithing table. - `Loom` = 13: Loom. - `Cartography` = 14: Cartography table. - `Beacon` = 15: Beacon. - `ShulkerBox` = 16: Shulker box. - `Barrel` = 17: Barrel. - `Lectern` = 18: Lectern. - `Merchant` = 19: Villager / wandering trader trades. - `PlayerInventory` = 20: The player's own inventory. - `Custom` = 21: A plugin-created menu. - `Unknown` = 22: Anything else. ## Location (struct, primitives) ```csharp public readonly struct Location : IEquatable ``` A position with a rotation in a world. - `public Location(World World, Vec3d Position, float Yaw = 0f, float Pitch = 0f)`: A position with a rotation in a world. - `public Location(World world, double x, double y, double z, float yaw = 0f, float pitch = 0f)`: Creates a location from coordinates. - `public BlockPos BlockPos { get; }`: The block containing the position. - `public float Pitch { get; init; }`: Pitch in degrees (-90 = up, 90 = down). - `public Vec3d Position { get; init; }`: Position in blocks. - `public World World { get; init; }`: The dimension. - `public double X { get; }`: X coordinate. - `public double Y { get; }`: Y coordinate. - `public float Yaw { get; init; }`: Yaw in degrees (0 = south, 90 = west). - `public double Z { get; }`: Z coordinate. ## Log (class, server) ```csharp public static class Log ``` Global logger (prefix "[scripting]"). Plugins should prefer their own `Logger`. - `public static void Error(string message)`: Writes an error. - `public static void Error(string message, Exception e)`: Writes an error with the exception. - `public static void Info(string message)`: Writes an info message with the "[scripting]" prefix. - `public static void Warn(string message)`: Writes a warning. ## LogLevel (enum, server) ```csharp public enum LogLevel : int ``` Log severity. - `Trace` = 0: Very detailed diagnostics. - `Debug` = 1: Debug output. - `Info` = 2: Normal operation. - `Warn` = 3: Something unexpected but harmless. - `Error` = 4: An error. ## Logger (class, server) ```csharp public sealed class Logger ``` Writes to the server log (native logger), prefixed with the plugin name. - `public Logger(string name)`: Creates a logger that prefixes `[name]`. - `public void Debug(string message)`: Writes a debug message. - `public void Error(string message)`: Writes an error. - `public void Error(string message, Exception exception)`: Writes an error with the exception and its stack trace. - `public void Info(string message)`: Writes an info message. - `public void Trace(string message)`: Writes a trace message. - `public void Warn(string message)`: Writes a warning. - `public void Write(LogLevel level, string message)`: Writes a message at `level`. ## McPermissionAttribute (class, permissions) ```csharp public sealed class McPermissionAttribute : Attribute ``` Declares a permission node of a source plugin (the `permissions` array of `plugin.json`): `[assembly: McPermission("homes.use", Default = "true", Description = "Use /home")]`. Like `McPluginAttribute` it is read from the syntax before compiling, so the arguments must be string literals. - `public McPermissionAttribute(string node)`: Declares a permission node of a source plugin (the `permissions` array of `plugin.json`): `[assembly: McPermission("homes.use", Default = "true", Description = "Use /home")]`. Like `McPluginAttribute` it is read from the syntax before compiling, so the arguments must be string literals. - `public string? Default { get; init; }`: Who has it by default: `true`, `false`, `op`, `op:N` or `not_op` (default `op`). - `public string? Description { get; init; }`: What the permission allows. - `public string Node { get; }`: The permission node. ## McPluginAttribute (class, plugins) ```csharp public sealed class McPluginAttribute : Attribute ``` The manifest of a **source plugin** (a `.cs` file or folder the server compiles at startup), written as an assembly attribute at the top of one of its files: `[assembly: McPlugin("homes", version: "1.2.0", depends: ["economy >=1.0"])]`. It carries the same information as `plugin.json` (a `plugin.json` next to the sources takes precedence). The server reads it from the syntax of the file **before** compiling, to compute the load order, so every argument must be a literal: string literals, `null`, and arrays or collection expressions of string literals (no constants, no `nameof`, no interpolation). The attribute is ignored on a compiled plugin (it has a `plugin.json` or `PluginInfoAttribute`). - `public McPluginAttribute(string id, string? version = null, string? name = null, string[]? depends = null, string[]? softDepends = null, string[]? loadBefore = null, string? description = null, string[]? authors = null, string? website = null, string? api = null, string? main = null)`: Declares the plugin. - `public string? Api { get; }`: The API version. - `public string[]? Authors { get; }`: The authors. - `public string[]? Depends { get; }`: The hard dependencies. - `public string? Description { get; }`: The description. - `public string Id { get; }`: The plugin id. - `public string[]? LoadBefore { get; }`: The plugins that load after this one. - `public string? Main { get; }`: The plugin class. - `public string? Name { get; }`: The display name. - `public string[]? SoftDepends { get; }`: The soft dependencies. - `public string? Version { get; }`: The plugin version. - `public string? Website { get; }`: The website. ## MenuCloseReason (enum, inventories) ```csharp public enum MenuCloseReason : byte ``` Why a plugin menu was closed for a player. - `Player` = 0: The player closed it (Escape, the inventory key) or the client sent a close. - `Plugin` = 1: A plugin called Menu.Close. - `Unload` = 2: The plugin that owns the menu unloaded or reloaded. - `Quit` = 3: The player left the server. - `Replaced` = 4: Another container was opened for the player. - `Invalid` = 5: The server closed it (death, dimension change, the menu no longer valid). ## NametagCollision (enum, display) ```csharp public enum NametagCollision : int ``` Who a player collides with (bumps into). - `Always` = 0: Collides with everybody. - `Never` = 1: Collides with nobody. - `PushOtherTeams` = 2: Pushes the players of other teams only. - `PushOwnTeam` = 3: Pushes the players of its own team only. ## NametagStyle (class, display) ```csharp public sealed class NametagStyle ``` The look of a nametag for one viewer (see `Nametags.Set`). - `public NametagStyle()` - `public NametagCollision Collision { get; init; }`: Who the player collides with. - `public int? Colour { get; init; }`: The team colour of the name, 0 to 15 (the colour codes), or null for none. - `public bool FriendlyFire { get; init; }`: Whether the players of the team hit each other. - `public Component? Prefix { get; init; }`: The text before the name, or null for none. - `public bool SeeInvisibles { get; init; }`: Whether the players of the team see the invisible players of the team. - `public Component? Suffix { get; init; }`: The text after the name, or null for none. The prefix and the suffix together are at most 8000 bytes. - `public NametagVisibility Visibility { get; init; }`: Who sees the nametag. ## NametagVisibility (enum, display) ```csharp public enum NametagVisibility : int ``` Who sees a nametag. - `Always` = 0: Everybody sees it. - `Never` = 1: Nobody sees it. - `HideForOtherTeams` = 2: Only the players of the same team see it. - `HideForOwnTeam` = 3: Only the players of other teams see it. ## Nametags (class, display) ```csharp public sealed class Nametags ``` Nametag styles per viewer: a prefix and a suffix around a player's name, a team colour, and the rules for seeing and bumping the team's players. Each style is a team of the client; the players with the same style share it. Reach it as `plugin.Nametags`. Call from the tick thread. What a plugin sets is removed when the plugin unloads. - `public void Clear(Player viewer, Player target)`: Puts one player's nametag back to the server's own for one viewer. - `public void Set(Player viewer, Player target, NametagStyle style)`: Gives one player a nametag style as one viewer sees him. A player has one style per viewer; a new style replaces the old one. ## NativeBytes (struct, primitives) ```csharp public readonly ref struct NativeBytes ``` Borrowed byte buffer (McBytes). Only valid during the dispatch that provided it. - `public NativeBytes(byte* data, long size)`: Wraps native memory. - `public int Length { get; }`: Length in bytes. - `public ReadOnlySpan Span { get; }`: The bytes (borrowed). - `public byte[] ToArray()`: Copies the bytes. ## NbtByte (class, nbt) ```csharp public sealed class NbtByte : NbtValueTag ``` Signed 8 bit integer (also the game's boolean). - `public NbtByte(sbyte value)`: Creates the tag. - `public override NbtType Type { get; }` - `public sbyte Value { get; set; }`: The value. - `public override NbtTag Clone()` ## NbtByteArray (class, nbt) ```csharp public sealed class NbtByteArray : NbtTag ``` An array of bytes. - `public NbtByteArray(sbyte[] value)`: Creates the tag. - `public override NbtType Type { get; }` - `public sbyte[] Value { get; set; }`: The values (not copied). - `public override NbtTag Clone()` ## NbtCompound (class, nbt) ```csharp public sealed class NbtCompound : NbtTag, IEnumerable, IEnumerable> ``` A compound: named tags (keys are unique; the enumeration order is the insertion order). - `public NbtCompound()` - `public int Count { get; }`: Number of entries. - `public IEnumerable Keys { get; }`: The keys. - `public override NbtType Type { get; }` - `public NbtTag this[string key] { get; set; }`: Gets or sets an entry. - `public void Clear()`: Removes every entry. - `public override NbtTag Clone()` - `public bool Contains(string key)`: Whether a key exists. - `public T? Get(string key) where T : NbtTag`: Gets an entry of a given type. - `public bool GetBool(string key, bool fallback = false)`: Reads a boolean (a non-zero numeric entry). - `public NbtCompound? GetCompound(string key)`: Reads a compound entry. - `public double GetDouble(string key, double fallback = 0)`: Reads a numeric entry as a double. - `public IEnumerator> GetEnumerator()` - `public int GetInt(string key, int fallback = 0)`: Reads a numeric entry as an int. - `public NbtList? GetList(string key)`: Reads a list entry. - `public long GetLong(string key, long fallback = 0)`: Reads a numeric entry as a long (any integer or float type; floats are truncated). - `public string GetString(string key, string fallback = "")`: Reads a string entry. - `public NbtCompound Merge(NbtCompound other)`: Deep-merges another compound into this one: nested compounds merge, everything else is copied (like `/data merge`). - `public NbtCompound Put(string key, NbtTag tag)`: Adds or replaces an entry. - `public NbtCompound PutBool(string key, bool value)`: Sets a byte entry (the game's boolean). - `public NbtCompound PutByte(string key, sbyte value)`: Sets a byte entry. - `public NbtCompound PutDouble(string key, double value)`: Sets a double entry. - `public NbtCompound PutFloat(string key, float value)`: Sets a float entry. - `public NbtCompound PutInt(string key, int value)`: Sets an int entry. - `public NbtCompound PutLong(string key, long value)`: Sets a long entry. - `public NbtCompound PutShort(string key, short value)`: Sets a short entry. - `public NbtCompound PutString(string key, string value)`: Sets a string entry. - `public bool Remove(string key)`: Removes an entry. - `public bool TryGet(string key, out NbtTag tag)`: Gets an entry. ## NbtDouble (class, nbt) ```csharp public sealed class NbtDouble : NbtValueTag ``` 64 bit float. - `public NbtDouble(double value)`: Creates the tag. - `public override NbtType Type { get; }` - `public double Value { get; set; }`: The value. - `public override NbtTag Clone()` ## NbtFloat (class, nbt) ```csharp public sealed class NbtFloat : NbtValueTag ``` 32 bit float. - `public NbtFloat(float value)`: Creates the tag. - `public override NbtType Type { get; }` - `public float Value { get; set; }`: The value. - `public override NbtTag Clone()` ## NbtInt (class, nbt) ```csharp public sealed class NbtInt : NbtValueTag ``` Signed 32 bit integer. - `public NbtInt(int value)`: Creates the tag. - `public override NbtType Type { get; }` - `public int Value { get; set; }`: The value. - `public override NbtTag Clone()` ## NbtIntArray (class, nbt) ```csharp public sealed class NbtIntArray : NbtTag ``` An array of 32 bit integers. - `public NbtIntArray(int[] value)`: Creates the tag. - `public override NbtType Type { get; }` - `public int[] Value { get; set; }`: The values (not copied). - `public override NbtTag Clone()` ## NbtIo (class, nbt) ```csharp public static class NbtIo ``` Binary NBT (the file form of the game: type byte, name, payload; big endian; strings in Java's modified UTF-8). Reading enforces a nesting depth of 512 like the game. - `public const long DefaultQuota = 33554432;`: How much memory (in the accounting of the game's NbtAccounter: about 48 bytes per tag, plus strings and arrays) a read may build: 32 MiB. A few bytes of input can describe a very large tree (a list of a million empty compounds is one megabyte of data and a hundred megabytes of objects); the quota stops it. - `public static NbtTag Read(ReadOnlySpan data)`: Parses a named root tag (the name is ignored). - `public static NbtTag Read(ReadOnlySpan data, long quota)`: Parses a named root tag with an explicit memory quota. - `public static byte[] Write(NbtTag tag)`: Serialises a tag as a named root with an empty name. ## NbtList (class, nbt) ```csharp public sealed class NbtList : NbtTag, ICollection, IEnumerable, IEnumerable, IList ``` A list of tags of one type. - `public NbtList()`: Creates an empty list. - `public NbtList(IEnumerable items)`: Creates a list with items. - `public int Count { get; }` - `public NbtType ElementType { get; }`: The type of the items (End while the list is empty). - `public bool IsReadOnly { get; }` - `public override NbtType Type { get; }` - `public NbtTag this[int index] { get; set; }` - `public void Add(NbtTag item)` - `public void Clear()` - `public override NbtTag Clone()` - `public bool Contains(NbtTag item)` - `public void CopyTo(NbtTag[] array, int arrayIndex)` - `public IEnumerator GetEnumerator()` - `public int IndexOf(NbtTag item)` - `public void Insert(int index, NbtTag item)` - `public bool Remove(NbtTag item)` - `public void RemoveAt(int index)` ## NbtLong (class, nbt) ```csharp public sealed class NbtLong : NbtValueTag ``` Signed 64 bit integer. - `public NbtLong(long value)`: Creates the tag. - `public override NbtType Type { get; }` - `public long Value { get; set; }`: The value. - `public override NbtTag Clone()` ## NbtLongArray (class, nbt) ```csharp public sealed class NbtLongArray : NbtTag ``` An array of 64 bit integers. - `public NbtLongArray(long[] value)`: Creates the tag. - `public override NbtType Type { get; }` - `public long[] Value { get; set; }`: The values (not copied). - `public override NbtTag Clone()` ## NbtShort (class, nbt) ```csharp public sealed class NbtShort : NbtValueTag ``` Signed 16 bit integer. - `public NbtShort(short value)`: Creates the tag. - `public override NbtType Type { get; }` - `public short Value { get; set; }`: The value. - `public override NbtTag Clone()` ## NbtString (class, nbt) ```csharp public sealed class NbtString : NbtValueTag ``` A string. - `public NbtString(string value)`: Creates the tag. - `public override NbtType Type { get; }` - `public string Value { get; set; }`: The value. - `public override NbtTag Clone()` ## NbtTag (class, nbt) ```csharp public abstract class NbtTag ``` An NBT value (the data format of block entities, entities, item components and level data). Tags are mutable managed objects: they are copies of the engine's data, so changing one changes nothing in the world until it is handed back (for example to `World.SetBlockEntityData`). Use `NbtTag.Clone` for a deep copy. - `protected NbtTag()` - `public abstract NbtType Type { get; }`: The type of this tag. - `public abstract NbtTag Clone()`: A deep copy. - `public byte[] ToBytes()`: Serialises the tag to the binary file form (named root with an empty name, big endian). - `public override string ToString()`: The tag as stringified NBT (what `/data get` prints), compact, compound keys sorted. - `public static bool DeepEquals(NbtTag? a, NbtTag? b)`: Whether two tags have the same type and contents (compounds compare by keys, not by order). - `public static NbtTag FromBytes(ReadOnlySpan data)`: Parses the binary file form. - `public static NbtTag Parse(string snbt)`: Parses stringified NBT (`{Health:20.0f,Tags:["a"]}`), with the grammar of the game (typed suffixes, quoted strings, `[B;1,2]` arrays ...). - `public static NbtCompound ParseCompound(string snbt)`: Parses stringified NBT that must be a compound. - `public static implicit operator NbtTag(sbyte v)`: An 8 bit integer tag. - `public static implicit operator NbtTag(short v)`: A 16 bit integer tag. - `public static implicit operator NbtTag(int v)`: A 32 bit integer tag. - `public static implicit operator NbtTag(long v)`: A 64 bit integer tag. - `public static implicit operator NbtTag(float v)`: A float tag. - `public static implicit operator NbtTag(double v)`: A double tag. - `public static implicit operator NbtTag(string v)`: A string tag. - `public static implicit operator NbtTag(bool v)`: A byte tag holding 1 or 0 (the game stores booleans as bytes). ## NbtType (enum, nbt) ```csharp public enum NbtType : byte ``` The type of an NBT tag (the id byte of the binary format). - `End` = 0: End marker (never a value). - `Byte` = 1: Signed 8 bit integer. - `Short` = 2: Signed 16 bit integer. - `Int` = 3: Signed 32 bit integer. - `Long` = 4: Signed 64 bit integer. - `Float` = 5: 32 bit float. - `Double` = 6: 64 bit float. - `ByteArray` = 7: Array of bytes. - `String` = 8: UTF-8 string. - `List` = 9: List of tags of one type. - `Compound` = 10: Named tags. - `IntArray` = 11: Array of 32 bit integers. - `LongArray` = 12: Array of 64 bit integers. ## NbtValueTag (class, nbt) ```csharp public abstract class NbtValueTag : NbtTag ``` A tag with a single numeric or text value. - `protected NbtValueTag()` ## Snbt (class, nbt) ```csharp public static class Snbt ``` Stringified NBT (the text form of `/data` and `/summon`), through the game's own parser and printer. - `public static string Format(NbtTag tag)`: Formats a tag as SNBT. - `public static NbtTag Parse(string text)`: Parses SNBT. ## Npc (class, visuals) ```csharp public sealed class Npc : IDisposable, IPluginResource ``` A fake player that stands in the world: the players see it with a name, a skin, equipment and an arm that swings, and a click on it fires {{cref:M:Mc.Scripting.Npcs.OnInteract(System.Action{Mc.Scripting.NpcInteraction})}}. Nothing of it exists on the server (it has no physics and no AI). Create it with `Npcs.Create`; it disappears for every viewer when the plugin unloads or when `Npc.Remove` is called. - `public ulong Handle { get; }`: The handle the engine gives a click on this NPC (see `NpcInteraction.Handle`). - `public bool IsActive { get; }`: Whether the NPC still exists. - `public string Name { get; }`: The profile name above its head. - `public float Pitch { get; }`: The vertical look, degrees. - `public Vec3d Position { get; }`: Where it stands. - `public float Yaw { get; }`: The horizontal look, degrees. - `public void Dispose()` - `public Npc HideFrom(Player player)`: Hides the NPC from one player (while it is shown to everybody). - `public Npc HideFromAll()`: Hides the NPC from everybody. - `public Npc Look(float yaw, float pitch)`: Turns the NPC where it stands. - `public Npc Move(Vec3d position, float yaw, float pitch)`: Moves the NPC (within its world) and turns it. - `public void Remove()`: Removes the NPC from every viewer (and from the tab list) and deletes it. - `public Npc SetEquipment(NpcSlot slot, ItemStack item)`: Sets what a slot of the NPC holds or wears (an empty stack clears it). - `public Npc SetPose(NpcPose pose)`: Changes the pose (standing or sneaking). - `public Npc ShowTo(Player player)`: Shows the NPC to one player (after `Npc.HideFromAll`), or hides it from one player while it is shown to everybody. - `public Npc ShowToAll()`: Shows the NPC to everybody again. - `public Npc Swing(bool offhand = false)`: Swings an arm. ## NpcClick (enum, visuals) ```csharp public enum NpcClick : byte ``` What a player did to an NPC. - `Attack` = 0: A left click (an attack). - `Interact` = 1: A right click (an interaction). ## NpcInteraction (struct, visuals) ```csharp public readonly struct NpcInteraction ``` A click on an NPC ({{cref:M:Mc.Scripting.Npcs.OnInteract(System.Action{Mc.Scripting.NpcInteraction})}}). - `public NpcClick Action { get; }`: A left click (an attack) or a right click (an interaction). - `public Hand Hand { get; }`: The hand of the click (a left click is the main hand). - `public ulong Handle { get; }`: The handle of the NPC (the engine's; `NpcInteraction.Npc` of it is `NpcInteraction.Npc`.Handle). - `public Npc? Npc { get; }`: The NPC, if it still exists. - `public Player Player { get; }`: The player who clicked. - `public bool Sneaking { get; }`: Whether the player was sneaking. ## NpcPose (enum, visuals) ```csharp public enum NpcPose : int ``` The pose of an NPC. - `Standing` = 0: Standing. - `Crouching` = 5: Sneaking. ## NpcSkin (class, visuals) ```csharp public sealed class NpcSkin ``` The skin of an NPC: a texture (the value and signature of a "textures" profile property), or the skin of a player whose profile id is known. - `public Uuid CopyOf { get; }`: The profile id whose skin is copied (default when the texture is given). - `public string? Signature { get; }`: The texture signature, when there is one. - `public string Value { get; }`: The texture value (base64), empty when the skin is copied. - `public static NpcSkin FromTexture(string value, string? signature = null)`: A skin from a texture property (at most 8192 bytes of value and 1024 of signature). - `public static NpcSkin OfPlayer(Player player)`: The skin of a player. - `public static NpcSkin OfProfile(Uuid profile)`: The skin of a profile id (an online player's skin is copied; an offline one gives the default skin). ## NpcSlot (enum, visuals) ```csharp public enum NpcSlot : int ``` The equipment slots of an NPC. - `MainHand` = 0: The main hand. - `OffHand` = 1: The off hand. - `Feet` = 2: The boots. - `Legs` = 3: The leggings. - `Chest` = 4: The chestplate. - `Head` = 5: The helmet. ## Npcs (class, visuals) ```csharp public sealed class Npcs ``` Creates NPCs for this plugin and listens to the clicks on them. Everything made here is removed when the plugin unloads. - `public Npc Create(World world, Vec3d position, string name, NpcSkin? skin = null, bool showInTab = false, float yaw = 0f, float pitch = 0f)`: Creates an NPC that everybody sees. - `public Subscription OnInteract(Action handler)`: Calls `handler` when a player clicks an NPC of this plugin's server (any NPC: check `NpcInteraction.Npc`). ## Objective (class, display) ```csharp public sealed class Objective ``` An objective of the server scoreboard (the one `/scoreboard` edits and the world saves): scores per holder name, shown in a display slot. For content that differs per player use `Sidebar` instead. - `public string Name { get; }`: The objective's name (what commands use). - `public int AddScore(string holder, int amount)`: Adds to the score of a holder (a missing score counts as 0). - `public int? GetScore(string holder)`: The score of a holder (a player name, or any text for a fake entry), null when none is set. - `public bool ResetScore(string holder)`: Removes the score of a holder. - `public void SetScore(string holder, int value)`: Sets the score of a holder. - `public void SetTitle(Component title)`: Sets the title shown above the sidebar. ## OfflinePlayers (class, players) ```csharp public static class OfflinePlayers ``` Players who are not online, looked up by name or uuid: the server's name cache (usercache.json, vanilla's), the first join and last seen times, and the saved player data. Reads only; a plugin that changes an offline player's data does it through the owner's own store. Thread: tick thread (the reads go to the engine). - `public static Uuid? Find(string name)`: The uuid of a player name (case-insensitive) that the server remembers; null when it does not know the name. - `public static OfflineProfile? Get(Uuid uuid)`: The profile of a uuid the server knows, online or not (online players have their newer state in the player's own handle). ## OfflineProfile (class, players) ```csharp public sealed class OfflineProfile : IEquatable ``` What the server knows about a player who is not online: the name, the first join and the last time seen (both from the server's records; null when unknown), and the position in the saved player data (null when there is no saved data). - `public OfflineProfile(Uuid Id, string Name, DateTimeOffset? FirstJoin, DateTimeOffset? LastSeen, string? Dimension, Vec3d? Position, float Yaw, float Pitch)`: What the server knows about a player who is not online: the name, the first join and the last time seen (both from the server's records; null when unknown), and the position in the saved player data (null when there is no saved data). - `public string? Dimension { get; init; }`: The dimension key ("minecraft:overworld") the player was saved in; null without saved data. - `public DateTimeOffset? FirstJoin { get; init; }`: When the player first joined (null when unknown). - `public Uuid Id { get; init; }`: The uuid. - `public DateTimeOffset? LastSeen { get; init; }`: When the player was last saved or left (null when unknown). - `public string Name { get; init; }`: The newest name the server has for the uuid. - `public float Pitch { get; init; }`: The saved pitch; 0 without saved data. - `public Vec3d? Position { get; init; }`: The saved feet position; null without saved data. - `public float Yaw { get; init; }`: The saved yaw; 0 without saved data. ## PacketBase (class, packets) ```csharp public abstract class PacketBase ``` Base of every typed packet (the generated classes derive from it). The wire form is the packet's fields in declaration order; the engine decodes and validates them (src/scripting/ipc/packets.g.cpp). Plugins read and write the public fields. - `protected PacketBase()` - `public abstract int PacketIndex { get; }`: The packet's index in the engine's catalogue (its class has the constant Index). ## PacketEvent (class, packets) ```csharp public sealed class PacketEvent where T : PacketBase ``` One packet handed to a handler. In intercept mode the handler may change `PacketEvent.Packet` and call `PacketEvent.Cancel`. - `public bool Cancelled { get; }`: True once a handler cancelled the packet. - `public T Packet { get; }`: The packet. Changing its fields changes what goes on (the engine validates the new values like any other). - `public Player Player { get; }`: The player the packet is from (serverbound) or to (clientbound). - `public void Cancel()`: Stops the packet: a serverbound packet's handler does not run, a clientbound packet is not sent. Only in intercept mode; an observer's cancel is ignored. ## PacketMode (enum, packets) ```csharp public enum PacketMode : int ``` How a packet handler is called (see `Packets.On`,Mc.Scripting.PacketMode,Mc.Scripting.EventPriority)}}). - `Observe` = 0: A copy of the packet after the fact: the handler cannot change or cancel it, and nothing waits for it. - `Intercept` = 1: The handler runs before the packet goes on and may cancel it or change its fields. The engine waits for the answer within a small budget; when none comes in time the packet goes on unchanged. ## PacketSendStatus (enum, packets) ```csharp public enum PacketSendStatus : int ``` What `Packets.Send` did with the packet. - `Sent` = 0: The packet is in the player's stream, in order with the engine's packets of the tick. - `Refused` = 1: The packet was refused: a value out of range, an unknown registry id, an entity id this plugin may not name, or a packet the engine does not send to players. - `NotInPlay` = 2: The player is not in play (nothing was sent). - `OverBudget` = 3: The player's (or the server's) byte budget for plugin packets of this tick is spent (nothing was sent). - `NotAvailable` = 4: The engine does not carry packets here (the in-process runtime, a test fixture): nothing was sent. ## Packets (class, packets) ```csharp public static class Packets ``` Typed packets: send a packet to a player, and observe, change or cancel the packets players send and the ones sent to them. The engine encodes every packet with its own codecs, so nothing malformed reaches a client. Remote mode only (the worker process). docs/agents/scripting.md, "Packets"; the ProtocolLib / PacketEvents equivalents are listed there. - `public static int AllocateEntityId()`: A new entity id for a fake entity (an AddEntity of this plugin). The ids are reserved for plugins, so they never collide with a real entity. Release it with `Packets.ReleaseEntityId` when the client should forget it. 0 when none is available. - `public static IDisposable On(Action> handler, PacketMode mode = PacketMode.Observe, EventPriority priority = EventPriority.Normal) where T : PacketBase, new()`: Subscribes to one packet type. In `PacketMode.Observe` the handler sees a copy of each packet; in `PacketMode.Intercept` it may cancel the packet or change its fields. Handlers run in priority order (lowest first). Returns the subscription: dispose it to unsubscribe (also done when the plugin unloads). - `public static bool ReleaseEntityId(int id)`: Gives back an id from `Packets.AllocateEntityId`. False when this plugin does not hold it. - `public static PacketSendStatus Send(Player player, PacketBase packet)`: Sends a packet to a player. The packet is encoded by the engine, after the engine checks its values (ranges, registry ids, entity ids: an entity a plugin creates needs `Packets.AllocateEntityId`). It is written in order with the engine's own packets of the tick. - `public static PacketSendStatus SendPacket(Player player, PacketBase packet)`: Shorthand for `Packets.Send`: `player.SendPacket(new Packets.Clientbound.AddEntity { ... })`. ## Particle (class, players) ```csharp public static class Particle ``` Helpers for the textual particle descriptions `Player.SpawnParticle` takes (the same syntax as the `/particle` command). - `public static string Block(BlockState state)`: Block breaking particles of a block state. - `public static string Dust(float red, float green, float blue, float scale = 1f)`: A coloured dust particle. - `public static string Of(string key)`: A particle by its registry key (`minecraft:flame`, `happy_villager`, ...). ## FilePermissionProvider (class, permissions) ```csharp public sealed class FilePermissionProvider : IPermissionProvider ``` The built-in backend: groups (weighted, inheriting, with prefix and suffix) and per-player overrides, stored in `permissions.json`. Resolution order: the player's own entries, then his groups by weight (highest first, parents after their child), then the implicit "default" group everybody belongs to. Within one layer the most specific node wins: an exact entry beats `a.b.*`, a longer wildcard beats a shorter one, `*` comes last; a `false` value is a denial. A key written as `"-node"` in the file means the same as `"node": false`. - `public FilePermissionProvider(string path)`: Opens (and creates, when missing) the permission file. A file that cannot be read (malformed JSON) does not stop the server: the provider starts empty (the registered defaults apply), logs the problem once and leaves the file alone so that it can be fixed and reloaded with `FilePermissionProvider.Reload`. - `public IReadOnlyList GroupNames { get; }`: The group names. - `public string? LoadError { get; }`: Why the file could not be read (null when it was, or since it was fixed and reloaded). - `public string Name { get; }` - `public void AddToGroup(Uuid player, string group, string? name = null)`: Adds the player to a group. - `public void CreateGroup(string name, int weight = 0)`: Creates a group (no-op when it exists). - `public bool DeleteGroup(string name)`: Deletes a group and removes it from every player and parent list. - `public PermissionValue Get(Uuid player, string node)` - `public string GetPrefix(Uuid player)`: The prefix of the player's highest-weight group that has one (empty if none). - `public string GetSuffix(Uuid player)`: The suffix of the player's highest-weight group that has one (empty if none). - `public IReadOnlyDictionary GroupEntries(string group)`: The explicit entries of a group, for admin tools. - `public IReadOnlyCollection Groups(Uuid player)` - `public IReadOnlyDictionary PlayerEntries(Uuid player)`: The explicit entries of a player (without groups), for admin tools. - `public void Reload()`: Re-reads the file (a missing file is created with a "default" and an "admin" group). - `public bool RemoveFromGroup(Uuid player, string group)`: Removes the player from a group. - `public void Save()`: Writes the file if something changed. The engine writes it (a host call: the worker's sandbox refuses writes to the server folder), atomically (temp file, flushed, then renamed over the old one). The engine refuses a write that comes too soon after the last one (its rate limit): then the changes stay pending and the next call (the runtime's flush, a second later) tries again. Never throws; a refusal is logged once per streak. - `public void SetGroupMeta(string group, string prefix, string suffix)`: Sets prefix and suffix text of a group (metadata for chat plugins). - `public void SetGroupParents(string group, IEnumerable parents)`: Sets the groups a group inherits from. - `public void SetGroupPermission(string group, string node, bool? value)`: Sets (true/false) or removes (null) a permission entry of a group. - `public void SetPlayerPermission(Uuid player, string node, bool? value, string? name = null)`: Sets (true/false) or removes (null) a permission entry of a player. - `public event Action? Changed;` ## IPermissionProvider (interface, permissions) ```csharp public interface IPermissionProvider ``` A permission backend: answers "does this player have this node?" with an explicit value or no opinion. Implement it to replace the built-in file-based backend (for example with a database or a LuckPerms-style system) and install it with `PermissionManager.SetProvider`. - `string Name { get; }`: A short name for logs and admin tools. - `PermissionValue Get(Uuid player, string node)`: The explicit value of `node` for the player: wildcards (`a.b.*`, `*`) and groups are the provider's business. Return `PermissionValue.Unset` when nothing matches, so the node's registered default decides. - `IReadOnlyCollection Groups(Uuid player)`: The names of the groups the player belongs to (for display and admin commands). - `public event Action? Changed;`: Raised when permissions changed (the player's id, or null when a group or everything changed): the server refreshes the command trees. ## NativePermissionTable (class, permissions) ```csharp public static class NativePermissionTable ``` Feeds the engine's permission table (docs/agents/permissions.md): declares node defaults, sets and changes the rules of a player, claims the table, and reads its statistics. The engine validates every message as a whole; a refused message changes nothing (the status is the host call's: 0 applied, -1 malformed, -2 oversize, -3 refused, -4 version gap, -5 unknown kind). While a claim is held the engine answers the permission requirements of commands from the table; the rules describe a player's permissions, and a player whose rules were never sent gets the defaults. - `public const byte Any = 255;`: The wildcard of a context position. - `public const int MaxMessageBytes = 262144;`: The largest message the engine accepts. - `public const int MaxRulesPerMessage = 512;`: Rules in one message (a larger subject is sent with delta messages). - `public static int Apply(byte[] message)`: Applies a message built by one of the functions below. - `public static byte[] BeginSnapshot()`: Starts a full resynchronisation (after a worker start): every subject sent before `NativePermissionTable.EndSnapshot` is kept, the others are dropped at the end. Until then the last state keeps answering. - `public static bool Claim(bool on)`: Claims (true) or releases (false) the table. Releasing returns command requirements to the built-in backend. - `public static bool Declare(Permission permission)`: Declares a node's defaults for its five operator levels (what the node answers for a subject without a rule). Called by the server for every registered permission node; a plugin does not need it for nodes it registers through `PermissionManager.Register`. - `public static byte[] DeltaSubject(Uuid subject, uint baseVersion, uint version, IReadOnlyList> changes)`: Upserts or removes rules of a subject whose version is `baseVersion`; the engine refuses a delta on any other version (-4) and then the subject needs {{cref:M:Mc.Scripting.Permissions.NativePermissionTable.SetSubject(Mc.Scripting.Uuid,System.UInt32,System.Byte,System.Byte,System.Collections.Generic.IReadOnlyList{Mc.Scripting.Permissions.NativeRule})}}. - `public static byte[] DropSubject(Uuid subject)`: Drops a subject's state (a player who left; the engine frees it). - `public static byte[] EndSnapshot()`: Ends a full resynchronisation (see `NativePermissionTable.BeginSnapshot`). - `public static byte[] SetSlots(Uuid subject, byte slot0, byte slot1)`: Sets a subject's plugin context slot values without touching its rules. - `public static byte[] SetSubject(Uuid subject, uint version, byte slot0, byte slot1, IReadOnlyList rules)`: Replaces every rule of a subject (at most `NativePermissionTable.MaxRulesPerMessage` of them). - `public static NativeTableStats? Stats()`: The table's counters (the engine's `perm_stats`). - `public static bool WriteBuiltin(string json)`: Writes the built-in backend's file (permissions.json) through the engine: the worker's own sandbox refuses the write. ## NativeRule (struct, permissions) ```csharp public readonly struct NativeRule : IEquatable ``` A rule of the engine's permission table: a node or pattern over a context pattern, with a layer and a value. - `public NativeRule(string Node, bool Allow, ushort Layer = 0, byte[]? Context = null)`: A rule of the engine's permission table: a node or pattern over a context pattern, with a layer and a value. - `public bool Allow { get; init; }`: True grants the node, false denies it. - `public byte[]? Context { get; init; }`: The six context positions (world, dimension, gamemode, server, slot 0, slot 1); `NativePermissionTable.Any` matches every value. Null means any in every position. - `public ushort Layer { get; init; }`: Lower layers win: 0 the player's own rules, then the groups by weight (highest first), then the default group. - `public string Node { get; init; }`: The node or a pattern: "homes.use", "homes.*", "*". ## NativeTableStats (struct, permissions) ```csharp public readonly struct NativeTableStats : IEquatable ``` The engine's permission table counters. - `public NativeTableStats(ulong Subjects, ulong Rules, ulong Nodes, ulong Applied, ulong Rejected, ulong Generation, int LastStatus, bool Claimed)`: The engine's permission table counters. - `public ulong Applied { get; init; }`: Messages applied. - `public bool Claimed { get; init; }`: Whether the table is claimed. - `public ulong Generation { get; init; }`: Snapshot generations begun. - `public int LastStatus { get; init; }`: Status of the last message. - `public ulong Nodes { get; init; }`: Interned names. - `public ulong Rejected { get; init; }`: Messages refused. - `public ulong Rules { get; init; }`: Rules over all subjects. - `public ulong Subjects { get; init; }`: Subjects with state. ## Permission (class, permissions) ```csharp public sealed class Permission : IEquatable ``` A declared permission node: its name, default and description. - `public Permission(string Node, PermissionDefault Default, string Description = "")`: A declared permission node: its name, default and description. - `public PermissionDefault Default { get; init; }`: The value when nothing else decides. - `public string Description { get; init; }`: What the permission allows (shown by admin tools). - `public string Node { get; init; }`: The dotted node, e.g. "homes.use". ## PermissionDefault (struct, permissions) ```csharp public readonly struct PermissionDefault : IEquatable ``` What a permission node is worth when no group or player entry decides it. - `public static PermissionDefault False { get; }`: Nobody has it (until granted). - `public static PermissionDefault NotOp { get; }`: Everybody who is not an operator (level below 2). - `public static PermissionDefault Op { get; }`: Operators of vanilla permission level 2 and up (the "gamemaster" level: command blocks and most admin commands). - `public static PermissionDefault True { get; }`: Everybody has it. - `public bool Evaluate(int permissionLevel)`: Evaluates the default for a player of the given vanilla permission level. - `public override string ToString()`: The name used in `plugin.json` and `permissions.json`: "true", "false", "op", "op:3", "not_op". - `public static PermissionDefault OpLevel(int level)`: Operators of at least vanilla level `level` (1 = bypass spawn protection, 2 = gamemaster, 3 = admin, 4 = owner). - `public static bool TryParse(string? text, out PermissionDefault value)`: Parses the text form (see `PermissionDefault.ToString`). ## PermissionManager (class, permissions) ```csharp public static class PermissionManager ``` The server's permission system: declared nodes with defaults, the active backend (`IPermissionProvider`) and the question "does this player have this node?". The default backend is a `FilePermissionProvider` on `permissions.json`; a plugin may replace it. Resolution: the provider's explicit value (wildcards, groups and player overrides are its business); if it has no opinion, the node's registered default decides (`PermissionDefault.Op` and friends are evaluated against the player's vanilla operator level); an unregistered node without an entry is denied, except `minecraft.command.` which defaults to the vanilla operator level of that command. The console always has every permission. Everything here runs on the tick thread. - `public static FilePermissionProvider BuiltIn { get; }`: The built-in file backend (created on first use at `permissions.json` in the server directory). - `public static IPermissionProvider Provider { get; }`: The active backend. - `public static IReadOnlyList Registered { get; }`: A snapshot of the declared nodes. - `public static PermissionValue GetExplicit(Uuid player, string node)`: The tri-state value the backend holds for the player, without applying defaults. - `public static bool Has(Player player, string node)`: Whether the player has the permission. - `public static bool Has(Uuid player, string node)`: Whether the player (online or not) has the permission. The operator level is looked up in the server's ops list. - `public static bool Has(Entity entity, string node)`: Whether the entity has the permission (`Entity.Null` is the console and has everything). - `public static bool IsRegistered(string node)`: Whether a node was declared. - `public static void Register(Permission permission)`: Declares a permission node (the plugin-scoped `plugin.Permissions.Register` unregisters it on unload). - `public static void Reload()`: Reloads the built-in backend's file and fires `PermissionsReloaded`. - `public static void SetProvider(IPermissionProvider provider)`: Replaces the backend. The previous one is restored when the owning plugin unloads (use the plugin-scoped overload from plugins). ## PermissionValue (enum, permissions) ```csharp public enum PermissionValue : int ``` The three-valued answer of a permission provider. - `Unset` = 0: The provider has no opinion; the node's default decides. - `True` = 1: Explicitly granted. - `False` = 2: Explicitly denied (beats a grant of the same specificity). ## PluginPermissions (class, permissions) ```csharp public sealed class PluginPermissions ``` Permissions as seen by one plugin (`plugin.Permissions`): declare the nodes it uses, query players, and optionally replace the backend. Everything declared here is removed when the plugin unloads or hot-reloads. - `public FilePermissionProvider BuiltIn { get; }`: The built-in file backend (groups and player overrides stored in `permissions.json`). - `public bool Has(Player player, string node)`: Whether the player has the permission. - `public bool Has(Uuid player, string node)`: Whether the player (online or not) has the permission. - `public void Register(Permission permission)`: Declares a permission node. - `public void Register(string node, PermissionDefault default, string description = "")`: Declares a permission node with its default and a description. - `public void SetProvider(IPermissionProvider provider)`: Replaces the permission backend for as long as this plugin is loaded (a LuckPerms-style plugin does this in `OnEnable`). ## Phase (enum, systems) ```csharp public enum Phase : int ``` Server tick phases in execution order (the engine's phases). Deferred changes (entity creation, component add/set/remove) made in one phase are visible to every later phase of the same tick, e.g. an entity a native NetworkReceive system creates is seen by Tick systems. - `NetworkReceive` = 0: Drain inbound packets, apply them to entities. - `PreTick` = 1: Timers, scheduled tasks, input integration. - `Tick` = 2: Game logic (entities, blocks, ...). - `PostTick` = 3: Removals, collision resolution, derived state. - `Tracking` = 4: Spatial index update, view diffs. - `NetworkSend` = 5: Build and encode outbound packets. - `AsyncIntegration` = 6: Integrate worker results (chunk loads, lighting). ## Player (struct, players) ```csharp public readonly struct Player : IEquatable ``` A handle to a connected player. Blittable (usable in events and ECS components). A handle stays valid only while the player is online: after he leaves it goes stale. Stale handles fail safe: reads return defaults, actions do nothing (see `Player.IsOnline`). A player who rejoins gets a new handle. All members must be called on the tick thread. - `public Player(Entity entity)`: Wraps an entity that is a player. - `public readonly Entity Entity;`: The underlying ECS entity. - `public double Absorption { get; set; }`: The golden hearts of absorption in half hearts. - `public IReadOnlyList ActiveEffects { get; }`: The keys of the effects the player has. - `public string Address { get; }`: The client's address as the server sees it ("127.0.0.1:50321"), empty for a stale handle. - `public int Air { get; set; }`: The remaining air in ticks (300 is full; below 0 the player drowns). - `public bool AllowFlight { get; set; }`: Whether the player may fly (creative mode sets it automatically). - `public IReadOnlyList Channels { get; }`: The channels the player's client registered (minecraft:register), in the order it sent them. Empty for a stale handle. - `public Component? DisplayName { get; set; }`: The name the server shows for this player where vanilla uses Player.getDisplayName: the join and leave lines, the chat sender, death and advancement messages (the team format applies to it). Null when none is set, and then the profile `Player.Name` is shown. Set it to null to clear it. Not persisted: it is cleared when the player leaves, so a plugin that keeps nicknames sets it again on join. The profile name stays the key of commands, the scoreboard and the click and insertion text of chat. Stale handles: reads give null, writes do nothing. - `public double Exhaustion { get; set; }`: The exhaustion, 0..40: at 4 the saturation (or food level) drops by one. - `public int ExperienceLevel { get; set; }`: The experience level shown in the HUD. Setting resets the progress within the level. - `public double ExperienceProgress { get; set; }`: The progress to the next level, 0..1 (the green bar). - `public int FireTicks { get; set; }`: The ticks the player will keep burning (0 when not on fire). Setting 0 extinguishes. - `public float FlySpeed { get; set; }`: The flying speed, 0..1 (default 0.05). - `public int FoodLevel { get; set; }`: The food level, 0..20 (the hunger bar; below 7 sprinting stops, at 0 the player starves). - `public GameMode GameMode { get; set; }`: The player's game mode. Setting it fires `PlayerGameModeChangeEvent` (a handler may cancel it). - `public double Health { get; set; }`: Health in half hearts (20 is full). Setting is clamped to 0..`Player.MaxHealth`; 0 kills the player through the normal death handling. - `public ItemStack HeldItem { get; }`: The stack in the player's main hand (empty for a stale handle). - `public PlayerInventory Inventory { get; }`: The player's inventory: slots, hotbar, armor, off hand and the cursor (`PlayerInventory`). A stale handle (a player who left) gives an inventory with no slots. - `public bool Invulnerable { get; set; }`: Whether the player takes no damage (creative mode sets it automatically). - `public bool IsFlying { get; set; }`: Whether the player is flying (creative or spectator flight, or `Player.AllowFlight`). Setting it only works while `Player.AllowFlight` is true. - `public bool IsGliding { get; }`: Whether the player is gliding with elytra. - `public bool IsNone { get; }`: Whether this is the "no player" handle. - `public bool IsOnGround { get; }`: Whether the player is on the ground. - `public bool IsOnline { get; }`: Whether the player is connected and in the play state. - `public bool IsSneaking { get; }`: Whether the player is sneaking (crouching). - `public bool IsSprinting { get; }`: Whether the player is sprinting. - `public bool IsSwimming { get; }`: Whether the player is swimming. - `public int Latency { get; }`: The round trip time to the client in milliseconds (-1 for a stale handle). - `public string Locale { get; }`: The client's language, e.g. "en_us". - `public Location Location { get; }`: Position and rotation in the player's world. - `public double MaxHealth { get; }`: The maximum health (the attribute value, 20 by default). Read only: change it with attribute modifiers. - `public string Name { get; }`: The player's name, empty for a stale handle. - `public int PermissionLevel { get; }`: The vanilla operator level of the player (0-4, from ops.json); 0 for an offline handle. - `public Vec3d Position { get; }`: The player's position (feet), `default` for a stale handle. - `public double Saturation { get; set; }`: The saturation, 0..`Player.FoodLevel`: the buffer that is used up before the food level drops. - `public int TotalExperience { get; }`: The total experience points ever collected (drives the score on the death screen). Read only; use `Player.GiveExperience`. - `public Uuid Uuid { get; }`: The player's profile id, `default` for a stale handle. - `public float WalkSpeed { get; set; }`: The walking speed, 0..1 (default 0.1). - `public World World { get; }`: The dimension the player is in (the overworld for a stale handle). - `public static Player None { get; }`: The "no player" handle. - `public bool AddEffect(PotionEffect effect)`: Adds a status effect (a stronger or longer effect of the same kind replaces the current one like in vanilla). - `public bool ClearEffects()`: Removes every status effect. - `public bool ClearFakeBlock(BlockPos position)`: Shows the world's real block at a position again. - `public void ClearFakeBlocks()`: Ends every fake block of this player. - `public void ClearTitle(bool resetTimes = false)`: Hides the current title and subtitle. - `public bool Equals(Player other)` - `public override bool Equals(object? obj)` - `public ref T Get() where T : unmanaged`: Reference to a plugin component on the player's entity; throws if the player lacks it. The reference points into ECS storage and dies with the next structural change: re-fetch instead of keeping it. - `public PotionEffect? GetEffect(string effect)`: The active effect of a kind, null when the player does not have it. - `public override int GetHashCode()` - `public void GiveExperience(int points)`: Gives experience points (negative takes them away), levelling up as needed, like `/xp add`. - `public void GiveExperienceLevels(int levels)`: Gives whole experience levels (negative takes them away). - `public bool Has() where T : unmanaged`: Whether the player has a plugin component. - `public bool HasPermission(string node)`: Whether the player has a permission node (groups, per-player entries, wildcards and node defaults are resolved by the permission backend). - `public void Kick(Component reason)`: Disconnects the player. Fires `PlayerKickEvent` first (a handler may cancel it). - `public bool ListensOn(string channel)`: Whether the player's client registered the channel (minecraft:register), that is, whether it listens on it. False for a stale handle. - `public void PlaySound(string sound, SoundCategory category = SoundCategory.Master, float volume = 1f, float pitch = 1f)`: Plays a sound to this player only, at his position. - `public void PlaySound(string sound, Vec3d position, SoundCategory category = SoundCategory.Master, float volume = 1f, float pitch = 1f)`: Plays a sound to this player only, coming from a position. - `public Player Remove() where T : unmanaged`: Removes a plugin component from the player. - `public void RemoveAllResourcePacks()`: Removes every resource pack the server pushed. - `public bool RemoveEffect(string effect)`: Removes a status effect. - `public void RemoveResourcePack(Uuid id)`: Removes a resource pack from the client. - `public void SendActionBar(Component message)`: Shows a message in the action bar above the hotbar. No-op for a stale handle. - `public void SendMessage(Component message)`: Sends a chat (system) message to the player. No-op for a stale handle. - `public bool SendPluginMessage(string channel, ReadOnlySpan data)`: Sends a plugin message to the player on a channel (a custom payload of the play state). Vanilla clients ignore channels they did not register; clients with a mod or a proxy that listens on it receive the bytes. - `public void SendResourcePack(ResourcePack pack)`: Asks the client to download and apply a resource pack. The answer arrives as `PlayerResourcePackStatusEvent` events. - `public Player Set(in T value) where T : unmanaged`: Adds or overwrites a plugin component on the player. The component disappears when the player leaves. - `public bool SetGlowingFor(Player viewer, bool glowing)`: Makes this player glow for one viewer: the viewer sees the outline (for him only, the others do not change). See `Entity.SetGlowingFor`. - `public void SetListName(Component? name)`: Changes the name everybody sees for this player in the tab list (colours, prefixes). Null restores the profile name. The name is kept until the player leaves. - `public void SetTabList(Component? header, Component? footer)`: Sets the text above and below the player list (Tab). Null clears a part. - `public bool ShowBlock(BlockPos position, BlockState state)`: Shows a block state at a position to this player only. The world keeps its real block; the server still collides with the real one, and the client predicts collisions against what it sees. The fake block is re-sent with its chunk and after real changes of its position, and dropped when the player changes world or leaves. - `public void ShowSubtitle(Component subtitle)`: Shows only a subtitle (it appears together with the next title; vanilla shows nothing for a subtitle without a title). - `public void ShowTitle(Component title, Component? subtitle = null, TitleTimes? times = null)`: Shows a title (and optionally a subtitle) in the middle of the screen. No-op for a stale handle. - `public bool SpawnParticle(string particle, Vec3d position, int count = 1, Vec3d offset = default, float speed = 0f, bool force = false)`: Shows a particle effect to this player only. - `public void StopSound(string? sound = null, SoundCategory? category = null)`: Stops sounds this player is hearing. - `public bool Teleport(Location location)`: Teleports the player to a location, also into another dimension. Fires `PlayerTeleportEvent` (cause `TeleportCause.Plugin`); a handler may change the destination or cancel it. - `public bool Teleport(Vec3d position)`: Teleports the player within his dimension, keeping his rotation. - `public override string ToString()` - `public void Transfer(string host, int port = 25565)`: Sends the client to another server (it must accept transfers: `accepts-transfers=true` there). - `public bool TryGet(out T value) where T : unmanaged`: Copies a plugin component of the player if present. - `public static bool operator !=(Player a, Player b)`: Inequality. - `public static bool operator ==(Player a, Player b)`: Equality (same entity id). - `public static implicit operator Entity(Player p)`: A player is the entity it wraps. ## PlayerAction (enum, events) ```csharp public enum PlayerAction : int ``` Values of the protocol enum PlayerAction (the wire carries the ordinal). - `StartDestroyBlock` = 0: PlayerAction start_destroy_block (ordinal 0). - `ChangeDestroyDirection` = 1: PlayerAction change_destroy_direction (ordinal 1). - `AbortDestroyBlock` = 2: PlayerAction abort_destroy_block (ordinal 2). - `StopDestroyBlock` = 3: PlayerAction stop_destroy_block (ordinal 3). - `DropAllItems` = 4: PlayerAction drop_all_items (ordinal 4). - `DropItem` = 5: PlayerAction drop_item (ordinal 5). - `ReleaseUseItem` = 6: PlayerAction release_use_item (ordinal 6). - `SwapItemWithOffhand` = 7: PlayerAction swap_item_with_offhand (ordinal 7). - `Stab` = 8: PlayerAction stab (ordinal 8). ## PlayerSnapshot (struct, players) ```csharp public readonly struct PlayerSnapshot : IEquatable ``` One online player as the engine reports him (see `Server.PlayersSnapshot`). - `public PlayerSnapshot(ulong EntityId, Guid Uuid, int World, int GameMode, int Ping, bool Listed, bool AllowsListing, bool HasDisplayName, string Name)`: One online player as the engine reports him (see `Server.PlayersSnapshot`). - `public bool AllowsListing { get; init; }`: Whether he allows being listed (the server list setting of his client). - `public ulong EntityId { get; init; }`: The entity handle; `PlayerSnapshot.Player` wraps it. - `public int GameMode { get; init; }`: The game mode, 0 survival, 1 creative, 2 adventure, 3 spectator. - `public bool HasDisplayName { get; init; }`: Whether he has a display name in the tab list. - `public bool Listed { get; init; }`: Whether he is listed in the tab list. - `public string Name { get; init; }`: His name. - `public int Ping { get; init; }`: The latency in milliseconds. - `public Player Player { get; }`: The player handle (valid while he is online). - `public Guid Uuid { get; init; }`: The profile id. - `public int World { get; init; }`: The index of his dimension, -1 when unknown (see `Server.Worlds`). ## Plugin (class, plugins) ```csharp public abstract class Plugin : IPlugin ``` Convenience base class exposing the plugin's scoped services. - `protected Plugin()`: Plugins have a parameterless constructor (implicit). Do not do work in it; use `OnEnable`. - `public BossBars BossBars { get; }`: Boss bars of this plugin (removed from every viewer when the plugin unloads). - `public ChannelRegistry Channels { get; }`: Plugin messaging channels: listen on a channel (`ChannelRegistry.On`). - `public ChunkTickets ChunkTickets { get; }`: Chunk tickets of this plugin: chunks it keeps loaded (released when the plugin unloads). - `public CommandRegistry Commands { get; }`: Commands of this plugin. - `public PluginConfig Config { get; }`: Configuration files and the data folder of this plugin. - `public PluginContext Context { get; }`: The plugin's scoped services. - `public string DataFolder { get; }`: The plugin's data folder (`plugins//`), created on first access. - `public Ecs Ecs { get; }`: ECS access (components, queries). - `public EventRegistry Events { get; }`: Event subscriptions of this plugin. - `public Holograms Holograms { get; }`: Holograms: text that floats in the world for the players (removed when the plugin unloads). - `public PluginMetadata Info { get; }`: What the plugin declares about itself (id, version, dependencies, ...). - `public Logger Log { get; }`: The plugin's logger. - `public Menus Menus { get; }`: Custom GUIs of this plugin (closed for every viewer when the plugin unloads). - `public Npcs Npcs { get; }`: NPCs: fake players with a skin, equipment and click events (removed when the plugin unloads). - `public PluginPermissions Permissions { get; }`: Permission nodes of this plugin and permission queries. - `public Scheduler Scheduler { get; }`: Scheduled tasks of this plugin. - `public ServiceManager Services { get; }`: Plugin-to-plugin services: register an implementation, look up another plugin's. - `public Sidebars Sidebars { get; }`: Per-player sidebar scoreboards of this plugin (hidden when the plugin unloads). - `public SystemRegistry Systems { get; }`: ECS systems of this plugin. - `protected virtual void OnDisable()`: Optional cleanup; registrations are released automatically afterwards. - `protected abstract void OnEnable()`: Register events, commands and systems here. ```csharp using Mc.Scripting; [PluginInfo("Hello", Version = "1.0.0", Author = "you")] public sealed class Hello : Plugin { protected override void OnEnable() { Events.On((ref PlayerJoinEvent e) => Log.Info($"{e.Username.ToString()} joined")); } protected override void OnDisable() => Log.Info("bye"); } ``` ## PluginContext (class, plugins) ```csharp public sealed class PluginContext ``` Per-plugin services. Everything registered through it is released automatically when the plugin unloads or hot-reloads. Plugins normally use the same members through the `Plugin` base class properties (`Events`, `Commands`, ...). Use the context directly when your code lives outside the plugin class: pass it to helper classes so everything they register is released with the plugin. - `public string AssemblyPath { get; }`: Path of the plugin assembly. - `public BossBars BossBars { get; }`: Boss bars. - `public ChannelRegistry Channels { get; }`: Plugin messaging channels of this plugin (`ChannelRegistry`). - `public ChunkTickets ChunkTickets { get; }`: Chunk tickets. - `public CommandRegistry Commands { get; }`: Commands. - `public PluginConfig Config { get; }`: Configuration files and the data folder. - `public Ecs Ecs { get; }`: ECS access. - `public EventRegistry Events { get; }`: Event subscriptions. - `public Holograms Holograms { get; }`: Holograms (text displays). - `public int Id { get; }`: Stable plugin id (kept across hot reloads). - `public PluginMetadata Info { get; }`: What the plugin declares about itself (`plugin.json` or `PluginInfoAttribute`). - `public bool IsUnloaded { get; }`: Whether the plugin was unloaded. - `public Logger Log { get; }`: The plugin's logger. - `public Menus Menus { get; }`: Custom GUIs (menus) owned by this plugin. - `public string Name { get; }`: The plugin name. - `public Npcs Npcs { get; }`: NPCs (fake players). - `public PluginPermissions Permissions { get; }`: Permissions. - `public Scheduler Scheduler { get; }`: Scheduler. - `public ServiceManager Services { get; }`: Plugin-to-plugin services. - `public Sidebars Sidebars { get; }`: Sidebars. - `public SystemRegistry Systems { get; }`: Systems. - `public string Version { get; }`: The plugin version. - `public void Track(IPluginResource resource)`: Registers an arbitrary resource to be disposed on unload. ## PluginDependency (struct, plugins) ```csharp public readonly struct PluginDependency : IEquatable ``` A dependency declaration of a plugin: the id of the other plugin and an optional version constraint. - `public PluginDependency(string Id, string? Constraint)`: A dependency declaration of a plugin: the id of the other plugin and an optional version constraint. - `public string? Constraint { get; init; }`: A version constraint such as ">=1.2", "^1.2", "=1.2.3" (null = any version). - `public string Id { get; init; }`: The other plugin's id. - `public bool Accepts(string version)`: Whether `version` satisfies the constraint. - `public override string ToString()` - `public static PluginDependency Parse(string text)`: Parses "economy" or "economy >=1.2". ## PluginInfoAttribute (class, plugins) ```csharp public sealed class PluginInfoAttribute : Attribute ``` Optional metadata for a plugin type. - `public PluginInfoAttribute(string name)`: Optional metadata for a plugin type. - `public string? Author { get; init; }`: Author. - `public string? Description { get; init; }`: Description. - `public string Name { get; }`: Display name. - `public string Version { get; init; }`: Version text. ```csharp [PluginInfo("Homes", Version = "1.2.0", Author = "you", Description = "Set and teleport to homes")] public sealed class HomesPlugin : Plugin { protected override void OnEnable() { /* ... */ } } ``` ## PluginMessageHandler (delegate, plugins) ```csharp public delegate void PluginMessageHandler(Player player, ReadOnlySpan data) ``` Receives a message a client sent on a channel the plugin listens on. `data` is valid for the call only. ## PluginMetadata (class, plugins) ```csharp public sealed class PluginMetadata ``` What a plugin declares about itself: from `plugin.json`, or from `PluginInfoAttribute` when there is no manifest. - `public PluginMetadata(string id, string name, string version)`: Creates metadata (the runtime does this; plugins read it from `PluginContext.Info`). - `public ApiVersion Api { get; init; }`: The plugin API version the plugin was written for. - `public bool ApiDeclared { get; init; }`: Whether the manifest named an API version (otherwise 1.0 was assumed). - `public IReadOnlyList Authors { get; init; }`: The author names. - `public IReadOnlyList Depends { get; init; }`: Plugins that must be loaded (and enabled) before this one; a missing one prevents this plugin from loading. - `public string Description { get; init; }`: A one-line description. - `public string Id { get; }`: Unique id: lower case letters, digits, '-' and '_' (1-32 characters). Also the data folder name. - `public IReadOnlyList LoadBefore { get; init; }`: Plugins that should be loaded after this one (the reverse of a soft dependency). - `public string? Main { get; init; }`: The full name of the plugin class when the assembly has several candidates (default: the single public `IPlugin`). - `public string Name { get; }`: Display name (used as the log prefix). - `public IReadOnlyList SoftDepends { get; init; }`: Plugins that are loaded before this one when present. - `public string Source { get; init; }`: The path of the manifest or assembly this was read from. - `public string Version { get; }`: Plugin version. - `public string Website { get; init; }`: A website or repository url. - `public static bool IsValidId(string id)`: Whether `id` is a valid plugin id. ## Plugins (class, plugins) ```csharp public static class Plugins ``` Read-only view of the loaded plugins: for admin tools (a `/plugins` command), for plugins that adapt to an optional other plugin, and for diagnosing why a plugin did not load. - `public static IReadOnlyList All { get; }`: The loaded (enabled) plugins in load order: dependencies before their dependents. - `public static IReadOnlyList Problems { get; }`: The problems found while discovering and loading plugins (bad manifests, missing or outdated dependencies, dependency cycles, an unsupported API version, failures in `OnEnable`), one readable sentence each, newest scan last. - `public static PluginMetadata? Get(string id)`: The metadata of a loaded plugin, null when no plugin with this id is loaded. - `public static bool IsLoaded(string id)`: Whether a plugin is loaded. ## PortalCreateReason (enum, events) ```csharp public enum PortalCreateReason : byte ``` Why a portal is being created. - `Fire` = 0: Lit with fire. - `NetherPair` = 1: The destination portal of a nether portal trip. - `Plugin` = 2: A plugin. ## PotionEffect (struct, players) ```csharp public readonly struct PotionEffect : IEquatable ``` A status effect to apply to a player (a potion effect in Bukkit terms). - `public PotionEffect(string Effect, int DurationTicks, int Amplifier = 0, bool Ambient = false, bool ShowParticles = true, bool ShowIcon = true)`: A status effect to apply to a player (a potion effect in Bukkit terms). - `public const int Infinite = -1;`: The duration of an effect that never runs out. - `public bool Ambient { get; init; }`: Whether it comes from a beacon (translucent particles). - `public int Amplifier { get; init; }`: The level minus one: 0 is level I, 1 is level II, up to 255. - `public int DurationTicks { get; init; }`: How long it lasts in ticks (20 per second), or `PotionEffect.Infinite`. - `public string Effect { get; init; }`: The effect key, e.g. `minecraft:speed` (the namespace may be left out). - `public bool IsInfinite { get; }`: Whether the effect never runs out. - `public bool ShowIcon { get; init; }`: Whether the effect shows in the player's HUD. - `public bool ShowParticles { get; init; }`: Whether the effect shows particles around the player. - `public static PotionEffect For(string effect, TimeSpan duration, int amplifier = 0)`: Creates an effect lasting a number of seconds. ## PresenceExtensions (class, display) ```csharp public static class PresenceExtensions ``` The presence API of a plugin: `plugin.Tablist`, `plugin.Nametags` and `plugin.Belowname`, on the plugin base class and on the plugin context. Everything a plugin sets through them is restored and removed when the plugin unloads. - `public static Belowname get_Belowname(Plugin plugin)` - `public static Belowname get_Belowname(PluginContext context)` - `public static Nametags get_Nametags(Plugin plugin)` - `public static Nametags get_Nametags(PluginContext context)` - `public static Tablist get_Tablist(Plugin plugin)` - `public static Tablist get_Tablist(PluginContext context)` ## Query (class, ecs) ```csharp public abstract class Query : IDisposable, IPluginResource ``` A cached ECS query. Iterate with `foreach (var chunk in query)` for the fastest path (SoA spans per archetype), or use `Each` for convenience. Iterate `foreach (var chunk in query)` for the fastest path (one `QueryChunk` per archetype, with a `Span` per component), or call `Each` on the typed query. Structural changes made while iterating (adding or removing components, destroying entities) are deferred until the iteration ends. - `public int Count()`: Number of matching entities (iterates). - `public void Dispose()`: Destroys the query. - `public QueryEnumerator GetEnumerator()`: Starts an iteration over the matching archetype chunks. ## QueryAction (delegate, ecs) ```csharp public delegate void QueryAction(Entity entity, ref T1? c1) ``` Callback for `Each` over one component. ## QueryAction (delegate, ecs) ```csharp public delegate void QueryAction(Entity entity, ref T1? c1, ref T2? c2) ``` Callback for `Each` over two components. ## QueryAction (delegate, ecs) ```csharp public delegate void QueryAction(Entity entity, ref T1? c1, ref T2? c2, ref T3? c3) ``` Callback for `Each` over three components. ## QueryChunk (struct, ecs) ```csharp public readonly ref struct QueryChunk ``` One archetype's worth of query results: entity ids and one column per query term. - `public int Count { get; }`: Number of entities in the chunk. - `public ReadOnlySpan Entities { get; }`: The entity ids of the chunk. - `public Span Field(int index) where T : unmanaged`: Column for query term `index` (the order of the query's type arguments). ## QueryEnumerator (struct, ecs) ```csharp public ref struct QueryEnumerator ``` Iterates archetype chunks. Disposing early releases the native iterator. - `public QueryChunk Current { get; }`: The current chunk. - `public void Dispose()`: Releases the native iterator when abandoning the loop early. - `public bool MoveNext()`: Advances to the next chunk. ## Query (class, ecs) ```csharp public sealed class Query : Query where T1 : unmanaged ``` A query over one component type. - `public void Each(QueryAction action)`: Calls `action` for every matching entity. ```csharp // _mana is a Query created in OnEnable with Ecs.Query() _mana.Each((Entity entity, ref Mana m) => m.Value++); ``` ## Query (class, ecs) ```csharp public sealed class Query : Query where T1 : unmanaged where T2 : unmanaged ``` A query over two component types. - `public void Each(QueryAction action)`: Calls `action` for every matching entity. ## Query (class, ecs) ```csharp public sealed class Query : Query where T1 : unmanaged where T2 : unmanaged where T3 : unmanaged ``` A query over three component types. - `public void Each(QueryAction action)`: Calls `action` for every matching entity. ## ResourcePack (class, players) ```csharp public sealed class ResourcePack : IEquatable ``` A resource pack the server asks clients to download. - `public ResourcePack(Uuid Id, string Url, string? Sha1 = null, bool Required = false, Component? Prompt = null)`: A resource pack the server asks clients to download. - `public Uuid Id { get; init; }`: The pack id (clients keep one pack per id: pushing the same id replaces it). - `public Component? Prompt { get; init; }`: Text shown in the download prompt, null for the default. - `public bool Required { get; init; }`: Whether the client is disconnected when it declines the pack. - `public string? Sha1 { get; init; }`: The SHA-1 hash of the zip as 40 hex characters; null skips the check (clients then re-download on every push). - `public string Url { get; init; }`: The download address (http or https). - `public static ResourcePack FromUrl(string url, string? sha1 = null, bool required = false, Component? prompt = null)`: Creates a pack whose id is derived from its url, so pushing the same url twice replaces the first push. ## ResourcePackStatus (enum, players) ```csharp public enum ResourcePackStatus : byte ``` Client report about a pushed resource pack (protocol order). - `SuccessfullyLoaded` = 0: The pack was applied. - `Declined` = 1: The player declined it. - `FailedDownload` = 2: The download failed. - `Accepted` = 3: The player accepted it (download starts). - `Downloaded` = 4: Downloaded, not applied yet. - `InvalidUrl` = 5: The url was invalid. - `FailedReload` = 6: Applying the pack failed. - `Discarded` = 7: The pack was discarded. ## RespawnReason (enum, events) ```csharp public enum RespawnReason : byte ``` Why a player is respawning. - `Death` = 0: The player died. - `EndExit` = 1: Returning from the end after the credits. ## ScheduledTask (class, scheduler) ```csharp public sealed class ScheduledTask : IDisposable, IPluginResource ``` A scheduled task. Dispose/Cancel to stop it. - `public bool IsCancelled { get; }`: Whether the task was cancelled or finished. - `public void Cancel()`: Stops the task. - `public void Dispose()`: Same as `ScheduledTask.Cancel`. ## Scheduler (class, scheduler) ```csharp public sealed class Scheduler ``` Tick-based scheduling (20 ticks = 1 second) and the bridge between the tick thread and worker threads. Scheduled tasks run on the tick thread and are cancelled when the plugin unloads. `Scheduler.RunLater`/`Scheduler.RunEvery` must be called on the tick thread. `Scheduler.Post`, `Scheduler.RunSync`, `Scheduler.RunAsync` and the awaiters are thread-safe. - `public Task Delay(long ticks)`: Awaitable that resumes on the tick thread after `ticks` ticks. - `public void Post(Action action)`: Thread-safe: queue `action` to run on the tick thread next tick (fire and forget). - `public Task RunAsync(Action action)`: Runs `action` on the .NET thread pool. Must not touch game APIs (use `Scheduler.RunSync` to hop back). - `public Task RunAsync(Func func)`: Runs `func` on the .NET thread pool. - `public ScheduledTask RunEvery(long periodTicks, Action action, long initialDelayTicks = 0)`: Runs `action` every `periodTicks` ticks, starting after `initialDelayTicks` (at least 1). - `public ScheduledTask RunLater(long delayTicks, Action action)`: Runs `action` after `delayTicks` ticks on the tick thread. - `public Task RunSync(Action action)`: Thread-safe: runs `action` on the tick thread at the next tick boundary (FIFO). - `public Task RunSync(Func func)`: Thread-safe: runs `func` on the tick thread at the next tick boundary and returns its result. - `public TickAwaitable ToAsync()`: Awaitable that resumes on a thread-pool thread: `await Scheduler.ToAsync();` (leave the tick thread before blocking work). - `public TickAwaitable ToTick()`: Awaitable that resumes on the tick thread at the next tick boundary: `await Scheduler.ToTick();`. If the plugin unloads first the continuation is dropped. ```csharp Scheduler.RunLater(20, () => Server.Broadcast("one second later")); ScheduledTask autosave = Scheduler.RunEvery(20 * 60, SaveAll); // every minute // autosave.Cancel(); // from a thread-pool task: fetch off the tick thread, then apply on it _ = Task.Run(async () => { string json = await Http.Client.GetStringAsync(url); Scheduler.Post(() => Apply(json)); // back on the tick thread }); ``` ## ScoreFormat (enum, display) ```csharp public enum ScoreFormat : int ``` How a score is shown next to its holder. - `None` = 0: The number is shown. - `Blank` = 1: The number is hidden. - `Fixed` = 2: A fixed text is shown instead of the number. ## Scoreboard (class, display) ```csharp public static class Scoreboard ``` The server's scoreboard (shared by all players, saved with the world, edited by `/scoreboard`): objectives with scores per holder name and their display slots. Every change reaches every player's client. Use `Sidebar` for per-player content. - `public static Objective CreateObjective(string name, Component? title = null, string criteria = "dummy")`: Creates an objective. Objectives are part of the world: they stay until removed (they are not owned by the plugin). - `public static Objective? GetObjective(string name)`: An existing objective by name; null when there is none. - `public static bool RemoveObjective(Objective objective)`: Deletes an objective and all its scores. - `public static void SetDisplay(DisplaySlot slot, Objective? objective)`: Shows an objective in a display slot (null clears the slot). ## Server (class, server) ```csharp public static class Server ``` Server-wide services: the tick clock, the online players, the worlds and broadcast messages. Everything here except `Server.IsTickThread` and `Server.CurrentTick` must be called on the tick thread. - `public static long CommittedBytes { get; }`: The server's private commit in bytes, -1 when unknown. - `public static ulong CurrentTick { get; }`: Current server tick (as reported by the engine; thread-safe). - `public static bool IsTickThread { get; }`: Whether the calling thread is the server tick thread (thread-safe, ~1 ns). - `public static long MemoryBytes { get; }`: The server's RAM in bytes as Task Manager's Memory column shows it (private working set; Linux: resident minus shared), -1 when the platform cannot tell. Plugins run in a separate worker process, so `Environment.WorkingSet` is the worker's, not the server's. - `public static World Overworld { get; }`: The overworld. - `public static int PlayerCount { get; }`: Number of players in the play state. - `public static double PlayerMoveThreshold { get; set; }`: How far a player has to move (blocks) before `PlayerMoveEvent` fires again (a turn of one degree also counts). 0, the default, fires for every change of position or rotation (about 20 events a second per moving player): raise it when your handler only cares about coarse movement. - `public static IReadOnlyList Players { get; }`: A snapshot of the online players (in the play state). - `public static long PluginMemoryBytes { get; }`: The RAM of the process the plugins run in (the worker; in-process plugins: the server), measured like `Server.MemoryBytes`: private working set (Linux: resident minus shared). One system call, no allocation, any thread; -1 when unknown. Prefer it to `Process.GetCurrentProcess()`, which snapshots every process of the machine. - `public static long WorkingSetBytes { get; }`: The server's working set in bytes (resident, shared pages included), -1 when unknown. - `public static IReadOnlyList Worlds { get; }`: The dimensions of the server (overworld first). - `public static void Broadcast(string message)`: Sends a plain-text system message to everybody (and the console). - `public static void Broadcast(Component message)`: Sends a rich text message to every online player and logs its plain text on the console. - `public static Player? GetPlayer(string name)`: Finds an online player by name (case-insensitive). - `public static Player? GetPlayer(Uuid uuid)`: Finds an online player by uuid. - `public static World? GetWorld(string key)`: Finds a dimension by key ("minecraft:the_nether"; "the_nether" also works for minecraft keys). - `public static IReadOnlyList PlayersSnapshot()`: A snapshot of every online player with the data the engine keeps about him: the handle, the profile id, the dimension, the game mode, the ping and the tab list flags. One pass over the players, read in pages from the engine; use `Server.Players` when only the handles are needed. - `public static void SendMessage(Entity target, string message)`: Sends a system chat message to `target` (Entity.Null = console). - `public static TickTimes TickTimes(int ticks = 20)`: The engine's own tick time over the last `ticks` ticks (1 to 6000), as `/tick query` reports it: the busy time of each tick. Prefer it to timing your own systems: plugins run in a separate worker process, and a Stopwatch there also counts the round trips to the engine. Values are -1 when the engine cannot tell. ## ServiceUnavailableException (class, services) ```csharp public sealed class ServiceUnavailableException : InvalidOperationException ``` A plugin-to-plugin service that was registered is no longer available (its provider unloaded). - `public ServiceUnavailableException(Type serviceType)`: Creates the exception. ## Affix (class, services) ```csharp public sealed class Affix : IEquatable ``` One prefix or suffix of a player: its text, its weight (the highest weight comes first) and the group or plugin that supplied it. - `public Affix(string Text, int Weight, string Group)`: One prefix or suffix of a player: its text, its weight (the highest weight comes first) and the group or plugin that supplied it. - `public string Group { get; init; }`: Where it came from (a permission group, or a plugin's own name). - `public string Text { get; init; }`: The text, as the provider stores it (the chat and tab plugins decorate it). - `public int Weight { get; init; }`: The rank among the affixes of the same kind; a higher weight comes first. ## AffixKind (enum, services) ```csharp public enum AffixKind : int ``` Whether an affix goes before the player's name (a prefix) or after it (a suffix). - `Prefix` = 0: Before the name, as in "[Admin] Steve". - `Suffix` = 1: After the name, as in "Steve [AFK]". ## BanEntry (class, services) ```csharp public sealed class BanEntry : IEquatable ``` One ban: a player (uuid and the name it was banned under) or an address. `BanEntry.Expires` null means forever. - `public BanEntry(Uuid? Player, string? Address, string? Name, DateTimeOffset Created, DateTimeOffset? Expires, string Source, string? Reason)`: One ban: a player (uuid and the name it was banned under) or an address. `BanEntry.Expires` null means forever. - `public string? Address { get; init; }`: The banned address without port; null for a player ban. - `public DateTimeOffset Created { get; init; }`: When the ban was written. - `public DateTimeOffset? Expires { get; init; }`: When it ends; null = forever. - `public string? Name { get; init; }`: The name the player was banned under; null for an address ban. - `public Uuid? Player { get; init; }`: The banned player; null for an address ban. - `public string? Reason { get; init; }`: The reason shown on the disconnect screen; null for the default text. - `public string Source { get; init; }`: Who wrote it ("(Unknown)" when nobody said). ## Currency (class, services) ```csharp public sealed class Currency : IEquatable ``` A currency. Amounts are long minor units (cents when `Currency.FractionDigits` is 2): no floating point on any path. - `public Currency(string Name, string Symbol, int FractionDigits, string PluralName)`: A currency. Amounts are long minor units (cents when `Currency.FractionDigits` is 2): no floating point on any path. - `public int FractionDigits { get; init; }`: Minor units per major unit as a power of ten, 0 to 9 (checked when the currency is made). - `public string Name { get; init; }`: The name ("dollar"). - `public string PluralName { get; init; }`: The plural name ("dollars"). - `public string Symbol { get; init; }`: The symbol written before the amount ("$"). - `public string Format(long minor)`: The amount as text: the symbol, the major units and the minor units after a point ("$12.05"). Negative amounts start with a minus sign. ## EconomyResult (enum, services) ```csharp public enum EconomyResult : int ``` The outcome of a money operation. - `Ok` = 0: Done. - `Unknown` = 1: The account does not exist (the player never had one, or the provider does not create it on demand). - `NotEnough` = 2: The account lacks the money. - `OverMax` = 3: The deposit would go over the provider's maximum balance. - `Refused` = 4: Refused: a zero or negative amount, a frozen account, or another rule of the provider. ## IBanList (interface, services) ```csharp public interface IBanList ``` The server's ban lists: the vanilla files banned-players.json and banned-ips.json. The engine's login check reads the same files, so a ban written here, by /ban or by another plugin is enforced the same way; a plugin that replaces this service (a moderation plugin) must write the same files or keep them as its only store. A write does not kick an online player: the caller does that (`Player.Kick`). Thread: tick thread. - `bool AddAddress(string address, string? reason, DateTimeOffset? expires, string source)`: Bans an address (the bare IP; a port is dropped). - `bool AddPlayer(Uuid player, string name, string? reason, DateTimeOffset? expires, string source)`: Bans a player. An identical ban that is still in force is kept as it is. - `IReadOnlyList Addresses()`: The address bans that are still in force. - `BanEntry? Check(Uuid player, string? address)`: The ban that refuses a login of this player or address (the player ban first), null when none. - `IReadOnlyList Players()`: The player bans that are still in force. - `bool RemoveAddress(string address)`: Removes an address ban. - `bool RemovePlayer(Uuid player)`: Removes a player's ban. ## IEconomy (interface, services) ```csharp public interface IEconomy ``` An economy: balances of players (online or offline, by uuid), the transfers between them, and the currency. Provided by an economy plugin (Essentials is the first-party default, at a lower priority than any real economy). Without a provider, money rules must do nothing rather than invent money: check `ServiceManager.TryGet` for null. Thread: tick thread. - `Currency Currency { get; }`: The currency of the economy. - `long Balance(Uuid player)`: The balance in minor units; 0 for an unknown account. - `EconomyResult Deposit(Uuid player, long minor, string reason)`: Adds money to an account. - `bool Has(Uuid player, long minor)`: Whether the account holds at least the amount. - `bool HasAccount(Uuid player)`: Whether the uuid has an account. - `EconomyResult Transfer(Uuid from, Uuid to, long minor, string reason)`: Moves money from one account to another as one step: both or neither. - `EconomyResult Withdraw(Uuid player, long minor, string reason)`: Takes money from an account, refusing to go below zero. - `public event Action? Changed;`: Raised when an account's balance changed (tick thread). A null uuid means many accounts may have changed. ## IPlaceholders (interface, services) ```csharp public interface IPlaceholders ``` The placeholder registry: text templates such as "Online: %server_online%" are rendered through it, and any plugin can add its own placeholders (the chat format, the tab list, nametags, kits, the messages of other plugins). Process-wide: the built-in instance is registered with `ServiceManager`, so every plugin in the same process sees the same names. Rendering is pure managed code (no engine call of its own): a value that reads engine state pays for that read, which is why each placeholder declares how long its value may be reused. Thread: tick thread (the values are evaluated where the text is rendered). - `void Invalidate(string id)`: Drops every cached value of a placeholder so the next render evaluates it again. - `bool IsRegistered(string id)`: Whether an id is registered. - `IPluginResource Register(string id, PlaceholderScope scope, TimeSpan cacheFor, PlaceholderValue value)`: Adds a placeholder. The id is lowercase letters, digits and underscores ("charter_prefix"); "%charter_prefix%" renders it. A registration with the same id replaces the earlier one (a hot-reload successor). Disposing the result (or unloading the owning plugin, when it registered from a plugin's enable or event handler) removes it. - `string Resolve(Player subject, string text)`: Renders a template for a subject. "%%" is a literal percent sign; a token that is not a registered placeholder is left as written. Text without a percent sign comes back unchanged. ## IPlayerMeta (interface, services) ```csharp public interface IPlayerMeta ``` Player meta and affixes: the prefix, suffix and typed values that a permission provider (or any plugin) supplies for a player, read by the chat, tab and nametag plugins. Registered with `ServiceManager`; the highest-priority provider answers. Thread: tick thread. Ramjet's first-party permissions plugin implements it; without one, a built-in provider answers with no meta and no affixes. Contexts (a world, a server) are not part of this contract yet: a provider that needs them offers its own interface. - `IReadOnlyList Affixes(Uuid player, AffixKind kind)`: The prefixes or suffixes of a player, highest weight first. - `bool TryGet(Uuid player, MetaKey key, out T? value)`: The value of a meta key for a player, from the provider's highest layer that has it. - `public event Action? Changed;`: Raised when a player's meta or affixes changed (tick thread). A null uuid means every player may have changed. ## MetaKey (class, services) ```csharp public sealed class MetaKey ``` A typed key of player meta, declared by the plugin that owns the value (`new MetaKey("charter:level")`). Only string, int, bool and TimeSpan are allowed: a value of another type is refused by the provider, so readers never parse text. - `public MetaKey(string name)`: Creates a key. Throws for a value type other than string, int, bool or TimeSpan. - `public string Name { get; }`: The key. - `public override string ToString()` ## PlaceholderScope (enum, services) ```csharp public enum PlaceholderScope : int ``` Whether a placeholder has one value for everybody (`PlaceholderScope.Global`) or one value per player. - `Global` = 0: One value for everybody; the subject passed to the value is `Player.None`. - `Player` = 1: One value per subject player. ## PlaceholderValue (delegate, services) ```csharp public delegate string PlaceholderValue(Player subject, string[] args) ``` Computes a placeholder's value for a subject from the arguments written after its name: "%id:arg1:arg2%" passes ["arg1", "arg2"]. Throwing leaves the last good value in place (or the token as written) and logs the error once a minute. ## PlayerMetaExtensions (class, services) ```csharp public static class PlayerMetaExtensions ``` Helpers over `IPlayerMeta` for the plugins that only need text. - `public static string AffixText(IPlayerMeta meta, Uuid player, AffixKind kind, string separator = " ")`: The affixes of one kind joined into one text, highest weight first. ## ServiceManager (class, services) ```csharp public sealed class ServiceManager ``` Plugin-to-plugin services: a plugin registers an implementation of an interface (an economy, a chat formatter, a region lookup) and other plugins look it up without referencing the provider's code. The interface type lives in the provider's assembly or in a shared one; consumers declare `depends` / `softDepends` so the runtime resolves that assembly into their load context (the types then unify). Registrations are owned by the registering plugin and removed when it unloads, which fires `ServiceManager.Changed` so consumers drop their references. - `public IDisposable Changed(Action onChanged) where T : class`: Calls `onChanged` (on the tick thread) whenever the set of providers of `T` changes: a registration, an unregistration or a provider plugin unloading. Use it to refresh a cached `ServiceManager.TryGet` result. - `public T Get() where T : class`: The highest-priority provider of `T`; throws `ServiceUnavailableException` when none is registered. - `public IReadOnlyList> GetAll() where T : class`: All registrations of `T`, best first. - `public void Register(T instance, ServicePriority priority = ServicePriority.Normal) where T : class`: Registers an implementation of `T`. - `public T? TryGet() where T : class`: The highest-priority provider of `T`, null when none is registered. Thread-safe. - `public bool Unregister() where T : class`: Removes this plugin's registration of `T`. ## ServicePriority (enum, services) ```csharp public enum ServicePriority : int ``` How a provider ranks when several plugins register the same service type (the highest wins). - `Lowest` = -2: Used only when nothing better is registered. - `Low` = -1: Below normal. - `Normal` = 0: The default. - `High` = 1: Preferred over normal providers. - `Highest` = 2: Preferred over everything else. ## ServiceRegistration (class, services) ```csharp public sealed class ServiceRegistration : IEquatable> where T : class ``` One registered service: the implementation, who provided it and how it ranks. - `public ServiceRegistration(T Instance, string Provider, ServicePriority Priority)`: One registered service: the implementation, who provided it and how it ranks. - `public T Instance { get; init; }`: The implementation. - `public ServicePriority Priority { get; init; }`: Its rank. - `public string Provider { get; init; }`: The id of the registering plugin. ## Sidebar (class, display) ```csharp public sealed class Sidebar : IDisposable, IPluginResource ``` A sidebar scoreboard (the list of lines at the right of the screen) shown to chosen players. Each sidebar is a private objective sent only to its viewers, so different players can see different content at the same time (create one sidebar per player for personal lines). Lines are rich text, line 0 is the top; vanilla shows at most 15. Create it with `Sidebars.Create`; it disappears from the viewers when the plugin unloads. - `public const int MaxLines = 15;`: The number of lines the client can show. - `public int LineCount { get; }`: The number of lines currently set. - `public Component Title { get; set; }`: The title above the lines. - `public IReadOnlyList Viewers { get; }`: The players who currently see this sidebar (those who left are dropped). - `public void Clear()`: Removes every line. - `public void Dispose()` - `public void Hide(Player player)`: Hides the sidebar from a player. - `public bool IsShownTo(Player player)`: Whether the player sees this sidebar. - `public void Remove()`: Hides the sidebar from everybody and releases it. - `public void SetLine(int index, Component? text)`: Sets one line (0 is the top). A null text removes the line. - `public void SetLines(IReadOnlyList lines)`: Replaces all lines: the first text becomes line 0, lines beyond the given ones are removed. - `public void Show(Player player)`: Shows the sidebar to a player (replacing any sidebar of the server scoreboard on his screen). ## Sidebars (class, display) ```csharp public sealed class Sidebars ``` Creates sidebars owned by a plugin (`plugin.Sidebars`). - `public Sidebar Create(Component title)`: Creates a sidebar nobody sees yet. ## SoundCategory (enum, players) ```csharp public enum SoundCategory : int ``` Where a sound is mixed in the client's volume sliders. - `Master` = 0: The master volume only. - `Music` = 1: Music. - `Records` = 2: Jukebox and note blocks. - `Weather` = 3: Weather. - `Blocks` = 4: Blocks. - `Hostile` = 5: Hostile creatures. - `Neutral` = 6: Friendly creatures. - `Players` = 7: Players. - `Ambient` = 8: Ambient and cave sounds. - `Voice` = 9: Voice (narration). - `Ui` = 10: User interface sounds. ## SpawnReason (enum, events) ```csharp public enum SpawnReason : byte ``` Why an entity is being added to the world. - `Default` = 0: Unknown / not classified. - `Natural` = 1: Natural mob spawning. - `ChunkGeneration` = 2: Created with a freshly generated chunk. - `Breeding` = 3: A baby from breeding. - `SpawnEgg` = 4: A spawn egg. - `Spawner` = 5: A mob spawner. - `Command` = 6: /summon. - `Plugin` = 7: Spawned by a plugin. - `Structure` = 8: Spawned by a structure. - `Reinforcements` = 9: Zombie reinforcements. - `Lightning` = 10: Lightning (skeleton trap, zombified piglin ...). - `SlimeSplit` = 11: A slime split. - `Mount` = 12: A jockey / mount. - `Dispenser` = 13: Spawned by a dispenser. - `Drop` = 14: Created by a game mechanic: item drops, experience orbs, falling blocks, projectiles. - `Portal` = 15: Spawned by a portal. ## Subscription (class, events) ```csharp public sealed class Subscription : IDisposable, IPluginResource ``` An active event subscription. Dispose to unsubscribe. - `public bool IsActive { get; }`: Whether the subscription is still active. - `public void Dispose()`: Unsubscribes the listener. ## SystemCallback (delegate, systems) ```csharp public delegate void SystemCallback(float deltaTime) ``` Per-tick system callback; receives the tick's delta time in seconds. ## SystemHandle (class, systems) ```csharp public sealed class SystemHandle : IDisposable, IPluginResource ``` A registered system. Dispose to remove it from the pipeline. - `public string Name { get; }`: The system's effective registry name: the requested one (namespaced by the plugin, see `SystemRegistry.Register`), or, while a hot-reload predecessor still holds that name, the temporary "name#2" form. Renamed back to the plain name once the predecessor is released. - `public void Dispose()`: Removes the system from the pipeline. ## SystemPlacement (enum, systems) ```csharp public enum SystemPlacement : int ``` Where a system goes relative to the engine's registry. - `Append` = 0: End of the phase. - `After` = 1: Right after the system named by the anchor (same phase). - `Before` = 2: Right before the system named by the anchor (same phase). - `Replace` = 3: Takes the anchor's place: the anchor is disabled and this runs right after it. Disposing the handle (or unloading the plugin) re-enables the original. ## SystemRegistry (class, systems) ```csharp public sealed class SystemRegistry ``` Registers ECS systems in the engine's tick pipeline, scoped to a plugin (released on unload). - `public SystemHandle Add(string name, Phase phase, SystemCallback callback)`: Adds a system that runs once per tick at the end of `phase` on the tick thread. - `public SystemHandle After(string anchor, string name, SystemCallback callback)`: Registers `name` to run right after the system `anchor`. - `public SystemHandle Before(string anchor, string name, SystemCallback callback)`: Registers `name` to run right before the system `anchor`. - `public SystemHandle Register(Phase phase, string name, SystemCallback callback, SystemPlacement placement = SystemPlacement.Append, string? anchor = null)`: Registers a system in the engine's registry. The registry name is namespaced by the plugin: `"{plugin name}:{name}"` (left as is when `name` already starts with that prefix), so plugins cannot collide; a collision with a native system or with another plugin's system throws. `anchor` is the full registry name of an existing (native or scripted) system for After/Before/Replace (the phase is then the anchor's and `phase` is ignored). Called from inside a system, the registration takes effect when the tick ends. - `public SystemHandle Replace(string target, string name, SystemCallback callback)`: Replaces the (native or scripted) system `target`: it is disabled and `name` runs in its place. Dispose the handle (or unload the plugin) to restore it. - `public bool SetEnabled(string name, bool enabled)`: Enables or disables a registered system by name (native or scripted). A system this plugin disabled is re-enabled when the plugin unloads. Returns false if no such system exists. ```csharp Systems.Add("regen", Phase.Tick, dt => { /* runs every tick */ }); Systems.After("vanilla:physics", "after_physics", dt => { }); var h = Systems.Replace("vanilla:ai", "my_ai", dt => { }); // disables vanilla:ai h.Dispose(); // restores it ``` ## TabEntry (class, display) ```csharp public sealed class TabEntry ``` The fields of one player's tab entry for one viewer. A null field keeps what it was (see `Tablist.Set`). - `public TabEntry()` - `public Component? Display { get; init; }`: The name shown in the list, or null to keep it. - `public GameMode? GameMode { get; init; }`: The game mode shown, or null to keep it. - `public int? Latency { get; init; }`: The latency shown in milliseconds (0 to 60000), or null to keep it. - `public bool? Listed { get; init; }`: Whether the player is listed, or null to keep it. - `public int? Order { get; init; }`: The order in the list (lower first), or null to keep it. ## Tablist (class, display) ```csharp public sealed class Tablist ``` The tab list as one viewer sees it: the name, order, latency and game mode of each player in the list, the header and footer, fake entries (extra slots with their own name and skin) and the objectives shown in the list or under the names. Every change is seen by the viewer only. What a plugin sets is restored and removed when the plugin unloads. Reach it as `plugin.Tablist`. Call from the tick thread. - `public void AddFakeEntry(Player viewer, Guid slotId, string name, string? skinValue, string? skinSignature, int latency, int order, Component? display, bool hat)`: Adds a fake entry: an extra row of the tab list with its own name and skin, for one viewer. - `public void Header(Player viewer, Component? header, Component? footer)`: Sets the header and footer of the tab list for one viewer. Null (or an empty component) is no header or footer. - `public void RemoveFakeEntry(Player viewer, Guid slotId)`: Removes a fake entry added with `Tablist.AddFakeEntry`. - `public void RemoveObjective(Player viewer, string name)`: Removes an objective of one viewer, with its scores. - `public void ResetScore(Player viewer, string objective, string holder)`: Removes one holder's score from an objective of one viewer. - `public void Restore(Player viewer, Player target)`: Puts back the server's own tab values of one player for one viewer (name, listed, order, latency and game mode), whoever set them. - `public void Set(Player viewer, Player target, TabEntry entry)`: Changes how one viewer sees one player in the tab list. A field left null keeps what it was. - `public void SetObjective(Player viewer, int slot, string name, Component title)`: Defines an objective for one viewer: slot 0 is the tab list (the value after each name), slot 2 is below the name. - `public void SetScore(Player viewer, string objective, string holder, int score, Component? display, ScoreFormat format, Component? fixedFormat)`: Sets one holder's score on an objective of one viewer. ## TeleportCause (enum, events) ```csharp public enum TeleportCause : byte ``` Why a player is being teleported. - `Unknown` = 0: Unknown cause. - `Command` = 1: /tp or another command. - `Plugin` = 2: A plugin called Teleport. - `EnderPearl` = 3: An ender pearl. - `ChorusFruit` = 4: A chorus fruit. - `NetherPortal` = 5: A nether portal. - `EndPortal` = 6: An end portal. - `EndGateway` = 7: An end gateway. - `Spectate` = 8: Teleporting to an entity as a spectator. - `Respawn` = 9: Respawning. - `Dismount` = 10: Dismounting a vehicle. ## ClickAction (enum, chat) ```csharp public enum ClickAction : int ``` What happens when a player clicks a text component. - `OpenUrl` = 0: Opens a URL (the client asks for confirmation). - `RunCommand` = 1: Runs a command as the player (the value includes the slash). - `SuggestCommand` = 2: Puts text into the chat box. - `CopyToClipboard` = 3: Copies the value to the clipboard. - `ChangePage` = 4: Changes the page of a book. ## ClickEvent (struct, chat) ```csharp public readonly struct ClickEvent : IEquatable ``` A click event of a text component. - `public ClickEvent(ClickAction Action, string Value)`: A click event of a text component. - `public ClickAction Action { get; init; }`: What happens. - `public string Value { get; init; }`: Url / command / clipboard text / page number. - `public static ClickEvent ChangePage(int page)`: Change the book page. - `public static ClickEvent CopyToClipboard(string text)`: Copy text to the clipboard. - `public static ClickEvent OpenUrl(string url)`: Open a URL. - `public static ClickEvent RunCommand(string command)`: Run a command as the player ("/spawn"). - `public static ClickEvent SuggestCommand(string command)`: Suggest a command in the chat box. ## Component (class, ecs) ```csharp public sealed class Component ``` An immutable rich text component (vanilla "text component"): text, translatable text, keybind, score or selector content with a style and children. Every method that "changes" a component returns a new one. Strings convert implicitly to a literal text component (no formatting codes are interpreted, so user input is safe); use `Component.Legacy` for `&` codes and `Component.Parse` for MiniMessage-style tags. - `public IReadOnlyList Children { get; }`: The children appended after this component's own content. - `public string Content { get; }`: The literal text of a text component (empty for other kinds). - `public bool IsEmpty { get; }`: Whether this is the empty literal component without style or children. - `public TextColor TextColour { get; }`: The colour, `TextColor.None` if inherited. - `public static Component Empty { get; }`: The empty component. - `public Component Append(Component child)`: Appends a child component. - `public Component Append(string text)`: Appends literal text. - `public Component Bold(bool value = true)`: Sets bold. - `public Component Color(TextColor color)`: Sets the colour. - `public Component Font(string font)`: Sets the font resource key. - `public Component Insertion(string text)`: Text inserted into the chat box when the component is shift-clicked. - `public Component Italic(bool value = true)`: Sets italic. - `public Component Obfuscated(bool value = true)`: Sets obfuscated (random characters). - `public Component OnClick(ClickEvent click)`: Sets the click event. - `public Component OnHover(HoverEvent hover)`: Sets the hover event. - `public Component Strikethrough(bool value = true)`: Sets strikethrough. - `public string ToJson()`: Serialises to the vanilla JSON text component form (what the native layer parses). - `public string ToPlainText()`: The text of this component and its children without formatting (translatable components show their key). - `public override string ToString()` - `public Component Tooltip(Component text)`: Shortcut for a text tooltip. - `public Component Underlined(bool value = true)`: Sets underlined. - `public static Component FromJson(string json)`: Parses the vanilla JSON form (also a bare JSON string). - `public static Component FromJson(Utf8String json)`: Parses the vanilla JSON form from engine memory. - `public static Component Join(params Component[] parts)`: Concatenates components into one (an empty parent with the parts as children). - `public static Component Join(Component separator, IEnumerable parts)`: Joins with a separator. - `public static Component Keybind(string key)`: A component showing the client's key binding (e.g. "key.jump"). - `public static Component Legacy(string text, char prefix = '&')`: Converts text with `&` colour/format codes ("&aGreen &lbold") to a component. `&&` is a literal ampersand, `&#RRGGBB` a custom colour, `&r` resets. - `public static Component Parse(string markup)`: Parses MiniMessage-style tags: ``, `<#ff8800>`, ``, ``, ``, ``, ``, ``, ``, ``, and closing tags ``. Unknown tags are kept as text. Escape a literal `<` as `\<`. - `public static Component Score(string name, string objective)`: A scoreboard score. - `public static Component Selector(string selector)`: An entity selector (resolved when sent). - `public static Component Text(string text)`: A literal text component. - `public static Component Translatable(string key, params Component[] args)`: A translatable component (resolved by the client's language). - `public static implicit operator Component(string text)`: Literal text converts implicitly. ## HoverEvent (class, chat) ```csharp public sealed class HoverEvent ``` A hover event of a text component. - `public static HoverEvent ShowEntity(string entityTypeKey, Uuid uuid, Component? name = null)`: Shows an entity tooltip. - `public static HoverEvent ShowItem(string itemKey, int count = 1)`: Shows an item tooltip (item key and count; components are not part of v1). - `public static HoverEvent ShowText(Component text)`: Shows a text tooltip. ## TextColor (struct, chat) ```csharp public readonly struct TextColor : IEquatable ``` A named or RGB text colour. - `public bool IsSet { get; }`: Whether a colour is set. - `public int Rgb { get; }`: The colour as 0xRRGGBB (0 when unset). - `public static TextColor Aqua { get; }`: Aqua (&b). - `public static TextColor Black { get; }`: Black (&0). - `public static TextColor Blue { get; }`: Blue (&9). - `public static TextColor DarkAqua { get; }`: Dark aqua (&3). - `public static TextColor DarkBlue { get; }`: Dark blue (&1). - `public static TextColor DarkGray { get; }`: Dark gray (&8). - `public static TextColor DarkGreen { get; }`: Dark green (&2). - `public static TextColor DarkPurple { get; }`: Dark purple (&5). - `public static TextColor DarkRed { get; }`: Dark red (&4). - `public static TextColor Gold { get; }`: Gold (&6). - `public static TextColor Gray { get; }`: Gray (&7). - `public static TextColor Green { get; }`: Green (&a). - `public static TextColor LightPurple { get; }`: Light purple (&d). - `public static TextColor None { get; }`: No colour (inherit). - `public static TextColor Red { get; }`: Red (&c). - `public static TextColor White { get; }`: White (&f). - `public static TextColor Yellow { get; }`: Yellow (&e). - `public bool Equals(TextColor other)` - `public override bool Equals(object? obj)` - `public override int GetHashCode()` - `public override string ToString()`: The vanilla serialisation: a colour name for the 16 named colours, "#RRGGBB" otherwise. - `public static TextColor FromRgb(int rgb)`: A custom 24-bit colour. - `public static TextColor Parse(string text)`: Parses a vanilla colour name ("red") or "#RRGGBB". - `public static bool operator !=(TextColor a, TextColor b)`: Inequality. - `public static bool operator ==(TextColor a, TextColor b)`: Equality. ## TickAwaitable (struct, scheduler) ```csharp public readonly struct TickAwaitable : ICriticalNotifyCompletion, INotifyCompletion ``` The awaitable returned by `Scheduler.ToTick` / `Scheduler.ToAsync`. - `public bool IsCompleted { get; }`: Awaiter protocol: already on the target thread means no hop. - `public TickAwaitable GetAwaiter()`: Awaiter protocol. - `public void GetResult()`: Awaiter protocol. - `public void OnCompleted(Action continuation)`: Awaiter protocol. - `public void UnsafeOnCompleted(Action continuation)`: Awaiter protocol. ## TickTimes (struct, server) ```csharp public readonly struct TickTimes : IEquatable ``` The engine's tick time over a window of ticks (`Server.TickTimes`): average (MSPT), max, p95 and p50 in milliseconds, the ticks per second over the window's length, and the last tick. - `public TickTimes(double AverageMs, double MaxMs, double P95Ms, double P50Ms, double Tps, double LastMs)`: The engine's tick time over a window of ticks (`Server.TickTimes`): average (MSPT), max, p95 and p50 in milliseconds, the ticks per second over the window's length, and the last tick. - `public double AverageMs { get; init; }` - `public double LastMs { get; init; }` - `public double MaxMs { get; init; }` - `public double P50Ms { get; init; }` - `public double P95Ms { get; init; }` - `public double Tps { get; init; }` ## TimeSkipReason (enum, events) ```csharp public enum TimeSkipReason : byte ``` Why the day time jumped. - `Commands` = 0: /time set or /time add. - `NightSkip` = 1: Players slept through the night. - `Custom` = 2: A plugin. ## TitleTimes (struct, display) ```csharp public readonly struct TitleTimes : IEquatable ``` The timings of a title, in ticks (20 per second). - `public TitleTimes(int FadeIn, int Stay, int FadeOut)`: The timings of a title, in ticks (20 per second). - `public int FadeIn { get; init; }`: Time to fade in. - `public int FadeOut { get; init; }`: Time to fade out. - `public int Stay { get; init; }`: Time on screen. - `public static TitleTimes Default { get; }`: The vanilla defaults: 0.5 s in, 3.5 s on screen, 1 s out. - `public static TitleTimes From(TimeSpan fadeIn, TimeSpan stay, TimeSpan fadeOut)`: Creates timings from durations. ## Utf8String (struct, primitives) ```csharp public readonly ref struct Utf8String ``` Borrowed UTF-8 string (McStr). Only valid during the call/dispatch that provided it: call `Utf8String.ToString` to keep a copy. Event string fields (`Message`, `Username`, ...) are this type: borrowed UTF-8 memory owned by the engine. Only `ToString()` and `Contains()` allocate. - `public Utf8String(byte* data, long size)`: Wraps native memory (the engine does this; plugins receive these in events). - `public ReadOnlySpan Bytes { get; }`: The UTF-8 bytes (borrowed). - `public bool IsEmpty { get; }`: Whether the string is empty. - `public int Length { get; }`: Length in bytes. - `public bool Contains(string text, StringComparison comparison = StringComparison.Ordinal)`: Whether the decoded text contains `text` (allocates). - `public bool EqualsAscii(ReadOnlySpan utf8)`: Byte-wise equality with an ASCII/UTF-8 literal (no allocation). - `public override string ToString()`: Copies the text into a managed string. ```csharp Events.On((ref PlayerChatEvent e) => { if (e.Message.IsEmpty) return; // no allocation if (e.Message.EqualsAscii("!help"u8)) { /* ... */ } // byte compare, no allocation string copy = e.Message.ToString(); // allocates; safe to keep }); ``` ## Uuid (struct, primitives) ```csharp public readonly struct Uuid : IEquatable ``` Java-style UUID (two big-endian halves). - `public Uuid(ulong most, ulong least)`: Creates a uuid from its two halves. - `public readonly ulong Least;`: Least significant 64 bits. - `public readonly ulong Most;`: Most significant 64 bits. - `public bool Equals(Uuid other)`: Equality. - `public override bool Equals(object? obj)`: Equality. - `public override int GetHashCode()`: Hash code. - `public override string ToString()`: The canonical 8-4-4-4-12 text form. - `public static bool operator !=(Uuid a, Uuid b)`: Inequality. - `public static bool operator ==(Uuid a, Uuid b)`: Equality. ## Vec3d (struct, primitives) ```csharp public struct Vec3d : IEquatable ``` A 3D vector of doubles (positions, velocities). - `public Vec3d(double x, double y, double z)`: Creates a vector. - `public double X;`: Components. - `public double Y;`: Components. - `public double Z;`: Components. - `public double Length { get; }`: The length of the vector. - `public static Vec3d Zero { get; }`: The zero vector. - `public double DistanceTo(Vec3d other)`: The distance to another point. - `public bool Equals(Vec3d o)`: Equality. - `public override bool Equals(object? obj)`: Equality. - `public override int GetHashCode()`: Hash code. - `public override string ToString()`: Text form. - `public static bool operator !=(Vec3d a, Vec3d b)`: Inequality. - `public static Vec3d operator *(Vec3d a, double s)`: Scales the vector. - `public static Vec3d operator +(Vec3d a, Vec3d b)`: Component-wise sum. - `public static Vec3d operator -(Vec3d a, Vec3d b)`: Component-wise difference. - `public static bool operator ==(Vec3d a, Vec3d b)`: Equality. ## VersionConstraint (class, plugins) ```csharp public static class VersionConstraint ``` Compares dotted versions against constraints (`>=`, `>`, `=`, `<`, `<=`, `^` = same major and at least). - `public static bool Satisfies(string? constraint, string version)`: Whether a version satisfies a constraint. ## BlockState (struct, worlds) ```csharp public readonly struct BlockState : IEquatable ``` A block state: a block plus its properties, as the numeric id of the global palette. Blittable. - `public BlockState(uint id)`: Wraps a state id. - `public readonly uint Id;`: The numeric id in the vanilla block state registry (0 = air). - `public string BlockKey { get; }`: The block key without properties ("minecraft:oak_stairs"). - `public bool IsAir { get; }`: Whether this is the air block. - `public string Key { get; }`: The state as text, e.g. "minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]". - `public static BlockState Air { get; }`: Air (id 0). - `public bool Equals(BlockState other)` - `public override bool Equals(object? obj)` - `public override int GetHashCode()` - `public override string ToString()` - `public static BlockState Parse(string text)`: Parses "minecraft:stone" or "minecraft:oak_stairs[facing=north]" (missing properties take the block's defaults). - `public static bool TryParse(string text, out BlockState state)`: Parses a block state text. - `public static bool operator !=(BlockState a, BlockState b)`: Inequality. - `public static bool operator ==(BlockState a, BlockState b)`: Equality. ## BlockUpdateFlags (enum, worlds) ```csharp public enum BlockUpdateFlags : int ``` How a block change is propagated, the update flags of the game's `Level.setBlock` (combine with `|`). - `None` = 0: Nothing: the block is written silently (no neighbour update, clients are not told). Rarely what you want. - `Neighbors` = 1: Notify the neighbouring blocks (`neighborChanged`): redstone, doors, plants that lose their support ... - `Clients` = 2: Send the change to the players tracking the chunk. - `All` = 3: Neighbours and clients: the default of `World.SetBlock`. - `NoRender` = 4: Do not re-render the block on the clients (they still get the data). - `Immediate` = 8: Run the update on the client immediately instead of at the next frame. - `KnownShape` = 16: The neighbour shape updates (a fence connecting to its neighbours) are skipped: the new state is taken as it is. - `SuppressDrops` = 32: The old block does not drop its items when it is replaced (for example a flower on a block that is removed). - `MovedByPiston` = 64: The change is a piston moving the block. - `SkipShapeUpdateOnWire` = 128: Redstone wire: the shape updates of the wire's neighbours are skipped (the game uses it while it recalculates a wire network). - `SkipBlockEntitySideEffects` = 256: The old block entity is replaced without its removal side effects (no contents drop). - `SkipOnPlace` = 512: The new block's placement logic (`onPlace`) does not run. ## ChunkTicket (class, worlds) ```csharp public sealed class ChunkTicket : IDisposable, IPluginResource ``` A hold on a chunk: while the ticket exists the chunk is kept loaded (and ticking when a player is near enough, like any chunk). Tickets are counted: the chunk unloads after the last holder released it. Create tickets with `plugin.ChunkTickets`; all of a plugin's tickets are released when it unloads. - `public int ChunkX { get; }`: The chunk x. - `public int ChunkZ { get; }`: The chunk z. - `public bool IsActive { get; }`: Whether the ticket is still held. - `public bool IsLoaded { get; }`: Whether the chunk has finished loading (loading takes a few ticks, and a chunk that has to be generated takes longer). - `public World World { get; }`: The dimension. - `public void Dispose()`: Same as `ChunkTicket.Release`. - `public void Release()`: Gives the chunk back (it may unload after a grace period). Does nothing a second time. ## ChunkTickets (class, worlds) ```csharp public sealed class ChunkTickets ``` Chunk tickets of one plugin (`plugin.ChunkTickets`): they are released automatically when the plugin unloads or reloads. - `public const int MaxTicketsPerPlugin = 4096;`: How many chunks one plugin may hold at once (a ticket keeps a chunk in memory and ticking: a runaway loop must not load the whole world). - `public int ActiveCount { get; }`: How many tickets this plugin currently holds. - `public ChunkTicket Add(World world, int chunkX, int chunkZ)`: Asks for a chunk to be loaded and kept. - `public IReadOnlyList AddArea(World world, int centerX, int centerZ, int radius)`: Asks for the chunks of a square around a centre chunk (`radius` 0 is one chunk, 1 is 3 x 3). ## HeightMap (enum, worlds) ```csharp public enum HeightMap : int ``` Which surface `World.HighestBlockY` looks for. - `Surface` = 0: The highest block that is not air. - `OceanFloor` = 1: The highest block of the sea floor: not air and not water. - `MotionBlocking` = 2: The highest block that stops movement or holds a fluid (what rain and the sun see). - `MotionBlockingNoLeaves` = 3: Like `HeightMap.MotionBlocking` but leaves do not count. ## WeatherType (enum, worlds) ```csharp public enum WeatherType : int ``` The weather of a dimension. - `Clear` = 0: No precipitation. - `Rain` = 1: Rain (snow in cold biomes). - `Thunder` = 2: Rain with thunderstorms and lightning. ## World (struct, worlds) ```csharp public readonly struct World : IEquatable ``` A handle to a dimension (overworld, nether, end ...). Blittable. Dimensions live as long as the server. The members are split over several files by topic: blocks and block entities (`World.Blocks.cs`), entities and effects (`World.Entities.cs`), time, weather, spawn and chunk loading (`World.Environment.cs`). Everything here must be called on the tick thread. - `public World(int index)`: Wraps a dimension slot. - `public readonly int Index;`: The dimension slot (overworld 0, nether 1, end 2). - `public long DayTime { get; set; }`: The time of day clock in ticks since the world was created (a day is 24000 ticks: 0 is sunrise, 6000 noon, 12000 sunset, 18000 midnight). Setting it moves the sun for every player at once and fires `TimeSkipEvent` (reason `Custom`). - `public bool DayTimePaused { get; set; }`: Whether the day/night clock is stopped (`/time pause`). - `public float DayTimeRate { get; set; }`: How fast the day/night clock runs (1 = normal, 2 = twice as fast). - `public IReadOnlyList Entities { get; }`: Every entity of the dimension (players, mobs, item entities, experience orbs, projectiles, falling blocks ...). A snapshot: entities that spawn or vanish afterwards are not reflected. - `public long GameTime { get; }`: The number of ticks the dimension has been running (does not change with `/time set`; 20 per second). - `public long GeneratedChunkCount { get; }`: How many chunks this dimension has generated from scratch since the server started (chunks loaded from disk do not count): sample it twice to get a generation rate. - `public bool HasDayTime { get; }`: Whether this dimension has a day/night clock. In this server version only the overworld has one; the nether and the end have a fixed time. - `public int Height { get; }`: The build height: the number of block layers (384 in the overworld). The highest block y is `MinY + Height - 1`. - `public bool IsRaining { get; }`: Whether it is raining (also during a thunderstorm). - `public bool IsThundering { get; }`: Whether a thunderstorm is going on. - `public bool IsValid { get; }`: Whether the slot names a dimension of this server. - `public string Key { get; }`: The dimension key, e.g. "minecraft:overworld" (empty if the index is invalid). - `public int LoadedChunkCount { get; }`: How many chunks are loaded in this dimension right now. - `public int MaxY { get; }`: The highest block y of the dimension. - `public int MinY { get; }`: The lowest block y of the dimension (-64 in the overworld). - `public IReadOnlyList Players { get; }`: The players in the dimension. - `public int SeaLevel { get; }`: The sea level (63 in the overworld). - `public Location SpawnLocation { get; }`: The spawn point of the dimension (where new players appear and where players without a bed respawn) and its facing. Only the overworld has a world spawn the game keeps and saves; the other dimensions report a placeholder. - `public WeatherType Weather { get; }`: The weather. Only the overworld has weather in this server version; the other dimensions always report `WeatherType.Clear`. - `public static World End { get; }`: The end. - `public static World Nether { get; }`: The nether. - `public static World Overworld { get; }`: The overworld. - `public void AddDayTime(long ticks)`: Moves the day time forward (or back with a negative amount). - `public bool DestroyBlock(BlockPos pos, bool dropItems = true, Entity breaker = default)`: Breaks a block the way the game does (`Level.destroyBlock`): the break particles and sound, optionally the loot, and the block becomes air (or its fluid). A block entity's contents drop too. - `public Entity DropItem(BlockPos block, ItemStack stack)`: Drops a stack at the centre of a block (the way a broken block drops its loot). - `public Entity DropItem(Vec3d position, ItemStack stack, bool randomMotion = true)`: Drops an item entity with the full stack (data components included) at a position, like a broken block's drop: a small random sideways drift and a short pickup delay. `EntitySpawnEvent` fires with reason `Plugin` and can cancel it. - `public bool Equals(World other)` - `public override bool Equals(object? obj)` - `public bool Explode(Vec3d position, float power, bool setFire = false, bool breakBlocks = true, Entity source = default)`: Creates an explosion like a creeper's: it damages and pushes entities (with exposure and distance falloff), destroys blocks (dropping loot, with decay), optionally lights fires, and the clients get the particles and the sound. - `public BlockState GetBlock(BlockPos pos)`: Reads a block state. A position in an unloaded chunk or outside the build height reads as air (check `World.IsBlockLoaded` to tell); the chunk is not loaded by the read. - `public NbtCompound? GetBlockEntityData(BlockPos pos)`: The data of the block entity at a position (a chest's items, a sign's text, a spawner ...) in the form `/data get block` shows, including the `id` and the position keys. - `public override int GetHashCode()` - `public BlockInventory? GetInventory(BlockPos pos)`: The inventory of the block entity at a position (`BlockInventory.At`). - `public int HighestBlockY(int x, int z, HeightMap kind = HeightMap.MotionBlocking)`: The y of the highest block of a column (read from the chunk's height maps, so it is cheap). - `public bool IsBlockLoaded(BlockPos pos)`: Whether the block position is inside a loaded chunk and within the build height. - `public bool IsChunkLoaded(int chunkX, int chunkZ)`: Whether the chunk is loaded and ready (generated, lit, in memory). Blocks of other chunks read as air and cannot be changed. - `public int PlaySound(string sound, Vec3d position, SoundCategory category = SoundCategory.Master, float volume = 1f, float pitch = 1f)`: Plays a sound at a position to every player of the dimension who is close enough to hear it (16 blocks, or farther for a volume above 1). - `public bool SetBlock(BlockPos pos, BlockState state, BlockUpdateFlags flags = BlockUpdateFlags.All)`: Changes a block, like `/setblock`: the old block entity goes away, the new block's placement logic and the neighbour updates run (per `flags`), the clients are told. No player event fires (this is not a player placing the block); the engine's own block events (a fire spreading, a piston moving) are the ones the block triggers itself. - `public bool SetBlock(BlockPos pos, string state, BlockUpdateFlags flags = BlockUpdateFlags.All)`: Changes a block to the state parsed from text (`"minecraft:oak_stairs[facing=north]"`). - `public bool SetBlockEntityData(BlockPos pos, NbtCompound data, bool merge = true)`: Changes the data of a block entity, like `/data merge block` (or `/data modify` with the full compound). The clients receive the new data. - `public bool SetSpawn(BlockPos pos, float yaw = 0f, float pitch = 0f)`: Changes the spawn point (like `/setworldspawn`; the clients are told the new position of the compass and of the respawn, and it is saved with the world). - `public bool SetWeather(WeatherType weather, int? durationTicks = null)`: Sets the weather like `/weather`. The change reaches the players immediately and fires `WeatherChangeEvent`. - `public Entity SpawnEntity(string type, Vec3d position, NbtCompound? nbt = null, float yaw = 0f, float pitch = 0f)`: Spawns an entity of a type, like `/summon`. Mobs the engine simulates (zombies, cows, creepers ...) get their normal spawn logic: with no NBT their random attributes and equipment are rolled; with NBT the data is applied as given (`{Health:5f,CustomName:'"Bob"'}`). Item entities (`minecraft:item` with `Item:{id:"minecraft:diamond",count:3}`) and experience orbs (`minecraft:experience_orb` with `Value`) are supported too. The spawn fires `EntitySpawnEvent` (reason `Plugin`) where the entity kind has that event, and a handler may cancel it. - `public int SpawnParticle(string particle, Vec3d position, int count = 1, Vec3d offset = default, float speed = 0f, bool force = false)`: Shows a particle effect at a position to the players of the dimension within 32 blocks (512 with `force`). - `public override string ToString()` - `public bool TryGetBlock(BlockPos pos, out BlockState state)`: Reads a block state, saying whether the position was readable. - `public static bool operator !=(World a, World b)`: Inequality. - `public static bool operator ==(World a, World b)`: Equality. ## WrongThreadException (class, scheduler) ```csharp public sealed class WrongThreadException : InvalidOperationException ``` A game-facing API was called from a thread other than the server tick thread. Use `Scheduler.RunSync` to hop to the tick thread. - `public WrongThreadException(string api)`: Creates the exception for the named API.