Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ config/
data/
logs/
levels/
texpacks/
plugins/.removed/

# editors and OS files
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## Unreleased

### Added
- MCGalaxy's command shortcuts and aliases (`lib/commands/shortcuts.js`), including ones that carry arguments,
like `/zadd` for `/zone add` or `/cw` for `/cuboid wire`. `/v` is now `/paste` and `/pl` is `/place`, as in
MCGalaxy (`/vanish` keeps `/hide`, `/plugins` keeps `/plist`).
- `/main <level>` sets the main level; `/autoload` and `autoloadLevels` load more levels at startup.
- New commands from MCGalaxy: `/vote` (`/yes`, `/no`), `/timer`, `/pronouns` (shown in `/whois`), `/quit`,
`/ragequit`, `/alts`, `/like` and `/dislike` (shown in `/mapinfo`), `/replacenot`, `/triangle`, `/delete`
and `/static`.
- `biomes` generator: plains, forests, deserts and snowy mountains with rivers, caves and ores.
- Texture packs in `texpacks/` are served over HTTP on the game port; `/texture mypack.zip` uses them.
`publicAddress` sets the address players' clients download from.
- Pterodactyl: the port is read from `SERVER_PORT`, the allocation address is used for texture packs, and
`/restart` lets the panel start the server again. Setup steps in the README.

## 2.1.0 (2026-10-05)

### Added
Expand Down
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<p align="center"><img src="docs/images/banner.png" alt="MCScript, a ClassiCube server written in JavaScript" width="100%"></p>

# MCScript

