msss 0.0.1__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.
msss-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MnlSmile
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.
msss-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: msss
3
+ Version: 0.0.1
4
+ Summary: MnlSmile Scaffolding Server 是一个用于架设 Scaffolding 中心服务器的简单框架。
5
+ Home-page: https://github.com/MnlSmile/MSSS
6
+ Author: MnlSmile
7
+ Author-email: kedoukedou33@163.com
8
+ License: MIT
9
+ Project-URL: Bug Tracker, https://github.com/MnlSmile/MSSS/issues
10
+ Project-URL: Source Code, https://github.com/MnlSmile/MSSS
11
+ Keywords: scaffolding
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Topic :: Games/Entertainment
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: py-machineid>=0.5.0
21
+ Requires-Dist: async-timer>=1.0.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: description
27
+ Dynamic: description-content-type
28
+ Dynamic: home-page
29
+ Dynamic: keywords
30
+ Dynamic: license
31
+ Dynamic: license-file
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
36
+
37
+ # MSSS – MnlSmile Scaffolding Server
38
+
39
+ **MSSS** 是一个用于快速架设 [Scaffolding](https://github.com/Scaffolding-MC/Scaffolding-MC) 中心服务器的 Python 异步框架。
40
+
41
+ # MSSS 由一个没有工程经验的初学者开发,能基本应付日常游玩,但请谨慎用于生产环境。
42
+
43
+ ---
44
+
45
+ ## 特性
46
+
47
+ - **开箱即用** – 几行代码即可启动一个完整的 Scaffolding 中心服务器。
48
+ - **自动化** - 能将创建房间的过程自动化,可轻松把本地《我的世界》服务器接入众多启动器联机生态。
49
+ - **协议可扩展** – 通过 `@app.protocol('namespace:protocol')` 装饰器即可注册自定义协议处理器。
50
+
51
+ ---
52
+
53
+ ## 安装
54
+
55
+ ```bash
56
+ pip install msss
57
+ ```
58
+
59
+ 或者从源码安装:
60
+
61
+ ```bash
62
+ git clone https://github.com/MnlSmile/MSSS.git
63
+ cd MSSS
64
+ pip install .
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 快速开始
70
+
71
+ 以下示例启动一个监听 `0.0.0.0:13659` 的 Scaffolding 服务器,并假定《我的世界》服务器运行在 `25565` 端口。
72
+
73
+ ```python
74
+ import asyncio
75
+ import logging
76
+ import subprocess
77
+ from msss import Scaffolding
78
+
79
+ logging.basicConfig(level=logging.INFO)
80
+
81
+ app = Scaffolding(host='0.0.0.0', port=13659, mc_port=25565)
82
+ et_proc = subprocess.Popen(app.easytier('U/0000-0000-0000-0000'), shell=True)
83
+ asyncio.run(app.run())
84
+ et_proc.terminate()
85
+ et_proc.wait()
86
+
87
+ ```
88
+
89
+ 启动后,您的朋友们可以在启动器通过房间码 **U/0000-0000-0000-0000** 加入房间,然后在《我的世界》多人游戏中直接连接。
90
+
91
+ > **提示**:`app.easytier(房间码)` 可以生成对应的 EasyTier 启动命令,详见下文。
92
+
93
+ ---
94
+
95
+ ## 自定义协议处理器
96
+
97
+ 使用 `@app.protocol('命名空间:协议名')` 装饰器即可注册自己的协议。
98
+ 协议处理函数接收一个 `SRequest` 对象,可以返回多种类型的值(会自动编码为正确的响应格式)。如果编码不符合预期,也可以读取 `req.body` 获取原始字节数据。
99
+
100
+ ```python
101
+ @app.protocol('my:hello')
102
+ async def handle_hello(req):
103
+ # 解析客户端发送的 JSON 数据
104
+ data = req.as_json()
105
+ name = data.get('name', 'Guest')
106
+ return {'message': f'Hello, {name}!'}
107
+ ```
108
+
109
+ **支持的返回类型**:
110
+ - `None` → 空响应
111
+ - `str` → UTF-8 编码
112
+ - `bytes` / `bytearray` → 按原样发送
113
+ - `BaseModel` (Pydantic) → JSON 编码
114
+ - `SStrList` → Scaffolding 字符串列表(按 `\0` 分隔)
115
+ - `dict` / `list` → JSON 编码
116
+
117
+ 如果协议未注册,且您注册了 `any` 后备处理器,则会调用它;否则自动返回错误码 `255`。
118
+
119
+ ## 与 EasyTier 集成
120
+
121
+ `<Scaffolding>.easytier(code, path='./easytier-core', dhcp=True, extras=[])` 方法会根据房间码生成一个适用于 Windows 平台的命令。
122
+
123
+ - **`code`**:Scaffolding 房间码,格式例如 `U/AAAA-AAAA-AAAA-AAAA`。
124
+ - **`path`**:`easytier-core` 可执行文件的路径。
125
+ - **`dhcp`**:是否让 EasyTier 自动分配虚拟 IP(强烈建议开启)。
126
+ - **`extras`**:额外的命令行参数列表。
127
+
128
+ 示例输出:
129
+
130
+ ```bash
131
+ ./easytier-core --network-name scaffolding-mc-AAAA-AAAA --network-secret AAAA-AAAA --hostname scaffolding-mc-server-13659 -p "https://etnode.zkitefly.eu.org/node1" -p "https://etnode.zkitefly.eu.org/node2" -d
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 默认行为
137
+
138
+ 默认启用的心跳管理器会:
139
+ - 监听 `c:player_ping` 协议,更新每位玩家的最后活跃时间。
140
+ - 每隔 `checking_freq` 秒检查一次,将超过 `llt` 秒未发送心跳的客户端连接关闭并从玩家列表中移除。
141
+ - 房主(`host`)不会被自动清理。
142
+
143
+ 您可以在创建 `Scaffolding` 实例时调整这两个参数:
144
+
145
+ ```python
146
+ app = Scaffolding(..., llt=30.0, checking_freq=5.0)
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 协议实现参考
152
+
153
+ [Scaffolding 官方协议](https://github.com/Scaffolding-MC/Scaffolding-MC)
154
+
155
+ ---
156
+
157
+ ## 依赖项
158
+
159
+ - [py-machineid](https://github.com/ab77/py-machineid)
160
+ - [async-timer](https://github.com/MnlSmile/async-timer)
161
+ - [pydantic](https://github.com/pydantic/pydantic)
162
+
163
+ ---
164
+
165
+ ## 许可证
166
+
167
+ 本项目使用 **MIT 许可证** 发布。
168
+
169
+ ---
170
+
171
+ ## 相关链接
172
+
173
+ - [Scaffolding 协议](https://github.com/Scaffolding-MC/Scaffolding-MC)
174
+ - [EasyTier](https://github.com/EasyTier/EasyTier)
175
+ - [MSSS](https://github.com/MnlSmile/MSSS)
msss-0.0.1/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # MSSS – MnlSmile Scaffolding Server
2
+
3
+ **MSSS** 是一个用于快速架设 [Scaffolding](https://github.com/Scaffolding-MC/Scaffolding-MC) 中心服务器的 Python 异步框架。
4
+
5
+ # MSSS 由一个没有工程经验的初学者开发,能基本应付日常游玩,但请谨慎用于生产环境。
6
+
7
+ ---
8
+
9
+ ## 特性
10
+
11
+ - **开箱即用** – 几行代码即可启动一个完整的 Scaffolding 中心服务器。
12
+ - **自动化** - 能将创建房间的过程自动化,可轻松把本地《我的世界》服务器接入众多启动器联机生态。
13
+ - **协议可扩展** – 通过 `@app.protocol('namespace:protocol')` 装饰器即可注册自定义协议处理器。
14
+
15
+ ---
16
+
17
+ ## 安装
18
+
19
+ ```bash
20
+ pip install msss
21
+ ```
22
+
23
+ 或者从源码安装:
24
+
25
+ ```bash
26
+ git clone https://github.com/MnlSmile/MSSS.git
27
+ cd MSSS
28
+ pip install .
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 快速开始
34
+
35
+ 以下示例启动一个监听 `0.0.0.0:13659` 的 Scaffolding 服务器,并假定《我的世界》服务器运行在 `25565` 端口。
36
+
37
+ ```python
38
+ import asyncio
39
+ import logging
40
+ import subprocess
41
+ from msss import Scaffolding
42
+
43
+ logging.basicConfig(level=logging.INFO)
44
+
45
+ app = Scaffolding(host='0.0.0.0', port=13659, mc_port=25565)
46
+ et_proc = subprocess.Popen(app.easytier('U/0000-0000-0000-0000'), shell=True)
47
+ asyncio.run(app.run())
48
+ et_proc.terminate()
49
+ et_proc.wait()
50
+
51
+ ```
52
+
53
+ 启动后,您的朋友们可以在启动器通过房间码 **U/0000-0000-0000-0000** 加入房间,然后在《我的世界》多人游戏中直接连接。
54
+
55
+ > **提示**:`app.easytier(房间码)` 可以生成对应的 EasyTier 启动命令,详见下文。
56
+
57
+ ---
58
+
59
+ ## 自定义协议处理器
60
+
61
+ 使用 `@app.protocol('命名空间:协议名')` 装饰器即可注册自己的协议。
62
+ 协议处理函数接收一个 `SRequest` 对象,可以返回多种类型的值(会自动编码为正确的响应格式)。如果编码不符合预期,也可以读取 `req.body` 获取原始字节数据。
63
+
64
+ ```python
65
+ @app.protocol('my:hello')
66
+ async def handle_hello(req):
67
+ # 解析客户端发送的 JSON 数据
68
+ data = req.as_json()
69
+ name = data.get('name', 'Guest')
70
+ return {'message': f'Hello, {name}!'}
71
+ ```
72
+
73
+ **支持的返回类型**:
74
+ - `None` → 空响应
75
+ - `str` → UTF-8 编码
76
+ - `bytes` / `bytearray` → 按原样发送
77
+ - `BaseModel` (Pydantic) → JSON 编码
78
+ - `SStrList` → Scaffolding 字符串列表(按 `\0` 分隔)
79
+ - `dict` / `list` → JSON 编码
80
+
81
+ 如果协议未注册,且您注册了 `any` 后备处理器,则会调用它;否则自动返回错误码 `255`。
82
+
83
+ ## 与 EasyTier 集成
84
+
85
+ `<Scaffolding>.easytier(code, path='./easytier-core', dhcp=True, extras=[])` 方法会根据房间码生成一个适用于 Windows 平台的命令。
86
+
87
+ - **`code`**:Scaffolding 房间码,格式例如 `U/AAAA-AAAA-AAAA-AAAA`。
88
+ - **`path`**:`easytier-core` 可执行文件的路径。
89
+ - **`dhcp`**:是否让 EasyTier 自动分配虚拟 IP(强烈建议开启)。
90
+ - **`extras`**:额外的命令行参数列表。
91
+
92
+ 示例输出:
93
+
94
+ ```bash
95
+ ./easytier-core --network-name scaffolding-mc-AAAA-AAAA --network-secret AAAA-AAAA --hostname scaffolding-mc-server-13659 -p "https://etnode.zkitefly.eu.org/node1" -p "https://etnode.zkitefly.eu.org/node2" -d
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 默认行为
101
+
102
+ 默认启用的心跳管理器会:
103
+ - 监听 `c:player_ping` 协议,更新每位玩家的最后活跃时间。
104
+ - 每隔 `checking_freq` 秒检查一次,将超过 `llt` 秒未发送心跳的客户端连接关闭并从玩家列表中移除。
105
+ - 房主(`host`)不会被自动清理。
106
+
107
+ 您可以在创建 `Scaffolding` 实例时调整这两个参数:
108
+
109
+ ```python
110
+ app = Scaffolding(..., llt=30.0, checking_freq=5.0)
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 协议实现参考
116
+
117
+ [Scaffolding 官方协议](https://github.com/Scaffolding-MC/Scaffolding-MC)
118
+
119
+ ---
120
+
121
+ ## 依赖项
122
+
123
+ - [py-machineid](https://github.com/ab77/py-machineid)
124
+ - [async-timer](https://github.com/MnlSmile/async-timer)
125
+ - [pydantic](https://github.com/pydantic/pydantic)
126
+
127
+ ---
128
+
129
+ ## 许可证
130
+
131
+ 本项目使用 **MIT 许可证** 发布。
132
+
133
+ ---
134
+
135
+ ## 相关链接
136
+
137
+ - [Scaffolding 协议](https://github.com/Scaffolding-MC/Scaffolding-MC)
138
+ - [EasyTier](https://github.com/EasyTier/EasyTier)
139
+ - [MSSS](https://github.com/MnlSmile/MSSS)
@@ -0,0 +1,433 @@
1
+ import asyncio as aio
2
+ import json
3
+ import time
4
+ import machineid
5
+ import logging
6
+ import uuid
7
+
8
+ from async_timer import Timer
9
+ from pydantic import BaseModel, Field, ConfigDict
10
+ from typing import Callable, Optional, TypeVar, Awaitable, Iterable
11
+ from enum import Enum
12
+
13
+ class SKindEnum(str, Enum):
14
+ host = 'HOST'
15
+ HOST = 'HOST'
16
+ guest = 'GUEST'
17
+ GUEST = 'GUEST'
18
+
19
+ async def void_func(*args, **kwargs) -> None:
20
+ return
21
+
22
+ class SStrList(list): ...
23
+
24
+ class SProfile(BaseModel):
25
+ name:str
26
+ machine_id:str
27
+ vendor:str
28
+ kind:SKindEnum = SKindEnum.guest
29
+ easytier_id:str = None
30
+
31
+ T = TypeVar('T')
32
+
33
+ _Type = type
34
+
35
+ class SRequest(BaseModel):
36
+ model_config = ConfigDict(arbitrary_types_allowed=True)
37
+
38
+ ip:str
39
+ port:int
40
+ addr:tuple[str, int]
41
+ type:str
42
+ body:bytes
43
+ reader:aio.StreamReader
44
+ writer:aio.StreamWriter
45
+ time:float = Field(default_factory = time.time)
46
+
47
+ def as_scaffolding_str_list(self) -> list[str]:
48
+ """
49
+ 将请求体按 Scaffolding 字符串列表解析。
50
+
51
+ Scaffolding 字符串列表是使用 '\0' 作为分隔符的 utf-8 字节串。
52
+ """
53
+ return self.body.decode('utf-8').split('\0')
54
+ def as_json(self) -> dict:
55
+ """
56
+ 将请求体按 json 解析。
57
+ """
58
+ return json.loads(self.body.decode('utf-8'))
59
+ def as_pydantic(self, _t:_Type[T]) -> T:
60
+ """
61
+ 将请求体按 pydantic 模型解析。
62
+
63
+ Args:
64
+ _t (type): 要解析为的 pydantic 模型类型。
65
+ """
66
+ return _t.model_validate_json(self.body)
67
+
68
+ class SException(Exception):
69
+ def __init__(self, code:int, message:str = '', *args):
70
+ """
71
+ Args:
72
+ code (int): 返回给客户端的状态码。取值范围 [0, 255]
73
+ message (str): 返回给客户端的信息。
74
+ """
75
+ self.code = code
76
+ self.message = message
77
+ super().__init__(*args)
78
+
79
+ class SServer:
80
+ def __init__(self, host:str, port:int, mc_port:int, logger:Optional[logging.Logger] = None):
81
+ """
82
+ Args:
83
+ host (str): 要绑定的地址。
84
+ port (int): 要监听的端口。
85
+ mc_port (int): 《我的世界》服务器运行的端口。
86
+ logger (Logger): 要使用的日志记录器实例。其应提供和标准库 logging.Logger 类似的 API 接口。
87
+ """
88
+ self.host = host
89
+ self.port = port
90
+ self.mc_port = mc_port
91
+ self.logger = logger if logger else logging.getLogger(f"<SServer python_id='{id(self)} host='{self.host}' port='{self.port}' mc_port='{self.mc_port}'>")
92
+
93
+ self.protocol_handlers:dict[str, Callable[[SRequest], Awaitable[bytearray|bytes|BaseModel|dict|list]]] = {}
94
+ self.connected_callback:Callable[[tuple[str, int], Awaitable[None]]] = void_func
95
+ self.disconnected_callback:Callable[[tuple[str, int], Awaitable[None]]] = void_func
96
+
97
+ def protocol(self, protocol:str):
98
+ """
99
+ 将函数声明为协议处理器。
100
+ 原函数不会被装饰器替换。
101
+
102
+ Args:
103
+ protocol (str): 要处理的协议,用半角冒号分隔,其中冒号前为命名空间,冒号后为协议名。传入的协议若为 any,则会注册为针对未实现协议的后备处理程序。
104
+ """
105
+ def _d(func):
106
+ self.protocol_handlers[protocol] = func
107
+ return func
108
+ return _d
109
+
110
+ async def request_handler(self, _t:str, addr:tuple[str, int], body:bytes, reader:aio.StreamReader, writer:aio.StreamWriter) -> tuple[int, bytes|bytearray]:
111
+ """
112
+ 解析来自客户端的原始请求。
113
+
114
+ 返回值为空时,自动转换为空字节串。
115
+ 返回值类型为 str 时,自动转换为 utf-8 编码字节串。
116
+ 返回值类型为 bytes 或 bytearray 时,不作转换。
117
+ 返回值类型为 SStrList 时,自动转换为 Scaffolding 字符串列表。
118
+ 返回值类型为 list 或 dict 时,自动转换为 json。
119
+ """
120
+ req = SRequest(ip = addr[0], port = addr[1], addr = addr, type = _t, body = body, reader = reader, writer = writer)
121
+ try:
122
+ if _t in self.protocol_handlers:
123
+ r = await self.protocol_handlers[_t](req)
124
+ else:
125
+ if 'any' in self.protocol_handlers:
126
+ r = await self.protocol_handlers['any'](req)
127
+ else:
128
+ return 255, f"Protocol not supported: {_t}".encode('utf-8')
129
+ except SException as se:
130
+ return se.code, se.message.encode('utf-8')
131
+
132
+ if r is None:
133
+ return 0, bytearray()
134
+ elif isinstance(r, str):
135
+ return 0, r.encode('utf-8')
136
+ elif isinstance(r, (bytes, bytearray)):
137
+ return 0, r
138
+ elif isinstance(r, BaseModel):
139
+ return 0, r.model_dump_json().encode('utf-8')
140
+ elif isinstance(r, SStrList):
141
+ return 0, '\0'.join(r).encode('utf-8')
142
+ elif isinstance(r,(dict, list)):
143
+ return 0, json.dumps(r).encode('utf-8')
144
+
145
+ def connected(self, func):
146
+ """
147
+ 将函数声明为连接建立时自动调用的 connected 回调函数。
148
+ 原函数不会被装饰器替换。
149
+ """
150
+ self.connected_callback = func
151
+ return func
152
+
153
+ def disconnected(self, func):
154
+ """
155
+ 将函数声明为连断开时自动调用的 disconnected 回调函数。
156
+ 原函数不会被装饰器替换。
157
+ """
158
+ self.disconnected_callback = func
159
+ return func
160
+
161
+ async def connection_handler(self, reader:aio.StreamReader, writer:aio.StreamWriter):
162
+ """
163
+ 处理来自客户端的原始连接。
164
+ """
165
+ addr = writer.get_extra_info('peername')
166
+ self.logger.info(f"接收到来自 {addr} 的连接。")
167
+ try:
168
+ await self.connected_callback(addr)
169
+ except Exception as e:
170
+ self.logger.error(f"在 connected 回调函数中发生错误: {e}")
171
+ try:
172
+ while True:
173
+ type_len_data = await reader.readexactly(1)
174
+ if not type_len_data:
175
+ break
176
+ type_len = type_len_data[0]
177
+
178
+ req_type_bytes = await reader.readexactly(type_len)
179
+ req_type = req_type_bytes.decode('ascii')
180
+
181
+ body_len_data = await reader.readexactly(4)
182
+ body_len = int.from_bytes(body_len_data, byteorder='big', signed=False)
183
+
184
+ body = await reader.readexactly(body_len) if body_len > 0 else b''
185
+
186
+ try:
187
+ status, response_body = await self.request_handler(req_type, addr, body, reader, writer)
188
+ except Exception as e:
189
+ status, response_body = 255,str(e).encode('utf-8')
190
+ self.logger.error(f"在请求处理器中发生错误: {e}")
191
+
192
+ writer.write(status.to_bytes(1, byteorder='big', signed=False))
193
+ writer.write(len(response_body).to_bytes(4, byteorder='big'))
194
+ if response_body:
195
+ writer.write(response_body)
196
+ await writer.drain()
197
+ except (aio.IncompleteReadError, ConnectionResetError):
198
+ self.logger.info(f"来自 {addr} 的连接已断开。")
199
+ return
200
+ except Exception as e:
201
+ self.logger.error(f"处理来自 {addr} 的请求时发生一般错误: {e}")
202
+ finally:
203
+ try:
204
+ writer.close()
205
+ await writer.wait_closed()
206
+ except Exception as e:
207
+ pass
208
+
209
+ try:
210
+ await self.disconnected_callback(addr)
211
+ except Exception as e:
212
+ self.logger.error(f"在 disconnect 回调函数中发生错误: {e}")
213
+
214
+ async def run(self):
215
+ """
216
+ 运行服务器。
217
+ """
218
+ server = await aio.start_server(
219
+ self.connection_handler, host = self.host, port = self.port
220
+ )
221
+ async with server:
222
+ await server.serve_forever()
223
+
224
+ class ScaffoldingGetPlayersContextManager:
225
+ def __init__(self, parent:'Scaffolding'):
226
+ self.parent = parent
227
+
228
+ async def __aenter__(self) -> 'ScaffoldingGetPlayersContextManager':
229
+ await self.parent.lock.acquire()
230
+ return self
231
+
232
+ async def __aexit__(self, *args):
233
+ self.parent.lock.release()
234
+
235
+ def __iter__(self) -> Iterable:
236
+ return iter(self.parent.players.values())
237
+
238
+ class Scaffolding:
239
+ def __init__(self, host:str = '0.0.0.0', port:int = 13659, mc_port:int = 25565, server:Optional[SServer] = None, host_player:Optional[SProfile] = None, setup_default_handlers:bool = True, llt:float = 15.0, checking_freq:float = 1.0, logger:Optional[logging.Logger] = None):
240
+ """
241
+ Args:
242
+ host (str): 要绑定的地址。仅在不传入 server 时才有效。
243
+ port (int): 要监听的端口。仅在不传入 server 时才有效。
244
+ mc_port (int): 《我的世界》服务器运行的端口。仅在不传入 server 时才有效。
245
+ server (SServer): 要使用的 SServer 实例。
246
+ host_player (SProfile): 房主(即本机)的玩家身份。该身份会展示给所有玩家。
247
+ setup_default_handlers (bool): 是否启用 MSSS 默认的标准协议处理函数和一般事件回调函数。默认的心跳管理器采用批处理模式。风险提示:默认行为一切从简,可能包含意外和漏洞。
248
+ llt (float): 使用默认心跳管理器时,允许客户端无心跳的最长时间。超过该时间未心跳的客户端会被清除。
249
+ checking_freq (float): 使用默认心跳管理器时,批处理程序的处理间隔。每隔该时间间隔都会检查一次客户端的心跳情况。
250
+ logger (Logger): 要使用的日志记录器实例。其应提供和标准库 logging.Logger 类似的 API 接口。
251
+ """
252
+ try:
253
+ mid = machineid.id()
254
+ except Exception:
255
+ mid = str(uuid.uuid4())
256
+ self.host_player = host_player if host_player else SProfile(
257
+ name = 'MnlSmile Scaffolding Server',
258
+ machine_id = mid,
259
+ vendor = 'MSSS',
260
+ kind = SKindEnum.host
261
+ )
262
+ self.logger = logger if logger else logging.getLogger(f"<Scaffolding id='{id(self)} host='{host}' port='{port}' mc_port='{mc_port}' host_player='SProfile({host_player})'>")
263
+ s = self.server = server if server else SServer(host, port, mc_port, logger = self.logger)
264
+ self.players = { self.host_player.machine_id: self.host_player }
265
+ self.players_lt:dict[str, float] = {}
266
+ self.conns:dict[str, aio.StreamWriter] = {}
267
+ self.lock = aio.Lock()
268
+ self._clean_worker:Optional[Callable[[], Awaitable[None]]] = None
269
+ self._clean_worker_task:Optional[aio.Task] = None
270
+
271
+ if setup_default_handlers:
272
+ @s.protocol('c:ping')
273
+ async def ping(req:SRequest) -> bytes:
274
+ return req.body
275
+
276
+ @s.protocol('c:protocols')
277
+ async def protocols(req:SRequest) -> list:
278
+ return [k for k in s.protocol_handlers]
279
+
280
+ @s.protocol('c:server_port')
281
+ async def server_port(req:SRequest) -> bytes:
282
+ return s.mc_port.to_bytes(2, byteorder='big', signed=False)
283
+
284
+ @s.protocol('c:player_ping')
285
+ async def player_ping(req:SRequest) -> None:
286
+ p = req.as_pydantic(SProfile)
287
+ async with self.lock:
288
+ if not self.players.get(p.machine_id):
289
+ self.logger.info(f"新玩家加入: <SProfile {p}>")
290
+ if req.writer is not (w := self.conns[p.machine_id]):
291
+ self.logger.info(f"玩家 <SProfile {p}> 使用一个新连接加入了房间,正在关闭旧连接。")
292
+ try:
293
+ w.close()
294
+ await w.wait_closed()
295
+ except Exception:
296
+ pass
297
+
298
+ self.players[p.machine_id] = p
299
+ self.conns[p.machine_id] = req.writer
300
+ self.players_lt[p.machine_id] = time.time()
301
+
302
+ @s.protocol('c:player_easytier_id')
303
+ async def player_easytier_id(req:SRequest) -> None:
304
+ pass
305
+
306
+ @s.protocol('c:player_profiles_list')
307
+ async def player_profiles_list(req:SRequest) -> list[dict]:
308
+ async with self.lock:
309
+ return [p.model_dump(mode='json') for p in self.players.values()]
310
+
311
+ @s.disconnected
312
+ async def disconnected(addr:tuple[str, int]) -> None:
313
+ async with self.lock:
314
+ for mid, w in list(self.conns.items()):
315
+ if w.get_extra_info('peername') == addr:
316
+ try:
317
+ del self.players[mid]
318
+ del self.players_lt[mid]
319
+ del self.conns[mid]
320
+ except Exception as e:
321
+ pass
322
+ finally:
323
+ break
324
+
325
+ async def clean_worker(llt:float = llt, checking_freq:float = 1.0):
326
+ async with Timer(checking_freq, target = time.time) as timer:
327
+ async for t in timer:
328
+ async with self.lock:
329
+ for mid, ct in [i for i in self.players_lt.items()]:
330
+ if t - ct > llt and not mid == self.host_player.machine_id:
331
+ try:
332
+ self.logger.info(f"Cleaning player {mid}")
333
+ w = self.conns[mid]
334
+ w.close()
335
+ await w.wait_closed()
336
+ del self.players[mid]
337
+ del self.players_lt[mid]
338
+ del self.conns[mid]
339
+ except Exception:
340
+ pass
341
+ self._clean_worker = clean_worker
342
+
343
+ async def get_players_async(self) -> ScaffoldingGetPlayersContextManager:
344
+ """
345
+ 通过自动加锁的异步上下文管理器安全地获取玩家列表。
346
+
347
+ 示例:
348
+ >>> app = Scaffolding(...)
349
+ >>> async with app.get_players_async() as players:
350
+ >>> for p in players:
351
+ >>> print(f"<SProfile {p}>")
352
+ """
353
+ return ScaffoldingGetPlayersContextManager(self)
354
+
355
+ async def kick(self, mid:str) -> bool:
356
+ """
357
+ 踢出玩家。
358
+ 方法不保证成功踢出。
359
+ 风险提示:由于 Scaffolding 协议运行在 EasyTier 网络上,且《我的世界》端口号公开透明,玩家有办法完全绕开 Scaffolding 协议,直接通过 EasyTier 网络加入《我的世界》服务器。
360
+
361
+ Args:
362
+ mid (str): 目标玩家的机器标识符。
363
+ """
364
+ try:
365
+ async with self.lock:
366
+ w = self.conns[mid]
367
+ w.close()
368
+ await w.wait_closed()
369
+ return True
370
+ except Exception:
371
+ return False
372
+
373
+ def protocol(self, protocol:str):
374
+ """
375
+ 将函数声明为协议处理器。
376
+ 原函数不会被装饰器替换。
377
+
378
+ Args:
379
+ protocol (str): 要处理的协议,用半角冒号分隔,其中冒号前为命名空间,冒号后为协议名。传入的协议若为 any,则会注册为针对未实现协议的后备处理程序。
380
+ """
381
+ return self.server.protocol(protocol)
382
+
383
+ def connected(self):
384
+ """
385
+ 将函数声明为连接建立时自动调用的 connected 回调函数。
386
+ 原函数不会被装饰器替换。
387
+ """
388
+ return self.server.connected
389
+
390
+ def disconnected(self):
391
+ """
392
+ 将函数声明为连断开时自动调用的 disconnected 回调函数。
393
+ 原函数不会被装饰器替换。
394
+ """
395
+ return self.server.disconnected
396
+
397
+ def easytier(self, code:str, path:str = './easytier-core', dhcp:bool = True, extras:list[str] = []) -> str:
398
+ """
399
+ 按照传入的房间码,生成一种适用于 Windows 平台的 EasyTier 启动命令。
400
+
401
+ Args:
402
+ code (str): Scaffolding 房间码。
403
+ path (str): EasyTier 的路径。
404
+ dhcp (bool): 是否开启 DHCP。如果不开启,须要在 extras 参数中传入要使用的私网地址。
405
+ extras (list[str]): 传递给 EasyTier 额外参数。
406
+ """
407
+ return ' '.join([
408
+ path,
409
+ f"--network-name scaffolding-mc-{code[2:11]}",
410
+ f"--network-secret {code[12:]}",
411
+ f"--hostname scaffolding-mc-server-{self.server.port}",
412
+ f"-p \"https://etnode.zkitefly.eu.org/node1\"",
413
+ f"-p \"https://etnode.zkitefly.eu.org/node2\""
414
+ ] + ([f"-d"] if dhcp else []) + extras[:])
415
+
416
+ async def run(self) -> None:
417
+ """
418
+ 运行 Scaffolding 服务器。
419
+ """
420
+ if self._clean_worker:
421
+ self._clean_worker_task = aio.create_task(self._clean_worker())
422
+ await self.server.run()
423
+ if self._clean_worker_task:
424
+ self._clean_worker_task.cancel()
425
+
426
+ if __name__ == '__main__':
427
+ app = Scaffolding()
428
+ logger = logging.getLogger(__name__)
429
+ logging.basicConfig(level=logging.INFO)
430
+ logger.info('Use command:')
431
+ logger.info(app.easytier('U/AAAA-AAAA-AAAA-AAAA'))
432
+ logger.info('to launch EasyTier')
433
+ aio.run(app.run())
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: msss
3
+ Version: 0.0.1
4
+ Summary: MnlSmile Scaffolding Server 是一个用于架设 Scaffolding 中心服务器的简单框架。
5
+ Home-page: https://github.com/MnlSmile/MSSS
6
+ Author: MnlSmile
7
+ Author-email: kedoukedou33@163.com
8
+ License: MIT
9
+ Project-URL: Bug Tracker, https://github.com/MnlSmile/MSSS/issues
10
+ Project-URL: Source Code, https://github.com/MnlSmile/MSSS
11
+ Keywords: scaffolding
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Topic :: Games/Entertainment
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: py-machineid>=0.5.0
21
+ Requires-Dist: async-timer>=1.0.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: description
27
+ Dynamic: description-content-type
28
+ Dynamic: home-page
29
+ Dynamic: keywords
30
+ Dynamic: license
31
+ Dynamic: license-file
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
36
+
37
+ # MSSS – MnlSmile Scaffolding Server
38
+
39
+ **MSSS** 是一个用于快速架设 [Scaffolding](https://github.com/Scaffolding-MC/Scaffolding-MC) 中心服务器的 Python 异步框架。
40
+
41
+ # MSSS 由一个没有工程经验的初学者开发,能基本应付日常游玩,但请谨慎用于生产环境。
42
+
43
+ ---
44
+
45
+ ## 特性
46
+
47
+ - **开箱即用** – 几行代码即可启动一个完整的 Scaffolding 中心服务器。
48
+ - **自动化** - 能将创建房间的过程自动化,可轻松把本地《我的世界》服务器接入众多启动器联机生态。
49
+ - **协议可扩展** – 通过 `@app.protocol('namespace:protocol')` 装饰器即可注册自定义协议处理器。
50
+
51
+ ---
52
+
53
+ ## 安装
54
+
55
+ ```bash
56
+ pip install msss
57
+ ```
58
+
59
+ 或者从源码安装:
60
+
61
+ ```bash
62
+ git clone https://github.com/MnlSmile/MSSS.git
63
+ cd MSSS
64
+ pip install .
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 快速开始
70
+
71
+ 以下示例启动一个监听 `0.0.0.0:13659` 的 Scaffolding 服务器,并假定《我的世界》服务器运行在 `25565` 端口。
72
+
73
+ ```python
74
+ import asyncio
75
+ import logging
76
+ import subprocess
77
+ from msss import Scaffolding
78
+
79
+ logging.basicConfig(level=logging.INFO)
80
+
81
+ app = Scaffolding(host='0.0.0.0', port=13659, mc_port=25565)
82
+ et_proc = subprocess.Popen(app.easytier('U/0000-0000-0000-0000'), shell=True)
83
+ asyncio.run(app.run())
84
+ et_proc.terminate()
85
+ et_proc.wait()
86
+
87
+ ```
88
+
89
+ 启动后,您的朋友们可以在启动器通过房间码 **U/0000-0000-0000-0000** 加入房间,然后在《我的世界》多人游戏中直接连接。
90
+
91
+ > **提示**:`app.easytier(房间码)` 可以生成对应的 EasyTier 启动命令,详见下文。
92
+
93
+ ---
94
+
95
+ ## 自定义协议处理器
96
+
97
+ 使用 `@app.protocol('命名空间:协议名')` 装饰器即可注册自己的协议。
98
+ 协议处理函数接收一个 `SRequest` 对象,可以返回多种类型的值(会自动编码为正确的响应格式)。如果编码不符合预期,也可以读取 `req.body` 获取原始字节数据。
99
+
100
+ ```python
101
+ @app.protocol('my:hello')
102
+ async def handle_hello(req):
103
+ # 解析客户端发送的 JSON 数据
104
+ data = req.as_json()
105
+ name = data.get('name', 'Guest')
106
+ return {'message': f'Hello, {name}!'}
107
+ ```
108
+
109
+ **支持的返回类型**:
110
+ - `None` → 空响应
111
+ - `str` → UTF-8 编码
112
+ - `bytes` / `bytearray` → 按原样发送
113
+ - `BaseModel` (Pydantic) → JSON 编码
114
+ - `SStrList` → Scaffolding 字符串列表(按 `\0` 分隔)
115
+ - `dict` / `list` → JSON 编码
116
+
117
+ 如果协议未注册,且您注册了 `any` 后备处理器,则会调用它;否则自动返回错误码 `255`。
118
+
119
+ ## 与 EasyTier 集成
120
+
121
+ `<Scaffolding>.easytier(code, path='./easytier-core', dhcp=True, extras=[])` 方法会根据房间码生成一个适用于 Windows 平台的命令。
122
+
123
+ - **`code`**:Scaffolding 房间码,格式例如 `U/AAAA-AAAA-AAAA-AAAA`。
124
+ - **`path`**:`easytier-core` 可执行文件的路径。
125
+ - **`dhcp`**:是否让 EasyTier 自动分配虚拟 IP(强烈建议开启)。
126
+ - **`extras`**:额外的命令行参数列表。
127
+
128
+ 示例输出:
129
+
130
+ ```bash
131
+ ./easytier-core --network-name scaffolding-mc-AAAA-AAAA --network-secret AAAA-AAAA --hostname scaffolding-mc-server-13659 -p "https://etnode.zkitefly.eu.org/node1" -p "https://etnode.zkitefly.eu.org/node2" -d
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 默认行为
137
+
138
+ 默认启用的心跳管理器会:
139
+ - 监听 `c:player_ping` 协议,更新每位玩家的最后活跃时间。
140
+ - 每隔 `checking_freq` 秒检查一次,将超过 `llt` 秒未发送心跳的客户端连接关闭并从玩家列表中移除。
141
+ - 房主(`host`)不会被自动清理。
142
+
143
+ 您可以在创建 `Scaffolding` 实例时调整这两个参数:
144
+
145
+ ```python
146
+ app = Scaffolding(..., llt=30.0, checking_freq=5.0)
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 协议实现参考
152
+
153
+ [Scaffolding 官方协议](https://github.com/Scaffolding-MC/Scaffolding-MC)
154
+
155
+ ---
156
+
157
+ ## 依赖项
158
+
159
+ - [py-machineid](https://github.com/ab77/py-machineid)
160
+ - [async-timer](https://github.com/MnlSmile/async-timer)
161
+ - [pydantic](https://github.com/pydantic/pydantic)
162
+
163
+ ---
164
+
165
+ ## 许可证
166
+
167
+ 本项目使用 **MIT 许可证** 发布。
168
+
169
+ ---
170
+
171
+ ## 相关链接
172
+
173
+ - [Scaffolding 协议](https://github.com/Scaffolding-MC/Scaffolding-MC)
174
+ - [EasyTier](https://github.com/EasyTier/EasyTier)
175
+ - [MSSS](https://github.com/MnlSmile/MSSS)
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ msss/__init__.py
5
+ msss.egg-info/PKG-INFO
6
+ msss.egg-info/SOURCES.txt
7
+ msss.egg-info/dependency_links.txt
8
+ msss.egg-info/requires.txt
9
+ msss.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ py-machineid>=0.5.0
2
+ async-timer>=1.0.0
3
+ pydantic>=2.0.0
@@ -0,0 +1 @@
1
+ msss
msss-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
msss-0.0.1/setup.py ADDED
@@ -0,0 +1,35 @@
1
+ import setuptools
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setuptools.setup(
7
+ name="msss",
8
+ version="0.0.1",
9
+ author="MnlSmile",
10
+ author_email="kedoukedou33@163.com",
11
+ description="MnlSmile Scaffolding Server 是一个用于架设 Scaffolding 中心服务器的简单框架。",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ url="https://github.com/MnlSmile/MSSS",
15
+ project_urls={
16
+ "Bug Tracker": "https://github.com/MnlSmile/MSSS/issues",
17
+ "Source Code": "https://github.com/MnlSmile/MSSS",
18
+ },
19
+ classifiers=[
20
+ "Programming Language :: Python :: 3",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Framework :: AsyncIO",
24
+ "Topic :: Games/Entertainment",
25
+ ],
26
+ packages=setuptools.find_packages(),
27
+ python_requires=">=3.8",
28
+ install_requires=[
29
+ "py-machineid>=0.5.0",
30
+ "async-timer>=1.0.0",
31
+ "pydantic>=2.0.0",
32
+ ],
33
+ license="MIT",
34
+ keywords="scaffolding",
35
+ )