- Net autoload: headless dedicated server (--server) on UDP 24242 via ENet, server.json preferences (flags override per run, never saved), reserved names in names.json - Identity: RSA-2048 key per install (Godot has no Ed25519), player id = key hash, challenge/response so ids can't be faked; profile with name and limited appearance (tunic, hair, beard) - Server refuses bans, allowlist misses, duplicate logins, bad or taken names, wrong passwords, old protocol versions and a full server, each with a clear reason; first player on a new server becomes admin - Title screen: Profile, Join Game, and a lobby with server info, rules and who's online - test_net_lobby: real headless server + bot processes over UDP (38 checks); --exit-with so test servers never outlive the test
255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
# M2 multiplayer plan
|
|
|
|
Status: **plan, decisions agreed** (§12), nothing built yet. It refines `ARCHITECTURE.md` §2-3 into
|
|
concrete decisions for this codebase, as it stands after M1.
|
|
|
|
## 1. Goals
|
|
- **Self-hosted**, 2-8 players (up to 16 possible). No central service, no
|
|
accounts: a world lives on whoever hosts it.
|
|
- **Three ways to play, one code path:**
|
|
- **Single-player**: the game runs a server inside itself and connects to
|
|
it (loopback). It plays exactly as now.
|
|
- **Host & play**: "Host game" on the title screen, and friends join you.
|
|
- **Dedicated server**: a headless "server only" process with no local
|
|
player. It keeps the world running (or paused) for whoever connects.
|
|
- **Server-authoritative:** the server decides everything, and clients send
|
|
intents (the Actions layer already works this way).
|
|
- **Co-op or adversarial**, chosen by server rules (§5).
|
|
|
|
## 2. Running a server
|
|
*Built in M2.1* (`scripts/net/net.gd`, `server_config.gd`, `identity.gd`).
|
|
As of M2.1 the server accepts players, checks who they are and keeps the
|
|
player list. The world itself isn't served yet (M2.3). Flags: `--server`,
|
|
`--port`, `--config`, `--server-name`, `--max-players`, `--password`,
|
|
`--world`, and `--exit-with <pid>` (quit when that process exits; the tests
|
|
use it so a crashed test never leaves a server running). Flags apply to
|
|
that run only and are never written to `server.json`.
|
|
```bash
|
|
# Dedicated server (no window, no player)
|
|
godot --headless --path . -- --server --config server.json
|
|
# or quick flags (override the config file)
|
|
godot --headless --path . -- --server --world "Valley of Us" --port 24242
|
|
```
|
|
- **Transport:** ENet over **UDP, one port**, default **24242**,
|
|
configurable. It works on Linux, macOS and Windows.
|
|
- **Reaching it from outside:**
|
|
- **Tailscale (recommended for friends).** Share the server machine from
|
|
your tailnet; friends install Tailscale and join `100.x.y.z:24242` (or
|
|
the MagicDNS name). Nothing is exposed to the internet.
|
|
- **Port forwarding.** Forward UDP 24242 on your router. No extra
|
|
software, but your home IP and that port are public.
|
|
- **A small cloud server.** The same command on a cheap Linux server.
|
|
- **Cloudflare:** a normal Cloudflare tunnel doesn't carry raw UDP.
|
|
Possible later via a **WebSocket transport** (Godot's
|
|
`WebSocketMultiplayerPeer`, TCP, a little more stutter on poor links)
|
|
behind `cloudflared`. It's planned as an optional `--transport ws` and
|
|
isn't needed for v1.
|
|
- **Runs as a service:** a sample systemd unit and a launchd plist go in
|
|
`server/`. Logs go to stdout, and a console accepts admin commands (§6).
|
|
|
|
## 3. Players: identity and names
|
|
- **Identity:** on first run each game install generates a private key
|
|
(RSA-2048, since Godot's Crypto has no Ed25519) and keeps it in
|
|
`user://identity/key.pem`, with the name and appearance in
|
|
`user://identity/profile.json`. The public key's hash is
|
|
the **player id**. On joining, the client signs a server-chosen challenge,
|
|
so nobody can claim someone else's id. No passwords and no central
|
|
accounts.
|
|
- **Name:** the first time you open the game it asks for a **display
|
|
name**, which you can change later in Settings.
|
|
- 3-16 characters: letters, digits, space, `_` and `-`. No leading or
|
|
trailing spaces, and no names that only differ by case.
|
|
- Shown on a **name tag over your character** to other players (a
|
|
billboarded `Label3D`, fading with distance, hidden behind walls), in
|
|
the player list (Tab) and in chat.
|
|
- A server **reserves a name for the player id that first used it**, so
|
|
someone else can't join as "Paul". If your name is taken there, you're
|
|
asked to pick another. Admins can rename and release names.
|
|
- **Character:** players need a visible body (today there's none). M2
|
|
includes a simple procedural low-poly character with idle, walk, run,
|
|
swim and swing poses, plus a held-item attachment. v1 appearance is
|
|
deliberately limited: tunic colour and hair colour from small palettes,
|
|
and a beard on/off. It's stored with your identity and sent on join.
|
|
|
|
## 4. Server preferences
|
|
One file, `server.json`, created with defaults on first run and editable
|
|
by hand. Admins can also change most settings in game (§6). `*` marks
|
|
settings fixed when the world is created.
|
|
|
|
```json
|
|
{
|
|
"server_name": "Paul's island",
|
|
"motd": "Welcome! Be nice to the wolves.",
|
|
"port": 24242,
|
|
"max_players": 8,
|
|
"password": "",
|
|
"world": "valley-of-us",
|
|
"worldlet": "valley", "_note": "* (see WORLDLETS_DESIGN.md)",
|
|
"seed": 1337, "_note2": "*",
|
|
|
|
"pvp": false,
|
|
"raiding": false,
|
|
"locks": "owner_and_friends",
|
|
"death_penalty": "grave",
|
|
"sleep": "all_online",
|
|
"pause_when_empty": true,
|
|
"day_length_minutes": 20,
|
|
"wolf_grace_days": 3,
|
|
"difficulty": "normal",
|
|
|
|
"autosave_minutes": 2,
|
|
"backups_kept": 10,
|
|
|
|
"admins": [], "_note3": "empty = first player to join becomes admin",
|
|
"allowlist": [],
|
|
"banned": []
|
|
}
|
|
```
|
|
|
|
What each rule means:
|
|
| Setting | Values | Effect |
|
|
|---|---|---|
|
|
| `pvp` | false / true | Whether players can hurt each other. With it off, weapons pass through other players. |
|
|
| `raiding` | false / true | Whether players can damage or dismantle **other players'** pieces. |
|
|
| `locks` | `off` / `owner` / `owner_and_friends` | Who may open or take from a chest, use a door or bed, or dismantle a piece. Pieces remember who placed them. |
|
|
| `death_penalty` | `grave` / `keep` / `drop_all` | Today's grave bag; keep everything; or drop it all on the ground (hardcore-ish). |
|
|
| `sleep` | `all_online` / `half` / `off` | When a night can be slept through. |
|
|
| `pause_when_empty` | true / false | Freeze the world when nobody's on (fires don't burn out, no time passes). |
|
|
| `wolf_grace_days` | 0-10 | Days before wolves arrive; hunts start the night after. |
|
|
| `difficulty` | `easy` / `normal` / `hard` | Multipliers on hunger and diet drain, animal damage and pack size. |
|
|
|
|
Common setups:
|
|
- **Co-op (default):** PvP off, raiding off, locks for owner and friends.
|
|
- **Friendly rivals:** PvP on, raiding off. You can fight, but bases are
|
|
safe.
|
|
- **Adversarial:** PvP on, raiding on, locks for owners only, and
|
|
`drop_all` on death.
|
|
|
|
**Friends** are per player: a player lists other players (from the player
|
|
list) whose doors and chests they share. It's stored on the server.
|
|
|
|
## 5. Who owns what (the authority model)
|
|
| State | Owner | Clients get |
|
|
|---|---|---|
|
|
| Terrain, scenery, den site, weather | regenerated from worldlet + seed (deterministic) | nothing to sync except the seed, the clock and edits |
|
|
| Day/night clock | server | the time, every few seconds (clients interpolate) |
|
|
| Trees, rocks, pickups, bushes: damage, felled, regrowth | server | events (`node_hit`, `node_felled`, `regrown`), plus the full list on join |
|
|
| Building pieces, chests, fires, doors, beds, graves, dropped items | server (with an owner per piece) | create, remove and change events, plus a snapshot on join |
|
|
| Animals and wolves (AI, health) | server | position, yaw and state at 10-20 Hz; clients interpolate and animate |
|
|
| Projectiles, fishing floats | server | spawn events; clients simulate the flight visually |
|
|
| Each player's position | **client-predicted**, server-validated | everyone else sees it interpolated |
|
|
| Each player's inventory, stats, diet, buffs, cloak, bed | server | the owning client only (others see the held item and name) |
|
|
|
|
**Intents to RPCs.** Each of today's `Actions.*` intents (craft, pickup,
|
|
harvest_hit, place, dismantle, toggle_door, use_bed, melee, throw_spear,
|
|
shoot, butcher, cast_line, reel, wear, take_off, forage, eat, add_fuel,
|
|
transfer...) becomes a client-to-server RPC. The server runs the same
|
|
function it runs today, with the server's copy of the player, then sends
|
|
back the results. The rules don't move; only who calls them.
|
|
|
|
**Movement.** The client moves its own player immediately and sends inputs
|
|
(30 Hz). The server simulates the same inputs and sends back authoritative
|
|
positions. The client corrects only if it's off by more than a small
|
|
tolerance, which keeps things smooth on Tailscale and home links. Basic
|
|
anti-cheat: the server rejects moves faster than sprint, or through walls
|
|
and the world boundary.
|
|
|
|
**Interest.** On a 320 m worldlet with 8 players, the server sends every
|
|
entity to everyone at first. Distance-based interest is a later
|
|
optimisation.
|
|
|
|
## 6. Admin and console
|
|
In-game chat starts with `/`; the server console takes the same commands
|
|
without it.
|
|
- `/players`, `/kick <name>`, `/ban <name>`, `/unban`, `/admin <name>`
|
|
- `/save`, `/backup`, `/time 08:00`, `/weather clear|rain|storm|snow`
|
|
- `/rules` (show) and `/set pvp on` (change at runtime, saved to
|
|
`server.json`)
|
|
- `/rename <old> <new>`, `/release-name <name>`
|
|
- `/say <message>` (a server message)
|
|
|
|
Chat: `T` to talk. It's global, so everyone on the server sees it (no
|
|
proximity chat in v1).
|
|
|
|
## 7. Saves on the server
|
|
```
|
|
worlds/<world>/
|
|
meta.json world name, worldlet, seed, created, play time
|
|
world.json shared state: pieces (with owner ids), resource nodes,
|
|
pickups, regrowth, dropped items, clock
|
|
players/<player id>.json position, inventory, stats, diet, buffs, cloak, bed,
|
|
friends, name, appearance
|
|
backups/<timestamp>/ rolling copies (backups_kept)
|
|
```
|
|
- Single-player saves (today's `world.json` with a `"player"` block) are
|
|
migrated on first load. That block becomes the local player's file. The
|
|
test suite keeps an old-format save fixture to prove it.
|
|
- Per-player graves already work, since graves are pieces. With
|
|
`death_penalty: grave` they get an owner, and only the owner (and friends,
|
|
depending on locks) can loot them.
|
|
|
|
## 8. What has to change in the code
|
|
Today the code assumes one player in a few places. Each becomes "every
|
|
player" or "this player":
|
|
- `world.player` holds a dictionary of players by peer, and the local
|
|
player is a client-side notion.
|
|
- Wildlife spawns around each player within the budget. Wolves target the
|
|
nearest player who is outside, unprotected and not by a fire.
|
|
- The fishing float, the bow draw, the build ghost and the hands are per
|
|
player (some are client-only visuals).
|
|
- Sleeping follows the `sleep` rule, and it fast-forwards for everyone.
|
|
- HUD toasts become per-player server messages (`Actions.notify` gets a
|
|
target player).
|
|
- Autosave and the window-close save happen on the server. Clients just
|
|
disconnect.
|
|
- `Saves` splits world and player files (§7).
|
|
|
|
## 9. Phases
|
|
| Phase | Delivers | Done when |
|
|
|---|---|---|
|
|
| **M2.0** | worldlet W0 (terrain from data, no visible change), recommended first | seed 1337 generates the same height grid as today; all suites pass |
|
|
| **M2.1** *(done)* | network skeleton: `--server`, ENet on 24242, Join screen (address, name), handshake with identity and challenge, name rules, player list | two clients and a headless server on one Mac connect, see the player list, and disconnect cleanly |
|
|
| **M2.2** | player bodies and name tags; predicted movement, interpolation for others | two players walk around together at 100 ms simulated latency without visible rubber-banding |
|
|
| **M2.3** | world state replicated: resource nodes, pickups, pieces, chests, fires, doors, clock, weather | one chops a tree and the other sees it fall; one builds a wall and the other walks into it |
|
|
| **M2.4** | intents as RPCs; per-player inventory, stats, diet, death and graves; per-player saves and migration | two players craft, eat, die and loot their own graves; an old single-player save loads |
|
|
| **M2.5** | wildlife and wolves server-side, projectiles, fishing, sleeping with the rule | a pack hunts two players at once; arrows hit on both screens |
|
|
| **M2.6** | server rules: PvP, raiding, locks and friends, death penalty, difficulty; admin console and chat | each preset (co-op, rivals, adversarial) behaves as described in §4 |
|
|
| **M2.7** | hosting polish: "Host game" button, systemd and launchd samples, backups, a Tailscale how-to in the docs | Paul runs it on the home server, and a friend joins over Tailscale and plays for an hour |
|
|
|
|
## 10. Testing
|
|
- **Headless multi-process tests:** a script starts a headless server and
|
|
two headless "bot" clients that send scripted intents, then checks both
|
|
clients' views against the server's.
|
|
- **Network conditions:** a test transport adds latency, jitter and packet
|
|
loss, so prediction and reconciliation are tested at 100-200 ms.
|
|
- **Rules tests:** one per server rule (PvP off means no damage between
|
|
players; raiding off means you can't dismantle a stranger's wall; locks
|
|
keep strangers out of your chest).
|
|
- **Save tests:** world and player file round trips, migration of old
|
|
saves, backups.
|
|
- The existing 9 suites keep running in single-player (loopback) mode.
|
|
|
|
## 11. Risks
|
|
- **Prediction feel** is the hard part. It gets an early phase (M2.2) so
|
|
it's tuned before everything else depends on it.
|
|
- **Sleeping and pausing with several players** can feel unfair, which is
|
|
why the rules are configurable.
|
|
- **Performance:** wildlife and wolves for 8 spread-out players. The
|
|
budget is per player but capped per world.
|
|
- **Security:** the server checks every intent (reach, cost, ownership,
|
|
cooldowns). Clients are never trusted for results. That's already how
|
|
Actions is written.
|
|
|
|
## 12. Decisions (agreed with Paul, 2026-11-04)
|
|
1. **Port:** UDP **24242** by default (configurable).
|
|
2. **Appearance in v1: limited.** Pick a tunic colour and a hair colour from
|
|
small palettes, plus a beard on/off toggle. More options later.
|
|
3. **Chat: global only.** No proximity chat in v1.
|
|
4. **Empty server: pause.** `pause_when_empty` defaults to `true`, so the
|
|
world freezes (no time, burning or regrowth) while nobody is on.
|
|
5. **Admin bootstrap: the first player** to join a new server with an
|
|
empty `admins` list becomes admin, and their id is written to
|
|
`server.json`. This is a stopgap and will be revisited before servers
|
|
are shared more widely (risk: whoever joins first wins, so start a new
|
|
server and join it yourself before telling friends the address).
|