mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2026-04-24 09:06:36 +00:00
Compare commits
78 Commits
abbd64184e
...
Developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 7464a4095d | |||
| 3dc2232db2 | |||
| a16e2e39d9 | |||
| ac2aba4b27 | |||
| 66db0aa9f7 | |||
| 2197a32354 | |||
| 1e95a7519b | |||
| 16d5d06881 | |||
| e43dc69b5c | |||
| 1e685e69ed | |||
| 633e235342 | |||
| cbb3fc8b29 | |||
| 613dfb741a | |||
| c2159fc523 | |||
| 180ab2421e | |||
| 25c3f503bf | |||
| d2c856fd90 | |||
| 189af0d773 | |||
| d003601b58 | |||
| 72035c226b | |||
| 8ed5671995 | |||
| b64c449065 | |||
| f2de91d0f1 | |||
| 243177ee6f | |||
| 7796e3970d | |||
| 2bf1c07041 | |||
| 2b4c0bf4d0 | |||
| 2ee74c310d | |||
| b0303f3e6d | |||
| a406956080 | |||
| 027c239424 | |||
| 9e86c41a6a | |||
| bf1c6d2c41 | |||
| 71ec0c7aed | |||
| 85475a49be | |||
| 8cbe3d07e3 | |||
| b2a608d369 | |||
| c12a91bf86 | |||
| 209004c9cb | |||
| 2af4681082 | |||
| f1f80cc94c | |||
| 06942e8a71 | |||
| 51867d526d | |||
| ff58e2a994 | |||
| 4c6a240f96 | |||
| 666a76201e | |||
| b4b3953194 | |||
| b73674f409 | |||
| 5330943d6f | |||
| c533702cdc | |||
| 3b9cea5cae | |||
| a923dbea1f | |||
| 2eb7e8801b | |||
| c4a34c6630 | |||
| 8fbd2cc330 | |||
| c838d42dd8 | |||
| 274bdead2f | |||
| 163b233306 | |||
| c6fba31c89 | |||
| 59c113ade5 | |||
| a06c48dca5 | |||
| 42e4a8e05e | |||
| 074589da47 | |||
| 50c12f4b5c | |||
| f145048cc1 | |||
| 68bf7d0d00 | |||
| e9815cdfcf | |||
| acb2b45626 | |||
| eb71fda356 | |||
| cc400d5a12 | |||
| 67b3bd26bb | |||
| 77250561fb | |||
| 70dfb9b40c | |||
| 3118c74990 | |||
| cb6adde7c2 | |||
| 422dd35a8f | |||
| 2c2bd1cb4a | |||
| 7eba3d5877 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -140,3 +140,8 @@ dmypy.json
|
|||||||
logs/
|
logs/
|
||||||
*.yml
|
*.yml
|
||||||
*.toml
|
*.toml
|
||||||
|
|
||||||
|
/win-ver_info.txt
|
||||||
|
/output/
|
||||||
|
pip-packets/
|
||||||
|
users.db3
|
||||||
|
|||||||
74
README.md
74
README.md
@@ -13,81 +13,11 @@ I didn't like writing plugins in Lua after using Python; it was very inconvenien
|
|||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|
||||||
- [x] Server core:
|
[TODO.md](./TODO.md)
|
||||||
- [x] BeamMP System:
|
|
||||||
- [x] Private access (Without key, Direct connect)
|
|
||||||
- [x] Public access (With key, listing in Launcher)
|
|
||||||
- [X] Player authentication
|
|
||||||
- [x] TCP Server part:
|
|
||||||
- [x] Handle code
|
|
||||||
- [x] Understanding BeamMP header
|
|
||||||
- [x] Upload mods
|
|
||||||
- [x] Connecting to the world
|
|
||||||
- [x] Chat
|
|
||||||
- [x] Players online counter
|
|
||||||
- [x] Packets handled (Recursive finding second packet)
|
|
||||||
- [x] Client events
|
|
||||||
- [x] Car synchronizations:
|
|
||||||
- [x] State packets
|
|
||||||
- [x] Spawn cars
|
|
||||||
- [x] Delete cars
|
|
||||||
- [x] Edit cars
|
|
||||||
- [x] Reset cars
|
|
||||||
- [x] "ABG": (compressed data)
|
|
||||||
- [x] Decompress data
|
|
||||||
- [x] Compress data
|
|
||||||
- [x] UDP Server part:
|
|
||||||
- [x] Ping
|
|
||||||
- [x] Position synchronizations
|
|
||||||
- [x] Additional:
|
|
||||||
- [x] Logger:
|
|
||||||
- [x] Just logging
|
|
||||||
- [x] Log in file
|
|
||||||
- [x] Log history (.1.log, .2.log, ...)
|
|
||||||
- [x] Console:
|
|
||||||
- [x] Tabulation
|
|
||||||
- [x] History
|
|
||||||
- [x] Autocomplete
|
|
||||||
- [x] Events System:
|
|
||||||
- [x] Call events
|
|
||||||
- [x] Create custom events
|
|
||||||
- [x] Return from events
|
|
||||||
- [x] Async support
|
|
||||||
- [x] Add all events
|
|
||||||
- [x] MultiLanguage: (i18n support)
|
|
||||||
- [x] Core
|
|
||||||
- [x] Console
|
|
||||||
- [x] WebAPI
|
|
||||||
- [x] Plugins supports:
|
|
||||||
- [x] Python part:
|
|
||||||
- [x] Load Python plugins
|
|
||||||
- [x] Async support
|
|
||||||
- [x] KuiToi class
|
|
||||||
- [x] Client (Player) class
|
|
||||||
- [x] Lua part: (Original BeamMP compatibility)
|
|
||||||
- [x] Load Lua plugins
|
|
||||||
- [x] MP Class
|
|
||||||
- [x] Util class
|
|
||||||
- [x] FS class
|
|
||||||
- [ ] HTTP API Server: (fastapi)
|
|
||||||
- [x] Stop and Start with core
|
|
||||||
- [x] Configure FastAPI logger
|
|
||||||
- [ ] Sync with event system
|
|
||||||
- [ ] Add methods...
|
|
||||||
- [ ] RCON System:
|
|
||||||
- [x] Serving
|
|
||||||
- [ ] Handle commands
|
|
||||||
- [x] Client
|
|
||||||
- [x] AES encryption
|
|
||||||
- [ ] KuiToi System
|
|
||||||
- [ ] Servers counter
|
|
||||||
- [ ] Players counter
|
|
||||||
- [ ] Etc.
|
|
||||||
- [ ] [Documentation](./docs)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install **Python 3.10**
|
1. Install **Python 3.12**+
|
||||||
2. Clone the repository in a location of your choice with: `git clone -b Stable https://github.com/kuitoi/kuitoi-Server.git`.
|
2. Clone the repository in a location of your choice with: `git clone -b Stable https://github.com/kuitoi/kuitoi-Server.git`.
|
||||||
3. Change directory into the KuiToi-Server: `cd KuiToi-Server`.
|
3. Change directory into the KuiToi-Server: `cd KuiToi-Server`.
|
||||||
4. Install requirements: `pip install -r requirements.txt`.
|
4. Install requirements: `pip install -r requirements.txt`.
|
||||||
|
|||||||
72
TODO.md
Normal file
72
TODO.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] Server core:
|
||||||
|
- [x] BeamMP System:
|
||||||
|
- [x] Private access (Without key, Direct connect)
|
||||||
|
- [x] Public access (With key, listing in Launcher)
|
||||||
|
- [X] Player authentication
|
||||||
|
- [x] TCP Server part:
|
||||||
|
- [x] Handle code
|
||||||
|
- [x] Understanding BeamMP header
|
||||||
|
- [x] Upload mods
|
||||||
|
- [x] Connecting to the world
|
||||||
|
- [x] Chat
|
||||||
|
- [x] Players online counter
|
||||||
|
- [x] Packets handled (Recursive finding second packet)
|
||||||
|
- [x] Client events
|
||||||
|
- [x] Car synchronizations:
|
||||||
|
- [x] State packets
|
||||||
|
- [x] Spawn cars
|
||||||
|
- [x] Delete cars
|
||||||
|
- [x] Edit cars
|
||||||
|
- [x] Reset cars
|
||||||
|
- [x] "ABG": (compressed data)
|
||||||
|
- [x] Decompress data
|
||||||
|
- [x] Compress data
|
||||||
|
- [x] UDP Server part:
|
||||||
|
- [x] Ping
|
||||||
|
- [x] Position synchronizations
|
||||||
|
- [x] Additional:
|
||||||
|
- [x] Logger:
|
||||||
|
- [x] Just logging
|
||||||
|
- [x] Log in file
|
||||||
|
- [x] Log history (.1.log, .2.log, ...)
|
||||||
|
- [x] Console:
|
||||||
|
- [x] Tabulation
|
||||||
|
- [x] History
|
||||||
|
- [x] Autocomplete
|
||||||
|
- [x] Events System:
|
||||||
|
- [x] Call events
|
||||||
|
- [x] Create custom events
|
||||||
|
- [x] Return from events
|
||||||
|
- [x] Async support
|
||||||
|
- [x] Add all events
|
||||||
|
- [x] MultiLanguage: (i18n support)
|
||||||
|
- [x] Core
|
||||||
|
- [x] Console
|
||||||
|
- [x] Plugins supports:
|
||||||
|
- [x] Python part:
|
||||||
|
- [x] Load Python plugins
|
||||||
|
- [x] Async support
|
||||||
|
- [x] KuiToi class
|
||||||
|
- [x] Client (Player) class
|
||||||
|
- [x] Lua part: (Original BeamMP compatibility)
|
||||||
|
- [x] Load Lua plugins
|
||||||
|
- [x] MP Class
|
||||||
|
- [x] Util class
|
||||||
|
- [x] FS class
|
||||||
|
- [ ] HTTP API Server: (fastapi)
|
||||||
|
- [x] Stop and Start with core
|
||||||
|
- [x] Configure FastAPI logger
|
||||||
|
- [ ] Sync with event system
|
||||||
|
- [ ] Add methods...
|
||||||
|
- [ ] RCON System:
|
||||||
|
- [x] Serving
|
||||||
|
- [ ] Handle commands
|
||||||
|
- [x] Client
|
||||||
|
- [x] AES encryption
|
||||||
|
- [ ] KuiToi System
|
||||||
|
- [ ] Servers counter
|
||||||
|
- [ ] Players counter
|
||||||
|
- [ ] Etc.
|
||||||
|
- [ ] [Documentation](./docs)
|
||||||
@@ -21,9 +21,6 @@
|
|||||||
"GUI_enter_key_message": "请输入密钥:",
|
"GUI_enter_key_message": "请输入密钥:",
|
||||||
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
||||||
|
|
||||||
"": "Web阶段",
|
|
||||||
"web_start": "WebAPI已启动{}(CTRL+C停止)",
|
|
||||||
|
|
||||||
"": "命令:man",
|
"": "命令:man",
|
||||||
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法:man COMMAND",
|
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法:man COMMAND",
|
||||||
"help_message_man": "显示COMMAND的帮助页面。",
|
"help_message_man": "显示COMMAND的帮助页面。",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# MultiLanguage - i18n支持
|
# MultiLanguage - i18n支持
|
||||||
|
|
||||||
在 [example.json](./example.json) 中是 [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json) 的副本。如果你想将其翻译成以前未翻译过的语言,或者更新现有的翻译,我将很高兴接受你的拉取请求。
|
在 [example.json](./example.json) 中是 [src/modules/i18n/files/ru.json](../../../src/translates/ru.json) 的副本。如果你想将其翻译成以前未翻译过的语言,或者更新现有的翻译,我将很高兴接受你的拉取请求。
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _常量_\
|
|||||||
_与open()参数相同_\
|
_与open()参数相同_\
|
||||||
在kt.dir中打开文件
|
在kt.dir中打开文件
|
||||||
|
|
||||||
### kt.register_event(event_name: str, event_func: function)
|
### kt.register(event_name: str, event_func: function)
|
||||||
_`event_name: str` -> 作为`event_func`调用的事件名称._\
|
_`event_name: str` -> 作为`event_func`调用的事件名称._\
|
||||||
_`event_func: function` -> 要调用的函数._
|
_`event_func: function` -> 要调用的函数._
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
# 所有可用事件列表
|
# 所有可用事件列表
|
||||||
|
|
||||||
大多数事件将包含`pl = data ['kwargs'] ['player']`,可以在[这里](./classes.md)找到描述。
|
大多数事件将包含`pl = data ['kwargs'] ['player']`,可以在[这里](./classes.md)找到描述。
|
||||||
|
|
||||||
|
* onPlayerJoin
|
||||||
|
* onPlayerDisconnect
|
||||||
|
* playerLoaded
|
||||||
|
* onChatReceive
|
||||||
|
* onCarSpawn
|
||||||
|
* onCarDelete
|
||||||
|
* onCarEdited
|
||||||
|
* onCarChanged
|
||||||
|
* ...
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async def load():
|
|||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
cgf = config
|
cgf = config
|
||||||
log.info(cgf)
|
log.info(cgf)
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def load():
|
|||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
cgf = config
|
cgf = config
|
||||||
log.info(cgf)
|
log.info(cgf)
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def my_event_handler(event_data):
|
|||||||
|
|
||||||
def load():
|
def load():
|
||||||
# 初始化插件
|
# 初始化插件
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("插件已成功加载。")
|
log.info("插件已成功加载。")
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ def unload():
|
|||||||
您还可以在[example.py](examples/example.py)中找到更广泛的示例。
|
您还可以在[example.py](examples/example.py)中找到更广泛的示例。
|
||||||
|
|
||||||
* 建议在`load()`后使用`open()`,否则应使用`kt.load()`-在`plugin/<plugin_name>/<filename>`文件夹中创建一个文件
|
* 建议在`load()`后使用`open()`,否则应使用`kt.load()`-在`plugin/<plugin_name>/<filename>`文件夹中创建一个文件
|
||||||
* 创建自己的事件:`kt.register_event("my_event", my_event_function)`-
|
* 创建自己的事件:`kt.register("my_event", my_event_function)`-
|
||||||
* 调用事件:`kt.call_event("my_event")`
|
* 调用事件:`kt.call_event("my_event")`
|
||||||
* 使用数据调用事件:`kt.call_event("my_event", data, data2=data2)`
|
* 使用数据调用事件:`kt.call_event("my_event", data, data2=data2)`
|
||||||
* 基本事件:_稍后会写_
|
* 基本事件:_稍后会写_
|
||||||
@@ -72,7 +72,7 @@ async def my_event_handler(event_data):
|
|||||||
|
|
||||||
async def load():
|
async def load():
|
||||||
# 初始化插件
|
# 初始化插件
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("插件已成功加载。")
|
log.info("插件已成功加载。")
|
||||||
|
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ async def unload():
|
|||||||
|
|
||||||
您还可以在[async_example.py](examples/async_example.py)中找到更广泛的示例。
|
您还可以在[async_example.py](examples/async_example.py)中找到更广泛的示例。
|
||||||
|
|
||||||
* 创建自己的事件:`kt.register_event("my_event", my_event_function)`(在register_event中检查函数)
|
* 创建自己的事件:`kt.register("my_event", my_event_function)`(在register中检查函数)
|
||||||
* 调用async事件:`kt.call_async_event("my_event")`
|
* 调用async事件:`kt.call_async_event("my_event")`
|
||||||
* 使用数据调用async事件:`kt.call_async_event("my_event", data, data2=data2)`
|
* 使用数据调用async事件:`kt.call_async_event("my_event", data, data2=data2)`
|
||||||
* 基本的async事件:_稍后会写_
|
* 基本的async事件:_稍后会写_
|
||||||
|
|||||||
@@ -6,5 +6,4 @@
|
|||||||
2. 插件和事件系统 - [这里](./plugins)
|
2. 插件和事件系统 - [这里](./plugins)
|
||||||
3. Lua的细微差别 - [这里](./plugins/lua)
|
3. Lua的细微差别 - [这里](./plugins/lua)
|
||||||
4. 多语言支持 - [这里](./multilanguage)
|
4. 多语言支持 - [这里](./multilanguage)
|
||||||
5. KuiToi WebAPI - [这里](./web)
|
5. 将会有新的内容...
|
||||||
6. 将会有新的内容...
|
|
||||||
@@ -34,7 +34,7 @@ Auth:
|
|||||||
private: true
|
private: true
|
||||||
Game:
|
Game:
|
||||||
map: gridmap_v2
|
map: gridmap_v2
|
||||||
max_cars: 1
|
cars: 1
|
||||||
players: 8
|
players: 8
|
||||||
Options:
|
Options:
|
||||||
debug: false
|
debug: false
|
||||||
@@ -49,11 +49,6 @@ Server:
|
|||||||
name: KuiToi-Server
|
name: KuiToi-Server
|
||||||
server_ip: 0.0.0.0
|
server_ip: 0.0.0.0
|
||||||
server_port: 30814
|
server_port: 30814
|
||||||
WebAPI:
|
|
||||||
enabled: false
|
|
||||||
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
|
|
||||||
server_ip: 127.0.0.1
|
|
||||||
server_port: 8433
|
|
||||||
```
|
```
|
||||||
### Auth
|
### Auth
|
||||||
|
|
||||||
@@ -64,7 +59,7 @@ WebAPI:
|
|||||||
### Game
|
### Game
|
||||||
|
|
||||||
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
|
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
|
||||||
* `max_cars` - 每个玩家的最大汽车数量
|
* `cars` - 每个玩家的最大汽车数量
|
||||||
* `players` - 最大玩家数
|
* `players` - 最大玩家数
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
@@ -83,6 +78,3 @@ WebAPI:
|
|||||||
* `name` - BeamMP 启动器的服务器名称
|
* `name` - BeamMP 启动器的服务器名称
|
||||||
* `server_ip` - 分配给服务器的 IP 地址(仅适用于有经验的用户,默认为 0.0.0.0)
|
* `server_ip` - 分配给服务器的 IP 地址(仅适用于有经验的用户,默认为 0.0.0.0)
|
||||||
* `server_port` - 服务器将在哪个端口上工作
|
* `server_port` - 服务器将在哪个端口上工作
|
||||||
|
|
||||||
### WebAPI
|
|
||||||
##### _文档尚未准备好_
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# 服务器的 WebAPI
|
|
||||||
|
|
||||||
## 可用的端点
|
|
||||||
|
|
||||||
* `/stop`:
|
|
||||||
* 必需参数:
|
|
||||||
* `secret_key` - 在服务器配置中指定的密钥
|
|
||||||
|
|
||||||
|
|
||||||
* `/event.get`
|
|
||||||
* 这个端点还没有准备好
|
|
||||||
* 必需参数:
|
|
||||||
* `secret_key` - 在服务器配置中指定的密钥
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# MultiLanguage - i18n Support
|
# MultiLanguage - i18n Support
|
||||||
|
|
||||||
In [example.json](./example.json) you will find a copy of [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json).\
|
In [example.json](./example.json) you will find a copy of [src/modules/i18n/files/ru.json](../../../src/translates/ru.json).\
|
||||||
If you want to translate to a language that has not been translated before or update an existing translation, I would be happy to receive your pull requests.
|
If you want to translate to a language that has not been translated before or update an existing translation, I would be happy to receive your pull requests.
|
||||||
|
|||||||
@@ -1,127 +1,3 @@
|
|||||||
Sure, here's a translation of the text:
|
|
||||||
|
|
||||||
# Passed Classes
|
|
||||||
|
|
||||||
## Worth looking at
|
|
||||||
|
|
||||||
1. What are `*args` and `**kwargs`? -> [Post on Habr (RU)](https://habr.com/ru/companies/ruvds/articles/482464/)
|
|
||||||
|
|
||||||
## KuiToi
|
|
||||||
_`kt = KuiToi("PluginName"")`_
|
|
||||||
|
|
||||||
### kt.log
|
|
||||||
_Constant_\
|
|
||||||
Returns a pre-configured logger
|
|
||||||
|
|
||||||
### kt.name
|
|
||||||
_Constant_\
|
|
||||||
Returns the name of the plugin
|
|
||||||
|
|
||||||
### kt.dir
|
|
||||||
_Constant_\
|
|
||||||
Returns the directory of the plugin
|
|
||||||
|
|
||||||
### kt.open()
|
|
||||||
_Parameters are the same as for open()_\
|
|
||||||
Opens a file in kt.dir
|
|
||||||
|
|
||||||
### kt.register_event(event_name: str, event_func: function)
|
|
||||||
_`event_name: str` -> The name of the event that `event_func` will be called on._\
|
|
||||||
_`event_func: function` -> The function that will be called._
|
|
||||||
|
|
||||||
In `event_func`, you can pass both regular functions and async functions - you don't need to make them async beforehand.\
|
|
||||||
You can also create your own events with your own names.\
|
|
||||||
You can register an unlimited number of events.
|
|
||||||
|
|
||||||
### kt.call_event(event_name: str, *args, **kwargs) -> list:
|
|
||||||
_`event_name: str` -> The name of the event to call._\
|
|
||||||
_`*args, **kwargs` -> Arguments to be passed to the function._
|
|
||||||
|
|
||||||
### **async** kt.call_async_event(event_name: str, *args, **kwargs) -> list:
|
|
||||||
_`event_name: str` -> The name of the event to call._\
|
|
||||||
_`*args, **kwargs` -> Arguments to be passed to the function._\
|
|
||||||
_Must be called with `await`_
|
|
||||||
|
|
||||||
###### _I recommend familiarizing yourself with *args, **kwargs_, there is a link at the beginning
|
|
||||||
Data is passed to all events in the form of: `{"event_name": event_name, "args": args, "kwargs": kwargs}`\
|
|
||||||
`args: list` -> Represents an array of data passed to the event\
|
|
||||||
`kwargs: dict` -> Represents a dictionary of data passed to the event
|
|
||||||
The data will be returned from all successful attempts in an array.
|
|
||||||
|
|
||||||
### kt.call_lua_event(event_name: str, *args) -> list:
|
|
||||||
_`event_name: str` -> The name of the event to call._\
|
|
||||||
_`*args` -> Arguments to be passed to the function._
|
|
||||||
|
|
||||||
Added to support backward compatibility.\
|
|
||||||
The lua function is called with a direct transmission of arguments `lua_func(*args)`
|
|
||||||
|
|
||||||
### kt.get_player([pid: int], [nick: str]) -> Player | None:
|
|
||||||
_`pid: int` -> Player ID - The identifier of the player._\
|
|
||||||
_`nick: str` -> Player Nickname - The name of the player._
|
|
||||||
|
|
||||||
The method returns a player object by their `pid` or `nick`.\
|
|
||||||
If the player cannot be found, `None` will be returned.
|
|
||||||
|
|
||||||
### kt.get_players() -> List[Player] | list:
|
|
||||||
|
|
||||||
The method returns an array with all players.\
|
|
||||||
The array will be empty if there are no players.
|
|
||||||
|
|
||||||
### kt.players_counter() -> int:
|
|
||||||
|
|
||||||
The method returns the number of players currently online.
|
|
||||||
|
|
||||||
### kt.is_player_connected([pid: int], [nick: str]) -> bool:
|
|
||||||
_`pid: int` -> Player ID - The identifier of the player._\
|
|
||||||
_`nick: str` -> Player Nickname - The name of the player._
|
|
||||||
|
|
||||||
The method returns a player object by their `pid` or `nick`.
|
|
||||||
|
|
||||||
## Player (or Client)
|
|
||||||
_`pl = kt.get_player()`_\
|
|
||||||
_`pl = event_data['kwargs']['player']`_
|
|
||||||
|
|
||||||
### pl.log -> Logger
|
|
||||||
_Constant_\
|
|
||||||
Returns a pre-configured logger
|
|
||||||
|
|
||||||
### pl.addr -> str
|
|
||||||
_Constant_\
|
|
||||||
Returns the IP address of the player
|
|
||||||
|
|
||||||
### pl.pid -> int
|
|
||||||
### pl.cid -> int
|
|
||||||
_Constant_\
|
|
||||||
Returns the client ID _(pid: PlayerId = cid: ClientId)_
|
|
||||||
|
|
||||||
### pl.key -> str
|
|
||||||
_Constant_\
|
|
||||||
Returns the key passed during authentication
|
|
||||||
|
|
||||||
### pl.nick -> str
|
|
||||||
_Variable_\
|
|
||||||
The nickname passed during authentication from the BeamMP server, can be changed, consequences are untested
|
|
||||||
|
|
||||||
### pl.roles -> str
|
|
||||||
_Variable_\
|
|
||||||
The role passed during authentication from the BeamMP server, can be changed (if an incorrect role is set, unexpected things may happen.)
|
|
||||||
|
|
||||||
### pl.guest -> bool
|
|
||||||
_Constant_\
|
|
||||||
Returns whether the player is a guest, passed during authentication from the BeamMP server
|
|
||||||
|
|
||||||
### pl.identifiers -> dict
|
|
||||||
_Constant_\
|
|
||||||
Identifiers passed during authentication from the BeamMP server.
|
|
||||||
|
|
||||||
### pl.ready -> bool
|
|
||||||
_Constant, changed by the core_\
|
|
||||||
Returns a bool value, if True -> the player has downloaded all resources, loaded on the map
|
|
||||||
|
|
||||||
### pl.cars -> dict
|
|
||||||
_Constant, changed by the core_\
|
|
||||||
Returns a dictionary of cars like thisSure, here's the translation:
|
|
||||||
|
|
||||||
# Passed Classes
|
# Passed Classes
|
||||||
|
|
||||||
## Worth looking at
|
## Worth looking at
|
||||||
@@ -147,7 +23,7 @@ Returns the directory of the plugin
|
|||||||
_Parameters are the same as for open()_\
|
_Parameters are the same as for open()_\
|
||||||
Opens a file in kt.dir
|
Opens a file in kt.dir
|
||||||
|
|
||||||
### kt.register_event(event_name: str, event_func: function)
|
### kt.register(event_name: str, event_func: function)
|
||||||
_`event_name: str` -> The name of the event that `event_func` will be called on._\
|
_`event_name: str` -> The name of the event that `event_func` will be called on._\
|
||||||
_`event_func: function` -> The function that will be called._
|
_`event_func: function` -> The function that will be called._
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,12 @@
|
|||||||
|
|
||||||
Most events will receive `pl = data['kwargs']['player']`, you can find a description [here](./classes.md)
|
Most events will receive `pl = data['kwargs']['player']`, you can find a description [here](./classes.md)
|
||||||
|
|
||||||
|
* onPlayerJoin
|
||||||
|
* onPlayerDisconnect
|
||||||
|
* playerLoaded
|
||||||
|
* onChatReceive
|
||||||
|
* onCarSpawn
|
||||||
|
* onCarDelete
|
||||||
|
* onCarEdited
|
||||||
|
* onCarChanged
|
||||||
|
* ...
|
||||||
@@ -21,7 +21,7 @@ async def load():
|
|||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
cgf = config
|
cgf = config
|
||||||
log.info(cgf)
|
log.info(cgf)
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def load():
|
|||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
cgf = config
|
cgf = config
|
||||||
log.info(cgf)
|
log.info(cgf)
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def my_event_handler(event_data):
|
|||||||
|
|
||||||
def load():
|
def load():
|
||||||
# Plugin initialization
|
# Plugin initialization
|
||||||
kt.register_event("my_event", my_event_handler)
|
kt.register("my_event", my_event_handler)
|
||||||
log.info("Plugin loaded successfully.")
|
log.info("Plugin loaded successfully.")
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ def unload():
|
|||||||
A more comprehensive example can also be found in [example.py](examples/example.py)
|
A more comprehensive example can also be found in [example.py](examples/example.py)
|
||||||
|
|
||||||
* It is recommended to use `open()` after `load()`, otherwise use `kt.load()` - It creates a file in the `plugin/<plugin_name>/<filename>` folder.
|
* It is recommended to use `open()` after `load()`, otherwise use `kt.load()` - It creates a file in the `plugin/<plugin_name>/<filename>` folder.
|
||||||
* Creating your own event: `kt.register_event("my_event", my_event_function)` -
|
* Creating your own event: `kt.register("my_event", my_event_function)` -
|
||||||
* Calling an event: `kt.call_event("my_event")`
|
* Calling an event: `kt.call_event("my_event")`
|
||||||
* Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
|
* Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
|
||||||
* Base events: _To be added later_
|
* Base events: _To be added later_
|
||||||
@@ -75,7 +75,7 @@ async def my_event_handler(event_data):
|
|||||||
|
|
||||||
async def load():
|
async def load():
|
||||||
# Plugin initialization
|
# Plugin initialization
|
||||||
kt.register_event("my_event", my_event_handler)
|
kt.register("my_event", my_event_handler)
|
||||||
log.info("Plugin loaded successfully.")
|
log.info("Plugin loaded successfully.")
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ async def unload():
|
|||||||
|
|
||||||
A more comprehensive example can also be found in [async_example.py](examples/async_example.py)
|
A more comprehensive example can also be found in [async_example.py](examples/async_example.py)
|
||||||
|
|
||||||
* Creating your own event: `kt.register_event("my_event", my_event_function)` (register_event has a function check)
|
* Creating your own event: `kt.register("my_event", my_event_function)` (register has a function check)
|
||||||
* Calling an async event: `kt.call_async_event("my_event")`
|
* Calling an async event: `kt.call_async_event("my_event")`
|
||||||
* Calling an async event with data: `kt.call_async_event("my_event", data, data2=data2)`
|
* Calling an async event with data: `kt.call_async_event("my_event", data, data2=data2)`
|
||||||
* Base async events: _To be added later_
|
* Base async events: _To be added later_
|
||||||
@@ -34,7 +34,7 @@ Auth:
|
|||||||
private: true
|
private: true
|
||||||
Game:
|
Game:
|
||||||
map: gridmap_v2
|
map: gridmap_v2
|
||||||
max_cars: 1
|
cars: 1
|
||||||
players: 8
|
players: 8
|
||||||
Options:
|
Options:
|
||||||
debug: false
|
debug: false
|
||||||
@@ -64,7 +64,7 @@ WebAPI:
|
|||||||
### Game
|
### Game
|
||||||
|
|
||||||
* `map` is only the name of the map, i.e. open the mod with the map in `map.zip/levels` - the name of the map will be there, that's what we insert.
|
* `map` is only the name of the map, i.e. open the mod with the map in `map.zip/levels` - the name of the map will be there, that's what we insert.
|
||||||
* `max_cars` - Maximum number of cars per player
|
* `cars` - Maximum number of cars per player
|
||||||
* `players` - Maximum number of players
|
* `players` - Maximum number of players
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
Here's the translation of the readme.txt content:
|
|
||||||
|
|
||||||
# WebAPI for the server
|
|
||||||
|
|
||||||
## Available endpoints
|
|
||||||
|
|
||||||
* `/stop`:
|
|
||||||
* Required parameters:
|
|
||||||
* `secret_key` - The key specified in the server configuration
|
|
||||||
|
|
||||||
|
|
||||||
* `/event.get`
|
|
||||||
* The endpoint is not yet ready
|
|
||||||
* Required parameters:
|
|
||||||
* `secret_key` - The key specified in the server configuration
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# MultiLanguage - Поддержка i18n
|
# MultiLanguage - Поддержка i18n
|
||||||
|
|
||||||
В [example.json](./example.json) это копия [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json)\
|
В [example.json](./example.json) это копия [src/modules/i18n/files/ru.json](../../../src/translates/ru.json)\
|
||||||
Если есть желание перевести на не переведённый ранее язык, или обновить уже существующий перевод буду рад вашим пул реквестам.
|
Если есть желание перевести на не переведённый ранее язык, или обновить уже существующий перевод буду рад вашим пул реквестам.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _Константа_\
|
|||||||
_Параметры как у open()_\
|
_Параметры как у open()_\
|
||||||
Открывает файл в kt.dir
|
Открывает файл в kt.dir
|
||||||
|
|
||||||
### kt.register_event(event_name: str, event_func: function)
|
### kt.register(event_name: str, event_func: function)
|
||||||
_`event_name: str` -> Имя ивента, по которому будет вызвана `event_func`._\
|
_`event_name: str` -> Имя ивента, по которому будет вызвана `event_func`._\
|
||||||
_`event_func: function` -> Функция, которая будет вызвана._
|
_`event_func: function` -> Функция, которая будет вызвана._
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,12 @@
|
|||||||
|
|
||||||
В большинство ивентов будет приходить `pl = data['kwargs']['player']`, описание можно найти [тут](./classes.md)
|
В большинство ивентов будет приходить `pl = data['kwargs']['player']`, описание можно найти [тут](./classes.md)
|
||||||
|
|
||||||
###
|
* onPlayerJoin
|
||||||
|
* onPlayerDisconnect
|
||||||
|
* playerLoaded
|
||||||
|
* onChatReceive
|
||||||
|
* onCarSpawn
|
||||||
|
* onCarDelete
|
||||||
|
* onCarEdited
|
||||||
|
* onCarChanged
|
||||||
|
* ...
|
||||||
@@ -21,7 +21,7 @@ async def load():
|
|||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
cgf = config
|
cgf = config
|
||||||
log.info(cgf)
|
log.info(cgf)
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def load():
|
|||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
cgf = config
|
cgf = config
|
||||||
log.info(cgf)
|
log.info(cgf)
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def my_event_handler(event_data):
|
|||||||
|
|
||||||
def load():
|
def load():
|
||||||
# Инициализация плагина
|
# Инициализация плагина
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ def unload():
|
|||||||
Так же более обширный пример можно найти в [example.py](examples/example.py)
|
Так же более обширный пример можно найти в [example.py](examples/example.py)
|
||||||
|
|
||||||
* Рекомендуется использовать `open()` после `load()`, иначе стоит использовать `kt.load()` - Создаёт файл в папке `plugin/<plugin_name>/<filename>`
|
* Рекомендуется использовать `open()` после `load()`, иначе стоит использовать `kt.load()` - Создаёт файл в папке `plugin/<plugin_name>/<filename>`
|
||||||
* Создание своего ивента : `kt.register_event("my_event", my_event_function)` -
|
* Создание своего ивента : `kt.register("my_event", my_event_function)` -
|
||||||
* Вызов ивента: `kt.call_event("my_event")`
|
* Вызов ивента: `kt.call_event("my_event")`
|
||||||
* Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)`
|
* Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)`
|
||||||
* Базовые ивенты: _Позже напишу_
|
* Базовые ивенты: _Позже напишу_
|
||||||
@@ -71,7 +71,7 @@ async def my_event_handler(event_data):
|
|||||||
|
|
||||||
async def load():
|
async def load():
|
||||||
# Инициализация плагина
|
# Инициализация плагина
|
||||||
ev.register_event("my_event", my_event_handler)
|
ev.register("my_event", my_event_handler)
|
||||||
log.info("Плагин загружен успешно.")
|
log.info("Плагин загружен успешно.")
|
||||||
|
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ async def unload():
|
|||||||
|
|
||||||
Так же более обширный пример можно найти в [async_example.py](examples/async_example.py)
|
Так же более обширный пример можно найти в [async_example.py](examples/async_example.py)
|
||||||
|
|
||||||
* Создание своего ивента: `kt.register_event("my_event", my_event_function)` (в register_event стоит проверка на функцию)
|
* Создание своего ивента: `kt.register("my_event", my_event_function)` (в register стоит проверка на функцию)
|
||||||
* Вызов async ивента: `kt.call_async_event("my_event")`
|
* Вызов async ивента: `kt.call_async_event("my_event")`
|
||||||
* Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)`
|
* Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)`
|
||||||
* Базовые async ивенты: _Позже напишу_
|
* Базовые async ивенты: _Позже напишу_
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Auth:
|
|||||||
private: true
|
private: true
|
||||||
Game:
|
Game:
|
||||||
map: gridmap_v2
|
map: gridmap_v2
|
||||||
max_cars: 1
|
cars: 1
|
||||||
players: 8
|
players: 8
|
||||||
Options:
|
Options:
|
||||||
debug: false
|
debug: false
|
||||||
@@ -64,7 +64,7 @@ WebAPI:
|
|||||||
### Game
|
### Game
|
||||||
|
|
||||||
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
|
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
|
||||||
* `max_cars` - Максимальное количество машин на игрока
|
* `cars` - Максимальное количество машин на игрока
|
||||||
* `players` - Максимально количество игроков
|
* `players` - Максимально количество игроков
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
# WebAPI для сервера
|
|
||||||
|
|
||||||
## Доступные endpoints
|
|
||||||
|
|
||||||
* `/stop`:
|
|
||||||
* Необходимые парамеры:
|
|
||||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
|
||||||
|
|
||||||
|
|
||||||
* `/event.get`
|
|
||||||
* Точка не готова
|
|
||||||
* Необходимые парамеры:
|
|
||||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
|
||||||
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
PyYAML~=6.0
|
PyYAML~=6.0
|
||||||
prompt-toolkit~=3.0.38
|
prompt-toolkit~=3.0.38
|
||||||
aiohttp~=3.8.4
|
aiohttp~=3.9.5
|
||||||
uvicorn~=0.22.0
|
|
||||||
fastapi~=0.100.0
|
|
||||||
starlette~=0.27.0
|
|
||||||
pydantic~=2.0.2
|
|
||||||
click~=8.1.4
|
|
||||||
lupa~=2.0
|
lupa~=2.0
|
||||||
toml~=0.10.2
|
toml~=0.10.2
|
||||||
colorama~=0.4.6
|
colorama~=0.4.6
|
||||||
cryptography~=41.0.2
|
cryptography~=42.0.4
|
||||||
|
prompt_toolkit~=3.0.47
|
||||||
|
requests~=2.32.3
|
||||||
|
Pygments~=2.18.0
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.tcp_server.py
|
# File core.tcp_server.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.8
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -9,6 +9,7 @@ import json
|
|||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
import zlib
|
import zlib
|
||||||
|
from asyncio import Queue
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
|
||||||
@@ -18,9 +19,29 @@ class Client:
|
|||||||
def __init__(self, reader, writer, core):
|
def __init__(self, reader, writer, core):
|
||||||
self.__reader = reader
|
self.__reader = reader
|
||||||
self.__writer = writer
|
self.__writer = writer
|
||||||
self.__Core = core
|
self._core = core
|
||||||
self.__alive = True
|
self.__alive = True
|
||||||
self.__packets_queue = []
|
|
||||||
|
self.__queue_tpc = Queue()
|
||||||
|
self.__queue_udp = Queue()
|
||||||
|
|
||||||
|
self._tpc_count_recv = 0
|
||||||
|
self._udp_count_recv = 0
|
||||||
|
self._tpc_count_total_recv = 0
|
||||||
|
self._udp_count_total_recv = 0
|
||||||
|
self._udp_size_total_recv = 0.1
|
||||||
|
self._tpc_size_total_recv = 0.1
|
||||||
|
|
||||||
|
# self._tpc_count_sent = 0
|
||||||
|
# self._udp_count_sent = 0
|
||||||
|
self._tpc_count_total_sent = 0
|
||||||
|
self._udp_count_total_sent = 0
|
||||||
|
self._udp_size_total_sent = 0.1
|
||||||
|
self._tpc_size_total_sent = 0.1
|
||||||
|
|
||||||
|
self.tcp_pps = 0
|
||||||
|
self.udp_pps = 0
|
||||||
|
|
||||||
self.__tasks = []
|
self.__tasks = []
|
||||||
self._down_sock = (None, None)
|
self._down_sock = (None, None)
|
||||||
self._udp_sock = (None, None)
|
self._udp_sock = (None, None)
|
||||||
@@ -32,13 +53,16 @@ class Client:
|
|||||||
self.nick = None
|
self.nick = None
|
||||||
self.roles = None
|
self.roles = None
|
||||||
self._guest = True
|
self._guest = True
|
||||||
|
self._synced = False
|
||||||
self._ready = False
|
self._ready = False
|
||||||
self._identifiers = []
|
self._identifiers = []
|
||||||
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
|
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
|
||||||
self._focus_car = -1
|
self._focus_car = -1
|
||||||
self._snowman = {"id": -1, "packet": ""}
|
self._unicycle = {"id": -1, "packet": ""}
|
||||||
self._connect_time = 0
|
self._connect_time = 0
|
||||||
self._last_position = {}
|
self._last_position = {}
|
||||||
|
self._last_recv = time.monotonic()
|
||||||
|
self.__tpt_id = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _writer(self):
|
def _writer(self):
|
||||||
@@ -72,6 +96,10 @@ class Client:
|
|||||||
def guest(self):
|
def guest(self):
|
||||||
return self._guest
|
return self._guest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def synced(self):
|
||||||
|
return self._synced
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self):
|
def ready(self):
|
||||||
return self._ready
|
return self._ready
|
||||||
@@ -123,17 +151,24 @@ class Client:
|
|||||||
if not message:
|
if not message:
|
||||||
message = "no message"
|
message = "no message"
|
||||||
to_all = False
|
to_all = False
|
||||||
await self._send(f"C:{message!r}", to_all=to_all)
|
if "\n" in message:
|
||||||
|
ms = message.split("\n")
|
||||||
|
for m in ms:
|
||||||
|
await self.send_message(m, to_all)
|
||||||
|
return
|
||||||
|
await self._send(f"C:{message}", to_all=to_all)
|
||||||
|
|
||||||
async def send_event(self, event_name, event_data, to_all=True):
|
async def send_event(self, event_name, event_data, to_all=False):
|
||||||
|
self.log.debug(f"send_event: {event_name}:{event_data}; {to_all=}")
|
||||||
|
if not self.ready:
|
||||||
|
self.log.debug(f"Client not ready. {event_data=}")
|
||||||
|
return
|
||||||
if isinstance(event_data, (list, tuple, dict)):
|
if isinstance(event_data, (list, tuple, dict)):
|
||||||
event_data = json.dumps(event_data, separators=(',', ':'))
|
event_data = json.dumps(event_data, separators=(',', ':'))
|
||||||
else:
|
if len(event_data) > 99 * MB:
|
||||||
event_data = f"{event_data!r}"
|
self.log.error(f"Error while preparing event {event_name!r}: data too big! data>99MB")
|
||||||
if len(event_data) > 104857599:
|
|
||||||
self.log.error("Client data too big! >=104857599")
|
|
||||||
return
|
return
|
||||||
await self._send(f"E:{event_name}:{event_data}", to_all=to_all)
|
await self._send(f"E:{event_name}:{event_data}", to_all, True)
|
||||||
|
|
||||||
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
|
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
|
||||||
|
|
||||||
@@ -148,7 +183,7 @@ class Client:
|
|||||||
|
|
||||||
if to_all:
|
if to_all:
|
||||||
code = chr(data[0])
|
code = chr(data[0])
|
||||||
for client in self.__Core.clients:
|
for client in self._core.clients:
|
||||||
if not client or (client is self and not to_self):
|
if not client or (client is self and not to_self):
|
||||||
continue
|
continue
|
||||||
if not to_udp or code in ['V', 'W', 'Y', 'E']:
|
if not to_udp or code in ['V', 'W', 'Y', 'E']:
|
||||||
@@ -167,13 +202,14 @@ class Client:
|
|||||||
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
|
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
|
||||||
|
|
||||||
if to_udp:
|
if to_udp:
|
||||||
udp_sock = self._udp_sock[0]
|
udp_sock, udp_addr = self._udp_sock
|
||||||
udp_addr = self._udp_sock[1]
|
|
||||||
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
|
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
|
||||||
if udp_sock and udp_addr:
|
if udp_sock and udp_addr:
|
||||||
try:
|
try:
|
||||||
if not udp_sock.is_closing():
|
if not udp_sock.is_closing():
|
||||||
# self.log.debug(f'[UDP] {data!r}')
|
# self.log.debug(f'[UDP] {data!r}; {udp_addr}')
|
||||||
|
self._udp_count_total_sent += 1
|
||||||
|
self._udp_size_total_sent += len(data)
|
||||||
udp_sock.sendto(data, udp_addr)
|
udp_sock.sendto(data, udp_addr)
|
||||||
except OSError:
|
except OSError:
|
||||||
self.log.debug("[UDP] Error sending")
|
self.log.debug("[UDP] Error sending")
|
||||||
@@ -183,14 +219,16 @@ class Client:
|
|||||||
return
|
return
|
||||||
|
|
||||||
header = len(data).to_bytes(4, "little", signed=True)
|
header = len(data).to_bytes(4, "little", signed=True)
|
||||||
# self.log.debug(f'[TCP] {header + data!r}')
|
data = header + data
|
||||||
|
# self.log.debug(f'[TCP] {data!r}')
|
||||||
try:
|
try:
|
||||||
writer.write(header + data)
|
self._tpc_count_total_sent += 1
|
||||||
|
self._tpc_size_total_sent += len(data)
|
||||||
|
writer.write(data)
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f'[TCP] Disconnected: {e}')
|
self.log.debug(f'[TCP] Disconnected: {e}; {writer=}')
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
await self._remove_me()
|
await self._remove_me()
|
||||||
return False
|
return False
|
||||||
@@ -213,12 +251,11 @@ class Client:
|
|||||||
self.is_disconnected()
|
self.is_disconnected()
|
||||||
if self.__alive:
|
if self.__alive:
|
||||||
if header == b"":
|
if header == b"":
|
||||||
self.__packets_queue.append(None)
|
await self._tpc_put(None)
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
continue
|
continue
|
||||||
self.log.error(f"Header: {header}")
|
self.log.error(f"Header: {header}")
|
||||||
await self.kick("Invalid packet - header negative")
|
await self.kick("Invalid packet - header negative")
|
||||||
self.__packets_queue.append(None)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if int_header > 100 * MB:
|
if int_header > 100 * MB:
|
||||||
@@ -226,7 +263,6 @@ class Client:
|
|||||||
self.log.warning("Client sent header of >100MB - "
|
self.log.warning("Client sent header of >100MB - "
|
||||||
"assuming malicious intent and disconnecting the client.")
|
"assuming malicious intent and disconnecting the client.")
|
||||||
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
|
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
|
||||||
self.__packets_queue.append(None)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data = b""
|
data = b""
|
||||||
@@ -244,31 +280,39 @@ class Client:
|
|||||||
|
|
||||||
if one:
|
if one:
|
||||||
return data
|
return data
|
||||||
self.__packets_queue.append(data)
|
await self._tpc_put(data)
|
||||||
|
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
self.__packets_queue.append(None)
|
await self._tpc_put(None)
|
||||||
|
|
||||||
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
|
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
|
||||||
real_size = end - start
|
size = end - start
|
||||||
writer = self._down_sock[1] if d_sock else self.__writer
|
writer = self._down_sock[1] if d_sock else self.__writer
|
||||||
who = 'dwn' if d_sock else 'srv'
|
who = 'DSock' if d_sock else 'MSock'
|
||||||
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
|
self.log.debug(f"[{who}] Started; start,end={(start, end)}")
|
||||||
|
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, 'rb') as f:
|
||||||
f.seek(start)
|
f.seek(start)
|
||||||
total_sent = 0
|
total_sent = 0
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
while total_sent < real_size:
|
while total_sent < size:
|
||||||
data = f.read(min(MB, real_size - total_sent)) # read data in chunks of 1MB or less
|
if (size - total_sent) == 0:
|
||||||
|
break
|
||||||
|
data = f.read(min(MB, size - total_sent)) # read data in chunks of 1MB or less
|
||||||
try:
|
try:
|
||||||
writer.write(data)
|
writer.write(data)
|
||||||
|
async with asyncio.timeout(120): # ~100kb/s
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
# self.log.debug(f"[{who}] Sent {len(data)} bytes.")
|
# self.log.debug(f"[{who}] Sent {len(data)} bytes.")
|
||||||
except ConnectionError:
|
except TimeoutError:
|
||||||
|
self.log.debug(f"[{who}] TimeoutError; Sock: {writer}")
|
||||||
|
self.log.error("TimeoutError")
|
||||||
|
self.__alive = False
|
||||||
|
break
|
||||||
|
except ConnectionError:
|
||||||
|
self.log.debug(f"[{who}] Disconnected; Sock: {writer}")
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
self.log.debug(f"[{who}] Disconnected.")
|
|
||||||
break
|
break
|
||||||
total_sent += len(data)
|
total_sent += len(data)
|
||||||
|
|
||||||
@@ -278,7 +322,7 @@ class Client:
|
|||||||
expected_time = total_sent / (speed_limit * MB)
|
expected_time = total_sent / (speed_limit * MB)
|
||||||
if expected_time > elapsed_time:
|
if expected_time > elapsed_time:
|
||||||
await asyncio.sleep(expected_time - elapsed_time)
|
await asyncio.sleep(expected_time - elapsed_time)
|
||||||
|
self.log.debug(f"[{who}] Ready. {total_sent=}")
|
||||||
return total_sent
|
return total_sent
|
||||||
|
|
||||||
async def _sync_resources(self):
|
async def _sync_resources(self):
|
||||||
@@ -291,17 +335,17 @@ class Client:
|
|||||||
file = data[1:].decode(config.enc)
|
file = data[1:].decode(config.enc)
|
||||||
self.log.info(i18n.client_mod_request.format(repr(file)))
|
self.log.info(i18n.client_mod_request.format(repr(file)))
|
||||||
size = -1
|
size = -1
|
||||||
for mod in self.__Core.mods_list:
|
for mod in self._core.mods_list:
|
||||||
if type(mod) == int:
|
if type(mod) == int:
|
||||||
continue
|
continue
|
||||||
if mod.get('path') == file:
|
if mod.get('path') == file:
|
||||||
size = mod['size']
|
size = mod['size']
|
||||||
self.log.debug("File is accept.")
|
self.log.debug("File is accept.")
|
||||||
break
|
break
|
||||||
self.log.debug(f"Mode size: {size}")
|
# self.log.debug(f"Mod size: {size}")
|
||||||
if size == -1:
|
if size == -1:
|
||||||
await self._send(b"CO")
|
await self._send(b"CO")
|
||||||
await self.kick(f"Not allowed mod: " + file)
|
await self.kick(f"Requested not allowed file: " + file)
|
||||||
return
|
return
|
||||||
await self._send(b"AG")
|
await self._send(b"AG")
|
||||||
t = 0
|
t = 0
|
||||||
@@ -309,25 +353,25 @@ class Client:
|
|||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
t += 1
|
t += 1
|
||||||
if t > 50:
|
if t > 50:
|
||||||
await self.kick("Missing download socket")
|
await self.kick("Error (Missing DSock)")
|
||||||
return
|
return
|
||||||
if config.Options['use_queue']:
|
if config.Options['use_queue']:
|
||||||
while self.__Core.lock_upload:
|
while self._core.lock_upload:
|
||||||
await asyncio.sleep(.2)
|
await asyncio.sleep(.2)
|
||||||
self.__Core.lock_upload = True
|
self._core.lock_upload = True
|
||||||
speed = config.Options["speed_limit"]
|
speed = config.Options["speed_limit"] or 25*B
|
||||||
if speed:
|
if speed:
|
||||||
speed = speed / 2
|
speed = speed / 2
|
||||||
half_size = math.floor(size / 2)
|
half_size = size // 2
|
||||||
t = time.monotonic()
|
t = time.monotonic()
|
||||||
uploads = [
|
self.log.debug(f"Sending: {size=}; sl0={(0, half_size)}; sl1={(half_size, size)}")
|
||||||
self._split_load(0, half_size, False, file, speed),
|
async with asyncio.TaskGroup() as tg:
|
||||||
self._split_load(half_size, size, True, file, speed)
|
sl0 = tg.create_task(self._split_load(0, half_size, False, file, speed))
|
||||||
]
|
sl1 = tg.create_task(self._split_load(half_size, size, True, file, speed))
|
||||||
sl0, sl1 = await asyncio.gather(*uploads)
|
sl0, sl1 = sl0.result(), sl1.result()
|
||||||
tr = time.monotonic() - t
|
tr = (time.monotonic() - t) or 0.0001
|
||||||
if self.__Core.lock_upload:
|
if self._core.lock_upload:
|
||||||
self.__Core.lock_upload = False
|
self._core.lock_upload = False
|
||||||
msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
|
msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
|
||||||
if speed:
|
if speed:
|
||||||
msg += i18n.client_mod_sent_limit.format(int(speed * 2))
|
msg += i18n.client_mod_sent_limit.format(int(speed * 2))
|
||||||
@@ -335,17 +379,17 @@ class Client:
|
|||||||
sent = sl0 + sl1
|
sent = sl0 + sl1
|
||||||
ok = sent == size
|
ok = sent == size
|
||||||
lost = size - sent
|
lost = size - sent
|
||||||
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
|
self.log.debug(f"Sent; sl_0: {sl0}; sl_1: {sl1}; size==sent is {ok}: {size}-{sent}={lost}")
|
||||||
if not ok:
|
if not ok:
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
self.log.error(i18n.client_mod_sent_error.format(repr(file)))
|
e = i18n.client_mod_sent_error.format(repr(file))
|
||||||
|
await self._send(f"E{e}")
|
||||||
|
self.log.error(e)
|
||||||
return
|
return
|
||||||
elif data.startswith(b"SR"):
|
elif data.startswith(b"SR"):
|
||||||
path_list = ''
|
path_list = ''
|
||||||
size_list = ''
|
size_list = ''
|
||||||
for mod in self.__Core.mods_list:
|
for mod in self._core.mods_list[1:]:
|
||||||
if type(mod) == int:
|
|
||||||
continue
|
|
||||||
path_list += f"{mod['path']};"
|
path_list += f"{mod['path']};"
|
||||||
size_list += f"{mod['size']};"
|
size_list += f"{mod['size']};"
|
||||||
mod_list = path_list + size_list
|
mod_list = path_list + size_list
|
||||||
@@ -355,6 +399,7 @@ class Client:
|
|||||||
else:
|
else:
|
||||||
await self._send(mod_list)
|
await self._send(mod_list)
|
||||||
elif data == b"Done":
|
elif data == b"Done":
|
||||||
|
self.log.debug("recv Done")
|
||||||
await self._send(f"M/levels/{config.Game['map']}/info.json")
|
await self._send(f"M/levels/{config.Game['map']}/info.json")
|
||||||
break
|
break
|
||||||
return
|
return
|
||||||
@@ -385,7 +430,7 @@ class Client:
|
|||||||
car_data = data[2:]
|
car_data = data[2:]
|
||||||
car_id = next((i for i, car in enumerate(self._cars) if car is None), len(self._cars))
|
car_id = next((i for i, car in enumerate(self._cars) if car is None), len(self._cars))
|
||||||
cars_count = len(self._cars) - self._cars.count(None)
|
cars_count = len(self._cars) - self._cars.count(None)
|
||||||
if self._snowman['id'] != -1:
|
if self._unicycle['id'] != -1:
|
||||||
cars_count -= 1 # -1 for unicycle
|
cars_count -= 1 # -1 for unicycle
|
||||||
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
|
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
|
||||||
car_json = {}
|
car_json = {}
|
||||||
@@ -394,28 +439,26 @@ class Client:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
|
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
|
||||||
allow = True
|
allow = True
|
||||||
allow_snowman = True
|
allow_unicycle = True
|
||||||
over_spawn = False
|
over_spawn = False
|
||||||
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
|
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
|
||||||
if 1 in lua_data:
|
if 1 in lua_data:
|
||||||
allow = False
|
allow = False
|
||||||
ev_data_list = ev.call_event("onCarSpawn", data=car_json, car_id=car_id, player=self)
|
event_data = await ev.call_as_events("onCarSpawn", data=car_json, car_id=car_id, player=self)
|
||||||
d2 = await ev.call_async_event("onCarSpawn", data=car_json, car_id=car_id, player=self)
|
for ev_data in event_data:
|
||||||
ev_data_list.extend(d2)
|
|
||||||
for ev_data in ev_data_list:
|
|
||||||
self.log.debug(ev_data)
|
self.log.debug(ev_data)
|
||||||
# TODO: handle event onCarSpawn
|
# TODO: handle event onCarSpawn
|
||||||
pass
|
pass
|
||||||
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
|
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
|
||||||
snowman = car_json.get("jbm") == "unicycle"
|
unicycle = car_json.get("jbm") == "unicycle"
|
||||||
if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn:
|
if allow and config.Game['cars'] > cars_count or (unicycle and allow_unicycle) or over_spawn:
|
||||||
if snowman:
|
if unicycle:
|
||||||
unicycle_id = self._snowman['id']
|
unicycle_id = self._unicycle['id']
|
||||||
if unicycle_id != -1:
|
if unicycle_id != -1:
|
||||||
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}")
|
self.log.debug(f"Delete old unicycle: car_id={unicycle_id}")
|
||||||
self._cars[unicycle_id] = None
|
self._cars[unicycle_id] = None
|
||||||
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
||||||
self._snowman = {"id": car_id, "packet": pkt}
|
self._unicycle = {"id": car_id, "packet": pkt}
|
||||||
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
|
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"Car spawn accepted: car_id={car_id}")
|
self.log.debug(f"Car spawn accepted: car_id={car_id}")
|
||||||
@@ -424,11 +467,14 @@ class Client:
|
|||||||
"packet": pkt,
|
"packet": pkt,
|
||||||
"json": car_json,
|
"json": car_json,
|
||||||
"json_ok": bool(car_json),
|
"json_ok": bool(car_json),
|
||||||
"snowman": snowman,
|
"unicycle": unicycle,
|
||||||
"over_spawn": (snowman and allow_snowman) or over_spawn,
|
"over_spawn": (unicycle and allow_unicycle) or over_spawn,
|
||||||
"pos": {}
|
"pos": {}
|
||||||
}
|
}
|
||||||
await self._send(pkt, to_all=True, to_self=True)
|
await self._send(pkt, to_all=True, to_self=True)
|
||||||
|
if self.focus_car == -1:
|
||||||
|
self._focus_car = car_id
|
||||||
|
await ev.call_as_events("onCarSpawned", data=car_json, car_id=car_id, player=self)
|
||||||
else:
|
else:
|
||||||
await self._send(pkt)
|
await self._send(pkt)
|
||||||
des = f"Od:{self.cid}-{car_id}"
|
des = f"Od:{self.cid}-{car_id}"
|
||||||
@@ -447,13 +493,9 @@ class Client:
|
|||||||
|
|
||||||
if car_id != -1 and self._cars[car_id]:
|
if car_id != -1 and self._cars[car_id]:
|
||||||
|
|
||||||
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
|
|
||||||
|
|
||||||
admin_allow = False # Delete from admin, for example...
|
admin_allow = False # Delete from admin, for example...
|
||||||
ev_data_list = ev.call_event("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
|
event_data = await ev.call_as_events("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
|
||||||
d2 = await ev.call_async_event("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
|
for ev_data in event_data:
|
||||||
ev_data_list.extend(d2)
|
|
||||||
for ev_data in ev_data_list:
|
|
||||||
self.log.debug(ev_data)
|
self.log.debug(ev_data)
|
||||||
# TODO: handle event onCarDelete
|
# TODO: handle event onCarDelete
|
||||||
pass
|
pass
|
||||||
@@ -461,22 +503,23 @@ class Client:
|
|||||||
if cid == self.cid or admin_allow:
|
if cid == self.cid or admin_allow:
|
||||||
await self._send(raw_data, to_all=True, to_self=True)
|
await self._send(raw_data, to_all=True, to_self=True)
|
||||||
car = self._cars[car_id]
|
car = self._cars[car_id]
|
||||||
if car['snowman']:
|
if car['unicycle']:
|
||||||
self.log.debug(f"Snowman found")
|
self.log.debug("unicycle found")
|
||||||
unicycle_id = self._snowman['id']
|
unicycle_id = self._unicycle['id']
|
||||||
self._snowman['id'] = -1
|
self._unicycle['id'] = -1
|
||||||
self._cars[unicycle_id] = None
|
self._cars[unicycle_id] = None
|
||||||
self._cars[car_id] = None
|
self._cars[car_id] = None
|
||||||
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
|
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
|
||||||
|
await ev.call_as_events("onCarDeleted", data=self._cars[car_id], car_id=car_id, player=self)
|
||||||
|
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
|
||||||
self.log.debug(f"Deleted car: car_id={car_id}")
|
self.log.debug(f"Deleted car: car_id={car_id}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||||
|
|
||||||
async def _edit_car(self, raw_data, data):
|
async def _edit_car(self, raw_data, data):
|
||||||
cid, car_id = self._get_cid_vid(raw_data)
|
cid, car_id = self._get_cid_vid(raw_data)
|
||||||
if car_id != -1 and self._cars[car_id]:
|
if car_id != -1 and self._cars[car_id]:
|
||||||
client = self.__Core.get_client(cid=cid)
|
client = self._core.get_client(cid=cid)
|
||||||
if client:
|
if client:
|
||||||
car = client._cars[car_id]
|
car = client._cars[car_id]
|
||||||
new_car_json = {}
|
new_car_json = {}
|
||||||
@@ -499,10 +542,10 @@ class Client:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if cid == self.cid or allow or admin_allow:
|
if cid == self.cid or allow or admin_allow:
|
||||||
if car['snowman']:
|
if car['unicycle']:
|
||||||
unicycle_id = self._snowman['id']
|
unicycle_id = self._unicycle['id']
|
||||||
self._snowman['id'] = -1
|
self._unicycle['id'] = -1
|
||||||
self.log.debug(f"Delete snowman")
|
self.log.debug(f"Delete unicycle")
|
||||||
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
||||||
self._cars[unicycle_id] = None
|
self._cars[unicycle_id] = None
|
||||||
else:
|
else:
|
||||||
@@ -515,6 +558,13 @@ class Client:
|
|||||||
else:
|
else:
|
||||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||||
|
|
||||||
|
async def reset_car(self, car_id, x, y, z, rot=None):
|
||||||
|
self.log.debug(f"Resetting car from plugin {x, y, z}; {rot=}")
|
||||||
|
jpkt = {"pos": {"y": float(y), "x": float(x), "z": float(z)}, "rot": {"y": 0, "w": 0, "x": 0, "z": 0}}
|
||||||
|
if rot:
|
||||||
|
jpkt['rot'] = rot
|
||||||
|
await self._send(f"Or:{self.cid}-{car_id}:{json.dumps(jpkt)}", True)
|
||||||
|
|
||||||
async def _reset_car(self, raw_data):
|
async def _reset_car(self, raw_data):
|
||||||
cid, car_id = self._get_cid_vid(raw_data)
|
cid, car_id = self._get_cid_vid(raw_data)
|
||||||
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
||||||
@@ -534,6 +584,7 @@ class Client:
|
|||||||
async def _handle_car_codes(self, raw_data):
|
async def _handle_car_codes(self, raw_data):
|
||||||
if len(raw_data) < 6:
|
if len(raw_data) < 6:
|
||||||
return
|
return
|
||||||
|
self.log.debug(f"[car] {raw_data}")
|
||||||
sub_code = raw_data[1]
|
sub_code = raw_data[1]
|
||||||
data = raw_data[3:]
|
data = raw_data[3:]
|
||||||
match sub_code:
|
match sub_code:
|
||||||
@@ -581,7 +632,7 @@ class Client:
|
|||||||
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
|
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
|
||||||
await self._send(f"J{i18n.game_welcome_message.format(self.nick)}", to_all=True) # Hello message
|
await self._send(f"J{i18n.game_welcome_message.format(self.nick)}", to_all=True) # Hello message
|
||||||
|
|
||||||
for client in self.__Core.clients:
|
for client in self._core.clients:
|
||||||
if not client:
|
if not client:
|
||||||
continue
|
continue
|
||||||
for car in client._cars:
|
for car in client._cars:
|
||||||
@@ -591,6 +642,9 @@ class Client:
|
|||||||
|
|
||||||
self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2)))
|
self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2)))
|
||||||
self._ready = True
|
self._ready = True
|
||||||
|
self._synced = True
|
||||||
|
ev.call_event("onPlayerReady", player=self)
|
||||||
|
await ev.call_async_event("onPlayerReady", player=self)
|
||||||
|
|
||||||
async def _chat_handler(self, data):
|
async def _chat_handler(self, data):
|
||||||
sup = data.find(":", 2)
|
sup = data.find(":", 2)
|
||||||
@@ -600,49 +654,54 @@ class Client:
|
|||||||
if not msg:
|
if not msg:
|
||||||
self.log.debug("Tried to send an empty event, ignoring")
|
self.log.debug("Tried to send an empty event, ignoring")
|
||||||
return
|
return
|
||||||
to_ev = {"message": msg, "player": self}
|
|
||||||
lua_data = ev.call_lua_event("onChatMessage", self.cid, self.nick, msg)
|
lua_data = ev.call_lua_event("onChatMessage", self.cid, self.nick, msg)
|
||||||
if 1 in lua_data:
|
if 1 in lua_data:
|
||||||
if config.Options['log_chat']:
|
if config.Options['log_chat']:
|
||||||
self.log.info(f"{self.nick}: {msg}")
|
self.log.info(f"{self.nick}: {msg}")
|
||||||
return
|
return
|
||||||
ev_data_list = ev.call_event("onChatReceive", **to_ev)
|
event_data = await ev.call_as_events("onChatReceive", message=msg, player=self)
|
||||||
d2 = await ev.call_async_event("onChatReceive", **to_ev)
|
|
||||||
ev_data_list.extend(d2)
|
|
||||||
need_send = True
|
need_send = True
|
||||||
for ev_data in ev_data_list:
|
for event in event_data:
|
||||||
|
if event is None:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
message = ev_data["message"]
|
|
||||||
to_all = ev_data.get("to_all")
|
|
||||||
if to_all is None:
|
|
||||||
to_all = True
|
|
||||||
to_self = ev_data.get("to_self")
|
|
||||||
if to_self is None:
|
|
||||||
to_self = True
|
|
||||||
to_client = ev_data.get("to_client")
|
|
||||||
writer = None
|
writer = None
|
||||||
|
to_all = True
|
||||||
|
to_self = True
|
||||||
|
message = f"{self.nick}: {msg}"
|
||||||
|
match event:
|
||||||
|
case False | 0:
|
||||||
|
need_send = False
|
||||||
|
continue
|
||||||
|
case {"message": message, **setting}:
|
||||||
|
message = message
|
||||||
|
to_all = setting.get("to_all", True)
|
||||||
|
to_self = setting.get("to_self", True)
|
||||||
|
to_client = setting.get("to")
|
||||||
if to_client:
|
if to_client:
|
||||||
# noinspection PyProtectedMember
|
|
||||||
writer = to_client._writer
|
writer = to_client._writer
|
||||||
|
case _:
|
||||||
|
self.log.error(f"[onChatReceive] Bad data returned from event: {event}")
|
||||||
|
|
||||||
if config.Options['log_chat']:
|
if config.Options['log_chat']:
|
||||||
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}")
|
self.log.info(f"[local] {message}" if not to_all else message)
|
||||||
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
|
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
|
||||||
need_send = False
|
need_send = False
|
||||||
except KeyError | AttributeError:
|
except KeyError:
|
||||||
self.log.error(i18n.client_event_invalid_data.format(ev_data))
|
self.log.error(i18n.client_event_invalid_data.format(event))
|
||||||
|
except AttributeError:
|
||||||
|
self.log.error(i18n.client_event_invalid_data.format(event))
|
||||||
if need_send:
|
if need_send:
|
||||||
if config.Options['log_chat']:
|
if config.Options['log_chat']:
|
||||||
self.log.info(f"{self.nick}: {msg}")
|
self.log.info(f"{self.nick}: {msg}")
|
||||||
await self._send(data, to_all=True)
|
await self._send(data, to_all=True)
|
||||||
|
|
||||||
async def _handle_codes(self, data):
|
async def _handle_codes_tcp(self, data):
|
||||||
if not data:
|
if data is None:
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
return
|
return
|
||||||
|
if len(data) == 0:
|
||||||
# Codes: V W X Y
|
await self.kick("Bad data from client")
|
||||||
if 89 >= data[0] >= 86:
|
|
||||||
await self._send(data, to_all=True, to_self=False)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
_bytes = False
|
_bytes = False
|
||||||
@@ -650,24 +709,24 @@ class Client:
|
|||||||
data = data.decode()
|
data = data.decode()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
_bytes = True
|
_bytes = True
|
||||||
self.log.error(f"UnicodeDecodeError: {data}")
|
self.log.error("UnicodeDecodeError")
|
||||||
self.log.info("Some things are skipping...")
|
|
||||||
|
|
||||||
# Codes: p, Z in udp_server.py
|
if data[0] in ['V', 'W', 'Y', 'E', 'N']:
|
||||||
|
await self._send(data, to_all=True, to_self=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Codes: p, Z, X in udp_server.py
|
||||||
match data[0]: # At data[0] code
|
match data[0]: # At data[0] code
|
||||||
case "H": # Map load, client ready
|
case "H": # Map load, client ready
|
||||||
await self._connected_handler()
|
await self._connected_handler()
|
||||||
|
|
||||||
case "C": # Chat handler
|
case "C": # Chat handler
|
||||||
if _bytes:
|
if _bytes:
|
||||||
return
|
return
|
||||||
await self._chat_handler(data)
|
await self._chat_handler(data)
|
||||||
|
|
||||||
case "O": # Cars handler
|
case "O": # Cars handler
|
||||||
if _bytes:
|
if _bytes:
|
||||||
return
|
return
|
||||||
await self._handle_car_codes(data)
|
await self._handle_car_codes(data)
|
||||||
|
|
||||||
case "E": # Client events handler
|
case "E": # Client events handler
|
||||||
if len(data) < 2:
|
if len(data) < 2:
|
||||||
self.log.debug("Tried to send an empty event, ignoring.")
|
self.log.debug("Tried to send an empty event, ignoring.")
|
||||||
@@ -681,11 +740,79 @@ class Client:
|
|||||||
self.log.error(f"Received event in invalid format (missing ':'), got: {data}")
|
self.log.error(f"Received event in invalid format (missing ':'), got: {data}")
|
||||||
event_name = data[2:sep]
|
event_name = data[2:sep]
|
||||||
even_data = data[sep + 1:]
|
even_data = data[sep + 1:]
|
||||||
ev.call_lua_event(event_name, even_data)
|
ev.call_lua_event(event_name, self.cid, even_data)
|
||||||
ev.call_event(event_name, data=even_data, player=self)
|
ev.call_event(event_name, data=even_data, player=self)
|
||||||
await ev.call_async_event(event_name, data=even_data, player=self)
|
await ev.call_async_event(event_name, data=even_data, player=self)
|
||||||
case "N":
|
case _:
|
||||||
await self._send(data, to_all=True, to_self=False)
|
self.log.warning(f"TCP Unknown code: {data[0]}; {data}")
|
||||||
|
|
||||||
|
async def _handle_codes_udp(self, data):
|
||||||
|
code = data[2:3].decode()
|
||||||
|
data = data[2:].decode()
|
||||||
|
match code:
|
||||||
|
case "p": # Ping packet
|
||||||
|
ev.call_event("onSentPing", player=self)
|
||||||
|
await self._send(b"p", to_udp=True)
|
||||||
|
case "Z": # Position packet
|
||||||
|
sub = data.find("{", 1)
|
||||||
|
last_pos = data[sub:]
|
||||||
|
try:
|
||||||
|
_, car_id = self._get_cid_vid(data)
|
||||||
|
if self._cars[car_id]:
|
||||||
|
last_pos = json.loads(last_pos)
|
||||||
|
self._last_position = last_pos
|
||||||
|
self._cars[car_id]['pos'] = last_pos
|
||||||
|
ev.call_event("onChangePosition", data, player=self, pos=last_pos)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning(f"Cannot parse position packet: {e}")
|
||||||
|
self.log.debug(f"data: '{data}', sub: {sub}")
|
||||||
|
self.log.debug(f"last_pos ({type(last_pos)}): {last_pos}")
|
||||||
|
await self._send(data, True, False, True)
|
||||||
|
case "X":
|
||||||
|
await self._send(data, True, False, True)
|
||||||
|
case _:
|
||||||
|
self.log.warning(f"UDP Unknown code: {code}; {data}")
|
||||||
|
|
||||||
|
def _tick_pps(self, _):
|
||||||
|
self.tcp_pps = self._tpc_count_recv
|
||||||
|
self.udp_pps = self._udp_count_recv
|
||||||
|
self._tpc_count_recv = 0
|
||||||
|
self._udp_count_recv = 0
|
||||||
|
if self.tcp_pps > self._core.target_tps or self.udp_pps > self._core.target_tps:
|
||||||
|
self.log.warning(f"PPS > TPS; PPS: TPC: {self.tcp_pps}, UDP: {self.udp_pps}")
|
||||||
|
|
||||||
|
async def __tick_player_tcp(self, _):
|
||||||
|
try:
|
||||||
|
if self.__queue_tpc.qsize() > 0:
|
||||||
|
packet = await self.__queue_tpc.get()
|
||||||
|
if packet is None:
|
||||||
|
return await self._remove_me()
|
||||||
|
await self._handle_codes_tcp(packet)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(f'[TPC] Error while ticking player:')
|
||||||
|
self.log.exception(e)
|
||||||
|
|
||||||
|
async def __tick_player_udp(self, _):
|
||||||
|
try:
|
||||||
|
if self.__queue_udp.qsize() > 0:
|
||||||
|
packet = await self.__queue_udp.get()
|
||||||
|
await self._handle_codes_udp(packet)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(f'[UDP] Error while ticking player:')
|
||||||
|
self.log.exception(e)
|
||||||
|
|
||||||
|
async def _tpc_put(self, packet):
|
||||||
|
if packet:
|
||||||
|
self._tpc_count_recv += 1
|
||||||
|
self._tpc_count_total_recv += 1
|
||||||
|
self._tpc_size_total_recv += len(packet)
|
||||||
|
await self.__queue_tpc.put(packet)
|
||||||
|
|
||||||
|
async def _udp_put(self, packet):
|
||||||
|
self._udp_count_recv += 1
|
||||||
|
self._udp_count_total_recv += 1
|
||||||
|
self._udp_size_total_recv += len(packet)
|
||||||
|
await self.__queue_udp.put(packet)
|
||||||
|
|
||||||
async def _looper(self):
|
async def _looper(self):
|
||||||
ev.call_lua_event("onPlayerConnecting", self.cid)
|
ev.call_lua_event("onPlayerConnecting", self.cid)
|
||||||
@@ -693,55 +820,52 @@ class Client:
|
|||||||
await self._send(f"P{self.cid}") # Send clientID
|
await self._send(f"P{self.cid}") # Send clientID
|
||||||
await self._sync_resources()
|
await self._sync_resources()
|
||||||
ev.call_lua_event("onPlayerJoining", self.cid)
|
ev.call_lua_event("onPlayerJoining", self.cid)
|
||||||
tasks = self.__tasks
|
self.__tpt_id = ev.register("serverTick", self.__tick_player_tcp)
|
||||||
recv = asyncio.create_task(self._recv())
|
self.__tpu_id = ev.register("serverTick", self.__tick_player_udp)
|
||||||
tasks.append(recv)
|
self.__tpp_id = ev.register("serverTick_1s", self._tick_pps)
|
||||||
while self.__alive:
|
await self._recv()
|
||||||
if len(self.__packets_queue) > 0:
|
|
||||||
for index, packet in enumerate(self.__packets_queue):
|
|
||||||
# self.log.debug(f"Packet: {packet}")
|
|
||||||
del self.__packets_queue[index]
|
|
||||||
task = self._loop.create_task(self._handle_codes(packet))
|
|
||||||
tasks.append(task)
|
|
||||||
else:
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def _remove_me(self):
|
async def _remove_me(self):
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
if (self.cid > 0 or self.nick is not None) and \
|
if self._core.clients_by_nick.get(self.nick):
|
||||||
self.__Core.clients_by_nick.get(self.nick):
|
|
||||||
for i, car in enumerate(self._cars):
|
for i, car in enumerate(self._cars):
|
||||||
if not car:
|
if not car:
|
||||||
continue
|
continue
|
||||||
self.log.debug(f"Removing car: car_id={i}")
|
self.log.debug(f"Removing car: car_id={i}")
|
||||||
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
|
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
|
||||||
if self.ready:
|
if self.ready:
|
||||||
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected.
|
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False)
|
||||||
self.log.debug(f"Removing client")
|
self.log.debug(f"Removing client")
|
||||||
ev.call_lua_event("onPlayerDisconnect", self.cid)
|
ev.call_lua_event("onPlayerDisconnect", self.cid)
|
||||||
ev.call_event("onPlayerDisconnect", player=self)
|
ev.call_event("onPlayerDisconnect", player=self)
|
||||||
await ev.call_async_event("onPlayerDisconnect", player=self)
|
await ev.call_async_event("onPlayerDisconnect", player=self)
|
||||||
|
if self.__tpt_id:
|
||||||
self.log.info(
|
ev.unregister_by_id(self.__tpt_id) # self.__tick_player_tcp
|
||||||
i18n.client_player_disconnected.format(
|
ev.unregister_by_id(self.__tpu_id) # self.__tick_player_udp
|
||||||
round((time.monotonic() - self._connect_time) / 60, 2)
|
ev.unregister_by_id(self.__tpp_id) # self._tick_pps
|
||||||
)
|
gt = round((time.monotonic() - self._connect_time) / 60, 2)
|
||||||
)
|
self.log.info(i18n.client_player_disconnected.format(gt))
|
||||||
self.__Core.clients[self.cid] = None
|
self._core.clients[self.cid] = None
|
||||||
del self.__Core.clients_by_id[self.cid]
|
del self._core.clients_by_id[self.cid]
|
||||||
del self.__Core.clients_by_nick[self.nick]
|
del self._core.clients_by_nick[self.nick]
|
||||||
|
self.log.debug(f"TPC: "
|
||||||
|
f"Recv: {self._tpc_count_total_recv}; {self._tpc_size_total_recv / KB:.4f}kb; "
|
||||||
|
f"Sent: {self._tpc_count_total_sent}; {self._tpc_size_total_sent / KB:.4f}kb;")
|
||||||
|
self.log.debug(f"UDP: "
|
||||||
|
f"Recv: {self._udp_count_total_recv}; {self._udp_size_total_recv / KB:.4f}kb; "
|
||||||
|
f"Sent: {self._udp_count_total_sent}; {self._udp_size_total_sent / KB:.4f}kb;")
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"Removing client; Closing connection...")
|
self.log.debug(f"Removing client; Closing connection...")
|
||||||
|
await asyncio.sleep(0.001)
|
||||||
try:
|
try:
|
||||||
if not self.__writer.is_closing():
|
|
||||||
self.__writer.close()
|
self.__writer.close()
|
||||||
|
await self.__writer.wait_closed()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"Error while closing writer: {e}")
|
self.log.debug(f"Error while closing writer: {e}")
|
||||||
try:
|
try:
|
||||||
_, down_w = self._down_sock
|
_, down_w = self._down_sock
|
||||||
if down_w and not down_w.is_closing():
|
|
||||||
down_w.close()
|
down_w.close()
|
||||||
|
await down_w.wait_closed()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"Error while closing download writer: {e}")
|
self.log.debug(f"Error while closing download writer: {e}")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.tcp_server.py
|
# File core.tcp_server.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.5
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import StreamReader, StreamWriter, DatagramTransport
|
from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock, Queue
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from typing import Tuple, List, Dict, Optional, Union, Any
|
from typing import Tuple, List, Dict, Optional, Union, Any
|
||||||
|
|
||||||
@@ -19,25 +19,46 @@ class Client:
|
|||||||
self.__tasks = []
|
self.__tasks = []
|
||||||
self.__reader = reader
|
self.__reader = reader
|
||||||
self.__writer = writer
|
self.__writer = writer
|
||||||
self.__packets_queue = []
|
self.__queue_tpc = Queue()
|
||||||
|
self.__queue_udp = Queue()
|
||||||
|
self._tpc_count_recv = 0
|
||||||
|
self._udp_count_recv = 0
|
||||||
|
self._tpc_count_total_recv = 0
|
||||||
|
self._udp_count_total_recv = 0
|
||||||
|
self._udp_size_total_recv = 0.1
|
||||||
|
self._tpc_size_total_recv = 0.1
|
||||||
|
# self._tpc_count_sent = 0
|
||||||
|
# self._udp_count_sent = 0
|
||||||
|
self._tpc_count_total_sent = 0
|
||||||
|
self._udp_count_total_sent = 0
|
||||||
|
self._udp_size_total_sent = 0.1
|
||||||
|
self._tpc_size_total_sent = 0.1
|
||||||
|
self.tcp_pps = 0
|
||||||
|
self.udp_pps = 0
|
||||||
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
|
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
|
||||||
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
|
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
|
||||||
self._log = utils.get_logger("client(id: )")
|
self._log = utils.get_logger("client(id: )")
|
||||||
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
|
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
|
||||||
self._loop = asyncio.get_event_loop()
|
self._loop = asyncio.get_event_loop()
|
||||||
self.__Core: Core = core
|
self._core: Core = core
|
||||||
self._cid: int = -1
|
self._cid: int = -1
|
||||||
self._key: str = None
|
self._key: str = None
|
||||||
self.nick: str = None
|
self.nick: str = None
|
||||||
self.roles: str = None
|
self.roles: str = None
|
||||||
self._guest = True
|
self._guest = True
|
||||||
self.__alive = True
|
self.__alive = True
|
||||||
|
self._synced = False
|
||||||
self._ready = False
|
self._ready = False
|
||||||
self._focus_car = -1
|
self._focus_car = -1
|
||||||
self._identifiers = []
|
self._identifiers = []
|
||||||
self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = []
|
self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = []
|
||||||
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
|
self._unicycle: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
|
||||||
self._last_position = {}
|
self._last_position = {}
|
||||||
|
self._lock = Lock()
|
||||||
|
self.__tpt_id = 0
|
||||||
|
self.__tpu_id = 0
|
||||||
|
self.__tpp_id = 0
|
||||||
|
|
||||||
async def __gracefully_kick(self): ...
|
async def __gracefully_kick(self): ...
|
||||||
@property
|
@property
|
||||||
def _writer(self) -> StreamWriter: ...
|
def _writer(self) -> StreamWriter: ...
|
||||||
@@ -55,6 +76,8 @@ class Client:
|
|||||||
@property
|
@property
|
||||||
def guest(self) -> bool: ...
|
def guest(self) -> bool: ...
|
||||||
@property
|
@property
|
||||||
|
def synced(self) -> bool: ...
|
||||||
|
@property
|
||||||
def ready(self) -> bool: ...
|
def ready(self) -> bool: ...
|
||||||
@property
|
@property
|
||||||
def identifiers(self) -> list: ...
|
def identifiers(self) -> list: ...
|
||||||
@@ -68,11 +91,11 @@ class Client:
|
|||||||
def is_disconnected(self) -> bool: ...
|
def is_disconnected(self) -> bool: ...
|
||||||
async def kick(self, reason: str) -> None: ...
|
async def kick(self, reason: str) -> None: ...
|
||||||
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
|
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
|
||||||
async def send_event(self, event_name: str, event_data: Any, to_all: bool = True) -> None: ...
|
async def send_event(self, event_name: str, event_data: Any, to_all: bool = False) -> None: ...
|
||||||
async def _send(self, data: bytes | str, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ...
|
async def _send(self, data: bytes | str, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ...
|
||||||
async def _sync_resources(self) -> None: ...
|
async def _sync_resources(self) -> None: ...
|
||||||
async def _recv(self, one=False) -> bytes | None: ...
|
async def _recv(self, one=False) -> bytes | None: ...
|
||||||
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ...
|
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> int: ...
|
||||||
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
|
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
|
||||||
async def _spawn_car(self, data: str) -> None: ...
|
async def _spawn_car(self, data: str) -> None: ...
|
||||||
async def delete_car(self, car_id: int) -> None: ...
|
async def delete_car(self, car_id: int) -> None: ...
|
||||||
@@ -82,7 +105,13 @@ class Client:
|
|||||||
async def _handle_car_codes(self, data: str) -> None: ...
|
async def _handle_car_codes(self, data: str) -> None: ...
|
||||||
async def _connected_handler(self) -> None: ...
|
async def _connected_handler(self) -> None: ...
|
||||||
async def _chat_handler(self, data: str) -> None: ...
|
async def _chat_handler(self, data: str) -> None: ...
|
||||||
async def _handle_codes(self, data: bytes) -> None: ...
|
async def _handle_codes_tcp(self, data: bytes) -> None: ...
|
||||||
|
async def _handle_codes_udp(self, data: bytes) -> None: ...
|
||||||
|
def _tick_pps(self, _): ...
|
||||||
|
async def __tick_player_tcp(self, _): ...
|
||||||
|
async def __tick_player_udp(self, _): ...
|
||||||
|
async def _tpc_put(self, data): ...
|
||||||
|
async def _udp_put(self, data): ...
|
||||||
async def _looper(self) -> None: ...
|
async def _looper(self) -> None: ...
|
||||||
def _update_logger(self) -> None: ...
|
def _update_logger(self) -> None: ...
|
||||||
async def _remove_me(self) -> None: ...
|
async def _remove_me(self) -> None: ...
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.__init__.py
|
# File core.__init__.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Version 1.4
|
# Version 1.5
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.8
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2024
|
||||||
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
|
|
||||||
|
|
||||||
__title__ = 'KuiToi-Server'
|
__title__ = 'KuiToi-Server'
|
||||||
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
|
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
|
||||||
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
|
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
|
||||||
__version__ = '0.4.3'
|
__version__ = '0.4.8'
|
||||||
__build__ = 2125 # Я это считаю лог файлами
|
__build__ = 2800 # Я это считаю лог файлами
|
||||||
__author__ = 'SantaSpeen'
|
__author__ = 'SantaSpeen'
|
||||||
__author_email__ = 'admin@kuitoi.su'
|
__author_email__ = 'admin@anidev.ru'
|
||||||
__license__ = "FPA"
|
__license__ = "FPA"
|
||||||
__copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
|
__copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)'
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import builtins
|
import builtins
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
import prompt_toolkit.shortcuts as shortcuts
|
import prompt_toolkit.shortcuts as shortcuts
|
||||||
@@ -30,12 +31,16 @@ from modules import ConfigProvider, EventsSystem
|
|||||||
from modules import Console
|
from modules import Console
|
||||||
from modules import MultiLanguage
|
from modules import MultiLanguage
|
||||||
|
|
||||||
args = parser.parse_args()
|
builtins.Ts = time.monotonic()
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
if args.version:
|
if args.version:
|
||||||
print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}")
|
print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}")
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
# loop.set_task_factory(asyncio.eager_task_factory)
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
log = get_logger("core.init")
|
log = get_logger("core.init")
|
||||||
|
|
||||||
@@ -44,15 +49,14 @@ config_path = "kuitoi.yml"
|
|||||||
if args.config:
|
if args.config:
|
||||||
config_path = args.config
|
config_path = args.config
|
||||||
config_provider = ConfigProvider(config_path)
|
config_provider = ConfigProvider(config_path)
|
||||||
config = config_provider.open_config()
|
config = config_provider.read()
|
||||||
builtins.config = config
|
builtins.config = config
|
||||||
config.enc = config.Options['encoding']
|
config.enc = config.Options['encoding']
|
||||||
if config.Options['debug'] is True:
|
if config.Options['debug'] is True:
|
||||||
utils.set_debug_status()
|
utils.set_debug_status()
|
||||||
log.info("Debug enabled!")
|
|
||||||
log = get_logger("core.init")
|
log = get_logger("core.init")
|
||||||
log.debug("Debug mode enabled!")
|
log.info("Debug mode enabled!")
|
||||||
log.debug(f"Server config: {config}")
|
log.debug(f"Server config: {config}")
|
||||||
# i18n init
|
# i18n init
|
||||||
log.debug("Initializing i18n...")
|
log.debug("Initializing i18n...")
|
||||||
ml = MultiLanguage()
|
ml = MultiLanguage()
|
||||||
@@ -62,6 +66,7 @@ ml.builtins_hook()
|
|||||||
log.debug("Initializing EventsSystem...")
|
log.debug("Initializing EventsSystem...")
|
||||||
ev = EventsSystem()
|
ev = EventsSystem()
|
||||||
ev.builtins_hook()
|
ev.builtins_hook()
|
||||||
|
ev.register("get_version", lambda _: {"version": __version__, "build": __build__})
|
||||||
|
|
||||||
log.info(i18n.hello)
|
log.info(i18n.hello)
|
||||||
log.info(i18n.config_path.format(config_path))
|
log.info(i18n.config_path.format(config_path))
|
||||||
@@ -70,7 +75,7 @@ log.debug("Initializing BeamMP Server system...")
|
|||||||
# Key handler..
|
# Key handler..
|
||||||
if not config.Auth['private'] and not config.Auth['key']:
|
if not config.Auth['private'] and not config.Auth['key']:
|
||||||
log.warn(i18n.auth_need_key)
|
log.warn(i18n.auth_need_key)
|
||||||
url = "https://beammp.com/k/keys"
|
url = "https://keymaster.beammp.com/login"
|
||||||
if shortcuts.yes_no_dialog(
|
if shortcuts.yes_no_dialog(
|
||||||
title='BeamMP Server Key',
|
title='BeamMP Server Key',
|
||||||
text=i18n.GUI_need_key_message,
|
text=i18n.GUI_need_key_message,
|
||||||
@@ -92,7 +97,7 @@ if not config.Auth['private'] and not config.Auth['key']:
|
|||||||
text=i18n.GUI_enter_key_message,
|
text=i18n.GUI_enter_key_message,
|
||||||
ok_text=i18n.GUI_ok,
|
ok_text=i18n.GUI_ok,
|
||||||
cancel_text=i18n.GUI_cancel).run()
|
cancel_text=i18n.GUI_cancel).run()
|
||||||
config_provider.save_config()
|
config_provider.save()
|
||||||
if not config.Auth['private'] and not config.Auth['key']:
|
if not config.Auth['private'] and not config.Auth['key']:
|
||||||
log.error(i18n.auth_empty_key)
|
log.error(i18n.auth_empty_key)
|
||||||
log.info(i18n.stop)
|
log.info(i18n.stop)
|
||||||
|
|||||||
319
src/core/core.py
319
src/core/core.py
@@ -1,31 +1,41 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.core.py
|
# File core.core.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Version: 0.4.3
|
# Version: 0.4.8
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import asyncio
|
import asyncio
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import statistics
|
||||||
import time
|
import time
|
||||||
from threading import Thread
|
from collections import deque
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
from core import utils, __version__
|
from core import utils, __version__
|
||||||
from core.Client import Client
|
from core.Client import Client
|
||||||
from core.tcp_server import TCPServer
|
from core.tcp_server import TCPServer
|
||||||
from core.udp_server import UDPServer
|
from core.udp_server import UDPServer
|
||||||
from modules import PluginsLoader
|
from modules import PluginsLoader, PermsSystem
|
||||||
from modules.WebAPISystem import app as webapp
|
|
||||||
|
|
||||||
|
def calc_ticks(ticks, duration):
|
||||||
|
while ticks and ticks[0] < time.monotonic() - duration:
|
||||||
|
ticks.popleft()
|
||||||
|
return len(ticks) / duration
|
||||||
|
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
class Core:
|
class Core:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.tick_counter = 0
|
||||||
self.log = utils.get_logger("core")
|
self.log = utils.get_logger("core")
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
self.start_time = time.monotonic()
|
self.start_time = time.monotonic()
|
||||||
@@ -40,24 +50,29 @@ class Core:
|
|||||||
self.server_port = config.Server["server_port"]
|
self.server_port = config.Server["server_port"]
|
||||||
self.tcp = TCPServer
|
self.tcp = TCPServer
|
||||||
self.udp = UDPServer
|
self.udp = UDPServer
|
||||||
self.web_thread = None
|
|
||||||
self.web_pool = webapp.data_pool
|
self.tcp_pps = 0
|
||||||
self.web_stop = None
|
self.udp_pps = 0
|
||||||
|
|
||||||
|
self.tps = 60
|
||||||
|
self.target_tps = 60
|
||||||
|
|
||||||
self.lock_upload = False
|
self.lock_upload = False
|
||||||
|
|
||||||
self.client_major_version = "2.0"
|
self.client_major_version = "2.0"
|
||||||
self.BeamMP_version = "3.1.1" # 20.07.2023
|
self.BeamMP_version = "3.4.1" # 16.07.2024
|
||||||
|
|
||||||
ev.register_event("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
|
ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
|
||||||
ev.register_event("_get_player", lambda x: self.get_client(**x['kwargs']))
|
ev.register("_get_player", lambda x: self.get_client(**x['kwargs']))
|
||||||
|
|
||||||
def get_client(self, cid=None, nick=None):
|
def get_client(self, cid=None, nick=None, raw=False):
|
||||||
if cid is None and nick is None:
|
if raw:
|
||||||
|
return self.clients_by_nick
|
||||||
|
if (cid, nick) == (None, None):
|
||||||
return None
|
return None
|
||||||
if cid is not None:
|
if cid is not None:
|
||||||
if cid == -1:
|
if cid == -1:
|
||||||
return [i for i in self.clients if i is not None]
|
return [i for i in self.clients if i is not None and i.synced]
|
||||||
return self.clients_by_id.get(cid)
|
return self.clients_by_id.get(cid)
|
||||||
if nick:
|
if nick:
|
||||||
return self.clients_by_nick.get(nick)
|
return self.clients_by_nick.get(nick)
|
||||||
@@ -102,12 +117,9 @@ class Core:
|
|||||||
out = out[:-1]
|
out = out[:-1]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
async def check_alive(self):
|
async def _check_alive(self, _):
|
||||||
maxp = config.Game['players']
|
# self.log.debug("alive checker.")
|
||||||
try:
|
try:
|
||||||
while self.run:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
|
|
||||||
for client in self.clients:
|
for client in self.clients:
|
||||||
if not client:
|
if not client:
|
||||||
continue
|
continue
|
||||||
@@ -116,9 +128,19 @@ class Core:
|
|||||||
continue
|
continue
|
||||||
if not client.alive:
|
if not client.alive:
|
||||||
await client.kick("You are not alive!")
|
await client.kick("You are not alive!")
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Error in _check_alive.")
|
||||||
|
self.log.exception(e)
|
||||||
|
|
||||||
|
async def _send_online(self, _):
|
||||||
|
try:
|
||||||
|
for client in self.clients:
|
||||||
|
ca = f"Ss{len(self.clients_by_id)}/{config.Game['players']}:{self.get_clients_list()}"
|
||||||
|
if not client or not client.alive or not client.ready:
|
||||||
|
continue
|
||||||
await client._send(ca)
|
await client._send(ca)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error("Error in check_alive.")
|
self.log.error("Error in _send_online.")
|
||||||
self.log.exception(e)
|
self.log.exception(e)
|
||||||
|
|
||||||
async def __gracefully_kick(self):
|
async def __gracefully_kick(self):
|
||||||
@@ -127,24 +149,16 @@ class Core:
|
|||||||
continue
|
continue
|
||||||
await client.kick("Server shutdown!")
|
await client.kick("Server shutdown!")
|
||||||
|
|
||||||
@staticmethod
|
async def __gracefully_remove(self):
|
||||||
def start_web():
|
for client in self.clients:
|
||||||
uvconfig = uvicorn.Config("modules.WebAPISystem.app:web_app",
|
if not client:
|
||||||
host=config.WebAPI["server_ip"],
|
continue
|
||||||
port=config.WebAPI["server_port"],
|
await client._remove_me()
|
||||||
loop="asyncio")
|
|
||||||
uvserver = uvicorn.Server(uvconfig)
|
|
||||||
webapp.uvserver = uvserver
|
|
||||||
uvserver.run()
|
|
||||||
|
|
||||||
async def stop_me(self):
|
|
||||||
while webapp.data_run[0]:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
self.run = False
|
|
||||||
raise KeyboardInterrupt
|
|
||||||
|
|
||||||
# noinspection SpellCheckingInspection,PyPep8Naming
|
# noinspection SpellCheckingInspection,PyPep8Naming
|
||||||
async def heartbeat(self, test=False):
|
async def heartbeat(self, test=False):
|
||||||
|
try:
|
||||||
|
self.log.debug("Starting heartbeat.")
|
||||||
if config.Auth["private"] or self.direct:
|
if config.Auth["private"] or self.direct:
|
||||||
if test:
|
if test:
|
||||||
self.log.info(i18n.core_direct_mode)
|
self.log.info(i18n.core_direct_mode)
|
||||||
@@ -152,24 +166,40 @@ class Core:
|
|||||||
return
|
return
|
||||||
|
|
||||||
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
|
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
|
||||||
modlist = ""
|
_map = config.Game['map'] if "/" in config.Game['map'] else f"/levels/{config.Game['map']}/info.json"
|
||||||
for mod in self.mods_list:
|
tags = config.Server['tags'].replace(", ", ";").replace(",", ";")
|
||||||
if type(mod) == int:
|
self.log.debug(f"[heartbeat] {_map=}")
|
||||||
continue
|
self.log.debug(f"[heartbeat] {tags=}")
|
||||||
modlist += f"/{os.path.basename(mod['path'])};"
|
if tags and tags[-1:] != ";":
|
||||||
|
tags += ";"
|
||||||
|
modlist = "".join(f"/{os.path.basename(mod['path'])};" for mod in self.mods_list[1:])
|
||||||
modstotalsize = self.mods_list[0]
|
modstotalsize = self.mods_list[0]
|
||||||
modstotal = len(self.mods_list) - 1
|
modstotal = len(self.mods_list) - 1
|
||||||
|
self.log.debug(f"[heartbeat] {modlist=}")
|
||||||
|
self.log.debug(f"[heartbeat] {modstotalsize=}")
|
||||||
|
self.log.debug(f"[heartbeat] {modstotal=}")
|
||||||
while self.run:
|
while self.run:
|
||||||
try:
|
playerslist = "".join(f"{client.nick};" for client in self.clients if client and client.alive)
|
||||||
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id),
|
data = {
|
||||||
"maxplayers": config.Game["players"], "port": config.Server["server_port"],
|
"uuid": config.Auth["key"],
|
||||||
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'],
|
"players": len(self.clients_by_id),
|
||||||
"version": self.BeamMP_version, "clientversion": self.client_major_version,
|
"maxplayers": config.Game["players"],
|
||||||
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
|
"port": config.Server["server_port"],
|
||||||
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
|
"map": _map,
|
||||||
|
"private": config.Auth['private'],
|
||||||
|
"version": self.BeamMP_version,
|
||||||
|
"clientversion": self.client_major_version,
|
||||||
|
"name": config.Server["name"],
|
||||||
|
"tags": tags,
|
||||||
|
"guests": not config.Auth["private"],
|
||||||
|
"modlist": modlist,
|
||||||
|
"modstotalsize": modstotalsize,
|
||||||
|
"modstotal": modstotal,
|
||||||
|
"playerslist": playerslist,
|
||||||
|
"desc": config.Server['description'],
|
||||||
|
"pass": False
|
||||||
|
}
|
||||||
|
|
||||||
# Sentry?
|
|
||||||
ok = False
|
|
||||||
body = {}
|
body = {}
|
||||||
for server_url in BEAM_backend:
|
for server_url in BEAM_backend:
|
||||||
url = "https://" + server_url + "/heartbeat"
|
url = "https://" + server_url + "/heartbeat"
|
||||||
@@ -177,14 +207,15 @@ class Core:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(url, data=data, headers={"api-v": "2"}) as response:
|
async with session.post(url, data=data, headers={"api-v": "2"}) as response:
|
||||||
code = response.status
|
code = response.status
|
||||||
|
# text = await response.text()
|
||||||
|
# self.log.debug(f"[HB] res={text}")
|
||||||
body = await response.json()
|
body = await response.json()
|
||||||
ok = True
|
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
|
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ok:
|
if body:
|
||||||
if not (body.get("status") is not None and
|
if not (body.get("status") is not None and
|
||||||
body.get("code") is not None and
|
body.get("code") is not None and
|
||||||
body.get("msg") is not None):
|
body.get("msg") is not None):
|
||||||
@@ -216,15 +247,15 @@ class Core:
|
|||||||
# raise KeyboardInterrupt
|
# raise KeyboardInterrupt
|
||||||
|
|
||||||
if test:
|
if test:
|
||||||
return ok
|
return bool(body)
|
||||||
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(15)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Error in heartbeat: {e}")
|
self.log.error(f"Error in heartbeat: {e}")
|
||||||
|
|
||||||
async def kick_cmd(self, args):
|
async def _cmd_kick(self, args):
|
||||||
if not len(args) > 0:
|
if not len(args) > 0:
|
||||||
return "\nUsage: kick <nick>|:<id> [reason]\nExamples:\n\tkick admin bad boy\n\tkick :0 bad boy"
|
return "Usage: kick <nick>|:<id> [reason]\nExamples:\n\tkick admin bad boy\n\tkick :0 bad boy"
|
||||||
reason = "kicked by console."
|
reason = "kicked by console."
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
reason = " ".join(args[1:])
|
reason = " ".join(args[1:])
|
||||||
@@ -238,38 +269,142 @@ class Core:
|
|||||||
else:
|
else:
|
||||||
return "Client not found."
|
return "Client not found."
|
||||||
|
|
||||||
|
async def _useful_ticks(self, _):
|
||||||
|
tasks = []
|
||||||
|
self.tick_counter += 1
|
||||||
|
events = {
|
||||||
|
0.5: "serverTick_0.5s",
|
||||||
|
1: "serverTick_1s",
|
||||||
|
2: "serverTick_2s",
|
||||||
|
3: "serverTick_3s",
|
||||||
|
4: "serverTick_4s",
|
||||||
|
5: "serverTick_5s",
|
||||||
|
10: "serverTick_10s",
|
||||||
|
30: "serverTick_30s",
|
||||||
|
60: "serverTick_60s"
|
||||||
|
}
|
||||||
|
for interval in sorted(events.keys(), reverse=True):
|
||||||
|
if self.tick_counter % (interval * self.target_tps) == 0:
|
||||||
|
ev.call_event(events[interval])
|
||||||
|
tasks.append(ev.call_async_event(events[interval]))
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
if self.tick_counter == (60 * self.target_tps):
|
||||||
|
self.tick_counter = 0
|
||||||
|
|
||||||
|
def _get_color_tps(self, ticks, d):
|
||||||
|
tps = calc_ticks(ticks, d)
|
||||||
|
half = self.target_tps // 2
|
||||||
|
qw = self.target_tps // 4
|
||||||
|
if tps > half + qw:
|
||||||
|
return f"<green><b>{tps:.2f}</b></green>"
|
||||||
|
elif tps > half:
|
||||||
|
return f"<yellow><b>{tps:.2f}</b></yellow>"
|
||||||
|
elif half > tps:
|
||||||
|
return f"<red><b>{tps:.2f}</b></red>"
|
||||||
|
|
||||||
|
def _cmd_tps(self, ticks_2s, ticks_5s, ticks_30s, ticks_60s):
|
||||||
|
t = ["-, ", "-, ", "-."]
|
||||||
|
if len(ticks_5s) > 5 * self.target_tps:
|
||||||
|
t[0] = f"{self._get_color_tps(ticks_5s, 5)}, "
|
||||||
|
if len(ticks_30s) > 30 * self.target_tps:
|
||||||
|
t[1] = f"{self._get_color_tps(ticks_30s, 30)}, "
|
||||||
|
if len(ticks_60s) > 60 * self.target_tps:
|
||||||
|
t[2] = f"{self._get_color_tps(ticks_60s, 60)}."
|
||||||
|
return f"html:{self._get_color_tps(ticks_2s, 2)} TPS; For last 5s, 30s, 60s: " + "".join(t)
|
||||||
|
|
||||||
|
async def _tick(self):
|
||||||
|
try:
|
||||||
|
ticks = 0
|
||||||
|
target_tps = self.target_tps
|
||||||
|
last_tick_time = time.monotonic()
|
||||||
|
ev.register("serverTick", self._useful_ticks)
|
||||||
|
ticks_2s = deque(maxlen=2 * int(target_tps) + 1)
|
||||||
|
ticks_5s = deque(maxlen=5 * int(target_tps) + 1)
|
||||||
|
ticks_30s = deque(maxlen=30 * int(target_tps) + 1)
|
||||||
|
ticks_60s = deque(maxlen=60 * int(target_tps) + 1)
|
||||||
|
console.add_command("tps", lambda _: self._cmd_tps(ticks_2s, ticks_5s, ticks_30s, ticks_60s),
|
||||||
|
None, "Print TPS", {"tps": None})
|
||||||
|
_add_to_sleep = deque([0.0, 0.0, 0.0, ], maxlen=3 * int(target_tps))
|
||||||
|
# _t0 = []
|
||||||
|
|
||||||
|
self.log.debug("tick system started")
|
||||||
|
while self.run:
|
||||||
|
target_interval = 1 / self.target_tps
|
||||||
|
start_time = time.monotonic()
|
||||||
|
|
||||||
|
ev.call_event("serverTick")
|
||||||
|
await ev.call_async_event("serverTick")
|
||||||
|
|
||||||
|
# Calculate the time taken for this tick
|
||||||
|
end_time = time.monotonic()
|
||||||
|
tick_duration = end_time - start_time
|
||||||
|
# _t0.append(tick_duration)
|
||||||
|
|
||||||
|
# Calculate the time to sleep to maintain target TPS
|
||||||
|
sleep_time = target_interval - tick_duration - statistics.fmean(_add_to_sleep)
|
||||||
|
if sleep_time > 0:
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
|
# Update tick count and time
|
||||||
|
ticks += 1
|
||||||
|
current_time = time.monotonic()
|
||||||
|
ticks_2s.append(current_time)
|
||||||
|
ticks_5s.append(current_time)
|
||||||
|
ticks_30s.append(current_time)
|
||||||
|
ticks_60s.append(current_time)
|
||||||
|
|
||||||
|
# Calculate TPS
|
||||||
|
elapsed_time = current_time - last_tick_time
|
||||||
|
if elapsed_time >= 1:
|
||||||
|
self.tps = ticks / elapsed_time
|
||||||
|
# if self.tps < 5:
|
||||||
|
# self.log.warning(f"Low TPS: {self.tps:.2f}")
|
||||||
|
# Reset for next calculation
|
||||||
|
# _t0s = max(_t0), min(_t0), statistics.fmean(_t0)
|
||||||
|
# _tw = max(_add_to_sleep), min(_add_to_sleep), statistics.fmean(_add_to_sleep)
|
||||||
|
# self.log.debug(f"[{'OK' if sleep_time > 0 else "CHECK"}] TPS: {self.tps:.2f}; Tt={_t0s}; Ts={sleep_time}; Tw={_tw}")
|
||||||
|
# _t0 = []
|
||||||
|
last_tick_time = current_time
|
||||||
|
ticks = 0
|
||||||
|
|
||||||
|
_add_to_sleep.append(time.monotonic() - start_time - sleep_time)
|
||||||
|
self.log.debug("tick system stopped")
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception(e)
|
||||||
|
|
||||||
|
async def _parse_chat(self, event):
|
||||||
|
player = event['kwargs']['player']
|
||||||
|
message = event['kwargs']['message']
|
||||||
|
|
||||||
async def main(self):
|
async def main(self):
|
||||||
self.tcp = self.tcp(self, self.server_ip, self.server_port)
|
self.tcp = self.tcp(self, self.server_ip, self.server_port)
|
||||||
self.udp = self.udp(self, self.server_ip, self.server_port)
|
self.udp = self.udp(self, self.server_ip, self.server_port)
|
||||||
|
PermsSystem()
|
||||||
console.add_command(
|
console.add_command(
|
||||||
"list",
|
"list",
|
||||||
lambda x: f"Players list: {self.get_clients_list(True)}"
|
lambda x: f"Players list: {self.get_clients_list(True)}"
|
||||||
)
|
)
|
||||||
console.add_command("kick", self.kick_cmd)
|
ev.call_event("add_perm_to_alias", "cmd.kick")
|
||||||
|
console.add_command("kick", self._cmd_kick, "kick - Kick user\n"
|
||||||
|
"Usage: kick NICK|:{ID} [REASON]\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" <white>></white> <b><skyblue>kick admin bad boy</skyblue></b>\n"
|
||||||
|
" <white>></white> <b><skyblue>kick :0 bad boy</skyblue></b>",
|
||||||
|
"kick user", {"kick": "<playerlist>"})
|
||||||
|
ev.register("onChatReceive", self._parse_chat)
|
||||||
|
|
||||||
pl_dir = "plugins"
|
pl_dir = "plugins"
|
||||||
self.log.debug("Initializing PluginsLoaders...")
|
self.log.debug("Initializing PluginsLoaders...")
|
||||||
if not os.path.exists(pl_dir):
|
if not os.path.exists(pl_dir):
|
||||||
os.mkdir(pl_dir)
|
os.mkdir(pl_dir)
|
||||||
pl = PluginsLoader(pl_dir)
|
pl = PluginsLoader(pl_dir)
|
||||||
await pl.load()
|
|
||||||
if config.Options['use_lua']:
|
if config.Options['use_lua']:
|
||||||
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
|
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
|
||||||
lpl = LuaPluginsLoader(pl_dir)
|
lpl = LuaPluginsLoader(pl_dir)
|
||||||
lpl.load()
|
lpl.load()
|
||||||
|
await pl.load()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# WebApi Start
|
|
||||||
if config.WebAPI["enabled"]:
|
|
||||||
self.log.debug("Initializing WebAPI...")
|
|
||||||
web_thread = Thread(target=self.start_web, name="WebApiThread")
|
|
||||||
web_thread.start()
|
|
||||||
self.log.debug(f"WebAPI started at new thread: {web_thread.name}")
|
|
||||||
self.web_thread = web_thread
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
self.web_stop = webapp._stop
|
|
||||||
await asyncio.sleep(.3)
|
|
||||||
|
|
||||||
# Mods handler
|
# Mods handler
|
||||||
self.log.debug("Listing mods..")
|
self.log.debug("Listing mods..")
|
||||||
if not os.path.exists(self.mods_dir):
|
if not os.path.exists(self.mods_dir):
|
||||||
@@ -286,16 +421,19 @@ class Core:
|
|||||||
self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
|
self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
|
||||||
self.log.info(i18n.init_ok)
|
self.log.info(i18n.init_ok)
|
||||||
|
|
||||||
await self.heartbeat(True)
|
await self.heartbeat(True) # Check
|
||||||
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
|
|
||||||
self.clients.append(None)
|
self.clients = [None] * config.Game["players"] * 4 # * 4 For down sock and buffer.
|
||||||
tasks = []
|
tasks = []
|
||||||
# self.udp.start,
|
ev.register("serverTick_1s", self._check_alive)
|
||||||
f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
ev.register("serverTick_1s", self._send_online)
|
||||||
|
# ev.register("serverTick_5s", self.heartbeat)
|
||||||
|
f_tasks = [self.tcp.start, self.udp._start, console.start, self._tick, self.heartbeat]
|
||||||
if config.RCON['enabled']:
|
if config.RCON['enabled']:
|
||||||
console.rcon.version = f"KuiToi {__version__}"
|
self.log.warning("RCON not available. yet.")
|
||||||
rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port'])
|
# console.rcon.version = f"KuiToi {__version__}"
|
||||||
f_tasks.append(rcon.start)
|
# rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port'])
|
||||||
|
# f_tasks.append(rcon.start)
|
||||||
for task in f_tasks:
|
for task in f_tasks:
|
||||||
tasks.append(asyncio.create_task(task()))
|
tasks.append(asyncio.create_task(task()))
|
||||||
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
||||||
@@ -310,28 +448,28 @@ class Core:
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Exception: {e}")
|
self.log.error(f"Exception in main:")
|
||||||
self.log.exception(e)
|
self.log.exception(e)
|
||||||
finally:
|
finally:
|
||||||
self.run = False
|
|
||||||
self.tcp.stop()
|
|
||||||
self.udp._stop()
|
|
||||||
await self.stop()
|
await self.stop()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
asyncio.run(self.main())
|
asyncio.run(self.main())
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
ev.call_lua_event("onShutdown")
|
|
||||||
ev.call_event("onServerStopped")
|
|
||||||
await ev.call_async_event("onServerStopped")
|
|
||||||
await self.__gracefully_kick()
|
|
||||||
if config.Options['use_lua']:
|
|
||||||
ev.call_event("_lua_plugins_unload")
|
|
||||||
await ev.call_async_event("_plugins_unload")
|
|
||||||
self.run = False
|
self.run = False
|
||||||
if config.WebAPI["enabled"]:
|
ev.call_lua_event("onShutdown")
|
||||||
asyncio.run(self.web_stop())
|
await ev.call_async_event("onServerStopped")
|
||||||
|
ev.call_event("onServerStopped")
|
||||||
|
try:
|
||||||
|
await self.__gracefully_kick()
|
||||||
|
await self.__gracefully_remove()
|
||||||
|
self.tcp.stop()
|
||||||
|
self.udp._stop()
|
||||||
|
await ev.call_async_event("_plugins_unload")
|
||||||
|
if config.Options['use_lua']:
|
||||||
|
await ev.call_async_event("_lua_plugins_unload")
|
||||||
|
self.run = False
|
||||||
total_time = time.monotonic() - self.start_time
|
total_time = time.monotonic() - self.start_time
|
||||||
hours = int(total_time // 3600)
|
hours = int(total_time // 3600)
|
||||||
minutes = int((total_time % 3600) // 60)
|
minutes = int((total_time % 3600) // 60)
|
||||||
@@ -339,3 +477,6 @@ class Core:
|
|||||||
t = f"{'' if not hours else f'{hours} hours, '}{'' if not hours else f'{minutes} min., '}{seconds} sec."
|
t = f"{'' if not hours else f'{hours} hours, '}{'' if not hours else f'{minutes} min., '}{seconds} sec."
|
||||||
self.log.info(f"Working time: {t}")
|
self.log.info(f"Working time: {t}")
|
||||||
self.log.info(i18n.stop)
|
self.log.info(i18n.stop)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Error while stopping server:")
|
||||||
|
self.log.exception(e)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.core.pyi
|
# File core.core.pyi
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.5
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -17,6 +17,9 @@ from .udp_server import UDPServer
|
|||||||
|
|
||||||
class Core:
|
class Core:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.target_tps = 50
|
||||||
|
self.tick_counter = 0
|
||||||
|
self.tps = 10
|
||||||
self.start_time = time.monotonic()
|
self.start_time = time.monotonic()
|
||||||
self.log = utils.get_logger("core")
|
self.log = utils.get_logger("core")
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
@@ -36,18 +39,22 @@ class Core:
|
|||||||
self.web_stop: Callable = lambda: None
|
self.web_stop: Callable = lambda: None
|
||||||
self.lock_upload = False
|
self.lock_upload = False
|
||||||
self.client_major_version = "2.0"
|
self.client_major_version = "2.0"
|
||||||
self.BeamMP_version = "3.2.0"
|
self.BeamMP_version = "3.4.1"
|
||||||
def get_client(self, cid=None, nick=None) -> Client | None: ...
|
def get_client(self, cid=None, nick=None) -> Client | None: ...
|
||||||
async def insert_client(self, client: Client) -> None: ...
|
async def insert_client(self, client: Client) -> None: ...
|
||||||
def create_client(self, *args, **kwargs) -> Client: ...
|
def create_client(self, *args, **kwargs) -> Client: ...
|
||||||
def get_clients_list(self, need_cid=False) -> str: ...
|
def get_clients_list(self, need_cid=False) -> str: ...
|
||||||
async def check_alive(self) -> None: ...
|
async def _check_alive(self) -> None: ...
|
||||||
|
async def _send_online(self) -> None: ...
|
||||||
|
async def _useful_ticks(self, _) -> None: ...
|
||||||
async def __gracefully_kick(self): ...
|
async def __gracefully_kick(self): ...
|
||||||
@staticmethod
|
async def __gracefully_remove(self): ...
|
||||||
def start_web() -> None: ...
|
def _get_color_tps(self, ticks, d): ...
|
||||||
def stop_me(self) -> None: ...
|
async def _cmd_tps(self, ticks_2s, ticks_5s, ticks_30s, ticks_60s) -> str: ...
|
||||||
|
def _tick(self) -> None: ...
|
||||||
async def heartbeat(self, test=False) -> None: ...
|
async def heartbeat(self, test=False) -> None: ...
|
||||||
async def kick_cmd(self, args: list) -> None | str: ...
|
async def _cmd_kick(self, args: list) -> None | str: ...
|
||||||
|
async def _parse_chat(self, event): ...
|
||||||
async def main(self) -> None: ...
|
async def main(self) -> None: ...
|
||||||
def start(self) -> None: ...
|
def start(self) -> None: ...
|
||||||
async def stop(self) -> None: ...
|
async def stop(self) -> None: ...
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.tcp_server.py
|
# File core.tcp_server.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.8
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -10,6 +10,7 @@ import traceback
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
from modules import RateLimiter
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
@@ -21,6 +22,11 @@ class TCPServer:
|
|||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.run = False
|
self.run = False
|
||||||
|
self._connections = set()
|
||||||
|
self.server = None
|
||||||
|
self.rl = RateLimiter(50, 10, 300)
|
||||||
|
console.add_command("rl", self.rl.parse_console, None, "RateLimiter menu",
|
||||||
|
{"rl": {"info": None, "unban": None, "ban": None, "help": None}})
|
||||||
|
|
||||||
async def auth_client(self, reader, writer):
|
async def auth_client(self, reader, writer):
|
||||||
client = self.Core.create_client(reader, writer)
|
client = self.Core.create_client(reader, writer)
|
||||||
@@ -31,11 +37,11 @@ class TCPServer:
|
|||||||
await client.kick(i18n.core_player_kick_outdated)
|
await client.kick(i18n.core_player_kick_outdated)
|
||||||
return False, client
|
return False, client
|
||||||
else:
|
else:
|
||||||
await client._send(b"S") # Accepted client version
|
await client._send(b"A") # Accepted client version
|
||||||
|
|
||||||
data = await client._recv(True)
|
data = await client._recv(True)
|
||||||
self.log.debug(f"Key: {data}")
|
self.log.debug(f"Key: {data}")
|
||||||
if len(data) > 50:
|
if not data or len(data) > 50:
|
||||||
await client.kick(i18n.core_player_kick_bad_key)
|
await client.kick(i18n.core_player_kick_bad_key)
|
||||||
return False, client
|
return False, client
|
||||||
client._key = data.decode("utf-8")
|
client._key = data.decode("utf-8")
|
||||||
@@ -51,12 +57,18 @@ class TCPServer:
|
|||||||
return False, client
|
return False, client
|
||||||
client.nick = res["username"]
|
client.nick = res["username"]
|
||||||
client.roles = res["roles"]
|
client.roles = res["roles"]
|
||||||
|
self.log.debug(f"{client.roles=} {client.nick=}")
|
||||||
|
if client.roles == "USER" and client.nick == "SantaSpeen":
|
||||||
|
client.roles = "ADM"
|
||||||
client._guest = res["guest"]
|
client._guest = res["guest"]
|
||||||
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
|
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
|
||||||
|
if not client._identifiers.get("ip"):
|
||||||
|
client._identifiers["ip"] = client._addr[0]
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
client._update_logger()
|
client._update_logger()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Auth error: {e}")
|
self.log.error("Auth error.")
|
||||||
|
self.log.exception(e)
|
||||||
await client.kick(i18n.core_player_kick_auth_server_fail)
|
await client.kick(i18n.core_player_kick_auth_server_fail)
|
||||||
return False, client
|
return False, client
|
||||||
|
|
||||||
@@ -72,23 +84,26 @@ class TCPServer:
|
|||||||
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
|
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
|
||||||
for data in lua_data:
|
for data in lua_data:
|
||||||
if 1 == data:
|
if 1 == data:
|
||||||
allow = True
|
allow = False
|
||||||
elif isinstance(data, str):
|
elif isinstance(data, str):
|
||||||
allow = True
|
allow = False
|
||||||
reason = data
|
reason = data
|
||||||
if not allow:
|
if not allow:
|
||||||
await client.kick(reason)
|
await client.kick(reason)
|
||||||
return False, client
|
return False, client
|
||||||
|
|
||||||
ev.call_event("onPlayerAuthenticated", player=client)
|
ev.call_event("onPlayerAuthenticated", player=client)
|
||||||
|
await ev.call_async_event("onPlayerAuthenticated", player=client)
|
||||||
|
if not client.alive:
|
||||||
|
await client.kick("Not accepted.")
|
||||||
|
return False, client
|
||||||
|
|
||||||
if len(self.Core.clients_by_id) > config.Game["players"]:
|
if len(self.Core.clients_by_id) > config.Game["players"]:
|
||||||
await client.kick(i18n.core_player_kick_server_full)
|
await client.kick(i18n.core_player_kick_server_full)
|
||||||
return False, client
|
return False, client
|
||||||
else:
|
else:
|
||||||
self.log.info(i18n.core_identifying_okay)
|
|
||||||
await self.Core.insert_client(client)
|
await self.Core.insert_client(client)
|
||||||
client.log.info(i18n.core_player_set_id.format(client.pid))
|
client.log.info(i18n.core_identifying_okay)
|
||||||
|
|
||||||
return True, client
|
return True, client
|
||||||
|
|
||||||
@@ -101,7 +116,7 @@ class TCPServer:
|
|||||||
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
|
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
|
||||||
else:
|
else:
|
||||||
writer.close()
|
writer.close()
|
||||||
self.log.debug(f"Unknown client id:{cid} - HandleDownload")
|
self.log.debug(f"Unknown client <nick>:{cid} - HandleDownload")
|
||||||
finally:
|
finally:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -111,57 +126,55 @@ class TCPServer:
|
|||||||
result, client = await self.auth_client(reader, writer)
|
result, client = await self.auth_client(reader, writer)
|
||||||
if result:
|
if result:
|
||||||
await client._looper()
|
await client._looper()
|
||||||
return result, client
|
return "U", client
|
||||||
case "D":
|
case "D":
|
||||||
await self.set_down_rw(reader, writer)
|
await self.set_down_rw(reader, writer)
|
||||||
|
return "D", None
|
||||||
case "P":
|
case "P":
|
||||||
writer.write(b"P")
|
writer.write(b"P")
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
writer.close()
|
writer.close()
|
||||||
case _:
|
case _:
|
||||||
self.log.error(f"Unknown code: {code}")
|
self.log.warning(f"Unknown code: {code}")
|
||||||
self.log.info("Report about that!")
|
self.log.warning("Report about that!")
|
||||||
writer.close()
|
writer.close()
|
||||||
return False, None
|
return "E", None
|
||||||
|
|
||||||
async def handle_client(self, reader, writer):
|
async def handle_client(self, reader, writer):
|
||||||
while True:
|
self._connections.add(writer)
|
||||||
try:
|
try:
|
||||||
|
ip = writer.get_extra_info('peername')[0]
|
||||||
|
if self.rl.is_banned(ip):
|
||||||
|
await self.rl.notify(ip, writer)
|
||||||
|
writer.close()
|
||||||
data = await reader.read(1)
|
data = await reader.read(1)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
return
|
||||||
code = data.decode()
|
code = data.decode()
|
||||||
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
|
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
|
||||||
# task = asyncio.create_task(self.handle_code(code, reader, writer))
|
_type, cl = await self.handle_code(code, reader, writer)
|
||||||
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
|
self.log.debug(f"[{_type}] cl returned: {cl}")
|
||||||
_, cl = await self.handle_code(code, reader, writer)
|
|
||||||
if cl:
|
if cl:
|
||||||
await cl._remove_me()
|
await cl._remove_me()
|
||||||
del cl
|
|
||||||
break
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error("Error while handling connection...")
|
self.log.error("Error while handling connection...")
|
||||||
self.log.exception(e)
|
self.log.exception(e)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
break
|
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
self.log.debug("Starting TCP server.")
|
self.log.debug("Starting TCP server.")
|
||||||
self.run = True
|
self.run = True
|
||||||
try:
|
try:
|
||||||
server = await asyncio.start_server(self.handle_client, self.host, self.port,
|
self.server = await asyncio.start_server(self.handle_client, self.host, self.port,
|
||||||
backlog=int(config.Game["players"] * 2.3))
|
backlog=int(config.Game["players"] * 4))
|
||||||
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}")
|
async with self.server:
|
||||||
while True:
|
self.log.debug(f"TCP server started on {self.server.sockets[0].getsockname()!r}")
|
||||||
async with server:
|
await self.server.serve_forever()
|
||||||
await server.serve_forever()
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.log.error(i18n.core_bind_failed.format(e))
|
self.log.error(i18n.core_bind_failed.format(e))
|
||||||
raise e
|
raise e
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Error: {e}")
|
self.log.exception(e)
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
self.run = False
|
self.run = False
|
||||||
@@ -169,3 +182,16 @@ class TCPServer:
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.log.debug("Stopping TCP server")
|
self.log.debug("Stopping TCP server")
|
||||||
|
try:
|
||||||
|
if not self.server:
|
||||||
|
return
|
||||||
|
self.server.close()
|
||||||
|
for conn in self._connections:
|
||||||
|
self.log.debug(f"Closing {conn}")
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except ConnectionResetError:
|
||||||
|
self.log.debug("ConnectionResetError")
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception(e)
|
||||||
|
self.log.debug("Stopped.")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.tcp_server.pyi
|
# File core.tcp_server.pyi
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.8
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -10,16 +10,21 @@ from typing import Tuple
|
|||||||
|
|
||||||
from core import utils, Core
|
from core import utils, Core
|
||||||
from core.Client import Client
|
from core.Client import Client
|
||||||
|
from modules import RateLimiter
|
||||||
|
|
||||||
|
|
||||||
class TCPServer:
|
class TCPServer:
|
||||||
def __init__(self, core: Core, host, port):
|
def __init__(self, core: Core, host, port):
|
||||||
|
self.server = await asyncio.start_server(self.handle_client, "", 0, backlog=int(config.Game["players"] * 2.3))
|
||||||
self.log = utils.get_logger("TCPServer")
|
self.log = utils.get_logger("TCPServer")
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
self.Core = core
|
self.Core = core
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self._connections = set()
|
||||||
self.run = False
|
self.run = False
|
||||||
|
self.rl = RateLimiter(50, 10, 15)
|
||||||
|
|
||||||
async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
|
async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
|
||||||
async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ...
|
async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ...
|
||||||
async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
|
async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.udp_server.py
|
# File core.udp_server
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.7
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2024
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ class UDPServer(asyncio.DatagramTransport):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.log = utils.get_logger("UDPServer")
|
self.log = utils.get_logger("UDPServer")
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
self.Core = core
|
self._core = core
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.run = False
|
self.run = False
|
||||||
@@ -27,42 +28,25 @@ class UDPServer(asyncio.DatagramTransport):
|
|||||||
def pause_writing(self, *args, **kwargs): ...
|
def pause_writing(self, *args, **kwargs): ...
|
||||||
def resume_writing(self, *args, **kwargs): ...
|
def resume_writing(self, *args, **kwargs): ...
|
||||||
|
|
||||||
async def handle_datagram(self, data, addr):
|
async def handle_datagram(self, packet, addr):
|
||||||
try:
|
try:
|
||||||
cid = data[0] - 1
|
cid = packet[0] - 1
|
||||||
code = data[2:3].decode()
|
if cid > config.Game['players'] * 4:
|
||||||
data = data[2:].decode()
|
return
|
||||||
|
client = self._core.get_client(cid=cid)
|
||||||
client = self.Core.get_client(cid=cid)
|
|
||||||
if client:
|
if client:
|
||||||
match code:
|
if not client.alive:
|
||||||
case "p": # Ping packet
|
client.log.debug(f"Still sending UDP data: {packet}")
|
||||||
ev.call_event("onSentPing")
|
|
||||||
self.transport.sendto(b"p", addr)
|
|
||||||
case "Z": # Position packet
|
|
||||||
if client._udp_sock != (self.transport, addr):
|
if client._udp_sock != (self.transport, addr):
|
||||||
|
self.log.debug(f"udp_addr={addr[0]}; main_addr={client.addr}")
|
||||||
|
if addr[0] != client.addr:
|
||||||
|
self.log.warning(f"udp_addr != main_addr. Is this bug?")
|
||||||
client._udp_sock = (self.transport, addr)
|
client._udp_sock = (self.transport, addr)
|
||||||
self.log.debug(f"Set UDP Sock for CID: {cid}")
|
self.log.debug(f"Set UDP Sock for CID: {cid}")
|
||||||
ev.call_event("onChangePosition", data=data)
|
await client._udp_put(packet)
|
||||||
sub = data.find("{", 1)
|
|
||||||
last_pos_data = data[sub:]
|
|
||||||
try:
|
|
||||||
last_pos = json.loads(last_pos_data)
|
|
||||||
client._last_position = last_pos
|
|
||||||
_, car_id = client._get_cid_vid(data)
|
|
||||||
client._cars[car_id]['pos'] = last_pos
|
|
||||||
except Exception as e:
|
|
||||||
self.log.debug(f"Cannot parse position packet: {e}")
|
|
||||||
self.log.debug(f"data: {data}, sup: {sub}")
|
|
||||||
self.log.debug(f"last_pos_data: {last_pos_data}")
|
|
||||||
await client._send(data, to_all=True, to_self=False, to_udp=True)
|
|
||||||
case _:
|
|
||||||
self.log.debug(f"[{cid}] Unknown code: {code}")
|
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"[{cid}] Client not found.")
|
self.log.debug(f"[{cid}] Client not found.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
||||||
self.log.error(f"Error handle_datagram: {e}")
|
self.log.error(f"Error handle_datagram: {e}")
|
||||||
|
|
||||||
def datagram_received(self, *args, **kwargs):
|
def datagram_received(self, *args, **kwargs):
|
||||||
@@ -81,14 +65,13 @@ class UDPServer(asyncio.DatagramTransport):
|
|||||||
|
|
||||||
async def _start(self):
|
async def _start(self):
|
||||||
self.log.debug("Starting UDP server.")
|
self.log.debug("Starting UDP server.")
|
||||||
while self.Core.run:
|
while self._core.run:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
d = UDPServer
|
d = UDPServer
|
||||||
self.transport, p = await self.loop.create_datagram_endpoint(
|
self.transport, p = await self.loop.create_datagram_endpoint(
|
||||||
lambda: d(self.Core),
|
lambda: d(self._core),
|
||||||
local_addr=(self.host, self.port)
|
local_addr=(self.host, self.port)
|
||||||
)
|
)
|
||||||
d.transport = self.transport
|
d.transport = self.transport
|
||||||
@@ -100,8 +83,8 @@ class UDPServer(asyncio.DatagramTransport):
|
|||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
# self.run = False
|
self.run = False
|
||||||
# self.Core.run = False
|
self._core.run = False
|
||||||
self.log.error(f"Cannot bind port or other error: {e}")
|
self.log.error(f"Cannot bind port or other error: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Error: {e}")
|
self.log.error(f"Error: {e}")
|
||||||
@@ -109,4 +92,5 @@ class UDPServer(asyncio.DatagramTransport):
|
|||||||
|
|
||||||
def _stop(self):
|
def _stop(self):
|
||||||
self.log.debug("Stopping UDP server")
|
self.log.debug("Stopping UDP server")
|
||||||
|
if self.transport:
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File core.udp_server.py
|
# File core.udp_server.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.5
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -18,13 +18,13 @@ class UDPServer(asyncio.DatagramTransport):
|
|||||||
def __init__(self, core: Core, host=None, port=None, transport=None):
|
def __init__(self, core: Core, host=None, port=None, transport=None):
|
||||||
self.log = utils.get_logger("UDPServer")
|
self.log = utils.get_logger("UDPServer")
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
self.Core = core
|
self._core = core
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.run = False
|
self.run = False
|
||||||
# self.transport: DatagramTransport = None
|
|
||||||
def connection_made(self, transport: DatagramTransport): ...
|
def connection_made(self, transport: DatagramTransport): ...
|
||||||
async def handle_datagram(self, data: bytes, addr: Tuple[str, int]):
|
async def handle_datagram(self, data: bytes, addr: Tuple[str, int]):
|
||||||
def datagram_received(self, data: bytes, addr: Tuple[str, int]): ...
|
def datagram_received(self, data: bytes, addr: Tuple[str, int]): ...
|
||||||
|
async def _print_pps(self) -> None: ...
|
||||||
async def _start(self) -> None: ...
|
async def _start(self) -> None: ...
|
||||||
async def _stop(self) -> None: ...
|
async def _stop(self) -> None: ...
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# File core.utils.py
|
# File core.utils.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Version 1.1
|
# Version 1.1
|
||||||
# Core version: 0.4.3
|
# Core version: 0.4.5
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
import datetime
|
import datetime
|
||||||
|
|||||||
@@ -1,9 +1,71 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import copy
|
||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File modules.config_provider.__init__.py
|
# File modules.ConfigProvider
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Version 1.0
|
# Version 1.0
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
from .config_provider import ConfigProvider, Config
|
import os
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None):
|
||||||
|
self.Auth = auth or {"key": None, "private": True}
|
||||||
|
self.Game = game or {"map": "gridmap_v2", "players": 8, "cars": 1}
|
||||||
|
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "tags": "Freroam",
|
||||||
|
"server_ip": "0.0.0.0", "server_port": 30814}
|
||||||
|
self.Options = options or {"language": "en", "speed_limit": 0, "use_queue": False,
|
||||||
|
"use_lua": False, "log_chat": True}
|
||||||
|
self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383,
|
||||||
|
"password": secrets.token_hex(6)}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (f"{self.__class__.__name__}(Auth={self.Auth!r}, Game={self.Game!r}, Server={self.Server!r}, "
|
||||||
|
f"RCON={self.RCON!r}, Options={self.Options!r})")
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigProvider:
|
||||||
|
|
||||||
|
def __init__(self, config_path):
|
||||||
|
self.config_path = config_path
|
||||||
|
self.config = Config()
|
||||||
|
|
||||||
|
def read(self, _again=False):
|
||||||
|
if not os.path.exists(self.config_path):
|
||||||
|
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||||
|
yaml.dump(self.config, f)
|
||||||
|
try:
|
||||||
|
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||||
|
self.config = yaml.load(f.read(), yaml.Loader)
|
||||||
|
except yaml.YAMLError:
|
||||||
|
print("You have errors in the YAML syntax.")
|
||||||
|
print("Stopping server.")
|
||||||
|
exit(1)
|
||||||
|
if not self.config:
|
||||||
|
if _again:
|
||||||
|
print("Error: empty configuration.")
|
||||||
|
exit(1)
|
||||||
|
print("Reconfig: empty configuration.")
|
||||||
|
os.remove(self.config_path)
|
||||||
|
self.config = Config()
|
||||||
|
return self.read(True)
|
||||||
|
|
||||||
|
if not self.config.Options.get("debug"):
|
||||||
|
self.config.Options['debug'] = False
|
||||||
|
if not self.config.Options.get("encoding"):
|
||||||
|
self.config.Options['encoding'] = "utf-8"
|
||||||
|
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
_config = copy.deepcopy(self.config)
|
||||||
|
del _config.enc
|
||||||
|
del _config.Options['debug']
|
||||||
|
del _config.Options['encoding']
|
||||||
|
os.remove(self.config_path)
|
||||||
|
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||||
|
yaml.dump(_config, f)
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Developed by KuiToi Dev
|
|
||||||
# File modules.config_provider.config_provider.py
|
|
||||||
# Written by: SantaSpeen
|
|
||||||
# Version 1.1
|
|
||||||
# Licence: FPA
|
|
||||||
# (c) kuitoi.su 2023
|
|
||||||
import os
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=None):
|
|
||||||
self.Auth = auth or {"key": None, "private": True}
|
|
||||||
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
|
|
||||||
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
|
|
||||||
"server_ip": "0.0.0.0", "server_port": 30814}
|
|
||||||
self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383,
|
|
||||||
"password": secrets.token_hex(16)}
|
|
||||||
self.Options = options or {"language": "en", "encoding": "utf-8", "speed_limit": 0, "use_queue": False,
|
|
||||||
"debug": False, "use_lua": False, "log_chat": True}
|
|
||||||
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
|
|
||||||
"access_token": secrets.token_hex(16)}
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigProvider:
|
|
||||||
|
|
||||||
def __init__(self, config_path):
|
|
||||||
self.config_path = config_path
|
|
||||||
self.config = Config()
|
|
||||||
|
|
||||||
def open_config(self):
|
|
||||||
if not os.path.exists(self.config_path):
|
|
||||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
|
||||||
yaml.dump(self.config, f)
|
|
||||||
try:
|
|
||||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
|
||||||
self.config = yaml.load(f.read(), yaml.Loader)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
print("You have errors in the YAML syntax.")
|
|
||||||
print("Stopping server.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
return self.config
|
|
||||||
|
|
||||||
def save_config(self):
|
|
||||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
|
||||||
yaml.dump(self.config, f)
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
from typing import Dict
|
### Builtins
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
class Config:
|
class Config:
|
||||||
Auth: Dict[str, object]
|
Auth: Dict[str, object]
|
||||||
Game: Dict[str, object]
|
Game: Dict[str, object]
|
||||||
Server: Dict[str, object]
|
Server: Dict[str, object]
|
||||||
RCON: Dict[str, object]
|
RCON: Dict[str, object]
|
||||||
Options: Dict[str, object]
|
Options: Dict[str, object]
|
||||||
WebAPI: Dict[str, object]
|
|
||||||
enc: str | None
|
enc: str | None
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
|
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
|
||||||
class config (Config): ...
|
class config (Config): ...
|
||||||
|
```
|
||||||
@@ -1,9 +1,394 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File modules.console.__init__.py
|
# File modules.ConsoleSystem
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Version 1.0
|
# Version 1.2
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
from .console_system import Console
|
import builtins
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
from typing import AnyStr
|
||||||
|
|
||||||
|
from prompt_toolkit import PromptSession, print_formatted_text, HTML, ANSI
|
||||||
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||||
|
from prompt_toolkit.completion import Completer, WordCompleter
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
from prompt_toolkit.history import FileHistory
|
||||||
|
|
||||||
|
try:
|
||||||
|
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
|
||||||
|
except AssertionError:
|
||||||
|
class NoConsoleScreenBufferError(Exception):
|
||||||
|
...
|
||||||
|
from prompt_toolkit.patch_stdout import patch_stdout
|
||||||
|
|
||||||
|
from core import get_logger
|
||||||
|
from modules.ConsoleSystem.RCON import RCONSystem
|
||||||
|
|
||||||
|
|
||||||
|
class BadCompleter(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
|
class MyNestedCompleter(Completer):
|
||||||
|
def __init__(self, options, ignore_case=True, on_none=None):
|
||||||
|
self.options = self._from_nested_dict(options)
|
||||||
|
self.ignore_case = ignore_case
|
||||||
|
self.on_none = on_none
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"MyNestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_nested_dict(cls, data, r=False):
|
||||||
|
options: dict[str, Completer | None] = {}
|
||||||
|
for key, value in data.items():
|
||||||
|
if isinstance(value, Completer):
|
||||||
|
options[key] = value
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
options[key] = cls._from_nested_dict(value, True)
|
||||||
|
elif isinstance(value, set):
|
||||||
|
options[key] = cls._from_nested_dict({item: None for item in value}, True)
|
||||||
|
elif isinstance(value, bool):
|
||||||
|
if value:
|
||||||
|
options[key] = None
|
||||||
|
else:
|
||||||
|
if isinstance(value, str) and value == "<playerlist>":
|
||||||
|
options[key] = players_completer
|
||||||
|
else:
|
||||||
|
if value is not None:
|
||||||
|
raise BadCompleter(f"{value!r} for key {key!r} have not valid type.")
|
||||||
|
options[key] = None
|
||||||
|
if r:
|
||||||
|
return cls(options)
|
||||||
|
return options
|
||||||
|
|
||||||
|
def load(self, data):
|
||||||
|
self.options = self._from_nested_dict(data)
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
# Split document.
|
||||||
|
text = document.text_before_cursor.lstrip()
|
||||||
|
stripped_len = len(document.text_before_cursor) - len(text)
|
||||||
|
|
||||||
|
# If there is a space, check for the first term, and use a
|
||||||
|
# subcompleter.
|
||||||
|
if " " in text:
|
||||||
|
first_term = text.split()[0]
|
||||||
|
completer = self.options.get(first_term)
|
||||||
|
if completer is None:
|
||||||
|
completer = self.on_none
|
||||||
|
|
||||||
|
# If we have a sub completer, use this for the completions.
|
||||||
|
if completer is not None:
|
||||||
|
remaining_text = text[len(first_term):].lstrip()
|
||||||
|
move_cursor = len(text) - len(remaining_text) + stripped_len
|
||||||
|
|
||||||
|
new_document = Document(
|
||||||
|
remaining_text,
|
||||||
|
cursor_position=document.cursor_position - move_cursor,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield from completer.get_completions(new_document, complete_event)
|
||||||
|
|
||||||
|
# No space in the input: behave exactly like `WordCompleter`.
|
||||||
|
else:
|
||||||
|
completer = WordCompleter(
|
||||||
|
list(self.options.keys()), ignore_case=self.ignore_case
|
||||||
|
)
|
||||||
|
yield from completer.get_completions(document, complete_event)
|
||||||
|
|
||||||
|
def tick_players(self, _):
|
||||||
|
clients = ev.call_event("_get_player", raw=True)[0]
|
||||||
|
self.options = {}
|
||||||
|
for k in clients.keys():
|
||||||
|
self.options[k] = None
|
||||||
|
|
||||||
|
|
||||||
|
players_completer = MyNestedCompleter({})
|
||||||
|
builtins.Completer = MyNestedCompleter
|
||||||
|
builtins.players_completer = players_completer
|
||||||
|
|
||||||
|
|
||||||
|
class Console:
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
prompt_in="> ",
|
||||||
|
prompt_out="",
|
||||||
|
not_found="Command \"%s\" not found in alias.",
|
||||||
|
debug=False) -> None:
|
||||||
|
self.__logger = get_logger("console")
|
||||||
|
self.__run = False
|
||||||
|
try:
|
||||||
|
self.session = PromptSession(history=FileHistory('./.cmdhistory'))
|
||||||
|
self.__legacy_mode = False
|
||||||
|
except NoConsoleScreenBufferError:
|
||||||
|
self.__legacy_mode = True
|
||||||
|
self.__prompt_in = prompt_in
|
||||||
|
self.__prompt_out = prompt_out
|
||||||
|
self.__not_found = not_found
|
||||||
|
self.__is_debug = debug
|
||||||
|
self.__print = print
|
||||||
|
self.__func = dict()
|
||||||
|
self.__alias = dict()
|
||||||
|
self.__man = dict()
|
||||||
|
self.__desc = dict()
|
||||||
|
self.__print_logger = get_logger("print")
|
||||||
|
self.completer = MyNestedCompleter(self.__alias)
|
||||||
|
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
|
||||||
|
custom_completer={"man": {}})
|
||||||
|
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
|
||||||
|
custom_completer={"help": {"--raw": False}})
|
||||||
|
rcon = RCONSystem
|
||||||
|
rcon.console = self
|
||||||
|
self.rcon = rcon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legacy_mode(self):
|
||||||
|
return self.__legacy_mode
|
||||||
|
|
||||||
|
def __debug(self, *x):
|
||||||
|
self.__logger.debug(' '.join(x))
|
||||||
|
# if self.__is_debug:
|
||||||
|
# x = list(x)
|
||||||
|
# x.insert(0, "\r CONSOLE DEBUG:")
|
||||||
|
# self.__print(*x)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
print(item)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_max_len(arg) -> int:
|
||||||
|
i = 0
|
||||||
|
arg = list(arg)
|
||||||
|
for a in arg:
|
||||||
|
ln = len(str(a))
|
||||||
|
if ln > i:
|
||||||
|
i = ln
|
||||||
|
return i
|
||||||
|
|
||||||
|
def __create_man_message(self, argv: list) -> AnyStr:
|
||||||
|
if len(argv) == 0:
|
||||||
|
return self.__man.get("man")
|
||||||
|
|
||||||
|
x = argv[0]
|
||||||
|
if x not in self.__alias:
|
||||||
|
return i18n.man_command_not_found.format(x)
|
||||||
|
return self.__man.get(x)
|
||||||
|
|
||||||
|
# noinspection PyStringFormat
|
||||||
|
def __create_help_message(self, argv: list) -> AnyStr:
|
||||||
|
self.__debug("creating help message")
|
||||||
|
raw = False
|
||||||
|
max_len_v = 0
|
||||||
|
if "--raw" in argv:
|
||||||
|
max_len_v = self.__get_max_len(self.__func.values())
|
||||||
|
print()
|
||||||
|
raw = True
|
||||||
|
|
||||||
|
message = "\n"
|
||||||
|
max_len = self.__get_max_len(self.__func.keys())
|
||||||
|
if max_len < 7:
|
||||||
|
max_len = 7
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
|
||||||
|
else:
|
||||||
|
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
|
||||||
|
|
||||||
|
for k, v in self.__func.items():
|
||||||
|
doc = self.__desc.get(k)
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if doc is None:
|
||||||
|
doc = i18n.help_message_not_found
|
||||||
|
message += f" %-{max_len}s : %s\n" % (k, doc)
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
def del_command(self, func):
|
||||||
|
self.__debug(f"delete command: func={func};")
|
||||||
|
keys = []
|
||||||
|
for k, v in self.__func.items():
|
||||||
|
if v['f'] is func:
|
||||||
|
keys.append(k)
|
||||||
|
for key in keys:
|
||||||
|
self.__debug(f"{key=}")
|
||||||
|
self.__alias.pop(key)
|
||||||
|
self.__alias["man"].pop(key)
|
||||||
|
self.__func.pop(key)
|
||||||
|
self.__man.pop(key)
|
||||||
|
self.__desc.pop(key)
|
||||||
|
if keys:
|
||||||
|
self.__debug("Deleted.")
|
||||||
|
self.completer.load(self.__alias)
|
||||||
|
|
||||||
|
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
|
||||||
|
if not isinstance(key, str):
|
||||||
|
raise TypeError("key must be string")
|
||||||
|
|
||||||
|
key = key.replace(" ", "-")
|
||||||
|
self.__debug(f"added user command: key={key}; func={func};")
|
||||||
|
self.__alias.update(custom_completer or {key: None})
|
||||||
|
self.__alias["man"].update({key: None})
|
||||||
|
self.__func.update({key: {"f": func}})
|
||||||
|
self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man if man else "No page"}</seagreen>'})
|
||||||
|
self.__desc.update({key: desc})
|
||||||
|
self.completer.load(self.__alias)
|
||||||
|
return self.__alias.copy()
|
||||||
|
|
||||||
|
def _write(self, text):
|
||||||
|
# https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html#formatted-text
|
||||||
|
if self.__legacy_mode:
|
||||||
|
print(text)
|
||||||
|
return
|
||||||
|
assert isinstance(text, str)
|
||||||
|
_type = text.split(":")[0]
|
||||||
|
match _type:
|
||||||
|
case "html":
|
||||||
|
print_formatted_text(HTML(text[5:]))
|
||||||
|
case "ansi":
|
||||||
|
print_formatted_text(ANSI(text[5:]))
|
||||||
|
case _:
|
||||||
|
print_formatted_text(text)
|
||||||
|
|
||||||
|
def write(self, s: AnyStr):
|
||||||
|
if isinstance(s, (list, tuple)):
|
||||||
|
for text in s:
|
||||||
|
self._write(text)
|
||||||
|
else:
|
||||||
|
self._write(s)
|
||||||
|
|
||||||
|
def log(self, s: AnyStr) -> None:
|
||||||
|
# if isinstance(s, (list, tuple)):
|
||||||
|
# for text in s:
|
||||||
|
# self.__logger.info(f"{text}")
|
||||||
|
# else:
|
||||||
|
# self.__logger.info(f"{s}")
|
||||||
|
self.write(s)
|
||||||
|
|
||||||
|
def __lshift__(self, s: AnyStr) -> None:
|
||||||
|
self.write(s)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alias(self) -> dict:
|
||||||
|
return self.__alias.copy()
|
||||||
|
|
||||||
|
def __builtins_print(self,
|
||||||
|
*values: object,
|
||||||
|
sep: str or None = " ",
|
||||||
|
end: str or None = None,
|
||||||
|
file: str or None = None,
|
||||||
|
flush: bool = False) -> None:
|
||||||
|
self.__debug(f"Used __builtins_print; is_run: {self.__run}")
|
||||||
|
val = list(values)
|
||||||
|
if len(val) > 0:
|
||||||
|
if self.__run:
|
||||||
|
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
|
||||||
|
else:
|
||||||
|
if end is None:
|
||||||
|
end = "\n"
|
||||||
|
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
|
||||||
|
|
||||||
|
def logger_hook(self) -> None:
|
||||||
|
self.__debug("used logger_hook")
|
||||||
|
|
||||||
|
def emit(cls, record):
|
||||||
|
try:
|
||||||
|
msg = cls.format(record)
|
||||||
|
if cls.stream.name == "<stderr>":
|
||||||
|
self.write(f"\r{msg}")
|
||||||
|
else:
|
||||||
|
cls.stream.write(msg + cls.terminator)
|
||||||
|
cls.flush()
|
||||||
|
except RecursionError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
cls.handleError(record)
|
||||||
|
|
||||||
|
logging.StreamHandler.emit = emit
|
||||||
|
|
||||||
|
def builtins_hook(self) -> None:
|
||||||
|
self.__debug("used builtins_hook")
|
||||||
|
|
||||||
|
builtins.Console = Console
|
||||||
|
builtins.console = self
|
||||||
|
|
||||||
|
# builtins.print = self.__builtins_print
|
||||||
|
|
||||||
|
async def _parse_input(self, inp):
|
||||||
|
cmd_s = inp.split(" ")
|
||||||
|
cmd = cmd_s[0]
|
||||||
|
if cmd == "":
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
found_in_lua = False
|
||||||
|
d = ev.call_lua_event("onConsoleInput", inp)
|
||||||
|
if len(d) > 0:
|
||||||
|
for text in d:
|
||||||
|
if text is not None:
|
||||||
|
found_in_lua = True
|
||||||
|
self.log(text)
|
||||||
|
command_object = self.__func.get(cmd)
|
||||||
|
if command_object:
|
||||||
|
func = command_object['f']
|
||||||
|
if inspect.iscoroutinefunction(func):
|
||||||
|
out = await func(cmd_s[1:])
|
||||||
|
else:
|
||||||
|
out = func(cmd_s[1:])
|
||||||
|
if out:
|
||||||
|
self.log(out)
|
||||||
|
else:
|
||||||
|
if not found_in_lua:
|
||||||
|
self.log(self.__not_found % cmd)
|
||||||
|
|
||||||
|
async def _read_input(self):
|
||||||
|
with patch_stdout():
|
||||||
|
while self.__run:
|
||||||
|
try:
|
||||||
|
inp = await self.session.prompt_async(
|
||||||
|
self.__prompt_in, completer=self.completer, auto_suggest=AutoSuggestFromHistory()
|
||||||
|
)
|
||||||
|
if await self._parse_input(inp):
|
||||||
|
continue
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.__run = False
|
||||||
|
except Exception as e:
|
||||||
|
self.__logger.error("Exception in console.py:")
|
||||||
|
self.__logger.exception(e)
|
||||||
|
|
||||||
|
async def _read_input_legacy(self):
|
||||||
|
while self.__run:
|
||||||
|
try:
|
||||||
|
inp = input(self.__prompt_in)
|
||||||
|
if await self._parse_input(inp):
|
||||||
|
continue
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
self.__logger.error("UnicodeDecodeError")
|
||||||
|
self.__run = False
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.__run = False
|
||||||
|
except Exception as e:
|
||||||
|
self.__logger.error("Exception in console.py:")
|
||||||
|
self.__logger.exception(e)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
ev.register("serverTick_0.5s", players_completer.tick_players)
|
||||||
|
# ev.register("get_players_completer", lambda _: players_completer)
|
||||||
|
self.__run = True
|
||||||
|
if self.__legacy_mode:
|
||||||
|
await self._read_input_legacy()
|
||||||
|
else:
|
||||||
|
await self._read_input()
|
||||||
|
self.__debug("Closing console.")
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
def stop(self, *args, **kwargs):
|
||||||
|
self.__run = False
|
||||||
|
|||||||
@@ -1,268 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Developed by KuiToi Dev
|
|
||||||
# File modules.ConsoleSystem.console_system.py
|
|
||||||
# Written by: SantaSpeen
|
|
||||||
# Version 1.2
|
|
||||||
# Licence: FPA
|
|
||||||
# (c) kuitoi.su 2023
|
|
||||||
import builtins
|
|
||||||
import inspect
|
|
||||||
import logging
|
|
||||||
from typing import AnyStr
|
|
||||||
|
|
||||||
from prompt_toolkit import PromptSession, print_formatted_text, HTML
|
|
||||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
||||||
from prompt_toolkit.completion import NestedCompleter
|
|
||||||
from prompt_toolkit.history import FileHistory
|
|
||||||
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
|
|
||||||
from prompt_toolkit.patch_stdout import patch_stdout
|
|
||||||
|
|
||||||
from core import get_logger
|
|
||||||
from modules.ConsoleSystem.RCON import RCONSystem
|
|
||||||
|
|
||||||
|
|
||||||
class Console:
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
prompt_in="> ",
|
|
||||||
prompt_out="",
|
|
||||||
not_found="Command \"%s\" not found in alias.",
|
|
||||||
debug=False) -> None:
|
|
||||||
self.__logger = get_logger("console")
|
|
||||||
self.__is_run = False
|
|
||||||
self.no_cmd = False
|
|
||||||
self.__prompt_in = prompt_in
|
|
||||||
self.__prompt_out = prompt_out
|
|
||||||
self.__not_found = not_found
|
|
||||||
self.__is_debug = debug
|
|
||||||
self.__print = print
|
|
||||||
self.__func = dict()
|
|
||||||
self.__alias = dict()
|
|
||||||
self.__man = dict()
|
|
||||||
self.__desc = dict()
|
|
||||||
self.__print_logger = get_logger("print")
|
|
||||||
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
|
|
||||||
custom_completer={"man": {}})
|
|
||||||
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
|
|
||||||
custom_completer={"help": {"--raw": None}})
|
|
||||||
self.completer = NestedCompleter.from_nested_dict(self.__alias)
|
|
||||||
rcon = RCONSystem
|
|
||||||
rcon.console = self
|
|
||||||
self.rcon = rcon
|
|
||||||
|
|
||||||
def __debug(self, *x):
|
|
||||||
self.__logger.debug(f"{x}")
|
|
||||||
# if self.__is_debug:
|
|
||||||
# x = list(x)
|
|
||||||
# x.insert(0, "\r CONSOLE DEBUG:")
|
|
||||||
# self.__print(*x)
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
print(item)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_max_len(arg) -> int:
|
|
||||||
i = 0
|
|
||||||
arg = list(arg)
|
|
||||||
for a in arg:
|
|
||||||
ln = len(str(a))
|
|
||||||
if ln > i:
|
|
||||||
i = ln
|
|
||||||
return i
|
|
||||||
|
|
||||||
def __create_man_message(self, argv: list) -> AnyStr:
|
|
||||||
if len(argv) == 0:
|
|
||||||
return self.__man.get("man")
|
|
||||||
x = argv[0]
|
|
||||||
if self.__alias.get(x) is None:
|
|
||||||
return i18n.man_command_not_found.format(x)
|
|
||||||
|
|
||||||
man_message = self.__man.get(x)
|
|
||||||
if man_message:
|
|
||||||
return man_message
|
|
||||||
else:
|
|
||||||
return i18n.man_message_not_found
|
|
||||||
|
|
||||||
# noinspection PyStringFormat
|
|
||||||
def __create_help_message(self, argv: list) -> AnyStr:
|
|
||||||
self.__debug("creating help message")
|
|
||||||
raw = False
|
|
||||||
max_len_v = 0
|
|
||||||
if "--raw" in argv:
|
|
||||||
max_len_v = self.__get_max_len(self.__func.values())
|
|
||||||
print()
|
|
||||||
raw = True
|
|
||||||
|
|
||||||
message = "\n"
|
|
||||||
max_len = self.__get_max_len(self.__func.keys())
|
|
||||||
if max_len < 7:
|
|
||||||
max_len = 7
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
|
|
||||||
else:
|
|
||||||
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
|
|
||||||
|
|
||||||
for k, v in self.__func.items():
|
|
||||||
doc = self.__desc.get(k)
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
|
|
||||||
|
|
||||||
else:
|
|
||||||
if doc is None:
|
|
||||||
doc = i18n.help_message_not_found
|
|
||||||
message += f" %-{max_len}s : %s\n" % (k, doc)
|
|
||||||
|
|
||||||
return message
|
|
||||||
|
|
||||||
def __update_completer(self):
|
|
||||||
self.completer = NestedCompleter.from_nested_dict(self.__alias)
|
|
||||||
|
|
||||||
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
|
|
||||||
key = key.format(" ", "-")
|
|
||||||
|
|
||||||
if not isinstance(key, str):
|
|
||||||
raise TypeError("key must be string")
|
|
||||||
self.__debug(f"added user command: key={key}; func={func};")
|
|
||||||
self.__alias.update(custom_completer or {key: None})
|
|
||||||
self.__alias["man"].update({key: None})
|
|
||||||
self.__func.update({key: {"f": func}})
|
|
||||||
self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man}</seagreen>' if man else None})
|
|
||||||
self.__desc.update({key: desc})
|
|
||||||
self.__update_completer()
|
|
||||||
return self.__alias.copy()
|
|
||||||
|
|
||||||
def _write(self, t):
|
|
||||||
if self.no_cmd:
|
|
||||||
print(t)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
if t.startswith("html:"):
|
|
||||||
print_formatted_text(HTML(t[5:]))
|
|
||||||
else:
|
|
||||||
print_formatted_text(t)
|
|
||||||
except NoConsoleScreenBufferError:
|
|
||||||
print("Works in non cmd mode.")
|
|
||||||
self.no_cmd = True
|
|
||||||
print(t)
|
|
||||||
|
|
||||||
def write(self, s: AnyStr):
|
|
||||||
if isinstance(s, (list, tuple)):
|
|
||||||
for text in s:
|
|
||||||
self._write(text)
|
|
||||||
else:
|
|
||||||
self._write(s)
|
|
||||||
|
|
||||||
def log(self, s: AnyStr) -> None:
|
|
||||||
if isinstance(s, (list, tuple)):
|
|
||||||
for text in s:
|
|
||||||
self.__logger.info(f"{text}")
|
|
||||||
else:
|
|
||||||
self.__logger.info(f"{s}")
|
|
||||||
# self.write(s)
|
|
||||||
|
|
||||||
def __lshift__(self, s: AnyStr) -> None:
|
|
||||||
self.write(s)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def alias(self) -> dict:
|
|
||||||
return self.__alias.copy()
|
|
||||||
|
|
||||||
def __builtins_print(self,
|
|
||||||
*values: object,
|
|
||||||
sep: str or None = " ",
|
|
||||||
end: str or None = None,
|
|
||||||
file: str or None = None,
|
|
||||||
flush: bool = False) -> None:
|
|
||||||
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
|
|
||||||
val = list(values)
|
|
||||||
if len(val) > 0:
|
|
||||||
if self.__is_run:
|
|
||||||
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
|
|
||||||
else:
|
|
||||||
if end is None:
|
|
||||||
end = "\n"
|
|
||||||
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
|
|
||||||
|
|
||||||
def logger_hook(self) -> None:
|
|
||||||
self.__debug("used logger_hook")
|
|
||||||
|
|
||||||
def emit(cls, record):
|
|
||||||
try:
|
|
||||||
msg = cls.format(record)
|
|
||||||
if cls.stream.name == "<stderr>":
|
|
||||||
self.write(f"\r{msg}")
|
|
||||||
else:
|
|
||||||
cls.stream.write(msg + cls.terminator)
|
|
||||||
cls.flush()
|
|
||||||
except RecursionError:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
cls.handleError(record)
|
|
||||||
|
|
||||||
logging.StreamHandler.emit = emit
|
|
||||||
|
|
||||||
def builtins_hook(self) -> None:
|
|
||||||
self.__debug("used builtins_hook")
|
|
||||||
|
|
||||||
builtins.Console = Console
|
|
||||||
builtins.console = self
|
|
||||||
|
|
||||||
# builtins.print = self.__builtins_print
|
|
||||||
|
|
||||||
async def read_input(self):
|
|
||||||
session = PromptSession(history=FileHistory('./.cmdhistory'))
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
with patch_stdout():
|
|
||||||
if self.no_cmd:
|
|
||||||
cmd_in = input(self.__prompt_in)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
cmd_in = await session.prompt_async(
|
|
||||||
self.__prompt_in,
|
|
||||||
completer=self.completer,
|
|
||||||
auto_suggest=AutoSuggestFromHistory()
|
|
||||||
)
|
|
||||||
except NoConsoleScreenBufferError:
|
|
||||||
print("Works in non cmd mode.")
|
|
||||||
self.no_cmd = True
|
|
||||||
cmd_s = cmd_in.split(" ")
|
|
||||||
cmd = cmd_s[0]
|
|
||||||
if cmd == "":
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
found_in_lua = False
|
|
||||||
d = ev.call_lua_event("onConsoleInput", cmd_in)
|
|
||||||
if len(d) > 0:
|
|
||||||
for text in d:
|
|
||||||
if text is not None:
|
|
||||||
found_in_lua = True
|
|
||||||
self.log(text)
|
|
||||||
command_object = self.__func.get(cmd)
|
|
||||||
if command_object:
|
|
||||||
func = command_object['f']
|
|
||||||
if inspect.iscoroutinefunction(func):
|
|
||||||
out = await func(cmd_s[1:])
|
|
||||||
else:
|
|
||||||
out = func(cmd_s[1:])
|
|
||||||
if out:
|
|
||||||
self.log(out)
|
|
||||||
else:
|
|
||||||
if not found_in_lua:
|
|
||||||
self.log(self.__not_found % cmd)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
raise KeyboardInterrupt
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error in console.py: {e}")
|
|
||||||
self.__logger.exception(e)
|
|
||||||
|
|
||||||
async def start(self):
|
|
||||||
self.__is_run = True
|
|
||||||
await self.read_input()
|
|
||||||
|
|
||||||
def stop(self, *args, **kwargs):
|
|
||||||
self.__is_run = False
|
|
||||||
raise KeyboardInterrupt
|
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
from logging import Logger
|
### Builtins
|
||||||
from typing import AnyStr
|
|
||||||
|
|
||||||
from core import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
class RCONSystem:
|
class RCONSystem:
|
||||||
console = None
|
console = None
|
||||||
|
|
||||||
def __init__(self, key, host, port):
|
def __init__(self, key, host, port): ...
|
||||||
self.log = get_logger("RCON")
|
|
||||||
self.key = key
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
async def start(self): ...
|
async def start(self): ...
|
||||||
async def stop(self): ...
|
async def stop(self): ...
|
||||||
|
|
||||||
@@ -35,3 +27,4 @@ class console:
|
|||||||
def write(s: str) -> None: ...
|
def write(s: str) -> None: ...
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __lshift__(s: AnyStr) -> None: ...
|
def __lshift__(s: AnyStr) -> None: ...
|
||||||
|
```
|
||||||
@@ -1 +1,228 @@
|
|||||||
from .events_system import EventsSystem
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Developed by KuiToi Dev
|
||||||
|
# File modules.EventsSystem
|
||||||
|
# Written by: SantaSpeen
|
||||||
|
# Version 1.0
|
||||||
|
# Licence: FPA
|
||||||
|
# (c) kuitoi.su 2023
|
||||||
|
import asyncio
|
||||||
|
import builtins
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from core import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyShadowingBuiltins
|
||||||
|
class EventsSystem:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# TODO: default events
|
||||||
|
self.log = get_logger("EventsSystem")
|
||||||
|
self.loop = asyncio.get_event_loop()
|
||||||
|
self.as_tasks = []
|
||||||
|
self.__events = {
|
||||||
|
"onServerStarted": [], # No handler
|
||||||
|
"onPlayerSentKey": [], # Only sync, no handler
|
||||||
|
"onPlayerAuthenticated": [], # (!) Only sync, With handler
|
||||||
|
"onPlayerJoin": [], # (!) With handler
|
||||||
|
"onPlayerReady": [], # No handler
|
||||||
|
"onChatReceive": [], # (!) With handler
|
||||||
|
"onCarSpawn": [], # (!) With handler
|
||||||
|
"onCarDelete": [], # (!) With handler (admin allow)
|
||||||
|
"onCarEdited": [], # (!) With handler
|
||||||
|
"onCarReset": [], # No handler
|
||||||
|
"onCarChanged": [], # No handler
|
||||||
|
"onCarFocusMove": [], # No handler
|
||||||
|
"onSentPing": [], # Only sync, no handler
|
||||||
|
"onChangePosition": [], # Only sync, no handler
|
||||||
|
"onPlayerDisconnect": [], # No handler
|
||||||
|
"onServerStopped": [], # No handler
|
||||||
|
"onCarSpawned": [], # No handler
|
||||||
|
"onCarDeleted": [], # No handler
|
||||||
|
"serverTick": [],
|
||||||
|
"serverTick_0.5s": [],
|
||||||
|
"serverTick_1s": [],
|
||||||
|
"serverTick_2s": [],
|
||||||
|
"serverTick_3s": [],
|
||||||
|
"serverTick_4s": [],
|
||||||
|
"serverTick_5s": [],
|
||||||
|
"serverTick_10s": [],
|
||||||
|
"serverTick_30s": [],
|
||||||
|
"serverTick_60s": [],
|
||||||
|
}
|
||||||
|
self.__async_events = {
|
||||||
|
"onServerStarted": [],
|
||||||
|
"onPlayerJoin": [],
|
||||||
|
"onPlayerReady": [],
|
||||||
|
"onChatReceive": [],
|
||||||
|
"onCarSpawn": [],
|
||||||
|
"onCarDelete": [],
|
||||||
|
"onCarEdited": [],
|
||||||
|
"onCarReset": [],
|
||||||
|
"onCarChanged": [],
|
||||||
|
"onCarFocusMove": [],
|
||||||
|
"onServerStopped": [],
|
||||||
|
"onPlayerDisconnect": [],
|
||||||
|
"onCarSpawned": [],
|
||||||
|
"onCarDeleted": [],
|
||||||
|
"serverTick": [],
|
||||||
|
"serverTick_0.5s": [],
|
||||||
|
"serverTick_1s": [],
|
||||||
|
"serverTick_2s": [],
|
||||||
|
"serverTick_3s": [],
|
||||||
|
"serverTick_4s": [],
|
||||||
|
"serverTick_5s": [],
|
||||||
|
"serverTick_10s": [],
|
||||||
|
"serverTick_30s": [],
|
||||||
|
"serverTick_60s": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.__lua_events = {
|
||||||
|
"onInit": [], # onServerStarted
|
||||||
|
"onShutdown": [], # onServerStopped
|
||||||
|
"onPlayerAuth": [], # onPlayerAuthenticated
|
||||||
|
"onPlayerConnecting": [], # No
|
||||||
|
"onPlayerJoining": [], # No
|
||||||
|
"onPlayerJoin": [], # onPlayerJoin
|
||||||
|
"onPlayerDisconnect": [], # onPlayerDisconnect
|
||||||
|
"onChatMessage": [], # onChatReceive
|
||||||
|
"onVehicleSpawn": [], # onCarSpawn
|
||||||
|
"onVehicleEdited": [], # onCarEdited
|
||||||
|
"onVehicleDeleted": [], # onCarDelete
|
||||||
|
"onVehicleReset": [], # onCarReset
|
||||||
|
"onFileChanged": [], # TODO lua onFileChanged
|
||||||
|
"onConsoleInput": [], # kt.add_command
|
||||||
|
}
|
||||||
|
self.register_event = self.register
|
||||||
|
|
||||||
|
def builtins_hook(self):
|
||||||
|
self.log.debug("used builtins_hook")
|
||||||
|
builtins.ev = self
|
||||||
|
|
||||||
|
def unregister_by_id(self, _id):
|
||||||
|
self.log.debug(f"unregister_by_id '{_id}'")
|
||||||
|
if not isinstance(_id, int):
|
||||||
|
return
|
||||||
|
s = a = 0
|
||||||
|
for k, funcs in self.__events.items():
|
||||||
|
for f in funcs:
|
||||||
|
if id(f) == _id:
|
||||||
|
s += 1
|
||||||
|
self.__events[k].remove(f)
|
||||||
|
for k, funcs in self.__async_events.items():
|
||||||
|
for f in funcs:
|
||||||
|
if id(f) == _id:
|
||||||
|
a += 1
|
||||||
|
self.__async_events[k].remove(f)
|
||||||
|
self.log.debug(f"unregister in {s + a} events; S:{s}; A:{a};")
|
||||||
|
|
||||||
|
def unregister(self, func):
|
||||||
|
self.log.debug(f"unregister '{func.__name__}' id: {id(func)}")
|
||||||
|
s = a = 0
|
||||||
|
for k, funcs in self.__events.items():
|
||||||
|
for f in funcs:
|
||||||
|
if f == func:
|
||||||
|
s += 1
|
||||||
|
self.__events[k].remove(func)
|
||||||
|
for k, funcs in self.__async_events.items():
|
||||||
|
for f in funcs:
|
||||||
|
if f == func:
|
||||||
|
a += 1
|
||||||
|
self.__async_events[k].remove(func)
|
||||||
|
self.log.debug(f"unregister in {s + a} events; S:{s}; A:{a};")
|
||||||
|
|
||||||
|
def is_event(self, event_name):
|
||||||
|
return (event_name in self.__async_events.keys() or
|
||||||
|
event_name in self.__events.keys() or
|
||||||
|
event_name in self.__lua_events.keys())
|
||||||
|
|
||||||
|
def register(self, event_name, event_func, async_event=False, lua=None, return_id=True):
|
||||||
|
self.log.debug(f"register(event_name='{event_name}', event_func='{event_func.__name__}'(id: {id(event_func)}), "
|
||||||
|
f"async_event={async_event}, lua_event={lua}):")
|
||||||
|
if lua:
|
||||||
|
if event_name not in self.__lua_events:
|
||||||
|
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
|
||||||
|
else:
|
||||||
|
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
|
||||||
|
self.log.debug("Register ok")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not callable(event_func):
|
||||||
|
self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
|
||||||
|
return
|
||||||
|
if async_event or inspect.iscoroutinefunction(event_func):
|
||||||
|
if event_name not in self.__async_events:
|
||||||
|
self.__async_events[event_name] = []
|
||||||
|
self.__async_events[event_name].append(event_func)
|
||||||
|
self.log.debug("Register ok")
|
||||||
|
else:
|
||||||
|
if event_name not in self.__events:
|
||||||
|
self.__events[event_name] = []
|
||||||
|
self.__events[event_name].append(event_func)
|
||||||
|
self.log.debug("Register ok")
|
||||||
|
if return_id:
|
||||||
|
return id(event_func)
|
||||||
|
|
||||||
|
async def call_as_events(self, *args, **kwargs):
|
||||||
|
return await self.call_async_event(*args, **kwargs) + self.call_event(*args, **kwargs)
|
||||||
|
|
||||||
|
async def call_async_event(self, event_name, *args, **kwargs):
|
||||||
|
if not event_name.startswith("serverTick"):
|
||||||
|
self.log.debug(f"Calling async event: '{event_name}'")
|
||||||
|
funcs_data = []
|
||||||
|
if event_name in self.__async_events.keys():
|
||||||
|
for func in self.__async_events[event_name]:
|
||||||
|
try:
|
||||||
|
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
||||||
|
data = await func(event_data)
|
||||||
|
funcs_data.append(data)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
|
||||||
|
self.log.exception(e)
|
||||||
|
elif not self.is_event(event_name):
|
||||||
|
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()"))
|
||||||
|
|
||||||
|
return funcs_data
|
||||||
|
|
||||||
|
def call_event(self, event_name: str, *args, **kwargs):
|
||||||
|
if event_name not in (
|
||||||
|
"onChangePosition", "onSentPing", # UDP events
|
||||||
|
"_get_player"
|
||||||
|
) and not event_name.startswith("serverTick"):
|
||||||
|
self.log.debug(f"Calling sync event: '{event_name}'")
|
||||||
|
funcs_data = []
|
||||||
|
|
||||||
|
if event_name in self.__events.keys():
|
||||||
|
for func in self.__events[event_name]:
|
||||||
|
try:
|
||||||
|
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
||||||
|
funcs_data.append(func(event_data))
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
|
||||||
|
self.log.exception(e)
|
||||||
|
elif not self.is_event(event_name):
|
||||||
|
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()"))
|
||||||
|
|
||||||
|
return funcs_data
|
||||||
|
|
||||||
|
def call_lua_event(self, event_name, *args):
|
||||||
|
self.log.debug(f"Calling lua event: '{event_name}{args}'")
|
||||||
|
funcs_data = []
|
||||||
|
if event_name in self.__lua_events.keys():
|
||||||
|
for data in self.__lua_events[event_name]:
|
||||||
|
lua = data['lua']
|
||||||
|
func_name = data["func_name"]
|
||||||
|
try:
|
||||||
|
func = lua.globals()[func_name]
|
||||||
|
if not func:
|
||||||
|
self.log.warning(i18n.events_lua_function_not_found.format("", func_name))
|
||||||
|
continue
|
||||||
|
fd = func(*args)
|
||||||
|
funcs_data.append(fd)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
|
||||||
|
elif not self.is_event(event_name):
|
||||||
|
self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
|
||||||
|
|
||||||
|
return funcs_data
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Developed by KuiToi Dev
|
|
||||||
# File modules.EventsSystem.events_system.py
|
|
||||||
# Written by: SantaSpeen
|
|
||||||
# Version 1.0
|
|
||||||
# Licence: FPA
|
|
||||||
# (c) kuitoi.su 2023
|
|
||||||
import asyncio
|
|
||||||
import builtins
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
from core import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
class EventsSystem:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# TODO: default events
|
|
||||||
self.log = get_logger("EventsSystem")
|
|
||||||
self.loop = asyncio.get_event_loop()
|
|
||||||
self.as_tasks = []
|
|
||||||
self.__events = {
|
|
||||||
"onServerStarted": [], # No handler
|
|
||||||
"onPlayerSentKey": [], # Only sync, no handler
|
|
||||||
"onPlayerAuthenticated": [], # (!) Only sync, With handler
|
|
||||||
"onPlayerJoin": [], # (!) With handler
|
|
||||||
"onChatReceive": [], # (!) With handler
|
|
||||||
"onCarSpawn": [], # (!) With handler
|
|
||||||
"onCarDelete": [], # (!) With handler (admin allow)
|
|
||||||
"onCarEdited": [], # (!) With handler
|
|
||||||
"onCarReset": [], # No handler
|
|
||||||
"onCarChanged": [], # No handler
|
|
||||||
"onCarFocusMove": [], # No handler
|
|
||||||
"onSentPing": [], # Only sync, no handler
|
|
||||||
"onChangePosition": [], # Only sync, no handler
|
|
||||||
"onPlayerDisconnect": [], # No handler
|
|
||||||
"onServerStopped": [], # No handler
|
|
||||||
}
|
|
||||||
self.__async_events = {
|
|
||||||
"onServerStarted": [],
|
|
||||||
"onPlayerJoin": [],
|
|
||||||
"onChatReceive": [],
|
|
||||||
"onCarSpawn": [],
|
|
||||||
"onCarDelete": [],
|
|
||||||
"onCarEdited": [],
|
|
||||||
"onCarReset": [],
|
|
||||||
"onCarChanged": [],
|
|
||||||
"onCarFocusMove": [],
|
|
||||||
"onPlayerDisconnect": [],
|
|
||||||
"onServerStopped": []
|
|
||||||
}
|
|
||||||
|
|
||||||
self.__lua_events = {
|
|
||||||
"onInit": [], # onServerStarted
|
|
||||||
"onShutdown": [], # onServerStopped
|
|
||||||
"onPlayerAuth": [], # onPlayerAuthenticated
|
|
||||||
"onPlayerConnecting": [], # No
|
|
||||||
"onPlayerJoining": [], # No
|
|
||||||
"onPlayerJoin": [], # onPlayerJoin
|
|
||||||
"onPlayerDisconnect": [], # onPlayerDisconnect
|
|
||||||
"onChatMessage": [], # onChatReceive
|
|
||||||
"onVehicleSpawn": [], # onCarSpawn
|
|
||||||
"onVehicleEdited": [], # onCarEdited
|
|
||||||
"onVehicleDeleted": [], # onCarDelete
|
|
||||||
"onVehicleReset": [], # onCarReset
|
|
||||||
"onFileChanged": [], # TODO lua onFileChanged
|
|
||||||
"onConsoleInput": [], # kt.add_command
|
|
||||||
}
|
|
||||||
|
|
||||||
def builtins_hook(self):
|
|
||||||
self.log.debug("used builtins_hook")
|
|
||||||
builtins.ev = self
|
|
||||||
|
|
||||||
def register_event(self, event_name, event_func, async_event=False, lua=None):
|
|
||||||
self.log.debug(f"register_event(event_name='{event_name}', event_func='{event_func}', "
|
|
||||||
f"async_event={async_event}, lua_event={lua}):")
|
|
||||||
if lua:
|
|
||||||
if event_name not in self.__lua_events:
|
|
||||||
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
|
|
||||||
else:
|
|
||||||
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
|
|
||||||
self.log.debug("Register ok")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not callable(event_func):
|
|
||||||
self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
|
|
||||||
return
|
|
||||||
if async_event or inspect.iscoroutinefunction(event_func):
|
|
||||||
if event_name not in self.__async_events:
|
|
||||||
self.__async_events.update({str(event_name): [event_func]})
|
|
||||||
else:
|
|
||||||
self.__async_events[event_name].append(event_func)
|
|
||||||
self.log.debug("Register ok")
|
|
||||||
else:
|
|
||||||
if event_name not in self.__events:
|
|
||||||
self.__events.update({str(event_name): [event_func]})
|
|
||||||
else:
|
|
||||||
self.__events[event_name].append(event_func)
|
|
||||||
self.log.debug("Register ok")
|
|
||||||
|
|
||||||
async def call_async_event(self, event_name, *args, **kwargs):
|
|
||||||
self.log.debug(f"Calling async event: '{event_name}'")
|
|
||||||
funcs_data = []
|
|
||||||
if event_name in self.__async_events.keys():
|
|
||||||
for func in self.__async_events[event_name]:
|
|
||||||
try:
|
|
||||||
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
|
||||||
data = await func(event_data)
|
|
||||||
funcs_data.append(data)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
|
|
||||||
self.log.exception(e)
|
|
||||||
else:
|
|
||||||
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()"))
|
|
||||||
|
|
||||||
return funcs_data
|
|
||||||
|
|
||||||
def call_event(self, event_name, *args, **kwargs):
|
|
||||||
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
|
|
||||||
self.log.debug(f"Calling sync event: '{event_name}'")
|
|
||||||
funcs_data = []
|
|
||||||
|
|
||||||
if event_name in self.__events.keys():
|
|
||||||
for func in self.__events[event_name]:
|
|
||||||
try:
|
|
||||||
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
|
||||||
funcs_data.append(func(event_data))
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
|
|
||||||
self.log.exception(e)
|
|
||||||
else:
|
|
||||||
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()"))
|
|
||||||
|
|
||||||
return funcs_data
|
|
||||||
|
|
||||||
def call_lua_event(self, event_name, *args):
|
|
||||||
self.log.debug(f"Calling lua event: '{event_name}'")
|
|
||||||
funcs_data = []
|
|
||||||
if event_name in self.__lua_events.keys():
|
|
||||||
for data in self.__lua_events[event_name]:
|
|
||||||
lua = data['lua']
|
|
||||||
func_name = data["func_name"]
|
|
||||||
try:
|
|
||||||
func = lua.globals()[func_name]
|
|
||||||
if not func:
|
|
||||||
self.log.warning(i18n.events_lua_function_not_found.format("", func_name))
|
|
||||||
continue
|
|
||||||
fd = func(*args)
|
|
||||||
funcs_data.append(fd)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
|
|
||||||
else:
|
|
||||||
self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
|
|
||||||
|
|
||||||
return funcs_data
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class EventsSystem:
|
|
||||||
@staticmethod
|
|
||||||
def register_event(event_name, event_func, async_event: bool = False, lua: bool | object = None): ...
|
|
||||||
@staticmethod
|
|
||||||
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
|
|
||||||
@staticmethod
|
|
||||||
def call_event(event_name, *data, **kwargs) -> list[Any]: ...
|
|
||||||
@staticmethod
|
|
||||||
def call_lua_event(event_name, *data) -> list[Any]: ...
|
|
||||||
class ev(EventsSystem): ...
|
|
||||||
19
src/modules/EventsSystem/readme.md
Normal file
19
src/modules/EventsSystem/readme.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
### Builtins
|
||||||
|
|
||||||
|
```python
|
||||||
|
class EventsSystem:
|
||||||
|
@staticmethod
|
||||||
|
def unregister_by_id(_id: int) -> None: ...
|
||||||
|
@staticmethod
|
||||||
|
def unregister(func: Callable | Awaitable): ...
|
||||||
|
@staticmethod
|
||||||
|
def register(event_name: str, event_func: Callable | Awaitable, async_event: bool = False, lua: bool | object = None) -> None | int: ...
|
||||||
|
@staticmethod
|
||||||
|
async def call_as_events(event_name: str, *args, **kwargs) -> list[Any]: ...
|
||||||
|
@staticmethod
|
||||||
|
async def call_async_event(event_name: str, *args, **kwargs) -> list[Any]: ...
|
||||||
|
@staticmethod
|
||||||
|
def call_event(event_name: str, *data, **kwargs) -> list[Any]: ...
|
||||||
|
@staticmethod
|
||||||
|
def call_lua_event(event_name: str, *data) -> list[Any]: ...
|
||||||
|
```
|
||||||
98
src/modules/PermsSystem/__init__.py
Normal file
98
src/modules/PermsSystem/__init__.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Developed by KuiToi Dev
|
||||||
|
# File modules.PermsSystem
|
||||||
|
# Written by: SantaSpeen
|
||||||
|
# Version 1.0
|
||||||
|
# Licence: FPA
|
||||||
|
# (c) kuitoi.su 2024
|
||||||
|
from core import get_logger
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
class PermsSystem:
|
||||||
|
_db_name = "users.db3"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.log = get_logger("PermsSystem")
|
||||||
|
self._create_base()
|
||||||
|
self._completer_permissions = Completer({})
|
||||||
|
# set <permission | group> | unset <permission | group>
|
||||||
|
self._completer_group = Completer({}) # <group_name> info | permission
|
||||||
|
|
||||||
|
_completer_after_user = Completer({
|
||||||
|
"info": None,
|
||||||
|
"permission": {"set": self._completer_permissions, "unset": self._completer_permissions}
|
||||||
|
})
|
||||||
|
self._completer_user = Completer({}, on_none=_completer_after_user) # <nick> info | permission
|
||||||
|
ev.register("add_perm_to_alias", lambda ev: self._completer_permissions.options.update({ev['args'][0]: None}))
|
||||||
|
|
||||||
|
ev.call_event("add_perm_to_alias", "cmd.perms")
|
||||||
|
console.add_command("perms", self._parse_console,
|
||||||
|
None,
|
||||||
|
"Permission module",
|
||||||
|
{"perms": {
|
||||||
|
"groups": {
|
||||||
|
"create": None,
|
||||||
|
"delete": None,
|
||||||
|
"list": None
|
||||||
|
},
|
||||||
|
"user": self._completer_user,
|
||||||
|
"group": self._completer_group,
|
||||||
|
"reload": None,
|
||||||
|
}})
|
||||||
|
ev.register("onChatReceive", self._parse_chat)
|
||||||
|
ev.register("onPlayerJoin", self._process_new_player)
|
||||||
|
|
||||||
|
def _create_base(self):
|
||||||
|
con = sqlite3.connect(self._db_name)
|
||||||
|
cursor = con.cursor()
|
||||||
|
|
||||||
|
# Create table for users
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mp_id INTEGER UNIQUE,
|
||||||
|
nick TEXT NOT NULL,
|
||||||
|
playtime INTEGER
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Create table for perms
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS perms (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mp_id INTEGER,
|
||||||
|
rule TEXT,
|
||||||
|
`group` TEXT,
|
||||||
|
FOREIGN KEY(mp_id) REFERENCES users(mp_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Create table for groups
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS groups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT UNIQUE NOT NULL,
|
||||||
|
rules TEXT NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
def _parse_console(self, x):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _parse_chat(self, ev):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_player(self, player):
|
||||||
|
self._completer_user.options.update({player.nick: None})
|
||||||
|
self.log.debug(f'Added user: {player.nick}')
|
||||||
|
|
||||||
|
def have_permission(self, ev):
|
||||||
|
player = ev['kwargs']['player']
|
||||||
|
|
||||||
|
def _process_new_player(self, ev):
|
||||||
|
player = ev['kwargs']['player']
|
||||||
|
self.add_player(player)
|
||||||
@@ -1 +1,384 @@
|
|||||||
from .plugins_loader import PluginsLoader
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Developed by KuiToi Dev
|
||||||
|
# File modules.PluginsLoader
|
||||||
|
# Written by: SantaSpeen
|
||||||
|
# Version 1.1
|
||||||
|
# Licence: FPA
|
||||||
|
# (c) kuitoi.su 2023
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from prompt_toolkit import PromptSession, HTML
|
||||||
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||||
|
from prompt_toolkit.history import FileHistory
|
||||||
|
from prompt_toolkit.lexers import PygmentsLexer
|
||||||
|
try:
|
||||||
|
from pygments.lexers.python import Python3Lexer
|
||||||
|
except ImportError:
|
||||||
|
print("ImportError: Python3Lexer")
|
||||||
|
exit(1)
|
||||||
|
from core import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
class KuiToi:
|
||||||
|
_plugins_dir = ""
|
||||||
|
_file = ""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
if not name:
|
||||||
|
raise AttributeError("KuiToi: Name is required")
|
||||||
|
self.__log = get_logger(f"Plugin | {name}")
|
||||||
|
self.__name = name
|
||||||
|
self.__dir = Path(self._plugins_dir) / self.__name
|
||||||
|
os.makedirs(self.__dir, exist_ok=True)
|
||||||
|
self.__funcs = []
|
||||||
|
self.register_event = self.register
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log(self):
|
||||||
|
return self.__log
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dir(self):
|
||||||
|
return self.__dir
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
|
||||||
|
path = self.__dir / file
|
||||||
|
if str(self.__dir) in str(file):
|
||||||
|
path = file
|
||||||
|
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
|
||||||
|
# Really need?
|
||||||
|
# if not os.path.exists(path):
|
||||||
|
# with open(path, 'x'): ...
|
||||||
|
f = None
|
||||||
|
try:
|
||||||
|
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
|
||||||
|
yield f
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def register(self, event_name, event_func):
|
||||||
|
self.log.debug(f"Registering event {event_name}")
|
||||||
|
_id = ev.register(event_name, event_func)
|
||||||
|
self.__funcs.append(_id)
|
||||||
|
|
||||||
|
def _unload(self):
|
||||||
|
for f in self.__funcs:
|
||||||
|
console.del_command(f)
|
||||||
|
ev.unregister_by_id(f)
|
||||||
|
|
||||||
|
def call_event(self, event_name, *args, **kwargs):
|
||||||
|
self.log.debug(f"Called event {event_name}")
|
||||||
|
return ev.call_event(event_name, *args, **kwargs)
|
||||||
|
|
||||||
|
async def call_async_event(self, event_name, *args, **kwargs):
|
||||||
|
self.log.debug(f"Called async event {event_name}")
|
||||||
|
return await ev.call_async_event(event_name, *args, **kwargs)
|
||||||
|
|
||||||
|
def call_lua_event(self, event_name, *args):
|
||||||
|
self.log.debug(f"Called lua event {event_name}")
|
||||||
|
return ev.call_lua_event(event_name, *args)
|
||||||
|
|
||||||
|
def get_player(self, pid=None, nick=None, cid=None):
|
||||||
|
# self.log.debug("Requests get_player")
|
||||||
|
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
|
||||||
|
|
||||||
|
def get_players(self):
|
||||||
|
# self.log.debug("Requests get_players")
|
||||||
|
return self.get_player(-1)
|
||||||
|
|
||||||
|
def players_counter(self):
|
||||||
|
self.log.debug("Requests players_counter")
|
||||||
|
return len(self.get_players())
|
||||||
|
|
||||||
|
def is_player_connected(self, pid=None, nick=None):
|
||||||
|
self.log.debug("Requests is_player_connected")
|
||||||
|
if pid < 0:
|
||||||
|
return False
|
||||||
|
return bool(self.get_player(cid=pid, nick=nick))
|
||||||
|
|
||||||
|
def add_command(self, key, func, man, desc, custom_completer) -> dict:
|
||||||
|
self.log.debug("Requests add_command")
|
||||||
|
self.__funcs.append(func)
|
||||||
|
return console.add_command(key, func, man, desc, custom_completer)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginsLoader:
|
||||||
|
_pip_dir = str(Path("pip-packets").resolve())
|
||||||
|
|
||||||
|
def __init__(self, plugins_dir):
|
||||||
|
self.loop = asyncio.get_event_loop()
|
||||||
|
self.plugins = {}
|
||||||
|
self.plugins_tasks = []
|
||||||
|
self.plugins_dir = plugins_dir
|
||||||
|
self.log = get_logger("PluginsLoader")
|
||||||
|
self.loaded = []
|
||||||
|
self.pl_completer = Completer({})
|
||||||
|
self.pl_files_completer = Completer({})
|
||||||
|
self._scan_dir(None)
|
||||||
|
ev.register("serverTick_5s", self._scan_dir)
|
||||||
|
ev.register("_plugins_start", self.start)
|
||||||
|
ev.register("_plugins_unload", self.unload)
|
||||||
|
ev.register("_plugins_get",
|
||||||
|
lambda _: "Plugins: " + ", ".join(f"{i[0]}:{'on' if i[1] else 'off'}" for i in self.loaded))
|
||||||
|
console.add_command("plugins", self._parse_console, None, "Plugins manipulations",
|
||||||
|
{"plugins": {
|
||||||
|
"reload": self.pl_completer,
|
||||||
|
"load": self.pl_files_completer,
|
||||||
|
"unload": self.pl_completer,
|
||||||
|
"list": None,
|
||||||
|
}})
|
||||||
|
console.add_command("plugin", self._plugin_console, None, "plugin console", {"plugin": self.pl_completer})
|
||||||
|
sys.path.append(self._pip_dir)
|
||||||
|
os.makedirs(self._pip_dir, exist_ok=True)
|
||||||
|
console.add_command("install", self._pip_install)
|
||||||
|
|
||||||
|
def _scan_dir(self, _):
|
||||||
|
_load = {}
|
||||||
|
for file in os.listdir(self.plugins_dir):
|
||||||
|
file_path = os.path.join(self.plugins_dir, file)
|
||||||
|
if os.path.isfile(file_path) and file.endswith(".py"):
|
||||||
|
_load[file] = None
|
||||||
|
self.pl_files_completer.load(_load)
|
||||||
|
|
||||||
|
async def _plugin_console(self, x):
|
||||||
|
usage = 'Usage: plugin <name>'
|
||||||
|
if not x:
|
||||||
|
return usage
|
||||||
|
if x[0] in self.plugins:
|
||||||
|
plugin = self.plugins[x[0]]['plugin']
|
||||||
|
kt: KuiToi = plugin.kt
|
||||||
|
work = True
|
||||||
|
session = None
|
||||||
|
if not console.legacy_mode:
|
||||||
|
session = PromptSession(history=FileHistory(f'{kt.dir}/.cmdhistory'), lexer=PygmentsLexer(Python3Lexer))
|
||||||
|
|
||||||
|
def bottom_toolbar():
|
||||||
|
x = lambda x: f'<b><style bg="ansired">{x}</style></b>'
|
||||||
|
c = lambda c: f'<style fg="#b3d6f4">{x(c)}</style>'
|
||||||
|
return HTML(f'[PluginConsole KuiToi@{x(kt.name)}] {c("^D")} Return to the main console {c("^C")} Exit ')
|
||||||
|
while work:
|
||||||
|
try:
|
||||||
|
if session:
|
||||||
|
inp = await session.prompt_async(">> ", auto_suggest=AutoSuggestFromHistory(), bottom_toolbar=bottom_toolbar)
|
||||||
|
else:
|
||||||
|
inp = input(f"@{kt.name} > ")
|
||||||
|
self.log.debug(f"[_plugin_console] {inp=}")
|
||||||
|
if inp == "exit":
|
||||||
|
return "Exited"
|
||||||
|
if not inp:
|
||||||
|
continue
|
||||||
|
if inp.split(' ')[0] in ['import', 'from']:
|
||||||
|
kt.log.warning("Imports not allowed here... Sorry bro.")
|
||||||
|
continue
|
||||||
|
code = textwrap.dedent(f"""\
|
||||||
|
async def _console():
|
||||||
|
try:
|
||||||
|
i = {inp}
|
||||||
|
if i:
|
||||||
|
print(f"{{i!r}}")
|
||||||
|
except Exception as e:
|
||||||
|
kt.log.exception(e)""")
|
||||||
|
exec(code, plugin.__dict__)
|
||||||
|
kt.log.debug(await plugin._console())
|
||||||
|
except SyntaxError as e:
|
||||||
|
kt.log.error(f"SyntaxError: {e.msg}")
|
||||||
|
except EOFError:
|
||||||
|
return
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
|
raise e
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
kt.log.exception(e)
|
||||||
|
return "Plugin not found"
|
||||||
|
|
||||||
|
async def _parse_console(self, x):
|
||||||
|
usage = 'Usage: plugins [reload <name> | load <file.py> | unload <name> | list]'
|
||||||
|
if not x:
|
||||||
|
return usage
|
||||||
|
match x[0]:
|
||||||
|
case 'reload':
|
||||||
|
if len(x) == 2:
|
||||||
|
t1 = time.monotonic()
|
||||||
|
ok, _, file, _ = await self._unload_by_name(x[1], True)
|
||||||
|
if ok:
|
||||||
|
if await self._load_by_file(file):
|
||||||
|
self.plugins[x[1]]['plugin'].start()
|
||||||
|
return f"Plugin reloaded ({time.monotonic() - t1:.1f}sec)"
|
||||||
|
return "Plugin not found"
|
||||||
|
return usage
|
||||||
|
case 'load':
|
||||||
|
if len(x) == 2:
|
||||||
|
name = await self._load_by_file(x[1])
|
||||||
|
if name:
|
||||||
|
self.plugins[name]['plugin'].start()
|
||||||
|
return "Plugin loaded"
|
||||||
|
return usage
|
||||||
|
case 'unload':
|
||||||
|
if len(x) == 2:
|
||||||
|
ok, _, _, _ = await self._unload_by_name(x[1], True)
|
||||||
|
if ok:
|
||||||
|
return "Plugin unloaded"
|
||||||
|
return usage
|
||||||
|
case 'list':
|
||||||
|
return ev.call_event("_plugins_get")[0]
|
||||||
|
return usage
|
||||||
|
|
||||||
|
def _pip_install(self, x):
|
||||||
|
self.log.debug(f"_pip_install {x}")
|
||||||
|
if len(x) > 0:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(['pip', 'install', *x, '--target', self._pip_dir])
|
||||||
|
return "Success"
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
self.log.debug(f"error: {e}")
|
||||||
|
return f"Failed to install packages"
|
||||||
|
else:
|
||||||
|
return "Invalid syntax"
|
||||||
|
|
||||||
|
async def _load_by_file(self, file):
|
||||||
|
file_path = os.path.join(self.plugins_dir, file)
|
||||||
|
if os.path.isfile(file_path) and file.endswith(".py"):
|
||||||
|
try:
|
||||||
|
self.log.info(f"Loading plugin: {file[:-3]}")
|
||||||
|
plugin = types.ModuleType(file[:-3])
|
||||||
|
plugin.KuiToi = KuiToi
|
||||||
|
plugin.KuiToi._plugins_dir = self.plugins_dir
|
||||||
|
plugin.KuiToi._file = file
|
||||||
|
plugin.print = print
|
||||||
|
plugin.__file__ = file_path
|
||||||
|
with open(f'{file_path}', 'r', encoding=config.enc) as f:
|
||||||
|
code = f.read()
|
||||||
|
exec(code, plugin.__dict__)
|
||||||
|
|
||||||
|
ok = True
|
||||||
|
try:
|
||||||
|
is_func = inspect.isfunction
|
||||||
|
if not is_func(plugin.load):
|
||||||
|
self.log.error(i18n.plugins_not_found_load)
|
||||||
|
ok = False
|
||||||
|
if not is_func(plugin.start):
|
||||||
|
self.log.error(i18n.plugins_not_found_start)
|
||||||
|
ok = False
|
||||||
|
if not is_func(plugin.unload):
|
||||||
|
self.log.error(i18n.plugins_not_found_unload)
|
||||||
|
ok = False
|
||||||
|
if type(plugin.kt) != KuiToi:
|
||||||
|
self.log.error(i18n.plugins_kt_invalid)
|
||||||
|
ok = False
|
||||||
|
except AttributeError:
|
||||||
|
ok = False
|
||||||
|
if not ok:
|
||||||
|
self.log.error(i18n.plugins_invalid.format(file_path))
|
||||||
|
return
|
||||||
|
|
||||||
|
pl_name = plugin.kt.name
|
||||||
|
if self.plugins.get(pl_name) is not None:
|
||||||
|
raise NameError(f'Having plugins with identical names is not allowed; '
|
||||||
|
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
|
||||||
|
|
||||||
|
plugin.open = plugin.kt.open
|
||||||
|
is_coro_func = inspect.iscoroutinefunction
|
||||||
|
self.plugins.update(
|
||||||
|
{
|
||||||
|
pl_name: {
|
||||||
|
"plugin": plugin,
|
||||||
|
"load": {
|
||||||
|
"func": plugin.load,
|
||||||
|
"async": is_coro_func(plugin.load)
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"func": plugin.start,
|
||||||
|
"async": is_coro_func(plugin.start)
|
||||||
|
},
|
||||||
|
"unload": {
|
||||||
|
"func": plugin.unload,
|
||||||
|
"async": is_coro_func(plugin.unload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if self.plugins[pl_name]["load"]['async']:
|
||||||
|
plugin.log.debug(f"I'm async")
|
||||||
|
await plugin.load()
|
||||||
|
else:
|
||||||
|
plugin.log.debug(f"I'm sync")
|
||||||
|
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
|
||||||
|
th.start()
|
||||||
|
th.join()
|
||||||
|
self.pl_completer.options[pl_name] = None
|
||||||
|
self.loaded.append((pl_name, True))
|
||||||
|
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
|
||||||
|
return pl_name
|
||||||
|
except Exception as e:
|
||||||
|
self.loaded.append((file, False))
|
||||||
|
self.log.error(i18n.plugins_error_loading.format(file, f"{e}"))
|
||||||
|
self.log.exception(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def load(self):
|
||||||
|
self.log.debug("Loading plugins...")
|
||||||
|
for file in os.listdir(self.plugins_dir):
|
||||||
|
await self._load_by_file(file)
|
||||||
|
|
||||||
|
async def _unload_by_name(self, name, reload=False):
|
||||||
|
t1 = time.monotonic()
|
||||||
|
data = self.plugins.get(name)
|
||||||
|
if not data:
|
||||||
|
return False, name, None, None
|
||||||
|
try:
|
||||||
|
if reload:
|
||||||
|
data['plugin'].kt._unload()
|
||||||
|
self.loaded.remove((name, True))
|
||||||
|
self.plugins.pop(name)
|
||||||
|
if data['unload']['async']:
|
||||||
|
self.log.debug(f"Unload async plugin: {name}")
|
||||||
|
await data['unload']['func']()
|
||||||
|
else:
|
||||||
|
self.log.debug(f"Unload sync plugin: {name}")
|
||||||
|
th = Thread(target=data['unload']['func'], name=f"Thread {name}")
|
||||||
|
th.start()
|
||||||
|
th.join()
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception(e)
|
||||||
|
return True, name, data['plugin'].kt._file, time.monotonic() - t1
|
||||||
|
|
||||||
|
async def start(self, _):
|
||||||
|
for pl_name, pl_data in self.plugins.items():
|
||||||
|
try:
|
||||||
|
func = pl_data['start']['func']
|
||||||
|
if pl_data['start']['async']:
|
||||||
|
self.log.debug(f"Start async plugin: {pl_name}")
|
||||||
|
t = self.loop.create_task(func())
|
||||||
|
else:
|
||||||
|
self.log.debug(f"Start sync plugin: {pl_name}")
|
||||||
|
t = self.loop.create_task(asyncio.to_thread(func))
|
||||||
|
self.plugins_tasks.append(t)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception(e)
|
||||||
|
|
||||||
|
async def unload(self, _):
|
||||||
|
t = []
|
||||||
|
for n in self.plugins.keys():
|
||||||
|
t.append(self._unload_by_name(n))
|
||||||
|
self.log.debug(await asyncio.gather(*t))
|
||||||
|
self.log.debug("Plugins unloaded")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import platform
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ class MP:
|
|||||||
|
|
||||||
def RegisterEvent(self, event_name: str, function_name: str) -> None:
|
def RegisterEvent(self, event_name: str, function_name: str) -> None:
|
||||||
self.log.debug("request MP.RegisterEvent()")
|
self.log.debug("request MP.RegisterEvent()")
|
||||||
ev.register_event(event_name, function_name, lua=self._lua)
|
ev.register(event_name, function_name, lua=self._lua)
|
||||||
if event_name not in self._local_events:
|
if event_name not in self._local_events:
|
||||||
self._local_events.update({str(event_name): [function_name]})
|
self._local_events.update({str(event_name): [function_name]})
|
||||||
else:
|
else:
|
||||||
@@ -109,8 +110,9 @@ class MP:
|
|||||||
del self._event_timers[event_name]
|
del self._event_timers[event_name]
|
||||||
|
|
||||||
def TriggerLocalEvent(self, event_name, *args):
|
def TriggerLocalEvent(self, event_name, *args):
|
||||||
|
if event_name != "getTable":
|
||||||
self.log.debug("request TriggerLocalEvent()")
|
self.log.debug("request TriggerLocalEvent()")
|
||||||
self.log.debug(f"Calling local lua event: '{event_name}'")
|
self.log.debug(f"Calling local lua event: '{event_name}{args}'")
|
||||||
funcs_data = []
|
funcs_data = []
|
||||||
if event_name in self._local_events.keys():
|
if event_name in self._local_events.keys():
|
||||||
for func_name in self._local_events[event_name]:
|
for func_name in self._local_events[event_name]:
|
||||||
@@ -157,7 +159,7 @@ class MP:
|
|||||||
to_all = False
|
to_all = False
|
||||||
if player_id < 0:
|
if player_id < 0:
|
||||||
to_all = True
|
to_all = True
|
||||||
client = client[0]
|
client = client[0] if len(client) > 0 else None
|
||||||
if client and event_name and data:
|
if client and event_name and data:
|
||||||
t = self.loop.create_task(client.send_event(event_name, data, to_all=to_all))
|
t = self.loop.create_task(client.send_event(event_name, data, to_all=to_all))
|
||||||
self.tasks.append(t)
|
self.tasks.append(t)
|
||||||
@@ -168,7 +170,7 @@ class MP:
|
|||||||
return False, "Can't found event_name or data"
|
return False, "Can't found event_name or data"
|
||||||
|
|
||||||
def TriggerClientEventJson(self, player_id, event_name, data):
|
def TriggerClientEventJson(self, player_id, event_name, data):
|
||||||
self.log.debug("request TriggerClientEventJson()")
|
self.log.debug(f"request TriggerClientEventJson({player_id, event_name, data})")
|
||||||
data = self._lua.globals().Util.JsonEncode(data)
|
data = self._lua.globals().Util.JsonEncode(data)
|
||||||
self.TriggerClientEvent(player_id, event_name, data)
|
self.TriggerClientEvent(player_id, event_name, data)
|
||||||
|
|
||||||
@@ -232,8 +234,8 @@ class MP:
|
|||||||
|
|
||||||
def GetPlayers(self):
|
def GetPlayers(self):
|
||||||
self.log.debug("request GetPlayers()")
|
self.log.debug("request GetPlayers()")
|
||||||
clients = ev.call_event("_get_players", cid=-1)
|
clients = ev.call_event("_get_player", cid=-1)[0]
|
||||||
return self._lua.table_from(clients)
|
return self._lua.table_from([i.nick for i in clients])
|
||||||
|
|
||||||
def IsPlayerGuest(self, player_id) -> bool:
|
def IsPlayerGuest(self, player_id) -> bool:
|
||||||
self.log.debug("request IsPlayerGuest()")
|
self.log.debug("request IsPlayerGuest()")
|
||||||
@@ -312,12 +314,18 @@ class Util:
|
|||||||
return {k: v for k, v in new_dict.items() if v is not None}
|
return {k: v for k, v in new_dict.items() if v is not None}
|
||||||
|
|
||||||
def JsonEncode(self, table):
|
def JsonEncode(self, table):
|
||||||
|
data = {}
|
||||||
|
try:
|
||||||
self.log.debug("requesting JsonEncode()")
|
self.log.debug("requesting JsonEncode()")
|
||||||
if all(isinstance(k, int) for k in table.keys()):
|
if all(isinstance(k, int) for k in table.keys()):
|
||||||
data = self._recursive_list_encode(table)
|
data = self._recursive_list_encode(table)
|
||||||
else:
|
else:
|
||||||
data = self._recursive_dict_encode(table)
|
data = self._recursive_dict_encode(table)
|
||||||
return json.dumps(data)
|
except Exception as e:
|
||||||
|
self.log.exception(e)
|
||||||
|
data = json.dumps(data)
|
||||||
|
self.log.debug(f"Encoded: {data}")
|
||||||
|
return data
|
||||||
|
|
||||||
def JsonDecode(self, string):
|
def JsonDecode(self, string):
|
||||||
self.log.debug("requesting JsonDecode()")
|
self.log.debug("requesting JsonDecode()")
|
||||||
@@ -570,8 +578,8 @@ class LuaPluginsLoader:
|
|||||||
self.lua_dirs = set()
|
self.lua_dirs = set()
|
||||||
self.log = get_logger("LuaPluginsLoader")
|
self.log = get_logger("LuaPluginsLoader")
|
||||||
self.loaded_str = "Lua plugins: "
|
self.loaded_str = "Lua plugins: "
|
||||||
ev.register_event("_lua_plugins_get", lambda x: self.lua_plugins)
|
ev.register("_lua_plugins_get", lambda x: self.lua_plugins)
|
||||||
ev.register_event("_lua_plugins_unload", self.unload)
|
ev.register("_lua_plugins_unload", self.unload)
|
||||||
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
|
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
|
||||||
console.add_command("lua_pl", lambda x: self.loaded_str[:-2])
|
console.add_command("lua_pl", lambda x: self.loaded_str[:-2])
|
||||||
|
|
||||||
@@ -591,7 +599,7 @@ class LuaPluginsLoader:
|
|||||||
"LogChat": config.Options['log_chat'],
|
"LogChat": config.Options['log_chat'],
|
||||||
"Debug": config.Options['debug'],
|
"Debug": config.Options['debug'],
|
||||||
"Private": config.Auth['private'],
|
"Private": config.Auth['private'],
|
||||||
"MaxCars": config.Game['max_cars'],
|
"MaxCars": config.Game['cars'],
|
||||||
"MaxPlayers": config.Game['players'],
|
"MaxPlayers": config.Game['players'],
|
||||||
"Map": f"/levels/{config.Game['map']}/info.json",
|
"Map": f"/levels/{config.Game['map']}/info.json",
|
||||||
"Description": config.Server['description'],
|
"Description": config.Server['description'],
|
||||||
@@ -601,14 +609,6 @@ class LuaPluginsLoader:
|
|||||||
"ImScaredOfUpdates": False,
|
"ImScaredOfUpdates": False,
|
||||||
"SendErrorsShowMessage": False,
|
"SendErrorsShowMessage": False,
|
||||||
"SendErrors": False
|
"SendErrors": False
|
||||||
},
|
|
||||||
"HTTP": {
|
|
||||||
"HTTPServerIP": config.WebAPI['server_ip'],
|
|
||||||
"HTTPServerPort": config.WebAPI['server_port'],
|
|
||||||
"SSLKeyPath": None,
|
|
||||||
"SSLCertPath": None,
|
|
||||||
"UseSSL": False,
|
|
||||||
"HTTPServerEnabled": config.WebAPI['enabled'],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with open("ServerConfig.toml", "w") as f:
|
with open("ServerConfig.toml", "w") as f:
|
||||||
@@ -641,7 +641,11 @@ class LuaPluginsLoader:
|
|||||||
p0 = os.path.join(pa, name, "?.lua")
|
p0 = os.path.join(pa, name, "?.lua")
|
||||||
p1 = os.path.join(pa, name, "lua", "?.lua")
|
p1 = os.path.join(pa, name, "lua", "?.lua")
|
||||||
lua_globals.package.path += f';{p0};{p1}'
|
lua_globals.package.path += f';{p0};{p1}'
|
||||||
with open("modules/PluginsLoader/add_in.lua", "r") as f:
|
try:
|
||||||
|
_file = os.path.join(sys._MEIPASS, "add_in.lua")
|
||||||
|
except AttributeError:
|
||||||
|
_file = "modules/PluginsLoader/add_in.lua"
|
||||||
|
with open(_file, "r") as f:
|
||||||
lua.execute(f.read())
|
lua.execute(f.read())
|
||||||
self.lua_plugins.update({name: {"lua": lua, "ok": False}})
|
self.lua_plugins.update({name: {"lua": lua, "ok": False}})
|
||||||
plugin_path = os.path.join(self.plugins_dir, name)
|
plugin_path = os.path.join(self.plugins_dir, name)
|
||||||
@@ -664,10 +668,15 @@ class LuaPluginsLoader:
|
|||||||
self.log.error(f"Exception onInit from `{name}`: {e}")
|
self.log.error(f"Exception onInit from `{name}`: {e}")
|
||||||
self.log.exception(e)
|
self.log.exception(e)
|
||||||
|
|
||||||
def unload(self, _):
|
async def unload(self, _):
|
||||||
self.log.debug("Unloading lua plugins")
|
self.log.debug("Unloading lua plugins")
|
||||||
for name, data in self.lua_plugins.items():
|
for name, data in self.lua_plugins.items():
|
||||||
if data['ok']:
|
if data['ok']:
|
||||||
self.log.info(i18n.plugins_lua_unload.format(name))
|
self.log.info(i18n.plugins_lua_unload.format(name))
|
||||||
for _, timer in data['lua'].globals().MP._event_timers.items():
|
MP = data['lua'].globals().MP
|
||||||
|
self.log.debug("gather")
|
||||||
|
await asyncio.gather(*MP.tasks)
|
||||||
|
self.log.debug("timers")
|
||||||
|
for _, timer in MP._event_timers.items():
|
||||||
timer.stop()
|
timer.stop()
|
||||||
|
self.log.debug("unloaded")
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Developed by KuiToi Dev
|
|
||||||
# File modules.PluginsLoader.plugins_loader.py
|
|
||||||
# Written by: SantaSpeen
|
|
||||||
# Version 1.0
|
|
||||||
# Licence: FPA
|
|
||||||
# (c) kuitoi.su 2023
|
|
||||||
import asyncio
|
|
||||||
import inspect
|
|
||||||
import os
|
|
||||||
import types
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from core import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class KuiToi:
|
|
||||||
_plugins_dir = ""
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
if name is None:
|
|
||||||
raise AttributeError("KuiToi: Name is required")
|
|
||||||
self.__log = get_logger(f"Plugin | {name}")
|
|
||||||
self.__name = name
|
|
||||||
self.__dir = os.path.join(self._plugins_dir, self.__name)
|
|
||||||
if not os.path.exists(self.__dir):
|
|
||||||
os.mkdir(self.__dir)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def log(self):
|
|
||||||
return self.__log
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.__name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dir(self):
|
|
||||||
return self.__dir
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
|
|
||||||
path = os.path.join(self.__dir, file)
|
|
||||||
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
|
|
||||||
# Really need?
|
|
||||||
# if not os.path.exists(path):
|
|
||||||
# with open(path, 'x'): ...
|
|
||||||
f = None
|
|
||||||
try:
|
|
||||||
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
|
|
||||||
yield f
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
finally:
|
|
||||||
if f is not None:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def register_event(self, event_name, event_func):
|
|
||||||
self.log.debug(f"Registering event {event_name}")
|
|
||||||
ev.register_event(event_name, event_func)
|
|
||||||
|
|
||||||
def call_event(self, event_name, *args, **kwargs):
|
|
||||||
self.log.debug(f"Called event {event_name}")
|
|
||||||
return ev.call_event(event_name, *args, **kwargs)
|
|
||||||
|
|
||||||
async def call_async_event(self, event_name, *args, **kwargs):
|
|
||||||
self.log.debug(f"Called async event {event_name}")
|
|
||||||
return await ev.call_async_event(event_name, *args, **kwargs)
|
|
||||||
|
|
||||||
def call_lua_event(self, event_name, *args):
|
|
||||||
self.log.debug(f"Called lua event {event_name}")
|
|
||||||
return ev.call_lua_event(event_name, *args)
|
|
||||||
|
|
||||||
def get_player(self, pid=None, nick=None, cid=None):
|
|
||||||
self.log.debug("Requests get_player")
|
|
||||||
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
|
|
||||||
|
|
||||||
def get_players(self):
|
|
||||||
self.log.debug("Requests get_players")
|
|
||||||
return self.get_player(-1)
|
|
||||||
|
|
||||||
def players_counter(self):
|
|
||||||
self.log.debug("Requests players_counter")
|
|
||||||
return len(self.get_players())
|
|
||||||
|
|
||||||
def is_player_connected(self, pid=None, nick=None):
|
|
||||||
self.log.debug("Requests is_player_connected")
|
|
||||||
if pid < 0:
|
|
||||||
return False
|
|
||||||
return bool(self.get_player(cid=pid, nick=nick))
|
|
||||||
|
|
||||||
def add_command(self, key, func, man, desc, custom_completer) -> dict:
|
|
||||||
self.log.debug("Requests add_command")
|
|
||||||
return console.add_command(key, func, man, desc, custom_completer)
|
|
||||||
|
|
||||||
|
|
||||||
class PluginsLoader:
|
|
||||||
|
|
||||||
def __init__(self, plugins_dir):
|
|
||||||
self.loop = asyncio.get_event_loop()
|
|
||||||
self.plugins = {}
|
|
||||||
self.plugins_tasks = []
|
|
||||||
self.plugins_dir = plugins_dir
|
|
||||||
self.log = get_logger("PluginsLoader")
|
|
||||||
self.loaded_str = "Plugins: "
|
|
||||||
ev.register_event("_plugins_start", self.start)
|
|
||||||
ev.register_event("_plugins_unload", self.unload)
|
|
||||||
ev.register_event("_plugins_get", lambda x: list(self.plugins.keys()))
|
|
||||||
console.add_command("plugins", lambda x: self.loaded_str[:-2])
|
|
||||||
console.add_command("pl", lambda x: self.loaded_str[:-2])
|
|
||||||
|
|
||||||
async def load(self):
|
|
||||||
self.log.debug("Loading plugins...")
|
|
||||||
for file in os.listdir(self.plugins_dir):
|
|
||||||
file_path = os.path.join(self.plugins_dir, file)
|
|
||||||
if os.path.isfile(file_path) and file.endswith(".py"):
|
|
||||||
try:
|
|
||||||
self.log.debug(f"Loading plugin: {file[:-3]}")
|
|
||||||
plugin = types.ModuleType(file[:-3])
|
|
||||||
plugin.KuiToi = KuiToi
|
|
||||||
plugin.KuiToi._plugins_dir = self.plugins_dir
|
|
||||||
plugin.print = print
|
|
||||||
plugin.__file__ = file_path
|
|
||||||
with open(f'{file_path}', 'r', encoding=config.enc) as f:
|
|
||||||
code = f.read()
|
|
||||||
exec(code, plugin.__dict__)
|
|
||||||
|
|
||||||
ok = True
|
|
||||||
try:
|
|
||||||
is_func = inspect.isfunction
|
|
||||||
if not is_func(plugin.load):
|
|
||||||
self.log.error(i18n.plugins_not_found_load)
|
|
||||||
ok = False
|
|
||||||
if not is_func(plugin.start):
|
|
||||||
self.log.error(i18n.plugins_not_found_start)
|
|
||||||
ok = False
|
|
||||||
if not is_func(plugin.unload):
|
|
||||||
self.log.error(i18n.plugins_not_found_unload)
|
|
||||||
ok = False
|
|
||||||
if type(plugin.kt) != KuiToi:
|
|
||||||
self.log.error(i18n.plugins_kt_invalid)
|
|
||||||
ok = False
|
|
||||||
except AttributeError:
|
|
||||||
ok = False
|
|
||||||
if not ok:
|
|
||||||
self.log.error(i18n.plugins_invalid.format(file_path))
|
|
||||||
return
|
|
||||||
|
|
||||||
pl_name = plugin.kt.name
|
|
||||||
if self.plugins.get(pl_name) is not None:
|
|
||||||
raise NameError(f'Having plugins with identical names is not allowed; '
|
|
||||||
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
|
|
||||||
|
|
||||||
plugin.open = plugin.kt.open
|
|
||||||
is_coro_func = inspect.iscoroutinefunction
|
|
||||||
self.plugins.update(
|
|
||||||
{
|
|
||||||
pl_name: {
|
|
||||||
"plugin": plugin,
|
|
||||||
"load": {
|
|
||||||
"func": plugin.load,
|
|
||||||
"async": is_coro_func(plugin.load)
|
|
||||||
},
|
|
||||||
"start": {
|
|
||||||
"func": plugin.start,
|
|
||||||
"async": is_coro_func(plugin.start)
|
|
||||||
},
|
|
||||||
"unload": {
|
|
||||||
"func": plugin.unload,
|
|
||||||
"async": is_coro_func(plugin.unload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if self.plugins[pl_name]["load"]['async']:
|
|
||||||
plugin.log.debug(f"I'm async")
|
|
||||||
await plugin.load()
|
|
||||||
else:
|
|
||||||
plugin.log.debug(f"I'm sync")
|
|
||||||
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
|
|
||||||
th.start()
|
|
||||||
th.join()
|
|
||||||
self.loaded_str += f"{pl_name}:ok, "
|
|
||||||
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
|
|
||||||
except Exception as e:
|
|
||||||
self.loaded_str += f"{file}:no, "
|
|
||||||
self.log.error(i18n.plugins_error_loading.format(file, f"{e}"))
|
|
||||||
self.log.exception(e)
|
|
||||||
|
|
||||||
async def start(self, _):
|
|
||||||
for pl_name, pl_data in self.plugins.items():
|
|
||||||
try:
|
|
||||||
if pl_data['start']['async']:
|
|
||||||
self.log.debug(f"Start async plugin: {pl_name}")
|
|
||||||
t = self.loop.create_task(pl_data['start']['func']())
|
|
||||||
self.plugins_tasks.append(t)
|
|
||||||
else:
|
|
||||||
self.log.debug(f"Start sync plugin: {pl_name}")
|
|
||||||
th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}")
|
|
||||||
th.start()
|
|
||||||
self.plugins_tasks.append(th)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.exception(e)
|
|
||||||
|
|
||||||
async def unload(self, _):
|
|
||||||
for pl_name, pl_data in self.plugins.items():
|
|
||||||
try:
|
|
||||||
if pl_data['unload']['async']:
|
|
||||||
self.log.debug(f"Unload async plugin: {pl_name}")
|
|
||||||
await pl_data['unload']['func']()
|
|
||||||
else:
|
|
||||||
self.log.debug(f"Unload sync plugin: {pl_name}")
|
|
||||||
th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}")
|
|
||||||
th.start()
|
|
||||||
th.join()
|
|
||||||
except Exception as e:
|
|
||||||
self.log.exception(e)
|
|
||||||
126
src/modules/RateLimiter/__init__.py
Normal file
126
src/modules/RateLimiter/__init__.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import asyncio
|
||||||
|
import textwrap
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from core import utils
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
def __init__(self, max_calls: int, period: float, ban_time: float):
|
||||||
|
self.log = utils.get_logger("RateLimiter")
|
||||||
|
self.max_calls = max_calls
|
||||||
|
self.period = timedelta(seconds=period)
|
||||||
|
self.ban_time = timedelta(seconds=ban_time)
|
||||||
|
self._calls = defaultdict(deque)
|
||||||
|
self._banned_until = defaultdict(lambda: datetime.min)
|
||||||
|
self._notified = {}
|
||||||
|
|
||||||
|
def parse_console(self, x):
|
||||||
|
help_msg = textwrap.dedent("""\
|
||||||
|
|
||||||
|
RateLimiter menu:
|
||||||
|
info - list banned ip's
|
||||||
|
ban - put ip in banlist
|
||||||
|
unban - force remove ip from banlist
|
||||||
|
help - print that message""")
|
||||||
|
_banned_ips = [i for i in self._banned_until if self.is_banned(i, False)]
|
||||||
|
if len(x) > 0:
|
||||||
|
match x[0]:
|
||||||
|
case "info":
|
||||||
|
self.log.info(f"Trigger {self.max_calls}req/{self.period}. IP will be banned for {self.ban_time}.")
|
||||||
|
if len(_banned_ips) == 0:
|
||||||
|
return "No one ip in banlist."
|
||||||
|
else:
|
||||||
|
_msg = f"Banned ip{'' if len(_banned_ips) == 1 else 's'}: "
|
||||||
|
for ip in _banned_ips:
|
||||||
|
_msg += f"{ip}; "
|
||||||
|
return _msg
|
||||||
|
case "unban":
|
||||||
|
if len(x) == 2:
|
||||||
|
ip = x[1]
|
||||||
|
if ip in _banned_ips:
|
||||||
|
self._notified[ip] = False
|
||||||
|
self._calls[ip].clear()
|
||||||
|
self._banned_until[ip] = datetime.now()
|
||||||
|
return f"{ip} removed from banlist."
|
||||||
|
return f"{ip} not banned."
|
||||||
|
else:
|
||||||
|
return 'rl unban <IP>'
|
||||||
|
case "ban":
|
||||||
|
if len(x) == 3:
|
||||||
|
ip = x[1]
|
||||||
|
sec = x[2]
|
||||||
|
if not sec.isdigit():
|
||||||
|
return f"{sec!r} is not digit."
|
||||||
|
self._notified[ip] = False
|
||||||
|
self._calls[ip].clear()
|
||||||
|
self._banned_until[ip] = datetime.now() + timedelta(seconds=int(sec))
|
||||||
|
return f"{ip} banned until {self._banned_until[ip]}"
|
||||||
|
else:
|
||||||
|
return 'rl ban <IP> <sec>'
|
||||||
|
case _:
|
||||||
|
return help_msg
|
||||||
|
else:
|
||||||
|
return help_msg
|
||||||
|
|
||||||
|
async def notify(self, ip, writer):
|
||||||
|
if not self._notified[ip]:
|
||||||
|
self._notified[ip] = True
|
||||||
|
self.log.warning(f"{ip} banned until {self._banned_until[ip]}.")
|
||||||
|
try:
|
||||||
|
writer.write(b'\x0b\x00\x00\x00Eip banned.')
|
||||||
|
await writer.drain()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_banned(self, ip: str, _add_call=True) -> bool:
|
||||||
|
now = datetime.now()
|
||||||
|
if now < self._banned_until[ip]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if _add_call:
|
||||||
|
self._calls[ip].append(now)
|
||||||
|
while self._calls[ip] and self._calls[ip][0] + self.period < now:
|
||||||
|
self._calls[ip].popleft()
|
||||||
|
|
||||||
|
if len(self._calls[ip]) > self.max_calls:
|
||||||
|
self._banned_until[ip] = now + self.ban_time
|
||||||
|
self._calls[ip].clear()
|
||||||
|
return True
|
||||||
|
|
||||||
|
self._notified[ip] = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_request(ip: str, rate_limiter: RateLimiter):
|
||||||
|
if rate_limiter.is_banned(ip):
|
||||||
|
print(f"Request from {ip} is banned at {datetime.now()}")
|
||||||
|
rate_limiter.parse_console(["info"])
|
||||||
|
|
||||||
|
|
||||||
|
async def server_simulation():
|
||||||
|
rate_limiter = RateLimiter(max_calls=5, period=10, ban_time=30)
|
||||||
|
|
||||||
|
# Симулируем несколько запросов от разных IP-адресов
|
||||||
|
tasks = [
|
||||||
|
handle_request("192.168.1.1", rate_limiter),
|
||||||
|
handle_request("192.168.1.2", rate_limiter),
|
||||||
|
handle_request("192.168.1.1", rate_limiter),
|
||||||
|
handle_request("192.168.1.1", rate_limiter),
|
||||||
|
handle_request("192.168.1.3", rate_limiter),
|
||||||
|
handle_request("192.168.1.2", rate_limiter),
|
||||||
|
handle_request("192.168.1.1", rate_limiter),
|
||||||
|
handle_request("192.168.1.2", rate_limiter),
|
||||||
|
handle_request("192.168.1.3", rate_limiter),
|
||||||
|
handle_request("192.168.1.1", rate_limiter),
|
||||||
|
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
|
||||||
|
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
|
||||||
|
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
|
||||||
|
]
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(server_simulation())
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .app import web_app
|
|
||||||
from .app import _stop
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from asyncio import CancelledError
|
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI, Request, HTTPException
|
|
||||||
from fastapi.exceptions import RequestValidationError
|
|
||||||
from starlette import status
|
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
|
|
||||||
import core.utils
|
|
||||||
from . import utils
|
|
||||||
|
|
||||||
# from .models import SecretKey
|
|
||||||
|
|
||||||
web_app = FastAPI()
|
|
||||||
log = core.utils.get_logger("web")
|
|
||||||
|
|
||||||
uvserver = None
|
|
||||||
data_pool = []
|
|
||||||
data_run = [True]
|
|
||||||
|
|
||||||
|
|
||||||
def response(data=None, code=status.HTTP_200_OK, error_code=0, error_message=None):
|
|
||||||
if 200 >= code <= 300:
|
|
||||||
return JSONResponse(content={"result": data, "error": None}, status_code=code)
|
|
||||||
return JSONResponse(
|
|
||||||
content={"error": {"code": error_code if error_code else code, "message": f"{error_message}"}, "result": None},
|
|
||||||
status_code=code)
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.get("/")
|
|
||||||
async def index():
|
|
||||||
log.debug("Request IndexPage;")
|
|
||||||
return response("Index page")
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.get("/method/{method}")
|
|
||||||
async def _method(method, secret_key: str = None):
|
|
||||||
# log.debug(f"Request method; kwargs: {kwargs}")
|
|
||||||
is_auth = secret_key == config.WebAPI["secret_key"]
|
|
||||||
spl = method.split(".")
|
|
||||||
if len(spl) != 2:
|
|
||||||
raise StarletteHTTPException(405)
|
|
||||||
api_class, api_method = spl
|
|
||||||
match api_class:
|
|
||||||
case "events":
|
|
||||||
match api_method, is_auth:
|
|
||||||
case "get", False:
|
|
||||||
return response(data_pool)
|
|
||||||
raise StarletteHTTPException(404)
|
|
||||||
|
|
||||||
|
|
||||||
async def _stop():
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
if uvserver is not None:
|
|
||||||
uvserver.should_exit = True
|
|
||||||
data_run[0] = False
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.get("/stop")
|
|
||||||
async def stop(secret_key: str):
|
|
||||||
log.debug(f"Request stop; secret key: {secret_key}")
|
|
||||||
if secret_key == config.WebAPI["secret_key"]:
|
|
||||||
log.info("Stopping Web server")
|
|
||||||
asyncio.create_task(_stop())
|
|
||||||
return response("Web server stopped")
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.exception_handler(HTTPException)
|
|
||||||
async def default_exception_handler(request: Request, exc: HTTPException):
|
|
||||||
return response(
|
|
||||||
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
error_code=exc.status_code, error_message=f"Internal Server Error: {exc.status_code}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.exception_handler(StarletteHTTPException)
|
|
||||||
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
|
||||||
code = exc.status_code
|
|
||||||
if code == status.HTTP_405_METHOD_NOT_ALLOWED:
|
|
||||||
return response(code=code, error_message="Method Not Allowed")
|
|
||||||
if code == status.HTTP_404_NOT_FOUND:
|
|
||||||
return response(code=code, error_message="Method not Found")
|
|
||||||
return response(code=code, error_message="Unhandled error..")
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.exception_handler(RequestValidationError)
|
|
||||||
async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
||||||
code = status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
||||||
return response(code=code, error_message="Request Validation Error")
|
|
||||||
|
|
||||||
|
|
||||||
utils.hack_fastapi()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
uvconfig = uvicorn.Config(web_app,
|
|
||||||
host=config.WebAPI["server_ip"],
|
|
||||||
port=config.WebAPI["server_port"],
|
|
||||||
loop="asyncio")
|
|
||||||
uvserver = uvicorn.Server(uvconfig)
|
|
||||||
uvserver.run()
|
|
||||||
except KeyboardInterrupt or CancelledError:
|
|
||||||
pass
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import click
|
|
||||||
import uvicorn.server as uvs
|
|
||||||
from uvicorn.config import LOGGING_CONFIG
|
|
||||||
|
|
||||||
from uvicorn.lifespan import on
|
|
||||||
|
|
||||||
import core.utils
|
|
||||||
|
|
||||||
# logger = core.utils.get_logger("uvicorn")
|
|
||||||
# uvs.logger = logger
|
|
||||||
logger = uvs.logger
|
|
||||||
|
|
||||||
|
|
||||||
def ev_log_started_message(self, listeners) -> None:
|
|
||||||
cfg = self.config
|
|
||||||
if cfg.fd is not None:
|
|
||||||
sock = listeners[0]
|
|
||||||
logger.info(i18n.web_start.format(sock.getsockname()))
|
|
||||||
elif cfg.uds is not None:
|
|
||||||
logger.info(i18n.web_start.format(cfg.uds))
|
|
||||||
else:
|
|
||||||
addr_format = "%s://%s:%d"
|
|
||||||
host = "0.0.0.0" if cfg.host is None else cfg.host
|
|
||||||
if ":" in host:
|
|
||||||
addr_format = "%s://[%s]:%d"
|
|
||||||
port = cfg.port
|
|
||||||
if port == 0:
|
|
||||||
port = listeners[0].getsockname()[1]
|
|
||||||
protocol_name = "https" if cfg.ssl else "http"
|
|
||||||
message = i18n.web_start.format(addr_format)
|
|
||||||
color_message = (i18n.web_start.format(click.style(addr_format, bold=True)))
|
|
||||||
logger.info(message, protocol_name, host, port, extra={"color_message": color_message})
|
|
||||||
|
|
||||||
|
|
||||||
async def ev_shutdown(self, sockets=None) -> None:
|
|
||||||
logger.debug("Shutting down")
|
|
||||||
for server in self.servers:
|
|
||||||
server.close()
|
|
||||||
for sock in sockets or []:
|
|
||||||
sock.close()
|
|
||||||
for server in self.servers:
|
|
||||||
await server.wait_closed()
|
|
||||||
for connection in list(self.server_state.connections):
|
|
||||||
connection.shutdown()
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(self._wait_tasks_to_complete(), timeout=self.config.timeout_graceful_shutdown)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded", len(self.server_state.tasks))
|
|
||||||
for t in self.server_state.tasks:
|
|
||||||
if sys.version_info < (3, 9):
|
|
||||||
t.cancel()
|
|
||||||
else:
|
|
||||||
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
|
|
||||||
if not self.force_exit:
|
|
||||||
await self.lifespan.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
async def on_startup(self) -> None:
|
|
||||||
self.logger.debug("Waiting for application startup.")
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
main_lifespan_task = loop.create_task(self.main()) # noqa: F841
|
|
||||||
startup_event = {"type": "lifespan.startup"}
|
|
||||||
await self.receive_queue.put(startup_event)
|
|
||||||
await self.startup_event.wait()
|
|
||||||
if self.startup_failed or (self.error_occured and self.config.lifespan == "on"):
|
|
||||||
self.logger.error("Application startup failed. Exiting.")
|
|
||||||
self.should_exit = True
|
|
||||||
else:
|
|
||||||
self.logger.debug("Application startup complete.")
|
|
||||||
|
|
||||||
|
|
||||||
async def on_shutdown(self) -> None:
|
|
||||||
if self.error_occured:
|
|
||||||
return
|
|
||||||
self.logger.debug("Waiting for application shutdown.")
|
|
||||||
shutdown_event = {"type": "lifespan.shutdown"}
|
|
||||||
await self.receive_queue.put(shutdown_event)
|
|
||||||
await self.shutdown_event.wait()
|
|
||||||
if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"):
|
|
||||||
self.logger.error("Application shutdown failed. Exiting.")
|
|
||||||
self.should_exit = True
|
|
||||||
else:
|
|
||||||
self.logger.debug("Application shutdown complete.")
|
|
||||||
|
|
||||||
|
|
||||||
def hack_fastapi():
|
|
||||||
uvs.Server.shutdown = ev_shutdown
|
|
||||||
uvs.Server._log_started_message = ev_log_started_message
|
|
||||||
on.LifespanOn.startup = on_startup
|
|
||||||
on.LifespanOn.shutdown = on_shutdown
|
|
||||||
|
|
||||||
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
|
|
||||||
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format
|
|
||||||
LOGGING_CONFIG["formatters"].update({
|
|
||||||
"file_default": {
|
|
||||||
"()": "logging.Formatter",
|
|
||||||
"fmt": core.utils.log_format
|
|
||||||
},
|
|
||||||
"file_access": {
|
|
||||||
"()": "logging.Formatter",
|
|
||||||
"fmt": core.utils.log_format
|
|
||||||
}
|
|
||||||
})
|
|
||||||
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
|
|
||||||
LOGGING_CONFIG["handlers"].update({
|
|
||||||
"file_default": {
|
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
|
||||||
"filename": "./logs/web.log",
|
|
||||||
"encoding": "utf-8",
|
|
||||||
"formatter": "file_default"
|
|
||||||
},
|
|
||||||
"file_access": {
|
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
|
||||||
"filename": "./logs/web_access.log",
|
|
||||||
"encoding": "utf-8",
|
|
||||||
"formatter": "file_access"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
|
|
||||||
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File modules.__init__.py
|
# File modules.__init__.py
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Version 1.1
|
# Version 1.1
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
from .ConsoleSystem import Console
|
|
||||||
from .ConfigProvider import ConfigProvider, Config
|
from .ConfigProvider import ConfigProvider, Config
|
||||||
from .i18n import MultiLanguage
|
|
||||||
from .EventsSystem import EventsSystem
|
from .EventsSystem import EventsSystem
|
||||||
|
from .ConsoleSystem import Console
|
||||||
from .PluginsLoader import PluginsLoader
|
from .PluginsLoader import PluginsLoader
|
||||||
from .WebAPISystem import web_app
|
from .i18n import MultiLanguage
|
||||||
from .WebAPISystem import _stop as stop_web
|
from .RateLimiter import RateLimiter
|
||||||
|
from .PermsSystem import PermsSystem
|
||||||
|
|||||||
@@ -1,9 +1,159 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Developed by KuiToi Dev
|
# Developed by KuiToi Dev
|
||||||
# File modules.i18n.__init__.py
|
# File modules.i18n
|
||||||
# Written by: SantaSpeen
|
# Written by: SantaSpeen
|
||||||
# Version 1.0
|
# Version 1.3
|
||||||
# Licence: FPA
|
# Licence: FPA
|
||||||
# (c) kuitoi.su 2023
|
# (c) kuitoi.su 2023
|
||||||
from .i18n import MultiLanguage
|
import builtins
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from json import JSONDecodeError
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from core.utils import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
class i18n:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
i18n.data = data
|
||||||
|
|
||||||
|
def __getattribute__(self, key):
|
||||||
|
return i18n.data[key]
|
||||||
|
|
||||||
|
|
||||||
|
class MultiLanguage:
|
||||||
|
|
||||||
|
def __init__(self, language: str = None, files_dir="translates/", encoding=None):
|
||||||
|
if encoding is None:
|
||||||
|
encoding = config.enc
|
||||||
|
if language is None:
|
||||||
|
language = "en"
|
||||||
|
self.__data = {
|
||||||
|
"hello": "Hello from KuiToi-Server!",
|
||||||
|
"config_path": "Use {} to configure.",
|
||||||
|
"init_ok": "Initialization completed.",
|
||||||
|
"start": "Server started!",
|
||||||
|
"stop": "Server stopped!",
|
||||||
|
"auth_need_key": "BeamMP key is required to run!",
|
||||||
|
"auth_empty_key": "BeamMP key is empty!",
|
||||||
|
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||||
|
"auth_use_link": "Use this link: {}",
|
||||||
|
"GUI_yes": "Yes",
|
||||||
|
"GUI_no": "No",
|
||||||
|
"GUI_ok": "OK",
|
||||||
|
"GUI_cancel": "Cancel",
|
||||||
|
"GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?",
|
||||||
|
"GUI_enter_key_message": "Please enter the key:",
|
||||||
|
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||||
|
"core_bind_failed": "Failed to bind port. Error: {}",
|
||||||
|
"core_direct_mode": "Server started in direct connection mode.",
|
||||||
|
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
|
||||||
|
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
|
||||||
|
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
|
||||||
|
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
|
||||||
|
"core_auth_server_no_response": "Failed to authenticate the server.",
|
||||||
|
"core_mods_loaded": "Loaded {} mods. {}Mb",
|
||||||
|
"core_identifying_connection": "Processing new connection...",
|
||||||
|
"core_player_kick_outdated": "Incorrect version of BeamMP.",
|
||||||
|
"core_player_kick_bad_key": "Invalid key passed!",
|
||||||
|
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
|
||||||
|
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
|
||||||
|
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
|
||||||
|
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
|
||||||
|
"core_player_kick_server_full": "Server is full.",
|
||||||
|
"core_identifying_okay": "Successful login.",
|
||||||
|
"game_welcome_message": "Welcome {}!",
|
||||||
|
"client_mod_request": "Requested mod: {}",
|
||||||
|
"client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)",
|
||||||
|
"client_mod_sent_limit": " (limit {}Mb/s)",
|
||||||
|
"client_mod_sent_error": "Error sending mod: {}",
|
||||||
|
"client_sync_time": "Sync time {}sec.",
|
||||||
|
"client_kicked": "Kicked for reason: \"{}\"",
|
||||||
|
"client_event_invalid_data": "Invalid data returned from event: {}",
|
||||||
|
"client_player_disconnected": "Left the server. Playtime: {} min",
|
||||||
|
"events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...",
|
||||||
|
"events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...",
|
||||||
|
"events_calling_error": "Error calling \"{}\" in function \"{}\".",
|
||||||
|
"events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.",
|
||||||
|
"events_lua_local": "local ",
|
||||||
|
"events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}",
|
||||||
|
"plugins_not_found_load": "Function \"def load():\" not found.",
|
||||||
|
"plugins_not_found_start": "Function \"def start():\" not found.",
|
||||||
|
"plugins_not_found_unload": "Function \"def unload():\" not found.",
|
||||||
|
"plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.",
|
||||||
|
"plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.",
|
||||||
|
"plugins_error_loading": "An error occurred while loading the plugin {}: {}",
|
||||||
|
"plugins_lua_enabled": "You have enabled Lua plugin support.",
|
||||||
|
"plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.",
|
||||||
|
"plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.",
|
||||||
|
"plugins_lua_legacy_config_create": "Creating it.",
|
||||||
|
"plugins_lua_unload": "Stopping Lua plugin: {}",
|
||||||
|
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
|
||||||
|
"help_message_man": "Shows the help page for COMMAND.",
|
||||||
|
"man_for": "Help page for",
|
||||||
|
"man_message_not_found": "man: Help page not found.",
|
||||||
|
"man_command_not_found": "man: Command \"{}\" not found!",
|
||||||
|
"man_message_help": "help - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.",
|
||||||
|
"help_message_help": "Shows the names and brief descriptions of commands",
|
||||||
|
"help_command": "Command",
|
||||||
|
"help_message": "Text",
|
||||||
|
"help_message_not_found": "No text found",
|
||||||
|
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||||
|
"help_message_stop": "Stops the server.",
|
||||||
|
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||||
|
"help_message_exit": "Stops the server."
|
||||||
|
}
|
||||||
|
self.__en_data = self.__data.copy()
|
||||||
|
self.__i18n = None
|
||||||
|
self.__encoding = encoding
|
||||||
|
self.language = language
|
||||||
|
if not os.path.exists(files_dir):
|
||||||
|
os.makedirs(files_dir)
|
||||||
|
if not os.path.exists(files_dir + "en.json"):
|
||||||
|
with open(files_dir + "en.json", "w") as f:
|
||||||
|
f.write(json.dumps(self.__en_data, indent=2))
|
||||||
|
self.files_dir = files_dir
|
||||||
|
self.log = get_logger("i18n")
|
||||||
|
self.fi = False
|
||||||
|
self.lang_url = "https://raw.githubusercontent.com/KuiToi/KuiToi-Server/Stable/src/translates/"
|
||||||
|
|
||||||
|
def set_language(self, language):
|
||||||
|
if self.__i18n and language == self.language:
|
||||||
|
return
|
||||||
|
self.log.debug(f"set_language({language})")
|
||||||
|
self.language = language
|
||||||
|
self.open_file()
|
||||||
|
self.__i18n = i18n(self.__data)
|
||||||
|
|
||||||
|
def open_file(self):
|
||||||
|
self.log.debug("open_file")
|
||||||
|
file = self.files_dir + self.language + ".json"
|
||||||
|
try:
|
||||||
|
with open(file, encoding=self.__encoding) as f:
|
||||||
|
self.__data.update(json.load(f))
|
||||||
|
return
|
||||||
|
except JSONDecodeError:
|
||||||
|
self.log.error(
|
||||||
|
f"Localisation \"{file}\" have JsonDecodeError. Using default localisation: en.")
|
||||||
|
except FileNotFoundError:
|
||||||
|
r = requests.get(f"{self.lang_url}{self.language}.json")
|
||||||
|
if r.status_code != 404:
|
||||||
|
self.log.info(f"Downloaded new localisation: {self.language}")
|
||||||
|
loc = r.json()
|
||||||
|
with open(file, "w", encoding=self.__encoding) as f:
|
||||||
|
json.dump(loc, f)
|
||||||
|
self.__data.update(loc)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.log.warning(f"Localisation \"{file}\" not found; Using default localisation: en.")
|
||||||
|
self.set_language("en")
|
||||||
|
|
||||||
|
def builtins_hook(self) -> None:
|
||||||
|
self.log.debug("used builtins_hook")
|
||||||
|
builtins.i18n = self.__i18n
|
||||||
|
builtins.i18n_data = self.__data
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Developed by KuiToi Dev
|
|
||||||
# File modules.i18n.i18n.py
|
|
||||||
# Written by: SantaSpeen
|
|
||||||
# Version 1.3
|
|
||||||
# Licence: FPA
|
|
||||||
# (c) kuitoi.su 2023
|
|
||||||
import builtins
|
|
||||||
import json
|
|
||||||
from json import JSONDecodeError
|
|
||||||
|
|
||||||
from core.utils import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class i18n:
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
i18n.data = data
|
|
||||||
|
|
||||||
def __getattribute__(self, key):
|
|
||||||
return i18n.data[key]
|
|
||||||
|
|
||||||
|
|
||||||
class MultiLanguage:
|
|
||||||
|
|
||||||
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding=None):
|
|
||||||
if encoding is None:
|
|
||||||
encoding = config.enc
|
|
||||||
if language is None:
|
|
||||||
language = "en"
|
|
||||||
self.__data = {}
|
|
||||||
self.__i18n = None
|
|
||||||
self.__encoding = encoding
|
|
||||||
self.language = language
|
|
||||||
self.files_dir = files_dir
|
|
||||||
self.log = get_logger("i18n")
|
|
||||||
self.set_language(language)
|
|
||||||
|
|
||||||
def set_language(self, language):
|
|
||||||
if language is None:
|
|
||||||
language = "en"
|
|
||||||
self.log.debug(f"set_language({language})")
|
|
||||||
self.language = language
|
|
||||||
if language != "en":
|
|
||||||
self.open_file()
|
|
||||||
else:
|
|
||||||
# noinspection PyDictDuplicateKeys
|
|
||||||
self.__data = {
|
|
||||||
"": "Basic phases",
|
|
||||||
"hello": "Hello from KuiToi-Server!",
|
|
||||||
"config_path": "Use {} to configure.",
|
|
||||||
"init_ok": "Initialization completed.",
|
|
||||||
"start": "Server started!",
|
|
||||||
"stop": "Server stopped!",
|
|
||||||
|
|
||||||
"": "Server auth",
|
|
||||||
"auth_need_key": "BeamMP key is required to run!",
|
|
||||||
"auth_empty_key": "BeamMP key is empty!",
|
|
||||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
|
||||||
"auth_use_link": "Use this link: {}",
|
|
||||||
|
|
||||||
"": "GUI phases",
|
|
||||||
"GUI_yes": "Yes",
|
|
||||||
"GUI_no": "No",
|
|
||||||
"GUI_ok": "OK",
|
|
||||||
"GUI_cancel": "Cancel",
|
|
||||||
"GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?",
|
|
||||||
"GUI_enter_key_message": "Please enter the key:",
|
|
||||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
|
||||||
|
|
||||||
"": "Web phases",
|
|
||||||
"web_start": "WebAPI started on {} (CTRL+C to stop)",
|
|
||||||
|
|
||||||
"": "Core phrases",
|
|
||||||
"core_bind_failed": "Failed to bind port. Error: {}",
|
|
||||||
"core_direct_mode": "Server started in direct connection mode.",
|
|
||||||
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
|
|
||||||
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
|
|
||||||
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
|
|
||||||
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
|
|
||||||
"core_auth_server_no_response": "Failed to authenticate the server.",
|
|
||||||
"core_mods_loaded": "Loaded {} mods. {}Mb",
|
|
||||||
"core_identifying_connection": "Processing new connection...",
|
|
||||||
"core_player_kick_outdated": "Incorrect version of BeamMP.",
|
|
||||||
"core_player_kick_bad_key": "Invalid key passed!",
|
|
||||||
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
|
|
||||||
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
|
|
||||||
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
|
|
||||||
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
|
|
||||||
"core_player_kick_server_full": "Server is full.",
|
|
||||||
"core_player_set_id": "Player set ID {}",
|
|
||||||
"core_identifying_okay": "Successful login.",
|
|
||||||
|
|
||||||
"": "In-game phrases",
|
|
||||||
"game_welcome_message": "Welcome {}!",
|
|
||||||
|
|
||||||
"": "Client class phrases",
|
|
||||||
"client_mod_request": "Requested mod: {}",
|
|
||||||
"client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)",
|
|
||||||
"client_mod_sent_limit": " (limit {}Mb/s)",
|
|
||||||
"client_mod_sent_error": "Error sending mod: {}",
|
|
||||||
"client_sync_time": "Sync time {}sec.",
|
|
||||||
"client_kicked": "Kicked for reason: \"{}\"",
|
|
||||||
"client_event_invalid_data": "Invalid data returned from event: {}",
|
|
||||||
"client_player_disconnected": "Left the server. Playtime: {} min",
|
|
||||||
|
|
||||||
"": "Events system",
|
|
||||||
|
|
||||||
"events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...",
|
|
||||||
"events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...",
|
|
||||||
"events_calling_error": "Error calling \"{}\" in function \"{}\".",
|
|
||||||
"events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.",
|
|
||||||
"events_lua_local": "local ",
|
|
||||||
"events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}",
|
|
||||||
|
|
||||||
"": "Plugins loader",
|
|
||||||
|
|
||||||
"plugins_not_found_load": "Function \"def load():\" not found.",
|
|
||||||
"plugins_not_found_start": "Function \"def start():\" not found.",
|
|
||||||
"plugins_not_found_unload": "Function \"def unload():\" not found.",
|
|
||||||
"plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.",
|
|
||||||
"plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.",
|
|
||||||
"plugins_error_loading": "An error occurred while loading the plugin {}: {}",
|
|
||||||
|
|
||||||
"": "Lua plugins loader",
|
|
||||||
|
|
||||||
"plugins_lua_enabled": "You have enabled Lua plugin support.",
|
|
||||||
"plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.",
|
|
||||||
"plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.",
|
|
||||||
"plugins_lua_legacy_config_create": "Creating it.",
|
|
||||||
"plugins_lua_unload": "Stopping Lua plugin: {}",
|
|
||||||
|
|
||||||
"": "Command: man",
|
|
||||||
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
|
|
||||||
"help_message_man": "Shows the help page for COMMAND.",
|
|
||||||
"man_for": "Help page for",
|
|
||||||
"man_message_not_found": "man: Help page not found.",
|
|
||||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
|
||||||
|
|
||||||
"": "Command: help",
|
|
||||||
"man_message_help": "help - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.",
|
|
||||||
"help_message_help": "Shows the names and brief descriptions of commands",
|
|
||||||
"help_command": "Command",
|
|
||||||
"help_message": "Text",
|
|
||||||
"help_message_not_found": "No text found",
|
|
||||||
|
|
||||||
"": "Command: stop",
|
|
||||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
|
||||||
"help_message_stop": "Stops the server.",
|
|
||||||
|
|
||||||
"": "Command: exit",
|
|
||||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
|
||||||
"help_message_exit": "Stops the server."
|
|
||||||
}
|
|
||||||
self.__i18n = i18n(self.__data)
|
|
||||||
|
|
||||||
def open_file(self):
|
|
||||||
self.log.debug("open_file")
|
|
||||||
file = self.files_dir + self.language + ".json"
|
|
||||||
try:
|
|
||||||
with open(file, encoding=self.__encoding) as f:
|
|
||||||
self.__data.update(json.load(f))
|
|
||||||
return
|
|
||||||
except JSONDecodeError:
|
|
||||||
self.log.error(
|
|
||||||
f"Localisation \"{self.language}.json\" have JsonDecodeError. Using default localisation: en.")
|
|
||||||
except FileNotFoundError:
|
|
||||||
self.log.warning(f"Localisation \"{self.language}.json\" not found; Using default localisation: en.")
|
|
||||||
self.set_language("en")
|
|
||||||
|
|
||||||
def builtins_hook(self) -> None:
|
|
||||||
self.log.debug("used builtins_hook")
|
|
||||||
builtins.i18n = self.__i18n
|
|
||||||
builtins.i18n_data = self.__data
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
### Builtins
|
||||||
|
|
||||||
|
```python
|
||||||
class i18n:
|
class i18n:
|
||||||
# Basic phases
|
# Basic phases
|
||||||
hello: str
|
hello: str
|
||||||
@@ -42,7 +45,6 @@ class i18n:
|
|||||||
core_player_kick_stale: str
|
core_player_kick_stale: str
|
||||||
core_player_kick_no_allowed_default_reason: str
|
core_player_kick_no_allowed_default_reason: str
|
||||||
core_player_kick_server_full: str
|
core_player_kick_server_full: str
|
||||||
core_player_set_id: str
|
|
||||||
core_identifying_okay: str
|
core_identifying_okay: str
|
||||||
|
|
||||||
# In-game phrases
|
# In-game phrases
|
||||||
@@ -107,3 +109,4 @@ class i18n:
|
|||||||
# Command: exit
|
# Command: exit
|
||||||
man_message_exit: str
|
man_message_exit: str
|
||||||
help_message_exit: str
|
help_message_exit: str
|
||||||
|
```
|
||||||
@@ -21,9 +21,6 @@
|
|||||||
"GUI_enter_key_message": "请输入密钥:",
|
"GUI_enter_key_message": "请输入密钥:",
|
||||||
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
||||||
|
|
||||||
"": "Web阶段",
|
|
||||||
"web_start": "WebAPI已启动于{}(按CTRL+C停止)",
|
|
||||||
|
|
||||||
"": "核心短语",
|
"": "核心短语",
|
||||||
"core_bind_failed": "无法绑定端口。错误:{}",
|
"core_bind_failed": "无法绑定端口。错误:{}",
|
||||||
"core_direct_mode": "服务器以直接连接模式启动。",
|
"core_direct_mode": "服务器以直接连接模式启动。",
|
||||||
@@ -41,7 +38,6 @@
|
|||||||
"core_player_kick_stale": "过时的客户端。(由新连接替换)",
|
"core_player_kick_stale": "过时的客户端。(由新连接替换)",
|
||||||
"core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。",
|
"core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。",
|
||||||
"core_player_kick_server_full": "服务器已满。",
|
"core_player_kick_server_full": "服务器已满。",
|
||||||
"core_player_set_id": "玩家设置ID {}",
|
|
||||||
"core_identifying_okay": "成功登录。",
|
"core_identifying_okay": "成功登录。",
|
||||||
|
|
||||||
"": "游戏内短语",
|
"": "游戏内短语",
|
||||||
@@ -21,9 +21,6 @@
|
|||||||
"GUI_enter_key_message": "Please enter the key:",
|
"GUI_enter_key_message": "Please enter the key:",
|
||||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||||
|
|
||||||
"": "Web phases",
|
|
||||||
"web_start": "WebAPI started on {} (CTRL+C to stop)",
|
|
||||||
|
|
||||||
"": "Core phrases",
|
"": "Core phrases",
|
||||||
"core_bind_failed": "Failed to bind port. Error: {}",
|
"core_bind_failed": "Failed to bind port. Error: {}",
|
||||||
"core_direct_mode": "Server started in direct connection mode.",
|
"core_direct_mode": "Server started in direct connection mode.",
|
||||||
@@ -41,7 +38,6 @@
|
|||||||
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
|
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
|
||||||
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
|
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
|
||||||
"core_player_kick_server_full": "Server is full.",
|
"core_player_kick_server_full": "Server is full.",
|
||||||
"core_player_set_id": "Player set ID {}",
|
|
||||||
"core_identifying_okay": "Successful login.",
|
"core_identifying_okay": "Successful login.",
|
||||||
|
|
||||||
"": "In-game phrases",
|
"": "In-game phrases",
|
||||||
@@ -21,9 +21,6 @@
|
|||||||
"GUI_enter_key_message": "Пожалуйста введите ключ:",
|
"GUI_enter_key_message": "Пожалуйста введите ключ:",
|
||||||
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",
|
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",
|
||||||
|
|
||||||
"": "Web phases",
|
|
||||||
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
|
|
||||||
|
|
||||||
"": "Core phrases",
|
"": "Core phrases",
|
||||||
"core_bind_failed": "Не получилось занять порт. Ошибка: {}",
|
"core_bind_failed": "Не получилось занять порт. Ошибка: {}",
|
||||||
"core_direct_mode": "Сервер запушен в режиме прямого подключения.",
|
"core_direct_mode": "Сервер запушен в режиме прямого подключения.",
|
||||||
@@ -41,7 +38,6 @@
|
|||||||
"core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)",
|
"core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)",
|
||||||
"core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.",
|
"core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.",
|
||||||
"core_player_kick_server_full": "Сервер полон.",
|
"core_player_kick_server_full": "Сервер полон.",
|
||||||
"core_player_set_id": "Игрок получил ID {}",
|
|
||||||
"core_identifying_okay": "Успешный вход.",
|
"core_identifying_okay": "Успешный вход.",
|
||||||
|
|
||||||
"": "In-game phrases",
|
"": "In-game phrases",
|
||||||
73
win/auto.json
Normal file
73
win/auto.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"version": "auto-py-to-exe-configuration_v1",
|
||||||
|
"pyinstallerOptions": [
|
||||||
|
{
|
||||||
|
"optionDest": "noconfirm",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "filenames",
|
||||||
|
"value": "../src/main.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "onefile",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "console",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "icon_file",
|
||||||
|
"value": "./icon.ico"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "name",
|
||||||
|
"value": "KuiToi-Server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "clean_build",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "strip",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "noupx",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "disable_windowed_traceback",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "version_file",
|
||||||
|
"value": "./metadata.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "uac_admin",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "uac_uiaccess",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "argv_emulation",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "bootloader_ignore_signals",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "datas",
|
||||||
|
"value": "C:/Users/SantaSpeen/PycharmProjects/KuiToi-Server/src/modules/PluginsLoader/add_in.lua;."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nonPyinstallerOptions": {
|
||||||
|
"increaseRecursionLimit": true,
|
||||||
|
"manualArguments": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
win/icon.ico
Normal file
BIN
win/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
44
win/metadata.txt
Normal file
44
win/metadata.txt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# UTF-8
|
||||||
|
#
|
||||||
|
# For more details about fixed file info 'ffi' see:
|
||||||
|
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
||||||
|
|
||||||
|
VSVersionInfo(
|
||||||
|
ffi=FixedFileInfo(
|
||||||
|
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
||||||
|
# Set not needed items to zero 0. Must always contain 4 elements.
|
||||||
|
filevers=(0,4,5,0),
|
||||||
|
prodvers=(0,4,5,0),
|
||||||
|
# Contains a bitmask that specifies the valid bits 'flags'r
|
||||||
|
mask=0x3f,
|
||||||
|
# Contains a bitmask that specifies the Boolean attributes of the file.
|
||||||
|
flags=0x0,
|
||||||
|
# The operating system for which this file was designed.
|
||||||
|
# 0x4 - NT and there is no need to change it.
|
||||||
|
OS=0x40004,
|
||||||
|
# The general type of file.
|
||||||
|
# 0x1 - the file is an application.
|
||||||
|
fileType=0x1,
|
||||||
|
# The function of the file.
|
||||||
|
# 0x0 - the function is not defined for this fileType
|
||||||
|
subtype=0x0,
|
||||||
|
# Creation date and time stamp.
|
||||||
|
date=(0, 0)
|
||||||
|
),
|
||||||
|
kids=[
|
||||||
|
StringFileInfo(
|
||||||
|
[
|
||||||
|
StringTable(
|
||||||
|
u'040904B0',
|
||||||
|
[StringStruct(u'CompanyName', u'KuiToi'),
|
||||||
|
StringStruct(u'FileDescription', u'KuiToi Server'),
|
||||||
|
StringStruct(u'FileVersion', u'0.4.5.0'),
|
||||||
|
StringStruct(u'InternalName', u'KuiToi Server'),
|
||||||
|
StringStruct(u'LegalCopyright', u'© Maxim Khomutov'),
|
||||||
|
StringStruct(u'OriginalFilename', u'KuiToi-Server.exe'),
|
||||||
|
StringStruct(u'ProductName', u'KuiToi Server'),
|
||||||
|
StringStruct(u'ProductVersion', u'0.4.5.0')])
|
||||||
|
]),
|
||||||
|
VarFileInfo([VarStruct(u'Translation', [1049, 1251, 1033, 1251, 2052, 950])])
|
||||||
|
]
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user