Migrating from Paper / Bukkit
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 has the exact signatures.
Concept mapping
Section titled “Concept mapping”Plugin model
Section titled “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 |
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/<id>/, created on first use |
saveDefaultConfig(), getConfig() |
Config.Load(MyJsonContext.Default.MyConfig) returns Config<T> |
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 |
Bukkit.getServer() |
the static Server class |
Events
Section titled “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 |
Scheduling and threads
Section titled “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
Section titled “Commands and permissions”| Paper / Bukkit | Ramjet | Notes |
|---|---|---|
commands: in plugin.yml + onCommand(...) |
Commands.Register(Command.Literal("name")...) |
A command tree node; see 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
Section titled “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 |
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
Section titled “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
Section titled “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 |
BossBar |
BossBars.Create(name, title, color, style, progress), then AddViewer(player), Progress, Remove() |
boss bars and titles |
| titles and subtitles | player.ShowTitle(...), player.ShowSubtitle(...), player.ClearTitle() |
Menus and inventories
Section titled “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 |
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
Section titled “Chat and text”| Paper / Bukkit | Ramjet | Notes |
|---|---|---|
Adventure Component, MiniMessage |
Mc.Scripting.Text.Component |
Component.Text(...), Component.Legacy("&a..."), Component.Parse("<red>...") 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
Section titled “Services and plugin messaging”| Paper / Bukkit | Ramjet | Notes |
|---|---|---|
ServicesManager (register, get) |
Services.Register(instance, priority), Services.Get<T>(), Services.TryGet<T>(), Services.Changed<T>(...) |
inter-plugin services |
| plugin messaging channels | Channels.On("namespace:channel", (player, data) => ...), player.SendPluginMessage(channel, bytes) |
Packets and NBT
Section titled “Packets and NBT”| Paper / Bukkit | Ramjet | Notes |
|---|---|---|
| ProtocolLib packet listeners | Packets.On<T>(handler, mode, priority) |
PacketMode.Observe gets a copy after the fact; PacketMode.Intercept may change or cancel the packet (PacketEvent<T>.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
Section titled “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
Section titled “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
Section titled “Side by side”A listener
Section titled “A listener”// Paperpublic 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 onEnablegetServer().getPluginManager().registerEvents(new ChatListener(), this);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
Section titled “A scheduler task”// Papernew BukkitRunnable() { public void run() { getLogger().info("tick"); } }.runTaskTimer(this, 0L, 20L * 60);using Mc.Scripting;
namespace RepeatingLogSample;
public sealed class RepeatingLogPlugin : Plugin{ protected override void OnEnable() { Scheduler.RunEvery(20 * 60, () => Log.Info("tick")); }}A command
Section titled “A command”// 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;}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
Section titled “What surprises Paper developers”- 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): callToString()to keep one. - Allocation matters. The tick thread has no spare time for GC pauses; hot-path handlers must not allocate. See performance.
- State in fields does not survive a reload; state in components does. Plugins are rebuilt and reloaded constantly; design for it (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. - There is no
Playerobject to hold on to. APlayeris a handle. CheckIsOnlinewhen you come back to it later: a stale handle reads defaults and its actions do nothing. - Ticks, not milliseconds. Durations are ticks (20 per second), unless the method takes a
TimeSpan. - Commands are Brigadier-native. There is no
plugin.ymlcommand section, nolabel, and no hand-written tab completion for the basics. - No reflection in the API. No
@EventHandlerscanning and no annotation processing: registrations are explicit calls, which is also what makes ownership and unloading exact. - Argument order differs in places.
RunEvery(periodTicks, action, initialDelayTicks)takes the period first, where Paper’srunTaskTimertakes the delay first. Check the signature in the API reference before you port a call.