diff --git a/docs/MULTIPLAYER_PLAN.md b/docs/MULTIPLAYER_PLAN.md new file mode 100644 index 0000000..9902ed2 --- /dev/null +++ b/docs/MULTIPLAYER_PLAN.md @@ -0,0 +1,245 @@ +# 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 +```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 + (Ed25519) and keeps it in `user://identity.key`. 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 `, `/ban `, `/unban`, `/admin ` +- `/save`, `/backup`, `/time 08:00`, `/weather clear|rain|storm|snow` +- `/rules` (show) and `/set pvp on` (change at runtime, saved to + `server.json`) +- `/rename `, `/release-name ` +- `/say ` (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// + 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/.json position, inventory, stats, diet, buffs, cloak, bed, + friends, name, appearance + backups// 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** | 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). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 994a3b3..d4c2155 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -40,6 +40,9 @@ Detailed plan in `docs/SURVIVAL_DESIGN.md` (phases M1a–M1d). ## M2 — Multiplayer · ~2–4 months **Goal:** self-hosted, authoritative, 2–8 players. +Detailed plan in `docs/MULTIPLAYER_PLAN.md` (hosting on UDP 24242 via Tailscale +or port forwarding, player identity and names, server rules for co-op/PvP, and +phases M2.0-M2.7). - [ ] Refactor state to server-authoritative (single-player becomes loopback server) - [ ] Headless dedicated server mode (`--headless -- --server`) - [ ] ENet transport, client prediction for local movement, interpolation for others