# Multiplayer

VoxelWorld's multiplayer mode uses an authoritative dedicated server. The
server authenticates each player and decides gameplay outcomes; clients send
input-like intents and render the resulting state. Clients still generate the
deterministic base terrain locally from the seed provided by the server, so the
headless server does not render or mesh chunks.

## What the server owns

Protocol v10 makes the server authoritative for:

- whether an account should receive its one-time opening introduction;
- each player's inventory, crafting cursor, selected hotbar slot, health,
  hunger, fire and air timers, drowning, fall/void damage, respawn, and
  administrator fly state;
- dropped-item creation, motion, pickup, despawn, and persistence;
- arrow and boss projectiles, weapon/ammo use, PvP hits, and damage;
- shared pigs, chickens, and fireflies outside private boss instances;
- shared, persistent campfire storage, cooking progress, and fire contact;
- each account's private crater forcefield unlock, boss, terrain, water, drops,
  and victory progress;
- shared world time and weather; and
- persistent block edits, bucket inventory changes, water-source decisions,
  flowing-water simulation, the shared starter chest, persistent attributed
  player chat, and the world seed.

Players have independent inventory, health, and hunger records. Shared objects
such as a dropped item or projectile have one server identity and one outcome,
so two clients cannot legitimately pick up or resolve the same object in
different ways. Crater-instance objects carry the owning account's scope and
are never exported to another player.

Fire follows the same survival boundary. The server tests each accepted player
pose against crater flames, placed campfires, and authoritative water, keeps
the ten-second burn tail, and applies one point of damage every two seconds.
Snapshots carry the server-tick burn deadline so the warning and screen flash
cannot become a client-authored health decision.

The server also tracks the player's underwater air, applies drowning and
landing damage, detects the world floor, and chooses the respawn pose. The
client mirrors those results instead of reporting health or claiming a safe
landing.

Campfires are shared server containers. Their input, fuel, output, burn time,
and cook progress have one canonical state visible to nearby users and survive
restarts. Placement, dismantling, item transfers, cooking, and contact fire are
validated and advanced by the server.

The purple crater forcefield encloses a per-account boss instance. A Magical
Arrow unlocks that account's border; crossing it hides the player from everyone
in the shared world and hides shared players from the entrant. Clients show a
smoke puff as either side of the visibility seam is crossed. The boss, its
projectiles and drops, terrain edits, flowing water, and completion progress
inside the boundary belong only to that account and persist across reconnects.
Leaving restores the shared outside world. This deliberately makes the current
crater fight solo while ordinary multiplayer continues outside it.

Chat requests contain only a monotonic request id and message text. The server
derives the author from the authenticated session, normalizes and rate-limits
the text, commits it to SQLite, then broadcasts the canonical message. A
joining client receives the latest bounded history before live chat can pass
it, so reconnects do not create a history gap.

Administrator fly mode is an explicit server-owned session state, not a client
movement claim. Only while that state is enabled does the server allow the
larger movement envelope, freeze hunger/eating, and permit instant removal
inside the spawn ward. Spawn placement remains protected, and the one
server-owned starter chest is shared by every player and cannot be removed.

The opening wizard scene is one-time account onboarding, not a device setting.
The server atomically claims it on the account's first accepted login and tells
that client to play it; reconnecting from the same or another computer does not
replay it.

Water follows the same rule: the server alone fills/empties bucket inventory,
collects or places source cells, and advances the five-tick fluid cadence.
Sources formed between two existing sources are persisted as server edits too.
Snapshots carry explicit bounds and the exact mutable water cells inside them,
so a late join or slower client sees the server's current flow rather than
starting its own copy of the flood. If an unusually dense flood cannot fit the
normal region in one frame, the server sends the largest complete inner region;
the client retains its recent outer halo and evicts water only after it leaves
the fixed 13-chunk interest area.

Clients send `GAME_ACTION` intents for actions such as inventory clicks, drops,
block interactions, bucket use, bow use, melee attacks, and eating. The server
validates them against the authenticated player's current state, then sends
each session its own `GAME_STATE_SNAPSHOT` frames. A client cannot award itself
an item, report its own hit, choose its health, or replace shared entity state.

