# HyperScript A lightweight scripting language for **Paper and Spigot** servers, **1.16.5 and newer** (estimated up to 26.x). Drop a `.hs` file into a folder and you have custom commands, event handlers, GUIs, scoreboards and persistent player data, with no Java and no restart: scripts reload the moment you save them. ``` on player join: add 1 to {%player%.joins} send "&aWelcome back %player.name%! This is visit #%{%player%.joins}%." ``` - [Overview and architecture](#overview-and-architecture) - [Installation and setup](#installation-and-setup) - [Configuration](#configuration) - [Quick tutorial](#quick-tutorial) - [Writing scripts](#writing-scripts) - [Cheat sheet](#cheat-sheet) - [Syntax guide](#syntax-guide) - [Complete example: `demo.hs`](#complete-example-demohs) - [Java implementation](#java-implementation) - [Building from source](#building-from-source) --- ## Overview and architecture | | | |---|---| | Plugin folder | `/plugins/HyperScript/` | | Scripts | `/plugins/HyperScript/scripts/*.hs` | | Settings | `/plugins/HyperScript/config.yml` | | Saved variables | `/plugins/HyperScript/data.json`, plus MySQL or SQLite if configured | | Backups | `/plugins/HyperScript/Backups/` | | Admin command | `/hyperscript ` (alias `/hs`, permission `hyperscript.admin`, default op) | | Requirements | Paper or Spigot 1.16.5+, Java 8+ | ### What is in the box | Feature | Summary | |---|---| | Commands and events | Declare commands with permissions, aliases and checked arguments; over 80 events; react to joins, chat, damage, jumps, block breaks, mob spawns, inventory clicks and more, with each event's details available as plain words such as `attacker` and `message` | | Cooldowns and timers | `cooldown: 1 hour` on any command or event; `every 5 seconds:` for repeating work; `every day at 18:00:` for the clock | | On-screen text | Action bars, titles with fade timings, boss bars, per-player sidebar scoreboards | | Variables | `{name}` is saved and shared by every script, `{_name}` lasts for one run; a name that starts with a player, such as `{%player%.coins}`, is that player's own. They hold numbers, text, lists, items, durations and locations | | Functions and options | `function reward({_who}, {_amount}):` for any script to call, `local function` for one file; `options:` for settings named once at the top of a file | | Players | `heal`, `feed`, health, levels, game mode, flight, potion effects, `freeze`, `vanish`, pushing, and thirty more things to read or set | | Entities | Keep a summoned mob in a variable: name it, heal it, send it after someone, remove it | | Dates | `now`, `now + 1 day`, `time since`, dates written out any way you like | | Error handling | `try:` and `on error:` for lines that might fail | | Top lists | `top 10 players by {coins}` across everyone who has ever played | | Teams and moderation | Coloured teams with tags; `kick`, `ban ... for 7 days`, and `on player login:` to turn players away | | Inventories | `if player has 5 diamond:`, `remove 5 diamond from player` | | Custom items | `item magic wand:` names an item once for every script, `recipe for magic wand:` makes it craftable; `give player 1 diamond sword named "&6Excalibur" with lore "..." enchanted with sharpness 5`; set what a player wears and holds; hidden tags so a script recognises its own items | | Chat | Clickable and hoverable messages: `send "&a[Home]" with click command "/home"` | | Sounds and resource packs | `play sound ... with pitch 1.8 and volume 1.0`; `send resource pack` with response events | | Worlds and regions | Set time and weather; define box regions and test `region at ` | | Web requests | `send get request to ...` and `send post request to ...` from `async:` blocks | | Text and files | `split`, `join`, `replace`, slicing, `parse "52" as number`; create, read and write text files in the plugin folder | | Lists and loops | `[1, 2, 3]`, `sorted`, `filter ... where`, `for each`, `loop N times`, `loop from 1 to 10`, `loop all players`, `loop-value`, `break`, `continue`, `switch` | | GUIs | Chest menus with clickable slots, written inline; pages and refreshing built in | | Async blocks | `async:` moves heavy work off the server thread, `sync:` comes back | | Server list and chat | MOTD, player counts and icon in the server list; tab list header and footer; join, quit and chat formats | | Waiting and loops | `wait 5 seconds` inside any trigger; `loop`, `for each` and `while` | | World effects | Floating text and blocks (display entities), particles with hex colours, particle circles, spheres and lines | | Discord | Webhooks, and through a bot: slash commands, messages sent to a channel, and reacting to what is written there | | Other plugins | Money through Vault (`balance of player`); PlaceholderAPI placeholders in scripts, and script variables as placeholders for other plugins | | Hot reloading | Scripts recompile when the file is saved | | Update check | Operators are told on joining when a newer version is out | | Characters and trading | `create npc "Guide" at ...` with `on npc click`, and the villager trading window with offers of your own | | Parties and queues | Players who belong together, and `on queue "arena" of 4:` when enough are waiting | | Enchantments and advancements | `enchantment lifesteal:` and `advancement "First blood":` blocks of your own | | The database | `run sql` and `sql rows of` on the database the variables are in; `shared.` variables every server of a network sees | | A web address of its own | `on web request "vote":` for websites and other programs to call | | Finding scripts | `/hyperscript search`, `install` and `update` fetch scripts and new versions from the website | | Testing | `test "...":` blocks run with `/hyperscript test`, and `/hyperscript debug` shows a script line by line | | Addons | Other plugins can add statements, values and events to the language | | Storage | `data.json`, optionally with a CSV file, SQLite, H2 or MySQL beside it and automatic fallback, plus scheduled backups | ### How a script runs ``` main.hs ──► Lexer ──► ScriptParser ──► execution tree ──► registered once ──► runs on every event (text) (one pass, (Java objects, (commands and at load) names resolved) event listeners) ``` HyperScript does all of its text work **once, when a script is loaded**. What runs afterwards is a tree of small Java objects, one per statement or operator, that call each other directly. To be precise about the word "compile": HyperScript compiles to an in-memory execution tree, not to JVM bytecode. The speed comes from moving work out of the hot path, not from code generation: | Work | A naive line-by-line interpreter | HyperScript | |---|---|---| | Parsing a line | Every time the line runs | Once, at load | | Local variables (`{_reward}`) | Map lookup by name | Array slot assigned at load | | Placeholders (`%player.name%`) | String search and replace per message | Bound to a resolver at load; the string is pre-split into parts | | Constant math (`20 * 60 * 5`) | Recomputed each run | Folded to a single constant at load | | Syntax errors | Discovered when the line is reached | Reported at load with a line number; the file is rejected | | Unused events | Often listened to anyway | Not registered at all | | Persistent variables | Disk or database per access | In-memory map; storage I/O on a background thread | A few consequences worth knowing: - **Errors are found up front.** A typo on line 80 is reported when the file loads, not the first time a player happens to trigger that branch. A file with an error is skipped entirely, so a half-valid script never runs. Other files still load. - **Events you do not use cost nothing.** If no script contains `on block break:`, HyperScript never registers a block-break listener. - **Variable reads never touch the disk or the database.** A player's data is read while they are logging in (off the server thread) and written back in the background. ### Why it survives server updates The language core (`dev.hyperscript.lang`, `dev.hyperscript.runtime`) contains **no Bukkit imports**. Everything the server provides goes through one interface, `ScriptHost`. The Bukkit side is compiled against the 1.16.5 API, the oldest supported version, with no version-specific (`net.minecraft`) code. The places where versions genuinely differ are handled at run time: | Feature | Difference between versions | How HyperScript handles it | |---|---|---| | `%server.tps%` | `getTPS()` exists on Paper only | Measures tick rate itself with a scheduler task | | `%player.ping%` | `Player#getPing()` exists from 1.17 | Uses it when present, otherwise reads the value reflectively; `-1` if neither works | | Registering commands at run time | No public Bukkit API | Reaches the server's command map reflectively | | Display entities | Added in 1.19.4 | Used reflectively when present; floating text falls back to an invisible armour stand | | Coloured dust particle | Renamed from `REDSTONE` to `DUST` in 1.20.5 | Looks up whichever name the server has | | GUI click handling | `InventoryView` changed from a class to an interface in 1.21 | Never touches `InventoryView` | --- ## Installation and setup 1. **Get the jar**, `HyperScript-1.7.71.jar`. 2. **Stop the server**, then copy the jar into the server's `plugins/` folder. 3. **Start the server.** HyperScript creates its folder, a `config.yml`, and an example script. 4. **Write a script.** Create a file ending in `.hs` inside `plugins/HyperScript/scripts/`, for example `main.hs`. Save it as UTF-8. Or start from a ready-made one: the **Scripts** page of the documentation site has scripts to set a server up with (a sidebar, a tab list, ranks, chat prefixes and join messages) to copy or download into that folder, and scripts other people have made. 5. **Save the file.** With hot reloading on (the default) the script is live a moment later, and the console says what was loaded. `/hyperscript reload` does the same by hand. 6. **Check on things** with `/hyperscript list` (loaded and disabled scripts) and `/hyperscript status` (storage, hot reloading, Discord bot). 7. **Check a script before using it**: `/hyperscript check ` reads the file and reports any mistake without loading it, so nothing that is running changes. It works on disabled files too. 8. **Switch a script off** without deleting it: `/hyperscript disable `, and `/hyperscript enable ` to bring it back. Press Tab to pick from the files. After the first start the folder looks like this: ``` plugins/ └── HyperScript/ ├── config.yml ├── analytics-id.txt <- a random id for the usage numbers, unless they are switched off ├── data.json <- saved variables, created on the first save ├── scripts/ <- every .hs file in here is loaded, subfolders included │ └── disabled_demo.hs <- example script, switched off; /hyperscript enable demo └── Backups/ <- copies of data.json, created on the first backup ``` Scripts are loaded a moment after the server has finished starting, once every plugin is enabled, so that addons have had the chance to add their syntax first. If a file has a mistake, that file is not loaded and you are told the file, line and reason: ``` [HyperScript] main.hs:12 Expected 'to' but found end of line ``` The message goes to the console and to whoever ran `/hyperscript reload`. When a hot reload finds the mistake, it is sent to every online player with the `hyperscript.admin` permission instead, so you see it in game right after saving the file. `/hyperscript remake demo` writes the example script `demo.hs` again, as it ships with the installed version of the plugin, and reloads. It replaces any `demo.hs` that is there, including your edits to it. A demo that is switched off stays switched off. **Subfolders.** Scripts may be sorted into folders inside `scripts/`, up to eight folders deep: `scripts/minigames/spleef.hs`, `scripts/staff/tools.hs`. Every `.hs` file under `scripts/` is loaded, in alphabetical order of its path, and is named by that path in messages and commands: `/hyperscript check minigames/spleef.hs`. A `local function` belongs to its own file, wherever the file is, and a plain `function` can be called from any folder. Disabling a script renames it to `disabled_.hs`, and any file with that prefix is skipped when scripts load. Renaming a file that way by hand does the same thing. > The bundled `demo.hs` is there to read and to try, and it is **switched off** on a new server: > the file is `disabled_demo.hs`. `/hyperscript enable demo` switches it on. It then adds `/coins`, > `/gamble`, `/shop` and several other commands and reacts to joins, block breaks and chat, so > switch it off again (`/hyperscript disable demo`) or delete it before the server goes public. > The first start downloads the Discord library (JDA, about 10 MB with its dependencies) through > the server's own library loader, whether or not the bot is enabled. On a server without internet > access, remove the `libraries` section from `src/main/resources/plugin.yml` and rebuild; > everything except the Discord bot (slash commands and channel messages) works without it. --- ## Configuration Settings live in `plugins/HyperScript/config.yml`. Changes need a server restart. ```yaml send-analytics: true language: english # english | french | spanish | portuguese | german combat-time: "15s" # how long "player is in combat" holds after a hit afk-time: "5m" # how long doing nothing makes "player is afk" money-format: "$%amount%" # how money(...) writes an amount limits: # the most HyperScript keeps going by itself; see Limits below particle-shapes: 200 particles-a-tick: 3000 slow-down-below-tps: 15 floating-texts: 300 temporary-blocks: 5000 structure-blocks: 250000 word-filter: censor # censor | block: what happens to chat with a blocked word in it mute-commands: [msg, tell, w, whisper, me, r, reply, say] # what a muted player cannot use hot-reloading: true storage: type: json # json | csv | sqlite | h2 | mysql autosave: "5m" shared-variables: false # {shared.name} is the same on every server that uses the database shared-refresh: "2s" sqlite: file: "data.db" csv: file: "data.csv" h2: file: "data-h2" mysql: host: "localhost" port: 3306 database: "minecraft" username: "root" password: "" use-ssl: false backups: enabled: true interval: "1h" keep: 24 update-check: true auto-update: false # true downloads newer versions by itself; they take effect at the next restart log-script-errors: true notify-script-errors: true sidebar-numbers: false # true shows the numbers beside the lines of a sidebar loop-limit: 100000 player-error-message: "&cSomething went wrong with that. The staff have been told." files: enabled: true web: enabled: true allow-private-addresses: false server: # the plugin's own web address, for 'on web request' enabled: false port: 8765 bind: "0.0.0.0" token: "" discord: webhooks: {} bot: enabled: false token: "" guild-id: "" read-messages: false options-file: false # every script's options: in one options.yml update-project: hyperscript ``` Durations are a number and a unit: `30s`, `5m`, `1h`, `1d`. `loop-limit` is how often a `loop times` may be asked to go round, and how often a `while` may go round without waiting, before the script is stopped as runaway: from 1,000 to 50,000,000. `update-project` is the name `/hyperscript update` looks for on Modrinth. `language` is the language scripts are written in; see [Scripts in another language](#scripts-in-another-language). `log-script-errors` writes a line to the console when a script fails while it is running, and `notify-script-errors` also tells players with `hyperscript.admin` in chat, at most once a minute for the same message. Mistakes found when a script loads are always reported. ### Limits Some lines keep something going after they have run: a particle shape that stays, text in front of a player, a block that is put back later. HyperScript does the keeping, every tick, and `limits:` in `config.yml` says how much of it there may be, so that a script written with too free a hand cannot slow the server down. | Setting | Unless changed | What happens beyond it | |---|---|---| | `particle-shapes` | 200 | A new shape that stays takes the place of the oldest | | `particles-a-tick` | 3000 | Shapes wait for their next turn, so they are drawn less often; the console is told, at most once every five minutes | | `slow-down-below-tps` | 15 | Not a limit but a brake: while the server manages fewer ticks a second than this, shapes that stay are drawn half as often | | `floating-texts` | 300 | A new one takes the place of the oldest | | `temporary-blocks` | 5000 | `set block ... for` does nothing until some have been put back | `/hyperscript timings` shows how much of each there is right now and how many milliseconds of a tick it takes. A tick has fifty; anything of HyperScript's taking more than two is shown yellow, more than five red. ### Update check With `update-check: true`, HyperScript asks its website once, each time the server starts, which version is the newest (`https://hyperscript.monoforge.net/api/latestversion`). If a newer one exists it says nothing in the console; instead, operators and players with `hyperscript.admin` are told when they join, and `/hyperscript status` shows it. - Nothing is downloaded or installed. The request carries only the version of the plugin. - It runs in the background and gives up after five seconds, so it never delays a start. - If the website cannot be reached, one line is written to the log: `Error: couldn't fetch newest version on startup`, and the server carries on as usual. - Set `update-check: false` to switch it off. - **`/hyperscript version`** shows the version the server runs and asks the website again there and then, whether or not the check at startup is on. It answers with the newer version, with "You are on the newest version", or with why the website could not be reached. ### Usage numbers With `send-analytics: true` (the default) the plugin tells the HyperScript website that a server is running it: once at start, every half minute, a moment after players join or leave, and once at stop. It sends: - a random id made for the server, which says nothing about it; - the versions of HyperScript, Minecraft, the server software and Java; - how many players are online and how many fit; - how many scripts are loaded, how many commands, events and timers they make, and how many scripts did not load; - the names of installed HyperScript addons; - a few numbers about the server itself: how long it has been up, its ticks a second, how much memory Java may use, how many processor cores there are, the kind of operating system (Windows, Linux or Mac), how many worlds and plugins are loaded, whether it is in online mode, and the language scripts are read in. It never sends player names, addresses, script contents, world names or the names of other plugins. `send-analytics: false` sends nothing at all. While it is on, the console says so at every start. ### Hot reloading With `hot-reloading: true`, HyperScript watches `plugins/HyperScript/scripts/`. Saving, creating or deleting a `.hs` file re-parses the scripts about half a second later, on the server thread, exactly as `/hyperscript reload` would. The console reports the result: ``` [HyperScript] Scripts changed on disk. Loaded 2 script(s): 9 command(s), 6 event handler(s) ``` - If the saved file has an error, it is reported and that file's commands and handlers are unavailable until the next save that fixes it. The other files are unaffected. - Variables are not touched by a reload. GUIs that are already open keep working with the code they were opened with. - Subfolders are watched too, including folders made while the server runs. ### Storage and fallback Variables are always held in memory while the server runs. `storage.type` decides where they are saved: | `storage.type` | Saved to | Read from | Set up | |---|---|---|---| | `json` (default) | `data.json` | `data.json` | Nothing to do | | `csv` | `data.csv` **and** `data.json` | `data.csv` | Nothing to do: the file is made by itself. A spreadsheet opens it | | `sqlite` | The SQLite file `data.db` **and** `data.json` | The SQLite file | Nothing to do: the database is made by itself | | `h2` | The H2 file `data-h2.mv.db` **and** `data.json` | The H2 file | Nothing to do on Minecraft 1.17 and newer: the server downloads H2 the first time, and the database is made by itself | | `mysql` | The MySQL/MariaDB server **and** `data.json` | The MySQL server | A server and a database of yours, given under `storage.mysql` | **Which is fastest?** For scripts, none: while the server runs every variable is in memory, and a script never waits for a file or a database. The kinds differ in what you can open the data with (`csv` in a spreadsheet, `sqlite` and `h2` with a database tool, `mysql` from several servers and from a website), and that is what to choose by. `json` stays the default because it needs nothing and is written in every mode anyway. `data.csv` has a row for each variable: whose it is (`global`, or a player's id), its name, and its value the way `data.json` writes it. Change it by hand only while the server is stopped; what you change is read when it starts. `data.json` is written in every mode, which is what makes the fallback work: - **The database is unreachable at startup, or disappears later.** HyperScript logs a warning and carries on using `data.json`. Nothing is lost and scripts do not notice. - **The database comes back.** At the next autosave HyperScript reconnects and copies across every player and global scope that changed during the outage, then resumes normal operation. This works across restarts too: the list of scopes still to copy is kept in `data.json`. - **A database is switched on for the first time.** If its table is empty, the contents of `data.json` are copied into it, so moving from JSON to a database needs no migration step. `/hyperscript status` shows which store is active right now. Things to know: - Changes are written when a player quits, every `storage.autosave`, and on shutdown. A server crash can lose up to one autosave interval of changes. - The database table is `hyperscript_variables (scope, name, value)`, one row per variable, with the value stored as JSON text. HyperScript uses the MySQL and SQLite drivers bundled with the server; H2 is fetched by the server itself (it is named in the plugin's `libraries`). - With MySQL, a value can be up to 16 MB (a table made by an older version is widened by itself), text is kept as `utf8mb4`, and MySQL 8 servers that refuse a login without SSL are logged into all the same. - The fallback assumes **one server per database**. If several servers share a MySQL database, each one's `data.json` only knows about its own players, so a fallback on one server can serve data that another server has since changed. - If `data.json` cannot be parsed at startup, HyperScript disables itself rather than start empty and overwrite the file. Restore a copy from `Backups/` or fix the file, then restart. ### Backups With `backups.enabled: true`, HyperScript saves all pending changes and then copies `data.json` to `plugins/HyperScript/Backups/data-_