Metadata-Version: 2.4
Name: dst-server
Version: 0.5.0
License-File: LICENSE
Summary: Native SDK and room agent for Don't Starve Together.
License-Expression: MIT
Requires-Python: >=3.14, <3.15
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# Don't Starve Together Dedicated Server

[![Steam](https://img.shields.io/badge/Steam-000000?logo=steam&logoColor=white)](https://steamcommunity.com/groups/lst99)
[![Discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/4N3aeNsFt8)

English | [简体中文](README.zh-Hans.md)

A Rust SDK, Agent and CLI for Don't Starve Together, with Python bindings.
Each room runs in one container: one Agent supervises every configured shard and exits with the games when the room closes.
An external scheduler starts the container at each opening.
Published binaries and Python wheels support Linux x86_64 and CPython 3.14.
The control socket is for trusted callers on the same host.

Version `0.4.0` replaced the earlier Python implementation.
Version `0.5.0` exits each container when its room closes and uses external scheduling for the next opening.
See the [migration guide](docs/migration.md) for API changes, verification and known limitations.
Internal readiness does not establish public network reachability.

Install the Python SDK with `pip install dst-server==0.5.0` in a CPython 3.14 environment.
The [GitHub release](https://github.com/LetsStarveTogether/dst-server/releases/tag/v0.5.0) also includes a standalone Linux x86_64 CLI.

## Build and Create a Room

Install Rust 1.99, a C/C++ build toolchain, Cap'n Proto, and [uv](https://docs.astral.sh/uv/).
Host deployment additionally requires Podman with Quadlet and systemd.
From this checkout:

```sh
cargo build --locked --release -p dst-server --bin dst-server
export PATH="$PWD/target/release:$PATH"
uv sync --python 3.14
dst-server --help
uv run python -m dst_server --help
```

The native binary and the Python entry point call the same Rust CLI.
Build the game image from this revision, supplying the exact game build that SteamCMD will install:

```sh
podman build --target game --build-arg GAME_VERSION=756039 \
  -t localhost/dst-server:rust .
```

The build checks the installed game version and packages the matching SDK Lua bundle.
Use the matching published image when creating rooms, or select your locally built image.

The default deployment image is `quay.io/wh2099/dst-server:latest`.
Set `deployment.image` or `--image` to `quay.io/wh2099/dst-server:beta` for beta.
Generated Quadlets keep these tags and set `AutoUpdate=registry` for both published channels.
Quadlets use `Pull=never`; pull the selected image before the first start.
Run `systemctl enable --now podman-auto-update.timer` on the host to enable periodic checks.
The timer updates containers and restarts them when a new image is available.

Create a Klei server token and store it in `/run/secrets/dst_cluster_token`.
Run the following with the account that owns the host room directories and systemd services:

```python
import asyncio
from pathlib import Path

from dst_server import Host
from dst_server.settings import Room, build_template


async def main():
    cluster = build_template(
        "pure_survival",
        number=299,
        token=Path("/run/secrets/dst_cluster_token").read_text().strip(),
    )
    room = Room(
        number=299,
        template="pure_survival",
        cluster=cluster,
        deployment={
            "image": "localhost/dst-server:rust",
            "volume_idmap": "uids=0-1000-1;gids=0-1000-1",
        },
    )
    await Host("/srv/dst", "/etc/containers/systemd").create(room)


asyncio.run(main())
```

This creates `/srv/dst/299` and `dst-299.container` without starting the service.
The illustrated ID mapping gives image UID/GID 1000 access to root-owned room files.
Choose ownership or mapping for your host.
Then start the service:

```sh
dst-server host start --room 299
dst-server host status --room 299
dst-server call list_players --socket /srv/dst/299/.dst-agent.sock
journalctl -u dst-299.service -n 100 -f
```

Use `host --root PATH --quadlet-dir PATH` for alternate directories, and `host --user` for the user systemd manager.
Room numbers range from `000` to `299` independently of gameplay templates.
The packaged fleet contains `000–099` and `200–219`:

```sh
dst-server template list
dst-server host fleet --room 000,030,209 \
  --token-file /run/secrets/dst_cluster_token --image localhost/dst-server:rust \
  --volume-idmap 'uids=0-1000-1;gids=0-1000-1'
```

## CLI

| Command | Purpose |
| --- | --- |
| `host` | Create, provision, edit, inspect and operate host services; batch by room, template or all rooms. |
| `call` | Invoke a room or shard operation through its Agent socket. |
| `describe` | Print method names, scope, argument schemas and default timeouts. |
| `subscribe` | Read bounded `logs`, `lifecycle` or `events` batches and their discarded counts. |
| `logs`, `archive` | Query or follow historical logs; export and upload consistent room archives. |
| `console` | Execute trusted Lua on an explicit shard, from text, a file or interactive input. |
| `config`, `template`, `inspect` | Validate configuration, inspect schemas and templates, discover native topology. |
| `scripts`, `annotations`, `completion` | Build or verify script bundles, generate Lua annotations and shell completion. |
| `agent` | Run the room supervisor, normally as the container entry point. |

Commands print JSON; `--json` uses compact JSON and structured errors.
A batch preserves individual results and fails its exit status when any target fails.
JSON arguments accept inline text, `@filename`, or `-` for stdin.

```sh
dst-server --json host list
dst-server host show 299 --field /cluster/settings/max_players
dst-server host schema
dst-server host diagnose --room 299
dst-server describe
dst-server call save --socket /srv/dst/299/.dst-agent.sock
dst-server call get_player '{"userid":"KU_example"}' --socket /srv/dst/299/.dst-agent.sock
dst-server subscribe lifecycle --socket /srv/dst/299/.dst-agent.sock
```

`call stop` gracefully closes every shard, then exits the Agent and container with code `0`.
`host stop --room 299` stops the container service, including its Agent.
Either stop permits the independent external scheduler to start the service at its next trigger.
`call restart` confirms the stop and requests container replacement; its reply does not confirm the next startup.
Use `host start` or `host restart` to wait for all shards to become ready.
Outside opening hours, startup exits successfully with the room closed.
Recovery that remains exhausted is reported as an error.
Stop the service before offline configuration changes:

```sh
dst-server host stop --room 299
dst-server host edit --room 299 --set '/cluster/settings/max_players=9'
dst-server host start --room 299
```

After container exit, `host status` reads the saved policy, current schedule, maintenance and recovery state.
Changing world-generation settings preserves existing saves; `regenerate` creates new worlds.

## Rust and Python SDKs

The native API exposes typed requests without exposing Cap'n Proto generated types:

```rust
use dst_server::{model::{Envelope, Request, Target}, rpc::Client};

async fn inspect_room() -> dst_server::model::Result<serde_json::Value> {
    let client = Client::connect("/srv/dst/299/.dst-agent.sock").await?;
    let result = client.call(Envelope::new(Target::Room, Request::Status {})?).await;
    client.close().await?;
    result
}
```

Python exposes asynchronous calls and explicit connection and subscription cleanup:

```python
import asyncio
from dst_server import Client


async def main():
    async with await Client.connect("/srv/dst/299/.dst-agent.sock") as client:
        print(await client.call("status"))
        print(await client.call("save"))
        async with await client.subscribe("events") as events:
            print(await events.next(max_items=100))


asyncio.run(main())
```

Use `describe()` or `dst-server describe --shard` for the exact method contract.
Operations cover world queries, saves, rollback, regeneration, pause and player control.
They also cover migration, permissions, Mods and exports.
Player operations locate `userid` across shards and verify its identity before execution.
Pass `shard="forest"` when an operation needs an explicit shard.
Entity references contain world session and runtime generation; old references become invalid after reload.

Each room permits one mutation at a time and returns `busy` for a conflicting operation.
Accepted mutations belong to the Agent.
Cancellation, disconnection and caller timeouts stop waiting without undoing the operation.
Check `status`, or `host status` after container exit, before deciding whether to retry an unknown result.
Save success requires native completion on every shard.
Graceful shutdown sends one native `c_shutdown()` to each shard and lets the game perform its final save.
The game may save more than once within its native shutdown path.
Normal shutdown requires exit code zero, process reaping and output drainage, with no force or protocol fault.
Failed shutdown retains each shard's exit status and confirmed saved snapshot.
Signal termination uses the negative signal number.
The default stop wait is 220 seconds, including its notice; changing the notice delay adjusts that budget.
Mods that disable saving can stop cleanly with `saved_snapshot: null`.
An explicit save returns an error when no new snapshot is confirmed.
Migration success requires confirmation in the destination world.
Console success means the Lua code returned; an asynchronous effect started by that code may still be running.

## Configuration and Files

```text
/srv/dst/299/
├── cluster.ini
├── cluster_token.txt
├── adminlist.txt / blocklist.txt / whitelist.txt
├── .dst-control.json
├── .dst-room.lock
├── .dst-agent.sock
├── mods/
│   ├── dedicated_server_mods_setup.lua
│   ├── modsettings.lua
│   └── ugc/
├── forest/
│   ├── server.ini
│   ├── worldgenoverride.lua
│   ├── modoverrides.lua
│   └── save/
└── cave/
    └── server.ini, world overrides, Mods and saves
```

Native INI/Lua files define game configuration; the Quadlet defines deployment.
`.dst-control.json` stores the template label, policy, activity and recovery state.
It is not a second copy of game settings.
Keep it across container replacement to retain recovery limits and maintenance progress.
Never remove or replace `.dst-room.lock` while a room exists.

Every directory with `server.ini` is an enabled shard.
Names are arbitrary, with exactly one master.
Keys and IDs must be consistent, with conflict-free game, query and shard ports.
Port allocation uses the actual shard set, an allocation lock and a persistent map; the previous four-shard limit is removed.
The default host allocation pool is `30000–65535`.
All shards share the container network and room Mods; each process has separate control pipes and readers.

Python configuration factories validate through Rust:

```python
from dst_server.settings import ClusterSettings, RoomStore, build_template

settings = ClusterSettings(max_players=9, cluster_name="Friday games")
cluster = build_template("pure_endless", number=299, settings=settings)
print(cluster.dump(defaults=True))
print(settings.schema())

store = RoomStore("/srv/dst")
room = store.load(299)
updated = room.edit("/cluster/settings/cluster_description", "Friday games")
# After stopping the entire service:
# store.save(updated)
```

`dump()` redacts token, shared key and password; `secrets=True` explicitly returns them.
Rendered native files and `files()` contain real credentials and must be handled accordingly.
Defaults are applied on demand; omitted fields and explicit `None` remain distinct.
Frozen schemas, defaults and presets are packaged with Rust, including 12 templates and all 120 fleet rooms.
Rust owns validation and file operations; Python provides bindings, conversion and asynchronous wrappers.

Offline writes use the shared room lock, reject symlinks and commit recoverable file transactions with synchronization.
General configuration saves preserve live admin, ban and whitelist files.
Use permission operations for online changes.
Structured Lua reads parse Lua 5.1 literals without execution.
Dynamic configurations can be loaded by the game; this API cannot structurally edit them.

## Opening Hours, Maintenance and Recovery

The default policy uses `Asia/Shanghai`, opens all day, enables automatic Mod maintenance and disables idle world regeneration.
A policy can be sent to a running Agent:

```sh
dst-server call set_policy - --socket /srv/dst/299/.dst-agent.sock <<'JSON'
{
  "policy": {
    "timezone": "Asia/Shanghai",
    "schedule": [{"start": "18:00", "end": "00:00"}],
    "mod_auto_update": true,
    "idle_regeneration": false
  }
}
JSON
```

The Agent checks opening hours at startup and every 30 seconds while running, including windows across midnight.
At closing time it stops all games and exits the container with code `0`.
Startup outside opening hours also exits with code `0` before starting games.
Configure an independent host scheduler to activate the service at each opening; see the [systemd timer example](docs/migration.md#external-opening-timer).
Update that scheduler separately when changing policy times or timezone.
Stopping a room leaves the external scheduler active; manage the scheduler separately when its next trigger should be suppressed.
Pending Mod maintenance runs in the next container's startup before opening games.
`call update_mods` queues maintenance and returns before the next container performs it.
With `restart: false`, that container closes after the update.
Explicit Mod maintenance may run outside opening hours, but games still follow the schedule.
The default maintenance countdown is 60 seconds with announcements every 30 seconds.
Failed downloads or validation receive two additional attempts, five minutes apart, then leave games closed.
An explicit maintenance request or a later opening can retry.

A required shard crash or sustained control failure stops the whole room.
Abnormal process exits, forced termination and IPC faults persist recovery state and exit with code `75` for container replacement.
Intentional restart and Mod maintenance use code `76`.
The generated Quadlet treats it as successful and forces a restart through the same startup path.
Update existing Quadlets with the [required restart directives](docs/migration.md#container-exit-contract) before using this lifecycle.
Retryable failures allow two restarts of the current save, 30 seconds apart.
Only confirmed save-loading failure can then try one previous complete room snapshot.
The loss must be proven to be at most one game day per world.
Targets are fixed before mutation; interrupted recovery resumes those same targets.
Unknown state, missing targets, unsupported failure causes or failed recovery leave the room closed.
Thirty minutes of stable operation restores the budget.
Exhausted rooms get another opportunity at the next opening, or explicit start for an always-open room.

Idle regeneration runs only when enabled, during opening hours, with every shard ready and reliably empty.
Its retention periods are 6, 24, 36, 72 and 168 hours according to world age.
Interrupted observations or changed worlds restart the retention period; clean scheduled closure preserves it.

## Logs, Exports and Utilities

The default game event profile is `history`; `agent --profile` also accepts `critical` and `off`.
Events are bounded and best effort.
Source gaps, subscriber overflow, output loss and export rejection have separate counters.
A subscription is not a replay log or a durable task trigger.

OTLP uses gRPC for logs, metrics and traces.
Signal-specific settings override common `OTEL_EXPORTER_OTLP_*` settings.
Their prefixes are `OTEL_EXPORTER_OTLP_LOGS_*`, `OTEL_EXPORTER_OTLP_METRICS_*` and `OTEL_EXPORTER_OTLP_TRACES_*`.
Configure endpoint, headers, TLS certificates, compression and timeout explicitly; timeout values are milliseconds.
Set a signal's `OTEL_*_EXPORTER=none`, or `OTEL_SDK_DISABLED=true` for all signals, to disable export.
Structured log transport preserves null, integers, floats, arrays and objects.
Netdata's flattened query projection has separate type limitations.

`dst_server.logs` provides `JournalLogs`, `JournalQuery`, `JournalStream`, `NetdataLogs` and `NetdataLogQuery`.
The CLI accepts those same query fields, including cursors, direction and filters:

```sh
dst-server logs journal --unit dst-299.service --query '{"limit":100,"since":"today"}'
dst-server logs journal --unit dst-299.service --follow
dst-server logs telemetry '{"since":1790899200,"until":1790985600,"limit":100}'
```

A follow stream emits one record at a time; interrupting a log query waits for its reader process to exit.
`dst_server.events` exposes the native event schema and validation through `event_schema()` and `validate_event()`.
`dst_server.KleiClient` provides bounded build, region, lobby and room requests with per-query batch failures.
HTTP failures are distinct from successful empty results.

Archive export prepares all shards while games are stopped and preserves native world/player data and Mod progress.
It removes credentials, permission lists and SDK runtime files.
Compression uses 7z with Zstd level 3 by default and one concurrent compressor.
Uploads use the native object-store implementation and explicitly abort incomplete multipart uploads on failure or cancellation.
Cleanup failures remain visible in the result and logs.
Python exposes `dst_server.archive.export_cluster` and `ClusterArchive.save/upload`.
Export requires stopped games and an exclusive room lock.
For an offline local export:

```sh
dst-server host stop --room 299
dst-server archive export 299 --output /tmp/room-299.7z
```

The output path must not already exist.
See [the migration guide](docs/migration.md) for entry points and evidence.

```sh
dst-server scripts build /path/to/native/scripts.zip --output /tmp/scripts.zip
dst-server scripts verify /tmp/scripts.zip --source /path/to/native/scripts.zip
dst-server annotations components /path/to/scripts/components --output /tmp/components.lua
dst-server annotations modutil /path/to/scripts/modutil.lua --output /tmp/modutil.lua
```

## Development and Validation

The workspace contains [`crates/dst-server`](crates/dst-server) and [`crates/dst-server-python`](crates/dst-server-python).
Python wrappers live under [`python/dst_server`](python/dst_server), with Lua resources under [`resources/lua`](resources/lua).
The container runs the Rust binary directly.

```sh
cargo fmt --all -- --check
cargo clippy --locked --workspace --all-targets -- -D warnings
cargo test --locked -p dst-server --all-targets
uv run ruff check python tests/rust tools
uv run ty check python
uv run python tests/rust/check_lua.py
uv build --python 3.14 --out-dir dist --no-sources
```

CI builds a wheel from the source distribution and installs it in an isolated environment.
It then runs native binding and process tests.
Lua contracts run under Lua 5.1 and LuaJIT.
Game build `756039` has an unresolved [native shutdown failure](docs/migration.md#known-native-shutdown-failure) observed with five connected shards.
Gorge and Forge deliberately disable saving; the SDK reports an unconfirmed save and can still close them cleanly.
Real game probes use disposable directories and containers; they must not target live rooms.
Build manifests record SDK, Lua, protocol, native game and artifact identities.
See the [migration guide](docs/migration.md) for verification and the deployment procedure.