The headless simulation runs at 20 ticks per second. Client poses use a bounded
monotonic-time movement-credit reserve, so a short server tick or lock stall
does not misclassify queued sprint samples as a speed hack. Every decoded,
in-window pose sequence is consumed even when its movement is rejected, and an
explicit correction frame returns the authoritative position. This prevents a
rejection streak from permanently locking movement while preserving replay and
far-ahead sequence checks.

The current design sends
bounded full entity snapshots plus nearby water state rather than general
spatially filtered deltas. That is a clear and testable baseline for a few
players on a LAN. Remote players render from a short buffered timeline with
bounded extrapolation keyed by the authoritative server tick, while projectile
mirrors reconcile stable server ids and move on the server-tick timeline with
short trajectory prediction. Packet arrival spacing therefore does not set
their apparent speed. Teleports,
instance visibility, impacts, embedding, damage, and despawn remain server
decisions.

Socket writes never run on the simulation tick. Each session has a bounded
outbound queue; superseded game snapshots coalesce without crossing ordered
world-edit barriers, and a client that fills its queue is disconnected instead
of stalling everyone else. Every 30 seconds the server prints a
`SERVER_METRICS` line containing connected/ready sessions, entity counts,
tick, snapshot-encoding, and checkpoint latency percentiles, snapshot bytes and
coalescing totals, and current/maximum/full outbound-queue counts. These
measurements are the initial baseline for load tests, not a replacement for
production monitoring.

Larger populations will still need broader interest management, delta
compression, and more spatial indexing.

## Basic anti-cheat model

The server rejects malformed or impossible input before changing gameplay
state. Checks include:

- strictly increasing action request ids, which make duplicate/replayed
  mutations harmless;
- expected inventory revisions, so stale clicks cannot overwrite newer state;
- per-session pose and action rate limits;
- finite coordinates, world bounds, movement-speed limits, swept terrain
  collision, grounded jump/fall limits, and pitch limits;
- interaction reach and action-specific cooldown checks; and
- valid slot, item, block, face, count, and entity identifiers.

Those checks are defense in depth, not a promise that the alpha is
cheat-proof. The server remains the only authority even when a client predicts
movement or presentation locally. New gameplay messages should continue to
describe what the player tried to do, never the result the client wants.

## Server quick start

VoxelWorld requires JDK 25. Before the first start, initialize the database and
replace the bootstrap password:

```sh
./dev server --args='--change-password=admin'
```

The command prompts for the new password without putting it in shell history.
Then start the server:

```sh
./dev server
```

`./dev server` first starts the dedicated Docker Tailscale service, restores
its persisted Funnel routes, verifies them, and only then starts the Java game
server. On the first run, it pauses and prints a Tailscale sign-in URL. Approve
this Mac once, then run `./dev server` again; later starts reuse the identity in
the `voxelslop-tailscale-state` Docker volume.

Public players enter `play.projectslop.xyz`. DNS SRV selects
`vs.tailf3317e.ts.net:443`, where Funnel terminates TLS and forwards the stream
to the local game listener on port `25565`. Funnel also exposes the read-only
health listener as HTTPS on `vs.tailf3317e.ts.net:8443`.

In another terminal, start a direct local development client:

```sh
./dev play
```

The Java server also listens for direct LAN gameplay on port `25565`, while the
development client connects directly to `127.0.0.1:25565` by default. Direct
connections are plaintext and are for local/LAN development only. A brand-new
server database creates one bootstrap account:

```text
Username: admin
Password: admin
```

> **Security warning:** `admin` / `admin` is a temporary bootstrap credential.
> Change it with the command above before allowing untrusted devices onto the
> LAN or publishing access through Funnel. Never forward raw port `25565` from
> the router.

To test with another player, stop the server and create a distinct account:

```sh
./dev server --args='--create-user=friend'
```

The command prompts for a password and grants the account game access. Restart
the server afterward. Two clients cannot control one account at the same time:
a successful newer login atomically takes ownership and closes the older
session, then receives fresh authoritative state.

The server creates `server-data/world.seed` and
`server-data/voxelworld.db`. Keep that directory to preserve the world identity,
accounts, player/campfire/private-instance state, shared edits, drops, and chat.
Back it up while the server is stopped.

