Skip to content

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.

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
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
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
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)
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
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
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()
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
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)
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)
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)
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)
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
// 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
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);
}
}
// Paper
new BukkitRunnable() { public void run() { getLogger().info("tick"); } }.runTaskTimer(this, 0L, 20L * 60);
RepeatingLog.cs
using Mc.Scripting;
namespace RepeatingLogSample;
public sealed class RepeatingLogPlugin : Plugin
{
protected override void OnEnable()
{
Scheduler.RunEvery(20 * 60, () => Log.Info("tick"));
}
}
// 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
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;
})));
}
}
  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.
  3. 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.
  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 before you port a call.