[![CI](https://github.com/perronosaurio/MCScript/actions/workflows/main.yml/badge.svg)](https://github.com/perronosaurio/MCScript/actions/workflows/main.yml)
Expand All @@ -17,7 +19,7 @@ classicube.net and BetaCraft.
- [Installing](#installing)
- [Your first server](#your-first-server)
- [Letting other people join](#letting-other-people-join)
- [Keeping it running](#keeping-it-running)
- [Keeping it running](#keeping-it-running) (including [Pterodactyl](#pterodactyl))
- [Updating](#updating)
- [Features](#features)
- [Bundled plugins](#bundled-plugins)
Expand Down Expand Up @@ -123,6 +125,25 @@ pm2 save && pm2 startup

The console isn't available as a service. Use the web panel plugin or join the game to run commands.

### Pterodactyl

MCScript runs on Pterodactyl panels (and others like it) with a Node.js egg, such as the generic Node.js egg from
[parkervcp/eggs](https://github.com/parkervcp/eggs).

1. **Create the server** with the Node.js egg. In **Startup**, pick a Docker image with Node.js 22 or newer
(for example `nodejs_22` or `nodejs_24`) and set the main file to `index.js`.
2. **Upload the server.** In **Files**, upload the release zip, right click it and choose **Unarchive**, then move
the contents of the `MCScript-...` folder to the top folder, so `index.js` and `package.json` sit next to
each other at `/home/container`.
3. **Start it.** The port comes from the panel's allocation (`SERVER_PORT`); you don't need to set it. The panel
console works like the normal console: type `/rank Name Owner` or any other command there.
4. Edit `config/server.json` from **Files** to set the name, MOTD and `owners`, and restart from the panel.

`/restart` exits and lets the panel start the server again. The panel restarts crashed servers by default; if
yours doesn't, use the panel's Restart button. Texture packs in `texpacks/` use the allocation's address
automatically; if the panel shows the allocation as `0.0.0.0`, set `publicAddress` in `config/server.json` to
the address players connect to (for example `play.example.com:25565`).

### Backups

Levels are backed up automatically to `levels/backups/`. `/restore` brings one back in-game. For a complete
Expand Down Expand Up @@ -156,13 +177,21 @@ Protocol
- Clients without an extension get sensible fallbacks, for example a replacement block for custom blocks.

Levels
- Several loaded at once. Generators: `flat`, `empty`, `pixel`, `space`, `ocean`, `island`, `terrain` (seeded).
- Several loaded at once. Generators: `flat`, `empty`, `pixel`, `space`, `ocean`, `island`, `terrain` and `biomes`
(plains, forests, deserts and snowy mountains with rivers, caves and ores). All seeded.
- `/main <level>` picks the level players join in; `/autoload` keeps more levels loaded from startup.
- ClassicWorld `.cw` files. MCGalaxy `.lvl` and the old `.dat` can be imported with `/import`.
- Autosave, periodic backups with `/restore`. Deleted levels go to `levels/deleted`.
- Per level: sky, fog, cloud and light colors, texture pack, weather, edge blocks, water level, MOTD hack flags
(`-hax +fly`), build and visit ranks, owners, physics and lockdown.
- Block history is written to disk, so `/about` and `/undoplayer` still work after a restart.

Texture packs: put a `.zip` in `texpacks/` and `/texture mypack.zip` serves it to players from the server itself,
on the game port. No web host needed (set `publicAddress` to your domain or IP).

MCGalaxy habits work: the shortcuts and aliases from MCGalaxy are there (`/z`, `/ld`, `/gen`, `/wcopy`, `/zadd`,
`/cw`, `/v`, `/fg`, `/h`...).

Custom blocks: `/gb` for every level and `/lb` for one level. Name, textures per face, shape, collision, speed,
sound, light, transparency, fog and fallback block. Presets included: invisible barrier, lamp, glass pane,
ladder, carpet, slabs, speed pad and more.
Expand All @@ -175,10 +204,10 @@ anti-grief.

| Plugin | Commands |
| --- | --- |
| core-essentials | `/spawn /main /tp /tphere /back /ascend /descend /tpa /kill /msg /reply /ignore /me /say /announce /rules /faq /news /view /players /whois /top /search /blocks /pclients /whonick /serverinfo /ping /where /time /model /modelscale /entityrot /skin /nick /color /title /tcolor /hold /reach /fly /afk /clear /roll /8ball /hug /high5 /send /inbox /loginmessage /logoutmessage /emotes /ccols /lastcmd` |
| core-moderation | `/rank /promote /demote /ranks /rankinfo /temprank /kick /warn /ban /unban /baninfo /banedit /banip /unbanip /xban /bans /mute /unmute /freeze /vanish /follow /p2p /patrol /moveall /moderate /voice /opchat /adminchat /rankmsg /report /whitelist /playeredit /limit /sudo /oprules` |
| core-worlds | `/newlvl /goto /levels /load /unload /save /deletelvl /copylvl /renamelvl /resizelvl /import /mapinfo /map /setspawn /backup /restore /lockdown /reload /fixgrass /unflood /env /weather /texture` |
| core-building | `/cuboid /replace /replaceall /line /sphere /spheroid /torus /pyramid /hollow /outline /fill /tree /maze /rainbow /drill /center /place /copy /paste /mirror /spin /write /mark /bind /mode /undo /undoplayer /redo /paint /about /measure /calculate` |
| core-essentials | `/spawn /main /tp /tphere /back /ascend /descend /tpa /kill /msg /reply /ignore /me /say /announce /rules /faq /news /view /players /whois /top /search /blocks /pclients /whonick /serverinfo /ping /where /time /model /modelscale /entityrot /skin /nick /color /title /tcolor /hold /reach /fly /afk /clear /roll /8ball /hug /high5 /send /inbox /loginmessage /logoutmessage /emotes /ccols /lastcmd /vote /yes /no /timer /pronouns /quit /ragequit` |
| core-moderation | `/rank /promote /demote /ranks /rankinfo /temprank /kick /warn /ban /unban /baninfo /banedit /banip /unbanip /xban /bans /mute /unmute /freeze /vanish /follow /p2p /patrol /moveall /moderate /voice /opchat /adminchat /rankmsg /report /whitelist /playeredit /limit /sudo /oprules /alts` |
| core-worlds | `/newlvl /goto /levels /load /unload /save /deletelvl /copylvl /renamelvl /resizelvl /import /mapinfo /map /setspawn /backup /restore /lockdown /reload /fixgrass /unflood /env /weather /texture /like /dislike /autoload` |
| core-building | `/cuboid /replace /replaceall /line /sphere /spheroid /torus /pyramid /hollow /outline /fill /tree /maze /rainbow /drill /center /place /copy /paste /mirror /spin /write /mark /bind /mode /undo /undoplayer /redo /paint /about /measure /calculate /replacenot /triangle /delete /static` |
| core-blocks | `/gb /lb` |
| warps | `/warp /home` |
| zones | `/zone` (protected areas, shown in the client) |
Expand Down
6 changes: 4 additions & 2 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ write its own copy over your changes on shutdown.
| `extraHeartbeats` | `[]` | More server lists to announce the server on, such as BetaCraft. See below |
| `allowWebClient` | `true` | Accept the browser client (WebSocket on the same port) |
| `trustProxy` | `false` | Use the `X-Forwarded-For` header as the player IP. Only turn this on behind your own reverse proxy |
| `mainLevel` | `main` | Level players spawn in |
| `mainLevel` | `main` | Level players spawn in. `/main <level>` changes it |
| `autoloadLevels` | `[]` | More levels to load at startup. `/autoload <level>` adds one |
| `defaultRank` | `Guest` | Rank given to new players |
| `owners` | `[]` | Names that always get the highest rank |
| `welcomeMessage` | | Sent to players when they join. `{player}` is replaced with their name |
| `rules` | three lines | Shown by `/rules` |
| `defaultTexture` | `""` | Texture pack URL for levels that don't set their own |
| `publicAddress` | `""` | Your server's domain or IP (and `:port` if needed), used to serve packs from `texpacks/`. On Pterodactyl it is detected |
| `autosaveMinutes` | `5` | `0` turns autosave off |
| `backupMinutes` | `30` | How often changed levels are backed up. `0` turns backups off |
| `backupsToKeep` | `10` | Backups kept per level |
Expand All @@ -46,7 +48,7 @@ containers and hosting panels.

| Variable | Key |
| --- | --- |
| `PORT` | `port` |
| `PORT` or `SERVER_PORT` | `port` (`SERVER_PORT` is what Pterodactyl sets) |
| `HOST` | `host` |
| `SERVER_NAME` | `name` |
| `MOTD` | `motd` |
Expand Down
2 changes: 2 additions & 0 deletions docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ ctx.command({
})
```

An alias can carry arguments: with `aliases: ['zadd add']`, typing `/zadd x` runs the command as `/zone add x`.

`run` can be `async` (for example to wait for the player to mark blocks). Owners can change the rank of any
command with `/cmdset <command> <rank>`.

Expand Down
Binary file added docs/images/banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions lib/commands/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ module.exports = function registerCoreCommands (server) {

commands.register({
name: 'plugins',
aliases: ['pl', 'plist'],
aliases: ['plist'],
category: 'server',
usage: '/plugins',
description: 'Lists loaded plugins',
Expand Down Expand Up @@ -276,8 +276,8 @@ module.exports = function registerCoreCommands (server) {
server.broadcast(`&e${reason}...`)
await server.stop(reason)
if (server.config.exitOnStop === false) return
// under systemd, pm2 or a start script loop the supervisor starts us again
if (process.env.MCSCRIPT_SUPERVISED) process.exit(0)
// under systemd, pm2, a start script loop or Pterodactyl the supervisor starts us again
if (process.env.MCSCRIPT_SUPERVISED || process.env.P_SERVER_UUID) process.exit(0)
const { spawn } = require('child_process')
spawn(process.argv[0], process.argv.slice(1), { stdio: 'inherit', cwd: process.cwd(), detached: true }).unref()
process.exit(0)
Expand Down
29 changes: 22 additions & 7 deletions lib/commands/manager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const fs = require('fs')
const SHORTCUTS = require('./shortcuts')

class CommandError extends Error {}

Expand All @@ -10,7 +11,7 @@ class CommandManager {
constructor (server, overridesFile) {
this.server = server
this.commands = new Map() // name -> def
this.aliases = new Map() // alias -> name
this.aliases = new Map() // alias -> { name, args } (args are put in front of what the player typed)
this.overridesFile = overridesFile
this.overrides = {}
if (overridesFile && fs.existsSync(overridesFile)) {
Expand All @@ -33,9 +34,12 @@ class CommandManager {
name
}
this.commands.set(name, cmd)
for (const alias of cmd.aliases) {
const a = alias.toLowerCase()
if (!this.commands.has(a) && !this.aliases.has(a)) this.aliases.set(a, name)
const extra = SHORTCUTS[name] || []
for (const entry of [...cmd.aliases, ...extra]) {
const [a, ...args] = entry.toLowerCase().split(' ')
if (this.commands.has(a) || this.aliases.has(a)) continue
this.aliases.set(a, { name, args })
if (!cmd.aliases.includes(a) && !args.length) cmd.aliases = [...cmd.aliases, a]
}
return () => this.unregister(name)
}
Expand All @@ -45,7 +49,7 @@ class CommandManager {
const cmd = this.commands.get(name)
if (!cmd) return false
this.commands.delete(name)
for (const [alias, target] of this.aliases) if (target === name) this.aliases.delete(alias)
for (const [alias, target] of this.aliases) if (target.name === name) this.aliases.delete(alias)
return true
}

Expand All @@ -56,7 +60,16 @@ class CommandManager {
find (name) {
if (!name) return null
name = name.toLowerCase()
return this.commands.get(name) || this.commands.get(this.aliases.get(name)) || null
const alias = this.aliases.get(name)
return this.commands.get(name) || (alias && this.commands.get(alias.name)) || null
}

// Arguments an alias adds in front of the player's own ('zadd' -> ['add'])
aliasArgs (label) {
label = String(label).toLowerCase()
if (this.commands.has(label)) return []
const alias = this.aliases.get(label)
return alias ? alias.args : []
}

// minimum permission number required to use a command
Expand Down Expand Up @@ -84,7 +97,9 @@ class CommandManager {
if (!line) return
const space = line.indexOf(' ')
const label = (space === -1 ? line : line.slice(0, space)).toLowerCase()
const raw = space === -1 ? '' : line.slice(space + 1).trim()
const typed = space === -1 ? '' : line.slice(space + 1).trim()
const prefix = this.aliasArgs(label)
const raw = [...prefix, typed].filter(Boolean).join(' ')
const args = raw.length ? raw.split(/\s+/) : []

const cmd = this.find(label)
Expand Down
93 changes: 93 additions & 0 deletions lib/commands/shortcuts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict'

// MCGalaxy's shortcuts and aliases, so people coming from MCGalaxy can keep typing what they are used to.
// An entry with a space carries arguments: 'zadd add' makes /zadd x the same as /zone add x.
// They are added when the command registers; a real command name always wins over an alias.
module.exports = {
// chat and info
adminchat: ['admin'],
opchat: ['op'],
clear: ['playercls', 'globalcls all', 'gcls all'],
about: ['whodid'],
blocks: ['materials'],
help: ['cmdhelp'],
mapinfo: ['mi', 'winfo', 'worldinfo'],
serverinfo: ['sinfo', 'host', 'zall'],
time: ['ti'],
top: ['most'],
whois: ['whowas', 'info', 'i'],
players: ['who'],
pclients: ['clients'],
levels: ['worlds', 'maps', 'loaded'],
lastcmd: ['last'],
rankinfo: ['ri'],
whonick: ['realname'],
bans: ['banned'],
vote: ['vo'],
alts: ['clones', 'whoip'],
// essentials
hold: ['holdthis'],
reach: ['reachdistance'],
entityrot: ['entrot'],
tp: ['move', 'teleport'],
tphere: ['summon', 's', 'fetch', 'bring'],
tpa: ['tpaccept accept', 'tpdeny deny'],
bot: ['botlist list'],
// economy and games
buy: ['purchase'],
give: ['gib'],
shop: ['store', 'item'],
explode: ['ex'],
// moderation
ban: ['tempban', 'tb', 'kickban', 'kb'],
banedit: ['be'],
banip: ['bi', 'ipban'],
unbanip: ['unipban'],
xban: ['banx'],
kick: ['k'],
freeze: ['fz'],
rank: ['setrank'],
moveall: ['ma'],
playeredit: ['pe', 'setinfo'],
report: ['reports list'],
undoplayer: ['xundo'],
whitelist: ['wl'],
// levels
copylvl: ['wcopy', 'worldcopy'],
deletelvl: ['wdelete', 'worlddelete', 'wremove'],
fixgrass: ['fg'],
load: ['mapload', 'wload'],
lockdown: ['ld', 'wlock', 'wunlock'],
main: ['h', 'wmain', 'worldmain'],
newlvl: ['gen'],
reload: ['reveal', 'rejoin', 'rd', 'wflush', 'worldflush'],
renamelvl: ['wrename', 'worldrename'],
resizelvl: ['wresize', 'worldresize'],
save: ['mapsave', 'wsave', 'worldsave'],
map: ['perbuild buildrank', 'wbuild buildrank', 'worldbuild buildrank', 'pervisit visitrank', 'waccess visitrank', 'worldaccess visitrank'],
zone: ['zadd add', 'zremove delete', 'zdelete delete', 'zonelist list', 'zonetest check', 'ztest check'],
// building
cuboid: ['cw wire', 'ch hollow', 'walls walls', 'hbox hollow'],
sphere: ['sph hollow', 'sphereh hollow'],
spheroid: ['eh hollow'],
fill: ['f3d 3d', 'fill3d 3d', 'f2d layer', 'fill2d layer'],
line: ['ln'],
mark: ['click', 'x'],
measure: ['ms'],
calculate: ['calc'],
center: ['centre'],
paint: ['p'],
paste: ['v'],
place: ['pl'],
mirror: ['flip'],
spin: ['rotate'],
portal: ['o'],
torus: ['tor', 'bagel'],
write: ['wrt', 'writetext'],
abort: ['a'],
replacenot: ['rn'],
triangle: ['tri'],
static: ['t'],
delete: ['d'],
bind: ['cmdbind', 'cb']
}
3 changes: 3 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const DEFAULTS = {
allowWebClient: true,
trustProxy: false,
mainLevel: 'main',
autoloadLevels: [],
defaultRank: 'Guest',
owners: [],
welcomeMessage: '&eWelcome to the server, &f{player}&e! Type &f/help&e for commands.',
Expand All @@ -28,6 +29,7 @@ const DEFAULTS = {
'&f3. &7No spamming or advertising.'
],
defaultTexture: '',
publicAddress: '', // your server's address as players reach it (domain or IP, optional :port), for texture packs in texpacks/
autosaveMinutes: 5,
backupMinutes: 30,
backupsToKeep: 10,
Expand All @@ -44,6 +46,7 @@ const DEFAULTS = {
// Environment variables that map onto config keys
const ENV_MAP = {
PORT: ['port', Number],
SERVER_PORT: ['port', Number], // Pterodactyl and similar panels
HOST: ['host', String],
SERVER_NAME: ['name', String],
MOTD: ['motd', String],
Expand Down
Loading
Loading