For a loopback-only test with no public routes, run:

```sh
./dev server-local
```

That command turns off both persisted Funnel routes, stops the dedicated
Tailscale container, binds both Java listeners to `127.0.0.1`, and starts the
server. `./dev funnel-status` verifies and displays the public routes without
starting Java. `./dev funnel-off` disables the routes and stops the container
without starting Java.

## Connection options

The `dev` helper aliases still accept normal Gradle options:

| Purpose | Development command | JVM system property |
| --- | --- | --- |
| Client host | `-PclientHost=HOST` | `voxelworld.client.host` |
| Client port | `-PclientPort=PORT` | `voxelworld.client.port` |
| Client transport | `-PclientTransport=direct\|tls-srv` | `voxelworld.client.transport` |
| Expected TLS/SRV target | `-PclientExpectedTlsHost=HOST.ts.net` | `voxelworld.client.expectedTlsHost` |
| Server bind address | `-PserverBind=ADDRESS` | `voxelworld.server.bind` |
| Server port | `-PserverPort=PORT` | `voxelworld.server.port` |
| Health HTTP bind | `-PserverHealthBind=ADDRESS` | `voxelworld.server.healthBind` |
| Health HTTP port | `-PserverHealthPort=PORT` | `voxelworld.server.healthPort` |
| Health HTTP enabled | `-PserverHealthEnabled=true\|false` | `voxelworld.server.healthEnabled` |
| Server data directory | `-PserverDataDir=PATH` | `voxelworld.server.dataDir` |

For example:

```sh
./dev server-local -PserverPort=25565 \
  -PserverDataDir=server-data
./dev play -PclientHost=127.0.0.1 -PclientPort=25565
```

Plain `./dev server` uses `0.0.0.0:25565` for gameplay and
`127.0.0.1:25566` for health after ensuring the public Funnel routes. The
`server-local` example above explicitly fixes both listeners to loopback. The
server starts its read-only HTTP status endpoint on the game port plus one by
default, so the normal local URL is:

```text
http://127.0.0.1:25566/health
```

Verify it without logging in:

```sh
curl --fail --no-cache http://127.0.0.1:25566/health
```

The compact JSON reports only availability, protocol/game versions, uptime,
current player and session counts, the configured connection limit, and a UTC
timestamp. It has no mutation or administration methods, returns `405` for
methods other than `GET`, `HEAD`, and CORS preflight `OPTIONS`, allows
cross-origin website probes, and sends `Cache-Control: no-store`.

The packaged applications use command-line options instead:

```sh
bin/VoxelWorldServer --bind=127.0.0.1 --port=25565 \
  --health-bind=127.0.0.1 --health-port=25566 \
  --data-dir=server-data
bin/VoxelWorld --server 127.0.0.1:25565
bin/VoxelWorld --public-server play.projectslop.xyz
```

Use `--no-health` (or `-PserverHealthEnabled=false` during development) to
disable status HTTP entirely. Run `bin/VoxelWorldServer --help` for the full
option list.

In direct mode, the multiplayer login screen accepts `HOST[:PORT]` in its
server field, including bracketed IPv6 such as `[::1]:25565`. TLS-SRV mode
accepts a DNS hostname without a port because the SRV record selects the
destination. Command-line and environment settings supply that field's initial
value.

Official packaged clients instead start with `play.projectslop.xyz`, port
`443`, and the `tls-srv` transport. The development defaults above do not
change that release configuration, and plain `./dev play` remains a direct
local connection to `127.0.0.1:25565`. The explicit `--server` option always
selects direct plaintext mode; `--public-server` deliberately selects TLS-SRV
mode and accepts only a hostname.

Packaged account-management commands use the same server executable:

```sh
bin/VoxelWorldServer --change-password=admin
bin/VoxelWorldServer --create-user=friend
```

Run `bin/VoxelWorldServer --help` for its accepted options. Client credentials
are entered in the login screen; never put passwords in command-line arguments
or JVM properties.

## Public hosting and LAN testing

The normal hosting command is:

```sh
./dev server
```

