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.
Files changed (59) hide show
  1. dst_server-0.1.0/LICENSE +21 -0
  2. dst_server-0.1.0/PKG-INFO +149 -0
  3. dst_server-0.1.0/README.md +131 -0
  4. dst_server-0.1.0/pyproject.toml +86 -0
  5. dst_server-0.1.0/src/dst_server/__init__.py +89 -0
  6. dst_server-0.1.0/src/dst_server/_fd_wrapper.py +24 -0
  7. dst_server-0.1.0/src/dst_server/annotations/__init__.py +5 -0
  8. dst_server-0.1.0/src/dst_server/annotations/cli.py +80 -0
  9. dst_server-0.1.0/src/dst_server/annotations/generator.py +66 -0
  10. dst_server-0.1.0/src/dst_server/annotations/values.py +82 -0
  11. dst_server-0.1.0/src/dst_server/annotations/visitors.py +252 -0
  12. dst_server-0.1.0/src/dst_server/arguments.py +47 -0
  13. dst_server-0.1.0/src/dst_server/cluster.py +120 -0
  14. dst_server-0.1.0/src/dst_server/console.py +82 -0
  15. dst_server-0.1.0/src/dst_server/driver.py +87 -0
  16. dst_server-0.1.0/src/dst_server/events/__init__.py +109 -0
  17. dst_server-0.1.0/src/dst_server/events/base.py +107 -0
  18. dst_server-0.1.0/src/dst_server/events/player.py +281 -0
  19. dst_server-0.1.0/src/dst_server/events/record.py +90 -0
  20. dst_server-0.1.0/src/dst_server/events/server.py +79 -0
  21. dst_server-0.1.0/src/dst_server/events/world.py +120 -0
  22. dst_server-0.1.0/src/dst_server/game/__init__.py +7 -0
  23. dst_server-0.1.0/src/dst_server/game/client.py +112 -0
  24. dst_server-0.1.0/src/dst_server/game/players.py +172 -0
  25. dst_server-0.1.0/src/dst_server/game/rpc.py +56 -0
  26. dst_server-0.1.0/src/dst_server/game/world.py +99 -0
  27. dst_server-0.1.0/src/dst_server/game_events.py +108 -0
  28. dst_server-0.1.0/src/dst_server/instrumentation.py +118 -0
  29. dst_server-0.1.0/src/dst_server/klei/__init__.py +32 -0
  30. dst_server-0.1.0/src/dst_server/klei/client.py +216 -0
  31. dst_server-0.1.0/src/dst_server/klei/enums.py +59 -0
  32. dst_server-0.1.0/src/dst_server/klei/lobby.py +112 -0
  33. dst_server-0.1.0/src/dst_server/klei/schema.py +14 -0
  34. dst_server-0.1.0/src/dst_server/klei/version.py +140 -0
  35. dst_server-0.1.0/src/dst_server/lua/dst_server/actions.lua +57 -0
  36. dst_server-0.1.0/src/dst_server/lua/dst_server/commands.lua +196 -0
  37. dst_server-0.1.0/src/dst_server/lua/dst_server/player_events.lua +211 -0
  38. dst_server-0.1.0/src/dst_server/lua/dst_server/player_queries.lua +82 -0
  39. dst_server-0.1.0/src/dst_server/lua/dst_server/player_values.lua +170 -0
  40. dst_server-0.1.0/src/dst_server/lua/dst_server/state.lua +16 -0
  41. dst_server-0.1.0/src/dst_server/lua/dst_server/telemetry.lua +53 -0
  42. dst_server-0.1.0/src/dst_server/lua/dst_server/values.lua +185 -0
  43. dst_server-0.1.0/src/dst_server/lua/dst_server/world_events.lua +136 -0
  44. dst_server-0.1.0/src/dst_server/lua/dst_server/world_queries.lua +151 -0
  45. dst_server-0.1.0/src/dst_server/lua/dst_server.lua +94 -0
  46. dst_server-0.1.0/src/dst_server/models/__init__.py +40 -0
  47. dst_server-0.1.0/src/dst_server/models/player.py +129 -0
  48. dst_server-0.1.0/src/dst_server/models/server.py +116 -0
  49. dst_server-0.1.0/src/dst_server/models/value.py +17 -0
  50. dst_server-0.1.0/src/dst_server/mods.py +134 -0
  51. dst_server-0.1.0/src/dst_server/observers.py +114 -0
  52. dst_server-0.1.0/src/dst_server/otel.py +206 -0
  53. dst_server-0.1.0/src/dst_server/process.py +336 -0
  54. dst_server-0.1.0/src/dst_server/protocol.py +135 -0
  55. dst_server-0.1.0/src/dst_server/py.typed +0 -0
  56. dst_server-0.1.0/src/dst_server/runner.py +240 -0
  57. dst_server-0.1.0/src/dst_server/schema.py +38 -0
  58. dst_server-0.1.0/src/dst_server/server_events.py +98 -0
  59. dst_server-0.1.0/src/dst_server/validation.py +50 -0
@@ -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
+ [![Steam](https://img.shields.io/badge/Steam-000000?logo=steam&logoColor=white)](https://steamcommunity.com/groups/lst99)
22
+ [![Discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/4N3aeNsFt8)
23
+
24
+ English | [简体中文](README.zh-Hans.md)
25
+
26
+ ![Steam store header](https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/322330/header_schinese.jpg?t=1736195686)
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
+ [![Steam](https://img.shields.io/badge/Steam-000000?logo=steam&logoColor=white)](https://steamcommunity.com/groups/lst99)
4
+ [![Discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/4N3aeNsFt8)
5
+
6
+ English | [简体中文](README.zh-Hans.md)
7
+
8
+ ![Steam store header](https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/322330/header_schinese.jpg?t=1736195686)
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,5 @@
1
+ from __future__ import annotations
2
+
3
+ from .generator import generate_components, generate_modutil
4
+
5
+ __all__ = ["generate_components", "generate_modutil"]
@@ -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 "")