124 lines
5.0 KiB
Markdown
124 lines
5.0 KiB
Markdown
# Zomboid client-hosted → dedicated server
|
||
|
||
Convert a locally **hosted** Project Zomboid multiplayer world into a **dedicated** server layout, then run it with Docker.
|
||
|
||
The world save format is the same (B42 `Saves/Multiplayer/…`). Conversion is packaging + naming + `players.db` world rename + mod recovery — not a binary rewrite.
|
||
|
||
## Layout
|
||
|
||
Paths below are relative to the **repo root** (parent of this `converter/` folder):
|
||
|
||
| Path | Role |
|
||
|------|------|
|
||
| `original/` | Source saves / zips (**do not edit the zips**) |
|
||
| `converter/` | This crate — Rust CLI (`zomboid-converter`) |
|
||
| `server/` | Docker dedicated server (`docker-compose.yml`, `.env`) |
|
||
| `server/server-data/` | Zomboid userdir (Saves, Server, db, Logs) |
|
||
| `server/server-files/` | Game install (Steam download target) |
|
||
|
||
Typical source pair from a host:
|
||
|
||
- `Saves/Multiplayer/<WorldName>/` — authoritative world (**use this**)
|
||
- `Saves/Multiplayer/<WorldName>_player/` — client cache (**ignore**)
|
||
|
||
Server settings live separately under `Zomboid/Server/<WorldName>.*` on the host machine. They are **not** inside the world folder.
|
||
|
||
## Prerequisites
|
||
|
||
- Rust toolchain (for the converter)
|
||
- Docker + Docker Compose
|
||
- Optional: local Steam workshop cache for app `108600` (to resolve `WorkshopItems`)
|
||
|
||
## Converter
|
||
|
||
```bash
|
||
cd converter
|
||
cargo build --release
|
||
|
||
cargo run --release -- \
|
||
--input ../original/serverb42stable \
|
||
--output ../server/server-data \
|
||
--server-name pzserver \
|
||
--workshop-dir /path/to/steamapps/workshop/content/108600 \
|
||
--force
|
||
```
|
||
|
||
`--input` may be a world **directory** or a **`.zip`** (opened read-only; extracted to a temp dir). Paths ending in `_player` are rejected.
|
||
|
||
### Arguments
|
||
|
||
| Flag | Description |
|
||
|------|-------------|
|
||
| `-i`, `--input` | Hosted world dir or zip |
|
||
| `-o`, `--output` | Dedicated `server-data` root |
|
||
| `-n`, `--server-name` | World + config basename (default: `pzserver`) |
|
||
| `--workshop-dir` | `…/workshop/content/108600` — fills `WorkshopItems=` |
|
||
| `--server-config` | Optional host `Server/` dir to copy/rename configs |
|
||
| `--password` | Server join password (`Password=` in ini); overrides `.env` |
|
||
| `--env-file` | `.env` for password (default: `<output>/../.env`) |
|
||
| `--force` | Replace existing `Saves/Multiplayer/<server-name>/` |
|
||
|
||
### What it does
|
||
|
||
1. Copies the world into `output/Saves/Multiplayer/<server-name>/`
|
||
2. Skips syncthing conflict sidecars (`*-ZwS1g3qi*`), `*.db-journal`, and player-cache junk
|
||
3. Sets `networkPlayers.world` in `players.db` to `<server-name>`
|
||
4. Recovers mod IDs from `WorldDictionaryReadable.lua` into `Server/<name>.ini` (`Mods=`)
|
||
5. If `--workshop-dir` is set, resolves Steam workshop IDs (`WorkshopItems=`)
|
||
6. Optionally sets `Password=` from `--password` or `SERVER_PASSWORD` / `PASSWORD` in `.env`
|
||
|
||
It does **not** overwrite `db/<server-name>.db` (accounts / whitelist stay managed by the server).
|
||
|
||
### Characters / login
|
||
|
||
Characters bind to the **account username** in `players.db` (`networkPlayers.username`), not Steam display name. Players must log in with that exact username (or you rename the column before they join).
|
||
|
||
## Dedicated server (Docker)
|
||
|
||
```bash
|
||
cd ../server
|
||
cp .env.example .env # if needed; set passwords / SERVER_NAME
|
||
docker compose up -d
|
||
docker compose logs -f
|
||
```
|
||
|
||
Important `.env` keys:
|
||
|
||
- `SERVER_NAME` — must match converter `--server-name` (default `pzserver`)
|
||
- `ADMIN_USERNAME` / `ADMIN_PASSWORD` — admin account
|
||
- `RCON_PASSWORD` — RCON
|
||
- `SERVER_PASSWORD` — optional join password for the converter (`Password=` in ini)
|
||
|
||
Ports: `16261/udp`, `16262/udp`, `27015/tcp`.
|
||
|
||
Stop the container before re-running the converter with `--force`.
|
||
|
||
## Recommended migration flow
|
||
|
||
1. Stop the dedicated server.
|
||
2. Copy the host world into `original/` (keep zips untouched as backups).
|
||
3. Ideally also copy the host’s `Zomboid/Server/<WorldName>.ini` (+ `_SandboxVars.lua`, spawn files) and pass `--server-config`.
|
||
4. Run the converter with `--workshop-dir` and `--force`.
|
||
5. Start Docker; wait until workshop items finish installing (`Workshop: … Installed` in logs).
|
||
6. Join with the **same username** as in the hosted `players.db`.
|
||
|
||
## Known gaps / caveats
|
||
|
||
| Item | Notes |
|
||
|------|--------|
|
||
| Host `SandboxVars` / spawn / `Map=` | Not in the world save. Without `--server-config`, the dedicated defaults remain. |
|
||
| Mods with no WorldDictionary entries | Pure Lua/QoL mods may not appear in recovered `Mods=`. |
|
||
| Workshop **versions** | IDs are resolved from your local workshop cache; newer mod builds than the host used can cause chunk `LENGTH mismatch` / `blam` errors and odd world state (e.g. zombies in cleared bases). |
|
||
| `ZombieMigrate` | Dedicated sandbox may allow migration even when respawn is off; align sandbox with the host when possible. |
|
||
| Running without mods first | Can rewrite `WorldDictionary` and map chunks. Re-import from `original/` if that happened. |
|
||
|
||
## Example passwords
|
||
|
||
```bash
|
||
# ../server/.env (next to docker-compose)
|
||
SERVER_PASSWORD=hunter2
|
||
|
||
# or CLI override (from this directory)
|
||
cargo run --release -- … --password 'hunter2'
|
||
```
|