The persistent Tailscale identity gives this Mac the stable public certificate
name `vs.tailf3317e.ts.net`. Funnel connects outbound through the current
network, so it continues to work when the ISP/router public IP changes and
through CGNAT. Do not configure router port forwarding, dynamic DNS, or a
public-IP updater. The domain stays at its current registrar; its one SRV record
maps the player-facing name to the stable Funnel endpoint:

```text
_voxelslop._tcp.play.projectslop.xyz.  SRV  0 0 443 vs.tailf3317e.ts.net.
```

Public game traffic uses certificate-verified TLS on Funnel port `443`, which
forwards to the Java listener on host port `25565`. Public website status uses
HTTPS on Funnel port `8443`, which forwards to the local health listener on
host port `25566`:

```text
https://vs.tailf3317e.ts.net:8443/health
```

The Funnel container has `restart: unless-stopped`, and its device identity and
routes live in a named Docker volume. Use `./dev funnel-status` to inspect the
routes and `./dev funnel-off` to take the public entry points down explicitly.

### macOS menu-bar server control

On macOS, `./dev server` also opens a Dock-less Voxel Slop controller in the
top-right menu bar. The light follows the same public availability contract as
the website: green requires a valid response from the public HTTPS health
endpoint; red means the server is offline or is running locally without a
working public route. Its menu shows the current player count and provides
Server On, Server Off, Server Status, Refresh Status, and Open Server Log.

The same operations are available from a terminal:

```sh
./dev tray
./dev server-on
./dev server-off
./dev server-status
```

Server On first builds the installed headless distribution, ensures Funnel is
ready, then launches the server through a non-restarting per-user macOS
service and waits for public health. It refuses duplicate starts and occupied
ports. Server Off verifies that both the service and listener belong to this
checkout, unloads the service, waits for authoritative state to finish saving,
and only then disables Funnel. It never force-kills an unverified or
slow-stopping process. Quitting the menu-bar controller does not stop the
server.

For a direct LAN-only client test, another computer can connect to the server
Mac's LAN address, for example:

```sh
./dev play -PclientHost=192.168.1.50 -PclientPort=25565
```

`0.0.0.0` is only a server bind address. It is not an address that clients can
connect to. To run a loopback-only server and turn Funnel off first, use
`./dev server-local`.

Allow inbound **TCP** traffic to port `25565` in the server computer's firewall
only when LAN clients need the direct development path. Even with
server-authoritative gameplay, do not forward that raw plaintext port to the
public internet.

The development task deliberately keeps health on `127.0.0.1` even though
gameplay accepts LAN connections. Funnel is the only public path to it and
adds HTTPS; raw port `25566` is not opened on the router. The packaged server
remains conservative: unless `--health-bind` is supplied, its health listener
follows its game bind. The health endpoint intentionally contains no
credentials, usernames, network addresses, world data, or writable controls.

## Transport security

The wire protocol remains length-delimited, versioned TCP. Official public
clients do not send it directly to the server port. They query
`_voxelslop._tcp.play.projectslop.xyz`, require the DNS SRV target to match the
exact `*.ts.net` hostname pinned into that client release, connect to the
record's TLS port, and validate the certificate and hostname before writing any
login bytes. A stale release or unexpected DNS target therefore fails closed
instead of silently following a redirect.

Tailscale Funnel terminates the public TLS connection in its dedicated Docker
service and forwards the stream to `host.docker.internal:25565`. PROXY protocol
v2 preserves the remote source address for rate limiting; the game server
accepts a forwarded address only when the immediate transport peer is loopback.
Plain localhost and LAN development clients still speak direct, unencrypted
TCP and remain compatible because they send the normal game frame first.

This protects credentials and gameplay in transit on the public path, but it
is not a complete production account system. Registration, password recovery,
session tokens, operational key rotation, and a broader abuse-response system
still need to be built.

## Packaging

Build both player clients and the server archive:

```sh
./dev package
```

The tracked release default is the exact SRV target
`vs.tailf3317e.ts.net`—not the branded `play.projectslop.xyz` name—so ordinary
builds need no environment setup. During a deliberate endpoint migration, the
pin can be replaced explicitly with
`-PpublicExpectedTlsHost=new-host.ts.net` or
`VOXELSLOP_PUBLIC_EXPECTED_TLS_HOST=new-host.ts.net`. Packaging rejects a
malformed value or one outside `*.ts.net`.

