Skip to content

Tutorial Run a Server

johnjohto edited this page Jul 28, 2026 · 5 revisions

Tutorial: Run a Server

Monworks games can meet online. A server is a thin lobby: it checks who may enter, authenticates handles, shows who's here, introduces matched players — and nothing more. Battles and trades always run between the two players' own copies of the game, exactly like a direct cable link; when two machines can't reach each other directly, the server relays their traffic without ever reading it. There is no account system, no central service, no telemetry — a server is something you run, for your friends or your community.

Start one

From the toolkit checkout:

python tools/server.py                                    # port 17226, data in user://
python tools/server.py --port 17300 --serverdata C:/srv/mygame --project build/mygame

That's the same engine binary you play with, in a headless server mode. It binds a project — by default the one in game/, or whatever --project points at — and prints who registers, enters, and leaves as it runs.

The door: your project IS the ticket

A connecting player is admitted only if their copy of the game is byte-identical to the server's — same game version, same engine build, same content hashes. Anyone whose data differs is refused with a message naming exactly what differed, before they can enter any session. You never configure this; it is the same handshake two directly-linked games already use.

Two consequences worth knowing:

  • A public server must bind your own original (or CC0) content. Extracted Kanto content is personal-use only — a Kanto-bound server structurally admits only people who built the same extraction themselves, so it cannot serve a public audience, and must not.
  • Cheated content can't get in the door. (Play-level fairness comes with replay verification on the ladder, a later milestone.)

Operator config — <serverdata>/server.json

{
 "invites": ["cinnabar", "fuchsia"],
 "motd": "welcome to the meadow",
 "name": "meadowbrook league",
 "registration": "invite",
 "strike_limit": 3,
 "verify": true
}
  • registrationopen (anyone may enroll a handle), invite (enrolling needs one of the codes in invites), or closed (no new handles). Anything else refuses to boot, loudly — a typo must never silently open your door.
  • motd — shown to players on entry.
  • verify — turn on replay verification (below). Off by default: a casual server records nothing at all.
  • strike_limit — suspend a handle once it accumulates this many strikes from failed verifications (0, the default, records strikes without enforcing).
  • Everything is optional; no file at all means an open server named "monworks server".

Replay verification — cheating can't win

With "verify": true, every match your server introduces is verified: both games independently submit a compact replay at battle end (the seed, both teams as exchanged, every action in order, and a fingerprint of the entire battle). When the two submissions agree — every honest match — the result is recorded instantly, no simulation, no cost. When they disagree, your server replays each side's story on the very engine it runs, because the server IS the game: the version whose replay matches its own claims stands, and the other is voided and struck. A hacked client that sends an illegal move mid-battle fares no better: the honest player's game refuses it on the spot ("ILLEGAL action! Battle voided.") and the cheater's own log convicts them in the replay. Strikes live in players.json; set strike_limit to suspend repeat offenders at the door. Replays and verdicts are plain JSON under <serverdata>/replays/ — keep or delete them as you please.

What this buys, plainly: a cheat cannot beat an honest player on a verified server. The worst any tampering achieves is a voided match — the same as never having played — and a strike on the way out.

The ladder — Elo, formats, clauses

A verified server keeps a ladder: plain Elo over verified results (a voided match never moves anyone), written to <serverdata>/ladder_<format>.json where anyone can read it. Tune it in server.jsonelo_k (default 32), elo_provisional_n and elo_provisional_mult (newcomers' first matches move double by default) — and end a season whenever you like by archiving the file.

Matches run under a format you author in <serverdata>/formats.json:

{"lc30": {"name": "LEVEL 30", "clauses": ["sleep", "freeze"],
          "level_cap": 30, "species_clause": true, "party_max": 6}}

Pick the active one with "format": "lc30" in server.json. A STANDARD format (all four clauses, level 100) is built in, so a fresh server just works. Formats enforce two kinds of rules:

  • Admission — level cap, species clause, party size. Checked at the table by both games before anyone battles (the match refuses to seat, naming the rule), and again by your server when results come in — so even two colluding hacked clients can't ladder an illegal team.
  • In-battle clausessleep, freeze, evasion, ohko, enforced inside the battle itself: a clause-breaking move is announced and simply fails, on both players' screens identically. The active set is cryptographically agreed by both games before turn 1 — a tampered client that disagrees about the rules can't get a match started at all.

Every format field is checked when the server boots, and a typo refuses loudly rather than quietly dropping a rule.

Registered players live in <serverdata>/players.json — a plain canonical-JSON table of handle → token hash. There is no database and no personal data: a player is a handle they chose plus a secret token their game stores. Deleting the file resets enrollment (players' games re-enroll automatically on their next visit).

Who is who

  • A handle (2–16 letters/digits/underscore) is registered once per server and authenticated by a server-minted token the player's game keeps. It is not the in-game character name — that stays cosmetic.
  • One live connection per handle. A second connection on the same handle is refused, so a leaked token can't silently displace its owner's session.
  • Each save-file character on a player's machine enrolls as its own handle — a character is a person as far as the server cares.

What players see

Once their game points at your server (see Join a Server), the Cable Club attendant grows an ONLINE option: a lobby of who's open, challenges either way, and then the normal link flow — Trade Center or Colosseum — between the two players directly. If a direct connection can't form (strict NATs), the session runs through your server's content-blind relay instead; players notice nothing, and a mid-battle connection blip reconnects and resumes on its own.

The trade board

Players can post monsters for trade and claim each other's listings even when the other side is offline — your server holds only a listing's public card and tells both parties the moment they're online together; the trade itself is always the two players' own Trade Center ceremony, and the server never holds a monster. Listings live in <serverdata>/board.json, expire after board_ttl_s in server.json (default a week), cap at three per player, and come down automatically when a trade completes.

Practicalities

  • Default port: 17226 (UDP). Friends outside your network need it forwarded or the machine otherwise reachable; matched sessions also use the game's own link ports (17225 and a small window above it) when connecting directly.
  • The server is quiet and tiny — it forwards small JSON frames and holds a lobby list. Any machine that runs the game can run it.
  • Stop it with Ctrl+C. State is just the two JSON files in <serverdata>.

The full protocol and design live in docs/engine/server.md.

Clone this wiki locally