dst-server 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dst_server-0.1.0/LICENSE +21 -0
- dst_server-0.1.0/PKG-INFO +149 -0
- dst_server-0.1.0/README.md +131 -0
- dst_server-0.1.0/pyproject.toml +86 -0
- dst_server-0.1.0/src/dst_server/__init__.py +89 -0
- dst_server-0.1.0/src/dst_server/_fd_wrapper.py +24 -0
- dst_server-0.1.0/src/dst_server/annotations/__init__.py +5 -0
- dst_server-0.1.0/src/dst_server/annotations/cli.py +80 -0
- dst_server-0.1.0/src/dst_server/annotations/generator.py +66 -0
- dst_server-0.1.0/src/dst_server/annotations/values.py +82 -0
- dst_server-0.1.0/src/dst_server/annotations/visitors.py +252 -0
- dst_server-0.1.0/src/dst_server/arguments.py +47 -0
- dst_server-0.1.0/src/dst_server/cluster.py +120 -0
- dst_server-0.1.0/src/dst_server/console.py +82 -0
- dst_server-0.1.0/src/dst_server/driver.py +87 -0
- dst_server-0.1.0/src/dst_server/events/__init__.py +109 -0
- dst_server-0.1.0/src/dst_server/events/base.py +107 -0
- dst_server-0.1.0/src/dst_server/events/player.py +281 -0
- dst_server-0.1.0/src/dst_server/events/record.py +90 -0
- dst_server-0.1.0/src/dst_server/events/server.py +79 -0
- dst_server-0.1.0/src/dst_server/events/world.py +120 -0
- dst_server-0.1.0/src/dst_server/game/__init__.py +7 -0
- dst_server-0.1.0/src/dst_server/game/client.py +112 -0
- dst_server-0.1.0/src/dst_server/game/players.py +172 -0
- dst_server-0.1.0/src/dst_server/game/rpc.py +56 -0
- dst_server-0.1.0/src/dst_server/game/world.py +99 -0
- dst_server-0.1.0/src/dst_server/game_events.py +108 -0
- dst_server-0.1.0/src/dst_server/instrumentation.py +118 -0
- dst_server-0.1.0/src/dst_server/klei/__init__.py +32 -0
- dst_server-0.1.0/src/dst_server/klei/client.py +216 -0
- dst_server-0.1.0/src/dst_server/klei/enums.py +59 -0
- dst_server-0.1.0/src/dst_server/klei/lobby.py +112 -0
- dst_server-0.1.0/src/dst_server/klei/schema.py +14 -0
- dst_server-0.1.0/src/dst_server/klei/version.py +140 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/actions.lua +57 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/commands.lua +196 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/player_events.lua +211 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/player_queries.lua +82 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/player_values.lua +170 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/state.lua +16 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/telemetry.lua +53 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/values.lua +185 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/world_events.lua +136 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server/world_queries.lua +151 -0
- dst_server-0.1.0/src/dst_server/lua/dst_server.lua +94 -0
- dst_server-0.1.0/src/dst_server/models/__init__.py +40 -0
- dst_server-0.1.0/src/dst_server/models/player.py +129 -0
- dst_server-0.1.0/src/dst_server/models/server.py +116 -0
- dst_server-0.1.0/src/dst_server/models/value.py +17 -0
- dst_server-0.1.0/src/dst_server/mods.py +134 -0
- dst_server-0.1.0/src/dst_server/observers.py +114 -0
- dst_server-0.1.0/src/dst_server/otel.py +206 -0
- dst_server-0.1.0/src/dst_server/process.py +336 -0
- dst_server-0.1.0/src/dst_server/protocol.py +135 -0
- dst_server-0.1.0/src/dst_server/py.typed +0 -0
- dst_server-0.1.0/src/dst_server/runner.py +240 -0
- dst_server-0.1.0/src/dst_server/schema.py +38 -0
- dst_server-0.1.0/src/dst_server/server_events.py +98 -0
- dst_server-0.1.0/src/dst_server/validation.py +50 -0
dst_server-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LST
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dst-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Start and control a Don't Starve Together server process.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: logbook>=1.9.2
|
|
8
|
+
Requires-Dist: luaparser>=4.0.1,<5
|
|
9
|
+
Requires-Dist: opentelemetry-api~=1.44.0
|
|
10
|
+
Requires-Dist: pydantic>=2.13.4
|
|
11
|
+
Requires-Dist: selectolax>=0.4.11,<0.5 ; extra == 'klei'
|
|
12
|
+
Requires-Dist: urllib3-future>=2.22.901,<3 ; extra == 'klei'
|
|
13
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc~=1.44.0 ; extra == 'otel'
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Provides-Extra: klei
|
|
16
|
+
Provides-Extra: otel
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Don't Starve Together Dedicated Server Image
|
|
20
|
+
|
|
21
|
+
[](https://steamcommunity.com/groups/lst99)
|
|
22
|
+
[](https://discord.gg/4N3aeNsFt8)
|
|
23
|
+
|
|
24
|
+
English | [简体中文](README.zh-Hans.md)
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
Container image: `quay.io/wh2099/dst-server`
|
|
29
|
+
|
|
30
|
+
The image runs every shard in one DST cluster and keeps the familiar console FIFO.
|
|
31
|
+
It can also export validated game events through OpenTelemetry.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
1. Install a container runtime such as [Podman](https://docs.podman.io/en/latest/index.html).
|
|
36
|
+
2. Create and download a dedicated-server configuration from [Klei's server management page](https://accounts.klei.com/account/game/servers?game=DontStarveTogether).
|
|
37
|
+
3. Extract the archive on the host.
|
|
38
|
+
|
|
39
|
+
The directory mounted at `/cluster` must directly contain `cluster.ini`, `cluster_token.txt`, and the shard directories:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
Cluster_1/
|
|
43
|
+
├── cluster.ini
|
|
44
|
+
├── cluster_token.txt
|
|
45
|
+
├── Master/
|
|
46
|
+
│ └── server.ini
|
|
47
|
+
└── Caves/
|
|
48
|
+
└── server.ini
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
4. Start the container, replacing the host path with the extracted cluster directory:
|
|
52
|
+
|
|
53
|
+
```shell
|
|
54
|
+
sudo podman run \
|
|
55
|
+
--name dst \
|
|
56
|
+
--detach \
|
|
57
|
+
--network host \
|
|
58
|
+
--volume "${HOME}/Cluster_1:/cluster" \
|
|
59
|
+
quay.io/wh2099/dst-server:latest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Operations
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
podman logs dst
|
|
66
|
+
podman stop dst
|
|
67
|
+
podman start dst
|
|
68
|
+
podman restart dst
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Stopping the container terminates every shard gracefully, and DST saves during shutdown.
|
|
72
|
+
|
|
73
|
+
The entrypoint creates a named `console` pipe for the master shard at the cluster root.
|
|
74
|
+
Each secondary shard receives one in its own directory:
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
echo 'c_announce("Server maintenance is coming.")' > "${HOME}/Cluster_1/console"
|
|
78
|
+
echo 'c_save()' > "${HOME}/Cluster_1/Caves/console"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Common lifecycle commands include `c_reset()`, `c_regenerateworld()`, `c_save()`, and `c_shutdown(false)`.
|
|
82
|
+
Use `c_announce("...")` to send a message and `c_listallplayers()` to inspect players.
|
|
83
|
+
|
|
84
|
+
## What the Entrypoint Does
|
|
85
|
+
|
|
86
|
+
On startup, [`entrypoint.py`](entrypoint.py) validates the cluster and prepares permission and Mod files.
|
|
87
|
+
It updates Workshop content once, discovers every shard, and starts one `Server` per shard in `-cloudserver` mode.
|
|
88
|
+
|
|
89
|
+
`DST_SKIP_MOD_UPDATE=1` skips the one-shot Workshop update.
|
|
90
|
+
|
|
91
|
+
`DST_INSTALL_PATH` and `DST_CLUSTER_PATH` override `/install` and `/cluster` for development and tests.
|
|
92
|
+
|
|
93
|
+
An `OTEL_EXPORTER_OTLP_*_ENDPOINT` variable enables the official OpenTelemetry pipeline.
|
|
94
|
+
The entrypoint then exports validated game events.
|
|
95
|
+
|
|
96
|
+
## Python SDK
|
|
97
|
+
|
|
98
|
+
The `dst-server` package starts and controls one Linux DST shard process.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import asyncio
|
|
102
|
+
|
|
103
|
+
from dst_server import Server, ServerArgs
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def main() -> None:
|
|
107
|
+
async with Server(ServerArgs(shard="Master")) as server:
|
|
108
|
+
world = await server.game.world.state()
|
|
109
|
+
players = await server.game.players.list()
|
|
110
|
+
await server.game.world.announce(
|
|
111
|
+
f"Day {world.day}: {len(players)} player(s) online"
|
|
112
|
+
)
|
|
113
|
+
await server.save()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The typed API covers world and player queries, inventory, administration, confirmed saves, and raw Lua.
|
|
120
|
+
It also exposes lifecycle and game events.
|
|
121
|
+
|
|
122
|
+
Install `dst-server[otel]` for OTLP export or `dst-server[klei]` for Klei build and lobby services.
|
|
123
|
+
|
|
124
|
+
## Lua Annotations
|
|
125
|
+
|
|
126
|
+
The `dst-annotations` command generates LSP-compatible Lua definitions from DST components or `modutil.lua`.
|
|
127
|
+
|
|
128
|
+
```console
|
|
129
|
+
dst-annotations dst-scripts/scripts/components --output components_def.lua
|
|
130
|
+
dst-annotations dst-scripts/scripts/modutil.lua --output modutil_def.lua
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Documentation
|
|
134
|
+
|
|
135
|
+
Game server reference:
|
|
136
|
+
|
|
137
|
+
- [Cluster architecture and configuration files](docs/dedicated-server-configuration.md)
|
|
138
|
+
- [Dedicated server command-line options](docs/dedicated-server-options.md)
|
|
139
|
+
|
|
140
|
+
Technical design:
|
|
141
|
+
|
|
142
|
+
- [`-cloudserver` bidirectional IPC](docs/cloudserver-ipc.md)
|
|
143
|
+
- [Game events and OpenTelemetry](docs/opentelemetry-game-events.md)
|
|
144
|
+
|
|
145
|
+
Source reference:
|
|
146
|
+
|
|
147
|
+
- [DST Lua source index](dst-scripts/index/README.md)
|
|
148
|
+
|
|
149
|
+
The detailed documentation is maintained in Simplified Chinese.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Don't Starve Together Dedicated Server Image
|
|
2
|
+
|
|
3
|
+
[](https://steamcommunity.com/groups/lst99)
|
|
4
|
+
[](https://discord.gg/4N3aeNsFt8)
|
|
5
|
+
|
|
6
|
+
English | [简体中文](README.zh-Hans.md)
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
Container image: `quay.io/wh2099/dst-server`
|
|
11
|
+
|
|
12
|
+
The image runs every shard in one DST cluster and keeps the familiar console FIFO.
|
|
13
|
+
It can also export validated game events through OpenTelemetry.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
1. Install a container runtime such as [Podman](https://docs.podman.io/en/latest/index.html).
|
|
18
|
+
2. Create and download a dedicated-server configuration from [Klei's server management page](https://accounts.klei.com/account/game/servers?game=DontStarveTogether).
|
|
19
|
+
3. Extract the archive on the host.
|
|
20
|
+
|
|
21
|
+
The directory mounted at `/cluster` must directly contain `cluster.ini`, `cluster_token.txt`, and the shard directories:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
Cluster_1/
|
|
25
|
+
├── cluster.ini
|
|
26
|
+
├── cluster_token.txt
|
|
27
|
+
├── Master/
|
|
28
|
+
│ └── server.ini
|
|
29
|
+
└── Caves/
|
|
30
|
+
└── server.ini
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. Start the container, replacing the host path with the extracted cluster directory:
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
sudo podman run \
|
|
37
|
+
--name dst \
|
|
38
|
+
--detach \
|
|
39
|
+
--network host \
|
|
40
|
+
--volume "${HOME}/Cluster_1:/cluster" \
|
|
41
|
+
quay.io/wh2099/dst-server:latest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Operations
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
podman logs dst
|
|
48
|
+
podman stop dst
|
|
49
|
+
podman start dst
|
|
50
|
+
podman restart dst
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Stopping the container terminates every shard gracefully, and DST saves during shutdown.
|
|
54
|
+
|
|
55
|
+
The entrypoint creates a named `console` pipe for the master shard at the cluster root.
|
|
56
|
+
Each secondary shard receives one in its own directory:
|
|
57
|
+
|
|
58
|
+
```shell
|
|
59
|
+
echo 'c_announce("Server maintenance is coming.")' > "${HOME}/Cluster_1/console"
|
|
60
|
+
echo 'c_save()' > "${HOME}/Cluster_1/Caves/console"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Common lifecycle commands include `c_reset()`, `c_regenerateworld()`, `c_save()`, and `c_shutdown(false)`.
|
|
64
|
+
Use `c_announce("...")` to send a message and `c_listallplayers()` to inspect players.
|
|
65
|
+
|
|
66
|
+
## What the Entrypoint Does
|
|
67
|
+
|
|
68
|
+
On startup, [`entrypoint.py`](entrypoint.py) validates the cluster and prepares permission and Mod files.
|
|
69
|
+
It updates Workshop content once, discovers every shard, and starts one `Server` per shard in `-cloudserver` mode.
|
|
70
|
+
|
|
71
|
+
`DST_SKIP_MOD_UPDATE=1` skips the one-shot Workshop update.
|
|
72
|
+
|
|
73
|
+
`DST_INSTALL_PATH` and `DST_CLUSTER_PATH` override `/install` and `/cluster` for development and tests.
|
|
74
|
+
|
|
75
|
+
An `OTEL_EXPORTER_OTLP_*_ENDPOINT` variable enables the official OpenTelemetry pipeline.
|
|
76
|
+
The entrypoint then exports validated game events.
|
|
77
|
+
|
|
78
|
+
## Python SDK
|
|
79
|
+
|
|
80
|
+
The `dst-server` package starts and controls one Linux DST shard process.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import asyncio
|
|
84
|
+
|
|
85
|
+
from dst_server import Server, ServerArgs
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def main() -> None:
|
|
89
|
+
async with Server(ServerArgs(shard="Master")) as server:
|
|
90
|
+
world = await server.game.world.state()
|
|
91
|
+
players = await server.game.players.list()
|
|
92
|
+
await server.game.world.announce(
|
|
93
|
+
f"Day {world.day}: {len(players)} player(s) online"
|
|
94
|
+
)
|
|
95
|
+
await server.save()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The typed API covers world and player queries, inventory, administration, confirmed saves, and raw Lua.
|
|
102
|
+
It also exposes lifecycle and game events.
|
|
103
|
+
|
|
104
|
+
Install `dst-server[otel]` for OTLP export or `dst-server[klei]` for Klei build and lobby services.
|
|
105
|
+
|
|
106
|
+
## Lua Annotations
|
|
107
|
+
|
|
108
|
+
The `dst-annotations` command generates LSP-compatible Lua definitions from DST components or `modutil.lua`.
|
|
109
|
+
|
|
110
|
+
```console
|
|
111
|
+
dst-annotations dst-scripts/scripts/components --output components_def.lua
|
|
112
|
+
dst-annotations dst-scripts/scripts/modutil.lua --output modutil_def.lua
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Documentation
|
|
116
|
+
|
|
117
|
+
Game server reference:
|
|
118
|
+
|
|
119
|
+
- [Cluster architecture and configuration files](docs/dedicated-server-configuration.md)
|
|
120
|
+
- [Dedicated server command-line options](docs/dedicated-server-options.md)
|
|
121
|
+
|
|
122
|
+
Technical design:
|
|
123
|
+
|
|
124
|
+
- [`-cloudserver` bidirectional IPC](docs/cloudserver-ipc.md)
|
|
125
|
+
- [Game events and OpenTelemetry](docs/opentelemetry-game-events.md)
|
|
126
|
+
|
|
127
|
+
Source reference:
|
|
128
|
+
|
|
129
|
+
- [DST Lua source index](dst-scripts/index/README.md)
|
|
130
|
+
|
|
131
|
+
The detailed documentation is maintained in Simplified Chinese.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dst-server"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Start and control a Don't Starve Together server process."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"logbook>=1.9.2",
|
|
11
|
+
"luaparser>=4.0.1,<5",
|
|
12
|
+
"opentelemetry-api~=1.44.0",
|
|
13
|
+
"pydantic>=2.13.4",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
klei = ["selectolax>=0.4.11,<0.5", "urllib3-future>=2.22.901,<3"]
|
|
18
|
+
otel = ["opentelemetry-exporter-otlp-proto-grpc~=1.44.0"]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
dst-annotations = "dst_server.annotations.cli:main"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"prek>=0.4.10",
|
|
26
|
+
"pytest>=9.1.0",
|
|
27
|
+
"pytest-asyncio>=1.4.0",
|
|
28
|
+
"ruff>=0.15.17",
|
|
29
|
+
"rumdl>=0.2.35",
|
|
30
|
+
"ty>=0.0.49",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build~=0.11"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
fix = true
|
|
39
|
+
preview = true
|
|
40
|
+
required-version = '>=0.15'
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ['ALL']
|
|
44
|
+
ignore = [
|
|
45
|
+
'missing-trailing-comma', # Conflicts with `ruff format`.
|
|
46
|
+
'commented-out-code', # False positives are common.
|
|
47
|
+
'TC', # Runtime use of typing constructs is common now.
|
|
48
|
+
'CPY', # Covered by the standalone LICENSE file.
|
|
49
|
+
'too-many-arguments', # Explicit is better than implicit.
|
|
50
|
+
'too-many-positional-arguments', # Complex is better than complicated.
|
|
51
|
+
'no-self-use', # Violates the usual practice.
|
|
52
|
+
'any-type', # Framework boundaries carry protocol and DI values.
|
|
53
|
+
'builtin-argument-shadowing', # Event(type=...) is part of the public API.
|
|
54
|
+
'blind-except', # Dispatch records handler exceptions before re-raising.
|
|
55
|
+
'D', # Too verbose.
|
|
56
|
+
'subclass-builtin', # Message and State intentionally mirror built-in containers.
|
|
57
|
+
'FBT', # Too strict.
|
|
58
|
+
'import-outside-top-level', # Optional runtime imports keep hard dependencies lazy.
|
|
59
|
+
'too-many-return-statements', # Small resolver functions are branchy by nature.
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.pydoclint]
|
|
63
|
+
ignore-one-line-docstrings = true
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
'src/dst_server/annotations/visitors.py' = ['invalid-function-name']
|
|
67
|
+
'tests/**/*.py' = [
|
|
68
|
+
'assert', # Assert statements used for pytest.
|
|
69
|
+
'implicit-namespace-package', # Colocated tests are plain directories, not import packages.
|
|
70
|
+
'magic-value-comparison', # Magic value used in test cases.
|
|
71
|
+
'import-private-name', # OpenTelemetry still exposes private log modules.
|
|
72
|
+
'compare-to-empty-string', # Explicit empty-string comparisons aid assertions.
|
|
73
|
+
'private-member-access', # Tests may cover implementation details.
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ty.environment]
|
|
77
|
+
python-platform = "linux"
|
|
78
|
+
|
|
79
|
+
[tool.pytest]
|
|
80
|
+
minversion = '9.0'
|
|
81
|
+
asyncio_mode = 'auto'
|
|
82
|
+
|
|
83
|
+
[tool.rumdl]
|
|
84
|
+
enable = ['ALL']
|
|
85
|
+
line-length = 120
|
|
86
|
+
exclude = ['.git', '.github', 'LICENSE']
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .arguments import ServerArgs
|
|
4
|
+
from .cluster import Shard, ShardConfig, discover_shards, ensure_fifo, prepare_cluster
|
|
5
|
+
from .events import (
|
|
6
|
+
DriverHealth,
|
|
7
|
+
EntityRef,
|
|
8
|
+
GameEvent,
|
|
9
|
+
ItemRef,
|
|
10
|
+
ObservedGameEvent,
|
|
11
|
+
ServerEvent,
|
|
12
|
+
ServerReadyEvent,
|
|
13
|
+
ServerSavedEvent,
|
|
14
|
+
ServerSessionEvent,
|
|
15
|
+
ServerShutdownEvent,
|
|
16
|
+
ServerStoppingEvent,
|
|
17
|
+
TelemetrySettings,
|
|
18
|
+
UnknownServerEvent,
|
|
19
|
+
)
|
|
20
|
+
from .game import GameClient
|
|
21
|
+
from .models import (
|
|
22
|
+
Age,
|
|
23
|
+
Container,
|
|
24
|
+
Entity,
|
|
25
|
+
EquipmentSlot,
|
|
26
|
+
Health,
|
|
27
|
+
Inventory,
|
|
28
|
+
InventorySlot,
|
|
29
|
+
Item,
|
|
30
|
+
Mod,
|
|
31
|
+
Player,
|
|
32
|
+
PlayerState,
|
|
33
|
+
Position,
|
|
34
|
+
Room,
|
|
35
|
+
Runtime,
|
|
36
|
+
ShardStatus,
|
|
37
|
+
Stat,
|
|
38
|
+
Temperature,
|
|
39
|
+
Vitals,
|
|
40
|
+
World,
|
|
41
|
+
)
|
|
42
|
+
from .mods import prepare_mods, update_server_mods, workshop_mod_ids
|
|
43
|
+
from .process import Server
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Age",
|
|
47
|
+
"Container",
|
|
48
|
+
"DriverHealth",
|
|
49
|
+
"Entity",
|
|
50
|
+
"EntityRef",
|
|
51
|
+
"EquipmentSlot",
|
|
52
|
+
"GameClient",
|
|
53
|
+
"GameEvent",
|
|
54
|
+
"Health",
|
|
55
|
+
"Inventory",
|
|
56
|
+
"InventorySlot",
|
|
57
|
+
"Item",
|
|
58
|
+
"ItemRef",
|
|
59
|
+
"Mod",
|
|
60
|
+
"ObservedGameEvent",
|
|
61
|
+
"Player",
|
|
62
|
+
"PlayerState",
|
|
63
|
+
"Position",
|
|
64
|
+
"Room",
|
|
65
|
+
"Runtime",
|
|
66
|
+
"Server",
|
|
67
|
+
"ServerArgs",
|
|
68
|
+
"ServerEvent",
|
|
69
|
+
"ServerReadyEvent",
|
|
70
|
+
"ServerSavedEvent",
|
|
71
|
+
"ServerSessionEvent",
|
|
72
|
+
"ServerShutdownEvent",
|
|
73
|
+
"ServerStoppingEvent",
|
|
74
|
+
"Shard",
|
|
75
|
+
"ShardConfig",
|
|
76
|
+
"ShardStatus",
|
|
77
|
+
"Stat",
|
|
78
|
+
"TelemetrySettings",
|
|
79
|
+
"Temperature",
|
|
80
|
+
"UnknownServerEvent",
|
|
81
|
+
"Vitals",
|
|
82
|
+
"World",
|
|
83
|
+
"discover_shards",
|
|
84
|
+
"ensure_fifo",
|
|
85
|
+
"prepare_cluster",
|
|
86
|
+
"prepare_mods",
|
|
87
|
+
"update_server_mods",
|
|
88
|
+
"workshop_mod_ids",
|
|
89
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
_PROTOCOL_FDS = (3, 4, 5)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
sources = tuple(int(value) for value in sys.argv[1:4])
|
|
11
|
+
command = sys.argv[4:]
|
|
12
|
+
if len(sources) != len(_PROTOCOL_FDS) or not command:
|
|
13
|
+
msg = "usage: _fd_wrapper.py FD3 FD4 FD5 COMMAND [ARG ...]"
|
|
14
|
+
raise SystemExit(msg)
|
|
15
|
+
for source, target in zip(sources, _PROTOCOL_FDS, strict=True):
|
|
16
|
+
os.dup2(source, target, inheritable=True)
|
|
17
|
+
for source in sources:
|
|
18
|
+
if source not in _PROTOCOL_FDS:
|
|
19
|
+
os.close(source)
|
|
20
|
+
os.execv(command[0], command) # ruff:ignore[start-process-with-no-shell]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .generator import generate_components, generate_modutil
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Generate LSP-compatible Lua annotations."
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("input", type=Path, help="Components directory or modutil file")
|
|
18
|
+
parser.add_argument("-o", "--output", type=Path, help="Output Lua file")
|
|
19
|
+
parser.add_argument("-m", "--max-workers", type=int, help="Worker process count")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--mode",
|
|
22
|
+
choices=("auto", "components", "modutil"),
|
|
23
|
+
default="auto",
|
|
24
|
+
)
|
|
25
|
+
return parser
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_mode(
|
|
29
|
+
parser: argparse.ArgumentParser,
|
|
30
|
+
input_path: Path,
|
|
31
|
+
requested: str,
|
|
32
|
+
) -> str:
|
|
33
|
+
if requested != "auto":
|
|
34
|
+
return requested
|
|
35
|
+
if input_path.is_dir():
|
|
36
|
+
return "components"
|
|
37
|
+
if input_path.is_file() and "modutil" in input_path.name.lower():
|
|
38
|
+
return "modutil"
|
|
39
|
+
return parser.error("Cannot infer mode; pass --mode explicitly.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_components(input_path: Path, output: Path | None, workers: int | None) -> int:
|
|
43
|
+
content = generate_components(input_path, workers)
|
|
44
|
+
if not content:
|
|
45
|
+
logger.error("No Lua component definitions generated from %s", input_path)
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
output_path = output or Path(f"{input_path.name}_def.lua")
|
|
49
|
+
output_path.write_text(content, encoding="utf-8")
|
|
50
|
+
logger.info("Output written to %s", output_path)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def write_modutil(input_path: Path, output: Path | None) -> int:
|
|
55
|
+
if not input_path.is_file():
|
|
56
|
+
raise FileNotFoundError(input_path)
|
|
57
|
+
|
|
58
|
+
content = generate_modutil(input_path)
|
|
59
|
+
if not content:
|
|
60
|
+
logger.error("No definitions found in %s", input_path)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
output_path = output or Path(f"{input_path.stem}_def.lua")
|
|
64
|
+
output_path.write_text(content, encoding="utf-8")
|
|
65
|
+
logger.info("Output written to %s", output_path)
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
70
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
71
|
+
parser = create_parser()
|
|
72
|
+
args = parser.parse_args(argv)
|
|
73
|
+
mode = resolve_mode(parser, args.input, args.mode)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
if mode == "components":
|
|
77
|
+
return write_components(args.input, args.output, args.max_workers)
|
|
78
|
+
return write_modutil(args.input, args.output)
|
|
79
|
+
except Exception as error:
|
|
80
|
+
parser.error(str(error))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
5
|
+
from itertools import repeat
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .visitors import DEFAULT_VAR, parse_component, parse_modutil
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_class_name(content: str) -> str | None:
|
|
14
|
+
for source_line in reversed(content.splitlines()):
|
|
15
|
+
line = source_line.strip()
|
|
16
|
+
if line.startswith("return "):
|
|
17
|
+
return line.removeprefix("return ").split(maxsplit=1)[0].rstrip(",")
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def component_definition(path: Path, folder_name: str) -> tuple[str, str] | None:
|
|
22
|
+
try:
|
|
23
|
+
content = path.read_text(encoding="utf-8")
|
|
24
|
+
class_name = extract_class_name(content) or path.stem
|
|
25
|
+
fields, definitions = parse_component(
|
|
26
|
+
content,
|
|
27
|
+
path.stem,
|
|
28
|
+
class_name,
|
|
29
|
+
folder_name,
|
|
30
|
+
)
|
|
31
|
+
except Exception as error:
|
|
32
|
+
logger.warning("Could not parse %s: %s", path, error)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
lines = [f"---@class {class_name}", *fields]
|
|
36
|
+
lines.extend((
|
|
37
|
+
f"local {DEFAULT_VAR}={{}}",
|
|
38
|
+
f"{folder_name}.{path.stem}={DEFAULT_VAR}",
|
|
39
|
+
"",
|
|
40
|
+
*definitions,
|
|
41
|
+
"",
|
|
42
|
+
))
|
|
43
|
+
return path.stem, "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_components(input_dir: Path, max_workers: int | None = None) -> str:
|
|
47
|
+
if not input_dir.is_dir():
|
|
48
|
+
raise NotADirectoryError(input_dir)
|
|
49
|
+
|
|
50
|
+
files = sorted(input_dir.rglob("*.lua"))
|
|
51
|
+
if max_workers == 1:
|
|
52
|
+
results = [component_definition(path, input_dir.name) for path in files]
|
|
53
|
+
else:
|
|
54
|
+
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
55
|
+
results = list(
|
|
56
|
+
executor.map(component_definition, files, repeat(input_dir.name))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
definitions = sorted(result for result in results if result is not None)
|
|
60
|
+
return "\n".join(content for _, content in definitions)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def generate_modutil(input_file: Path) -> str:
|
|
64
|
+
content = input_file.read_text(encoding="utf-8")
|
|
65
|
+
definitions = parse_modutil(content, input_file.stem)
|
|
66
|
+
return "\n".join(definitions) + ("\n" if definitions else "")
|