On an Apple Silicon Mac this produces:

```text
build/distributions/VoxelWorld-0.3.17-client-macos-arm64.zip
build/distributions/VoxelWorld-0.3.17-client-windows-x64.zip
build/distributions/VoxelWorld-0.3.17-client-windows-x64-setup.exe
build/distributions/VoxelWorld-0.3.17-server.zip
```

The normal client archive is labeled for the OS and CPU that ran Gradle because
it contains that host's LWJGL native libraries. The dedicated Windows x64 task
cross-packages Windows-only LWJGL libraries from macOS, wraps that verified
client in a guided NSIS installer, builds an icon-bearing GUI-subsystem
`Voxel Slop.exe`, and verifies that no macOS libraries, first-thread JVM
option, or server payload leaked into it. The native executable starts the
bundled `javaw.exe`, so the player sees the graphical diagnostic wrapper
instead of a Command Prompt window. Both client-launcher verification paths
also require the branded host, TLS-SRV transport, port, and pinned Funnel
target. The server archive is platform-independent Java and intentionally
contains no LWJGL or native jars.

After extracting an archive, use `bin/VoxelWorld` for macOS,
`Voxel Slop.exe` for Windows, or `bin/VoxelWorldServer` for the server.
Windows players should normally run the setup executable and use its Start
Menu shortcut. Both Windows packages bundle an app-local Eclipse Temurin
Java 25 JRE, contain no batch launchers, and do not require system Java. The
macOS client and dedicated server still need a 64-bit JDK 25. The unsigned
Windows installer will normally trigger a Microsoft Defender SmartScreen
warning until releases are code-signed. The installer and launcher PE
structures, GUI subsystem, icon payload, bundled runtime, and embedded client
can be verified on macOS, but the first graphical smoke test should still run
on a Windows x64 machine with no usable system Java.

To publish refreshed macOS and Windows clients to the companion website from
the Apple Silicon development Mac:

```sh
./dev release-site
```

This also regenerates the website's release manifest, displayed file sizes,
and SHA-256 checksums, then runs the website build and tests. Pass
`-PsiteDir=/absolute/path/to/VoxelSlopSite` if the site is not in its default
Documents/Github location.

## Current limitations

- Public packages use certificate-verified TLS with an SRV target pinned per
  release, but direct development/LAN connections are plaintext. There is no
  in-game registration, account recovery, session-token flow, or automated
  operational handling for Tailscale device-key expiry. Funnel manages its
  public TLS certificates; account creation and password changes remain
  server-console operations.
- Authority checks prevent basic forged inventory, duplicate-action,
  impossible-reach, speed/fly-through-wall, and client-reported-damage cheats.
  They do not yet provide lag compensation, anomaly scoring, automated
  sanctions, or a production moderation system.
- The 20 TPS server sends bounded full snapshots. There is no spatial interest
  management, entity delta stream, sharding, clustering, or large-population
  load balancing yet.
- A rejected gameplay intent is followed by canonical state. A disconnect
  returns the client to login so it cannot continue in an unsaved local fork.
- Directly extinguishing a procedural crater flame is still standalone-only.
  Multiplayer consumes that click without hiding the server-owned hazard;
  entering authoritative water still clears the player's burn immediately.
  Durable per-account crater-flame extinction has not been added yet.
- Player survival and inventories, shared drops, chest and campfire contents,
  world state, accepted block edits, attributed chat, and private crater
  progress/terrain/water/drops are durable SQLite state. A restart cannot
  restock the starter chest, lose recent chat, or expose both sides of an
  interrupted item transfer.
- Clients generate base terrain and chunk meshes locally from the server seed.
  The server distributes the seed and saved overrides rather than chunk mesh
  data.
- Reconnect and newest-login takeover receive a fresh authoritative snapshot;
  the transport does not yet resume an interrupted stream or preserve in-flight
  client intents.
- SQLite and the seed file are local to one server process. Schema migrations
  exist, but operational backup, replication, failover, monitoring, and abuse
  controls are not implemented.
