mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2026-04-24 00:56:36 +00:00
Compare commits
210 Commits
0.2.2-alph
...
abbd64184e
| Author | SHA1 | Date | |
|---|---|---|---|
| abbd64184e | |||
| e5dd63579b | |||
| ef286b7e03 | |||
| 3a42fa13e7 | |||
| cdec0b9949 | |||
| de91d075b4 | |||
| a7c02e0b52 | |||
| 6dd3de63a9 | |||
| 7e0c50a04e | |||
| f1ab07d49a | |||
| cdeacc16bf | |||
| 719e705bab | |||
| 21dd23cb55 | |||
| 9295ed2b7a | |||
| 7466f987ac | |||
| d29bb9de98 | |||
| 189bd0cc30 | |||
| 53dae25fcf | |||
| e241a7da4c | |||
| 13be12a7a1 | |||
| e348ffecc3 | |||
| 336aa31732 | |||
| d8c667ff51 | |||
| 658f9ed9c6 | |||
| c1cb8dcdba | |||
| f52c73ab76 | |||
| 6d01f0ef8d | |||
| eea03c835f | |||
| f28a783f7e | |||
| dbb27ff6d5 | |||
| e621c8dc7c | |||
| 76b568c248 | |||
| 4c7f5ac14b | |||
| 28386a0300 | |||
| 8140b3347a | |||
| 4276eb0fdb | |||
| d92aae7c47 | |||
| 6ccbe99d47 | |||
| a50decb470 | |||
| 9079750576 | |||
| e440cdf022 | |||
| 43fd56f327 | |||
| 2368fec501 | |||
| ecb9886882 | |||
| 9a8bcbfae0 | |||
| ddcfa56467 | |||
| 9e1017609c | |||
| d76262fc43 | |||
| 9253f24421 | |||
| 8435b00617 | |||
| 847fe68417 | |||
| 5f32bf8423 | |||
| 139143c517 | |||
| 4bd2e28a21 | |||
| 28203c5836 | |||
| 7de5837db4 | |||
| cb00829ae7 | |||
| 59b0e58801 | |||
| 5b814efbb9 | |||
| 17b8be1b9d | |||
| 25ac16c300 | |||
| 1fac1d5ae9 | |||
| d6021ddc2d | |||
| 8e8c66c3bf | |||
| 4273571d97 | |||
| 71df291391 | |||
| cf0f397465 | |||
| 7b579d2916 | |||
| a3386339d0 | |||
| bf0a3f3feb | |||
| 5e071c5705 | |||
| 132beb0dd6 | |||
| 33f2d2ba72 | |||
| 90113179d7 | |||
| cd178b815a | |||
| c1f3983856 | |||
| 96cc4b08db | |||
| d13a319f39 | |||
| f24ae23eac | |||
| b31b01d137 | |||
| e7be3c88be | |||
| 2dd8b5f5eb | |||
| 84c45d321a | |||
| b1162af681 | |||
| 91c9cd8454 | |||
| b8326ecdf8 | |||
| c068629c83 | |||
| 905c0a361d | |||
| d7073d9124 | |||
| 1b5ddbdd45 | |||
| 0d3699bfee | |||
| b3dffe74ec | |||
| 2992c9cbab | |||
| 43518ac57c | |||
| a96e8111e3 | |||
| ecf06bf1c9 | |||
| 8d57db4a23 | |||
| 3d9e08d05d | |||
| ac5f5ee894 | |||
| 92880a94df | |||
| dcc1f14b17 | |||
| f9f4df7438 | |||
| 4f7e83a00f | |||
| aa6716fa75 | |||
| 6fa07f3e07 | |||
| 9ae200d48a | |||
| 3b5324d115 | |||
| f181a82e0e | |||
| b271c80e39 | |||
| ef9a55c407 | |||
| 98b4878339 | |||
| b345588c02 | |||
| c8fea133ba | |||
| b80d519c8d | |||
| 2ace3fcd17 | |||
| dbe8b14d7f | |||
| 752e981462 | |||
| 744a7347a3 | |||
| 8139cbf8bc | |||
| eec7c8129d | |||
| 158599dfc5 | |||
| 06bd50f0fa | |||
| e086fea2e9 | |||
| b6038ee6d0 | |||
| 147e76e089 | |||
| 56b9049dcb | |||
| 78d323644d | |||
| 310c47162c | |||
| 27d49cf5cc | |||
| a5a7a5dfc9 | |||
| f6ff018b03 | |||
| 1829113ae5 | |||
| e72c371e20 | |||
| 57b7cebeca | |||
| 2a2d55946e | |||
| ea2d715cae | |||
| 102891c8e8 | |||
| 46b0419340 | |||
| 47cca3a0d8 | |||
| 77ee76c0c0 | |||
| 852e977a75 | |||
| 407127ec97 | |||
| 7dd3faac12 | |||
| ef69df10d6 | |||
| a226b17612 | |||
| 69348e9339 | |||
| 31d8cf7842 | |||
| 45d45a820c | |||
| aa440a1e3d | |||
| 63c9515e86 | |||
| cfeb2e9823 | |||
| 85b85114b5 | |||
| 792884d7b0 | |||
| a5b087f8b4 | |||
| a01567c89a | |||
| 041883644c | |||
| 3d33eec5fd | |||
| 3f2c5b24f9 | |||
| b7ea7ff362 | |||
| 07ec15170b | |||
| eb88af247c | |||
| 6dedf518e2 | |||
| 69ee180128 | |||
| 98f86b2248 | |||
| 98ef332193 | |||
| 642c91d59c | |||
| acdb32d900 | |||
| 50b1e7b176 | |||
| c9e6a0a9cd | |||
| cd098571d9 | |||
| a73b14f9b4 | |||
| e3e5c6ecbb | |||
| 5953923368 | |||
| 580b836e39 | |||
| 4974d48411 | |||
| 3b7842aa50 | |||
| db6eb361b8 | |||
| 479525a66e | |||
| 6d4bc1e72c | |||
| 9b3677de46 | |||
| 58137752c5 | |||
| 220c6068e4 | |||
| a9dad5ab8f | |||
| aa5725e8a5 | |||
| 939723acdd | |||
| 90beaf1302 | |||
| ee366a2d23 | |||
| d665021479 | |||
| 13ff3207b2 | |||
| 50b479c396 | |||
| 62fa4c6f25 | |||
| f0f8da962e | |||
| 840d8fd685 | |||
| 4629fbc43a | |||
| e9919459af | |||
| 5f8b70a2ee | |||
| a66f3d8b36 | |||
| 4c3da30a94 | |||
| 9c52e41b99 | |||
| 51f960f7c2 | |||
| 0cbed05d68 | |||
| c6c6ec31b0 | |||
| 8feba0e085 | |||
| a5202edf83 | |||
| 64ce662d04 | |||
| 08e4f0fcba | |||
| 99f40eadb0 | |||
| 5a40ab8b05 | |||
| 4f688d7c02 | |||
| c4fe201b86 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -137,4 +137,6 @@ dmypy.json
|
||||
/src/plugins
|
||||
/test/
|
||||
*test.py
|
||||
logs/
|
||||
logs/
|
||||
*.yml
|
||||
*.toml
|
||||
|
||||
2
LICENCE
2
LICENCE
@@ -6,6 +6,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
|
||||
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
80
README.md
80
README.md
@@ -1,59 +1,89 @@
|
||||
# KuiToi-Server
|
||||
|
||||
## About
|
||||
**_[Status: Alpha]_** \
|
||||
**_[Status: Beta]_** \
|
||||
BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
|
||||
|
||||
Why did I decide to write my own kernel from scratch?\
|
||||
I didn't like writing plugins in Lua after using Python; it was very inconvenient, there were always some nuances, which ultimately led me to create KuiToi!
|
||||
|
||||
**Our site**: [kuitoi.su](https://kuitoi.su) (WIP)\
|
||||
**Our forum**: [forum.kuitoi.su](https://forum.kuitoi.su) (WIP)\
|
||||
**Our discord**: [KuiToi](https://discord.gg/BAcgaAmdkJ)
|
||||
|
||||
## TODOs
|
||||
|
||||
- [ ] Server core
|
||||
- [x] BeamMP System
|
||||
- [x] Server core:
|
||||
- [x] BeamMP System:
|
||||
- [x] Private access (Without key, Direct connect)
|
||||
- [x] Public access (With key, listing in Launcher)
|
||||
- [X] Player authentication
|
||||
- [ ] KuiToi System
|
||||
- [ ] Servers counter
|
||||
- [ ] Players counter
|
||||
- [ ] Etc.
|
||||
- [ ] TCP Server part:
|
||||
- [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
|
||||
- [ ] Car state synchronizations _(Codes: We, Vi)_
|
||||
- [ ] "ABG:" (compressed data)
|
||||
- [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
|
||||
- [ ] Vehicle data _(Code: Os)_
|
||||
- [ ] UDP Server part:
|
||||
- [ ] Players synchronizations _(Code: Zp)_
|
||||
- [ ] Ping _(Code: p)_
|
||||
- [x] Compress data
|
||||
- [x] UDP Server part:
|
||||
- [x] Ping
|
||||
- [x] Position synchronizations
|
||||
- [x] Additional:
|
||||
- [x] Logger
|
||||
- [x] Logger:
|
||||
- [x] Just logging
|
||||
- [x] Log in file
|
||||
- [x] Log history (.1.log, .2.log, ...)
|
||||
- [x] Console:
|
||||
- [x] Tabulation
|
||||
- [ ] _(Deferred)_ Static text (bug)
|
||||
- [x] Events System
|
||||
- [x] History
|
||||
- [x] Autocomplete
|
||||
- [x] Events System:
|
||||
- [x] Call events
|
||||
- [x] Create custom events
|
||||
- [ ] Return from events
|
||||
- [x] Plugins support
|
||||
- [x] Load Python plugins
|
||||
- [ ] Load Lua plugins (Original BeamMP compatibility)
|
||||
- [x] MultiLanguage (i18n support)
|
||||
- [x] Return from events
|
||||
- [x] Async support
|
||||
- [x] Add all events
|
||||
- [x] MultiLanguage: (i18n support)
|
||||
- [x] Core
|
||||
- [x] Console
|
||||
- [x] WebAPI
|
||||
- [x] HTTP API Server (fastapi)
|
||||
- [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...
|
||||
- [ ] [Documentation](docs/en/readme.md)
|
||||
- [ ] RCON System:
|
||||
- [x] Serving
|
||||
- [ ] Handle commands
|
||||
- [x] Client
|
||||
- [x] AES encryption
|
||||
- [ ] KuiToi System
|
||||
- [ ] Servers counter
|
||||
- [ ] Players counter
|
||||
- [ ] Etc.
|
||||
- [ ] [Documentation](./docs)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -78,6 +108,6 @@ Copyright (c) 2023 SantaSpeen (Maxim Khomutov)
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions:
|
||||
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
```
|
||||
|
||||
48
docs/cn/multilanguage/example.json
Normal file
48
docs/cn/multilanguage/example.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"": "基础阶段",
|
||||
"hello": "来自KuiToi服务器的问候!",
|
||||
"config_path": "使用{}进行配置。",
|
||||
"init_ok": "初始化完成。",
|
||||
"start": "服务器已启动!",
|
||||
"stop": "服务器已停止!",
|
||||
|
||||
"": "服务器认证",
|
||||
"auth_need_key": "需要BeamMP密钥才能启动!",
|
||||
"auth_empty_key": "BeamMP密钥为空!",
|
||||
"auth_cannot_open_browser": "无法打开浏览器:{}",
|
||||
"auth_use_link": "使用此链接:{}",
|
||||
|
||||
"": "GUI阶段",
|
||||
"GUI_yes": "是",
|
||||
"GUI_no": "否",
|
||||
"GUI_ok": "确定",
|
||||
"GUI_cancel": "取消",
|
||||
"GUI_need_key_message": "需要BeamMP密钥才能启动!\n是否在浏览器中打开链接以获取密钥?",
|
||||
"GUI_enter_key_message": "请输入密钥:",
|
||||
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
||||
|
||||
"": "Web阶段",
|
||||
"web_start": "WebAPI已启动{}(CTRL+C停止)",
|
||||
|
||||
"": "命令:man",
|
||||
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法:man COMMAND",
|
||||
"help_message_man": "显示COMMAND的帮助页面。",
|
||||
"man_for": "帮助页面",
|
||||
"man_message_not_found": "man:找不到帮助页面。",
|
||||
"man_command_not_found": "man:找不到\"{}\"命令!",
|
||||
|
||||
"": "命令:help",
|
||||
"man_message_help": "help - 显示命令的名称和简短描述。\n用法:help [--raw]\n命令`help`列出所有可用的命令,并为每个命令提供简短描述。",
|
||||
"help_message_help": "显示命令的名称和简短描述。",
|
||||
"help_command": "命令",
|
||||
"help_message": "文本",
|
||||
"help_message_not_found": "无文本",
|
||||
|
||||
"": "命令:stop",
|
||||
"man_message_stop": "stop - 关闭服务器。\n用法:stop",
|
||||
"help_message_stop": "关闭服务器。",
|
||||
|
||||
"": "命令:exit",
|
||||
"man_message_exit": "exit - 关闭服务器。\n用法:exit",
|
||||
"help_message_exit": "关闭服务器。"
|
||||
}
|
||||
3
docs/cn/multilanguage/readme.md
Normal file
3
docs/cn/multilanguage/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# MultiLanguage - i18n支持
|
||||
|
||||
在 [example.json](./example.json) 中是 [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json) 的副本。如果你想将其翻译成以前未翻译过的语言,或者更新现有的翻译,我将很高兴接受你的拉取请求。
|
||||
174
docs/cn/plugins/classes.md
Normal file
174
docs/cn/plugins/classes.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 传递的类
|
||||
|
||||
## 值得一读
|
||||
|
||||
1. 什么是“*args”和“**kwargs”? -> [habr上的文章 ↗](https://habr.com/ru/companies/ruvds/articles/482464/)
|
||||
|
||||
## KuiToi
|
||||
_`kt = KuiToi("PluginName"")`_
|
||||
|
||||
### kt.log
|
||||
_常量_\
|
||||
返回预配置的记录器
|
||||
|
||||
### kt.name
|
||||
_常量_\
|
||||
返回插件名称
|
||||
|
||||
### kt.dir
|
||||
_常量_\
|
||||
返回插件文件夹
|
||||
|
||||
### kt.open()
|
||||
_与open()参数相同_\
|
||||
在kt.dir中打开文件
|
||||
|
||||
### kt.register_event(event_name: str, event_func: function)
|
||||
_`event_name: str` -> 作为`event_func`调用的事件名称._\
|
||||
_`event_func: function` -> 要调用的函数._
|
||||
|
||||
在`event_func`中,可以传递普通函数或async - 不需要提前进行await。\
|
||||
您也可以创建自己的事件,并使用自己的名称注册任意数量的事件。
|
||||
|
||||
### kt.call_event(event_name: str, *args, **kwargs) -> list:
|
||||
_`event_name: str` -> 要调用的事件名称._\
|
||||
_`*args, **kwargs` -> 要传递给函数的参数._
|
||||
|
||||
### **async** kt.call_async_event(event_name: str, *args, **kwargs) -> list:
|
||||
_`event_name: str` -> 要调用的事件名称._\
|
||||
_`*args, **kwargs` -> 要传递给函数的参数._\
|
||||
_需要用`await`调用_
|
||||
|
||||
###### _建议阅读*args, **kwargs,链接在开头_
|
||||
所有事件的数据都以以下格式传递:`{"event_name": event_name, "args": args, "kwargs": kwargs}`\
|
||||
`args: list` -> 表示传递到事件中的数据数组\
|
||||
`kwargs: dict` -> 表示传递到事件中的数据字典
|
||||
数据将以数组形式从所有成功的波动中返回。
|
||||
|
||||
### kt.call_lua_event(event_name: str, *args) -> list:
|
||||
_`event_name: str` -> 要调用的事件名称._\
|
||||
_`*args` -> 要传递给函数的参数._
|
||||
|
||||
添加用于向后兼容性。\
|
||||
lua函数使用直接传递参数`lua_func(*args)`进行调用。
|
||||
|
||||
### kt.get_player([pid: int], [nick: str]) -> Player | None:
|
||||
_`pid: int` -> Player ID - 玩家标识符._\
|
||||
_`nick: str` -> Player Nick - 玩家昵称._
|
||||
|
||||
该方法通过其`pid`或`nick`返回玩家对象。\
|
||||
如果无法找到玩家,则返回 `None`。
|
||||
|
||||
### kt.get_players() -> List[Player] | list:
|
||||
|
||||
该方法返回所有玩家的数组。\
|
||||
如果没有玩家,则数组将为空。
|
||||
|
||||
### kt.players_counter() -> int:
|
||||
|
||||
该方法返回在线的玩家数量。
|
||||
|
||||
### kt.is_player_connected([pid: int], [nick: str]) -> bool:
|
||||
_`pid: int` -> Player ID - 玩家标识符._\
|
||||
_`nick: str` -> Player Nick - 玩家昵称._
|
||||
|
||||
该方法通过其`pid`或`nick`返回玩家对象。
|
||||
|
||||
## Player (或 Client)
|
||||
_`pl = kt.get_player()`_\
|
||||
_`pl = event_data['kwargs']['player']`_
|
||||
|
||||
### pl.log -> Logger
|
||||
_常量_\
|
||||
返回预配置的记录器
|
||||
|
||||
### pl.addr -> str
|
||||
_常量_\
|
||||
返回玩家的 IP 地址
|
||||
|
||||
### pl.pid -> int
|
||||
### pl.cid -> int
|
||||
_常量_\
|
||||
返回客户端的 ID _(pid: PlayerId = cid: ClientId)_
|
||||
|
||||
### pl.key -> str
|
||||
_常量_\
|
||||
返回在身份验证期间传递的密钥
|
||||
|
||||
### pl.nick -> str
|
||||
_变量_\
|
||||
从 BeamMP 服务器传递的昵称,可以更改,后果未知
|
||||
|
||||
### pl.roles -> str
|
||||
_变量_\
|
||||
从 BeamMP 服务器传递的角色,可以更改(如果设置了不正确的角色,可能会发生意外情况。)
|
||||
|
||||
### pl.guest -> bool
|
||||
_常量_\
|
||||
返回玩家是否为游客,从 BeamMP 服务器传递
|
||||
|
||||
### pl.identifiers -> dict
|
||||
_常量_\
|
||||
标识符,从 BeamMP 服务器传递。
|
||||
|
||||
### pl.ready -> bool
|
||||
_常量,由核心更改_\
|
||||
返回布尔值,如果为 True-> 玩家已下载所有资源,在地图上加载
|
||||
|
||||
### pl.cars -> dict
|
||||
_常量,由核心更改_\
|
||||
按类型返回汽车字典:
|
||||
|
||||
```python
|
||||
{
|
||||
1: {
|
||||
"packet": car_packet,
|
||||
"json": car_json,
|
||||
"json_ok": bool(car_json),
|
||||
"snowman": snowman,
|
||||
"over_spawn": (snowman and allow_snowman) or over_spawn,
|
||||
"pos": {
|
||||
"pos":[0,0,0],
|
||||
"rvel":[0,0,0],
|
||||
"rot":[0,0,0],
|
||||
"vel":[0,0,0],
|
||||
"tim":0,
|
||||
"ping":0
|
||||
}
|
||||
},
|
||||
2: ...
|
||||
}
|
||||
```
|
||||
其中 `1` - car_id\
|
||||
其中 `pkt` - 未处理的从客户端收到的数据包(仅供非常有经验的用户使用)\
|
||||
其中 `json` - 以 dict 形式存储的已处理的数据包\
|
||||
其中 `json_ok` - 核心是否能够处理数据包\
|
||||
其中 `snowman` - 车辆是否为雪人\
|
||||
其中 `over_spawn` - 车辆是否超过了生成限制(通过插件允许)\
|
||||
其中 `pos` - 车辆位置(通过 UDP 传递)
|
||||
|
||||
### pl.last_position -> dict
|
||||
_常量,由核心更改_
|
||||
返回玩家的最后位置
|
||||
|
||||
|
||||
### **async** pl.kick([reason: str = "Kicked!"]) -> None
|
||||
_`reason: str` -> 踢出理由,参数可选,默认值为 `Kicked!`_
|
||||
将玩家踢出服务器
|
||||
|
||||
### **async** pl.send_message(message: str, [to_all: bool = True]) -> None
|
||||
_`message: str` -> 消息文本,不带 "Server:"_
|
||||
_`to_all: bool` -> 是否向所有人发送此消息?参数可选,默认值为 `True`_
|
||||
向玩家或所有人发送消息
|
||||
|
||||
### **async** pl.send_event(event_name: str, event_data: Any, [to_all: bool = True]) -> None
|
||||
_`event_name: str` -> 要调用的事件名称_
|
||||
_`event_data: Any` -> 发送到事件的数据。_
|
||||
_`to_all: bool` -> 是否向所有人发送此消息?参数可选,默认值为 `True`_
|
||||
将事件发送到客户端。\
|
||||
如果 event_data 是 tuple、list、dict,则核心会通过 json.dumps(event_data) 将其转换为 json,然后再发送。\
|
||||
否则,数据将是字符串,不受限制;
|
||||
|
||||
### **async** pl.delete_car(self, car_id: int) -> None
|
||||
_`car_id: int` -> 要删除的车辆的 ID_
|
||||
删除玩家的车辆
|
||||
3
docs/cn/plugins/events_list.md
Normal file
3
docs/cn/plugins/events_list.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 所有可用事件列表
|
||||
|
||||
大多数事件将包含`pl = data ['kwargs'] ['player']`,可以在[这里](./classes.md)找到描述。
|
||||
37
docs/cn/plugins/examples/async_example.py
Normal file
37
docs/cn/plugins/examples/async_example.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Запуск процессов плагина
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
async def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
37
docs/cn/plugins/examples/example.py
Normal file
37
docs/cn/plugins/examples/example.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
def start():
|
||||
# Запуск процессов плагина
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
27
docs/cn/plugins/lua/example.lua
Normal file
27
docs/cn/plugins/lua/example.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
print("example.lua")
|
||||
|
||||
--CreateTimer Testing
|
||||
local mytimer = MP.CreateTimer()
|
||||
--.--.--.--.--.--.--.
|
||||
|
||||
--GetOSName Testing
|
||||
print("OS Name: "..MP.GetOSName())
|
||||
--.--.--.--.--.--.-
|
||||
|
||||
--GetServerVersion Testing
|
||||
local major, minor, patch = MP.GetServerVersion()
|
||||
print("Server Version: "..major.."."..minor.."."..patch)
|
||||
--.--.--.--.--.--.--.--.--
|
||||
|
||||
--Events Testing--
|
||||
function handleChat(player_id, player_name, message)
|
||||
print("Lua handleChat:", player_id, player_name, message, "; Uptime: "..mytimer:GetCurrent())
|
||||
return 1
|
||||
end
|
||||
|
||||
MP.RegisterEvent("onChatMessage", "handleChat")
|
||||
--.--.--.--.--.--.
|
||||
|
||||
function onInit()
|
||||
print("Initializing ready!")
|
||||
end
|
||||
23
docs/cn/plugins/lua/readme.md
Normal file
23
docs/cn/plugins/lua/readme.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# BeamMP Lua反馈支持
|
||||
|
||||
KiuToi几乎完全支持BeamMP的lua插件,所有必要的方法都已经创建,测试显示以下细节:
|
||||
|
||||
在KiuToi中没有支持:`MP.Set()`
|
||||
|
||||
#### Economic Rework V2.0(付费,Discord(RU):[Hlebushek](https://discordapp.com/users/449634697593749516))
|
||||
|
||||
1. 要获取`pluginPath`,需要:`debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")`,因为路径返回值中包含`@`,这破坏了插件。
|
||||
|
||||
#### Cobalt Essentials V1.7.5(免费,[github ↗](https://github.com/prestonelam2003/CobaltEssentials/))
|
||||
|
||||
1. 要获取`pluginPath`,需要:`debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")`,因为路径返回值中包含`@`,这破坏了插件。
|
||||
|
||||
### 工作原理
|
||||
|
||||
插件加载经过几个阶段:
|
||||
|
||||
1. 扫描`plugins/`文件夹
|
||||
2. 如果文件夹不在PyPlugins中,并且文件夹中存在`*.lua`,则添加它,例如`plugins/LuaPlugin`
|
||||
3. 然后从该文件夹中进行`lua.loadfile({filename})`(这是lua中的标准方法)
|
||||
4. 最后调用事件和函数`onInit()`
|
||||
5. 如果在执行`onInit()`期间没有发生错误,则可以通过`lua_plugins`命令看到这样的消息:`Lua plugins: LuaPlugin:ok`
|
||||
97
docs/cn/plugins/readme.md
Normal file
97
docs/cn/plugins/readme.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 插件系统
|
||||
|
||||
### 事件:[这里](./events_list.md)
|
||||
### 类:[这里](./classes.md)
|
||||
|
||||
## 使用带有“Dummy”的库
|
||||
###### (这意味着它没有服务器无法工作,但IDE将指导API)
|
||||
###### (库还在开发中)
|
||||
|
||||
* 使用pip:\
|
||||
`$ pip install KuiToi`
|
||||
* 从源代码安装:\
|
||||
`git clone https://github.com/KuiToi/KuiToi-PyLib`
|
||||
|
||||
## 示例
|
||||
|
||||
```python
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("ExamplePlugin")
|
||||
log = kt.log
|
||||
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
def load():
|
||||
# 初始化插件
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("插件已成功加载。")
|
||||
|
||||
|
||||
def start():
|
||||
# 启动插件进程
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "一些数据", data="一些数据也是")
|
||||
log.info("插件已成功启动。")
|
||||
|
||||
|
||||
def unload():
|
||||
# 结束所有进程的代码
|
||||
log.info("插件已成功卸载。")
|
||||
```
|
||||
|
||||
您还可以在[example.py](examples/example.py)中找到更广泛的示例。
|
||||
|
||||
* 建议在`load()`后使用`open()`,否则应使用`kt.load()`-在`plugin/<plugin_name>/<filename>`文件夹中创建一个文件
|
||||
* 创建自己的事件:`kt.register_event("my_event", my_event_function)`-
|
||||
* 调用事件:`kt.call_event("my_event")`
|
||||
* 使用数据调用事件:`kt.call_event("my_event", data, data2=data2)`
|
||||
* 基本事件:_稍后会写_
|
||||
|
||||
## 异步函数
|
||||
|
||||
支持async
|
||||
|
||||
```python
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# 初始化插件
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("插件已成功加载。")
|
||||
|
||||
|
||||
async def start():
|
||||
# 启动插件进程
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "一些数据", data="一些数据也是")
|
||||
log.info("插件已成功启动。")
|
||||
|
||||
|
||||
async def unload():
|
||||
# 结束所有进程的代码
|
||||
log.info("插件已成功卸载。")
|
||||
|
||||
```
|
||||
|
||||
您还可以在[async_example.py](examples/async_example.py)中找到更广泛的示例。
|
||||
|
||||
* 创建自己的事件:`kt.register_event("my_event", my_event_function)`(在register_event中检查函数)
|
||||
* 调用async事件:`kt.call_async_event("my_event")`
|
||||
* 使用数据调用async事件:`kt.call_async_event("my_event", data, data2=data2)`
|
||||
* 基本的async事件:_稍后会写_
|
||||
10
docs/cn/readme.md
Normal file
10
docs/cn/readme.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# KuiToi服务器文档
|
||||
|
||||
### 文档尚未完善,但终有一天会完善
|
||||
|
||||
1. 服务器设置和启动 - [这里](./setup)
|
||||
2. 插件和事件系统 - [这里](./plugins)
|
||||
3. Lua的细微差别 - [这里](./plugins/lua)
|
||||
4. 多语言支持 - [这里](./multilanguage)
|
||||
5. KuiToi WebAPI - [这里](./web)
|
||||
6. 将会有新的内容...
|
||||
88
docs/cn/setup/readme.md
Normal file
88
docs/cn/setup/readme.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 来自 KuiToi 服务器的问候
|
||||
|
||||
## 好的,让我们开始吧
|
||||
|
||||
###### _(这里是 Linux 的命令)_
|
||||
|
||||
* 运行它需要 **Python 3.10.x**!只有这个版本才能运行,Python 3.11 不支持...
|
||||
* 您可以像这样检查 Python 版本(你要在这里笑):
|
||||
```bash
|
||||
python3 --version # Python 3.10.6
|
||||
```
|
||||
* 克隆存储库并导航到它
|
||||
* 安装所有必需的内容
|
||||
* 然后,使用我的“脚本”,删除所有不必要的文件并移动到核心源
|
||||
```bash
|
||||
git clone -b Stable https://github.com/kuitoi/KuiToi-Server.git && cd KuiToi-Server
|
||||
pip install -r requirements.txt
|
||||
mv ./src/ $HOME/ktsrc/ && rm -rf ./* && mv $HOME/ktsrc/* . && rm -rf $HOME/ktsrc
|
||||
```
|
||||
* 这是如何检查服务器信息并启动它的方法:
|
||||
```bash
|
||||
python3 main.py --help # 显示所有可用命令
|
||||
python3 main.py # 启动服务器
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
* 启动后,将创建 `kuitoi.yaml`
|
||||
* 默认情况下,它如下所示:
|
||||
```yaml
|
||||
!!python/object:modules.ConfigProvider.config_provider.Config
|
||||
Auth:
|
||||
key: null
|
||||
private: true
|
||||
Game:
|
||||
map: gridmap_v2
|
||||
max_cars: 1
|
||||
players: 8
|
||||
Options:
|
||||
debug: false
|
||||
encoding: utf-8
|
||||
language: en
|
||||
log_chat: true
|
||||
speed_limit: 0
|
||||
use_lua: true
|
||||
use_queue: false
|
||||
Server:
|
||||
description: Welcome to KuiToi Server!
|
||||
name: KuiToi-Server
|
||||
server_ip: 0.0.0.0
|
||||
server_port: 30814
|
||||
WebAPI:
|
||||
enabled: false
|
||||
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
|
||||
server_ip: 127.0.0.1
|
||||
server_port: 8433
|
||||
```
|
||||
### Auth
|
||||
|
||||
* 如果您将 `private: false` 并且不设置 `key`,服务器将请求一个 BeamMP 密钥,没有它无法启动。
|
||||
* 输入 BeamMP 密钥后,服务器将出现在启动器列表中。
|
||||
* 您可以在此处获取密钥:[https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
|
||||
|
||||
### Game
|
||||
|
||||
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
|
||||
* `max_cars` - 每个玩家的最大汽车数量
|
||||
* `players` - 最大玩家数
|
||||
|
||||
### Options
|
||||
|
||||
* `debug` - 是否输出调试消息(仅适用于有经验的用户,会略微降低性能)
|
||||
* `encoding` - 使用哪种编码打开文件
|
||||
* `language` - 服务器将使用哪种语言启动(当前可用:en,ru)
|
||||
* `log_chat` - 是否将聊天输出到控制台
|
||||
* `speed_limit` - 下载 mod 的下载速度限制(以 MB/s 为单位)
|
||||
* `use_lua` - 启用 lua 支持
|
||||
* `use_queue` - 按队列下载 mod,即一次只能下载一个客户端
|
||||
|
||||
### Server
|
||||
|
||||
* `description` - BeamMP 启动器的服务器描述
|
||||
* `name` - BeamMP 启动器的服务器名称
|
||||
* `server_ip` - 分配给服务器的 IP 地址(仅适用于有经验的用户,默认为 0.0.0.0)
|
||||
* `server_port` - 服务器将在哪个端口上工作
|
||||
|
||||
### WebAPI
|
||||
##### _文档尚未准备好_
|
||||
13
docs/cn/web/readme.md
Normal file
13
docs/cn/web/readme.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 服务器的 WebAPI
|
||||
|
||||
## 可用的端点
|
||||
|
||||
* `/stop`:
|
||||
* 必需参数:
|
||||
* `secret_key` - 在服务器配置中指定的密钥
|
||||
|
||||
|
||||
* `/event.get`
|
||||
* 这个端点还没有准备好
|
||||
* 必需参数:
|
||||
* `secret_key` - 在服务器配置中指定的密钥
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} for config.",
|
||||
"init_ok": "Initializing ready.",
|
||||
"hello": "Greetings from KuiToi Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization complete.",
|
||||
"start": "Server started!",
|
||||
"stop": "Goodbye!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BEAM key needed for starting the server!",
|
||||
"auth_empty_key": "Key is empty!",
|
||||
"auth_cannot_open_browser": "Cannot open browser: {}",
|
||||
"auth_need_key": "A BeamMP key is required to start the server!",
|
||||
"auth_empty_key": "The BeamMP key is empty!",
|
||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||
"auth_use_link": "Use this link: {}",
|
||||
|
||||
"": "GUI phases",
|
||||
@@ -17,32 +17,32 @@
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "Ok",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
||||
"GUI_enter_key_message": "Please type your key:",
|
||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
||||
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain 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 running on {} (Press CTRL+C to quit)",
|
||||
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
|
||||
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Display the manual page for COMMAND.",
|
||||
"man_for": "Manual for command",
|
||||
"man_message_not_found": "man: Manual message not found.",
|
||||
"man_command_not_found": "man: command \"{}\" not found!",
|
||||
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Displays 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 - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
||||
"help_message_help": "Display names and brief descriptions of available commands",
|
||||
"man_message_help": "help - Displays the names and short descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands and a brief description of each command.",
|
||||
"help_message_help": "Displays the names and short descriptions of commands.",
|
||||
"help_command": "Command",
|
||||
"help_message": "Help message",
|
||||
"help_message_not_found": "No help message found",
|
||||
"help_message": "Description",
|
||||
"help_message_not_found": "No description available.",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_stop": "Server shutdown.",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_exit": "Server shutdown."
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# 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).\
|
||||
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.
|
||||
|
||||
298
docs/en/plugins/classes.md
Normal file
298
docs/en/plugins/classes.md
Normal file
@@ -0,0 +1,298 @@
|
||||
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
|
||||
|
||||
## Worth looking at
|
||||
|
||||
1. What are `*args` and `**kwargs`? -> [Post on Habr ↗](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 preconfigured logger.
|
||||
|
||||
### pl.addr -> str
|
||||
_Constant_\
|
||||
Returns the player's IP address.
|
||||
|
||||
### pl.pid -> int
|
||||
### pl.cid -> int
|
||||
_Constant_\
|
||||
Returns the client ID _(pid: PlayerId = cid: ClientId)_.
|
||||
|
||||
### pl.key -> str
|
||||
_Constant_\
|
||||
Returns the key passed during authorization.
|
||||
|
||||
### pl.nick -> str
|
||||
_Variable_\
|
||||
Nickname passed during authorization from the BeamMP server, can be changed, consequences are not tested.
|
||||
|
||||
### pl.roles -> str
|
||||
_Variable_\
|
||||
Role passed during authorization from the BeamMP server, can be changed (If the wrong role is set, unexpected behavior may occur.)
|
||||
|
||||
### pl.guest -> bool
|
||||
_Constant_\
|
||||
Returns whether the player is a guest, passed during authorization from the BeamMP server.
|
||||
|
||||
### pl.identifiers -> dict
|
||||
_Constant_\
|
||||
Identifiers passed during authorization from the BeamMP server.
|
||||
|
||||
### pl.ready -> bool
|
||||
_Constant, changed by the core_\
|
||||
Returns a bool value, if True -> player has downloaded all resources and loaded on the map.
|
||||
|
||||
### pl.cars -> dict
|
||||
_Constant, changed by the core_\
|
||||
Returns a dictionary of cars by type:
|
||||
|
||||
```python
|
||||
{
|
||||
1: {
|
||||
"packet": car_packet,
|
||||
"json": car_json,
|
||||
"json_ok": bool(car_json),
|
||||
"snowman": snowman,
|
||||
"over_spawn": (snowman and allow_snowman) or over_spawn,
|
||||
"pos": {
|
||||
"pos":[0,0,0],
|
||||
"rvel":[0,0,0],
|
||||
"rot":[0,0,0],
|
||||
"vel":[0,0,0],
|
||||
"tim":0,
|
||||
"ping":0
|
||||
}
|
||||
},
|
||||
2: ...
|
||||
}
|
||||
```
|
||||
Where `1` - car_id\
|
||||
Where `pkt` - Unprocessed packet that came from the client (For very experienced users)\
|
||||
Where `json` - Processed packet stored as dict\
|
||||
Where `json_ok` - Whether the core was able to process the packet\
|
||||
Where `snowman` - Is the car a snowman\
|
||||
Where `over_spawn` - Is the car spawned over the limit (Allowed through plugins)\
|
||||
Where `pos` - Car position (Passed through UDP)
|
||||
|
||||
### pl.last_position -> dict
|
||||
_Constant, changed by the core_\
|
||||
Returns the player's last position
|
||||
|
||||
### **async** pl.kick([reason: str = "Kicked!"]) -> None
|
||||
_`reason: str` -> Kick reason. Parameter is optional, by default: `Kicked!`_\
|
||||
Kicks the player from the server.
|
||||
|
||||
### **async** pl.send_message(message: str, [to_all: bool = True]) -> None
|
||||
_`message: str` -> Message text, sent without "Server:"_\
|
||||
_`to_all: bool` -> Should this message be sent to everyone? Parameter is optional, by default: `True`_\
|
||||
Sends a message to the player or everyone.
|
||||
|
||||
### **async** pl.send_event(event_name: str, event_data: Any, [to_all: bool = True]) -> None
|
||||
_`event_name: str` -> Name of the event that will be called_\
|
||||
_`event_data: Any` -> Data sent to the event._\
|
||||
_`to_all: bool` -> Should this message be sent to everyone? Parameter is optional, by default: `True`_\
|
||||
Sends an event to the client.\
|
||||
If event_data is a tuple, list, dict, then before sending the core converts it to JSON via json.dumps(event_data)\
|
||||
Otherwise, the data will be a string without regulation.
|
||||
|
||||
### **async** pl.delete_car(self, car_id: int) -> None
|
||||
_`car_id: int` -> Car ID_\
|
||||
Deletes the player's car.
|
||||
4
docs/en/plugins/events_list.md
Normal file
4
docs/en/plugins/events_list.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# List of available events
|
||||
|
||||
Most events will receive `pl = data['kwargs']['player']`, you can find a description [here](./classes.md)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import KuiToi # Import server object
|
||||
|
||||
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
|
||||
log = beam.log # Use logger from server
|
||||
|
||||
|
||||
def on_load():
|
||||
# When plugin initialization Server uses plugin.load() to load plugin.
|
||||
# def load(): is really needed
|
||||
log.info(beam.name)
|
||||
|
||||
|
||||
# Events handlers
|
||||
|
||||
def on_started():
|
||||
# Simple event handler
|
||||
log.info("Server starting...")
|
||||
|
||||
|
||||
# Simple event register
|
||||
beam.register_event("on_started", on_started)
|
||||
|
||||
|
||||
def any_func(data=None):
|
||||
# Custom event handler
|
||||
log.info(f"Data from any_func: {data}")
|
||||
|
||||
|
||||
# Create custom event
|
||||
beam.register_event("my_event", any_func)
|
||||
|
||||
# Call custom event
|
||||
beam.call_event("my_event")
|
||||
beam.call_event("my_event", "Some data")
|
||||
# This will be an error since any_func accepts only one argument at the input
|
||||
beam.call_event("my_event", "Some data", "Some data1")
|
||||
37
docs/en/plugins/examples/async_example.py
Normal file
37
docs/en/plugins/examples/async_example.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Запуск процессов плагина
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
async def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
37
docs/en/plugins/examples/example.py
Normal file
37
docs/en/plugins/examples/example.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
def start():
|
||||
# Запуск процессов плагина
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
27
docs/en/plugins/lua/example.lua
Normal file
27
docs/en/plugins/lua/example.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
print("example.lua")
|
||||
|
||||
--CreateTimer Testing
|
||||
local mytimer = MP.CreateTimer()
|
||||
--.--.--.--.--.--.--.
|
||||
|
||||
--GetOSName Testing
|
||||
print("OS Name: "..MP.GetOSName())
|
||||
--.--.--.--.--.--.-
|
||||
|
||||
--GetServerVersion Testing
|
||||
local major, minor, patch = MP.GetServerVersion()
|
||||
print("Server Version: "..major.."."..minor.."."..patch)
|
||||
--.--.--.--.--.--.--.--.--
|
||||
|
||||
--Events Testing--
|
||||
function handleChat(player_id, player_name, message)
|
||||
print("Lua handleChat:", player_id, player_name, message, "; Uptime: "..mytimer:GetCurrent())
|
||||
return 1
|
||||
end
|
||||
|
||||
MP.RegisterEvent("onChatMessage", "handleChat")
|
||||
--.--.--.--.--.--.
|
||||
|
||||
function onInit()
|
||||
print("Initializing ready!")
|
||||
end
|
||||
23
docs/en/plugins/lua/readme.md
Normal file
23
docs/en/plugins/lua/readme.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Providing Backward Compatibility for BeamMP Lua
|
||||
|
||||
KiuToi provides almost full support for lua plugins with BeamMP. All necessary methods have been created, and testing has revealed the following nuances:
|
||||
|
||||
KiuToi does not support: `MP.Set()`
|
||||
|
||||
#### Economic Rework V2.0 (Paid, Discord (RU): [Hlebushek](https://discordapp.com/users/449634697593749516))
|
||||
|
||||
1. To obtain `pluginPath`, use: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` as the path returns with `@`, which broke the plugin.
|
||||
|
||||
#### Cobalt Essentials V1.7.5 (Free, [github](https://github.com/prestonelam2003/CobaltEssentials/))
|
||||
|
||||
1. To obtain `pluginPath`, use: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` as the path returns with `@`, which broke the plugin.
|
||||
|
||||
### A Little About How it Works
|
||||
|
||||
Plugin loading goes through several stages:
|
||||
|
||||
1. The `plugins/` folder is scanned.
|
||||
2. If the folder is not in PyPlugins and there are `*.lua` files in the folder, then it is added as a plugin folder, let's say it will be `plugins/LuaPlugin`
|
||||
3. Next, `lua.loadfile({filename})` is performed from this folder (this is the standard method in lua).
|
||||
4. Finally, the `onInit()` function is called, and an event is triggered.
|
||||
5. If no errors occur during the execution of `onInit()`, you can see the message `Lua plugins: LuaPlugin:ok` through the `lua_plugins` command.
|
||||
@@ -1,9 +1,16 @@
|
||||
# Plugins System
|
||||
Это описание системы плагинов для KuiToi сервера на Python:
|
||||
|
||||
## Install
|
||||
###### (Lib can't ready to use)
|
||||
## Events
|
||||
### Events list: [here](./events_list.md)
|
||||
|
||||
* From pip:\
|
||||
## Classes
|
||||
### Classes list: [here](./classes.md)
|
||||
|
||||
## Installing the library with "stubs"
|
||||
###### (This means it won't work without the server, but your IDE will suggest the API)
|
||||
###### (The library is still in development)
|
||||
|
||||
* Using pip:\
|
||||
`$ pip install KuiToi`
|
||||
* From source:\
|
||||
`git clone https://github.com/KuiToi/KuiToi-PyLib`
|
||||
@@ -11,22 +18,83 @@
|
||||
## Example
|
||||
|
||||
```python
|
||||
import KuiToi
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
beam = KuiToi("TestPlugin")
|
||||
logger = beam.log
|
||||
kt = KuiToi("ExamplePlugin")
|
||||
log = kt.log
|
||||
|
||||
def load(): # Plugins load from here
|
||||
print(beam.name)
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
def on_started():
|
||||
logger.info("Server starting...")
|
||||
def load():
|
||||
# Plugin initialization
|
||||
kt.register_event("my_event", my_event_handler)
|
||||
log.info("Plugin loaded successfully.")
|
||||
|
||||
beam.register_event("on_started", on_started)
|
||||
|
||||
def start():
|
||||
# Starting plugin processes
|
||||
kt.call_event("my_event")
|
||||
kt.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Plugin started successfully.")
|
||||
|
||||
|
||||
def unload():
|
||||
# Code that ends all processes
|
||||
log.info("Plugin unloaded successfully.")
|
||||
```
|
||||
|
||||
* Basic Events: ['on_started', 'on_auth, 'on_stop']
|
||||
* Create new event : `beam.register_event("my_event", my_event_function)`
|
||||
* Call event: `beam.call_event("my_event")`
|
||||
* Call event with some data: `beam.call_event("my_event", data, data2)`
|
||||
* Calls _**can't support**_ like this: `beam.call_event("my_event", data=data)`
|
||||
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.
|
||||
* Creating your own event: `kt.register_event("my_event", my_event_function)` -
|
||||
* Calling an event: `kt.call_event("my_event")`
|
||||
* Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
|
||||
* Base events: _To be added later_
|
||||
|
||||
## Async functions
|
||||
|
||||
Async support is available
|
||||
|
||||
```python
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# Plugin initialization
|
||||
kt.register_event("my_event", my_event_handler)
|
||||
log.info("Plugin loaded successfully.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Starting plugin processes
|
||||
await kt.call_async_event("my_event")
|
||||
await kt.call_async_event("my_event", "Some data", data="some data too")
|
||||
log.info("Plugin started successfully.")
|
||||
|
||||
|
||||
async def unload():
|
||||
# Code that ends all processes
|
||||
log.info("Plugin unloaded successfully.")
|
||||
|
||||
```
|
||||
|
||||
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)
|
||||
* 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)`
|
||||
* Base async events: _To be added later_
|
||||
@@ -1,9 +1,10 @@
|
||||
# Documentation for KuiToi Server
|
||||
|
||||
#### The documentation has not been perfected yet, but one day it will definitely happen
|
||||
### The documentation is not yet perfect, but someday it will be.
|
||||
|
||||
1. Setup and Start server - [here](setup)
|
||||
2. Plugins and Events system - [here](plugins)
|
||||
3. MultiLanguage - [here](./multilanguage)
|
||||
4. RESP API - [here](./web)
|
||||
5. Something new
|
||||
1. Setup and Launching the Server - [here](./setup)
|
||||
2. Plugins and Event System - [here](./plugins)
|
||||
3. Nuances of Working with Lua - [here](./plugins/lua)
|
||||
4. Multilanguage Support - [here](./multilanguage)
|
||||
5. KuiToi WebAPI - [here](./web)
|
||||
6. Something new will be added here soon...
|
||||
@@ -1,18 +1,32 @@
|
||||
# Hello from KuiToi Server
|
||||
# Greetings from KuiToi Server
|
||||
|
||||
## Start
|
||||
## Well, let's start
|
||||
|
||||
* Need **Python 3.10.x** to start!
|
||||
* After cloning use this:
|
||||
###### _(Here are the commands for Linux)_
|
||||
|
||||
* **Python 3.10.x** is required to run it! Only this version works, it won't work on Python 3.11...
|
||||
* You can check your Python version like this (you have to laugh here):
|
||||
```bash
|
||||
$ python3 --version # Python 3.10.6
|
||||
$ python3 main.py --help # Show help message
|
||||
$ python3 main.py # Start server
|
||||
python3 --version # Python 3.10.6
|
||||
```
|
||||
* Clone the repository and navigate to it
|
||||
* Install everything necessary
|
||||
* Then, using my "script", remove all unnecessary files and move to the core source
|
||||
```bash
|
||||
git clone -b Stable https://github.com/kuitoi/KuiToi-Server.git && cd KuiToi-Server
|
||||
pip install -r requirements.txt
|
||||
mv ./src/ $HOME/ktsrc/ && rm -rf ./* && mv $HOME/ktsrc/* . && rm -rf $HOME/ktsrc
|
||||
```
|
||||
* Here's how you can check server info and start it:
|
||||
```bash
|
||||
python3 main.py --help # Shows all available commands
|
||||
python3 main.py # Starts the server
|
||||
```
|
||||
|
||||
## Setup
|
||||
## Configuration
|
||||
|
||||
* After starting server creating `kuitoi.yaml`; Default:
|
||||
* After starting, `kuitoi.yaml` will be created
|
||||
* By default, it looks like this:
|
||||
```yaml
|
||||
!!python/object:modules.ConfigProvider.config_provider.Config
|
||||
Auth:
|
||||
@@ -22,12 +36,53 @@ Game:
|
||||
map: gridmap_v2
|
||||
max_cars: 1
|
||||
players: 8
|
||||
Options:
|
||||
debug: false
|
||||
encoding: utf-8
|
||||
language: en
|
||||
log_chat: true
|
||||
speed_limit: 0
|
||||
use_lua: true
|
||||
use_queue: false
|
||||
Server:
|
||||
debug: true
|
||||
description: This server uses KuiToi!
|
||||
description: Welcome to KuiToi Server!
|
||||
name: KuiToi-Server
|
||||
server_ip: 0.0.0.0
|
||||
server_port: 30814
|
||||
WebAPI:
|
||||
enabled: false
|
||||
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
|
||||
server_ip: 127.0.0.1
|
||||
server_port: 8433
|
||||
```
|
||||
* Server can't start without BEAM Auth.key
|
||||
### Auth
|
||||
|
||||
* If you set `private: false` and don't set a `key`, the server will request a BeamMP key and won't start without it.
|
||||
* After entering a BeamMP key, the server will appear in the launcher list.
|
||||
* You can get the key here: [https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
|
||||
|
||||
### 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.
|
||||
* `max_cars` - Maximum number of cars per player
|
||||
* `players` - Maximum number of players
|
||||
|
||||
### Options
|
||||
|
||||
* `debug` - Whether to output debug messages (for experienced users only, slightly reduces performance)
|
||||
* `encoding` - Which encoding to use to open files
|
||||
* `language` - Which language the server will start with (currently available: en, ru)
|
||||
* `log_chat` - Whether to output chat to the console
|
||||
* `speed_limit` - Download speed limit for mods (in MB/s)
|
||||
* `use_lua` - Enable lua support
|
||||
* `use_queue` - Download mods in queue, i.e. only 1 client can download at a time
|
||||
|
||||
### Server
|
||||
|
||||
* `description` - Server description for the BeamMP launcher
|
||||
* `name` - Server name for the BeamMP launcher
|
||||
* `server_ip` - IP address to assign to the server (for experienced users only, defaults to 0.0.0.0)
|
||||
* `server_port` - On which port the server will work
|
||||
|
||||
### WebAPI
|
||||
##### _Docs are not ready_
|
||||
@@ -1,3 +1,15 @@
|
||||
# Web RESP API for the Server
|
||||
Here's the translation of the readme.txt content:
|
||||
|
||||
In development...
|
||||
# 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,5 @@
|
||||
# Choose your language
|
||||
|
||||
### [English](./en)
|
||||
### [Русский](./ru)
|
||||
### [English](./en) (AI translation)
|
||||
### [Chinese](./cn) (AI translation)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# MultiLanguage - Поддержка i18n
|
||||
|
||||
В [example.json](./example.json) это копия [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json)\
|
||||
Если есть желание перевести на не переведённый ранее язык, или обновить уже существующий перевод буду рад вашим пул реквестам.
|
||||
|
||||
175
docs/ru/plugins/classes.md
Normal file
175
docs/ru/plugins/classes.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Передаваемые классы
|
||||
|
||||
## Стоит ознакомится
|
||||
|
||||
1. Что такое `*args` и `**kwargs`? -> [Пост на habr](https://habr.com/ru/companies/ruvds/articles/482464/)
|
||||
|
||||
## KuiToi
|
||||
_`kt = KuiToi("PluginName"")`_
|
||||
|
||||
### kt.log
|
||||
_Константа_\
|
||||
Вернёт преднастроенный логгер
|
||||
|
||||
### kt.name
|
||||
_Константа_\
|
||||
Вернёт имя плагина
|
||||
|
||||
### kt.dir
|
||||
_Константа_\
|
||||
Вернёт папку плагина
|
||||
|
||||
### kt.open()
|
||||
_Параметры как у open()_\
|
||||
Открывает файл в kt.dir
|
||||
|
||||
### kt.register_event(event_name: str, event_func: function)
|
||||
_`event_name: str` -> Имя ивента, по которому будет вызвана `event_func`._\
|
||||
_`event_func: function` -> Функция, которая будет вызвана._
|
||||
|
||||
В `event_func` можно передавать как обычную функцию, так и async - await не нужно делать заранее.\
|
||||
Ивенты можно создавать так же свои, со своим именем.\
|
||||
Зарегистрировать можно не ограниченное кол-во ивентов.
|
||||
|
||||
### kt.call_event(event_name: str, *args, **kwargs) -> list:
|
||||
_`event_name: str` -> Имя ивента, который будет вызван._\
|
||||
_`*args, **kwargs` -> Аргументы, передаваемые во функции._
|
||||
|
||||
### **async** kt.call_async_event(event_name: str, *args, **kwargs) -> list:
|
||||
_`event_name: str` -> Имя ивента, который будет вызван._\
|
||||
_`*args, **kwargs` -> Аргументы, передаваемые во функции._\
|
||||
_Необходимо вызывать с `await`_
|
||||
|
||||
###### _Советую ознакомиться с *args, **kwargs_, ссылка есть в начале
|
||||
Данные во все ивенты приходят по типу: `{"event_name": event_name, "args": args, "kwargs": kwargs}`\
|
||||
`args: list` -> Представляет из себя массив данных, которые переданы в ивент\
|
||||
`kwargs: dict` -> Представляет из себя словарь данных, которые переданы в ивент
|
||||
Данные вернутся от всех удачных волнений в массиве.
|
||||
|
||||
### kt.call_lua_event(event_name: str, *args) -> list:
|
||||
_`event_name: str` -> Имя ивента, который будет вызван._\
|
||||
_`*args` -> Аргументы, передаваемые во функции._
|
||||
|
||||
Добавлено для поддержки обратной совместимости.\
|
||||
lua функция вызывается с прямой передачей аргументов `lua_func(*args)`
|
||||
|
||||
### kt.get_player([pid: int], [nick: str]) -> Player | None:
|
||||
_`pid: int` -> Player ID - Идентификатор игрока._\
|
||||
_`nick: str` -> Player Nick - Ник игрока._
|
||||
|
||||
Метод возвращает объект игрока по его `pid` или `nick`.\
|
||||
Если не удалось найти игрока вернётся `None`.
|
||||
|
||||
### kt.get_players() -> List[Player] | list:
|
||||
|
||||
Метод возвращает массив со всеми игроками.\
|
||||
Массив будет пустой, если игроков нет.
|
||||
|
||||
### kt.players_counter() -> int:
|
||||
|
||||
Метод возвращает количество игроков, которые сейчас онлайн.
|
||||
|
||||
### kt.is_player_connected([pid: int], [nick: str]) -> bool:
|
||||
_`pid: int` -> Player ID - Идентификатор игрока._\
|
||||
_`nick: str` -> Player Nick - Ник игрока._
|
||||
|
||||
Метод возвращает объект игрока по его `pid`, `nick`.
|
||||
|
||||
## Player (или Client)
|
||||
_`pl = kt.get_player()`_\
|
||||
_`pl = event_data['kwargs']['player']`_
|
||||
|
||||
### pl.log -> Logger
|
||||
_Константа_\
|
||||
Вернёт преднастроенный логгер
|
||||
|
||||
### pl.addr -> str
|
||||
_Константа_\
|
||||
Вернёт IP адрес игрока
|
||||
|
||||
### pl.pid -> int
|
||||
### pl.cid -> int
|
||||
_Константа_\
|
||||
Вернёт id клиента _(pid: PlayerId = cid: ClientId)_
|
||||
|
||||
### pl.key -> str
|
||||
_Константа_\
|
||||
Вернёт ключ, переданный при авторизации
|
||||
|
||||
### pl.nick -> str
|
||||
_Переменная_\
|
||||
Ник, переданные при авторизации от сервера BeamMP, можно изменить, последствия не проверенны
|
||||
|
||||
### pl.roles -> str
|
||||
_Переменная_\
|
||||
Роль, переданная при авторизации от сервера BeamMP, можно изменить (Если установить не верную роль, могут происходить неожиданности.)
|
||||
|
||||
### pl.guest -> bool
|
||||
_Константа_\
|
||||
Вернёт является ли игрок гостем, передаётся при авторизации от сервера BeamMP
|
||||
|
||||
### pl.identifiers -> dict
|
||||
_Константа_\
|
||||
Идентификаторы, передаются при авторизации от сервера BeamMP.
|
||||
|
||||
### pl.ready -> bool
|
||||
_Константа, меняется ядром_\
|
||||
Вернёт bool значение, если True -> игрок скачал все ресурсы, прогрузился на карте
|
||||
|
||||
### pl.cars -> dict
|
||||
_Константа, меняется ядром_\
|
||||
Возвращает словарь автомобилей по типу:
|
||||
|
||||
```python
|
||||
{
|
||||
1: {
|
||||
"packet": car_packet,
|
||||
"json": car_json,
|
||||
"json_ok": bool(car_json),
|
||||
"snowman": snowman,
|
||||
"over_spawn": (snowman and allow_snowman) or over_spawn,
|
||||
"pos": {
|
||||
"pos":[0,0,0],
|
||||
"rvel":[0,0,0],
|
||||
"rot":[0,0,0],
|
||||
"vel":[0,0,0],
|
||||
"tim":0,
|
||||
"ping":0
|
||||
}
|
||||
},
|
||||
2: ...
|
||||
}
|
||||
```
|
||||
Где `1` - car_id\
|
||||
Где `pkt` - Необработанный пакет который пришел от клиента (Для очень опытных пользователй) \
|
||||
Где `json` - Обработанный пакет, хранящийся в виде dict\
|
||||
Где `json_ok` - Смогло ли ядро обработать пакет\
|
||||
Где `snowman` - Снеговик ли машина\
|
||||
Где `over_spawn` - Заспавнена ли машина сверх лимита (Разрешается через плагины)\
|
||||
Где `pos` - Позиция машины (Передаётся через udp)
|
||||
|
||||
### pl.last_position -> dict
|
||||
_Константа, меняется ядром_
|
||||
Возвращает последнюю позицию игрока
|
||||
|
||||
|
||||
### **async** pl.kick([reason: str = "Kicked!"]) -> None
|
||||
_`reason: str` -> Причина кика. Параметр не обязателен, по дефолту: `Kicked!`_
|
||||
Кикает игрока с сервера
|
||||
|
||||
### **async** pl.send_message(message: str, [to_all: bool = True]) -> None
|
||||
_`message: str` -> Текст сообщения, отправляется без "Server:"_
|
||||
_`to_all: bool` -> Нужно ли отправить это сообщение всем? Параметр не обязателен, по дефолту: `True`_
|
||||
Отправляет сообщение игроку или всем сразу
|
||||
|
||||
### **async** pl.send_event(event_name: str, event_data: Any, [to_all: bool = True]) -> None
|
||||
_`event_name: str` -> Имя ивента, который будет вызван_
|
||||
_`event_data: Any` -> Отправляемые данные в ивент._
|
||||
_`to_all: bool` -> Нужно ли отправить это сообщение всем? Параметр не обязателен, по дефолту: `True`_
|
||||
Отправляет ивент на клиент.\
|
||||
Если event_data будет tuple, list, dict, то перед отправкой ядро конвертирует в json через json.dumps(event_data)
|
||||
Иначе данные будут строкой, без регуляции;
|
||||
|
||||
### **async** pl.delete_car(self, car_id: int) -> None
|
||||
_`car_id: int` -> Идентификатор машины_
|
||||
Удалят машину у игрока
|
||||
5
docs/ru/plugins/events_list.md
Normal file
5
docs/ru/plugins/events_list.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Список ивентов
|
||||
|
||||
В большинство ивентов будет приходить `pl = data['kwargs']['player']`, описание можно найти [тут](./classes.md)
|
||||
|
||||
###
|
||||
@@ -1,36 +0,0 @@
|
||||
import KuiToi # Import server object
|
||||
|
||||
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
|
||||
log = beam.log # Use logger from server
|
||||
|
||||
|
||||
def on_load():
|
||||
# When plugin initialization Server uses plugin.load() to load plugin.
|
||||
# def load(): is really needed
|
||||
log.info(beam.name)
|
||||
|
||||
|
||||
# Events handlers
|
||||
|
||||
def on_started():
|
||||
# Simple event handler
|
||||
log.info("Server starting...")
|
||||
|
||||
|
||||
# Simple event register
|
||||
beam.register_event("on_started", on_started)
|
||||
|
||||
|
||||
def any_func(data=None):
|
||||
# Custom event handler
|
||||
log.info(f"Data from any_func: {data}")
|
||||
|
||||
|
||||
# Create custom event
|
||||
beam.register_event("my_event", any_func)
|
||||
|
||||
# Call custom event
|
||||
beam.call_event("my_event")
|
||||
beam.call_event("my_event", "Some data")
|
||||
# This will be an error since any_func accepts only one argument at the input
|
||||
beam.call_event("my_event", "Some data", "Some data1")
|
||||
37
docs/ru/plugins/examples/async_example.py
Normal file
37
docs/ru/plugins/examples/async_example.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Запуск процессов плагина
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
async def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
37
docs/ru/plugins/examples/example.py
Normal file
37
docs/ru/plugins/examples/example.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
def start():
|
||||
# Запуск процессов плагина
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
27
docs/ru/plugins/lua/example.lua
Normal file
27
docs/ru/plugins/lua/example.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
print("example.lua")
|
||||
|
||||
--CreateTimer Testing
|
||||
local mytimer = MP.CreateTimer()
|
||||
--.--.--.--.--.--.--.
|
||||
|
||||
--GetOSName Testing
|
||||
print("OS Name: "..MP.GetOSName())
|
||||
--.--.--.--.--.--.-
|
||||
|
||||
--GetServerVersion Testing
|
||||
local major, minor, patch = MP.GetServerVersion()
|
||||
print("Server Version: "..major.."."..minor.."."..patch)
|
||||
--.--.--.--.--.--.--.--.--
|
||||
|
||||
--Events Testing--
|
||||
function handleChat(player_id, player_name, message)
|
||||
print("Lua handleChat:", player_id, player_name, message, "; Uptime: "..mytimer:GetCurrent())
|
||||
return 1
|
||||
end
|
||||
|
||||
MP.RegisterEvent("onChatMessage", "handleChat")
|
||||
--.--.--.--.--.--.
|
||||
|
||||
function onInit()
|
||||
print("Initializing ready!")
|
||||
end
|
||||
23
docs/ru/plugins/lua/readme.md
Normal file
23
docs/ru/plugins/lua/readme.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Обеспечение обратной поддержки BeamMP Lua
|
||||
|
||||
В KiuToi есть практически полная поддержка lua плагинов с BeamMP, все необходимые методы созданы, тестирование показало следующие нюансы:
|
||||
|
||||
В KiuToi не будет поддержки: `MP.Set()`
|
||||
|
||||
#### Economic Rework V2.0 (Платный, Discord (RU): [Hlebushek](https://discordapp.com/users/449634697593749516))
|
||||
|
||||
1. Для получения `pluginPath` нужно: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` так как пусть возвращается с `@`, что сломало плагин.
|
||||
|
||||
#### Cobalt Essentials V1.7.5 (Бесплатный, [github](https://github.com/prestonelam2003/CobaltEssentials/))
|
||||
|
||||
1. Для получения `pluginPath` нужно: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` так как пусть возвращается с `@`, что сломало плагин.
|
||||
|
||||
### Немного о принципе работы
|
||||
|
||||
Загрузка плагина проходит в несколько этапов:
|
||||
|
||||
1. Сканируется папка `plugins/`
|
||||
2. Если папки нет в PyPlugins и в папке есть `*.lua`, то она добавляется, допустим это будет `plugins/LuaPlugin`
|
||||
3. Далее из этой папки проходит `lua.loadfile({filename})` (Это стандартный метод в lua)
|
||||
4. И в конце вызывается ивент и функция `onInit()`
|
||||
5. Если во время выполнения `onInit()` не произошло ошибок, можно будет увидеть через команду `lua_plugins` такое сообщение: `Lua plugins: LuaPlugin:ok`
|
||||
@@ -1,5 +1,8 @@
|
||||
# Система плагинов
|
||||
|
||||
### Ивенты: [тут](./events_list.md)
|
||||
### Классы: [тут](./classes.md)
|
||||
|
||||
## Установка библиотеки с "Заглушками"
|
||||
###### (Это значит, что не будет работать без сервера, но IDE подскажет API)
|
||||
###### (Библиотека ещё в разработке)
|
||||
@@ -12,24 +15,82 @@
|
||||
## Пример
|
||||
|
||||
```python
|
||||
import KuiToi
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
beam = KuiToi("TestPlugin")
|
||||
logger = beam.log
|
||||
kt = KuiToi("ExamplePlugin")
|
||||
log = kt.log
|
||||
|
||||
def load(): # Plugins load from here
|
||||
print(beam.name)
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
def on_started():
|
||||
logger.info("Server starting...")
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
def start():
|
||||
# Запуск процессов плагина
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
```
|
||||
Так же более обширный пример можно найти в [example.py](examples/example.py)
|
||||
|
||||
* Рекомендуется использовать `open()` после `load()`, иначе стоит использовать `kt.load()` - Создаёт файл в папке `plugin/<plugin_name>/<filename>`
|
||||
* Создание своего ивента : `kt.register_event("my_event", my_event_function)` -
|
||||
* Вызов ивента: `kt.call_event("my_event")`
|
||||
* Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)`
|
||||
* Базовые ивенты: _Позже напишу_
|
||||
|
||||
## Async функции
|
||||
|
||||
Поддержка async есть
|
||||
|
||||
```python
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# Инициализация плагина
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Запуск процессов плагина
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
async def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
|
||||
beam.register_event("on_started", on_started)
|
||||
```
|
||||
|
||||
Так же более обширный пример можно найти в [example.py](./example.py)
|
||||
Так же более обширный пример можно найти в [async_example.py](examples/async_example.py)
|
||||
|
||||
* Базовые ивенты: ['on_started', 'on_auth, 'on_stop']
|
||||
* Создание своего ивента : `beam.register_event("my_event", my_event_function)`
|
||||
* Вызов ивента: `beam.call_event("my_event")`
|
||||
* Вызов ивента с данными: `beam.call_event("my_event", data, data2)`
|
||||
* Вызовы с указанием переменой _**не поддерживаются**_: `beam.call_event("my_event", data=data)`
|
||||
* Создание своего ивента: `kt.register_event("my_event", my_event_function)` (в register_event стоит проверка на функцию)
|
||||
* Вызов async ивента: `kt.call_async_event("my_event")`
|
||||
* Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)`
|
||||
* Базовые async ивенты: _Позже напишу_
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
1. Настройка и Запуск сервера - [тута](./setup)
|
||||
2. Плагины и Ивент система - [тута](./plugins)
|
||||
3. Мультиязычность - [тута](./multilanguage)
|
||||
4. RESP API - [тута](./web)
|
||||
5. Тут будет что-то ново....
|
||||
3. Ньансы работы Lua - [тута](./plugins/lua)
|
||||
4. Мультиязычность - [тута](./multilanguage)
|
||||
5. KuiToi WebAPI - [тута](./web)
|
||||
6. Тут будет что-то новое....
|
||||
|
||||
@@ -36,14 +36,54 @@ Game:
|
||||
map: gridmap_v2
|
||||
max_cars: 1
|
||||
players: 8
|
||||
Server:
|
||||
Options:
|
||||
debug: false
|
||||
description: This server uses KuiToi!
|
||||
encoding: utf-8
|
||||
language: en
|
||||
log_chat: true
|
||||
speed_limit: 0
|
||||
use_lua: true
|
||||
use_queue: false
|
||||
Server:
|
||||
description: Welcome to KuiToi Server!
|
||||
name: KuiToi-Server
|
||||
server_ip: 0.0.0.0
|
||||
server_port: 30814
|
||||
WebAPI:
|
||||
enabled: false
|
||||
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
|
||||
server_ip: 127.0.0.1
|
||||
server_port: 8433
|
||||
```
|
||||
### Auth
|
||||
|
||||
* Если поставить `private: false` и не установить `key`, то сервер запросит BeamMP ключ, без него не запустится.
|
||||
* Введя BeamMP ключ сервер появится в списке лаунчера.
|
||||
* Взять ключ можно тут: [https://beammp.com/k/keys](https://beammp.com/k/keys)
|
||||
|
||||
### Game
|
||||
|
||||
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
|
||||
* `max_cars` - Максимальное количество машин на игрока
|
||||
* `players` - Максимально количество игроков
|
||||
|
||||
### Options
|
||||
|
||||
* `debug` - Нужно ли выводить debug сообщения (только для опытных пользователей, немного теряется в производительности)
|
||||
* `encoding` - С какой кодировкой открывать файлы
|
||||
* `language` - С каким языком запустится сервер (Доступные на данный момент: en, ru)
|
||||
* `log_chat` - Нужно-ли выводить чат в консоль
|
||||
* `speed_limit` - Ограничение скорости на скачивание модов (В Мб/с)
|
||||
* `use_lua` - Включить ли поддержку lua
|
||||
* `use_queue` - Скачивать по очереди, т.е. в один момент может скачивать только 1 клиент
|
||||
|
||||
### Server
|
||||
|
||||
* `description` - Описания сервера для лаунчера BeamMP
|
||||
* `name` - Названия сервер для лаунчера BeamMP
|
||||
* `server_ip` - Какой IP адрес занять серверу (только для опытных пользователей, по умолчанию 0.0.0.0)
|
||||
* `server_port` - На каком порту будет работать сервер
|
||||
|
||||
### WebAPI
|
||||
##### _Доки не готовы_
|
||||
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
# Web RESP API для сервера
|
||||
# WebAPI для сервера
|
||||
|
||||
## Доступные endpoints
|
||||
|
||||
* `/stop`:
|
||||
* Необходимые парамеры:
|
||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
||||
|
||||
|
||||
* `/event.get`
|
||||
* Точка не готова
|
||||
* Необходимые парамеры:
|
||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
||||
|
||||
В разработке
|
||||
@@ -5,4 +5,8 @@ uvicorn~=0.22.0
|
||||
fastapi~=0.100.0
|
||||
starlette~=0.27.0
|
||||
pydantic~=2.0.2
|
||||
click~=8.1.4
|
||||
click~=8.1.4
|
||||
lupa~=2.0
|
||||
toml~=0.10.2
|
||||
colorama~=0.4.6
|
||||
cryptography~=41.0.2
|
||||
747
src/core/Client.py
Normal file
747
src/core/Client.py
Normal file
@@ -0,0 +1,747 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from core import utils
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader, writer, core):
|
||||
self.__reader = reader
|
||||
self.__writer = writer
|
||||
self.__Core = core
|
||||
self.__alive = True
|
||||
self.__packets_queue = []
|
||||
self.__tasks = []
|
||||
self._down_sock = (None, None)
|
||||
self._udp_sock = (None, None)
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self._log = utils.get_logger("player(None:0)")
|
||||
self._addr = writer.get_extra_info("sockname")
|
||||
self._cid = -1
|
||||
self._key = None
|
||||
self.nick = None
|
||||
self.roles = None
|
||||
self._guest = True
|
||||
self._ready = False
|
||||
self._identifiers = []
|
||||
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
|
||||
self._focus_car = -1
|
||||
self._snowman = {"id": -1, "packet": ""}
|
||||
self._connect_time = 0
|
||||
self._last_position = {}
|
||||
|
||||
@property
|
||||
def _writer(self):
|
||||
return self.__writer
|
||||
|
||||
@property
|
||||
def alive(self):
|
||||
return self.__alive
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
return self._log
|
||||
|
||||
@property
|
||||
def addr(self):
|
||||
return self._addr[0]
|
||||
|
||||
@property
|
||||
def cid(self):
|
||||
return self._cid
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
return self._cid
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
@property
|
||||
def guest(self):
|
||||
return self._guest
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
return self._ready
|
||||
|
||||
@property
|
||||
def identifiers(self):
|
||||
return self._identifiers
|
||||
|
||||
@property
|
||||
def cars(self):
|
||||
return {i: v for i, v in enumerate(self._cars) if v is not None}
|
||||
|
||||
@property
|
||||
def focus_car(self):
|
||||
return self._focus_car
|
||||
|
||||
@property
|
||||
def last_position(self):
|
||||
return self._last_position
|
||||
|
||||
def _update_logger(self):
|
||||
self._log = utils.get_logger(f"{self.nick}:{self.cid}")
|
||||
self.log.debug(f"Update logger")
|
||||
|
||||
def is_disconnected(self):
|
||||
if not self.__alive:
|
||||
return True
|
||||
if self.__writer.is_closing():
|
||||
self.log.debug(f"is_d: Disconnected.")
|
||||
self.__alive = False
|
||||
return True
|
||||
else:
|
||||
self.__alive = True
|
||||
return False
|
||||
|
||||
async def kick(self, reason=None):
|
||||
if not reason:
|
||||
reason = "Kicked!"
|
||||
else:
|
||||
reason = f"{reason}"
|
||||
if not self.__alive:
|
||||
self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;")
|
||||
return
|
||||
self.log.info(i18n.client_kicked.format(reason))
|
||||
await self._send(f"K{reason}")
|
||||
self.__alive = False
|
||||
|
||||
async def send_message(self, message, to_all=True):
|
||||
if not message:
|
||||
message = "no message"
|
||||
to_all = False
|
||||
await self._send(f"C:{message!r}", to_all=to_all)
|
||||
|
||||
async def send_event(self, event_name, event_data, to_all=True):
|
||||
if isinstance(event_data, (list, tuple, dict)):
|
||||
event_data = json.dumps(event_data, separators=(',', ':'))
|
||||
else:
|
||||
event_data = f"{event_data!r}"
|
||||
if len(event_data) > 104857599:
|
||||
self.log.error("Client data too big! >=104857599")
|
||||
return
|
||||
await self._send(f"E:{event_name}:{event_data}", to_all=to_all)
|
||||
|
||||
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
|
||||
|
||||
# TNetwork.cpp; Line: 383
|
||||
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
|
||||
# [][][][][][]...[]
|
||||
# ^------^^---...-^
|
||||
# size data
|
||||
|
||||
if type(data) == str:
|
||||
data = bytes(data, config.enc)
|
||||
|
||||
if to_all:
|
||||
code = chr(data[0])
|
||||
for client in self.__Core.clients:
|
||||
if not client or (client is self and not to_self):
|
||||
continue
|
||||
if not to_udp or code in ['V', 'W', 'Y', 'E']:
|
||||
await client._send(data)
|
||||
else:
|
||||
await client._send(data, to_udp=to_udp)
|
||||
return
|
||||
|
||||
if not self.__alive:
|
||||
return False
|
||||
|
||||
if writer is None:
|
||||
writer = self.__writer
|
||||
|
||||
if len(data) > 400:
|
||||
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
|
||||
|
||||
if to_udp:
|
||||
udp_sock = self._udp_sock[0]
|
||||
udp_addr = self._udp_sock[1]
|
||||
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
|
||||
if udp_sock and udp_addr:
|
||||
try:
|
||||
if not udp_sock.is_closing():
|
||||
# self.log.debug(f'[UDP] {data!r}')
|
||||
udp_sock.sendto(data, udp_addr)
|
||||
except OSError:
|
||||
self.log.debug("[UDP] Error sending")
|
||||
except Exception as e:
|
||||
self.log.debug(f"[UDP] Error sending: {e}")
|
||||
self.log.exception(e)
|
||||
return
|
||||
|
||||
header = len(data).to_bytes(4, "little", signed=True)
|
||||
# self.log.debug(f'[TCP] {header + data!r}')
|
||||
try:
|
||||
writer.write(header + data)
|
||||
await writer.drain()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log.debug(f'[TCP] Disconnected: {e}')
|
||||
self.__alive = False
|
||||
await self._remove_me()
|
||||
return False
|
||||
|
||||
async def _recv(self, one=False):
|
||||
while self.__alive:
|
||||
try:
|
||||
header = b""
|
||||
while len(header) < 4 and self.__alive:
|
||||
h = await self.__reader.read(4)
|
||||
if not h:
|
||||
break
|
||||
else:
|
||||
header += h
|
||||
|
||||
int_header = int.from_bytes(header, byteorder='little', signed=True)
|
||||
|
||||
if int_header <= 0:
|
||||
await asyncio.sleep(0.1)
|
||||
self.is_disconnected()
|
||||
if self.__alive:
|
||||
if header == b"":
|
||||
self.__packets_queue.append(None)
|
||||
self.__alive = False
|
||||
continue
|
||||
self.log.error(f"Header: {header}")
|
||||
await self.kick("Invalid packet - header negative")
|
||||
self.__packets_queue.append(None)
|
||||
continue
|
||||
|
||||
if int_header > 100 * MB:
|
||||
await self.kick("Header size limit exceeded")
|
||||
self.log.warning("Client sent header of >100MB - "
|
||||
"assuming malicious intent and disconnecting the client.")
|
||||
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
|
||||
self.__packets_queue.append(None)
|
||||
continue
|
||||
|
||||
data = b""
|
||||
while len(data) < int_header and self.__alive:
|
||||
buffer = await self.__reader.read(int_header - len(data))
|
||||
if not buffer:
|
||||
break
|
||||
else:
|
||||
data += buffer
|
||||
|
||||
abg = b"ABG:"
|
||||
if len(data) > len(abg) and data.startswith(abg):
|
||||
data = zlib.decompress(data[len(abg):])
|
||||
# self.log.debug(f"ABG Packet: {len(data)}")
|
||||
|
||||
if one:
|
||||
return data
|
||||
self.__packets_queue.append(data)
|
||||
|
||||
except ConnectionError:
|
||||
self.__alive = False
|
||||
self.__packets_queue.append(None)
|
||||
|
||||
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
|
||||
real_size = end - start
|
||||
writer = self._down_sock[1] if d_sock else self.__writer
|
||||
who = 'dwn' if d_sock else 'srv'
|
||||
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
f.seek(start)
|
||||
total_sent = 0
|
||||
start_time = time.monotonic()
|
||||
while total_sent < real_size:
|
||||
data = f.read(min(MB, real_size - total_sent)) # read data in chunks of 1MB or less
|
||||
try:
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
# self.log.debug(f"[{who}] Sent {len(data)} bytes.")
|
||||
except ConnectionError:
|
||||
self.__alive = False
|
||||
self.log.debug(f"[{who}] Disconnected.")
|
||||
break
|
||||
total_sent += len(data)
|
||||
|
||||
# Calculate delay based on speed limit
|
||||
if speed_limit:
|
||||
elapsed_time = time.monotonic() - start_time
|
||||
expected_time = total_sent / (speed_limit * MB)
|
||||
if expected_time > elapsed_time:
|
||||
await asyncio.sleep(expected_time - elapsed_time)
|
||||
|
||||
return total_sent
|
||||
|
||||
async def _sync_resources(self):
|
||||
while self.__alive:
|
||||
data = await self._recv(True)
|
||||
if data is None:
|
||||
await self._remove_me()
|
||||
break
|
||||
if data.startswith(b"f"):
|
||||
file = data[1:].decode(config.enc)
|
||||
self.log.info(i18n.client_mod_request.format(repr(file)))
|
||||
size = -1
|
||||
for mod in self.__Core.mods_list:
|
||||
if type(mod) == int:
|
||||
continue
|
||||
if mod.get('path') == file:
|
||||
size = mod['size']
|
||||
self.log.debug("File is accept.")
|
||||
break
|
||||
self.log.debug(f"Mode size: {size}")
|
||||
if size == -1:
|
||||
await self._send(b"CO")
|
||||
await self.kick(f"Not allowed mod: " + file)
|
||||
return
|
||||
await self._send(b"AG")
|
||||
t = 0
|
||||
while not self._down_sock[0]:
|
||||
await asyncio.sleep(0.1)
|
||||
t += 1
|
||||
if t > 50:
|
||||
await self.kick("Missing download socket")
|
||||
return
|
||||
if config.Options['use_queue']:
|
||||
while self.__Core.lock_upload:
|
||||
await asyncio.sleep(.2)
|
||||
self.__Core.lock_upload = True
|
||||
speed = config.Options["speed_limit"]
|
||||
if speed:
|
||||
speed = speed / 2
|
||||
half_size = math.floor(size / 2)
|
||||
t = time.monotonic()
|
||||
uploads = [
|
||||
self._split_load(0, half_size, False, file, speed),
|
||||
self._split_load(half_size, size, True, file, speed)
|
||||
]
|
||||
sl0, sl1 = await asyncio.gather(*uploads)
|
||||
tr = time.monotonic() - t
|
||||
if self.__Core.lock_upload:
|
||||
self.__Core.lock_upload = False
|
||||
msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
|
||||
if speed:
|
||||
msg += i18n.client_mod_sent_limit.format(int(speed * 2))
|
||||
self.log.info(msg)
|
||||
sent = sl0 + sl1
|
||||
ok = sent == size
|
||||
lost = size - sent
|
||||
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
|
||||
if not ok:
|
||||
self.__alive = False
|
||||
self.log.error(i18n.client_mod_sent_error.format(repr(file)))
|
||||
return
|
||||
elif data.startswith(b"SR"):
|
||||
path_list = ''
|
||||
size_list = ''
|
||||
for mod in self.__Core.mods_list:
|
||||
if type(mod) == int:
|
||||
continue
|
||||
path_list += f"{mod['path']};"
|
||||
size_list += f"{mod['size']};"
|
||||
mod_list = path_list + size_list
|
||||
self.log.debug(f"Mods List: {mod_list}")
|
||||
if len(mod_list) == 0:
|
||||
await self._send(b"-")
|
||||
else:
|
||||
await self._send(mod_list)
|
||||
elif data == b"Done":
|
||||
await self._send(f"M/levels/{config.Game['map']}/info.json")
|
||||
break
|
||||
return
|
||||
|
||||
def _get_cid_vid(self, data: str):
|
||||
sep = data.find(":", 1) + 1
|
||||
s = data[sep:sep + 3]
|
||||
id_sep = s.find('-')
|
||||
if id_sep == -1:
|
||||
self.log.debug(
|
||||
f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}', {s}")
|
||||
return -1, -1
|
||||
cid = s[:id_sep]
|
||||
vid = s[id_sep + 1:]
|
||||
if cid.isdigit() and vid.isdigit():
|
||||
try:
|
||||
cid = int(cid)
|
||||
vid = int(vid)
|
||||
return cid, vid
|
||||
except ValueError:
|
||||
self.log.debug(f"Invalid packet: Could not parse cid/vid from packet, as one or both are not valid "
|
||||
f"numbers: '{s}'")
|
||||
return -1, -1
|
||||
self.log.debug(f"Invalid packet: Could not parse pid/vid from packet: '{data}'")
|
||||
return -1, -1
|
||||
|
||||
async def _spawn_car(self, data):
|
||||
car_data = data[2:]
|
||||
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)
|
||||
if self._snowman['id'] != -1:
|
||||
cars_count -= 1 # -1 for unicycle
|
||||
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
|
||||
car_json = {}
|
||||
try:
|
||||
car_json = json.loads(car_data[car_data.find("{"):])
|
||||
except Exception as e:
|
||||
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
|
||||
allow = True
|
||||
allow_snowman = True
|
||||
over_spawn = False
|
||||
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
|
||||
if 1 in lua_data:
|
||||
allow = False
|
||||
ev_data_list = ev.call_event("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)
|
||||
ev_data_list.extend(d2)
|
||||
for ev_data in ev_data_list:
|
||||
self.log.debug(ev_data)
|
||||
# TODO: handle event onCarSpawn
|
||||
pass
|
||||
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
|
||||
snowman = car_json.get("jbm") == "unicycle"
|
||||
if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn:
|
||||
if snowman:
|
||||
unicycle_id = self._snowman['id']
|
||||
if unicycle_id != -1:
|
||||
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}")
|
||||
self._cars[unicycle_id] = None
|
||||
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
||||
self._snowman = {"id": car_id, "packet": pkt}
|
||||
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
|
||||
else:
|
||||
self.log.debug(f"Car spawn accepted: car_id={car_id}")
|
||||
self._focus_car = car_id
|
||||
self._cars[car_id] = {
|
||||
"packet": pkt,
|
||||
"json": car_json,
|
||||
"json_ok": bool(car_json),
|
||||
"snowman": snowman,
|
||||
"over_spawn": (snowman and allow_snowman) or over_spawn,
|
||||
"pos": {}
|
||||
}
|
||||
await self._send(pkt, to_all=True, to_self=True)
|
||||
else:
|
||||
await self._send(pkt)
|
||||
des = f"Od:{self.cid}-{car_id}"
|
||||
await self._send(des)
|
||||
|
||||
async def delete_car(self, car_id):
|
||||
await self._delete_car(car_id=car_id)
|
||||
|
||||
async def _delete_car(self, raw_data=None, car_id=None):
|
||||
|
||||
if not car_id and raw_data:
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
else:
|
||||
cid = self.cid
|
||||
raw_data = f"Od:{self.cid}-{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...
|
||||
ev_data_list = ev.call_event("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)
|
||||
ev_data_list.extend(d2)
|
||||
for ev_data in ev_data_list:
|
||||
self.log.debug(ev_data)
|
||||
# TODO: handle event onCarDelete
|
||||
pass
|
||||
|
||||
if cid == self.cid or admin_allow:
|
||||
await self._send(raw_data, to_all=True, to_self=True)
|
||||
car = self._cars[car_id]
|
||||
if car['snowman']:
|
||||
self.log.debug(f"Snowman found")
|
||||
unicycle_id = self._snowman['id']
|
||||
self._snowman['id'] = -1
|
||||
self._cars[unicycle_id] = None
|
||||
self._cars[car_id] = None
|
||||
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
|
||||
self.log.debug(f"Deleted car: car_id={car_id}")
|
||||
|
||||
else:
|
||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||
|
||||
async def _edit_car(self, raw_data, data):
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
if car_id != -1 and self._cars[car_id]:
|
||||
client = self.__Core.get_client(cid=cid)
|
||||
if client:
|
||||
car = client._cars[car_id]
|
||||
new_car_json = {}
|
||||
try:
|
||||
new_car_json = json.loads(data[data.find("{"):])
|
||||
except Exception as e:
|
||||
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {data}")
|
||||
|
||||
allow = False
|
||||
admin_allow = False
|
||||
lua_data = ev.call_lua_event("onVehicleEdited", self.cid, car_id, data[data.find("{"):])
|
||||
if 1 in lua_data:
|
||||
allow = False
|
||||
ev_data_list = ev.call_event("onCarEdited", data=new_car_json, car_id=car_id, player=self)
|
||||
d2 = await ev.call_async_event("onCarEdited", data=new_car_json, car_id=car_id, player=self)
|
||||
ev_data_list.extend(d2)
|
||||
for ev_data in ev_data_list:
|
||||
self.log.debug(ev_data)
|
||||
# TODO: handle event onCarEdited
|
||||
pass
|
||||
|
||||
if cid == self.cid or allow or admin_allow:
|
||||
if car['snowman']:
|
||||
unicycle_id = self._snowman['id']
|
||||
self._snowman['id'] = -1
|
||||
self.log.debug(f"Delete snowman")
|
||||
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
||||
self._cars[unicycle_id] = None
|
||||
else:
|
||||
await self._send(raw_data, to_all=True, to_self=False)
|
||||
if car['json_ok']:
|
||||
old_car_json = car['json']
|
||||
old_car_json.update(new_car_json)
|
||||
car['json'] = old_car_json
|
||||
self.log.debug(f"Updated car: car_id={car_id}")
|
||||
else:
|
||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||
|
||||
async def _reset_car(self, raw_data):
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
||||
await self._send(raw_data, to_all=True, to_self=False)
|
||||
ev.call_lua_event("onVehicleReset", self.cid, car_id, raw_data[raw_data.find("{"):])
|
||||
car_json = {}
|
||||
try:
|
||||
car_json = json.loads(raw_data[raw_data.find("{"):])
|
||||
except Exception as e:
|
||||
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {raw_data}")
|
||||
ev.call_event("onCarReset", data=car_json, car_id=car_id, player=self)
|
||||
await ev.call_async_event("onCarReset", data=car_json, car_id=car_id, player=self)
|
||||
self.log.debug(f"Car reset: car_id={car_id}")
|
||||
else:
|
||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||
|
||||
async def _handle_car_codes(self, raw_data):
|
||||
if len(raw_data) < 6:
|
||||
return
|
||||
sub_code = raw_data[1]
|
||||
data = raw_data[3:]
|
||||
match sub_code:
|
||||
case "s": # Spawn car
|
||||
self.log.debug("Trying to spawn car")
|
||||
if data[0] == "0":
|
||||
await self._spawn_car(data)
|
||||
|
||||
case "d": # Delete car
|
||||
self.log.debug("Trying to delete car")
|
||||
await self._delete_car(raw_data)
|
||||
|
||||
case "c": # Edit car
|
||||
self.log.debug("Trying to edit car")
|
||||
await self._edit_car(raw_data, data)
|
||||
|
||||
case "r": # Reset car
|
||||
self.log.debug("Trying to reset car")
|
||||
await self._reset_car(raw_data)
|
||||
|
||||
case "t": # Broken details
|
||||
self.log.debug(f"Something changed/broken: {raw_data}")
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
||||
data = raw_data[raw_data.find("{"):]
|
||||
ev.call_event("onCarChanged", car_id=car_id, data=data)
|
||||
await ev.call_async_event("onCarChanged", car_id=car_id, data=data)
|
||||
await self._send(raw_data, to_all=True, to_self=False)
|
||||
|
||||
case "m": # Move focus car
|
||||
self.log.debug(f"Move focus to: {raw_data}")
|
||||
cid, car_id = self._get_cid_vid(raw_data[3:])
|
||||
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
||||
self._focus_car = car_id
|
||||
data = raw_data[raw_data.find("{"):]
|
||||
ev.call_event("onCarFocusMove", car_id=car_id, data=data)
|
||||
await ev.call_async_event("onCarFocusMove", car_id=car_id, data=data)
|
||||
await self._send(raw_data, to_all=True, to_self=True)
|
||||
|
||||
async def _connected_handler(self):
|
||||
# Client connected
|
||||
ev.call_event("onPlayerJoin", player=self)
|
||||
await ev.call_async_event("onPlayerJoin", player=self)
|
||||
|
||||
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
|
||||
|
||||
for client in self.__Core.clients:
|
||||
if not client:
|
||||
continue
|
||||
for car in client._cars:
|
||||
if not car:
|
||||
continue
|
||||
await self._send(car['packet'])
|
||||
|
||||
self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2)))
|
||||
self._ready = True
|
||||
|
||||
async def _chat_handler(self, data):
|
||||
sup = data.find(":", 2)
|
||||
if sup == -1:
|
||||
await self._send("C:Server: Invalid message.")
|
||||
msg = data[sup + 2:]
|
||||
if not msg:
|
||||
self.log.debug("Tried to send an empty event, ignoring")
|
||||
return
|
||||
to_ev = {"message": msg, "player": self}
|
||||
lua_data = ev.call_lua_event("onChatMessage", self.cid, self.nick, msg)
|
||||
if 1 in lua_data:
|
||||
if config.Options['log_chat']:
|
||||
self.log.info(f"{self.nick}: {msg}")
|
||||
return
|
||||
ev_data_list = ev.call_event("onChatReceive", **to_ev)
|
||||
d2 = await ev.call_async_event("onChatReceive", **to_ev)
|
||||
ev_data_list.extend(d2)
|
||||
need_send = True
|
||||
for ev_data in ev_data_list:
|
||||
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
|
||||
if to_client:
|
||||
# noinspection PyProtectedMember
|
||||
writer = to_client._writer
|
||||
if config.Options['log_chat']:
|
||||
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}")
|
||||
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
|
||||
need_send = False
|
||||
except KeyError | AttributeError:
|
||||
self.log.error(i18n.client_event_invalid_data.format(ev_data))
|
||||
if need_send:
|
||||
if config.Options['log_chat']:
|
||||
self.log.info(f"{self.nick}: {msg}")
|
||||
await self._send(data, to_all=True)
|
||||
|
||||
async def _handle_codes(self, data):
|
||||
if not data:
|
||||
self.__alive = False
|
||||
return
|
||||
|
||||
# Codes: V W X Y
|
||||
if 89 >= data[0] >= 86:
|
||||
await self._send(data, to_all=True, to_self=False)
|
||||
return
|
||||
|
||||
_bytes = False
|
||||
try:
|
||||
data = data.decode()
|
||||
except UnicodeDecodeError:
|
||||
_bytes = True
|
||||
self.log.error(f"UnicodeDecodeError: {data}")
|
||||
self.log.info("Some things are skipping...")
|
||||
|
||||
# Codes: p, Z in udp_server.py
|
||||
match data[0]: # At data[0] code
|
||||
case "H": # Map load, client ready
|
||||
await self._connected_handler()
|
||||
|
||||
case "C": # Chat handler
|
||||
if _bytes:
|
||||
return
|
||||
await self._chat_handler(data)
|
||||
|
||||
case "O": # Cars handler
|
||||
if _bytes:
|
||||
return
|
||||
await self._handle_car_codes(data)
|
||||
|
||||
case "E": # Client events handler
|
||||
if len(data) < 2:
|
||||
self.log.debug("Tried to send an empty event, ignoring.")
|
||||
return
|
||||
if _bytes:
|
||||
sep = data.find(b":", 2)
|
||||
self.log.warning("Bytes event!")
|
||||
else:
|
||||
sep = data.find(":", 2)
|
||||
if sep == -1:
|
||||
self.log.error(f"Received event in invalid format (missing ':'), got: {data}")
|
||||
event_name = data[2:sep]
|
||||
even_data = data[sep + 1:]
|
||||
ev.call_lua_event(event_name, even_data)
|
||||
ev.call_event(event_name, data=even_data, player=self)
|
||||
await ev.call_async_event(event_name, data=even_data, player=self)
|
||||
case "N":
|
||||
await self._send(data, to_all=True, to_self=False)
|
||||
|
||||
async def _looper(self):
|
||||
ev.call_lua_event("onPlayerConnecting", self.cid)
|
||||
self._connect_time = time.monotonic()
|
||||
await self._send(f"P{self.cid}") # Send clientID
|
||||
await self._sync_resources()
|
||||
ev.call_lua_event("onPlayerJoining", self.cid)
|
||||
tasks = self.__tasks
|
||||
recv = asyncio.create_task(self._recv())
|
||||
tasks.append(recv)
|
||||
while self.__alive:
|
||||
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):
|
||||
await asyncio.sleep(0.3)
|
||||
self.__alive = False
|
||||
if (self.cid > 0 or self.nick is not None) and \
|
||||
self.__Core.clients_by_nick.get(self.nick):
|
||||
for i, car in enumerate(self._cars):
|
||||
if not car:
|
||||
continue
|
||||
self.log.debug(f"Removing car: car_id={i}")
|
||||
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
|
||||
if self.ready:
|
||||
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected.
|
||||
self.log.debug(f"Removing client")
|
||||
ev.call_lua_event("onPlayerDisconnect", self.cid)
|
||||
ev.call_event("onPlayerDisconnect", player=self)
|
||||
await ev.call_async_event("onPlayerDisconnect", player=self)
|
||||
|
||||
self.log.info(
|
||||
i18n.client_player_disconnected.format(
|
||||
round((time.monotonic() - self._connect_time) / 60, 2)
|
||||
)
|
||||
)
|
||||
self.__Core.clients[self.cid] = None
|
||||
del self.__Core.clients_by_id[self.cid]
|
||||
del self.__Core.clients_by_nick[self.nick]
|
||||
else:
|
||||
self.log.debug(f"Removing client; Closing connection...")
|
||||
try:
|
||||
if not self.__writer.is_closing():
|
||||
self.__writer.close()
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error while closing writer: {e}")
|
||||
try:
|
||||
_, down_w = self._down_sock
|
||||
if down_w and not down_w.is_closing():
|
||||
down_w.close()
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error while closing download writer: {e}")
|
||||
88
src/core/Client.pyi
Normal file
88
src/core/Client.pyi
Normal file
@@ -0,0 +1,88 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import StreamReader, StreamWriter, DatagramTransport
|
||||
from logging import Logger
|
||||
from typing import Tuple, List, Dict, Optional, Union, Any
|
||||
|
||||
from core import Core, utils
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
|
||||
self._connect_time: float = 0.0
|
||||
self.__tasks = []
|
||||
self.__reader = reader
|
||||
self.__writer = writer
|
||||
self.__packets_queue = []
|
||||
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
|
||||
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
|
||||
self._log = utils.get_logger("client(id: )")
|
||||
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self.__Core: Core = core
|
||||
self._cid: int = -1
|
||||
self._key: str = None
|
||||
self.nick: str = None
|
||||
self.roles: str = None
|
||||
self._guest = True
|
||||
self.__alive = True
|
||||
self._ready = False
|
||||
self._focus_car = -1
|
||||
self._identifiers = []
|
||||
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._last_position = {}
|
||||
async def __gracefully_kick(self): ...
|
||||
@property
|
||||
def _writer(self) -> StreamWriter: ...
|
||||
@property
|
||||
def alive(self) -> bool: ...
|
||||
@property
|
||||
def log(self) -> Logger: ...
|
||||
@property
|
||||
def addr(self) -> Tuple[str, int]: ...
|
||||
@property
|
||||
def cid(self) -> int: ...
|
||||
def pid(self) -> int: ...
|
||||
@property
|
||||
def key(self) -> str: ...
|
||||
@property
|
||||
def guest(self) -> bool: ...
|
||||
@property
|
||||
def ready(self) -> bool: ...
|
||||
@property
|
||||
def identifiers(self) -> list: ...
|
||||
@property
|
||||
def cars(self) -> dict: ...
|
||||
@property
|
||||
def focus_car(self):
|
||||
return self._focus_car
|
||||
@property
|
||||
def last_position(self) -> dict: ...
|
||||
def is_disconnected(self) -> bool: ...
|
||||
async def kick(self, reason: str) -> 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(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 _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 _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
|
||||
async def _spawn_car(self, data: str) -> None: ...
|
||||
async def delete_car(self, car_id: int) -> None: ...
|
||||
async def _delete_car(self, raw_data: str = None, car_id: int = None) -> None: ...
|
||||
async def _edit_car(self, raw_data: str, data: str) -> None: ...
|
||||
async def _reset_car(self, raw_data: str) -> None: ...
|
||||
async def _handle_car_codes(self, data: str) -> None: ...
|
||||
async def _connected_handler(self) -> None: ...
|
||||
async def _chat_handler(self, data: str) -> None: ...
|
||||
async def _handle_codes(self, data: bytes) -> None: ...
|
||||
async def _looper(self) -> None: ...
|
||||
def _update_logger(self) -> None: ...
|
||||
async def _remove_me(self) -> None: ...
|
||||
@@ -1,8 +1,8 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.__init__.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.3
|
||||
# Core version: 0.2.2
|
||||
# Version 1.4
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
|
||||
@@ -10,8 +10,8 @@
|
||||
__title__ = 'KuiToi-Server'
|
||||
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
|
||||
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
|
||||
__version__ = '0.2.2'
|
||||
__build__ = 1113 # Я это считаю лог файлами
|
||||
__version__ = '0.4.3'
|
||||
__build__ = 2125 # Я это считаю лог файлами
|
||||
__author__ = 'SantaSpeen'
|
||||
__author_email__ = 'admin@kuitoi.su'
|
||||
__license__ = "FPA"
|
||||
@@ -19,7 +19,6 @@ __copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
import prompt_toolkit.shortcuts as shortcuts
|
||||
@@ -27,7 +26,7 @@ import prompt_toolkit.shortcuts as shortcuts
|
||||
from .utils import get_logger
|
||||
from core.core import Core
|
||||
from main import parser
|
||||
from modules import ConfigProvider, EventsSystem, PluginsLoader
|
||||
from modules import ConfigProvider, EventsSystem
|
||||
from modules import Console
|
||||
from modules import MultiLanguage
|
||||
|
||||
@@ -47,17 +46,17 @@ if args.config:
|
||||
config_provider = ConfigProvider(config_path)
|
||||
config = config_provider.open_config()
|
||||
builtins.config = config
|
||||
if config.Server['debug'] is True:
|
||||
config.enc = config.Options['encoding']
|
||||
if config.Options['debug'] is True:
|
||||
utils.set_debug_status()
|
||||
log.info("Debug enabled!")
|
||||
log = get_logger("core.init")
|
||||
log.debug("Debug mode enabled!")
|
||||
log.debug(f"Server config: {config}")
|
||||
|
||||
# i18n init
|
||||
log.debug("Initializing i18n...")
|
||||
ml = MultiLanguage()
|
||||
ml.set_language(args.language or config.Server['language'])
|
||||
ml.set_language(args.language or config.Options['language'])
|
||||
ml.builtins_hook()
|
||||
|
||||
log.debug("Initializing EventsSystem...")
|
||||
@@ -103,20 +102,12 @@ if not config.Auth['private'] and not config.Auth['key']:
|
||||
log.debug("Initializing console...")
|
||||
console = Console()
|
||||
console.builtins_hook()
|
||||
# console.logger_hook()
|
||||
console.logger_hook()
|
||||
console.add_command("stop", console.stop, i18n.man_message_stop, i18n.help_message_stop)
|
||||
console.add_command("exit", console.stop, i18n.man_message_exit, i18n.help_message_exit)
|
||||
|
||||
log.debug("Initializing PluginsLoader...")
|
||||
if not os.path.exists("plugins"):
|
||||
os.mkdir("plugins")
|
||||
pl = PluginsLoader("plugins")
|
||||
pl.load_plugins()
|
||||
|
||||
builtins.B = 1
|
||||
builtins.KB = B * 1024
|
||||
builtins.MB = KB * 1024
|
||||
builtins.GB = MB * 1024
|
||||
builtins.TB = GB * 1024
|
||||
|
||||
log.info(i18n.init_ok)
|
||||
|
||||
466
src/core/core.py
466
src/core/core.py
@@ -1,305 +1,34 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.core.py
|
||||
# Written by: SantaSpeen
|
||||
# Version: 0.2.2
|
||||
# Version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import zlib
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
import aiohttp
|
||||
import uvicorn
|
||||
|
||||
from core import utils
|
||||
from core import utils, __version__
|
||||
from core.Client import Client
|
||||
from core.tcp_server import TCPServer
|
||||
from core.udp_server import UDPServer
|
||||
from modules import PluginsLoader
|
||||
from modules.WebAPISystem import app as webapp
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader, writer, core):
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.down_rw = (None, None)
|
||||
self.log = utils.get_logger("client(None:0)")
|
||||
self.addr = writer.get_extra_info("sockname")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
self.cid = -1
|
||||
self.key = None
|
||||
self.nick = None
|
||||
self.roles = None
|
||||
self.guest = True
|
||||
self.alive = True
|
||||
self.ready = False
|
||||
|
||||
def _update_logger(self):
|
||||
self.log = utils.get_logger(f"{self.nick}:{self.cid})")
|
||||
self.log.debug(f"Update logger")
|
||||
|
||||
def is_disconnected(self):
|
||||
if not self.alive:
|
||||
return True
|
||||
res = self.writer.is_closing()
|
||||
if res:
|
||||
self.log.debug(f"Client Disconnected")
|
||||
self.alive = False
|
||||
return True
|
||||
else:
|
||||
self.log.debug(f"Client Alive")
|
||||
self.alive = True
|
||||
return False
|
||||
|
||||
async def kick(self, reason):
|
||||
if not self.alive:
|
||||
self.log.debug(f"Kick({reason}) skipped;")
|
||||
return
|
||||
self.log.info(f"Kicked with reason: \"{reason}\"")
|
||||
await self.tcp_send(b"K" + bytes(reason, "utf-8"))
|
||||
self.alive = False
|
||||
# await self.remove_me()
|
||||
|
||||
async def tcp_send(self, data, to_all=False, writer=None):
|
||||
|
||||
# TNetwork.cpp; Line: 383
|
||||
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
|
||||
# [][][][][][]...[]
|
||||
# ^------^^---...-^
|
||||
# size data
|
||||
|
||||
if writer is None:
|
||||
writer = self.writer
|
||||
|
||||
if to_all:
|
||||
for client in self.Core.clients:
|
||||
if not client:
|
||||
continue
|
||||
await client.tcp_send(data)
|
||||
return
|
||||
|
||||
# self.log.debug(f"tcp_send({data})")
|
||||
if len(data) == 10:
|
||||
data += b"."
|
||||
header = len(data).to_bytes(4, "little", signed=True)
|
||||
self.log.debug(f'len: {len(data)}; send: {header + data}')
|
||||
try:
|
||||
writer.write(header + data)
|
||||
await writer.drain()
|
||||
except ConnectionError:
|
||||
self.log.debug('tcp_send: Disconnected')
|
||||
self.alive = False
|
||||
|
||||
async def recv(self):
|
||||
try:
|
||||
header = await self.reader.read(4) # header: 4 bytes
|
||||
|
||||
int_header = 0
|
||||
for i in range(len(header)):
|
||||
int_header += header[i]
|
||||
|
||||
if int_header <= 0:
|
||||
await asyncio.sleep(0.1)
|
||||
self.is_disconnected()
|
||||
if self.alive:
|
||||
self.log.debug(f"Header: {header}")
|
||||
await self.kick("Invalid packet - header negative")
|
||||
return b""
|
||||
|
||||
if int_header > 100 * MB:
|
||||
await self.kick("Header size limit exceeded")
|
||||
self.log.warn(f"Client {self.nick}:{self.cid} sent header of >100MB - "
|
||||
f"assuming malicious intent and disconnecting the client.")
|
||||
return b""
|
||||
|
||||
data = await self.reader.read(100 * MB)
|
||||
self.log.debug(f"header: `{header}`; int_header: `{int_header}`; data: `{data}`;")
|
||||
|
||||
if len(data) != int_header:
|
||||
self.log.debug(f"WARN Expected to read {int_header} bytes, instead got {len(data)}")
|
||||
|
||||
abg = b"ABG:"
|
||||
if len(data) > len(abg) and data.startswith(abg):
|
||||
data = zlib.decompress(data[len(abg):])
|
||||
self.log.debug(f"ABG: {data}")
|
||||
return data
|
||||
return data
|
||||
except ConnectionError:
|
||||
self.alive = False
|
||||
return b""
|
||||
|
||||
async def _split_load(self, start, end, d_sock, filename):
|
||||
real_size = end - start
|
||||
writer = self.down_rw[1] if d_sock else self.writer
|
||||
who = 'dwn' if d_sock else 'srv'
|
||||
self.log.debug(f"[{who}] Real size: {real_size/MB}mb; {real_size == end}, {real_size*2 == end}")
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
f.seek(start)
|
||||
data = f.read(end)
|
||||
try:
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
self.log.debug(f"[{who}] File sent.")
|
||||
except ConnectionError:
|
||||
self.alive = False
|
||||
self.log.debug(f"[{who}] Disconnected.")
|
||||
# break
|
||||
return real_size
|
||||
|
||||
# chunk_size = 125 * MB
|
||||
# if chunk_size > real_size:
|
||||
# chunk_size = real_size
|
||||
# chunks = math.floor(real_size / chunk_size)
|
||||
# self.log.debug(f"[{who}] s:{start}, e:{end}, c:{chunks}, cz:{chunk_size/MB}mb, rs:{real_size/MB}mb")
|
||||
# dw = 0
|
||||
# for chunk in range(1, chunks + 1):
|
||||
# chunk_end = start + (chunk_size * chunk)
|
||||
# chunk_start = chunk_end - chunk_size
|
||||
# # if chunk_start != 0:
|
||||
# # chunk_start -= 1
|
||||
# real_size -= chunk_size
|
||||
# if chunk_size > real_size:
|
||||
# chunk_end = real_size
|
||||
# self.log.debug(f"[{who}] Chunk: {chunk}; Start: {chunk_start}; End: {chunk_end/MB};")
|
||||
# with open(filename, 'rb') as f:
|
||||
# f.seek(chunk_start)
|
||||
# data = f.read(chunk_end)
|
||||
# try:
|
||||
# writer.write(data)
|
||||
# await writer.drain()
|
||||
# except ConnectionError:
|
||||
# self.alive = False
|
||||
# self.log.debug(f"[{who}] Disconnected")
|
||||
# break
|
||||
# dw += len(data)
|
||||
# del data
|
||||
# self.log.debug(f"[{who}] File sent.")
|
||||
# return dw
|
||||
|
||||
async def sync_resources(self):
|
||||
while self.alive:
|
||||
data = await self.recv()
|
||||
self.log.debug(f"data: {data!r}")
|
||||
if data.startswith(b"f"):
|
||||
file = data[1:].decode("utf-8")
|
||||
self.log.debug(f"Sending File: {file}")
|
||||
size = -1
|
||||
for mod in self.Core.mods_list:
|
||||
if type(mod) == int:
|
||||
continue
|
||||
if mod.get('path') == file:
|
||||
size = mod['size']
|
||||
self.log.debug("File is accept.")
|
||||
break
|
||||
if size == -1:
|
||||
await self.tcp_send(b"CO")
|
||||
await self.kick(f"Not allowed mod: " + file)
|
||||
return
|
||||
await self.tcp_send(b"AG")
|
||||
t = 0
|
||||
while not self.down_rw[0]:
|
||||
await asyncio.sleep(0.1)
|
||||
t += 1
|
||||
if t > 50:
|
||||
await self.kick("Missing download socket")
|
||||
return
|
||||
self.log.info(f"Requested mode: {file!r}")
|
||||
self.log.debug(f"Mode size: {size/MB}")
|
||||
|
||||
msize = math.floor(size / 2)
|
||||
# uploads = [
|
||||
# asyncio.create_task(self._split_load(0, msize, False, file)), # SplitLoad_0
|
||||
# asyncio.create_task(self._split_load(msize, size, True, file)) # SplitLoad_1
|
||||
# ]
|
||||
# await asyncio.wait(uploads)
|
||||
uploads = [
|
||||
self._split_load(0, msize, False, file),
|
||||
self._split_load(msize, size, True, file)
|
||||
]
|
||||
sl0, sl1 = await asyncio.gather(*uploads)
|
||||
sent = sl0 + sl1
|
||||
ok = sent == size
|
||||
lost = size - sent
|
||||
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
|
||||
self.log.debug(f"SplitLoad_0: {sl0/MB}mb; "
|
||||
f"SplitLoad_1: {sl1/MB}MB; At all ({ok}): Sent: {sent/MB}mb; Lost: {lost/MB}mb")
|
||||
if not ok:
|
||||
self.alive = False
|
||||
self.log.error(f"Error while sending.")
|
||||
return
|
||||
elif data.startswith(b"SR"):
|
||||
path_list = ''
|
||||
size_list = ''
|
||||
for mod in self.Core.mods_list:
|
||||
if type(mod) == int:
|
||||
continue
|
||||
path_list += f"{mod['path']};"
|
||||
size_list += f"{mod['size']};"
|
||||
mod_list = path_list + size_list
|
||||
self.log.debug(f"Mods List: {mod_list}")
|
||||
if len(mod_list) == 0:
|
||||
await self.tcp_send(b"-")
|
||||
else:
|
||||
await self.tcp_send(bytes(mod_list, "utf-8"))
|
||||
elif data == b"Done":
|
||||
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
|
||||
break
|
||||
return
|
||||
|
||||
async def looper(self):
|
||||
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID
|
||||
await self.sync_resources()
|
||||
while self.alive:
|
||||
data = await self.recv()
|
||||
if data == b"":
|
||||
if not self.alive:
|
||||
break
|
||||
else:
|
||||
await asyncio.sleep(.2)
|
||||
self.is_disconnected()
|
||||
continue
|
||||
code = data.decode()[0]
|
||||
self.log.debug(f"Received code: {code}, data: {data}")
|
||||
match code:
|
||||
case "H":
|
||||
# Client connected
|
||||
self.ready = True
|
||||
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"), to_all=True)
|
||||
case "C":
|
||||
# Chat
|
||||
await self.tcp_send(data, to_all=True)
|
||||
|
||||
async def remove_me(self):
|
||||
await asyncio.sleep(0.3)
|
||||
self.alive = False
|
||||
if (self.cid > 0 or self.nick is not None) and \
|
||||
self.Core.clients_by_nick.get(self.nick):
|
||||
# if self.ready:
|
||||
# await self.tcp_send(b"", to_all=True) # I'm disconnected.
|
||||
self.log.debug(f"Removing client {self.nick}:{self.cid}")
|
||||
self.log.info("Disconnected")
|
||||
self.Core.clients[self.cid] = None
|
||||
self.Core.clients_by_id.pop(self.cid)
|
||||
self.Core.clients_by_nick.pop(self.nick)
|
||||
else:
|
||||
self.log.debug(f"Removing client; Closing connection...")
|
||||
if not self.writer.is_closing():
|
||||
self.writer.close()
|
||||
_, down_w = self.down_rw
|
||||
if down_w and not down_w.is_closing():
|
||||
down_w.close()
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class Core:
|
||||
|
||||
def __init__(self):
|
||||
self.log = utils.get_logger("core")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.start_time = time.monotonic()
|
||||
self.run = False
|
||||
self.direct = False
|
||||
self.clients = []
|
||||
@@ -315,11 +44,20 @@ class Core:
|
||||
self.web_pool = webapp.data_pool
|
||||
self.web_stop = None
|
||||
|
||||
self.lock_upload = False
|
||||
|
||||
self.client_major_version = "2.0"
|
||||
self.BeamMP_version = "3.2.0"
|
||||
self.BeamMP_version = "3.1.1" # 20.07.2023
|
||||
|
||||
ev.register_event("_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']))
|
||||
|
||||
def get_client(self, cid=None, nick=None):
|
||||
if cid is None and nick is None:
|
||||
return None
|
||||
if cid is not None:
|
||||
if cid == -1:
|
||||
return [i for i in self.clients if i is not None]
|
||||
return self.clients_by_id.get(cid)
|
||||
if nick:
|
||||
return self.clients_by_nick.get(nick)
|
||||
@@ -336,7 +74,7 @@ class Core:
|
||||
break
|
||||
await asyncio.sleep(random.randint(3, 9) * 0.01)
|
||||
if not self.clients[cid]:
|
||||
client.cid = cid
|
||||
client._cid = cid
|
||||
self.clients_by_nick.update({client.nick: client})
|
||||
self.log.debug(f"Inserting client: {client.nick}:{client.cid}")
|
||||
self.clients_by_id.update({client.cid: client})
|
||||
@@ -366,16 +104,28 @@ class Core:
|
||||
|
||||
async def check_alive(self):
|
||||
maxp = config.Game['players']
|
||||
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:
|
||||
if not client:
|
||||
continue
|
||||
if not client.ready:
|
||||
client.is_disconnected()
|
||||
continue
|
||||
await client.tcp_send(bytes(ca, "utf-8"))
|
||||
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:
|
||||
if not client:
|
||||
continue
|
||||
if not client.ready:
|
||||
client.is_disconnected()
|
||||
continue
|
||||
if not client.alive:
|
||||
await client.kick("You are not alive!")
|
||||
await client._send(ca)
|
||||
except Exception as e:
|
||||
self.log.error("Error in check_alive.")
|
||||
self.log.exception(e)
|
||||
|
||||
async def __gracefully_kick(self):
|
||||
for client in self.clients:
|
||||
if not client:
|
||||
continue
|
||||
await client.kick("Server shutdown!")
|
||||
|
||||
@staticmethod
|
||||
def start_web():
|
||||
@@ -397,7 +147,7 @@ class Core:
|
||||
async def heartbeat(self, test=False):
|
||||
if config.Auth["private"] or self.direct:
|
||||
if test:
|
||||
self.log.info(f"Server runnig in Direct connect mode.")
|
||||
self.log.info(i18n.core_direct_mode)
|
||||
self.direct = True
|
||||
return
|
||||
|
||||
@@ -411,13 +161,12 @@ class Core:
|
||||
modstotal = len(self.mods_list) - 1
|
||||
while self.run:
|
||||
try:
|
||||
data = {"uuid": config.Auth["key"], "players": len(self.clients), "maxplayers": config.Game["players"],
|
||||
"port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json",
|
||||
"private": config.Auth['private'], "version": self.BeamMP_version,
|
||||
"clientversion": self.client_major_version,
|
||||
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id),
|
||||
"maxplayers": config.Game["players"], "port": config.Server["server_port"],
|
||||
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'],
|
||||
"version": self.BeamMP_version, "clientversion": self.client_major_version,
|
||||
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
|
||||
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
|
||||
self.log.debug(f"Auth: data {data}")
|
||||
|
||||
# Sentry?
|
||||
ok = False
|
||||
@@ -429,7 +178,6 @@ class Core:
|
||||
async with session.post(url, data=data, headers={"api-v": "2"}) as response:
|
||||
code = response.status
|
||||
body = await response.json()
|
||||
self.log.debug(f"Auth: code {code}, body {body}")
|
||||
ok = True
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -440,29 +188,32 @@ class Core:
|
||||
if not (body.get("status") is not None and
|
||||
body.get("code") is not None and
|
||||
body.get("msg") is not None):
|
||||
self.log.error("Missing/invalid json members in backend response")
|
||||
raise KeyboardInterrupt
|
||||
self.log.error(i18n.core_auth_server_error)
|
||||
return
|
||||
|
||||
if test:
|
||||
status = body.get("status")
|
||||
msg = body.get("msg")
|
||||
if status == "2000":
|
||||
self.log.info(f"Authenticated! {msg}")
|
||||
elif status == "200":
|
||||
self.log.info(f"Resumed authenticated session. {msg}")
|
||||
else:
|
||||
self.log.error(f"Backend REFUSED the auth key. Reason: "
|
||||
f"{msg or 'Backend did not provide a reason'}")
|
||||
self.log.info(f"Server still runnig, but only in Direct connect mode.")
|
||||
self.direct = True
|
||||
status = body.get("status")
|
||||
msg = body.get("msg")
|
||||
if status == "2000":
|
||||
if test:
|
||||
self.log.debug(f"Authenticated! {msg}")
|
||||
elif status == "200":
|
||||
if test:
|
||||
self.log.debug(f"Resumed authenticated session. {msg}")
|
||||
else:
|
||||
self.log.debug(f"Auth: data {data}")
|
||||
self.log.debug(f"Auth: code {code}, body {body}")
|
||||
|
||||
self.log.error(i18n.core_auth_server_refused.format(
|
||||
msg or i18n.core_auth_server_refused_no_reason))
|
||||
self.log.info(i18n.core_auth_server_refused_direct_node)
|
||||
self.direct = True
|
||||
else:
|
||||
self.direct = True
|
||||
if test:
|
||||
self.log.error("Cannot auth...")
|
||||
if not config.Auth['private']:
|
||||
raise KeyboardInterrupt
|
||||
if test:
|
||||
self.log.info(f"Server still runnig, but only in Direct connect mode.")
|
||||
self.log.error(i18n.core_auth_server_no_response)
|
||||
self.log.info(i18n.core_auth_server_refused_direct_node)
|
||||
# if not config.Auth['private']:
|
||||
# raise KeyboardInterrupt
|
||||
|
||||
if test:
|
||||
return ok
|
||||
@@ -471,14 +222,42 @@ class Core:
|
||||
except Exception as e:
|
||||
self.log.error(f"Error in heartbeat: {e}")
|
||||
|
||||
async def kick_cmd(self, args):
|
||||
if not len(args) > 0:
|
||||
return "\nUsage: kick <nick>|:<id> [reason]\nExamples:\n\tkick admin bad boy\n\tkick :0 bad boy"
|
||||
reason = "kicked by console."
|
||||
if len(args) > 1:
|
||||
reason = " ".join(args[1:])
|
||||
cl = args[0]
|
||||
if cl.startswith(":") and cl[1:].isdigit():
|
||||
client = self.get_client(cid=int(cl[1:]))
|
||||
else:
|
||||
client = self.get_client(nick=cl)
|
||||
if client:
|
||||
await client.kick(reason)
|
||||
else:
|
||||
return "Client not found."
|
||||
|
||||
async def main(self):
|
||||
self.run = True
|
||||
self.tcp = self.tcp(self, self.server_ip, self.server_port)
|
||||
self.udp = self.udp(self, self.server_ip, self.server_port)
|
||||
console.add_command(
|
||||
"list",
|
||||
lambda x: f"Players list: {self.get_clients_list(True)}"
|
||||
)
|
||||
console.add_command("kick", self.kick_cmd)
|
||||
|
||||
pl_dir = "plugins"
|
||||
self.log.debug("Initializing PluginsLoaders...")
|
||||
if not os.path.exists(pl_dir):
|
||||
os.mkdir(pl_dir)
|
||||
pl = PluginsLoader(pl_dir)
|
||||
await pl.load()
|
||||
if config.Options['use_lua']:
|
||||
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
|
||||
lpl = LuaPluginsLoader(pl_dir)
|
||||
lpl.load()
|
||||
|
||||
try:
|
||||
# WebApi Start
|
||||
if config.WebAPI["enabled"]:
|
||||
@@ -502,40 +281,61 @@ class Core:
|
||||
self.mods_list.append({"path": path, "size": size})
|
||||
self.mods_list[0] += size
|
||||
self.log.debug(f"mods_list: {self.mods_list}")
|
||||
lmods = len(self.mods_list) - 1
|
||||
if lmods > 0:
|
||||
self.log.info(f"Loaded {lmods} mods: {round(self.mods_list[0] / MB, 2)}mb")
|
||||
len_mods = len(self.mods_list) - 1
|
||||
if len_mods > 0:
|
||||
self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
|
||||
self.log.info(i18n.init_ok)
|
||||
|
||||
await self.heartbeat(True)
|
||||
for i in range(int(config.Game["players"] * 1.3)):
|
||||
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
|
||||
self.clients.append(None)
|
||||
tasks = []
|
||||
# self.udp.start,
|
||||
nrtasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
||||
for task in nrtasks:
|
||||
f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
||||
if config.RCON['enabled']:
|
||||
console.rcon.version = f"KuiToi {__version__}"
|
||||
rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port'])
|
||||
f_tasks.append(rcon.start)
|
||||
for task in f_tasks:
|
||||
tasks.append(asyncio.create_task(task()))
|
||||
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
||||
|
||||
await ev.call_async_event("_plugins_start")
|
||||
|
||||
self.run = True
|
||||
self.log.info(i18n.start)
|
||||
ev.call_event("on_started")
|
||||
await t
|
||||
# Wait the end.
|
||||
ev.call_event("onServerStarted")
|
||||
await ev.call_async_event("onServerStarted")
|
||||
await t # Wait end.
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.log.error(f"Exception: {e}")
|
||||
self.log.exception(e)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.tcp.stop()
|
||||
# self.udp.stop()
|
||||
self.run = False
|
||||
self.tcp.stop()
|
||||
self.udp._stop()
|
||||
await self.stop()
|
||||
|
||||
def start(self):
|
||||
asyncio.run(self.main())
|
||||
|
||||
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.log.info(i18n.stop)
|
||||
if config.WebAPI["enabled"]:
|
||||
asyncio.run(self.web_stop())
|
||||
exit(0)
|
||||
total_time = time.monotonic() - self.start_time
|
||||
hours = int(total_time // 3600)
|
||||
minutes = int((total_time % 3600) // 60)
|
||||
seconds = math.ceil(total_time % 60)
|
||||
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(i18n.stop)
|
||||
|
||||
@@ -1,48 +1,23 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.core.pyi
|
||||
# Written by: SantaSpeen
|
||||
# Version 0.2.2
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import StreamWriter, StreamReader
|
||||
import time
|
||||
from threading import Thread
|
||||
from typing import Callable, List, Dict, Tuple
|
||||
from typing import Callable, List, Dict
|
||||
|
||||
from core import utils
|
||||
from .Client import Client
|
||||
from .tcp_server import TCPServer
|
||||
from .udp_server import UDPServer
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
|
||||
self.log = utils.get_logger("client(id: )")
|
||||
self.addr = writer.get_extra_info("sockname")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
self.cid: int = -1
|
||||
self.key: str = None
|
||||
self.nick: str = None
|
||||
self.roles: str = None
|
||||
self.guest = True
|
||||
self.alive = True
|
||||
self.ready = False
|
||||
def is_disconnected(self) -> bool: ...
|
||||
async def kick(self, reason: str) -> None: ...
|
||||
async def tcp_send(self, data: bytes, to_all:bool = False, writer: StreamWriter = None) -> None: ...
|
||||
async def sync_resources(self) -> None: ...
|
||||
async def recv(self) -> bytes: ...
|
||||
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str) -> None: ...
|
||||
async def looper(self) -> None: ...
|
||||
def _update_logger(self) -> None: ...
|
||||
async def remove_me(self) -> None: ...
|
||||
|
||||
class Core:
|
||||
def __init__(self):
|
||||
self.start_time = time.monotonic()
|
||||
self.log = utils.get_logger("core")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.run = False
|
||||
@@ -59,6 +34,7 @@ class Core:
|
||||
self.udp = UDPServer
|
||||
self.web_thread: Thread = None
|
||||
self.web_stop: Callable = lambda: None
|
||||
self.lock_upload = False
|
||||
self.client_major_version = "2.0"
|
||||
self.BeamMP_version = "3.2.0"
|
||||
def get_client(self, cid=None, nick=None) -> Client | None: ...
|
||||
@@ -66,10 +42,12 @@ class Core:
|
||||
def create_client(self, *args, **kwargs) -> Client: ...
|
||||
def get_clients_list(self, need_cid=False) -> str: ...
|
||||
async def check_alive(self) -> None: ...
|
||||
async def __gracefully_kick(self): ...
|
||||
@staticmethod
|
||||
def start_web() -> None: ...
|
||||
def stop_me(self) -> None: ...
|
||||
async def heartbeat(self, test=False) -> None: ...
|
||||
async def kick_cmd(self, args: list) -> None | str: ...
|
||||
async def main(self) -> None: ...
|
||||
def start(self) -> None: ...
|
||||
def stop(self) -> None: ...
|
||||
async def stop(self) -> None: ...
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
@@ -12,6 +12,7 @@ import aiohttp
|
||||
from core import utils
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class TCPServer:
|
||||
def __init__(self, core, host, port):
|
||||
self.log = utils.get_logger("TCPServer")
|
||||
@@ -23,56 +24,71 @@ class TCPServer:
|
||||
|
||||
async def auth_client(self, reader, writer):
|
||||
client = self.Core.create_client(reader, writer)
|
||||
self.log.info(f"Identifying new ClientConnection...")
|
||||
data = await client.recv()
|
||||
self.log.info(i18n.core_identifying_connection)
|
||||
data = await client._recv(True)
|
||||
self.log.debug(f"Version: {data}")
|
||||
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
|
||||
await client.kick("Outdated Version.")
|
||||
await client.kick(i18n.core_player_kick_outdated)
|
||||
return False, client
|
||||
else:
|
||||
await client.tcp_send(b"S") # Accepted client version
|
||||
await client._send(b"S") # Accepted client version
|
||||
|
||||
data = await client.recv()
|
||||
data = await client._recv(True)
|
||||
self.log.debug(f"Key: {data}")
|
||||
if len(data) > 50:
|
||||
await client.kick("Invalid Key (too long)!")
|
||||
await client.kick(i18n.core_player_kick_bad_key)
|
||||
return False, client
|
||||
client.key = data.decode("utf-8")
|
||||
ev.call_event("auth_sent_key", client)
|
||||
client._key = data.decode("utf-8")
|
||||
ev.call_event("onPlayerSentKey", player=client)
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
url = 'https://auth.beammp.com/pkToUser'
|
||||
async with session.post(url, data={'key': client.key}) as response:
|
||||
async with session.post(url, data={'key': client._key}) as response:
|
||||
res = await response.json()
|
||||
self.log.debug(f"res: {res}")
|
||||
if res.get("error"):
|
||||
await client.kick('Invalid key! Please restart your game.')
|
||||
await client.kick(i18n.core_player_kick_invalid_key)
|
||||
return False, client
|
||||
client.nick = res["username"]
|
||||
client.roles = res["roles"]
|
||||
client.guest = res["guest"]
|
||||
client._guest = res["guest"]
|
||||
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
|
||||
# noinspection PyProtectedMember
|
||||
client._update_logger()
|
||||
except Exception as e:
|
||||
self.log.error(f"Auth error: {e}")
|
||||
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
|
||||
await client.kick(i18n.core_player_kick_auth_server_fail)
|
||||
return False, client
|
||||
|
||||
for _client in self.Core.clients:
|
||||
if not _client:
|
||||
continue
|
||||
if _client.nick == client.nick and _client.guest == client.guest:
|
||||
await client.kick('Stale Client (replaced by new client)')
|
||||
return False, client
|
||||
await _client.kick(i18n.core_player_kick_stale)
|
||||
|
||||
ev.call_event("auth_ok", client)
|
||||
allow = True
|
||||
reason = i18n.core_player_kick_no_allowed_default_reason
|
||||
|
||||
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
|
||||
for data in lua_data:
|
||||
if 1 == data:
|
||||
allow = True
|
||||
elif isinstance(data, str):
|
||||
allow = True
|
||||
reason = data
|
||||
if not allow:
|
||||
await client.kick(reason)
|
||||
return False, client
|
||||
|
||||
ev.call_event("onPlayerAuthenticated", player=client)
|
||||
|
||||
if len(self.Core.clients_by_id) > config.Game["players"]:
|
||||
await client.kick("Server full!")
|
||||
await client.kick(i18n.core_player_kick_server_full)
|
||||
return False, client
|
||||
else:
|
||||
self.log.info("Identification success")
|
||||
self.log.info(i18n.core_identifying_okay)
|
||||
await self.Core.insert_client(client)
|
||||
client.log.info(i18n.core_player_set_id.format(client.pid))
|
||||
|
||||
return True, client
|
||||
|
||||
@@ -81,7 +97,7 @@ class TCPServer:
|
||||
cid = (await reader.read(1))[0]
|
||||
client = self.Core.get_client(cid=cid)
|
||||
if client:
|
||||
client.down_rw = (reader, writer)
|
||||
client._down_sock = (reader, writer)
|
||||
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
|
||||
else:
|
||||
writer.close()
|
||||
@@ -94,7 +110,7 @@ class TCPServer:
|
||||
case "C":
|
||||
result, client = await self.auth_client(reader, writer)
|
||||
if result:
|
||||
await client.looper()
|
||||
await client._looper()
|
||||
return result, client
|
||||
case "D":
|
||||
await self.set_down_rw(reader, writer)
|
||||
@@ -104,6 +120,7 @@ class TCPServer:
|
||||
writer.close()
|
||||
case _:
|
||||
self.log.error(f"Unknown code: {code}")
|
||||
self.log.info("Report about that!")
|
||||
writer.close()
|
||||
return False, None
|
||||
|
||||
@@ -119,11 +136,11 @@ class TCPServer:
|
||||
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
|
||||
_, cl = await self.handle_code(code, reader, writer)
|
||||
if cl:
|
||||
await cl.remove_me()
|
||||
await cl._remove_me()
|
||||
del cl
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.error("Error while connecting..")
|
||||
self.log.error("Error while handling connection...")
|
||||
self.log.exception(e)
|
||||
traceback.print_exc()
|
||||
break
|
||||
@@ -133,15 +150,17 @@ class TCPServer:
|
||||
self.run = True
|
||||
try:
|
||||
server = await asyncio.start_server(self.handle_client, self.host, self.port,
|
||||
backlog=int(config.Game["players"] * 1.3))
|
||||
backlog=int(config.Game["players"] * 2.3))
|
||||
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}")
|
||||
while True:
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
except OSError as e:
|
||||
self.log.error("Cannot bind port")
|
||||
self.log.error(i18n.core_bind_failed.format(e))
|
||||
raise e
|
||||
except BaseException as e:
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.log.error(f"Error: {e}")
|
||||
raise e
|
||||
finally:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.pyi
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
@@ -9,7 +9,7 @@ from asyncio import StreamWriter, StreamReader
|
||||
from typing import Tuple
|
||||
|
||||
from core import utils, Core
|
||||
from core.core import Client
|
||||
from core.Client import Client
|
||||
|
||||
|
||||
class TCPServer:
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.udp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import traceback
|
||||
import json
|
||||
|
||||
from core import utils
|
||||
|
||||
|
||||
class UDPServer:
|
||||
# noinspection PyProtectedMember
|
||||
class UDPServer(asyncio.DatagramTransport):
|
||||
transport = None
|
||||
|
||||
def __init__(self, core, host, port):
|
||||
def __init__(self, core, host=None, port=None):
|
||||
super().__init__()
|
||||
self.log = utils.get_logger("UDPServer")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
@@ -20,41 +23,90 @@ class UDPServer:
|
||||
self.port = port
|
||||
self.run = False
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
while True:
|
||||
try:
|
||||
data = await reader.read(1)
|
||||
if not data:
|
||||
break
|
||||
code = data.decode()
|
||||
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
|
||||
# await self.handle_code(code, reader, writer)
|
||||
# task = asyncio.create_task(self.handle_code(code, reader, writer))
|
||||
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
|
||||
if not writer.is_closing():
|
||||
writer.close()
|
||||
self.log.debug("Disconnected.")
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.error("Error while connecting..")
|
||||
self.log.error(f"Error: {e}")
|
||||
traceback.print_exc()
|
||||
break
|
||||
def connection_made(self, *args, **kwargs): ...
|
||||
def pause_writing(self, *args, **kwargs): ...
|
||||
def resume_writing(self, *args, **kwargs): ...
|
||||
|
||||
async def start(self):
|
||||
self.log.debug("Starting UDP server.")
|
||||
self.run = True
|
||||
async def handle_datagram(self, data, addr):
|
||||
try:
|
||||
pass
|
||||
except OSError as e:
|
||||
self.log.error("Cannot bind port or other error")
|
||||
raise e
|
||||
except BaseException as e:
|
||||
self.log.error(f"Error: {e}")
|
||||
raise e
|
||||
finally:
|
||||
self.run = False
|
||||
self.Core.run = False
|
||||
cid = data[0] - 1
|
||||
code = data[2:3].decode()
|
||||
data = data[2:].decode()
|
||||
|
||||
def stop(self):
|
||||
client = self.Core.get_client(cid=cid)
|
||||
if client:
|
||||
match code:
|
||||
case "p": # Ping packet
|
||||
ev.call_event("onSentPing")
|
||||
self.transport.sendto(b"p", addr)
|
||||
case "Z": # Position packet
|
||||
if client._udp_sock != (self.transport, addr):
|
||||
client._udp_sock = (self.transport, addr)
|
||||
self.log.debug(f"Set UDP Sock for CID: {cid}")
|
||||
ev.call_event("onChangePosition", data=data)
|
||||
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:
|
||||
self.log.debug(f"[{cid}] Client not found.")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
self.log.error(f"Error handle_datagram: {e}")
|
||||
|
||||
def datagram_received(self, *args, **kwargs):
|
||||
self.loop.create_task(self.handle_datagram(*args, **kwargs))
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if exc is not None and exc != KeyboardInterrupt:
|
||||
self.log.debug(f'Connection raised: {exc}')
|
||||
self.log.debug(f'Disconnected.')
|
||||
|
||||
def error_received(self, exc):
|
||||
self.log.debug(f'error_received: {exc}')
|
||||
self.log.exception(exc)
|
||||
self.connection_lost(exc)
|
||||
self.transport.close()
|
||||
|
||||
async def _start(self):
|
||||
self.log.debug("Starting UDP server.")
|
||||
while self.Core.run:
|
||||
try:
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
d = UDPServer
|
||||
self.transport, p = await self.loop.create_datagram_endpoint(
|
||||
lambda: d(self.Core),
|
||||
local_addr=(self.host, self.port)
|
||||
)
|
||||
d.transport = self.transport
|
||||
|
||||
self.log.debug(f"UDP server started on {self.transport.get_extra_info('sockname')}")
|
||||
|
||||
self.run = True
|
||||
while not self.transport.is_closing():
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
except OSError as e:
|
||||
# self.run = False
|
||||
# self.Core.run = False
|
||||
self.log.error(f"Cannot bind port or other error: {e}")
|
||||
except Exception as e:
|
||||
self.log.error(f"Error: {e}")
|
||||
self.log.exception(e)
|
||||
|
||||
def _stop(self):
|
||||
self.log.debug("Stopping UDP server")
|
||||
self.transport.close()
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.udp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import DatagramTransport
|
||||
from typing import Tuple, List
|
||||
|
||||
from core import utils
|
||||
from core.core import Core
|
||||
|
||||
|
||||
class UDPServer:
|
||||
class UDPServer(asyncio.DatagramTransport):
|
||||
transport: DatagramTransport = None
|
||||
|
||||
def __init__(self, core, host, port):
|
||||
def __init__(self, core: Core, host=None, port=None, transport=None):
|
||||
self.log = utils.get_logger("UDPServer")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.run = False
|
||||
async def handle_client(self, srv_sock) -> None: ...
|
||||
async def start(self) -> None: ...
|
||||
|
||||
async def stop(self) -> None: ...
|
||||
# self.transport: DatagramTransport = None
|
||||
def connection_made(self, transport: DatagramTransport): ...
|
||||
async def handle_datagram(self, data: bytes, addr: Tuple[str, int]):
|
||||
def datagram_received(self, data: bytes, addr: Tuple[str, int]): ...
|
||||
async def _start(self) -> None: ...
|
||||
async def _stop(self) -> None: ...
|
||||
@@ -2,7 +2,7 @@
|
||||
# File core.utils.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.1
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import datetime
|
||||
@@ -20,8 +20,8 @@ logging.basicConfig(level=log_level, format=log_format)
|
||||
if not os.path.exists(log_dir):
|
||||
os.mkdir(log_dir)
|
||||
if os.path.exists(log_file):
|
||||
mtime = os.path.getmtime(log_file)
|
||||
gz_path = log_dir + datetime.datetime.fromtimestamp(mtime).strftime('%d.%m.%Y') + "-%s.tar.gz"
|
||||
ftime = os.path.getmtime(log_file)
|
||||
gz_path = log_dir + datetime.datetime.fromtimestamp(ftime).strftime('%d.%m.%Y') + "-%s.tar.gz"
|
||||
index = 1
|
||||
while True:
|
||||
if not os.path.exists(gz_path % index):
|
||||
@@ -33,11 +33,15 @@ if os.path.exists(log_file):
|
||||
if os.path.exists(file):
|
||||
tar.add(file, os.path.basename(file))
|
||||
os.remove(file)
|
||||
fh = logging.FileHandler(log_file, encoding='utf-8')
|
||||
fh = logging.FileHandler(log_file, encoding="utf-8")
|
||||
fh.setFormatter(logging.Formatter(log_format))
|
||||
|
||||
|
||||
def get_logger(name):
|
||||
try:
|
||||
fh.encoding = config.enc
|
||||
except NameError:
|
||||
fh.encoding = "utf-8"
|
||||
log = logging.getLogger(name=name)
|
||||
log.addHandler(fh)
|
||||
log.level = log_level
|
||||
|
||||
@@ -14,17 +14,13 @@ parser.add_argument('-v', '--version', action="store_true", help='Print version
|
||||
parser.add_argument('--config', help='Patch to config file.', nargs='?', default=None, type=str)
|
||||
parser.add_argument('--language', help='Setting localisation.', nargs='?', default=None, type=str)
|
||||
|
||||
run = True
|
||||
|
||||
|
||||
def main():
|
||||
from core import Core
|
||||
core = Core()
|
||||
try:
|
||||
core.start()
|
||||
Core().start()
|
||||
except KeyboardInterrupt:
|
||||
core.run = False
|
||||
core.stop()
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class Config:
|
||||
Auth: dict
|
||||
Game: dict
|
||||
Server: dict
|
||||
WebAPI: dict
|
||||
Auth: Dict[str, object]
|
||||
Game: Dict[str, object]
|
||||
Server: Dict[str, object]
|
||||
RCON: Dict[str, object]
|
||||
Options: Dict[str, object]
|
||||
WebAPI: Dict[str, object]
|
||||
enc: str | None
|
||||
def __repr__(self):
|
||||
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
|
||||
class config (Config): ...
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.config_provider.config_provider.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Version 1.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import os
|
||||
@@ -13,13 +12,17 @@ import yaml
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, auth=None, game=None, server=None, web=None):
|
||||
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!", "language": "en",
|
||||
"server_ip": "0.0.0.0", "server_port": 30814, "debug": False}
|
||||
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,
|
||||
"secret_key": secrets.token_hex(16)}
|
||||
"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)
|
||||
|
||||
186
src/modules/ConsoleSystem/RCON.py
Normal file
186
src/modules/ConsoleSystem/RCON.py
Normal file
@@ -0,0 +1,186 @@
|
||||
import asyncio
|
||||
import binascii
|
||||
import hashlib
|
||||
import os
|
||||
import zlib
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
class RCONSystem:
|
||||
console = None
|
||||
version = "verError"
|
||||
|
||||
def __init__(self, key, host, port):
|
||||
self.log = get_logger("RCON")
|
||||
self.key = hashlib.sha256(key.encode(config.enc)).digest()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.run = False
|
||||
|
||||
def _encrypt(self, message):
|
||||
self.log.debug(f"Encrypt message: {message}")
|
||||
iv = os.urandom(16)
|
||||
cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv))
|
||||
encryptor = cipher.encryptor()
|
||||
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
||||
padded_data = padder.update(message) + padder.finalize()
|
||||
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
|
||||
encoded_data = b64encode(zlib.compress(encrypted_data, level=zlib.Z_BEST_COMPRESSION))
|
||||
encoded_iv = b64encode(iv)
|
||||
return encoded_iv + b":" + encoded_data
|
||||
|
||||
def _decrypt(self, ciphertext):
|
||||
self.log.debug(f"Decrypt message: {ciphertext}")
|
||||
encoded_iv, encoded_data = ciphertext.split(b":", 2)
|
||||
iv = b64decode(encoded_iv)
|
||||
encrypted_data = zlib.decompress(b64decode(encoded_data))
|
||||
cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv))
|
||||
decryptor = cipher.decryptor()
|
||||
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
||||
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
|
||||
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
|
||||
return unpadded_data
|
||||
|
||||
async def _recv(self, reader, writer) -> tuple[str, bool]:
|
||||
try:
|
||||
header = b""
|
||||
while len(header) < 4:
|
||||
h = await reader.read(4 - len(header))
|
||||
if not h:
|
||||
break
|
||||
else:
|
||||
header += h
|
||||
header = int.from_bytes(header, byteorder='little', signed=True)
|
||||
if header <= 0:
|
||||
self.log.warning("Connection closed!")
|
||||
writer.close()
|
||||
|
||||
encrypted_data = b""
|
||||
while len(encrypted_data) < header:
|
||||
buffer = await reader.read(header - len(encrypted_data))
|
||||
if not buffer:
|
||||
break
|
||||
else:
|
||||
encrypted_data += buffer
|
||||
try:
|
||||
data, s = self._decrypt(encrypted_data), True
|
||||
except binascii.Error:
|
||||
data, s = encrypted_data, False
|
||||
except ValueError:
|
||||
data, s = encrypted_data, False
|
||||
|
||||
self.log.debug(f"Received: {data}, {s}")
|
||||
return data.decode(config.enc), s
|
||||
except ConnectionResetError:
|
||||
self.log.warning("Connection reset.")
|
||||
return "", False
|
||||
|
||||
async def _send(self, data, writer, encrypt=True, warn=True):
|
||||
self.log.debug(f"Sending: \"{data}\"")
|
||||
if isinstance(data, str):
|
||||
data = data.encode(config.enc)
|
||||
|
||||
if encrypt:
|
||||
data = self._encrypt(data)
|
||||
self.log.debug(f"Send encrypted: {data}")
|
||||
|
||||
header = len(data).to_bytes(4, "little", signed=True)
|
||||
try:
|
||||
writer.write(header + data)
|
||||
await writer.drain()
|
||||
return True
|
||||
except ConnectionError:
|
||||
self.log.debug("Sending error...")
|
||||
if encrypt and warn:
|
||||
self.log.warning("Connection closed!")
|
||||
return False
|
||||
|
||||
async def send_hello(self, writer, work):
|
||||
while work[0]:
|
||||
await asyncio.sleep(5)
|
||||
if not await self._send("Cs:hello", writer, warn=False):
|
||||
work[0] = False
|
||||
writer.close()
|
||||
break
|
||||
|
||||
async def while_handle(self, reader, writer):
|
||||
ver, status = await self._recv(reader, writer)
|
||||
if ver == "ver" and status:
|
||||
await self._send(self.version, writer)
|
||||
cmds, status = await self._recv(reader, writer)
|
||||
if cmds == "commands" and status:
|
||||
await self._send("SKIP", writer)
|
||||
work = [True]
|
||||
t = asyncio.create_task(self.send_hello(writer, work))
|
||||
while work[0]:
|
||||
data, status = await self._recv(reader, writer)
|
||||
if not status:
|
||||
work[0] = False
|
||||
writer.close()
|
||||
break
|
||||
code = data[:2]
|
||||
message = data[data.find(":") + 1:]
|
||||
match code:
|
||||
case "Cs":
|
||||
match message:
|
||||
case "hello":
|
||||
await self._send("Os:hello", writer)
|
||||
case _:
|
||||
self.log.warning(f"Unknown command: {data}")
|
||||
case "C:":
|
||||
self.log.info(f"Called the command: {message}")
|
||||
if message == "exit":
|
||||
self.log.info("Connection closed.")
|
||||
writer.close()
|
||||
work[0] = False
|
||||
break
|
||||
|
||||
case "Os":
|
||||
match message:
|
||||
case "hello":
|
||||
pass
|
||||
# await self._send("Cs:hello", writer)
|
||||
case _:
|
||||
self.log.warning(f"Unknown command: {data}")
|
||||
case "O:":
|
||||
pass
|
||||
case _:
|
||||
self.log.warning(f"Unknown command: {data}")
|
||||
|
||||
await t
|
||||
|
||||
async def handle_connect(self, reader, writer):
|
||||
try:
|
||||
hello, status = await self._recv(reader, writer)
|
||||
if hello == "hello" and status:
|
||||
await self._send("hello", writer)
|
||||
await self.while_handle(reader, writer)
|
||||
else:
|
||||
await self._send("E:Wrong password", writer, False)
|
||||
writer.close()
|
||||
except Exception as e:
|
||||
self.log.error("Error while handling connection...")
|
||||
self.log.exception(e)
|
||||
|
||||
async def start(self):
|
||||
self.run = True
|
||||
try:
|
||||
server = await asyncio.start_server(self.handle_connect, self.host, self.port, backlog=5)
|
||||
self.log.info(f"RCON server started on {server.sockets[0].getsockname()!r}")
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
except OSError as e:
|
||||
self.log.error(i18n.core_bind_failed.format(e))
|
||||
raise e
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.log.error(f"Error: {e}")
|
||||
raise e
|
||||
finally:
|
||||
self.run = False
|
||||
@@ -1,36 +1,34 @@
|
||||
class Console(object):
|
||||
from logging import Logger
|
||||
from typing import AnyStr
|
||||
|
||||
def __init__(self,
|
||||
prompt_in: str = ">",
|
||||
prompt_out: str = "]:",
|
||||
not_found: str = "Command \"%s\" not found in alias.") -> None: ...
|
||||
from core import get_logger
|
||||
|
||||
def __getitem__(self, item): ...
|
||||
@property
|
||||
def alias(self) -> dict: ...
|
||||
def add(self, key: str, func: function) -> dict: ...
|
||||
def log(self, s: str, r='\r') -> None: ...
|
||||
def write(self, s: str, r='\r') -> None: ...
|
||||
def __lshift__(self, s: AnyStr) -> None: ...
|
||||
def logger_hook(self) -> None: ...
|
||||
def builtins_hook(self) -> None: ...
|
||||
async def start(self) -> None: ...
|
||||
|
||||
class console(object):
|
||||
class RCONSystem:
|
||||
console = None
|
||||
|
||||
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 stop(self): ...
|
||||
|
||||
class console:
|
||||
rcon: RCONSystem = RCONSystem
|
||||
|
||||
@staticmethod
|
||||
def alias() -> dict: ...
|
||||
@staticmethod
|
||||
def add_command(key: str, func: function) -> dict: ...
|
||||
|
||||
def add_command(key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict: ...
|
||||
@staticmethod
|
||||
async def start() -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def builtins_hook() -> None: ...
|
||||
@staticmethod
|
||||
def logger_hook() -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def log(s: str) -> None: ...
|
||||
@staticmethod
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File core.config_provider.py
|
||||
# File modules.ConsoleSystem.console_system.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.1
|
||||
# Version 1.2
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import builtins
|
||||
import inspect
|
||||
import logging
|
||||
from typing import AnyStr
|
||||
|
||||
@@ -14,9 +15,11 @@ 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:
|
||||
@@ -28,6 +31,7 @@ class Console:
|
||||
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
|
||||
@@ -43,6 +47,9 @@ class Console:
|
||||
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}")
|
||||
@@ -87,7 +94,7 @@ class Console:
|
||||
print()
|
||||
raw = True
|
||||
|
||||
message = str()
|
||||
message = "\n"
|
||||
max_len = self.__get_max_len(self.__func.keys())
|
||||
if max_len < 7:
|
||||
max_len = 7
|
||||
@@ -122,19 +129,38 @@ class Console:
|
||||
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.__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 s.startswith("html"):
|
||||
print_formatted_text(HTML(s[4:]))
|
||||
if isinstance(s, (list, tuple)):
|
||||
for text in s:
|
||||
self._write(text)
|
||||
else:
|
||||
print_formatted_text(s)
|
||||
self._write(s)
|
||||
|
||||
def log(self, s: AnyStr) -> None:
|
||||
self.__logger.info(f"{s}")
|
||||
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:
|
||||
@@ -191,23 +217,42 @@ class Console:
|
||||
while True:
|
||||
try:
|
||||
with patch_stdout():
|
||||
cmd_in = await session.prompt_async(
|
||||
self.__prompt_in,
|
||||
completer=self.completer,
|
||||
auto_suggest=AutoSuggestFromHistory()
|
||||
)
|
||||
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:
|
||||
out = command_object['f'](cmd_s[1:])
|
||||
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:
|
||||
self.log(self.__not_found % cmd)
|
||||
if not found_in_lua:
|
||||
self.log(self.__not_found % cmd)
|
||||
except KeyboardInterrupt:
|
||||
raise KeyboardInterrupt
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,46 +1,157 @@
|
||||
# -*- 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):
|
||||
self.__events = {
|
||||
"on_started": [self.on_started],
|
||||
"on_stop": [self.on_stop],
|
||||
"on_auth": [self.on_auth]
|
||||
}
|
||||
# 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):
|
||||
self.log.debug(f"register_event({event_name}, {event_func}):")
|
||||
if not callable(event_func):
|
||||
self.log.error(f"Cannot add event '{event_name}'. "
|
||||
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
|
||||
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 event_name not in self.__events:
|
||||
self.__events.update({str(event_name): [event_func]})
|
||||
else:
|
||||
self.__events[event_name].append(event_func)
|
||||
|
||||
def call_event(self, event_name, *data):
|
||||
self.log.debug(f"Using event '{event_name}'")
|
||||
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 event in self.__events[event_name]:
|
||||
event(*data)
|
||||
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(f"Event {event_name} does not exist. Just skipping it...")
|
||||
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()"))
|
||||
|
||||
def on_started(self):
|
||||
pass
|
||||
return funcs_data
|
||||
|
||||
def on_stop(self):
|
||||
pass
|
||||
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()"))
|
||||
|
||||
def on_auth(self, client):
|
||||
pass
|
||||
return funcs_data
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
class EventsSystem:
|
||||
@staticmethod
|
||||
def register_event(self, event_name, event_func): ...
|
||||
def register_event(event_name, event_func, async_event: bool = False, lua: bool | object = None): ...
|
||||
@staticmethod
|
||||
def call_event(self, event_name, *data): ...
|
||||
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): ...
|
||||
|
||||
100
src/modules/PluginsLoader/add_in.lua
Normal file
100
src/modules/PluginsLoader/add_in.lua
Normal file
@@ -0,0 +1,100 @@
|
||||
package.path = package.path..";modules/PluginsLoader/lua_libs/?.lua"
|
||||
|
||||
MP.CallStrategy = {
|
||||
BestEffort = 0,
|
||||
Precise = 1
|
||||
}
|
||||
|
||||
MP.Settings = {
|
||||
Debug = 0,
|
||||
Private = 1,
|
||||
MaxCars = 2,
|
||||
MaxPlayers = 3,
|
||||
Map = 4,
|
||||
Name = 5,
|
||||
Description = 6
|
||||
}
|
||||
|
||||
function MP.CreateTimer()
|
||||
MP.log.debug("request MP.CreateTimer()")
|
||||
local timer = {}
|
||||
timer.start_time = os.clock()
|
||||
|
||||
function timer:GetCurrent()
|
||||
return os.clock() - self.start_time
|
||||
end
|
||||
|
||||
function timer:Start()
|
||||
self.start_time = os.clock()
|
||||
end
|
||||
|
||||
return timer
|
||||
end
|
||||
|
||||
----Timer object for event timers
|
||||
--local TimedEvent = {}
|
||||
--TimedEvent.__index = TimedEvent
|
||||
--
|
||||
--function TimedEvent:new(interval_ms, event_name, strategy)
|
||||
-- local o = {}
|
||||
-- setmetatable(o, self)
|
||||
-- o.interval = interval_ms
|
||||
-- o.event_name = event_name
|
||||
-- o.strategy = strategy or MP.CallStrategy.BestEffort
|
||||
-- o.last_trigger_time = 0
|
||||
-- o.timer = MP.CreateTimer()
|
||||
-- return o
|
||||
--end
|
||||
--
|
||||
--function TimedEvent:trigger()
|
||||
-- MP.TriggerLocalEvent(self.event_name)
|
||||
-- self.last_trigger_time = self.timer:GetCurrent()
|
||||
--end
|
||||
--
|
||||
--function TimedEvent:is_ready()
|
||||
-- local elapsed_time = self.timer:GetCurrent() - self.last_trigger_time
|
||||
-- return elapsed_time * 1000 >= self.interval
|
||||
--end
|
||||
--
|
||||
---- Event timer management functions
|
||||
--MP.event_timers = {}
|
||||
--MP.event_timers_mutex = {}
|
||||
--
|
||||
--function MP.CreateEventTimer(event_name, interval_ms, strategy)
|
||||
-- MP.log.debug("request MP.CreateEventTimer()")
|
||||
-- strategy = strategy or MP.CallStrategy.BestEffort
|
||||
-- local timer = TimedEvent:new(interval_ms, event_name, strategy)
|
||||
-- table.insert(MP.event_timers, timer)
|
||||
-- MP.log.debug("created event timer for \"" .. event_name .. "\" with " .. interval_ms .. "ms interval")
|
||||
--end
|
||||
--
|
||||
--function MP.CancelEventTimer(event_name)
|
||||
-- MP.log.debug("request MP.CancelEventTimer()")
|
||||
-- for i, timer in ipairs(MP.event_timers) do
|
||||
-- if timer.event_name == event_name then
|
||||
-- table.remove(MP.event_timers, i)
|
||||
-- end
|
||||
-- end
|
||||
-- MP.log.debug("cancelled event timer for \"" .. event_name .. "\"")
|
||||
--end
|
||||
--
|
||||
--function MP.run_event_timers()
|
||||
-- MP.log.debug("request MP.run_event_timers()")
|
||||
-- while true do
|
||||
-- -- Wait for some time before checking timers
|
||||
-- MP.Sleep(100)
|
||||
--
|
||||
-- -- Check each timer and trigger events as necessary
|
||||
-- for _, timer in ipairs(MP.event_timers) do
|
||||
-- if timer:is_ready() then
|
||||
-- if timer.strategy == MP.CallStrategy.Precise then
|
||||
-- while timer:is_ready() do
|
||||
-- timer:trigger()
|
||||
-- end
|
||||
-- else
|
||||
-- timer:trigger()
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
--end
|
||||
673
src/modules/PluginsLoader/lua_plugins_loader.py
Normal file
673
src/modules/PluginsLoader/lua_plugins_loader.py
Normal file
@@ -0,0 +1,673 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
|
||||
import toml
|
||||
from lupa.lua53 import LuaRuntime
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
class EventTimer:
|
||||
def __init__(self, event_name, interval_ms, mp, strategy=None):
|
||||
self.log = get_logger(f"EventTimer | {mp.name}")
|
||||
self.mp = mp
|
||||
self.event_name = event_name
|
||||
self.interval_ms = interval_ms
|
||||
self.strategy = strategy
|
||||
self.timer = None
|
||||
self.stopped = False
|
||||
|
||||
def start(self):
|
||||
def callback():
|
||||
if not self.stopped:
|
||||
self.start()
|
||||
self.trigger_event()
|
||||
|
||||
self.timer = threading.Timer(self.interval_ms / 1000.0, callback)
|
||||
self.timer.start()
|
||||
|
||||
def stop(self):
|
||||
self.stopped = True
|
||||
if self.timer is not None:
|
||||
self.timer.cancel()
|
||||
|
||||
def trigger_event(self):
|
||||
self.log.debug(f"Event '{self.event_name}' triggered")
|
||||
self.mp.TriggerLocalEvent(self.event_name)
|
||||
|
||||
|
||||
# noinspection PyPep8Naming,PyProtectedMember
|
||||
class MP:
|
||||
|
||||
def __init__(self, name: str, lua: LuaRuntime):
|
||||
self.loaded = False
|
||||
self._event_waiters = []
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.log = get_logger(f"LuaPlugin | {name}")
|
||||
self.name = name
|
||||
self.tasks = []
|
||||
self._lua = lua
|
||||
self._local_events = {
|
||||
"onInit": [], "onShutdown": [], "onPlayerAuth": [], "onPlayerConnecting": [], "onPlayerJoining": [],
|
||||
"onPlayerJoin": [], "onPlayerDisconnect": [], "onChatMessage": [], "onVehicleSpawn": [],
|
||||
"onVehicleEdited": [], "onVehicleDeleted": [], "onVehicleReset": [], "onFileChanged": []
|
||||
}
|
||||
self._event_timers = {}
|
||||
|
||||
def _print(self, *args):
|
||||
args = list(args)
|
||||
for i, arg in enumerate(args):
|
||||
if isinstance(arg, str):
|
||||
try:
|
||||
text = arg.encode("CP1251").decode(config.enc).replace("\u001b", "\x1b")
|
||||
args[i] = re.sub(r'\x1b\[.*?m', '', text)
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
if "LuaTable" in str(type(arg)):
|
||||
args[i] = self._lua.globals().Util.JsonEncode(arg)
|
||||
s = " ".join(map(str, args))
|
||||
self.log.info(s)
|
||||
|
||||
def GetOSName(self) -> str:
|
||||
self.log.debug("request MP.GetOSName()")
|
||||
pl = platform.system()
|
||||
if pl in ["Linux", "Windows"]:
|
||||
return pl
|
||||
return "Other"
|
||||
|
||||
def GetServerVersion(self) -> tuple[int, int, int]:
|
||||
self.log.debug("request MP.GetServerVersion()")
|
||||
return ev.call_event("_get_BeamMP_version")[0]
|
||||
|
||||
def RegisterEvent(self, event_name: str, function_name: str) -> None:
|
||||
self.log.debug("request MP.RegisterEvent()")
|
||||
ev.register_event(event_name, function_name, lua=self._lua)
|
||||
if event_name not in self._local_events:
|
||||
self._local_events.update({str(event_name): [function_name]})
|
||||
else:
|
||||
self._local_events[event_name].append(function_name)
|
||||
self.log.debug("Register ok (local)")
|
||||
|
||||
def CreateEventTimer(self, event_name: str, interval_ms: int, strategy: int = None):
|
||||
self.log.debug("request CreateEventTimer()")
|
||||
event_timer = EventTimer(event_name, interval_ms, self, strategy)
|
||||
self._event_timers[event_name] = event_timer
|
||||
event_timer.start()
|
||||
|
||||
def CancelEventTimer(self, event_name: str):
|
||||
self.log.debug("request CancelEventTimer()")
|
||||
if event_name in self._event_timers:
|
||||
event_timer = self._event_timers[event_name]
|
||||
event_timer.stop()
|
||||
del self._event_timers[event_name]
|
||||
|
||||
def TriggerLocalEvent(self, event_name, *args):
|
||||
self.log.debug("request TriggerLocalEvent()")
|
||||
self.log.debug(f"Calling local lua event: '{event_name}'")
|
||||
funcs_data = []
|
||||
if event_name in self._local_events.keys():
|
||||
for func_name in self._local_events[event_name]:
|
||||
try:
|
||||
func = self._lua.globals()[func_name]
|
||||
if not func:
|
||||
self.log.warning(i18n.events_lua_function_not_found.format(i18n.events_lua_local, 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 self._lua.table_from(funcs_data)
|
||||
|
||||
def TriggerGlobalEvent(self, event_name, *args):
|
||||
self.log.debug("request TriggerGlobalEvent()")
|
||||
return self._lua.table(
|
||||
IsDone=lambda: True,
|
||||
GetResults=lambda: self._lua.table_from(ev.call_lua_event(event_name, *args))
|
||||
)
|
||||
|
||||
def Sleep(self, time_ms):
|
||||
self.log.debug(f"request Sleep(); Thread: {threading.current_thread().name}")
|
||||
time.sleep(time_ms * 0.001)
|
||||
|
||||
def SendChatMessage(self, player_id, message):
|
||||
self.log.debug("request SendChatMessage()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
to_all = False
|
||||
if player_id < 0:
|
||||
to_all = True
|
||||
client = client[0]
|
||||
if client and message:
|
||||
t = self.loop.create_task(client.send_message(f"Server: {message}", to_all=to_all))
|
||||
self.tasks.append(t)
|
||||
|
||||
def TriggerClientEvent(self, player_id, event_name, data):
|
||||
self.log.debug("request TriggerClientEvent()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
to_all = False
|
||||
if player_id < 0:
|
||||
to_all = True
|
||||
client = client[0]
|
||||
if client and event_name and data:
|
||||
t = self.loop.create_task(client.send_event(event_name, data, to_all=to_all))
|
||||
self.tasks.append(t)
|
||||
return True, None
|
||||
elif not client:
|
||||
return False, "Client expired"
|
||||
else:
|
||||
return False, "Can't found event_name or data"
|
||||
|
||||
def TriggerClientEventJson(self, player_id, event_name, data):
|
||||
self.log.debug("request TriggerClientEventJson()")
|
||||
data = self._lua.globals().Util.JsonEncode(data)
|
||||
self.TriggerClientEvent(player_id, event_name, data)
|
||||
|
||||
def GetPlayerCount(self):
|
||||
self.log.debug("request GetPlayerCount()")
|
||||
return len(ev.call_event("_get_player", cid=-1)[0])
|
||||
|
||||
def GetPositionRaw(self, player_id, car_id):
|
||||
self.log.debug("request GetPositionRaw()")
|
||||
if player_id < 0:
|
||||
return self._lua.table(), "Bad client"
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
car = client._cars[car_id]
|
||||
if car:
|
||||
return self._lua.table_from(car['pos'])
|
||||
return self._lua.table(), "Vehicle not found"
|
||||
return self._lua.table(), "Client expired"
|
||||
|
||||
def IsPlayerConnected(self, player_id):
|
||||
self.log.debug("request IsPlayerConnected()")
|
||||
if player_id < 0:
|
||||
return False
|
||||
return bool(ev.call_event("_get_player", cid=player_id)[0])
|
||||
|
||||
def GetPlayerName(self, player_id):
|
||||
self.log.debug("request GetPlayerName()")
|
||||
if player_id < 0:
|
||||
return None
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return client.nick
|
||||
return
|
||||
|
||||
def GetPlayerIDByName(self, player_name):
|
||||
self.log.debug("request GetPlayerIDByName()")
|
||||
if not isinstance(player_name, str):
|
||||
return None
|
||||
client = ev.call_event("_get_player", nick=player_name)[0]
|
||||
if client:
|
||||
return client.cid
|
||||
return
|
||||
|
||||
def RemoveVehicle(self, player_id, vehicle_id):
|
||||
self.log.debug("request RemoveVehicle()")
|
||||
if player_id < 0:
|
||||
return
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
t = self.loop.create_task(client._delete_car(car_id=vehicle_id))
|
||||
self.tasks.append(t)
|
||||
|
||||
def GetPlayerVehicles(self, player_id):
|
||||
self.log.debug("request GetPlayerVehicles()")
|
||||
if player_id < 0:
|
||||
return self._lua.table()
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return self._lua.table_from([f'{v["json"]}' for d in [i for i in client._cars if i is not None]
|
||||
for k, v in d.items() if k == "json"])
|
||||
|
||||
def GetPlayers(self):
|
||||
self.log.debug("request GetPlayers()")
|
||||
clients = ev.call_event("_get_players", cid=-1)
|
||||
return self._lua.table_from(clients)
|
||||
|
||||
def IsPlayerGuest(self, player_id) -> bool:
|
||||
self.log.debug("request IsPlayerGuest()")
|
||||
if player_id < 0:
|
||||
return True
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return client.guest
|
||||
return False
|
||||
|
||||
def DropPlayer(self, player_id, reason="Kicked"):
|
||||
self.log.debug("request DropPlayer()")
|
||||
if player_id < 0:
|
||||
return
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
t = self.loop.create_task(client.kick(reason))
|
||||
self.tasks.append(t)
|
||||
|
||||
def GetStateMemoryUsage(self):
|
||||
self.log.debug("request GetStateMemoryUsage()")
|
||||
return self._lua.get_memory_used()
|
||||
|
||||
def GetLuaMemoryUsage(self):
|
||||
self.log.debug("request GetStateMemoryUsage()")
|
||||
lua_plugins = ev.call_event("_lua_plugins_get")[0]
|
||||
return sum(pl['lua'].get_memory_used() for pls in lua_plugins.values() for pl in pls.values())
|
||||
|
||||
def GetPlayerIdentifiers(self, player_id):
|
||||
self.log.debug("request GetStateMemoryUsage()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return self._lua.table_from(client.identifiers)
|
||||
return self._lua.table()
|
||||
|
||||
def Set(self, *_):
|
||||
self.log.debug("request Set")
|
||||
self.log.warning("KuiToi cannot support this: MP.Set()")
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class Util:
|
||||
def __init__(self, name, lua):
|
||||
self.log = get_logger(f"LuaPlugin | Util | {name}")
|
||||
self.name = name
|
||||
self._lua = lua
|
||||
|
||||
def _recursive_list_encode(self, table):
|
||||
new_list = list(table.values())
|
||||
for i, v in enumerate(list(table.values())):
|
||||
if not isinstance(v, (int, float, bool, str, dict, list)) and "LuaTable" not in str(type(v)):
|
||||
new_list[i] = None
|
||||
continue
|
||||
if "LuaTable" in str(type(v)):
|
||||
d = dict(v)
|
||||
if all(isinstance(ii, int) for ii in d.keys()):
|
||||
new_list[i] = self._recursive_list_encode(d)
|
||||
continue
|
||||
else:
|
||||
new_list[i] = self._recursive_dict_encode(d)
|
||||
return [i for i in new_list if i is not None]
|
||||
|
||||
def _recursive_dict_encode(self, table):
|
||||
new_dict = dict(table)
|
||||
for k, v in table.items():
|
||||
if not isinstance(v, (int, float, bool, str, dict, list)) and "LuaTable" not in str(type(v)):
|
||||
new_dict[k] = None
|
||||
continue
|
||||
if "LuaTable" in str(type(v)):
|
||||
d = dict(v)
|
||||
if all(isinstance(i, int) for i in d.keys()):
|
||||
new_dict[k] = self._recursive_list_encode(d)
|
||||
continue
|
||||
else:
|
||||
new_dict[k] = self._recursive_dict_encode(d)
|
||||
return {k: v for k, v in new_dict.items() if v is not None}
|
||||
|
||||
def JsonEncode(self, table):
|
||||
self.log.debug("requesting JsonEncode()")
|
||||
if all(isinstance(k, int) for k in table.keys()):
|
||||
data = self._recursive_list_encode(table)
|
||||
else:
|
||||
data = self._recursive_dict_encode(table)
|
||||
return json.dumps(data)
|
||||
|
||||
def JsonDecode(self, string):
|
||||
self.log.debug("requesting JsonDecode()")
|
||||
return self._lua.table_from(json.loads(string))
|
||||
|
||||
def JsonPrettify(self, string):
|
||||
self.log.debug("requesting JsonPrettify()")
|
||||
data = json.loads(string)
|
||||
return json.dumps(data, indent=4, sort_keys=True)
|
||||
|
||||
def JsonMinify(self, string):
|
||||
self.log.debug("requesting JsonMinify()")
|
||||
data = json.loads(string)
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
def JsonFlatten(self, json_str):
|
||||
self.log.debug("request JsonFlatten()")
|
||||
json_obj = json.loads(json_str)
|
||||
flat_obj = {}
|
||||
|
||||
def flatten(obj, path=''):
|
||||
if isinstance(obj, dict):
|
||||
for key in obj:
|
||||
flatten(obj[key], path + '/' + key)
|
||||
elif isinstance(obj, list):
|
||||
for i in range(len(obj)):
|
||||
flatten(obj[i], path + '/' + str(i))
|
||||
else:
|
||||
flat_obj[path] = obj
|
||||
|
||||
flatten(json_obj)
|
||||
flat_json = json.dumps(flat_obj)
|
||||
return flat_json
|
||||
|
||||
def JsonUnflatten(self, flat_json):
|
||||
self.log.debug("request JsonUnflatten")
|
||||
flat_obj = json.loads(flat_json)
|
||||
|
||||
def unflatten(obj):
|
||||
result = {}
|
||||
for key in obj:
|
||||
parts = key.split('/')
|
||||
d = result
|
||||
for part in parts[:-1]:
|
||||
if part not in d:
|
||||
# create a new node in the dictionary
|
||||
# if the path doesn't exist
|
||||
d[part] = {}
|
||||
d = d[part]
|
||||
# assign the value to the last part of the path
|
||||
d[parts[-1]] = obj[key]
|
||||
return result
|
||||
|
||||
json_obj = unflatten(flat_obj)
|
||||
return json.dumps(json_obj)
|
||||
|
||||
def JsonDiff(self, a: str, b: str) -> str:
|
||||
self.log.debug("requesting JsonDiff()")
|
||||
a_obj = json.loads(a)
|
||||
b_obj = json.loads(b)
|
||||
diff = []
|
||||
for k, v in b_obj.items():
|
||||
if k not in a_obj:
|
||||
diff.append({"op": "add", "path": "/" + k, "value": v})
|
||||
elif a_obj[k] != v:
|
||||
diff.append({"op": "replace", "path": "/" + k, "value": v})
|
||||
for k in a_obj.keys() - b_obj.keys():
|
||||
diff.append({"op": "remove", "path": "/" + k})
|
||||
return json.dumps(diff)
|
||||
|
||||
@staticmethod
|
||||
def _apply_patch(base_obj, patch_obj):
|
||||
for patch in patch_obj:
|
||||
op = patch['op']
|
||||
path = patch['path']
|
||||
value = patch.get('value', None)
|
||||
tokens = path.strip('/').split('/')
|
||||
obj = base_obj
|
||||
for i, token in enumerate(tokens):
|
||||
if isinstance(obj, list):
|
||||
token = int(token)
|
||||
if i == len(tokens) - 1:
|
||||
if op == 'add':
|
||||
if isinstance(obj, list):
|
||||
obj.insert(int(token), value)
|
||||
else:
|
||||
obj[token] = value
|
||||
elif op == 'replace':
|
||||
obj[token] = value
|
||||
elif op == 'remove':
|
||||
if isinstance(obj, list):
|
||||
obj.pop(int(token))
|
||||
else:
|
||||
del obj[token]
|
||||
else:
|
||||
obj = obj[token]
|
||||
return base_obj
|
||||
|
||||
def JsonDiffApply(self, base: str, diff: str) -> str:
|
||||
self.log.debug("requesting JsonDiffApply()")
|
||||
base_obj = json.loads(base)
|
||||
diff_obj = json.loads(diff)
|
||||
result = self._apply_patch(base_obj, diff_obj)
|
||||
return json.dumps(result)
|
||||
|
||||
def Random(self) -> float:
|
||||
self.log.debug("requesting Random()")
|
||||
return random.random()
|
||||
|
||||
def RandomIntRange(self, min_v, max_v) -> int:
|
||||
self.log.debug("requesting RandomIntRange()")
|
||||
return random.randint(min_v, max_v)
|
||||
|
||||
def RandomRange(self, min_v, max_v) -> float:
|
||||
self.log.debug("requesting RandomRange()")
|
||||
return random.uniform(min_v, max_v)
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class FS:
|
||||
|
||||
def __init__(self, name: str, lua: LuaRuntime):
|
||||
self.log = get_logger(f"LuaPlugin | FP | {name}")
|
||||
self.name = name
|
||||
self._lua = lua
|
||||
|
||||
def CreateDirectory(self, path):
|
||||
self.log.debug("requesting CreateDirectory()")
|
||||
try:
|
||||
os.makedirs(path)
|
||||
return True, None
|
||||
except FileExistsError:
|
||||
return True, None
|
||||
except FileNotFoundError | NotADirectoryError as e:
|
||||
return False, f"{e}"
|
||||
except PermissionError as e:
|
||||
return False, f"{e}"
|
||||
except OSError as e:
|
||||
return False, f"{e}"
|
||||
except TypeError as e:
|
||||
return False, f"{e}"
|
||||
except ValueError as e:
|
||||
return False, f"{e}"
|
||||
|
||||
def Remove(self, path):
|
||||
self.log.debug("requesting Remove()")
|
||||
try:
|
||||
if os.path.isdir(path):
|
||||
os.rmdir(path)
|
||||
else:
|
||||
os.remove(path)
|
||||
return True, None
|
||||
except (FileNotFoundError, NotADirectoryError) as e:
|
||||
return False, f"{e}"
|
||||
except PermissionError as e:
|
||||
return False, f"{e}"
|
||||
except OSError as e:
|
||||
return False, f"{e}"
|
||||
except TypeError as e:
|
||||
return False, f"{e}"
|
||||
|
||||
def Rename(self, path_from, path_to):
|
||||
self.log.debug("requesting Rename()")
|
||||
try:
|
||||
os.rename(path_from, path_to)
|
||||
return True, None
|
||||
except (FileNotFoundError, NotADirectoryError) as e:
|
||||
return False, f"{e}"
|
||||
except PermissionError as e:
|
||||
return False, f"{e}"
|
||||
except OSError as e:
|
||||
return False, f"{e}"
|
||||
except TypeError as e:
|
||||
return False, f"{e}"
|
||||
|
||||
def Copy(self, path_from, path_to):
|
||||
self.log.debug("requesting Copy()")
|
||||
try:
|
||||
if os.path.isfile(path_from):
|
||||
shutil.copy2(path_from, path_to)
|
||||
elif os.path.isdir(path_from):
|
||||
shutil.copytree(path_from, path_to)
|
||||
else:
|
||||
raise ValueError("Invalid path: {}".format(path_from))
|
||||
return True, None
|
||||
except (FileNotFoundError, NotADirectoryError, shutil.Error) as e:
|
||||
return False, f"{e}"
|
||||
except PermissionError as e:
|
||||
return False, f"{e}"
|
||||
except OSError as e:
|
||||
return False, f"{e}"
|
||||
except TypeError as e:
|
||||
return False, f"{e}"
|
||||
|
||||
def GetFilename(self, path):
|
||||
self.log.debug("requesting GetFilename()")
|
||||
return os.path.basename(path)
|
||||
|
||||
def GetExtension(self, path):
|
||||
self.log.debug("requesting GetExtension()")
|
||||
return os.path.splitext(path)[1]
|
||||
|
||||
def GetParentFolder(self, path):
|
||||
self.log.debug("requesting GetParentFolder()")
|
||||
return os.path.dirname(path)
|
||||
|
||||
def Exists(self, path):
|
||||
self.log.debug("requesting Exists()")
|
||||
return os.path.exists(path)
|
||||
|
||||
def IsDirectory(self, path):
|
||||
self.log.debug("requesting IsDirectory()")
|
||||
return os.path.isdir(path)
|
||||
|
||||
def IsFile(self, path):
|
||||
self.log.debug("requesting IsFile()")
|
||||
return os.path.isfile(path)
|
||||
|
||||
def ListDirectories(self, path):
|
||||
self.log.debug("requesting ListDirectories()")
|
||||
directories = []
|
||||
for item in os.listdir(path):
|
||||
item_path = os.path.join(path, item)
|
||||
if os.path.isdir(item_path):
|
||||
directories.append(item)
|
||||
return self._lua.table_from(directories)
|
||||
|
||||
def ListFiles(self, path):
|
||||
self.log.debug("requesting ListFiles()")
|
||||
files = []
|
||||
for item in os.listdir(path):
|
||||
item_path = os.path.join(path, item)
|
||||
if os.path.isfile(item_path):
|
||||
files.append(item)
|
||||
return self._lua.table_from(files)
|
||||
|
||||
def ConcatPaths(self, *args):
|
||||
self.log.debug("requesting ConcatPaths()")
|
||||
return os.path.join(*args)
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class LuaPluginsLoader:
|
||||
|
||||
def __init__(self, plugins_dir):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.plugins_dir = plugins_dir
|
||||
self.lua_plugins = {}
|
||||
self.lua_plugins_tasks = []
|
||||
self.lua_dirs = set()
|
||||
self.log = get_logger("LuaPluginsLoader")
|
||||
self.loaded_str = "Lua plugins: "
|
||||
ev.register_event("_lua_plugins_get", lambda x: self.lua_plugins)
|
||||
ev.register_event("_lua_plugins_unload", self.unload)
|
||||
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
|
||||
console.add_command("lua_pl", lambda x: self.loaded_str[:-2])
|
||||
|
||||
def load(self):
|
||||
self.log.debug("Loading Lua plugins...")
|
||||
self.log.info(i18n.plugins_lua_enabled)
|
||||
self.log.warning(i18n.plugins_lua_nuances_warning)
|
||||
self.log.warning(i18n.plugins_lua_legacy_config_create_warning)
|
||||
self.log.info(i18n.plugins_lua_legacy_config_create)
|
||||
data = {
|
||||
"info": "ServerConfig.toml is created solely for backward compatibility support. "
|
||||
"This file will be updated every time the program is launched.",
|
||||
"General": {
|
||||
"Name": config.Server['name'],
|
||||
"Port": config.Server['server_port'],
|
||||
"AuthKey": config.Auth['key'],
|
||||
"LogChat": config.Options['log_chat'],
|
||||
"Debug": config.Options['debug'],
|
||||
"Private": config.Auth['private'],
|
||||
"MaxCars": config.Game['max_cars'],
|
||||
"MaxPlayers": config.Game['players'],
|
||||
"Map": f"/levels/{config.Game['map']}/info.json",
|
||||
"Description": config.Server['description'],
|
||||
"ResourceFolder": "plugins/"
|
||||
},
|
||||
"Misc": {
|
||||
"ImScaredOfUpdates": False,
|
||||
"SendErrorsShowMessage": 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:
|
||||
toml.dump(data, f)
|
||||
self.log.warning("KuiToi will not support at all: MP.Set()")
|
||||
py_folders = ev.call_event("_plugins_get")[0]
|
||||
for name in os.listdir(self.plugins_dir):
|
||||
path = os.path.join(self.plugins_dir, name)
|
||||
if os.path.isdir(path) and name not in py_folders and name not in "__pycache__":
|
||||
plugin_path = os.path.join(self.plugins_dir, name)
|
||||
for file in os.listdir(plugin_path):
|
||||
path = f"plugins/{name}/{file}"
|
||||
if os.path.isfile(path) and path.endswith(".lua"):
|
||||
self.lua_dirs.add(name)
|
||||
|
||||
self.log.debug(f"py_folders {py_folders}, lua_dirs {self.lua_dirs}")
|
||||
|
||||
for name in self.lua_dirs:
|
||||
# noinspection PyArgumentList
|
||||
lua = LuaRuntime(encoding="CP1251", source_encoding=config.enc, unpack_returned_tuples=True)
|
||||
lua_globals = lua.globals()
|
||||
lua_globals.printRaw = lua.globals().print
|
||||
lua_globals.exit = lambda x: self.log.info(f"KuiToi: You can't disable server..")
|
||||
mp = MP(name, lua)
|
||||
lua_globals.MP = mp
|
||||
lua_globals.print = mp._print
|
||||
lua_globals.Util = Util(name, lua)
|
||||
lua_globals.FS = FS(name, lua)
|
||||
pa = os.path.abspath(self.plugins_dir)
|
||||
p0 = os.path.join(pa, name, "?.lua")
|
||||
p1 = os.path.join(pa, name, "lua", "?.lua")
|
||||
lua_globals.package.path += f';{p0};{p1}'
|
||||
with open("modules/PluginsLoader/add_in.lua", "r") as f:
|
||||
lua.execute(f.read())
|
||||
self.lua_plugins.update({name: {"lua": lua, "ok": False}})
|
||||
plugin_path = os.path.join(self.plugins_dir, name)
|
||||
for file in os.listdir(plugin_path):
|
||||
path = f"plugins/{name}/{file}"
|
||||
if os.path.isfile(path) and path.endswith(".lua"):
|
||||
try:
|
||||
lua_globals.loadfile(path)()
|
||||
except Exception as e:
|
||||
self.loaded_str += f"{name}:no, "
|
||||
self.log.error(f"Cannot load lua plugin from `{path}`: {e}")
|
||||
try:
|
||||
lua_globals.MP.loaded = True
|
||||
lua_globals.MP.TriggerLocalEvent("onInit")
|
||||
lua_globals.onInit()
|
||||
self.lua_plugins[name]['ok'] = True
|
||||
self.loaded_str += f"{name}:ok, "
|
||||
except Exception as e:
|
||||
self.loaded_str += f"{name}:no, "
|
||||
self.log.error(f"Exception onInit from `{name}`: {e}")
|
||||
self.log.exception(e)
|
||||
|
||||
def unload(self, _):
|
||||
self.log.debug("Unloading lua plugins")
|
||||
for name, data in self.lua_plugins.items():
|
||||
if data['ok']:
|
||||
self.log.info(i18n.plugins_lua_unload.format(name))
|
||||
for _, timer in data['lua'].globals().MP._event_timers.items():
|
||||
timer.stop()
|
||||
@@ -1,52 +1,219 @@
|
||||
# -*- 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=None):
|
||||
def __init__(self, name):
|
||||
if name is None:
|
||||
raise Exception("BeamMP: Name is required")
|
||||
self.log = get_logger(f"PluginsLoader | {name}")
|
||||
self.name = name
|
||||
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)
|
||||
|
||||
def set_name(self, name):
|
||||
self.name = name
|
||||
@property
|
||||
def log(self):
|
||||
return self.__log
|
||||
|
||||
@staticmethod
|
||||
def register_event(event_name, event_func):
|
||||
@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)
|
||||
|
||||
@staticmethod
|
||||
def call_event(event_name, *data):
|
||||
ev.call_event(event_name, *data)
|
||||
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.__plugins = {}
|
||||
self.__plugins_dir = 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])
|
||||
|
||||
def load_plugins(self):
|
||||
async def load(self):
|
||||
self.log.debug("Loading plugins...")
|
||||
files = os.listdir(self.__plugins_dir)
|
||||
for file in files:
|
||||
if file.endswith(".py"):
|
||||
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}")
|
||||
plugin = types.ModuleType('plugin')
|
||||
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
|
||||
file = os.path.join(self.__plugins_dir, file)
|
||||
with open(f'{file}', 'r') as f:
|
||||
code = f.read().replace("import KuiToi\n", "")
|
||||
plugin.__file__ = file_path
|
||||
with open(f'{file_path}', 'r', encoding=config.enc) as f:
|
||||
code = f.read()
|
||||
exec(code, plugin.__dict__)
|
||||
plugin.load()
|
||||
self.__plugins.update({file[:-3]: plugin})
|
||||
self.log.debug(f"Plugin loaded: {file}")
|
||||
|
||||
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.log.error(f"Error loading plugin: {file}; Error: {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)
|
||||
|
||||
@@ -122,6 +122,5 @@ def hack_fastapi():
|
||||
})
|
||||
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
|
||||
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
|
||||
print(LOGGING_CONFIG)
|
||||
|
||||
|
||||
|
||||
107
src/modules/i18n/files/cn.json
Normal file
107
src/modules/i18n/files/cn.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"": "基本阶段",
|
||||
"hello": "来自KuiToi-Server的问候!",
|
||||
"config_path": "使用{}进行配置。",
|
||||
"init_ok": "初始化完成。",
|
||||
"start": "服务器已启动!",
|
||||
"stop": "服务器已停止!",
|
||||
|
||||
"": "服务器认证",
|
||||
"auth_need_key": "需要BeamMP密钥才能运行!",
|
||||
"auth_empty_key": "BeamMP密钥为空!",
|
||||
"auth_cannot_open_browser": "无法打开浏览器:{}",
|
||||
"auth_use_link": "使用此链接:{}",
|
||||
|
||||
"": "GUI阶段",
|
||||
"GUI_yes": "是",
|
||||
"GUI_no": "否",
|
||||
"GUI_ok": "确定",
|
||||
"GUI_cancel": "取消",
|
||||
"GUI_need_key_message": "需要BeamMP密钥才能运行!\n您是否要在浏览器中打开链接获取密钥?",
|
||||
"GUI_enter_key_message": "请输入密钥:",
|
||||
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
||||
|
||||
"": "Web阶段",
|
||||
"web_start": "WebAPI已启动于{}(按CTRL+C停止)",
|
||||
|
||||
"": "核心短语",
|
||||
"core_bind_failed": "无法绑定端口。错误:{}",
|
||||
"core_direct_mode": "服务器以直接连接模式启动。",
|
||||
"core_auth_server_error": "从BeamMP认证服务器接收到无效响应。",
|
||||
"core_auth_server_refused": "BeamMP认证服务器拒绝了您的密钥。原因:{}",
|
||||
"core_auth_server_refused_no_reason": "BeamMP认证服务器没有提供原因。",
|
||||
"core_auth_server_refused_direct_node": "服务器仍在运行,但以直接连接模式运行。",
|
||||
"core_auth_server_no_response": "无法验证服务器。",
|
||||
"core_mods_loaded": "已加载{}个模组。{}Mb",
|
||||
"core_identifying_connection": "正在处理新连接...",
|
||||
"core_player_kick_outdated": "BeamMP版本不正确。",
|
||||
"core_player_kick_bad_key": "传递的密钥无效!",
|
||||
"core_player_kick_invalid_key": "无效的密钥!请重新启动游戏。",
|
||||
"core_player_kick_auth_server_fail": "BeamMP认证服务器失败!请在5分钟后再次尝试连接。",
|
||||
"core_player_kick_stale": "过时的客户端。(由新连接替换)",
|
||||
"core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。",
|
||||
"core_player_kick_server_full": "服务器已满。",
|
||||
"core_player_set_id": "玩家设置ID {}",
|
||||
"core_identifying_okay": "成功登录。",
|
||||
|
||||
"": "游戏内短语",
|
||||
"game_welcome_message": "欢迎{}!",
|
||||
|
||||
"": "客户端类短语",
|
||||
"client_mod_request": "请求模组:{}",
|
||||
"client_mod_sent": "已发送模组:大小:{}mb,速度:{}Mb/s({}秒)",
|
||||
"client_mod_sent_limit": "(限制{}Mb/s)",
|
||||
"client_mod_sent_error": "发送模组时出错:{}",
|
||||
"client_sync_time": "同步时间{}秒。",
|
||||
"client_kicked": "因\"{}\"原因被踢出。",
|
||||
"client_event_invalid_data": "从事件返回的数据无效:{}",
|
||||
"client_player_disconnected": "离开服务器。游戏时间:{}分钟。",
|
||||
|
||||
"": "事件系统",
|
||||
|
||||
"events_not_callable": "无法添加事件\"{}\"。请改用\"{}\"。跳过...",
|
||||
"events_not_found": "事件\"{}\"未注册。也许{}?跳过...",
|
||||
"events_calling_error": "调用函数\"{}\"时出错。",
|
||||
"events_lua_function_not_found": "无法调用{}lua事件 - 未找到\"{}\"。",
|
||||
"events_lua_local": "本地 ",
|
||||
"events_lua_calling_error": "错误:\"{}\" - 调用lua事件\"{}\"时出错,函数:\"{}\",参数:{}",
|
||||
|
||||
"": "插件加载器",
|
||||
|
||||
"plugins_not_found_load": "未找到\"def load():\"函数。",
|
||||
"plugins_not_found_start": "未找到\"def start():\"函数。",
|
||||
"plugins_not_found_unload": "未找到\"def unload():\"函数。",
|
||||
"plugins_kt_invalid": "“kt”变量不属于KuiToi类。",
|
||||
"plugins_invalid": "无法在KuiToi中运行插件\"{}\"。",
|
||||
"plugins_error_loading": "加载插件{}时出错:{}",
|
||||
|
||||
"": "Lua插件加载器",
|
||||
|
||||
"plugins_lua_enabled": "您已启用Lua插件支持。",
|
||||
"plugins_lua_nuances_warning": "在使用KuiToi时有一些细微差别。如果您有关于解决方案的建议,并且它与KuiToi相关,请联系开发人员。",
|
||||
"plugins_lua_legacy_config_create_warning": "一些BeamMP插件需要一个正确配置的ServerConfig.toml文件才能正常运行。",
|
||||
"plugins_lua_legacy_config_create": "正在创建。",
|
||||
"plugins_lua_unload": "停止Lua插件:{}",
|
||||
|
||||
"": "命令:man",
|
||||
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法:man COMMAND",
|
||||
"help_message_man": "显示COMMAND的帮助页面。",
|
||||
"man_for": "帮助页面",
|
||||
"man_message_not_found": "man:找不到帮助页面。",
|
||||
"man_command_not_found": "man:找不到命令\"{}\"!",
|
||||
|
||||
"": "命令:help",
|
||||
"man_message_help": "help - 显示命令的名称和简要说明。\n用法:help [--raw]\n`help`命令显示所有可用命令的名称和简要说明。",
|
||||
"help_message_help": "显示命令的名称和简要说明。",
|
||||
"help_command": "命令",
|
||||
"help_message": "文本",
|
||||
"help_message_not_found": "未找到文本。",
|
||||
|
||||
"": "命令:stop",
|
||||
"man_message_stop": "stop - 停止服务器。\n用法:stop",
|
||||
"help_message_stop": "停止服务器。",
|
||||
|
||||
"": "命令:exit",
|
||||
"man_message_exit": "exit - 停止服务器。\n用法:exit",
|
||||
"help_message_exit": "停止服务器。"
|
||||
}
|
||||
@@ -1,48 +1,107 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} for config.",
|
||||
"init_ok": "Initializing ready.",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization completed.",
|
||||
"start": "Server started!",
|
||||
"stop": "Goodbye!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BEAM key needed for starting the server!",
|
||||
"auth_empty_key": "Key is empty!",
|
||||
"auth_cannot_open_browser": "Cannot open browser: {}",
|
||||
"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_ok": "OK",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
||||
"GUI_enter_key_message": "Please type your key:",
|
||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
||||
"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 running on {} (Press CTRL+C to quit)",
|
||||
"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 - display the manual page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Display the manual page for COMMAND.",
|
||||
"man_for": "Manual for command",
|
||||
"man_message_not_found": "man: Manual message not found.",
|
||||
"man_command_not_found": "man: command \"{}\" not found!",
|
||||
"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 - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
||||
"help_message_help": "Display names and brief descriptions of available commands",
|
||||
"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": "Help message",
|
||||
"help_message_not_found": "No help message found",
|
||||
"help_message": "Text",
|
||||
"help_message_not_found": "No text found",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_stop": "Server shutdown.",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_exit": "Server shutdown."
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
@@ -24,6 +24,65 @@
|
||||
"": "Web phases",
|
||||
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
|
||||
|
||||
"": "Core phrases",
|
||||
"core_bind_failed": "Не получилось занять порт. Ошибка: {}",
|
||||
"core_direct_mode": "Сервер запушен в режиме прямого подключения.",
|
||||
"core_auth_server_error": "Поступил не корректный ответ от сервером авторизации BeamMP.",
|
||||
"core_auth_server_refused": "Сервер авторизации BeamMP отклонил ваш ключ. Причина: {}",
|
||||
"core_auth_server_refused_no_reason": "Сервер авторизации BeamMP не сообщил причины.",
|
||||
"core_auth_server_refused_direct_node": "Сервер всё ещё работает, но в режиме прямого подключения.",
|
||||
"core_auth_server_no_response": "Не получилось авторизовать сервер.",
|
||||
"core_mods_loaded": "Загружено {} модов. {}Мб",
|
||||
"core_identifying_connection": "Обработка нового подключения...",
|
||||
"core_player_kick_outdated": "Не подходящая версия BeamMP.",
|
||||
"core_player_kick_bad_key": "Передан не правильный ключ!",
|
||||
"core_player_kick_invalid_key": "Неверный ключ! Пожалуйста, перезапустите свою игру.",
|
||||
"core_player_kick_auth_server_fail": "Сбой сервера аутентификации! Попробуйте снова подключиться через 5 минут.",
|
||||
"core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)",
|
||||
"core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.",
|
||||
"core_player_kick_server_full": "Сервер полон.",
|
||||
"core_player_set_id": "Игрок получил ID {}",
|
||||
"core_identifying_okay": "Успешный вход.",
|
||||
|
||||
"": "In-game phrases",
|
||||
"game_welcome_message": "Добро пожаловать {}!",
|
||||
|
||||
"": "Client class phrases",
|
||||
"client_mod_request": "Запрошен мод: {}",
|
||||
"client_mod_sent": "Мод отправлен: Вес: {}мб, Скорость: {}Мб/с ({}сек)",
|
||||
"client_mod_sent_limit": " (лимит {}Мб/с)",
|
||||
"client_mod_sent_error": "Ошибка при отправке мода: {}",
|
||||
"client_sync_time": "Время синхронизации {}сек.",
|
||||
"client_kicked": "Кикнут по причине: \"{}\"",
|
||||
"client_event_invalid_data": "Из ивента вернулись не верные данные: {}",
|
||||
"client_player_disconnected": "Вышел с сервера. Время игры: {} мин",
|
||||
|
||||
"": "Events system",
|
||||
|
||||
"events_not_callable": "Невозможно добавить ивент \"{}\". Использую лучше \"{}\". Скип...",
|
||||
"events_not_found": "Ивент \"{}\" не зарегистрирован. Может {}? Скип...",
|
||||
"events_calling_error": "Ошибка во время вызова \"{}\" в функции \"{}\".",
|
||||
"events_lua_function_not_found": "Невозможно вызвать {}lua ивент - \"{}\" не найдена.",
|
||||
"events_lua_local": "локальный ",
|
||||
"events_lua_calling_error": "Ошибка: \"{}\" - во время вызова lua ивента \"{}\", функция: \"{}\" , аргументы: {}",
|
||||
|
||||
"": "Plugins loader",
|
||||
|
||||
"plugins_not_found_load": "Функция \"def load():\" не найдена.",
|
||||
"plugins_not_found_start": "Функция \"def start():\" не найдена.",
|
||||
"plugins_not_found_unload": "Функция \"def unload():\" не найдена.",
|
||||
"plugins_kt_invalid": "Переменная \"kt\" не принадлежит классу KuiToi.",
|
||||
"plugins_invalid": "Плагин: \"{}\" - не может быть запущен в KuiToi.",
|
||||
"plugins_error_loading": "Произошла ошибка при загрузке плагина {}: {}",
|
||||
|
||||
"": "Lua plugins loader",
|
||||
|
||||
"plugins_lua_enabled": "Вы включили поддержку плагинов Lua.",
|
||||
"plugins_lua_nuances_warning": "В работе с Kuiti есть некоторые нюансы. Если у вас есть предложение по их решению, и оно связано с KuiToi, пожалуйста, свяжитесь с разработчиком.",
|
||||
"plugins_lua_legacy_config_create_warning": "Для работы некоторых плагинов BeamMP требуется правильно настроенный файл ServerConfig.toml.",
|
||||
"plugins_lua_legacy_config_create": "Создаю его.",
|
||||
"plugins_lua_unload": "Останавливаю Lua плагин: {}",
|
||||
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - Показывает страничку помощи для COMMAND.\nИспользование: man COMMAND",
|
||||
"help_message_man": "Показывает страничку помощи для COMMAND.",
|
||||
|
||||
@@ -1,93 +1,109 @@
|
||||
class i18n:
|
||||
# Basic phases
|
||||
hello: str = data["hello"]
|
||||
config_path: str = data["config_path"]
|
||||
init_ok: str = data["init_ok"]
|
||||
start: str = data["start"]
|
||||
stop: str = data["stop"]
|
||||
hello: str
|
||||
config_path: str
|
||||
init_ok: str
|
||||
start: str
|
||||
stop: str
|
||||
|
||||
# Server auth
|
||||
auth_need_key: str = data["auth_need_key"]
|
||||
auth_empty_key: str = data["auth_empty_key"]
|
||||
auth_cannot_open_browser: str = data["auth_cannot_open_browser"]
|
||||
auth_use_link: str = data["auth_use_link"]
|
||||
auth_need_key: str
|
||||
auth_empty_key: str
|
||||
auth_cannot_open_browser: str
|
||||
auth_use_link: str
|
||||
|
||||
# GUI phases
|
||||
GUI_yes: str = data["GUI_yes"]
|
||||
GUI_no: str = data["GUI_no"]
|
||||
GUI_ok: str = data["GUI_ok"]
|
||||
GUI_cancel: str = data["GUI_cancel"]
|
||||
GUI_need_key_message: str = data["GUI_need_key_message"]
|
||||
GUI_enter_key_message: str = data["GUI_enter_key_message"]
|
||||
GUI_cannot_open_browser: str = data["GUI_cannot_open_browser"]
|
||||
GUI_yes: str
|
||||
GUI_no: str
|
||||
GUI_ok: str
|
||||
GUI_cancel: str
|
||||
GUI_need_key_message: str
|
||||
GUI_enter_key_message: str
|
||||
GUI_cannot_open_browser: str
|
||||
|
||||
# Web phases
|
||||
web_start: str = data["web_start"]
|
||||
web_start: str
|
||||
|
||||
# Core phrases
|
||||
|
||||
core_bind_failed: str
|
||||
core_direct_mode: str
|
||||
core_auth_server_error: str
|
||||
core_auth_server_refused: str
|
||||
core_auth_server_refused_no_reason: str
|
||||
core_auth_server_refused_direct_node: str
|
||||
core_auth_server_no_response: str
|
||||
core_mods_loaded: str
|
||||
core_identifying_connection: str
|
||||
core_player_kick_outdated: str
|
||||
core_player_kick_bad_key: str
|
||||
core_player_kick_invalid_key: str
|
||||
core_player_kick_auth_server_fail: str
|
||||
core_player_kick_stale: str
|
||||
core_player_kick_no_allowed_default_reason: str
|
||||
core_player_kick_server_full: str
|
||||
core_player_set_id: str
|
||||
core_identifying_okay: str
|
||||
|
||||
# In-game phrases
|
||||
|
||||
game_welcome_message: str
|
||||
|
||||
# Client class phrases
|
||||
|
||||
client_mod_request: str
|
||||
client_mod_sent: str
|
||||
client_mod_sent_limit: str
|
||||
client_mod_sent_error: str
|
||||
client_sync_time: str
|
||||
client_kicked: str
|
||||
client_event_invalid_data: str
|
||||
client_player_disconnected: str
|
||||
|
||||
# Events system
|
||||
|
||||
events_not_callable: str
|
||||
events_not_found: str
|
||||
events_calling_error: str
|
||||
events_lua_function_not_found: str
|
||||
events_lua_local: str
|
||||
events_lua_calling_error: str
|
||||
|
||||
# Plugins loader
|
||||
|
||||
plugins_not_found_load: str
|
||||
plugins_not_found_start: str
|
||||
plugins_not_found_unload: str
|
||||
plugins_kt_invalid: str
|
||||
plugins_invalid: str
|
||||
plugins_error_loading: str
|
||||
|
||||
# Lua plugins loader
|
||||
|
||||
plugins_lua_enabled: str
|
||||
plugins_lua_nuances_warning: str
|
||||
plugins_lua_legacy_config_create_warning: str
|
||||
plugins_lua_legacy_config_create: str
|
||||
plugins_lua_unload: str
|
||||
|
||||
# Command: man
|
||||
man_message_man: str = data["man_message_man"]
|
||||
help_message_man: str = data["help_message_man"]
|
||||
man_for: str = data["man_for"]
|
||||
man_message_not_found: str = data["man_message_not_found"]
|
||||
man_command_not_found: str = data["man_command_not_found"]
|
||||
man_message_man: str
|
||||
help_message_man: str
|
||||
man_for: str
|
||||
man_message_not_found: str
|
||||
man_command_not_found: str
|
||||
|
||||
# Command: help
|
||||
man_message_help: str = data["man_message_help"]
|
||||
help_message_help: str = data["help_message_help"]
|
||||
help_command: str = data["help_command"]
|
||||
help_message: str = data["help_message"]
|
||||
help_message_not_found: str = data["help_message_not_found"]
|
||||
man_message_help: str
|
||||
help_message_help: str
|
||||
help_command: str
|
||||
help_message: str
|
||||
help_message_not_found: str
|
||||
|
||||
# Command: stop
|
||||
man_message_stop: str = data["man_message_stop"]
|
||||
help_message_stop: str = data["help_message_stop"]
|
||||
man_message_stop: str
|
||||
help_message_stop: str
|
||||
|
||||
# Command: exit
|
||||
man_message_exit: str = data["man_message_exit"]
|
||||
help_message_exit: str = data["help_message_exit"]
|
||||
|
||||
data = {
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} for config.",
|
||||
"init_ok": "Initializing ready.",
|
||||
"start": "Server started!",
|
||||
"stop": "Goodbye!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BEAM key needed for starting the server!",
|
||||
"auth_empty_key": "Key is empty!",
|
||||
"auth_cannot_open_browser": "Cannot 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": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
||||
"GUI_enter_key_message": "Please type your key:",
|
||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
||||
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Display the manual page for COMMAND.",
|
||||
"man_for": "Manual for command",
|
||||
"man_message_not_found": "man: Manual message not found.",
|
||||
"man_command_not_found": "man: command \"{}\" not found!",
|
||||
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
||||
"help_message_help": "Display names and brief descriptions of available commands",
|
||||
"help_command": "Command",
|
||||
"help_message": "Help message",
|
||||
"help_message_not_found": "No help message found",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_stop": "Server shutdown.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_exit": "Server shutdown."
|
||||
}
|
||||
man_message_exit: str
|
||||
help_message_exit: str
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.i18n.i18n.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Version 1.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import builtins
|
||||
@@ -14,61 +14,20 @@ from core.utils import get_logger
|
||||
|
||||
|
||||
class i18n:
|
||||
data = {}
|
||||
|
||||
def __init__(self, data):
|
||||
# Basic phases
|
||||
self.hello: str = data["hello"]
|
||||
self.config_path: str = data["config_path"]
|
||||
self.init_ok: str = data["init_ok"]
|
||||
self.start: str = data["start"]
|
||||
self.stop: str = data["stop"]
|
||||
i18n.data = data
|
||||
|
||||
# Server auth
|
||||
self.auth_need_key: str = data["auth_need_key"]
|
||||
self.auth_empty_key: str = data["auth_empty_key"]
|
||||
self.auth_cannot_open_browser: str = data["auth_cannot_open_browser"]
|
||||
self.auth_use_link: str = data["auth_use_link"]
|
||||
|
||||
# GUI phases
|
||||
self.GUI_yes: str = data["GUI_yes"]
|
||||
self.GUI_no: str = data["GUI_no"]
|
||||
self.GUI_ok: str = data["GUI_ok"]
|
||||
self.GUI_cancel: str = data["GUI_cancel"]
|
||||
self.GUI_need_key_message: str = data["GUI_need_key_message"]
|
||||
self.GUI_enter_key_message: str = data["GUI_enter_key_message"]
|
||||
self.GUI_cannot_open_browser: str = data["GUI_cannot_open_browser"]
|
||||
|
||||
# Web phases
|
||||
self.web_start: str = data["web_start"]
|
||||
|
||||
# Command: man
|
||||
self.man_message_man: str = data["man_message_man"]
|
||||
self.help_message_man: str = data["help_message_man"]
|
||||
self.man_for: str = data["man_for"]
|
||||
self.man_message_not_found: str = data["man_message_not_found"]
|
||||
self.man_command_not_found: str = data["man_command_not_found"]
|
||||
|
||||
# Command: help
|
||||
self.man_message_help: str = data["man_message_help"]
|
||||
self.help_message_help: str = data["help_message_help"]
|
||||
self.help_command: str = data["help_command"]
|
||||
self.help_message: str = data["help_message"]
|
||||
self.help_message_not_found: str = data["help_message_not_found"]
|
||||
|
||||
# Command: help
|
||||
self.man_message_stop: str = data["man_message_stop"]
|
||||
self.help_message_stop: str = data["help_message_stop"]
|
||||
|
||||
# Command: exit
|
||||
self.man_message_exit: str = data["man_message_exit"]
|
||||
self.help_message_exit: str = data["help_message_exit"]
|
||||
|
||||
self.data = data
|
||||
def __getattribute__(self, key):
|
||||
return i18n.data[key]
|
||||
|
||||
|
||||
class MultiLanguage:
|
||||
|
||||
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding="utf-8"):
|
||||
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 = {}
|
||||
@@ -91,50 +50,109 @@ class MultiLanguage:
|
||||
self.__data = {
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} for config.",
|
||||
"init_ok": "Initializing ready.",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization completed.",
|
||||
"start": "Server started!",
|
||||
"stop": "Goodbye!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BEAM key needed for starting the server!",
|
||||
"auth_empty_key": "Key is empty!",
|
||||
"auth_cannot_open_browser": "Cannot open browser: {}",
|
||||
"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_ok": "OK",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
||||
"GUI_enter_key_message": "Please type your key:",
|
||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
||||
"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 running on {} (Press CTRL+C to quit)",
|
||||
"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 - display the manual page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Display the manual page for COMMAND.",
|
||||
"man_for": "Manual for command",
|
||||
"man_message_not_found": "man: Manual message not found.",
|
||||
"man_command_not_found": "man: command \"{}\" not found!",
|
||||
"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 - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
||||
"help_message_help": "Display names and brief descriptions of available commands",
|
||||
"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": "Help message",
|
||||
"help_message_not_found": "No help message found",
|
||||
"help_message": "Text",
|
||||
"help_message_not_found": "No text found",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_stop": "Server shutdown.",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_exit": "Server shutdown."
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
self.__i18n = i18n(self.__data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user