Skip to content

Porting a Bukkit, Spigot, Paper or Folia plugin by hand

Status: Implemented as a mapping to the API that exists; rows marked planned name CLight features that aren’t in the game yet.

Celestial’s server does not run, convert or translate Java plugins (.jar files for Bukkit, Spigot, Paper or Folia), and there is no conversion tool. This guide maps plugin concepts to CLight so you can rewrite your own plugin. Read Porting existing mods to CLight: overview first.

A CLight server plugin is an ordinary CLight mod with "side": "server" and "sync": "server_only". Vanilla clients can join, just as with Paper. Lua is the natural language for most plugins: no build step, hot reload in dev mode, sandboxed, and fast enough for plugin logic (Server plugins, A server plugin (vanilla clients welcome)).

Project and metadata

Bukkit / Paper CLight
plugin.yml / paper-plugin.yml clight.mod.json with side: server, sync: server_only
name id (lowercase) and name
main (the plugin class) runtime.entry (Lua script) or the code entry point
api-version api (the CLight API major) and game (a game version range)
depend, softdepend, loadbefore depends, optional, load.before
commands: section Registered in code as command trees
permissions: section Command permission levels and names on the nodes (registered permission nodes planned)
plugins/ folder mods/ on the server

Listeners and events

Bukkit / Paper CLight
@EventHandler on a Listener cl.on("event.name", fn, opts)
EventPriority.LOWEST … HIGHEST, MONITOR priority = "first" … "last", "monitor" (note: lowest runs first in both)
ignoreCancelled = true the default (cancelled events skip later handlers); receive_cancelled = true to see them
PlayerJoinEvent, PlayerQuitEvent player.join, player.leave
AsyncPlayerChatEvent / AsyncChatEvent player.chat (message mutable, cancellable)
PlayerCommandPreprocessEvent player.command
BlockBreakEvent, BlockPlaceEvent block.break (xp, drop_items mutable), block.place
PlayerInteractEvent player.interact_block, player.interact_item
EntityDamageEvent, EntityDeathEvent entity.damage (amount mutable), entity.death
PlayerRespawnEvent player.respawn
EntityDamageByEntityEvent (attacker) entity.damage (attacker), player.attack_entity
InventoryClickEvent on your own GUIs the menu’s on_click
Move, teleport, world change, inventory open/close, async pre-login planned

Scheduling and threads

Bukkit / Paper CLight
runTask, runTaskLater cl.sched.run_server(delay, fn) (C++ clight::after)
runTaskTimer cl.sched.repeat_server(delay, period, fn) (C++ clight::every)
BukkitTask.cancel() task:cancel()
Waiting inside a task cl.spawn(function() ... cl.sleep_ticks(n) ... end)
Async tasks, Folia region schedulers planned (worker jobs); one mod’s callbacks never run on two threads at once

Commands

Bukkit / Paper CLight
CommandExecutor + plugin.yml cl.cmd.literal(...) trees in server.command_register
Brigadier commands (Paper) The same model: literals, typed arguments, executes, requirements
Argument parsing by hand Typed arguments: word, string, greedy, integer, double, bool, player, players, entity, entities (selectors included)
sender.hasPermission on a command requires_permission(level or name) on the node
TabCompleter Vanilla suggestions for typed arguments; custom suggestions planned
Bukkit.dispatchCommand(console, ...) cl.cmd.run("...")

Players, worlds and entities

Bukkit / Paper CLight
player.sendMessage, sendActionBar, sendTitle p:send_message, p:send_action_bar, p:send_title
player.getInventory().addItem p:give(id, n)
removeItem, contains p:take(id, n), p:has_item(id, n), p:count_item(id)
getItemInMainHand p:held_item("main") (a live slot)
player.getGameMode p.gamemode
player.getHealth / setHealth p.health / p:set_health(h)
teleport p:teleport(cl.vec(x, y, z)) within the level; across dimensions cl.cmd.run("execute in ... run tp ...")
Bukkit.getOnlinePlayers cl.entity.server_players()
Bukkit.broadcastMessage cl.entity.server_broadcast(text)
world.getBlockAt / setType level:get_block(pos) / level:set_block(pos, "minecraft:stone")
player.playSound cl.sound.play_to_player(p, pos, sound, category, volume, pitch)
Kick, ban, flight, food, XP, potion effects, spawning entities, world creation planned

Inventories and GUIs

Bukkit / Paper CLight
Bukkit.createInventory(null, 27, title) + click handling cl.menu.open(player, { kind = "chest_9x3", title = ..., on_click = ... }) (locked by default)
inventory.setItem(slot, item) menu:set_item(slot, "minecraft:diamond")
Custom item names and lore in menus planned (menu slots hold an item and a count)

Data storage

Bukkit / Paper CLight
config.yml cl.config.open("server") (JSON, typed options with comments)
Per-player YAML or SQLite files Player attachments (p:set_data), or files in data/ (cl.fs, cl_fs_*)
PersistentDataContainer attachments (e:data / e:set_data, typed with cl.attachment)

Permissions, economy and other plugin services

Bukkit / Paper CLight
Permission checks in commands Node requirements (requires_permission) and ctx:permission_level()
Vault (economy, permissions, chat) planned (services); until then, keep economy data in your own plugin
Plugin messaging (BungeeCord channel) planned (legacy channel names); mod payloads work between a Celestial server and client

Text

Bukkit / Paper CLight
Adventure components, ChatColor cl.text.literal("Hi"):color(0xFFAA00):bold(), click, hover, cl.text.translatable
MiniMessage, legacy & codes planned

Scoreboards, boss bars, tab list

Planned. Until then, cl.cmd.run("scoreboard ..."), bossbar and title commands do the job from code.

Packets and server internals

NMS access and packet interception have no equivalent. Use the published API, payloads to Celestial clients, and vanilla commands.

A porting example, in words

A “Spawn Protection” plugin cancels block breaking and placing within 30 blocks of spawn for non-operators, and has /spawnprot radius <n>.

  1. Manifest: side: server, sync: server_only, runtime.type: lua.
  2. Config: local config = cl.config.open("server"), config:define_i32("radius", 30, 0, 1000).
  3. Subscribe to block.break and block.place with priority = "first". In the handler: if event.world:dimension() == "minecraft:overworld" and the position is within the radius, and the player isn’t an operator (keep your own list, or check in a command), event:cancel().
  4. Register /spawnprot radius <n> with an integer argument and requires_permission(2), setting the config value and saving it.
  5. Test with a vanilla client: the cancelled break puts the block back for the client, exactly as vanilla does in adventure mode.