Skip to content

Guide · Step 16

Ship It

You'll build a production server.sw — auth, MOTD, permissions, a loaded world, an HTTP endpoint, and a scheduled announcer.

Thirteen steps of scripts; one step of server. Everything that turns your scripts folder into a shippable server lives in script files too — a server { } block for boot configuration, world statements for the map, api declarations for HTTP, and every blocks for schedules. Same compiler, same checks.

The server block

At most one across all your scripts (a duplicate is a compile error):

swoftlang
server {
    auth: offline
    host: "0.0.0.0"
    port: 25565
    brand: "SwoftLang"
    motd: "<green>A SwoftLang server"
    favicon: "server-icon.png"
}

Every field is optional — an empty block (or none at all) boots an offline-mode server on 0.0.0.0:25565. motd takes MiniMessage; favicon is a path to a 64×64 PNG, embedded at boot.

Auth

auth picks how players are verified:

ModeWrittenUse it when
offlineauth: offlinelocal dev; no verification (the default)
mojangauth: mojangstandalone public server — full account verification
velocityauth: velocity "secret"behind a Velocity proxy (modern forwarding)
bungeecordauth: bungeecordbehind BungeeCord
swoftlang
server {
    auth: velocity "kQ8mZ2vX9pL4"
    host: "127.0.0.1"
    port: 25566
}

Behind a proxy you bind to localhost and let the proxy face the internet — the secret must match your velocity.toml.

Permissions

Back in Step 02 you gated commands with permission:. Grants live here — a name-to-nodes map consumed by the default permission provider:

swoftlang
server {
    auth: mojang

    permissions {
        "Swofty": ["myserver.admin", "myserver.heal", "myserver.give"]
        "Notch": ["myserver.heal"]
    }
}

Anyone not listed lacks the node and gets the refusal message. The permission provider is replaceable for real deployments; the block above is the built-in default.

Worlds

Worlds load through loaders — values describing where world data lives:

  • anvil_loader("worlds/") — the vanilla region format; drop in any world folder.
  • polar_loader("worlds/") — a compact single-file format, fast to load and clone.
  • polar_storage_loader(<backend>) — polar bytes in a Step 10 storage backend, keyed by world name.
swoftlang
command "arena-setup" {
    permission: "myserver.admin"

    execute {
        set loader to anvil_loader("worlds/")

        if world_exists("arena", loader) {
            load world "arena" with loader
        } else {
            create world "arena" with loader
        }

        set w to world("arena")
        if w exists {
            set w.time to 6000
            teleport sender to location(0.5, 65.0, 0.5)
        }
    }
}

command "arena-reset" {
    permission: "myserver.admin"

    execute {
        unload world "arena" teleporting players to location(0.5, 65.0, 0.5)
        send "<green>Arena unloaded; players rescued to spawn." to sender
    }
}

The full statement set: create world, load world, save world, unload world <name> [without saving] [teleporting players to <loc>], clone world "a" to "b" with <loader>, delete world, plus the world_exists(name, loader) and all_worlds(loader) builtins. A loaded world resolves via world(name) — an optional<World>, as always.

Schedulers

Skript's every 5 minutes: exists here too — as a top-level declaration that starts at boot, with a body that is async-colored (it may wait):

a repeating announcer
skript
every 5 minutes:
    broadcast "&7Remember to vote!"
swoftlang
every 300 seconds {
    broadcast "<gray>Remember to vote!"
}
Why it maps this way

Durations are ticks, seconds, or millis — no minutes unit, so five minutes is 300 seconds. The body runs as a background task on the tick-aligned clock from Step 09: a slow body can't stall the game, and every scheduled task is cancelled cleanly on reload.

For dynamic schedules, schedule is an expression returning a handle you can cancel:

swoftlang
command "event-start" {
    permission: "myserver.admin"

    execute {
        set job to schedule after 10 seconds every 60 seconds {
            broadcast "<gold>The event is live — /warp event"
        }
        cancel schedule job
    }
}

The HTTP API

Declare endpoints and your server speaks HTTP — handy for dashboards, vote listeners, and status pages. Enable the listener in server { }, then declare api blocks:

swoftlang
server {
    http {
        port: 8080
        bind: "127.0.0.1"
    }
}

api "/stats/:name" {
    method: GET
    execute async {
        set p to player(request.params.name)
        if p exists {
            reply with "online, ping ${p.latency}ms"
        } else {
            reply code 404 with "offline"
        }
    }
}

:segments in the path become request.params.*; request also carries method, path, query, and body. reply [code N] with <string> sends the response — the checker only allows it inside api bodies. Handlers are async-colored (they run off the game thread), so the Step 09 rules apply unchanged: property writes hop to the tick thread by themselves, and that player(...) lookup is the same optional<Player> it always was.

A dynamic MOTD

The ServerPing event fires on every server-list ping, with writable motd, online, and max:

swoftlang
event ServerPing {
    execute {
        set event.motd to "<green>${length(all_players())} adventurers online"
        set event.max to 1000
    }
}

The payoff: server.sw

Everything above, as one shippable file:

swoftlang
server {
    auth: mojang
    host: "0.0.0.0"
    port: 25565
    brand: "SwoftLang"
    motd: "<green>A SwoftLang server"
    favicon: "server-icon.png"

    http {
        port: 8080
        bind: "127.0.0.1"
    }

    permissions {
        "Swofty": ["myserver.admin"]
    }
}

event ServerPing {
    execute {
        set event.motd to "<green>${length(all_players())} adventurers online"
    }
}

every 300 seconds {
    broadcast "<gray>Remember to vote! <yellow>/vote"
}

api "/health" {
    method: GET
    execute async {
        reply with "ok, ${length(all_players())} online"
    }
}
bash
$ swoftc check scripts/*.sw --addon-path addons
$ ./gradlew :java:run

Fourteen steps ago that check command had nothing to say. Now it's proving optionals, property chains, thread colors, GUI slot ranges, module boundaries, and this server block — before a single player connects. Keep the syntax cheatsheet open, raid the examples for full ported plugins, and go ship something.