swoftc · OCaml compiler · JVM runtime
The Minecraft scripting language that catches your bugs before the server boots.
Write commands, events, GUIs and scoreboards in readable English-flavored syntax. A flow-sensitive typechecker proves every maybe-missing value is handled — at compile time, with the error pointing at your line.
command "warp" {
description: "Send someone to spawn"
arguments {
who: optional<Player>
}
execute async {
set target to args.who
send "<gray>Warping ${target.name} in 3 seconds..." to target
wait 3 seconds
teleport target to location(0.5, 64.0, 0.5)
send "<green>Welcome to spawn, ${target.name}!" to target
}
}swoftc check warp.sw — real compiler output
warp.sw:10:14: error: this value is optional<Player> and may be missing; check it with 'if ... exists' or provide a fallback with 'otherwise'
send "<gray>Warping ${target.name} in 3 seconds..." to target
^+ 7 more errors downstream of the same missing value
who is an optional argument — the caller may omit it. The broken variant uses it as if it were always there, so swoftc check refuses to compile until the miss has a written-down answer. One otherwise sender later, the type is plain Player and the script ships. That's the whole philosophy — the Options guide is Step 07.
01
Skript, translated function by function
Everything you already know maps over — events, persistent variables, waits. Read your Skript on the left, its SwoftLang on the right, aligned pair by pair.
on join:
send "&aWelcome, %player%!" to player
broadcast "&7%player% joined"event PlayerJoin {
execute {
send "<green>Welcome, ${event.player.name}!" to event.player
send "<gray>${event.player.name} joined" to all
}
}on death of monster:
attacker is a player
add 1 to {kills::%uuid of attacker%}
send "&e%{kills::%uuid of attacker%}% kills" to attackerpersistent kills for Player: Integer = 0
event MobDeath {
execute {
if event.killer exists {
set kills for event.killer to kills for event.killer + 1
send "<yellow>${kills for event.killer} kills" to event.killer
}
}
}Why it maps this way
event.killer is optional<Player> — mobs die to lava too. Skript hands you an attacker that might silently be nothing; SwoftLang won't compile until the exists check is there. The persistent … for Player declaration replaces the {kills::%uuid%} list-variable idiom: typed, defaulted, and saved across restarts.
command /crate:
trigger:
send "&7Opening your crate..." to player
wait 3 seconds
send "&aYou won a diamond!" to playercommand "crate" {
execute async {
send "<gray>Opening your crate..." to sender
wait 3 seconds
send "<green>You won a diamond!" to sender
}
}Why it maps this way
Same shape, different guarantee: execute async parks a virtual thread, never the tick thread — and moving that wait into a sync handler is a compile error, not a laggy server.
02
The compiler knows your property table
Every event.player.… access is checked against the real Minestom-backed property table — with typo suggestions and read-only enforcement, before the server ever boots.
event PlayerJoin {
execute {
send "hi ${event.player.nmae}" to event.player
}
}swoftc check welcome.sw
welcome.sw:3:14: error: unknown property 'nmae' on Player; did you mean 'name'?
send "hi ${event.player.nmae}" to event.player
^03
Async that reads top to bottom
No callbacks, no schedulers, no runTaskLater. wait parks a virtual thread; spawn fires and forgets; async functions return values you can just add.
command "crate" {
description: "Open a timed reward crate"
execute async {
send "<gray>Opening your crate..." to sender
wait 3 seconds
send "<green>You won a diamond!" to sender
spawn announce(sender.name)
}
}
async function announce(name: String) {
wait 1 seconds
send "<gold>${name} just opened a crate" to all
}04
GUIs are declarations, not inventory-click plumbing
Describe the inventory — rows, slots, items, click handlers — and the runtime renders it, diffs it, and re-renders on state change.
gui "menu" {
rows: 3
title: "Server Menu"
slot 13 {
item { material: "COMPASS", name: "<aqua>Warp home" }
on_click {
send "<gray>Warping..." to player
close gui for player
}
}
}
command "menu" {
execute {
open gui "menu" to sender
}
}05
State that survives restarts
One keyword. Typed, defaulted, keyed by UUID when you say for Player, flushed write-behind to files, SQLite, MySQL or MongoDB. Reads are never missing.
persistent visits for Player: Integer = 0
event PlayerJoin {
execute {
set visits for event.player to visits for event.player + 1
send "<gray>Visit #${visits for event.player}" to event.player
}
}The guide is a numbered course — 16 steps, one concept each, ending in something runnable.
Step 01 · SetupOr go deeper: full Skript plugins ported line by line and the standard library addons.