Loading…
Loading…
Select this tab to load…
Loading…
Loading…
Loading…
+ Add Minigame
NolensiaScript (.ns) is the DSL for writing minigames without Java — parsed once,
tree-walked, no compile step, editable straight from Minigames → Add/Edit (type
ns). Source of truth: JavaNolensia/NolensiaScriptRuntime/
and nolensiascript/README.md in the repo. Live examples:
nolensiascript/*.ns (ForkSpleef, Duels, TowerWars, KingOfTheTowers,
ThePirates, OneInTheQuiver).
game <Name> {
settings { ... }
on [phase] <event>(<params>) { ... }
every <n> tick { ... }
fn <name>(<params>) { ... } // parsed but not yet callable — see Known limitations
}
settings {
difficulty: hard // peaceful | easy | normal | hard (default: normal)
nat_regen: false // natural health regen (default: true)
mob_spawning: false // default: true
gamemode: survival // survival | adventure (default: adventure)
block_breaking: false // default: true
block_placing: false // default: true
saturation: 20 // permanent ambient SATURATION effect, amplifier as int
min_players: 2 // default: 1
max_players: 8 // default: 100
team_mode: false // default: false
debug: true // logs every dispatched event + cancel state (default: false)
}
Unquoted words in a settings value (hard, survival, ...)
are read as plain strings — don't quote them.
phase is optional and, when present, must be followed by another
identifier (the event name) — otherwise the word is parsed as the event name
itself. Three phases exist, matching the Java GameConfigurations split:
| Phase (default: always) | Active window | Lifecycle hook on entry |
|---|---|---|
| (omitted) → always | STARTING → CLOSING | on start() |
| prepare | STARTING → ACTIVE | — |
| active | ACTIVE → CLOSING | on active() |
A handler for the same event name can be declared once per phase — e.g.
on prepare player_death(player) and on active player_death(player)
are independent handlers that never both fire for the same death. Call
cancel (bare keyword, or cancel()) anywhere in a handler
body to cancel the underlying Bukkit event — it does not stop executing the rest
of the handler body, it just flags the result.
| Event | Params | Notes |
|---|---|---|
| start | — | Lifecycle hook, fires once when the always adapter activates (STARTING). |
| active | — | Lifecycle hook, fires once when the active adapter activates (ACTIVE). |
| player_quit | player | |
| player_damage | player, cause | cause is a string, e.g. "LAVA", "FALL", "ENTITY_ATTACK". |
| block_break | player | |
| food_change | player, level | level is a number 0–20. |
| item_drop | player | |
| item_pickup | player | |
| player_move | player | Fires every tick the player moves — keep handlers cheap. |
| player_death | player, killer | killer is null if not killed by another player (fall, void, environment, ...). |
| player_interact | player, is_right_click, item_material, face_location | item_material is the main-hand item's material name string. face_location is null unless a block was clicked. |
| player_burn | player, fire_duration | |
| trident_shoot | player | |
| trident_hit | trident, hit_player | hit_player is null if the trident hit a block, not a player. |
| arrow_hit | shooter, hit_player, arrow | shooter/hit_player are null if not applicable (dispenser-fired / hit a block). arrow is appended last, not first — kept backwards-compatible with scripts declaring just (shooter, hit_player). |
every 20 tick {
game.message("tick!")
}
Runs every n ticks for the lifetime of the match (registered once,
cancelled automatically when the match ends). Each timer body's exceptions are
isolated — one broken every block logs an error but never stops other timers or
itself on future ticks. tick/ticks are interchangeable
and optional after the number, but the block itself is required.
let x = 5
if x > 3 { game.message("big") } else { game.message("small") }
for p in active_players { p.heal() }
cancel
return
return some_value
No "else if" chaining — else always expects a
{ right after it, never another if. Write
else { if cond { ... } else { ... } } (a real nested if inside the else
branch) instead of the flat } else if cond { form — the flat form fails
to parse entirely, not just at that one line; a script using it won't load at all.
let declares a variable in the current block scope (shadows outer
scopes; for/if bodies get their own child scope). for x in <list-expr>
requires the iterable to evaluate to a list (players,
active_players, or game.locations(prefix)) — anything
else throws. There's no numeric range loop and no list literals — bulk operations
like game.trident_rain or game.fill_disk exist precisely
because that plumbing doesn't exist at the script level; reach for a
game.* bulk helper before trying to hand-roll a loop.
Arithmetic: + - * / (numbers only, except + also does
string concatenation whenever either side is a string). Comparison:
== != < > <= >=. Logical: and or not
(short-circuiting). Unary minus: -x. Comments are
// line comment only — no block comments.
Rebuilt before every single event dispatch, so they're always fresh:
game (the running match), spawn (a Location),
players (everyone currently in the match, including
eliminated/spectating), active_players (players still alive/active).
| Method | Returns | Description |
|---|---|---|
| message(text) | — | Broadcasts to everyone in the match. |
| sound(name) / sound(name, pitch, volume) | — | Plays a sound to everyone. |
| remove(player) | — | Removes a player from the active set (eliminates them). |
| set_winner(player) | — | Marks the winner and ends the match. |
| location(name) | Location or null | A named location from the map config. |
| locations(prefix) | list of Location | All map-config locations whose key starts with prefix. |
| quitted_player(player) | — | Marks a player as having quit (vs. eliminated). |
| is_active(player) | bool | Whether a player is still in the active set. |
| apply_kit(player, index) | — | Applies the map's assigned kit at index to the player. |
| spawn_entity(player, location, type, opts) | — | Spawns an entity from a spawn-egg-style config. |
| player_team(player) | string or null | Team name, only meaningful when team_mode: true. |
| delay_item(player, material, ticks) | — | Gives one material item after a delay. |
| place_block_if_air(location, material) | bool | Places a block only if the target is air. |
| set_block(location, material) | — | Unconditionally sets a block. |
| drop_item(location, material) / drop_item(location, material, amount) | — | Drops a real, pickable item entity on the ground — unlike player.add_item (straight into an inventory), e.g. a missed-shot arrow's replacement lying where it landed. |
| active_count() | number | Also available as the game.active_count property. |
| state() | string | Also available as the game.state property ("starting", "active", ...). |
| delay_respawn(player, ticks) | — | Teleports the player back to spawn and sets ADVENTURE mode after a delay. |
| fill_disk(location, radius, y_offset, material) | — | Fills a flat disk. |
| clear_disk(location, radius, y_offset, material) | — | Clears (air) a flat disk. |
| int_conf(key) / int_conf(key, default) | number | Reads a per-map custom int property. |
| string_conf(key) | string or null | Reads a per-map custom string property. |
| bool_conf(key) | bool | Reads a per-map custom bool property. |
| destroy_area(location, radius) | — | Destroys blocks in a radius. |
| elapsed_seconds() | number | Seconds since the match went ACTIVE. |
| add_stat(player, name, amount) | — | Adds to a per-player numeric counter (e.g. a kill count). Same accumulator the orchestrator reads for custom EXP bonuses. |
| get_stat(player, name) | number | Reads a per-player counter, defaulting to 0. |
| has_flag(name) / set_flag(name) | bool / — | One-shot boolean flags — the idiomatic way to gate a "fire once" block inside a timer (see ForkSpleef's sudden-death warning). |
| spawn_trident(location, seek) | — | Drops a falling trident; homes in on location if seek is true. |
| chance(percent) | bool | percent in [0, 100]; game.chance(50) is a coin flip. |
| random_active_player() | Player or null | |
| trident_rain(count) | — | Drops count tridents above random active players (skips liquid tiles, each independently seeking or not). |
| assign_indices(prefix, stat) | — | Assigns every active player a unique 0-based index into a named location-prefix list, stored as stat. Enumerate-and-assign lives here since the language has no loop counter. |
| location_at(prefix, index) | Location or null | The Nth location (0-based) from a named prefix list — pairs with assign_indices. |
| delay_teleport(player, location, ticks) | — | Like delay_respawn, but to an arbitrary location instead of the map's shared spawn. |
| build_walls(location, radius, height, y_offset, material) | — | Builds a hollow box perimeter (walls only, no floor/ceiling) — pair with fill_disk (same y_offset) for an enclosed pod. |
| replace_disk(location, radius, y_offset, match_material, replacement) | — | Like clear_disk, but replaces matching blocks with replacement instead of always AIR — e.g. reverting a build-phase floor back to WATER. |
| clear_walls(location, radius, height, y_offset, match_material) | — | Reverts exactly what build_walls (same radius/height/y_offset) put up, back to air. |
| starting_seconds_remaining() | number | Seconds left in the STARTING countdown (settings.starting_seconds) — drive your own sidebar/milestone feedback for a build phase instead of the engine's default per-second chat line. |
| ready_up(floor_seconds) | — | Fast-forwards the STARTING countdown down to floor_seconds — never lengthens it, no-op once already at/below the floor. |
| mark_ready(player, floor_seconds) | — | Marks one player ready; only once EVERY player has done so does it actually fast-forward the countdown (see ready_up) and broadcast. Tracked in Java, not a script loop — see Known limitations. |
| update_stat_sidebar(title, stat_key) | — | Renders a live "Name: N" leaderboard sidebar (sorted descending by stat_key) to everyone in the match. Re-render on every change. |
| assign_ring_positions(center, radius, y, prefix) | — | Spreads every player (not just active ones — runs during the build phase, before the active roster exists) evenly around a ring centered on center at height y. No hand-placed map locations needed. Stored as stats; read back with stat_location. |
| stat_location(player, prefix) | Location or null | Reconstructs a location previously stored by assign_ring_positions (or anything else writing <prefix>_x/_y/_z stats). |
| capture_raft(owner, chest_anchor, radius_xz, height_down, height_up) | — | Captures whatever's currently built around chest_anchor as owner's moveable raft — call once their build phase ends. See the Rafts section below. |
| move_raft(owner, dx, dz) | — | Moves owner's captured raft by one block (dx/dz each -1, 0, or 1), carrying along anyone standing on it. No-op if they have no raft. |
| raft_chest_location(owner) | Location or null | owner's raft's current (post-movement) chest location. |
A captured raft turns every non-air block in the scanned box into a real, moveable
entity rather than tracking world blocks directly. If two players' capture_raft scan
boxes overlap (e.g. raft_radius too small for the player count on a given map),
whichever player is captured FIRST converts the shared area's real blocks to
water/air before the second player's own scan ever runs — so the second player can
end up with an empty or partial raft (no captured helm/chest at all) through no fault
of their own build. Every capture_raft call logs "[Raft capture] <owner> scanned
(x,y,z) size WxHxD -> N blocks, chest=?, helm=?" — if a player reports "my helm
doesn't do anything," check this line for them first before assuming it's a code bug.
Regular hull/wall blocks become
invisible Shulkers (real collision, so players can stand and walk on
the raft) paired with a BlockDisplay rendering the original block —
destroyed by combat via a hit counter (3 hits). A block placed matching the chest
material (CHEST) or the helm material (OAK_FENCE_GATE,
configured in Java as MinigameHandler.HELM_MATERIAL) instead becomes an
invisible ArmorStand — fully invulnerable to combat, no walking
collision. The only way to lose a chest/helm is to destroy whatever raft block was directly
supporting it — once unsupported it accelerates downward and is destroyed once it's
sunk a bit into real water (not the instant it touches the surface). A chest/helm
built flush into the raft's own floor row is always considered supported (nothing was
ever captured below the floor to compare against) — build it one block up on a raised
post if you want it to actually be sinkable.
Every block's world position — hull, chest, and helm alike — is fully recomputed from the raft's own state (a center point + a heading + each block's fixed local grid index), never read back from the entity and incrementally nudged. Nothing can ever knock an individual block sideways independent of the raft as a whole this way, by construction. A full re-render (every block re-teleported, a real packet each) only happens on ticks where the raft actually moved/turned/fell, not unconditionally every tick — an idle raft costs nothing. Physics itself (turning/accelerating/falling) runs every single game tick. BlockDisplay client-side interpolation still smooths the hull's own visible movement — deliberately tuned LONGER than the tick gap between updates, not matched to it, so a new update restarts the tween toward the latest target before the previous one finishes; the client is then always smoothly gliding toward a moving point instead of snap-arriving each update and sitting idle, which still reads as a stutter even with interpolation on if the duration is too tight.
Right-clicking the helm's dedicated Interaction entity (Mojang's own purpose-built "invisible precisely-clickable proxy" type, sized to a full block, mounted as a passenger of the helm same as the decorative wheel — clicking the helm's own ArmorStand hitbox directly was nearly impossible, even non-small ArmorStands are only ~0.5 blocks wide, buried among full-size hull Shulkers) claims piloting — this is NOT a mount. Three mount-based designs were tried in a row here (ride the helm directly, ride a separate seat mounted on the helm, back to riding the helm directly) chasing a click-hitbox issue, then a "bounces you very far away" report, then a "bounces forward off the helm while sailing" report that survived even after removing the nested seat — every one of those symptoms has "mount an entity to a Bukkit vehicle that gets teleported every tick" as its one common thread, and Minecraft's client-side smoothing for a ridden entity is native, specialized behavior real vehicle types (boat, horse, minecart) get that a generic mob standing in for one does not — a client-side rendering limitation no amount of server-side tuning fixes. Piloting a raft never actually needed the player mounted at all: reading their held W/A/S/D (PlayerInputEvent fires for any player regardless of mount state) and carrying them along with the ship (the very next paragraph, which every on-deck passenger already gets) both work without it. Piloting is tracked as a plain UUID (PaperRaft.pilotId) — claimed by clicking the Interaction entity, auto-released the moment the pilot wanders more than PILOT_RANGE (4 blocks) from the helm's current position or goes offline (checked every raft-tick) — no entity relationship, no vehicle events, nothing for a mount chain to desync. W accelerates forward, A/D ease the raft's heading into a turn — momentum-based like forward speed, not an instant snap — S actively brakes to a stop (never reverses), and it drifts to a stop on its own with no input held. Each block's own display model rotates in place around its own center right along with the raft's heading too (a real Transformation per block per render, using the same rotation formula as position so the two can't disagree), not just its position.
Anyone standing on deck — pilot included, no special-casing needed now that piloting
isn't a mount — is carried along via a velocity nudge each time the raft moves, not a
hard teleport, so they can still walk around/jump/step off freely instead of being
pinned in place. That velocity is a FLAT delta matching only the raft's own pivot
movement that tick, the same for every passenger regardless of where they're standing
on it, deliberately NOT derived from each passenger's own position (an earlier
version computed each passenger's exact offset from the pivot and chased the
resulting absolute target position via velocity every tick — that's a feedback loop:
any one-tick lag between setting velocity and Minecraft actually applying the
resulting move makes the next tick's catch-up delta bigger, not smaller, and that's
what was actually behind reports of getting launched a long way; a per-tick velocity
cap didn't fix it either, since capped-but-sustained for a couple seconds is still a
long way). The accepted trade-off is that passengers standing well off-center don't
get the small extra tangential push a full rotation would technically owe them while
turning hard — self-correcting once the turn eases, and a small price for removing
that whole bug class outright. No script-side handling needed —
this, hit-counting, and the fall/sink behavior are wired entirely in Java
(MinigameHandler/RaftChestHitListener), not exposed as
separate script builtins beyond capture_raft/move_raft/raft_chest_location
(move_raft is still just a discrete grid-step nudge — useful for other games doing
simple block-raft movement, but ThePirates itself no longer calls it at all now that
sailing is continuous).
Raft-block Shulkers are deliberately NOT shielded from EntityTeleportEvent anymore — a belt-and-suspenders cancellation aimed at Shulker's own evasion AI turned out to also silently block the raft's own render teleports for every hull block, since Bukkit's plugin-triggered Entity#teleport() fires that same event for a Shulker. Real damage that would trigger vanilla evasion is already cancelled upstream and setAI(false) suppresses the AI path anyway.
| Method | Returns | Description |
|---|---|---|
| heal() | — | |
| set_velocity(x, y, z) | — | |
| clear_inventory() | — | |
| damage(amount) | — | |
| damage_from(amount, attacker) | — | Like damage, but attributes the hit to attacker so player_death's killer param resolves correctly even if this is the fatal hit. |
| set_saturation(level) | — | |
| throw_tnt(fuseTicks) | — | |
| reduce_hand() | — | Reduces the main-hand item stack by one. |
| add_item(material) / add_item(material, amount) | — | |
| add_enchanted_item(material, amount, unbreakable, "NAME:LEVEL,NAME:LEVEL") | — | Enchant string is comma-separated NAME:LEVEL pairs. |
| teleport(location) | — | |
| set_gamemode(mode) | — | "survival" | "creative" | "spectator", anything else → adventure. |
| set_food(level) | — | Alias of set_saturation. |
| send(text) | — | Sends a chat message to just this player. |
| effect(type, ticks, amplifier) | — | Applies an ambient potion effect. |
| is_on_ground() | bool | |
| is_flying() | bool | |
| set_allow_flight(allow) | — | Grants/revokes flight capability outright (a real flight toggle, same as creative — not just "not falling"). Needed for anything standing on an entity-based structure (see Rafts) rather than a real block, since Paper's ground check doesn't count entity collision and will eventually kick the player for "floating". Trade-off: also lets the player double-jump into real flight in survival. |
| facing() | string | 4-way cardinal snap (NORTH/SOUTH/EAST/WEST) from yaw — NOT Bukkit's own 8-way getFacing(). |
| name() | string | |
| location() | Location | |
| block() | string | Material name of the block the player is standing in. |
player also exposes .name, .on_ground, .flying,
.facing as plain properties (no parentheses) — mostly equivalent to the
method forms above.
Location: x(), y(), z(), block()
(material name of the block there), and offset(dx, dy, dz) (returns a
new Location shifted by a relative block offset — x(), y(),
z(), and block() all reflect the offset position).
.x, .y, .z are also available as properties.
Trident (inside trident_hit only): location() and
remove() — also exposed as the .location property. Arrow
(inside arrow_hit only) is the same shape: location() and
remove().
- fn is parsed but not wired up. Declarations are collected by
the interpreter but nothing ever looks them up — there's also no call syntax for
a bare identifier (
myFn(x)) in the grammar, only<object>.<method>(...). Don't rely on fn yet; repeat logic inline or push it into a new built-in if it needs to be shared. - No numeric ranges or list literals. Bulk operations are built-in game.* helpers rather than expressible as a script-level loop.
- A for loop can't accumulate state into an outer-scope variable. Each iteration's body runs in its own fresh child Environment, so a running total/flag declared before the loop is never visible to mutate from inside it. Anything that needs "have ALL of these happened yet" (e.g. mark_ready's "is everyone ready") has to be tracked in Java instead.
- Each for iteration's exceptions are isolated the same way every timers are — one bad iteration is logged and skipped, it doesn't abort the rest of the loop or the handler.
- return outside of a handler is a no-op that just unwinds the current block — there is no user-callable function to return a value to.
- Comparing a number to anything other than an identical number/string/boolean
via
==/!=always returns false — no numeric-vs-string coercion. - A script error thrown inside a handler is logged (with a stack trace if
debug: true) and the event resolves as not cancelled — it does not crash the match.
game ForkSpleef {
settings {
debug: true
}
on start() {
// Nothing to set up during prepare phase
}
on active() {
for p in active_players {
p.add_item("TRIDENT")
}
}
every 20 tick {
if game.elapsed_seconds() >= 150 and not game.has_flag("sudden_death_warned") {
game.set_flag("sudden_death_warned")
game.message("Sudden death in 30 seconds!")
}
if game.elapsed_seconds() >= 180 {
if not game.has_flag("sudden_death_started") {
game.set_flag("sudden_death_started")
game.message("SUDDEN DEATH!")
game.sound("entity.lightning_bolt.thunder", 1.0, 1.0)
}
game.trident_rain(2)
}
}
on prepare player_death(player) {
cancel
game.delay_respawn(player, 1)
}
on active player_death(player) {
cancel
player.set_gamemode("spectator")
if game.is_active(player) {
game.message(player.name() + " has been ELIMINATED!")
game.remove(player)
game.sound("entity.player.attack.crit", 1.0, 1.0)
}
}
on active trident_shoot(player) {
game.delay_item(player, "TRIDENT", 20)
}
on active item_pickup(player) { cancel }
on active trident_hit(trident, hit_player) {
if hit_player != null {
cancel
game.destroy_area(hit_player.location(), 1)
} else {
game.destroy_area(trident.location(), 1)
}
trident.remove()
}
on player_move(player) {
let block = player.block()
if block == "WATER" or block == "BUBBLE_COLUMN" {
player.damage(9999)
}
}
on player_damage(player, cause) {
if cause == "LAVA" { player.damage(9999); cancel }
if cause == "FALL" { cancel }
if cause == "ENTITY_ATTACK" { cancel }
}
on item_drop(player) { cancel }
on block_break(player) { cancel }
on item_pickup(player) { cancel }
on player_burn(player, dur) { cancel }
on food_change(player, level) {
player.set_saturation(0)
cancel
}
on player_quit(player) {
player.set_gamemode("spectator")
game.message(player.name() + " has been removed from the game!")
game.quitted_player(player)
}
}
Drag-and-drop, Scratch-style editor for NolensiaScript, built on
Google
Blockly (the engine Scratch itself is built on). Blocks and .ns text
convert both ways: build with blocks and hit "Blocks → Code", or paste/load an
existing script and hit "Code → Blocks" to load it into the workspace. See
NolensiaScript
Docs for what every block actually does.
+ Add Map
Loading…
Loading…
Loading…
Metadata mirror of what's defined in LobbyPaper's Title/Pet/Morph code — bookkeeping only, not read by the game servers. Grant/revoke a specific player's cosmetics from Users → Edit.
Loading…
+ Add / Edit Cosmetic
Loading…
Optional announcement shown at the top of the public store page (e.g. a sale, an event).
These drive both the public store page and checkout — no code deploy needed to add/edit/retire one.
Loading…
+ Add / Edit Product
Every completed store checkout, recorded when the Stripe webhook fires. If a purchase doesn't show up here after a successful checkout, the webhook isn't reaching the server — check /var/log/nolensia-store.log and the endpoint's config in the Stripe Dashboard.
Loading…
Loading…
Loading…
Loading…
Loading…
Loading…
Loading…
Loading…
Loading…
Maintenance / Cleanup
Loading…
Select a table.
Loading…
Manage in-game /verify TOTP for staff members.
Loading…