mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2026-02-16 10:30:57 +00:00
Compare commits
245 Commits
0.2.3-alph
...
Stable
| Author | SHA1 | Date | |
|---|---|---|---|
| 16d5d06881 | |||
| e43dc69b5c | |||
| 1e685e69ed | |||
| 633e235342 | |||
| cbb3fc8b29 | |||
| 613dfb741a | |||
| c2159fc523 | |||
| 180ab2421e | |||
| 25c3f503bf | |||
| d2c856fd90 | |||
| 189af0d773 | |||
| d003601b58 | |||
| 72035c226b | |||
| 8ed5671995 | |||
| b64c449065 | |||
| f2de91d0f1 | |||
| 243177ee6f | |||
| 7796e3970d | |||
| 2bf1c07041 | |||
| 2b4c0bf4d0 | |||
| 2ee74c310d | |||
| b0303f3e6d | |||
| a406956080 | |||
| 027c239424 | |||
| 9e86c41a6a | |||
| bf1c6d2c41 | |||
| 71ec0c7aed | |||
| 85475a49be | |||
| 8cbe3d07e3 | |||
| b2a608d369 | |||
| c12a91bf86 | |||
| 209004c9cb | |||
| 2af4681082 | |||
| f1f80cc94c | |||
| 06942e8a71 | |||
| 51867d526d | |||
| ff58e2a994 | |||
| 4c6a240f96 | |||
| 666a76201e | |||
| b4b3953194 | |||
| b73674f409 | |||
| 5330943d6f | |||
| c533702cdc | |||
| 3b9cea5cae | |||
| a923dbea1f | |||
| 2eb7e8801b | |||
| c4a34c6630 | |||
| 8fbd2cc330 | |||
| c838d42dd8 | |||
| 274bdead2f | |||
| 163b233306 | |||
| c6fba31c89 | |||
| 59c113ade5 | |||
| a06c48dca5 | |||
| 42e4a8e05e | |||
| 074589da47 | |||
| 50c12f4b5c | |||
| f145048cc1 | |||
| 68bf7d0d00 | |||
| e9815cdfcf | |||
| acb2b45626 | |||
| eb71fda356 | |||
| cc400d5a12 | |||
| 67b3bd26bb | |||
| 77250561fb | |||
| 70dfb9b40c | |||
| 3118c74990 | |||
| cb6adde7c2 | |||
| 422dd35a8f | |||
| 2c2bd1cb4a | |||
| 7eba3d5877 | |||
| 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 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -137,4 +137,11 @@ dmypy.json
|
||||
/src/plugins
|
||||
/test/
|
||||
*test.py
|
||||
logs/
|
||||
logs/
|
||||
*.yml
|
||||
*.toml
|
||||
|
||||
/win-ver_info.txt
|
||||
/output/
|
||||
pip-packets/
|
||||
users.db3
|
||||
|
||||
63
README.md
63
README.md
@@ -1,66 +1,23 @@
|
||||
# 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] 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] 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] Decompress data
|
||||
- [ ] Vehicle data _(Code: Os)_
|
||||
- [ ] UDP Server part:
|
||||
- [ ] Players synchronizations _(Code: Zp)_
|
||||
- [ ] Ping _(Code: p)_
|
||||
- [x] Additional:
|
||||
- [x] Logger
|
||||
- [x] Just logging
|
||||
- [x] Log in file
|
||||
- [x] Log history (.1.log, .2.log, ...)
|
||||
- [x] Console:
|
||||
- [x] Tabulation
|
||||
- [x] _~~(By design)~~_ Static text (bug)
|
||||
- [x] Events System
|
||||
- [x] Call events
|
||||
- [x] Create custom events
|
||||
- [x] Return from events
|
||||
- [x] Async support
|
||||
- [x] Plugins support
|
||||
- [ ] KuiToi class
|
||||
- [x] Load Python plugins
|
||||
- [x] Async support
|
||||
- [ ] Load Lua plugins (Original BeamMP compatibility)
|
||||
- [x] MultiLanguage (i18n support)
|
||||
- [x] Core
|
||||
- [x] Console
|
||||
- [x] WebAPI
|
||||
- [x] HTTP API Server (fastapi)
|
||||
- [x] Stop and Start with core
|
||||
- [x] Configure FastAPI logger
|
||||
- [ ] Sync with event system
|
||||
- [ ] Add methods...
|
||||
- [ ] [Documentation](./docs/)
|
||||
[TODO.md](./TODO.md)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install **Python 3.10**
|
||||
1. Install **Python 3.12**+
|
||||
2. Clone the repository in a location of your choice with: `git clone -b Stable https://github.com/kuitoi/kuitoi-Server.git`.
|
||||
3. Change directory into the KuiToi-Server: `cd KuiToi-Server`.
|
||||
4. Install requirements: `pip install -r requirements.txt`.
|
||||
|
||||
72
TODO.md
Normal file
72
TODO.md
Normal file
@@ -0,0 +1,72 @@
|
||||
## TODOs
|
||||
|
||||
- [x] Server core:
|
||||
- [x] BeamMP System:
|
||||
- [x] Private access (Without key, Direct connect)
|
||||
- [x] Public access (With key, listing in Launcher)
|
||||
- [X] Player authentication
|
||||
- [x] TCP Server part:
|
||||
- [x] Handle code
|
||||
- [x] Understanding BeamMP header
|
||||
- [x] Upload mods
|
||||
- [x] Connecting to the world
|
||||
- [x] Chat
|
||||
- [x] Players online counter
|
||||
- [x] Packets handled (Recursive finding second packet)
|
||||
- [x] Client events
|
||||
- [x] Car synchronizations:
|
||||
- [x] State packets
|
||||
- [x] Spawn cars
|
||||
- [x] Delete cars
|
||||
- [x] Edit cars
|
||||
- [x] Reset cars
|
||||
- [x] "ABG": (compressed data)
|
||||
- [x] Decompress data
|
||||
- [x] Compress data
|
||||
- [x] UDP Server part:
|
||||
- [x] Ping
|
||||
- [x] Position synchronizations
|
||||
- [x] Additional:
|
||||
- [x] Logger:
|
||||
- [x] Just logging
|
||||
- [x] Log in file
|
||||
- [x] Log history (.1.log, .2.log, ...)
|
||||
- [x] Console:
|
||||
- [x] Tabulation
|
||||
- [x] History
|
||||
- [x] Autocomplete
|
||||
- [x] Events System:
|
||||
- [x] Call events
|
||||
- [x] Create custom events
|
||||
- [x] Return from events
|
||||
- [x] Async support
|
||||
- [x] Add all events
|
||||
- [x] MultiLanguage: (i18n support)
|
||||
- [x] Core
|
||||
- [x] Console
|
||||
- [x] Plugins supports:
|
||||
- [x] Python part:
|
||||
- [x] Load Python plugins
|
||||
- [x] Async support
|
||||
- [x] KuiToi class
|
||||
- [x] Client (Player) class
|
||||
- [x] Lua part: (Original BeamMP compatibility)
|
||||
- [x] Load Lua plugins
|
||||
- [x] MP Class
|
||||
- [x] Util class
|
||||
- [x] FS class
|
||||
- [ ] HTTP API Server: (fastapi)
|
||||
- [x] Stop and Start with core
|
||||
- [x] Configure FastAPI logger
|
||||
- [ ] Sync with event system
|
||||
- [ ] Add methods...
|
||||
- [ ] RCON System:
|
||||
- [x] Serving
|
||||
- [ ] Handle commands
|
||||
- [x] Client
|
||||
- [x] AES encryption
|
||||
- [ ] KuiToi System
|
||||
- [ ] Servers counter
|
||||
- [ ] Players counter
|
||||
- [ ] Etc.
|
||||
- [ ] [Documentation](./docs)
|
||||
45
docs/cn/multilanguage/example.json
Normal file
45
docs/cn/multilanguage/example.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"": "基础阶段",
|
||||
"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请使用此链接:{}",
|
||||
|
||||
"": "命令: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/translates/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_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_
|
||||
删除玩家的车辆
|
||||
13
docs/cn/plugins/events_list.md
Normal file
13
docs/cn/plugins/events_list.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 所有可用事件列表
|
||||
|
||||
大多数事件将包含`pl = data ['kwargs'] ['player']`,可以在[这里](./classes.md)找到描述。
|
||||
|
||||
* onPlayerJoin
|
||||
* onPlayerDisconnect
|
||||
* playerLoaded
|
||||
* onChatReceive
|
||||
* onCarSpawn
|
||||
* onCarDelete
|
||||
* onCarEdited
|
||||
* onCarChanged
|
||||
* ...
|
||||
@@ -21,7 +21,7 @@ async def load():
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
ev.register("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def load():
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
ev.register("my_event", my_event_handler)
|
||||
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("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("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("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("my_event", my_event_function)`(在register中检查函数)
|
||||
* 调用async事件:`kt.call_async_event("my_event")`
|
||||
* 使用数据调用async事件:`kt.call_async_event("my_event", data, data2=data2)`
|
||||
* 基本的async事件:_稍后会写_
|
||||
9
docs/cn/readme.md
Normal file
9
docs/cn/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# KuiToi服务器文档
|
||||
|
||||
### 文档尚未完善,但终有一天会完善
|
||||
|
||||
1. 服务器设置和启动 - [这里](./setup)
|
||||
2. 插件和事件系统 - [这里](./plugins)
|
||||
3. Lua的细微差别 - [这里](./plugins/lua)
|
||||
4. 多语言支持 - [这里](./multilanguage)
|
||||
5. 将会有新的内容...
|
||||
80
docs/cn/setup/readme.md
Normal file
80
docs/cn/setup/readme.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 来自 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
|
||||
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
|
||||
```
|
||||
### Auth
|
||||
|
||||
* 如果您将 `private: false` 并且不设置 `key`,服务器将请求一个 BeamMP 密钥,没有它无法启动。
|
||||
* 输入 BeamMP 密钥后,服务器将出现在启动器列表中。
|
||||
* 您可以在此处获取密钥:[https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
|
||||
|
||||
### Game
|
||||
|
||||
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
|
||||
* `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` - 服务器将在哪个端口上工作
|
||||
@@ -1,4 +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).\
|
||||
In [example.json](./example.json) you will find a copy of [src/modules/i18n/files/ru.json](../../../src/translates/ru.json).\
|
||||
If you want to translate to a language that has not been translated before or update an existing translation, I would be happy to receive your pull requests.
|
||||
|
||||
174
docs/en/plugins/classes.md
Normal file
174
docs/en/plugins/classes.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 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_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.
|
||||
13
docs/en/plugins/events_list.md
Normal file
13
docs/en/plugins/events_list.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# List of available events
|
||||
|
||||
Most events will receive `pl = data['kwargs']['player']`, you can find a description [here](./classes.md)
|
||||
|
||||
* onPlayerJoin
|
||||
* onPlayerDisconnect
|
||||
* playerLoaded
|
||||
* onChatReceive
|
||||
* onCarSpawn
|
||||
* onCarDelete
|
||||
* onCarEdited
|
||||
* onCarChanged
|
||||
* ...
|
||||
@@ -21,7 +21,7 @@ async def load():
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
ev.register("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def load():
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
ev.register("my_event", my_event_handler)
|
||||
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,12 +1,18 @@
|
||||
# Plugin System
|
||||
Это описание системы плагинов для KuiToi сервера на Python:
|
||||
|
||||
## Installing the Library with "Stubs"
|
||||
###### (This means that it will not work without a server, but the IDE will suggest the API)
|
||||
###### (The library is still under development)
|
||||
## Events
|
||||
### Events list: [here](./events_list.md)
|
||||
|
||||
## 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 code:\
|
||||
* From source:\
|
||||
`git clone https://github.com/KuiToi/KuiToi-PyLib`
|
||||
|
||||
## Example
|
||||
@@ -17,7 +23,7 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
kt = KuiToi("ExamplePlugin")
|
||||
log = kt.log
|
||||
|
||||
def my_event_handler(event_data):
|
||||
@@ -25,14 +31,14 @@ def my_event_handler(event_data):
|
||||
|
||||
def load():
|
||||
# Plugin initialization
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
kt.register("my_event", my_event_handler)
|
||||
log.info("Plugin loaded successfully.")
|
||||
|
||||
|
||||
def start():
|
||||
# Running plugin processes
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
# Starting plugin processes
|
||||
kt.call_event("my_event")
|
||||
kt.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Plugin started successfully.")
|
||||
|
||||
|
||||
@@ -41,15 +47,17 @@ def unload():
|
||||
log.info("Plugin unloaded successfully.")
|
||||
```
|
||||
|
||||
* It is recommended to use `open()` after `load()`. Otherwise, use `kt.load()` - creates a file in the `plugin/<plugin_name>/<filename>` folder.
|
||||
* Creating your own event: `kt.register_event("my_event", my_event_function)`
|
||||
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("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)`
|
||||
* Basic events: _Will write later_
|
||||
* Base events: _To be added later_
|
||||
|
||||
## Async Functions
|
||||
## Async functions
|
||||
|
||||
Async support is available.
|
||||
Async support is available
|
||||
|
||||
```python
|
||||
try:
|
||||
@@ -64,17 +72,17 @@ log = kt.log
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
|
||||
async def load():
|
||||
# Plugin initialization
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
kt.register("my_event", my_event_handler)
|
||||
log.info("Plugin loaded successfully.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Running plugin processes
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "Some data", data="some data too")
|
||||
# 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.")
|
||||
|
||||
|
||||
@@ -84,9 +92,9 @@ async def unload():
|
||||
|
||||
```
|
||||
|
||||
A more extensive example can also be found in [async_example.py](./async_example.py).
|
||||
A more comprehensive example can also be found in [async_example.py](examples/async_example.py)
|
||||
|
||||
* Creating your own event: `kt.register_event("my_event", my_event_function)` (register_event checks for function)
|
||||
* Creating your own event: `kt.register("my_event", my_event_function)` (register has a function check)
|
||||
* Calling an async event: `kt.call_async_event("my_event")`
|
||||
* Calling an async event with data: `kt.call_async_event("my_event", data, data2=data2)`
|
||||
* Basic async events: _Will write later_
|
||||
* Base async events: _To be added later_
|
||||
@@ -1,9 +1,10 @@
|
||||
# Documentation for KuiToi Server
|
||||
|
||||
### The documentation is not perfect yet, but it will be one day
|
||||
|
||||
1. Setup and Start server - [here](setup)
|
||||
2. Plugins and Events system - [here](plugins)
|
||||
3. MultiLanguage - [here](./multilanguage)
|
||||
4. KuiToi WebAPI - [here](./web)
|
||||
5. Something new...
|
||||
### The documentation is not yet perfect, but someday it will be.
|
||||
|
||||
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,31 +1,31 @@
|
||||
# Greetings from KuiToi Server
|
||||
|
||||
## Well, let's begin
|
||||
## Well, let's start
|
||||
|
||||
###### _(Here are the commands for Linux)_
|
||||
|
||||
* **Python 3.10.x** is required to run the server! It won't work on Python 3.11...
|
||||
* You can check the version of your Python installation with the following command:
|
||||
* **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
|
||||
```
|
||||
* Clone the repository and navigate to it.
|
||||
* Install everything that's needed.
|
||||
* Then, using my "script", remove all unnecessary files and move to the core source code.
|
||||
* 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 to view information about the server and start it:
|
||||
* Here's how you can check server info and start it:
|
||||
```bash
|
||||
python3 main.py --help # Displays all available commands
|
||||
python3 main.py --help # Shows all available commands
|
||||
python3 main.py # Starts the server
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
* After starting the server, a `kuitoi.yaml` file will be created.
|
||||
* After starting, `kuitoi.yaml` will be created
|
||||
* By default, it looks like this:
|
||||
```yaml
|
||||
!!python/object:modules.ConfigProvider.config_provider.Config
|
||||
@@ -34,42 +34,55 @@ Auth:
|
||||
private: true
|
||||
Game:
|
||||
map: gridmap_v2
|
||||
max_cars: 1
|
||||
cars: 1
|
||||
players: 8
|
||||
Server:
|
||||
Options:
|
||||
debug: false
|
||||
description: Welcome to KuiToi Server!
|
||||
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: 30813
|
||||
server_port: 30814
|
||||
WebAPI:
|
||||
enabled: false
|
||||
secret_key: <random_key>
|
||||
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
|
||||
server_ip: 127.0.0.1
|
||||
server_port: 8433
|
||||
|
||||
```
|
||||
### Auth
|
||||
|
||||
* If you set `private: false` and do not set a `key`, the server will request a BeamMP key and will not start without it.
|
||||
* By entering a BeamMP key, the server will appear in the launcher list.
|
||||
* You can get a key here: [https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
|
||||
* 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` specifies only the name of the map. That is, open the mod with the map in `map.zip/levels` - the name of the map will be there, and that's what you need to insert.
|
||||
* `max_cars` - the maximum number of cars per player
|
||||
* `players` - the maximum number of players
|
||||
* `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.
|
||||
* `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
|
||||
|
||||
* `debug` - should debug messages be displayed (for experienced users only; slightly affects performance)
|
||||
* `description` - server description for the BeamMP launcher
|
||||
* `language` - the language in which the server will run (currently available: en, ru)
|
||||
* `name` - server name for the BeamMP launcher
|
||||
* `server_ip` - the IP address to be used by the server (for experienced users only; defaults to 0.0.0.0)
|
||||
* `server_port` - the port on which the server will run
|
||||
* `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 yet_
|
||||
##### _Docs are not ready_
|
||||
@@ -1,15 +0,0 @@
|
||||
Here's the translation of the readme.txt content:
|
||||
|
||||
# WebAPI for the server
|
||||
|
||||
## Available endpoints
|
||||
|
||||
* `/stop`:
|
||||
* Required parameters:
|
||||
* `secret_key` - The key specified in the server configuration
|
||||
|
||||
|
||||
* `/event.get`
|
||||
* The endpoint is not yet ready
|
||||
* Required parameters:
|
||||
* `secret_key` - The key specified in the server configuration
|
||||
@@ -1,4 +1,5 @@
|
||||
# Choose your language
|
||||
|
||||
### [English](./en)
|
||||
### [Русский](./ru)
|
||||
### [English](./en) (AI translation)
|
||||
### [Chinese](./cn) (AI translation)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# MultiLanguage - Поддержка i18n
|
||||
|
||||
В [example.json](./example.json) это копия [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json)\
|
||||
В [example.json](./example.json) это копия [src/modules/i18n/files/ru.json](../../../src/translates/ru.json)\
|
||||
Если есть желание перевести на не переведённый ранее язык, или обновить уже существующий перевод буду рад вашим пул реквестам.
|
||||
|
||||
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_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` -> Идентификатор машины_
|
||||
Удалят машину у игрока
|
||||
13
docs/ru/plugins/events_list.md
Normal file
13
docs/ru/plugins/events_list.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Список ивентов
|
||||
|
||||
В большинство ивентов будет приходить `pl = data['kwargs']['player']`, описание можно найти [тут](./classes.md)
|
||||
|
||||
* onPlayerJoin
|
||||
* onPlayerDisconnect
|
||||
* playerLoaded
|
||||
* onChatReceive
|
||||
* onCarSpawn
|
||||
* onCarDelete
|
||||
* onCarEdited
|
||||
* onCarChanged
|
||||
* ...
|
||||
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("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("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)
|
||||
###### (Библиотека ещё в разработке)
|
||||
@@ -17,7 +20,7 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
kt = KuiToi("ExamplePlugin")
|
||||
log = kt.log
|
||||
|
||||
def my_event_handler(event_data):
|
||||
@@ -25,7 +28,7 @@ def my_event_handler(event_data):
|
||||
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
ev.register("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
@@ -40,9 +43,10 @@ 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.register("my_event", my_event_function)` -
|
||||
* Вызов ивента: `kt.call_event("my_event")`
|
||||
* Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)`
|
||||
* Базовые ивенты: _Позже напишу_
|
||||
@@ -67,7 +71,7 @@ async def my_event_handler(event_data):
|
||||
|
||||
async def load():
|
||||
# Инициализация плагина
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
ev.register("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
@@ -84,9 +88,9 @@ async def unload():
|
||||
|
||||
```
|
||||
|
||||
Так же более обширный пример можно найти в [async_example.py](./async_example.py)
|
||||
Так же более обширный пример можно найти в [async_example.py](examples/async_example.py)
|
||||
|
||||
* Создание своего ивента: `kt.register_event("my_event", my_event_function)` (в register_event стоит проверка на функцию)
|
||||
* Создание своего ивента: `kt.register("my_event", my_event_function)` (в register стоит проверка на функцию)
|
||||
* Вызов async ивента: `kt.call_async_event("my_event")`
|
||||
* Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)`
|
||||
* Базовые async ивенты: _Позже напишу_
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
1. Настройка и Запуск сервера - [тута](./setup)
|
||||
2. Плагины и Ивент система - [тута](./plugins)
|
||||
3. Мультиязычность - [тута](./multilanguage)
|
||||
4. KuiToi WebAPI - [тута](./web)
|
||||
5. Тут будет что-то ново....
|
||||
3. Ньансы работы Lua - [тута](./plugins/lua)
|
||||
4. Мультиязычность - [тута](./multilanguage)
|
||||
5. KuiToi WebAPI - [тута](./web)
|
||||
6. Тут будет что-то новое....
|
||||
|
||||
@@ -34,21 +34,26 @@ Auth:
|
||||
private: true
|
||||
Game:
|
||||
map: gridmap_v2
|
||||
max_cars: 1
|
||||
cars: 1
|
||||
players: 8
|
||||
Server:
|
||||
Options:
|
||||
debug: false
|
||||
description: Welcome to KuiToi Server!
|
||||
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: 30813
|
||||
server_port: 30814
|
||||
WebAPI:
|
||||
enabled: false
|
||||
secret_key: <random_key>
|
||||
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
|
||||
server_ip: 127.0.0.1
|
||||
server_port: 8433
|
||||
|
||||
```
|
||||
### Auth
|
||||
|
||||
@@ -59,14 +64,22 @@ WebAPI:
|
||||
### Game
|
||||
|
||||
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
|
||||
* `max_cars` - Максимальное количество машин на игрока
|
||||
* `cars` - Максимальное количество машин на игрока
|
||||
* `players` - Максимально количество игроков
|
||||
|
||||
### Options
|
||||
|
||||
* `debug` - Нужно ли выводить debug сообщения (только для опытных пользователей, немного теряется в производительности)
|
||||
* `encoding` - С какой кодировкой открывать файлы
|
||||
* `language` - С каким языком запустится сервер (Доступные на данный момент: en, ru)
|
||||
* `log_chat` - Нужно-ли выводить чат в консоль
|
||||
* `speed_limit` - Ограничение скорости на скачивание модов (В Мб/с)
|
||||
* `use_lua` - Включить ли поддержку lua
|
||||
* `use_queue` - Скачивать по очереди, т.е. в один момент может скачивать только 1 клиент
|
||||
|
||||
### Server
|
||||
|
||||
* `debug` - Нужно ли выводить debug сообщения (только для опытных пользователей, немного теряется в производительности)
|
||||
* `description` - Описания сервера для лаунчера BeamMP
|
||||
* `language` - С каким языком запустится сервер (Доступные на данный момент: en, ru)
|
||||
* `name` - Названия сервер для лаунчера BeamMP
|
||||
* `server_ip` - Какой IP адрес занять серверу (только для опытных пользователей, по умолчанию 0.0.0.0)
|
||||
* `server_port` - На каком порту будет работать сервер
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# WebAPI для сервера
|
||||
|
||||
## Доступные endpoints
|
||||
|
||||
* `/stop`:
|
||||
* Необходимые парамеры:
|
||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
||||
|
||||
|
||||
* `/event.get`
|
||||
* Точка не готова
|
||||
* Необходимые парамеры:
|
||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
PyYAML~=6.0
|
||||
prompt-toolkit~=3.0.38
|
||||
aiohttp~=3.8.4
|
||||
uvicorn~=0.22.0
|
||||
fastapi~=0.100.0
|
||||
starlette~=0.27.0
|
||||
pydantic~=2.0.2
|
||||
click~=8.1.4
|
||||
aiohttp~=3.9.5
|
||||
lupa~=2.0
|
||||
toml~=0.10.2
|
||||
colorama~=0.4.6
|
||||
cryptography~=42.0.4
|
||||
prompt_toolkit~=3.0.47
|
||||
requests~=2.32.3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.3
|
||||
# Core version: 0.4.5
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock, Queue
|
||||
from logging import Logger
|
||||
from typing import Tuple
|
||||
from typing import Tuple, List, Dict, Optional, Union, Any
|
||||
|
||||
from core import Core, utils
|
||||
|
||||
@@ -15,40 +15,100 @@ 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._down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
|
||||
self.__queue_tpc = Queue()
|
||||
self.__queue_udp = Queue()
|
||||
self._tpc_count_recv = 0
|
||||
self._udp_count_recv = 0
|
||||
self._tpc_count_total_recv = 0
|
||||
self._udp_count_total_recv = 0
|
||||
self._udp_size_total_recv = 0.1
|
||||
self._tpc_size_total_recv = 0.1
|
||||
# self._tpc_count_sent = 0
|
||||
# self._udp_count_sent = 0
|
||||
self._tpc_count_total_sent = 0
|
||||
self._udp_count_total_sent = 0
|
||||
self._udp_size_total_sent = 0.1
|
||||
self._tpc_size_total_sent = 0.1
|
||||
self.tcp_pps = 0
|
||||
self.udp_pps = 0
|
||||
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
|
||||
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
|
||||
self._log = utils.get_logger("client(id: )")
|
||||
self._addr = writer.get_extra_info("sockname")
|
||||
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self.__Core = core
|
||||
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._synced = False
|
||||
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._unicycle: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
|
||||
self._last_position = {}
|
||||
self._lock = Lock()
|
||||
|
||||
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 synced(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 = False) -> None: ...
|
||||
async def _send(self, data: bytes | str, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ...
|
||||
async def _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 _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_tcp(self, data: bytes) -> None: ...
|
||||
async def _handle_codes_udp(self, data: bytes) -> None: ...
|
||||
def _tick_pps(self, _): ...
|
||||
async def __tick_player_tcp(self, _): ...
|
||||
async def __tick_player_udp(self, _): ...
|
||||
async def _tpc_put(self, data): ...
|
||||
async def _udp_put(self, data): ...
|
||||
async def _looper(self) -> None: ...
|
||||
def _update_logger(self) -> None: ...
|
||||
async def _remove_me(self) -> None: ...
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.__init__.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.3
|
||||
# Core version: 0.2.3
|
||||
# Version 1.5
|
||||
# Core version: 0.4.8
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
|
||||
# (c) kuitoi.su 2024
|
||||
|
||||
__title__ = 'KuiToi-Server'
|
||||
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
|
||||
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
|
||||
__version__ = '0.2.3'
|
||||
__build__ = 1208 # Я это считаю лог файлами
|
||||
__version__ = '0.4.8'
|
||||
__build__ = 2679 # Я это считаю лог файлами
|
||||
__author__ = 'SantaSpeen'
|
||||
__author_email__ = 'admin@kuitoi.su'
|
||||
__author_email__ = 'admin@anidev.ru'
|
||||
__license__ = "FPA"
|
||||
__copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
|
||||
__copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)'
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
import prompt_toolkit.shortcuts as shortcuts
|
||||
@@ -30,13 +30,15 @@ from modules import ConfigProvider, EventsSystem
|
||||
from modules import Console
|
||||
from modules import MultiLanguage
|
||||
|
||||
args = parser.parse_args()
|
||||
args, _ = parser.parse_known_args()
|
||||
if args.version:
|
||||
print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}")
|
||||
exit(0)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
if sys.platform == 'win32':
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
log = get_logger("core.init")
|
||||
|
||||
# Config file init
|
||||
@@ -44,24 +46,24 @@ config_path = "kuitoi.yml"
|
||||
if args.config:
|
||||
config_path = args.config
|
||||
config_provider = ConfigProvider(config_path)
|
||||
config = config_provider.open_config()
|
||||
config = config_provider.read()
|
||||
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}")
|
||||
|
||||
log.info("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...")
|
||||
ev = EventsSystem()
|
||||
ev.builtins_hook()
|
||||
ev.register("get_version", lambda _: {"version": __version__, "build": __build__})
|
||||
|
||||
log.info(i18n.hello)
|
||||
log.info(i18n.config_path.format(config_path))
|
||||
@@ -70,7 +72,7 @@ log.debug("Initializing BeamMP Server system...")
|
||||
# Key handler..
|
||||
if not config.Auth['private'] and not config.Auth['key']:
|
||||
log.warn(i18n.auth_need_key)
|
||||
url = "https://beammp.com/k/keys"
|
||||
url = "https://keymaster.beammp.com/login"
|
||||
if shortcuts.yes_no_dialog(
|
||||
title='BeamMP Server Key',
|
||||
text=i18n.GUI_need_key_message,
|
||||
@@ -92,7 +94,7 @@ if not config.Auth['private'] and not config.Auth['key']:
|
||||
text=i18n.GUI_enter_key_message,
|
||||
ok_text=i18n.GUI_ok,
|
||||
cancel_text=i18n.GUI_cancel).run()
|
||||
config_provider.save_config()
|
||||
config_provider.save()
|
||||
if not config.Auth['private'] and not config.Auth['key']:
|
||||
log.error(i18n.auth_empty_key)
|
||||
log.info(i18n.stop)
|
||||
@@ -102,7 +104,7 @@ 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)
|
||||
|
||||
|
||||
440
src/core/core.py
440
src/core/core.py
@@ -1,30 +1,44 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.core.py
|
||||
# Written by: SantaSpeen
|
||||
# Version: 0.2.3
|
||||
# Version: 0.4.8
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from threading import Thread
|
||||
import statistics
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
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
|
||||
from modules import PluginsLoader, PermsSystem
|
||||
|
||||
|
||||
def calc_ticks(ticks, duration):
|
||||
while ticks and ticks[0] < time.monotonic() - duration:
|
||||
ticks.popleft()
|
||||
return len(ticks) / duration
|
||||
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class Core:
|
||||
|
||||
def __init__(self):
|
||||
self.tick_counter = 0
|
||||
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 = []
|
||||
@@ -36,19 +50,29 @@ class Core:
|
||||
self.server_port = config.Server["server_port"]
|
||||
self.tcp = TCPServer
|
||||
self.udp = UDPServer
|
||||
self.web_thread = None
|
||||
self.web_pool = webapp.data_pool
|
||||
self.web_stop = None
|
||||
|
||||
self.tcp_pps = 0
|
||||
self.udp_pps = 0
|
||||
|
||||
self.tps = 60
|
||||
self.target_tps = 60
|
||||
|
||||
self.lock_upload = False
|
||||
|
||||
self.client_major_version = "2.0"
|
||||
self.BeamMP_version = "3.2.0"
|
||||
self.BeamMP_version = "3.4.1" # 16.07.2024
|
||||
|
||||
ev.register_event("get_player", self.get_client)
|
||||
ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
|
||||
ev.register("_get_player", lambda x: self.get_client(**x['kwargs']))
|
||||
|
||||
def get_client(self, cid=None, nick=None, from_ev=None):
|
||||
if from_ev is not None:
|
||||
return self.get_client(*from_ev['args'], **from_ev['kwargs'])
|
||||
def get_client(self, cid=None, nick=None, raw=False):
|
||||
if raw:
|
||||
return self.clients_by_nick
|
||||
if (cid, nick) == (None, None):
|
||||
return None
|
||||
if cid is not None:
|
||||
if cid == -1:
|
||||
return [i for i in self.clients if i is not None and i.synced]
|
||||
return self.clients_by_id.get(cid)
|
||||
if nick:
|
||||
return self.clients_by_nick.get(nick)
|
||||
@@ -85,7 +109,7 @@ class Core:
|
||||
for client in self.clients:
|
||||
if not client:
|
||||
continue
|
||||
out += f"{client._nick}"
|
||||
out += f"{client.nick}"
|
||||
if need_cid:
|
||||
out += f":{client.cid}"
|
||||
out += ","
|
||||
@@ -93,64 +117,89 @@ class Core:
|
||||
out = out[:-1]
|
||||
return out
|
||||
|
||||
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()}"
|
||||
async def _check_alive(self, _):
|
||||
# self.log.debug("alive checker.")
|
||||
try:
|
||||
for client in self.clients:
|
||||
if not client:
|
||||
continue
|
||||
if not client.ready:
|
||||
client.is_disconnected()
|
||||
continue
|
||||
await client._send(bytes(ca, "utf-8"))
|
||||
if not client.alive:
|
||||
await client.kick("You are not alive!")
|
||||
except Exception as e:
|
||||
self.log.error("Error in _check_alive.")
|
||||
self.log.exception(e)
|
||||
|
||||
@staticmethod
|
||||
def start_web():
|
||||
uvconfig = uvicorn.Config("modules.WebAPISystem.app:web_app",
|
||||
host=config.WebAPI["server_ip"],
|
||||
port=config.WebAPI["server_port"],
|
||||
loop="asyncio")
|
||||
uvserver = uvicorn.Server(uvconfig)
|
||||
webapp.uvserver = uvserver
|
||||
uvserver.run()
|
||||
async def _send_online(self, _):
|
||||
try:
|
||||
for client in self.clients:
|
||||
ca = f"Ss{len(self.clients_by_id)}/{config.Game['players']}:{self.get_clients_list()}"
|
||||
if not client or not client.alive:
|
||||
continue
|
||||
await client._send(ca)
|
||||
except Exception as e:
|
||||
self.log.error("Error in _send_online.")
|
||||
self.log.exception(e)
|
||||
|
||||
async def stop_me(self):
|
||||
while webapp.data_run[0]:
|
||||
await asyncio.sleep(1)
|
||||
self.run = False
|
||||
raise KeyboardInterrupt
|
||||
async def __gracefully_kick(self):
|
||||
for client in self.clients:
|
||||
if not client:
|
||||
continue
|
||||
await client.kick("Server shutdown!")
|
||||
|
||||
async def __gracefully_remove(self):
|
||||
for client in self.clients:
|
||||
if not client:
|
||||
continue
|
||||
await client._remove_me()
|
||||
|
||||
# noinspection SpellCheckingInspection,PyPep8Naming
|
||||
async def heartbeat(self, test=False):
|
||||
if config.Auth["private"] or self.direct:
|
||||
if test:
|
||||
# TODO: i18n
|
||||
self.log.info(f"Server runnig in Direct connect mode.")
|
||||
self.direct = True
|
||||
return
|
||||
try:
|
||||
self.log.debug("Starting heartbeat.")
|
||||
if config.Auth["private"] or self.direct:
|
||||
if test:
|
||||
self.log.info(i18n.core_direct_mode)
|
||||
self.direct = True
|
||||
return
|
||||
|
||||
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
|
||||
modlist = ""
|
||||
for mod in self.mods_list:
|
||||
if type(mod) == int:
|
||||
continue
|
||||
modlist += f"/{os.path.basename(mod['path'])};"
|
||||
modstotalsize = self.mods_list[0]
|
||||
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,
|
||||
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
|
||||
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
|
||||
self.log.debug(f"Auth: data {data}")
|
||||
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
|
||||
_map = config.Game['map'] if "/" in config.Game['map'] else f"/levels/{config.Game['map']}/info.json"
|
||||
tags = config.Server['tags'].replace(", ", ";").replace(",", ";")
|
||||
self.log.debug(f"[heartbeat] {_map=}")
|
||||
self.log.debug(f"[heartbeat] {tags=}")
|
||||
if tags and tags[-1:] != ";":
|
||||
tags += ";"
|
||||
modlist = "".join(f"/{os.path.basename(mod['path'])};" for mod in self.mods_list[1:])
|
||||
modstotalsize = self.mods_list[0]
|
||||
modstotal = len(self.mods_list) - 1
|
||||
self.log.debug(f"[heartbeat] {modlist=}")
|
||||
self.log.debug(f"[heartbeat] {modstotalsize=}")
|
||||
self.log.debug(f"[heartbeat] {modstotal=}")
|
||||
while self.run:
|
||||
playerslist = "".join(f"{client.nick};" for client in self.clients if client and client.alive)
|
||||
data = {
|
||||
"uuid": config.Auth["key"],
|
||||
"players": len(self.clients_by_id),
|
||||
"maxplayers": config.Game["players"],
|
||||
"port": config.Server["server_port"],
|
||||
"map": _map,
|
||||
"private": config.Auth['private'],
|
||||
"version": self.BeamMP_version,
|
||||
"clientversion": self.client_major_version,
|
||||
"name": config.Server["name"],
|
||||
"tags": tags,
|
||||
"guests": not config.Auth["private"],
|
||||
"modlist": modlist,
|
||||
"modstotalsize": modstotalsize,
|
||||
"modstotal": modstotal,
|
||||
"playerslist": playerslist,
|
||||
"desc": config.Server['description'],
|
||||
"pass": False
|
||||
}
|
||||
|
||||
# Sentry?
|
||||
ok = False
|
||||
body = {}
|
||||
for server_url in BEAM_backend:
|
||||
url = "https://" + server_url + "/heartbeat"
|
||||
@@ -158,77 +207,204 @@ class Core:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, data=data, headers={"api-v": "2"}) as response:
|
||||
code = response.status
|
||||
# text = await response.text()
|
||||
# self.log.debug(f"[HB] res={text}")
|
||||
body = await response.json()
|
||||
self.log.debug(f"Auth: code {code}, body {body}")
|
||||
ok = True
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
|
||||
continue
|
||||
|
||||
if ok:
|
||||
if body:
|
||||
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":
|
||||
# TODO: i18n
|
||||
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:
|
||||
# TODO: i18n
|
||||
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
|
||||
return bool(body)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
except Exception as e:
|
||||
self.log.error(f"Error in heartbeat: {e}")
|
||||
await asyncio.sleep(15)
|
||||
except Exception as e:
|
||||
self.log.error(f"Error in heartbeat: {e}")
|
||||
|
||||
async def _cmd_kick(self, args):
|
||||
if not len(args) > 0:
|
||||
return "Usage: 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 _useful_ticks(self, _):
|
||||
tasks = []
|
||||
self.tick_counter += 1
|
||||
events = {
|
||||
0.5: "serverTick_0.5s",
|
||||
1: "serverTick_1s",
|
||||
2: "serverTick_2s",
|
||||
3: "serverTick_3s",
|
||||
4: "serverTick_4s",
|
||||
5: "serverTick_5s",
|
||||
10: "serverTick_10s",
|
||||
30: "serverTick_30s",
|
||||
60: "serverTick_60s"
|
||||
}
|
||||
for interval in sorted(events.keys(), reverse=True):
|
||||
if self.tick_counter % (interval * self.target_tps) == 0:
|
||||
ev.call_event(events[interval])
|
||||
tasks.append(ev.call_async_event(events[interval]))
|
||||
await asyncio.gather(*tasks)
|
||||
if self.tick_counter == (60 * self.target_tps):
|
||||
self.tick_counter = 0
|
||||
|
||||
def _get_color_tps(self, ticks, d):
|
||||
tps = calc_ticks(ticks, d)
|
||||
half = self.target_tps // 2
|
||||
qw = self.target_tps // 4
|
||||
if tps > half + qw:
|
||||
return f"<green><b>{tps:.2f}</b></green>"
|
||||
elif tps > half:
|
||||
return f"<yellow><b>{tps:.2f}</b></yellow>"
|
||||
elif half > tps:
|
||||
return f"<red><b>{tps:.2f}</b></red>"
|
||||
|
||||
def _cmd_tps(self, ticks_2s, ticks_5s, ticks_30s, ticks_60s):
|
||||
t = ["-, ", "-, ", "-."]
|
||||
if len(ticks_5s) > 5 * self.target_tps:
|
||||
t[0] = f"{self._get_color_tps(ticks_5s, 5)}, "
|
||||
if len(ticks_30s) > 30 * self.target_tps:
|
||||
t[1] = f"{self._get_color_tps(ticks_30s, 30)}, "
|
||||
if len(ticks_60s) > 60 * self.target_tps:
|
||||
t[2] = f"{self._get_color_tps(ticks_60s, 60)}."
|
||||
return f"html:{self._get_color_tps(ticks_2s, 2)} TPS; For last 5s, 30s, 60s: " + "".join(t)
|
||||
|
||||
async def _tick(self):
|
||||
try:
|
||||
ticks = 0
|
||||
target_tps = self.target_tps
|
||||
last_tick_time = time.monotonic()
|
||||
ev.register("serverTick", self._useful_ticks)
|
||||
ticks_2s = deque(maxlen=2 * int(target_tps) + 1)
|
||||
ticks_5s = deque(maxlen=5 * int(target_tps) + 1)
|
||||
ticks_30s = deque(maxlen=30 * int(target_tps) + 1)
|
||||
ticks_60s = deque(maxlen=60 * int(target_tps) + 1)
|
||||
console.add_command("tps", lambda _: self._cmd_tps(ticks_2s, ticks_5s, ticks_30s, ticks_60s),
|
||||
None, "Print TPS", {"tps": None})
|
||||
_add_to_sleep = deque([0.0, 0.0, 0.0, ], maxlen=3 * int(target_tps))
|
||||
# _t0 = []
|
||||
|
||||
self.log.debug("tick system started")
|
||||
while self.run:
|
||||
target_interval = 1 / self.target_tps
|
||||
start_time = time.monotonic()
|
||||
|
||||
ev.call_event("serverTick")
|
||||
await ev.call_async_event("serverTick")
|
||||
|
||||
# Calculate the time taken for this tick
|
||||
end_time = time.monotonic()
|
||||
tick_duration = end_time - start_time
|
||||
# _t0.append(tick_duration)
|
||||
|
||||
# Calculate the time to sleep to maintain target TPS
|
||||
sleep_time = target_interval - tick_duration - statistics.fmean(_add_to_sleep)
|
||||
if sleep_time > 0:
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
# Update tick count and time
|
||||
ticks += 1
|
||||
current_time = time.monotonic()
|
||||
ticks_2s.append(current_time)
|
||||
ticks_5s.append(current_time)
|
||||
ticks_30s.append(current_time)
|
||||
ticks_60s.append(current_time)
|
||||
|
||||
# Calculate TPS
|
||||
elapsed_time = current_time - last_tick_time
|
||||
if elapsed_time >= 1:
|
||||
self.tps = ticks / elapsed_time
|
||||
# if self.tps < 5:
|
||||
# self.log.warning(f"Low TPS: {self.tps:.2f}")
|
||||
# Reset for next calculation
|
||||
# _t0s = max(_t0), min(_t0), statistics.fmean(_t0)
|
||||
# _tw = max(_add_to_sleep), min(_add_to_sleep), statistics.fmean(_add_to_sleep)
|
||||
# self.log.debug(f"[{'OK' if sleep_time > 0 else "CHECK"}] TPS: {self.tps:.2f}; Tt={_t0s}; Ts={sleep_time}; Tw={_tw}")
|
||||
# _t0 = []
|
||||
last_tick_time = current_time
|
||||
ticks = 0
|
||||
|
||||
_add_to_sleep.append(time.monotonic() - start_time - sleep_time)
|
||||
self.log.debug("tick system stopped")
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
|
||||
async def _parse_chat(self, event):
|
||||
player = event['kwargs']['player']
|
||||
message = event['kwargs']['message']
|
||||
|
||||
async def main(self):
|
||||
self.tcp = self.tcp(self, self.server_ip, self.server_port)
|
||||
self.udp = self.udp(self, self.server_ip, self.server_port)
|
||||
PermsSystem()
|
||||
console.add_command(
|
||||
"list",
|
||||
lambda x: f"Players list: {self.get_clients_list(True)}"
|
||||
)
|
||||
ev.call_event("add_perm_to_alias", "cmd.kick")
|
||||
console.add_command("kick", self._cmd_kick, "kick - Kick user\n"
|
||||
"Usage: kick NICK|:{ID} [REASON]\n"
|
||||
"Examples:\n"
|
||||
" <white>></white> <b><skyblue>kick admin bad boy</skyblue></b>\n"
|
||||
" <white>></white> <b><skyblue>kick :0 bad boy</skyblue></b>",
|
||||
"kick user", {"kick": "<playerlist>"})
|
||||
ev.register("onChatReceive", self._parse_chat)
|
||||
|
||||
self.log.debug("Initializing PluginsLoader...")
|
||||
if not os.path.exists("plugins"):
|
||||
os.mkdir("plugins")
|
||||
pl = PluginsLoader("plugins")
|
||||
pl_dir = "plugins"
|
||||
self.log.debug("Initializing PluginsLoaders...")
|
||||
if not os.path.exists(pl_dir):
|
||||
os.mkdir(pl_dir)
|
||||
pl = PluginsLoader(pl_dir)
|
||||
if config.Options['use_lua']:
|
||||
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
|
||||
lpl = LuaPluginsLoader(pl_dir)
|
||||
lpl.load()
|
||||
await pl.load()
|
||||
|
||||
try:
|
||||
# WebApi Start
|
||||
if config.WebAPI["enabled"]:
|
||||
self.log.debug("Initializing WebAPI...")
|
||||
web_thread = Thread(target=self.start_web, name="WebApiThread")
|
||||
web_thread.start()
|
||||
self.log.debug(f"WebAPI started at new thread: {web_thread.name}")
|
||||
self.web_thread = web_thread
|
||||
# noinspection PyProtectedMember
|
||||
self.web_stop = webapp._stop
|
||||
await asyncio.sleep(.3)
|
||||
|
||||
# Mods handler
|
||||
self.log.debug("Listing mods..")
|
||||
if not os.path.exists(self.mods_dir):
|
||||
@@ -242,16 +418,22 @@ class Core:
|
||||
self.log.debug(f"mods_list: {self.mods_list}")
|
||||
len_mods = len(self.mods_list) - 1
|
||||
if len_mods > 0:
|
||||
# TODO: i18n
|
||||
self.log.info(f"Loaded {len_mods} mods: {round(self.mods_list[0] / MB, 2)}mb")
|
||||
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"] * 2.3)): # * 2.3 For down sock and buffer.
|
||||
self.clients.append(None)
|
||||
await self.heartbeat(True) # Check
|
||||
|
||||
self.clients = [None] * config.Game["players"] * 4 # * 4 For down sock and buffer.
|
||||
tasks = []
|
||||
# self.udp.start,
|
||||
f_tasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
||||
ev.register("serverTick_1s", self._check_alive)
|
||||
ev.register("serverTick_1s", self._send_online)
|
||||
# ev.register("serverTick_5s", self.heartbeat)
|
||||
f_tasks = [self.tcp.start, self.udp._start, console.start, self._tick, self.heartbeat]
|
||||
if config.RCON['enabled']:
|
||||
self.log.warning("RCON not available. yet.")
|
||||
# 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)
|
||||
@@ -260,29 +442,41 @@ class Core:
|
||||
|
||||
self.run = True
|
||||
self.log.info(i18n.start)
|
||||
ev.call_event("server_started")
|
||||
await ev.call_async_event("server_started")
|
||||
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.error(f"Exception in main:")
|
||||
self.log.exception(e)
|
||||
finally:
|
||||
self.run = False
|
||||
self.tcp.stop()
|
||||
# self.udp.stop()
|
||||
await self.stop()
|
||||
|
||||
def start(self):
|
||||
asyncio.run(self.main())
|
||||
|
||||
async def stop(self):
|
||||
ev.call_event("server_stopped")
|
||||
await ev.call_async_event("server_stopped")
|
||||
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)
|
||||
ev.call_lua_event("onShutdown")
|
||||
await ev.call_async_event("onServerStopped")
|
||||
ev.call_event("onServerStopped")
|
||||
try:
|
||||
await self.__gracefully_kick()
|
||||
await self.__gracefully_remove()
|
||||
self.tcp.stop()
|
||||
self.udp._stop()
|
||||
await ev.call_async_event("_plugins_unload")
|
||||
if config.Options['use_lua']:
|
||||
await ev.call_async_event("_lua_plugins_unload")
|
||||
self.run = False
|
||||
total_time = time.monotonic() - self.start_time
|
||||
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)
|
||||
except Exception as e:
|
||||
self.log.error("Error while stopping server:")
|
||||
self.log.exception(e)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.core.pyi
|
||||
# Written by: SantaSpeen
|
||||
# Version 0.2.3
|
||||
# Core version: 0.4.5
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import time
|
||||
from threading import Thread
|
||||
from typing import Callable, List, Dict
|
||||
|
||||
@@ -16,6 +17,10 @@ from .udp_server import UDPServer
|
||||
|
||||
class Core:
|
||||
def __init__(self):
|
||||
self.target_tps = 50
|
||||
self.tick_counter = 0
|
||||
self.tps = 10
|
||||
self.start_time = time.monotonic()
|
||||
self.log = utils.get_logger("core")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.run = False
|
||||
@@ -32,17 +37,24 @@ 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"
|
||||
self.BeamMP_version = "3.4.1"
|
||||
def get_client(self, cid=None, nick=None) -> Client | None: ...
|
||||
async def insert_client(self, client: Client) -> None: ...
|
||||
def create_client(self, *args, **kwargs) -> Client: ...
|
||||
def get_clients_list(self, need_cid=False) -> str: ...
|
||||
async def check_alive(self) -> None: ...
|
||||
@staticmethod
|
||||
def start_web() -> None: ...
|
||||
def stop_me(self) -> None: ...
|
||||
async def _check_alive(self) -> None: ...
|
||||
async def _send_online(self) -> None: ...
|
||||
async def _useful_ticks(self, _) -> None: ...
|
||||
async def __gracefully_kick(self): ...
|
||||
async def __gracefully_remove(self): ...
|
||||
def _get_color_tps(self, ticks, d): ...
|
||||
async def _cmd_tps(self, ticks_2s, ticks_5s, ticks_30s, ticks_60s) -> str: ...
|
||||
def _tick(self) -> None: ...
|
||||
async def heartbeat(self, test=False) -> None: ...
|
||||
async def _cmd_kick(self, args: list) -> None | str: ...
|
||||
async def _parse_chat(self, event): ...
|
||||
async def main(self) -> None: ...
|
||||
def start(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.3
|
||||
# Core version: 0.4.8
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
@@ -10,8 +10,10 @@ import traceback
|
||||
import aiohttp
|
||||
|
||||
from core import utils
|
||||
from modules import RateLimiter
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class TCPServer:
|
||||
def __init__(self, core, host, port):
|
||||
self.log = utils.get_logger("TCPServer")
|
||||
@@ -20,28 +22,30 @@ class TCPServer:
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.run = False
|
||||
self._connections = set()
|
||||
self.server = None
|
||||
self.rl = RateLimiter(50, 10, 300)
|
||||
console.add_command("rl", self.rl.parse_console, None, "RateLimiter menu",
|
||||
{"rl": {"info": None, "unban": None, "ban": None, "help": None}})
|
||||
|
||||
async def auth_client(self, reader, writer):
|
||||
client = self.Core.create_client(reader, writer)
|
||||
# TODO: i18n
|
||||
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}":
|
||||
# TODO: i18n
|
||||
await client.kick("Outdated Version.")
|
||||
await client.kick(i18n.core_player_kick_outdated)
|
||||
return False, client
|
||||
else:
|
||||
await client._send(b"S") # Accepted client version
|
||||
await client._send(b"A") # Accepted client version
|
||||
|
||||
data = await client._recv()
|
||||
data = await client._recv(True)
|
||||
self.log.debug(f"Key: {data}")
|
||||
if len(data) > 50:
|
||||
# TODO: i18n
|
||||
await client.kick("Invalid Key (too long)!")
|
||||
if not data or len(data) > 50:
|
||||
await client.kick(i18n.core_player_kick_bad_key)
|
||||
return False, client
|
||||
client._key = data.decode("utf-8")
|
||||
ev.call_event("auth_sent_key", player=client)
|
||||
ev.call_event("onPlayerSentKey", player=client)
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
url = 'https://auth.beammp.com/pkToUser'
|
||||
@@ -49,38 +53,57 @@ class TCPServer:
|
||||
res = await response.json()
|
||||
self.log.debug(f"res: {res}")
|
||||
if res.get("error"):
|
||||
# TODO: i18n
|
||||
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.nick = res["username"]
|
||||
client.roles = res["roles"]
|
||||
self.log.debug(f"{client.roles=} {client.nick=}")
|
||||
if client.roles == "USER" and client.nick == "SantaSpeen":
|
||||
client.roles = "ADM"
|
||||
client._guest = res["guest"]
|
||||
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
|
||||
if not client._identifiers.get("ip"):
|
||||
client._identifiers["ip"] = client._addr[0]
|
||||
# noinspection PyProtectedMember
|
||||
client._update_logger()
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error(f"Auth error: {e}")
|
||||
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
|
||||
self.log.error("Auth error.")
|
||||
self.log.exception(e)
|
||||
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:
|
||||
# TODO: i18n
|
||||
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", player=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 = False
|
||||
elif isinstance(data, str):
|
||||
allow = False
|
||||
reason = data
|
||||
if not allow:
|
||||
await client.kick(reason)
|
||||
return False, client
|
||||
|
||||
ev.call_event("onPlayerAuthenticated", player=client)
|
||||
await ev.call_async_event("onPlayerAuthenticated", player=client)
|
||||
if not client.alive:
|
||||
await client.kick("Not accepted.")
|
||||
return False, client
|
||||
|
||||
if len(self.Core.clients_by_id) > config.Game["players"]:
|
||||
# TODO: i18n
|
||||
await client.kick("Server full!")
|
||||
await client.kick(i18n.core_player_kick_server_full)
|
||||
return False, client
|
||||
else:
|
||||
# TODO: i18n
|
||||
self.log.info("Identification success")
|
||||
await self.Core.insert_client(client)
|
||||
client.log.info(i18n.core_identifying_okay)
|
||||
|
||||
return True, client
|
||||
|
||||
@@ -89,11 +112,11 @@ 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()
|
||||
self.log.debug(f"Unknown client id:{cid} - HandleDownload")
|
||||
self.log.debug(f"Unknown client <nick>:{cid} - HandleDownload")
|
||||
finally:
|
||||
return
|
||||
|
||||
@@ -111,14 +134,20 @@ class TCPServer:
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
case _:
|
||||
# TODO: i18n
|
||||
self.log.error(f"Unknown code: {code}")
|
||||
self.log.warning(f"Unknown code: {code}")
|
||||
self.log.warning("Report about that!")
|
||||
writer.close()
|
||||
return False, None
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
while True:
|
||||
while self.run:
|
||||
self._connections.add(writer)
|
||||
try:
|
||||
ip = writer.get_extra_info('peername')[0]
|
||||
if self.rl.is_banned(ip):
|
||||
await self.rl.notify(ip, writer)
|
||||
writer.close()
|
||||
break
|
||||
data = await reader.read(1)
|
||||
if not data:
|
||||
break
|
||||
@@ -127,13 +156,12 @@ class TCPServer:
|
||||
# task = asyncio.create_task(self.handle_code(code, reader, writer))
|
||||
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
|
||||
_, cl = await self.handle_code(code, reader, writer)
|
||||
self.log.debug(f"cl returned: {cl}")
|
||||
if cl:
|
||||
await cl._remove_me()
|
||||
del cl
|
||||
break
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error("Error while connecting..")
|
||||
self.log.error("Error while handling connection...")
|
||||
self.log.exception(e)
|
||||
traceback.print_exc()
|
||||
break
|
||||
@@ -142,20 +170,21 @@ class TCPServer:
|
||||
self.log.debug("Starting TCP server.")
|
||||
self.run = True
|
||||
try:
|
||||
server = await asyncio.start_server(self.handle_client, self.host, self.port,
|
||||
backlog=int(config.Game["players"] * 1.3))
|
||||
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}")
|
||||
self.server = await asyncio.start_server(self.handle_client, self.host, self.port,
|
||||
backlog=int(config.Game["players"] * 4))
|
||||
self.log.debug(f"TCP server started on {self.server.sockets[0].getsockname()!r}")
|
||||
while True:
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
async with self.server:
|
||||
await self.server.serve_forever()
|
||||
except OSError as e:
|
||||
# TODO: i18n
|
||||
self.log.error("Cannot bind port")
|
||||
self.log.error(i18n.core_bind_failed.format(e))
|
||||
raise e
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except ConnectionResetError as e:
|
||||
self.log.debug(f"ConnectionResetError {e}")
|
||||
except Exception as e:
|
||||
self.log.error(f"Error: {e}")
|
||||
self.log.exception(e)
|
||||
raise e
|
||||
finally:
|
||||
self.run = False
|
||||
@@ -163,3 +192,16 @@ class TCPServer:
|
||||
|
||||
def stop(self):
|
||||
self.log.debug("Stopping TCP server")
|
||||
try:
|
||||
if not self.server:
|
||||
return
|
||||
self.server.close()
|
||||
for conn in self._connections:
|
||||
self.log.debug(f"Closing {conn}")
|
||||
try:
|
||||
conn.close()
|
||||
except ConnectionResetError:
|
||||
self.log.debug("ConnectionResetError")
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
self.log.debug("Stopped.")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.pyi
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.3
|
||||
# Core version: 0.4.8
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
@@ -10,16 +10,21 @@ from typing import Tuple
|
||||
|
||||
from core import utils, Core
|
||||
from core.Client import Client
|
||||
from modules import RateLimiter
|
||||
|
||||
|
||||
class TCPServer:
|
||||
def __init__(self, core: Core, host, port):
|
||||
self.server = await asyncio.start_server(self.handle_client, "", 0, backlog=int(config.Game["players"] * 2.3))
|
||||
self.log = utils.get_logger("TCPServer")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
self.host = host
|
||||
self.port = port
|
||||
self._connections = set()
|
||||
self.run = False
|
||||
self.rl = RateLimiter(50, 10, 15)
|
||||
|
||||
async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
|
||||
async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ...
|
||||
async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
|
||||
|
||||
@@ -1,60 +1,90 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.udp_server.py
|
||||
# File core.udp_server
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.3
|
||||
# Core version: 0.4.7
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
# (c) kuitoi.su 2024
|
||||
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
|
||||
self._core = core
|
||||
self.host = host
|
||||
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, packet, 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 = packet[0] - 1
|
||||
client = self._core.get_client(cid=cid)
|
||||
if client:
|
||||
if not client.alive:
|
||||
client.log.debug(f"Still sending UDP data: {packet}")
|
||||
if client._udp_sock != (self.transport, addr):
|
||||
client._udp_sock = (self.transport, addr)
|
||||
self.log.debug(f"Set UDP Sock for CID: {cid}")
|
||||
await client._udp_put(packet)
|
||||
else:
|
||||
self.log.debug(f"[{cid}] Client not found.")
|
||||
except Exception as e:
|
||||
self.log.error(f"Error handle_datagram: {e}")
|
||||
|
||||
def stop(self):
|
||||
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")
|
||||
if self.transport:
|
||||
self.transport.close()
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.udp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.3
|
||||
# Core version: 0.4.5
|
||||
# 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._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 _print_pps(self) -> None: ...
|
||||
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.3
|
||||
# Core version: 0.4.5
|
||||
# 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
|
||||
|
||||
@@ -1,9 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import copy
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.config_provider.__init__.py
|
||||
# File modules.ConfigProvider
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
from .config_provider import ConfigProvider, Config
|
||||
import os
|
||||
import secrets
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None):
|
||||
self.Auth = auth or {"key": None, "private": True}
|
||||
self.Game = game or {"map": "gridmap_v2", "players": 8, "cars": 1}
|
||||
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "tags": "Freroam",
|
||||
"server_ip": "0.0.0.0", "server_port": 30814}
|
||||
self.Options = options or {"language": "en", "speed_limit": 0, "use_queue": False,
|
||||
"use_lua": False, "log_chat": True}
|
||||
self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383,
|
||||
"password": secrets.token_hex(6)}
|
||||
|
||||
def __repr__(self):
|
||||
return (f"{self.__class__.__name__}(Auth={self.Auth!r}, Game={self.Game!r}, Server={self.Server!r}, "
|
||||
f"RCON={self.RCON!r}, Options={self.Options!r})")
|
||||
|
||||
|
||||
class ConfigProvider:
|
||||
|
||||
def __init__(self, config_path):
|
||||
self.config_path = config_path
|
||||
self.config = Config()
|
||||
|
||||
def read(self, _again=False):
|
||||
if not os.path.exists(self.config_path):
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(self.config, f)
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self.config = yaml.load(f.read(), yaml.Loader)
|
||||
except yaml.YAMLError:
|
||||
print("You have errors in the YAML syntax.")
|
||||
print("Stopping server.")
|
||||
exit(1)
|
||||
if not self.config:
|
||||
if _again:
|
||||
print("Error: empty configuration.")
|
||||
exit(1)
|
||||
print("Reconfig: empty configuration.")
|
||||
os.remove(self.config_path)
|
||||
self.config = Config()
|
||||
return self.read(True)
|
||||
|
||||
if not self.config.Options.get("debug"):
|
||||
self.config.Options['debug'] = False
|
||||
if not self.config.Options.get("encoding"):
|
||||
self.config.Options['encoding'] = "utf-8"
|
||||
|
||||
return self.config
|
||||
|
||||
def save(self):
|
||||
_config = copy.deepcopy(self.config)
|
||||
del _config.enc
|
||||
del _config.Options['debug']
|
||||
del _config.Options['encoding']
|
||||
os.remove(self.config_path)
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(_config, f)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
class Config:
|
||||
Auth: dict
|
||||
Game: dict
|
||||
Server: dict
|
||||
WebAPI: dict
|
||||
def __repr__(self):
|
||||
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
|
||||
class config (Config): ...
|
||||
@@ -1,50 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.config_provider.config_provider.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import os
|
||||
import secrets
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, auth=None, game=None, server=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.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
|
||||
"secret_key": secrets.token_hex(16)}
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
|
||||
|
||||
|
||||
class ConfigProvider:
|
||||
|
||||
def __init__(self, config_path):
|
||||
self.config_path = config_path
|
||||
self.config = Config()
|
||||
|
||||
def open_config(self):
|
||||
if not os.path.exists(self.config_path):
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(self.config, f)
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self.config = yaml.load(f.read(), yaml.Loader)
|
||||
except yaml.YAMLError:
|
||||
print("You have errors in the YAML syntax.")
|
||||
print("Stopping server.")
|
||||
exit(1)
|
||||
|
||||
return self.config
|
||||
|
||||
def save_config(self):
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(self.config, f)
|
||||
14
src/modules/ConfigProvider/readme.md
Normal file
14
src/modules/ConfigProvider/readme.md
Normal file
@@ -0,0 +1,14 @@
|
||||
### Builtins
|
||||
|
||||
```python
|
||||
class Config:
|
||||
Auth: Dict[str, object]
|
||||
Game: Dict[str, object]
|
||||
Server: Dict[str, object]
|
||||
RCON: Dict[str, object]
|
||||
Options: 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): ...
|
||||
```
|
||||
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,9 +1,389 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.console.__init__.py
|
||||
# File modules.ConsoleSystem
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Version 1.2
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
from .console_system import Console
|
||||
import builtins
|
||||
import inspect
|
||||
import logging
|
||||
from typing import AnyStr
|
||||
|
||||
from prompt_toolkit import PromptSession, print_formatted_text, HTML, ANSI
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.completion import Completer, WordCompleter
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.history import FileHistory
|
||||
|
||||
try:
|
||||
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
|
||||
except AssertionError:
|
||||
class NoConsoleScreenBufferError(Exception):
|
||||
...
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
|
||||
from core import get_logger
|
||||
from modules.ConsoleSystem.RCON import RCONSystem
|
||||
|
||||
|
||||
class BadCompleter(Exception): ...
|
||||
|
||||
|
||||
class MyNestedCompleter(Completer):
|
||||
def __init__(self, options, ignore_case=True, on_none=None):
|
||||
self.options = self._from_nested_dict(options)
|
||||
self.ignore_case = ignore_case
|
||||
self.on_none = on_none
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MyNestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
|
||||
|
||||
@classmethod
|
||||
def _from_nested_dict(cls, data, r=False):
|
||||
options: dict[str, Completer | None] = {}
|
||||
for key, value in data.items():
|
||||
if isinstance(value, Completer):
|
||||
options[key] = value
|
||||
elif isinstance(value, dict):
|
||||
options[key] = cls._from_nested_dict(value, True)
|
||||
elif isinstance(value, set):
|
||||
options[key] = cls._from_nested_dict({item: None for item in value}, True)
|
||||
elif isinstance(value, bool):
|
||||
if value:
|
||||
options[key] = None
|
||||
else:
|
||||
if isinstance(value, str) and value == "<playerlist>":
|
||||
options[key] = players_completer
|
||||
else:
|
||||
if value is not None:
|
||||
raise BadCompleter(f"{value!r} for key {key!r} have not valid type.")
|
||||
options[key] = None
|
||||
if r:
|
||||
return cls(options)
|
||||
return options
|
||||
|
||||
def load(self, data):
|
||||
self.options = self._from_nested_dict(data)
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
# Split document.
|
||||
text = document.text_before_cursor.lstrip()
|
||||
stripped_len = len(document.text_before_cursor) - len(text)
|
||||
|
||||
# If there is a space, check for the first term, and use a
|
||||
# subcompleter.
|
||||
if " " in text:
|
||||
first_term = text.split()[0]
|
||||
completer = self.options.get(first_term)
|
||||
if completer is None:
|
||||
completer = self.on_none
|
||||
|
||||
# If we have a sub completer, use this for the completions.
|
||||
if completer is not None:
|
||||
remaining_text = text[len(first_term):].lstrip()
|
||||
move_cursor = len(text) - len(remaining_text) + stripped_len
|
||||
|
||||
new_document = Document(
|
||||
remaining_text,
|
||||
cursor_position=document.cursor_position - move_cursor,
|
||||
)
|
||||
|
||||
yield from completer.get_completions(new_document, complete_event)
|
||||
|
||||
# No space in the input: behave exactly like `WordCompleter`.
|
||||
else:
|
||||
completer = WordCompleter(
|
||||
list(self.options.keys()), ignore_case=self.ignore_case
|
||||
)
|
||||
yield from completer.get_completions(document, complete_event)
|
||||
|
||||
def tick_players(self, _):
|
||||
clients = ev.call_event("_get_player", raw=True)[0]
|
||||
self.options = {}
|
||||
for k in clients.keys():
|
||||
self.options[k] = None
|
||||
|
||||
|
||||
players_completer = MyNestedCompleter({})
|
||||
builtins.Completer = MyNestedCompleter
|
||||
builtins.players_completer = players_completer
|
||||
|
||||
|
||||
class Console:
|
||||
|
||||
def __init__(self,
|
||||
prompt_in="> ",
|
||||
prompt_out="",
|
||||
not_found="Command \"%s\" not found in alias.",
|
||||
debug=False) -> None:
|
||||
self.__logger = get_logger("console")
|
||||
self.__run = False
|
||||
try:
|
||||
self.session = PromptSession(history=FileHistory('./.cmdhistory'))
|
||||
self.__legacy_mode = False
|
||||
except NoConsoleScreenBufferError:
|
||||
self.__legacy_mode = True
|
||||
self.__prompt_in = prompt_in
|
||||
self.__prompt_out = prompt_out
|
||||
self.__not_found = not_found
|
||||
self.__is_debug = debug
|
||||
self.__print = print
|
||||
self.__func = dict()
|
||||
self.__alias = dict()
|
||||
self.__man = dict()
|
||||
self.__desc = dict()
|
||||
self.__print_logger = get_logger("print")
|
||||
self.completer = MyNestedCompleter(self.__alias)
|
||||
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
|
||||
custom_completer={"man": {}})
|
||||
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
|
||||
custom_completer={"help": {"--raw": False}})
|
||||
rcon = RCONSystem
|
||||
rcon.console = self
|
||||
self.rcon = rcon
|
||||
|
||||
def __debug(self, *x):
|
||||
self.__logger.debug(' '.join(x))
|
||||
# if self.__is_debug:
|
||||
# x = list(x)
|
||||
# x.insert(0, "\r CONSOLE DEBUG:")
|
||||
# self.__print(*x)
|
||||
|
||||
def __getitem__(self, item):
|
||||
print(item)
|
||||
|
||||
@staticmethod
|
||||
def __get_max_len(arg) -> int:
|
||||
i = 0
|
||||
arg = list(arg)
|
||||
for a in arg:
|
||||
ln = len(str(a))
|
||||
if ln > i:
|
||||
i = ln
|
||||
return i
|
||||
|
||||
def __create_man_message(self, argv: list) -> AnyStr:
|
||||
if len(argv) == 0:
|
||||
return self.__man.get("man")
|
||||
|
||||
x = argv[0]
|
||||
if x not in self.__alias:
|
||||
return i18n.man_command_not_found.format(x)
|
||||
return self.__man.get(x)
|
||||
|
||||
# noinspection PyStringFormat
|
||||
def __create_help_message(self, argv: list) -> AnyStr:
|
||||
self.__debug("creating help message")
|
||||
raw = False
|
||||
max_len_v = 0
|
||||
if "--raw" in argv:
|
||||
max_len_v = self.__get_max_len(self.__func.values())
|
||||
print()
|
||||
raw = True
|
||||
|
||||
message = "\n"
|
||||
max_len = self.__get_max_len(self.__func.keys())
|
||||
if max_len < 7:
|
||||
max_len = 7
|
||||
|
||||
if raw:
|
||||
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
|
||||
else:
|
||||
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
|
||||
|
||||
for k, v in self.__func.items():
|
||||
doc = self.__desc.get(k)
|
||||
|
||||
if raw:
|
||||
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
|
||||
|
||||
else:
|
||||
if doc is None:
|
||||
doc = i18n.help_message_not_found
|
||||
message += f" %-{max_len}s : %s\n" % (k, doc)
|
||||
|
||||
return message
|
||||
|
||||
def del_command(self, func):
|
||||
self.__debug(f"delete command: func={func};")
|
||||
keys = []
|
||||
for k, v in self.__func.items():
|
||||
if v['f'] is func:
|
||||
keys.append(k)
|
||||
for key in keys:
|
||||
self.__debug(f"Delete: key={key}")
|
||||
self.__alias.pop(key)
|
||||
self.__alias["man"].pop(key)
|
||||
self.__func.pop(key)
|
||||
self.__man.pop(key)
|
||||
self.__desc.pop(key)
|
||||
self.__debug("Deleted.")
|
||||
self.completer.load(self.__alias)
|
||||
|
||||
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
|
||||
if not isinstance(key, str):
|
||||
raise TypeError("key must be string")
|
||||
|
||||
key = key.replace(" ", "-")
|
||||
self.__debug(f"added user command: key={key}; func={func};")
|
||||
self.__alias.update(custom_completer or {key: None})
|
||||
self.__alias["man"].update({key: None})
|
||||
self.__func.update({key: {"f": func}})
|
||||
self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man if man else "No page"}</seagreen>'})
|
||||
self.__desc.update({key: desc})
|
||||
self.completer.load(self.__alias)
|
||||
return self.__alias.copy()
|
||||
|
||||
def _write(self, text):
|
||||
# https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html#formatted-text
|
||||
if self.__legacy_mode:
|
||||
print(text)
|
||||
return
|
||||
assert isinstance(text, str)
|
||||
_type = text.split(":")[0]
|
||||
match _type:
|
||||
case "html":
|
||||
print_formatted_text(HTML(text[5:]))
|
||||
case "ansi":
|
||||
print_formatted_text(ANSI(text[5:]))
|
||||
case _:
|
||||
print_formatted_text(text)
|
||||
|
||||
def write(self, s: AnyStr):
|
||||
if isinstance(s, (list, tuple)):
|
||||
for text in s:
|
||||
self._write(text)
|
||||
else:
|
||||
self._write(s)
|
||||
|
||||
def log(self, s: AnyStr) -> None:
|
||||
# if isinstance(s, (list, tuple)):
|
||||
# for text in s:
|
||||
# self.__logger.info(f"{text}")
|
||||
# else:
|
||||
# self.__logger.info(f"{s}")
|
||||
self.write(s)
|
||||
|
||||
def __lshift__(self, s: AnyStr) -> None:
|
||||
self.write(s)
|
||||
|
||||
@property
|
||||
def alias(self) -> dict:
|
||||
return self.__alias.copy()
|
||||
|
||||
def __builtins_print(self,
|
||||
*values: object,
|
||||
sep: str or None = " ",
|
||||
end: str or None = None,
|
||||
file: str or None = None,
|
||||
flush: bool = False) -> None:
|
||||
self.__debug(f"Used __builtins_print; is_run: {self.__run}")
|
||||
val = list(values)
|
||||
if len(val) > 0:
|
||||
if self.__run:
|
||||
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
|
||||
else:
|
||||
if end is None:
|
||||
end = "\n"
|
||||
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
|
||||
|
||||
def logger_hook(self) -> None:
|
||||
self.__debug("used logger_hook")
|
||||
|
||||
def emit(cls, record):
|
||||
try:
|
||||
msg = cls.format(record)
|
||||
if cls.stream.name == "<stderr>":
|
||||
self.write(f"\r{msg}")
|
||||
else:
|
||||
cls.stream.write(msg + cls.terminator)
|
||||
cls.flush()
|
||||
except RecursionError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(e)
|
||||
cls.handleError(record)
|
||||
|
||||
logging.StreamHandler.emit = emit
|
||||
|
||||
def builtins_hook(self) -> None:
|
||||
self.__debug("used builtins_hook")
|
||||
|
||||
builtins.Console = Console
|
||||
builtins.console = self
|
||||
|
||||
# builtins.print = self.__builtins_print
|
||||
|
||||
async def _parse_input(self, inp):
|
||||
cmd_s = inp.split(" ")
|
||||
cmd = cmd_s[0]
|
||||
if cmd == "":
|
||||
return True
|
||||
else:
|
||||
found_in_lua = False
|
||||
d = ev.call_lua_event("onConsoleInput", inp)
|
||||
if len(d) > 0:
|
||||
for text in d:
|
||||
if text is not None:
|
||||
found_in_lua = True
|
||||
self.log(text)
|
||||
command_object = self.__func.get(cmd)
|
||||
if command_object:
|
||||
func = command_object['f']
|
||||
if inspect.iscoroutinefunction(func):
|
||||
out = await func(cmd_s[1:])
|
||||
else:
|
||||
out = func(cmd_s[1:])
|
||||
if out:
|
||||
self.log(out)
|
||||
else:
|
||||
if not found_in_lua:
|
||||
self.log(self.__not_found % cmd)
|
||||
|
||||
async def _read_input(self):
|
||||
with patch_stdout():
|
||||
while self.__run:
|
||||
try:
|
||||
inp = await self.session.prompt_async(
|
||||
self.__prompt_in, completer=self.completer, auto_suggest=AutoSuggestFromHistory()
|
||||
)
|
||||
if await self._parse_input(inp):
|
||||
continue
|
||||
except EOFError:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
self.__run = False
|
||||
except Exception as e:
|
||||
self.__logger.error("Exception in console.py:")
|
||||
self.__logger.exception(e)
|
||||
|
||||
async def _read_input_legacy(self):
|
||||
while self.__run:
|
||||
try:
|
||||
inp = input(self.__prompt_in)
|
||||
if await self._parse_input(inp):
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
self.__logger.error("UnicodeDecodeError")
|
||||
self.__run = False
|
||||
except KeyboardInterrupt:
|
||||
self.__run = False
|
||||
except Exception as e:
|
||||
self.__logger.error("Exception in console.py:")
|
||||
self.__logger.exception(e)
|
||||
|
||||
async def start(self):
|
||||
ev.register("serverTick_0.5s", players_completer.tick_players)
|
||||
# ev.register("get_players_completer", lambda _: players_completer)
|
||||
self.__run = True
|
||||
if self.__legacy_mode:
|
||||
await self._read_input_legacy()
|
||||
else:
|
||||
await self._read_input()
|
||||
self.__debug("Closing console.")
|
||||
raise KeyboardInterrupt
|
||||
|
||||
def stop(self, *args, **kwargs):
|
||||
self.__run = False
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
class Console(object):
|
||||
|
||||
def __init__(self,
|
||||
prompt_in: str = ">",
|
||||
prompt_out: str = "]:",
|
||||
not_found: str = "Command \"%s\" not found in alias.") -> None: ...
|
||||
|
||||
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):
|
||||
|
||||
@staticmethod
|
||||
def alias() -> dict: ...
|
||||
@staticmethod
|
||||
def add_command(key: str, func: function) -> dict: ...
|
||||
|
||||
@staticmethod
|
||||
async def start() -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def builtins_hook() -> None: ...
|
||||
@staticmethod
|
||||
def logger_hook() -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def log(s: str) -> None: ...
|
||||
@staticmethod
|
||||
def write(s: str) -> None: ...
|
||||
@staticmethod
|
||||
def __lshift__(s: AnyStr) -> None: ...
|
||||
@@ -1,223 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.ConsoleSystem.console_system.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.2
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import builtins
|
||||
import logging
|
||||
from typing import AnyStr
|
||||
|
||||
from prompt_toolkit import PromptSession, print_formatted_text, HTML
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.completion import NestedCompleter
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
class Console:
|
||||
|
||||
def __init__(self,
|
||||
prompt_in="> ",
|
||||
prompt_out="",
|
||||
not_found="Command \"%s\" not found in alias.",
|
||||
debug=False) -> None:
|
||||
self.__logger = get_logger("console")
|
||||
self.__is_run = False
|
||||
self.__prompt_in = prompt_in
|
||||
self.__prompt_out = prompt_out
|
||||
self.__not_found = not_found
|
||||
self.__is_debug = debug
|
||||
self.__print = print
|
||||
self.__func = dict()
|
||||
self.__alias = dict()
|
||||
self.__man = dict()
|
||||
self.__desc = dict()
|
||||
self.__print_logger = get_logger("print")
|
||||
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
|
||||
custom_completer={"man": {}})
|
||||
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
|
||||
custom_completer={"help": {"--raw": None}})
|
||||
self.completer = NestedCompleter.from_nested_dict(self.__alias)
|
||||
|
||||
def __debug(self, *x):
|
||||
self.__logger.debug(f"{x}")
|
||||
# if self.__is_debug:
|
||||
# x = list(x)
|
||||
# x.insert(0, "\r CONSOLE DEBUG:")
|
||||
# self.__print(*x)
|
||||
|
||||
def __getitem__(self, item):
|
||||
print(item)
|
||||
|
||||
@staticmethod
|
||||
def __get_max_len(arg) -> int:
|
||||
i = 0
|
||||
arg = list(arg)
|
||||
for a in arg:
|
||||
ln = len(str(a))
|
||||
if ln > i:
|
||||
i = ln
|
||||
return i
|
||||
|
||||
def __create_man_message(self, argv: list) -> AnyStr:
|
||||
if len(argv) == 0:
|
||||
return self.__man.get("man")
|
||||
x = argv[0]
|
||||
if self.__alias.get(x) is None:
|
||||
return i18n.man_command_not_found.format(x)
|
||||
|
||||
man_message = self.__man.get(x)
|
||||
if man_message:
|
||||
return man_message
|
||||
else:
|
||||
return i18n.man_message_not_found
|
||||
|
||||
# noinspection PyStringFormat
|
||||
def __create_help_message(self, argv: list) -> AnyStr:
|
||||
self.__debug("creating help message")
|
||||
raw = False
|
||||
max_len_v = 0
|
||||
if "--raw" in argv:
|
||||
max_len_v = self.__get_max_len(self.__func.values())
|
||||
print()
|
||||
raw = True
|
||||
|
||||
message = "\n"
|
||||
max_len = self.__get_max_len(self.__func.keys())
|
||||
if max_len < 7:
|
||||
max_len = 7
|
||||
|
||||
if raw:
|
||||
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
|
||||
else:
|
||||
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
|
||||
|
||||
for k, v in self.__func.items():
|
||||
doc = self.__desc.get(k)
|
||||
|
||||
if raw:
|
||||
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
|
||||
|
||||
else:
|
||||
if doc is None:
|
||||
doc = i18n.help_message_not_found
|
||||
message += f" %-{max_len}s : %s\n" % (k, doc)
|
||||
|
||||
return message
|
||||
|
||||
def __update_completer(self):
|
||||
self.completer = NestedCompleter.from_nested_dict(self.__alias)
|
||||
|
||||
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
|
||||
key = key.format(" ", "-")
|
||||
|
||||
if not isinstance(key, str):
|
||||
raise TypeError("key must be string")
|
||||
self.__debug(f"added user command: key={key}; func={func};")
|
||||
self.__alias.update(custom_completer or {key: None})
|
||||
self.__alias["man"].update({key: None})
|
||||
self.__func.update({key: {"f": func}})
|
||||
self.__man.update({key: f'html<seagreen>{i18n.man_for} <b>{key}</b>\n{man}</seagreen>' if man else None})
|
||||
self.__desc.update({key: desc})
|
||||
self.__update_completer()
|
||||
return self.__alias.copy()
|
||||
|
||||
def write(self, s: AnyStr):
|
||||
if s.startswith("html"):
|
||||
print_formatted_text(HTML(s[4:]))
|
||||
else:
|
||||
print_formatted_text(s)
|
||||
|
||||
def log(self, s: AnyStr) -> None:
|
||||
self.__logger.info(f"{s}")
|
||||
# self.write(s)
|
||||
|
||||
def __lshift__(self, s: AnyStr) -> None:
|
||||
self.write(s)
|
||||
|
||||
@property
|
||||
def alias(self) -> dict:
|
||||
return self.__alias.copy()
|
||||
|
||||
def __builtins_print(self,
|
||||
*values: object,
|
||||
sep: str or None = " ",
|
||||
end: str or None = None,
|
||||
file: str or None = None,
|
||||
flush: bool = False) -> None:
|
||||
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
|
||||
val = list(values)
|
||||
if len(val) > 0:
|
||||
if self.__is_run:
|
||||
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
|
||||
else:
|
||||
if end is None:
|
||||
end = "\n"
|
||||
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
|
||||
|
||||
def logger_hook(self) -> None:
|
||||
self.__debug("used logger_hook")
|
||||
|
||||
def emit(cls, record):
|
||||
try:
|
||||
msg = cls.format(record)
|
||||
if cls.stream.name == "<stderr>":
|
||||
self.write(f"\r{msg}")
|
||||
else:
|
||||
cls.stream.write(msg + cls.terminator)
|
||||
cls.flush()
|
||||
except RecursionError:
|
||||
raise
|
||||
except Exception as e:
|
||||
cls.handleError(record)
|
||||
|
||||
logging.StreamHandler.emit = emit
|
||||
|
||||
def builtins_hook(self) -> None:
|
||||
self.__debug("used builtins_hook")
|
||||
|
||||
builtins.Console = Console
|
||||
builtins.console = self
|
||||
|
||||
# builtins.print = self.__builtins_print
|
||||
|
||||
async def read_input(self):
|
||||
session = PromptSession(history=FileHistory('./.cmdhistory'))
|
||||
while True:
|
||||
try:
|
||||
with patch_stdout():
|
||||
cmd_in = await session.prompt_async(
|
||||
self.__prompt_in,
|
||||
completer=self.completer,
|
||||
auto_suggest=AutoSuggestFromHistory()
|
||||
)
|
||||
cmd_s = cmd_in.split(" ")
|
||||
cmd = cmd_s[0]
|
||||
if cmd == "":
|
||||
continue
|
||||
else:
|
||||
command_object = self.__func.get(cmd)
|
||||
if command_object:
|
||||
out = command_object['f'](cmd_s[1:])
|
||||
if out:
|
||||
self.log(out)
|
||||
else:
|
||||
self.log(self.__not_found % cmd)
|
||||
except KeyboardInterrupt:
|
||||
raise KeyboardInterrupt
|
||||
except Exception as e:
|
||||
print(f"Error in console.py: {e}")
|
||||
self.__logger.exception(e)
|
||||
|
||||
async def start(self):
|
||||
self.__is_run = True
|
||||
await self.read_input()
|
||||
|
||||
def stop(self, *args, **kwargs):
|
||||
self.__is_run = False
|
||||
raise KeyboardInterrupt
|
||||
30
src/modules/ConsoleSystem/readme.md
Normal file
30
src/modules/ConsoleSystem/readme.md
Normal file
@@ -0,0 +1,30 @@
|
||||
### Builtins
|
||||
|
||||
```python
|
||||
class RCONSystem:
|
||||
console = None
|
||||
|
||||
def __init__(self, key, host, 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, 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
|
||||
def write(s: str) -> None: ...
|
||||
@staticmethod
|
||||
def __lshift__(s: AnyStr) -> None: ...
|
||||
```
|
||||
@@ -1 +1,205 @@
|
||||
from .events_system import EventsSystem
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.EventsSystem
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import builtins
|
||||
import inspect
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
class EventsSystem:
|
||||
|
||||
def __init__(self):
|
||||
# TODO: default events
|
||||
self.log = get_logger("EventsSystem")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.as_tasks = []
|
||||
self.__events = {
|
||||
"onServerStarted": [], # No handler
|
||||
"onPlayerSentKey": [], # Only sync, no handler
|
||||
"onPlayerAuthenticated": [], # (!) Only sync, With handler
|
||||
"onPlayerJoin": [], # (!) With handler
|
||||
"onPlayerReady": [], # No handler
|
||||
"onChatReceive": [], # (!) With handler
|
||||
"onCarSpawn": [], # (!) With handler
|
||||
"onCarDelete": [], # (!) With handler (admin allow)
|
||||
"onCarEdited": [], # (!) With handler
|
||||
"onCarReset": [], # No handler
|
||||
"onCarChanged": [], # No handler
|
||||
"onCarFocusMove": [], # No handler
|
||||
"onSentPing": [], # Only sync, no handler
|
||||
"onChangePosition": [], # Only sync, no handler
|
||||
"onPlayerDisconnect": [], # No handler
|
||||
"onServerStopped": [], # No handler
|
||||
"serverTick": [],
|
||||
"serverTick_0.5s": [],
|
||||
"serverTick_1s": [],
|
||||
"serverTick_2s": [],
|
||||
"serverTick_3s": [],
|
||||
"serverTick_4s": [],
|
||||
"serverTick_5s": [],
|
||||
"serverTick_10s": [],
|
||||
"serverTick_30s": [],
|
||||
"serverTick_60s": [],
|
||||
}
|
||||
self.__async_events = {
|
||||
"onServerStarted": [],
|
||||
"onPlayerJoin": [],
|
||||
"onPlayerReady": [],
|
||||
"onChatReceive": [],
|
||||
"onCarSpawn": [],
|
||||
"onCarDelete": [],
|
||||
"onCarEdited": [],
|
||||
"onCarReset": [],
|
||||
"onCarChanged": [],
|
||||
"onCarFocusMove": [],
|
||||
"onPlayerDisconnect": [],
|
||||
"onServerStopped": [],
|
||||
"serverTick": [],
|
||||
"serverTick_0.5s": [],
|
||||
"serverTick_1s": [],
|
||||
"serverTick_2s": [],
|
||||
"serverTick_3s": [],
|
||||
"serverTick_4s": [],
|
||||
"serverTick_5s": [],
|
||||
"serverTick_10s": [],
|
||||
"serverTick_30s": [],
|
||||
"serverTick_60s": [],
|
||||
}
|
||||
|
||||
self.__lua_events = {
|
||||
"onInit": [], # onServerStarted
|
||||
"onShutdown": [], # onServerStopped
|
||||
"onPlayerAuth": [], # onPlayerAuthenticated
|
||||
"onPlayerConnecting": [], # No
|
||||
"onPlayerJoining": [], # No
|
||||
"onPlayerJoin": [], # onPlayerJoin
|
||||
"onPlayerDisconnect": [], # onPlayerDisconnect
|
||||
"onChatMessage": [], # onChatReceive
|
||||
"onVehicleSpawn": [], # onCarSpawn
|
||||
"onVehicleEdited": [], # onCarEdited
|
||||
"onVehicleDeleted": [], # onCarDelete
|
||||
"onVehicleReset": [], # onCarReset
|
||||
"onFileChanged": [], # TODO lua onFileChanged
|
||||
"onConsoleInput": [], # kt.add_command
|
||||
}
|
||||
self.register_event = self.register
|
||||
|
||||
def builtins_hook(self):
|
||||
self.log.debug("used builtins_hook")
|
||||
builtins.ev = self
|
||||
|
||||
def unregister(self, func):
|
||||
self.log.debug(f"unregister {func}")
|
||||
s = a = 0
|
||||
for k, funcs in self.__events.items():
|
||||
for f in funcs:
|
||||
if f is func:
|
||||
s += 1
|
||||
self.__events[k].remove(func)
|
||||
for k, funcs in self.__async_events.items():
|
||||
for f in funcs:
|
||||
if f is func:
|
||||
a += 1
|
||||
self.__async_events[k].remove(func)
|
||||
self.log.debug(f"unregister in {s + a} events; S:{s}; A:{a};")
|
||||
|
||||
def is_event(self, event_name):
|
||||
return (event_name in self.__async_events.keys() or
|
||||
event_name in self.__events.keys() or
|
||||
event_name in self.__lua_events.keys())
|
||||
|
||||
def register(self, event_name, event_func, async_event=False, lua=None):
|
||||
self.log.debug(f"register(event_name='{event_name}', event_func='{event_func}', "
|
||||
f"async_event={async_event}, lua_event={lua}):")
|
||||
if lua:
|
||||
if event_name not in self.__lua_events:
|
||||
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
|
||||
else:
|
||||
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
|
||||
self.log.debug("Register ok")
|
||||
return
|
||||
|
||||
if not callable(event_func):
|
||||
self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
|
||||
return
|
||||
if async_event or inspect.iscoroutinefunction(event_func):
|
||||
if event_name not in self.__async_events:
|
||||
self.__async_events[event_name] = []
|
||||
self.__async_events[event_name].append(event_func)
|
||||
self.log.debug("Register ok")
|
||||
else:
|
||||
if event_name not in self.__events:
|
||||
self.__events[event_name] = []
|
||||
self.__events[event_name].append(event_func)
|
||||
self.log.debug("Register ok")
|
||||
|
||||
async def call_as_events(self, *args, **kwargs):
|
||||
return await self.call_async_event(*args, **kwargs) + self.call_event(*args, **kwargs)
|
||||
|
||||
async def call_async_event(self, event_name, *args, **kwargs):
|
||||
if not event_name.startswith("serverTick"):
|
||||
self.log.debug(f"Calling async event: '{event_name}'")
|
||||
funcs_data = []
|
||||
if event_name in self.__async_events.keys():
|
||||
for func in self.__async_events[event_name]:
|
||||
try:
|
||||
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
||||
data = await func(event_data)
|
||||
funcs_data.append(data)
|
||||
except Exception as e:
|
||||
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
|
||||
self.log.exception(e)
|
||||
elif not self.is_event(event_name):
|
||||
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()"))
|
||||
|
||||
return funcs_data
|
||||
|
||||
def call_event(self, event_name: str, *args, **kwargs):
|
||||
if event_name not in (
|
||||
"onChangePosition", "onSentPing", # UDP events
|
||||
"_get_player"
|
||||
) and not event_name.startswith("serverTick"):
|
||||
self.log.debug(f"Calling sync event: '{event_name}'")
|
||||
funcs_data = []
|
||||
|
||||
if event_name in self.__events.keys():
|
||||
for func in self.__events[event_name]:
|
||||
try:
|
||||
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
||||
funcs_data.append(func(event_data))
|
||||
except Exception as e:
|
||||
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
|
||||
self.log.exception(e)
|
||||
elif not self.is_event(event_name):
|
||||
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()"))
|
||||
|
||||
return funcs_data
|
||||
|
||||
def call_lua_event(self, event_name, *args):
|
||||
self.log.debug(f"Calling lua event: '{event_name}{args}'")
|
||||
funcs_data = []
|
||||
if event_name in self.__lua_events.keys():
|
||||
for data in self.__lua_events[event_name]:
|
||||
lua = data['lua']
|
||||
func_name = data["func_name"]
|
||||
try:
|
||||
func = lua.globals()[func_name]
|
||||
if not func:
|
||||
self.log.warning(i18n.events_lua_function_not_found.format("", func_name))
|
||||
continue
|
||||
fd = func(*args)
|
||||
funcs_data.append(fd)
|
||||
except Exception as e:
|
||||
self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
|
||||
elif not self.is_event(event_name):
|
||||
self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
|
||||
|
||||
return funcs_data
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.EventsSystem.events_system.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import builtins
|
||||
import inspect
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
class EventsSystem:
|
||||
|
||||
def __init__(self):
|
||||
# TODO: default events
|
||||
self.log = get_logger("EventsSystem")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.as_tasks = []
|
||||
self.__events = {
|
||||
"server_started": [],
|
||||
"auth_sent_key": [], # Only sync
|
||||
"auth_ok": [], # Only sync
|
||||
"player_join": [],
|
||||
"chat_receive": [],
|
||||
"server_stopped": [],
|
||||
}
|
||||
self.__async_events = {
|
||||
"server_started": [],
|
||||
"_plugins_start": [],
|
||||
"_plugins_unload": [],
|
||||
"player_join": [],
|
||||
"chat_receive": [],
|
||||
"server_stopped": []
|
||||
}
|
||||
|
||||
def builtins_hook(self):
|
||||
self.log.debug("used builtins_hook")
|
||||
builtins.ev = self
|
||||
|
||||
def register_event(self, event_name, event_func, async_event=False):
|
||||
self.log.debug(f"register_event({event_name}, {event_func}):")
|
||||
if not callable(event_func):
|
||||
# TODO: i18n
|
||||
self.log.error(f"Cannot add event '{event_name}'. "
|
||||
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
|
||||
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)
|
||||
else:
|
||||
if event_name not in self.__events:
|
||||
self.__events.update({str(event_name): [event_func]})
|
||||
else:
|
||||
self.__events[event_name].append(event_func)
|
||||
|
||||
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:
|
||||
# TODO: i18n
|
||||
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
|
||||
self.log.exception(e)
|
||||
else:
|
||||
# TODO: i18n
|
||||
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_event()?. Just skipping it...")
|
||||
|
||||
return funcs_data
|
||||
|
||||
def call_event(self, event_name, *args, **kwargs):
|
||||
self.log.debug(f"Calling sync event: '{event_name}'")
|
||||
funcs_data = []
|
||||
|
||||
if event_name in self.__events.keys():
|
||||
for func in self.__events[event_name]:
|
||||
try:
|
||||
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
||||
funcs_data.append(func(event_data))
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
|
||||
self.log.exception(e)
|
||||
else:
|
||||
# TODO: i18n
|
||||
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_async_event()?. Just skipping it...")
|
||||
|
||||
return funcs_data
|
||||
@@ -1,11 +1,14 @@
|
||||
from typing import Any
|
||||
|
||||
### Builtins
|
||||
|
||||
```python
|
||||
class EventsSystem:
|
||||
@staticmethod
|
||||
def register_event(event_name, event_func): ...
|
||||
def register(event_name, event_func, async_event: bool = False, lua: bool | object = None): ...
|
||||
@staticmethod
|
||||
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
|
||||
@staticmethod
|
||||
def call_event(event_name, *data, **kwargs) -> list[Any]: ...
|
||||
@staticmethod
|
||||
def call_lua_event(event_name, *data) -> list[Any]: ...
|
||||
class ev(EventsSystem): ...
|
||||
```
|
||||
98
src/modules/PermsSystem/__init__.py
Normal file
98
src/modules/PermsSystem/__init__.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.PermsSystem
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2024
|
||||
from core import get_logger
|
||||
import sqlite3
|
||||
|
||||
|
||||
class PermsSystem:
|
||||
_db_name = "users.db3"
|
||||
|
||||
def __init__(self):
|
||||
self.log = get_logger("PermsSystem")
|
||||
self._create_base()
|
||||
self._completer_permissions = Completer({})
|
||||
# set <permission | group> | unset <permission | group>
|
||||
self._completer_group = Completer({}) # <group_name> info | permission
|
||||
|
||||
_completer_after_user = Completer({
|
||||
"info": None,
|
||||
"permission": {"set": self._completer_permissions, "unset": self._completer_permissions}
|
||||
})
|
||||
self._completer_user = Completer({}, on_none=_completer_after_user) # <nick> info | permission
|
||||
ev.register("add_perm_to_alias", lambda ev: self._completer_permissions.options.update({ev['args'][0]: None}))
|
||||
|
||||
ev.call_event("add_perm_to_alias", "cmd.perms")
|
||||
console.add_command("perms", self._parse_console,
|
||||
None,
|
||||
"Permission module",
|
||||
{"perms": {
|
||||
"groups": {
|
||||
"create": None,
|
||||
"delete": None,
|
||||
"list": None
|
||||
},
|
||||
"user": self._completer_user,
|
||||
"group": self._completer_group,
|
||||
"reload": None,
|
||||
}})
|
||||
ev.register("onChatReceive", self._parse_chat)
|
||||
ev.register("onPlayerJoin", self._process_new_player)
|
||||
|
||||
def _create_base(self):
|
||||
con = sqlite3.connect(self._db_name)
|
||||
cursor = con.cursor()
|
||||
|
||||
# Create table for users
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mp_id INTEGER UNIQUE,
|
||||
nick TEXT NOT NULL,
|
||||
playtime INTEGER
|
||||
)
|
||||
''')
|
||||
|
||||
# Create table for perms
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS perms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mp_id INTEGER,
|
||||
rule TEXT,
|
||||
`group` TEXT,
|
||||
FOREIGN KEY(mp_id) REFERENCES users(mp_id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Create table for groups
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
rules TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
def _parse_console(self, x):
|
||||
pass
|
||||
|
||||
def _parse_chat(self, ev):
|
||||
pass
|
||||
|
||||
def add_player(self, player):
|
||||
self._completer_user.options.update({player.nick: None})
|
||||
self.log.debug(f'Added user: {player.nick}')
|
||||
|
||||
def have_permission(self, ev):
|
||||
player = ev['kwargs']['player']
|
||||
|
||||
def _process_new_player(self, ev):
|
||||
player = ev['kwargs']['player']
|
||||
self.add_player(player)
|
||||
@@ -1 +1,304 @@
|
||||
from .plugins_loader import PluginsLoader
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.PluginsLoader
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import types
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
class KuiToi:
|
||||
_plugins_dir = ""
|
||||
_file = ""
|
||||
|
||||
def __init__(self, name):
|
||||
if not name:
|
||||
raise AttributeError("KuiToi: Name is required")
|
||||
self.__log = get_logger(f"Plugin | {name}")
|
||||
self.__name = name
|
||||
self.__dir = Path(self._plugins_dir) / self.__name
|
||||
os.makedirs(self.__dir, exist_ok=True)
|
||||
self.__funcs = []
|
||||
self.register_event = self.register
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
return self.__log
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__name
|
||||
|
||||
@property
|
||||
def dir(self):
|
||||
return self.__dir
|
||||
|
||||
@contextmanager
|
||||
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
|
||||
path = self.__dir / file
|
||||
if str(self.__dir) in str(file):
|
||||
path = file
|
||||
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
|
||||
# Really need?
|
||||
# if not os.path.exists(path):
|
||||
# with open(path, 'x'): ...
|
||||
f = None
|
||||
try:
|
||||
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
|
||||
yield f
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
if f is not None:
|
||||
f.close()
|
||||
|
||||
def register(self, event_name, event_func):
|
||||
self.log.debug(f"Registering event {event_name}")
|
||||
self.__funcs.append(event_func)
|
||||
ev.register(event_name, event_func)
|
||||
|
||||
def _unload(self):
|
||||
for f in self.__funcs:
|
||||
console.del_command(f)
|
||||
ev.unregister(f)
|
||||
|
||||
def call_event(self, event_name, *args, **kwargs):
|
||||
self.log.debug(f"Called event {event_name}")
|
||||
return ev.call_event(event_name, *args, **kwargs)
|
||||
|
||||
async def call_async_event(self, event_name, *args, **kwargs):
|
||||
self.log.debug(f"Called async event {event_name}")
|
||||
return await ev.call_async_event(event_name, *args, **kwargs)
|
||||
|
||||
def call_lua_event(self, event_name, *args):
|
||||
self.log.debug(f"Called lua event {event_name}")
|
||||
return ev.call_lua_event(event_name, *args)
|
||||
|
||||
def get_player(self, pid=None, nick=None, cid=None):
|
||||
# self.log.debug("Requests get_player")
|
||||
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
|
||||
|
||||
def get_players(self):
|
||||
# self.log.debug("Requests get_players")
|
||||
return self.get_player(-1)
|
||||
|
||||
def players_counter(self):
|
||||
self.log.debug("Requests players_counter")
|
||||
return len(self.get_players())
|
||||
|
||||
def is_player_connected(self, pid=None, nick=None):
|
||||
self.log.debug("Requests is_player_connected")
|
||||
if pid < 0:
|
||||
return False
|
||||
return bool(self.get_player(cid=pid, nick=nick))
|
||||
|
||||
def add_command(self, key, func, man, desc, custom_completer) -> dict:
|
||||
self.log.debug("Requests add_command")
|
||||
self.__funcs.append(func)
|
||||
return console.add_command(key, func, man, desc, custom_completer)
|
||||
|
||||
|
||||
class PluginsLoader:
|
||||
_pip_dir = str(Path("pip-packets").resolve())
|
||||
|
||||
def __init__(self, plugins_dir):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.plugins = {}
|
||||
self.plugins_tasks = []
|
||||
self.plugins_dir = plugins_dir
|
||||
self.log = get_logger("PluginsLoader")
|
||||
self.loaded = []
|
||||
ev.register("_plugins_start", self.start)
|
||||
ev.register("_plugins_unload", self.unload)
|
||||
ev.register("_plugins_get", lambda _: "Plugins: " + ", ".join(f"{i[0]}:{'on' if i[1] else 'off'}" for i in self.loaded))
|
||||
console.add_command("plugins", self._parse_console, None, "Plugins manipulations", {"plugins": {"reload", "load", "unload", "list"}})
|
||||
console.add_command("pl", lambda _: ev.call_event("_plugins_get")[0])
|
||||
sys.path.append(self._pip_dir)
|
||||
os.makedirs(self._pip_dir, exist_ok=True)
|
||||
console.add_command("install", self._pip_install)
|
||||
|
||||
async def _parse_console(self, x):
|
||||
usage = 'Usage: plugin [reload <name> | load <file.py> | unload <name> | list]'
|
||||
if not x:
|
||||
return usage
|
||||
match x[0]:
|
||||
case 'reload':
|
||||
if len(x) == 2:
|
||||
t1 = time.monotonic()
|
||||
ok, _, file, _ = await self._unload_by_name(x[1], True)
|
||||
if ok:
|
||||
if await self._load_by_file(file):
|
||||
self.plugins[x[1]]['plugin'].start()
|
||||
return f"Plugin reloaded ({time.monotonic() - t1:.1f}sec)"
|
||||
return "Plugin not found"
|
||||
return usage
|
||||
case 'load':
|
||||
if len(x) == 2:
|
||||
name = await self._load_by_file(x[1])
|
||||
if name:
|
||||
self.plugins[name]['plugin'].start()
|
||||
return "Plugin loaded"
|
||||
return usage
|
||||
case 'unload':
|
||||
if len(x) == 2:
|
||||
ok, _, _, _ = await self._unload_by_name(x[1], True)
|
||||
if ok:
|
||||
return "Plugin unloaded"
|
||||
return usage
|
||||
case 'list':
|
||||
return ev.call_event("_plugins_get")[0]
|
||||
return usage
|
||||
|
||||
def _pip_install(self, x):
|
||||
self.log.debug(f"_pip_install {x}")
|
||||
if len(x) > 0:
|
||||
try:
|
||||
subprocess.check_call(['pip', 'install', *x, '--target', self._pip_dir])
|
||||
return "Success"
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.log.debug(f"error: {e}")
|
||||
return f"Failed to install packages"
|
||||
else:
|
||||
return "Invalid syntax"
|
||||
|
||||
async def _load_by_file(self, file):
|
||||
file_path = os.path.join(self.plugins_dir, file)
|
||||
if os.path.isfile(file_path) and file.endswith(".py"):
|
||||
try:
|
||||
self.log.info(f"Loading plugin: {file[:-3]}")
|
||||
plugin = types.ModuleType(file[:-3])
|
||||
plugin.KuiToi = KuiToi
|
||||
plugin.KuiToi._plugins_dir = self.plugins_dir
|
||||
plugin.KuiToi._file = file
|
||||
plugin.print = print
|
||||
plugin.__file__ = file_path
|
||||
with open(f'{file_path}', 'r', encoding=config.enc) as f:
|
||||
code = f.read()
|
||||
exec(code, plugin.__dict__)
|
||||
|
||||
ok = True
|
||||
try:
|
||||
is_func = inspect.isfunction
|
||||
if not is_func(plugin.load):
|
||||
self.log.error(i18n.plugins_not_found_load)
|
||||
ok = False
|
||||
if not is_func(plugin.start):
|
||||
self.log.error(i18n.plugins_not_found_start)
|
||||
ok = False
|
||||
if not is_func(plugin.unload):
|
||||
self.log.error(i18n.plugins_not_found_unload)
|
||||
ok = False
|
||||
if type(plugin.kt) != KuiToi:
|
||||
self.log.error(i18n.plugins_kt_invalid)
|
||||
ok = False
|
||||
except AttributeError:
|
||||
ok = False
|
||||
if not ok:
|
||||
self.log.error(i18n.plugins_invalid.format(file_path))
|
||||
return
|
||||
|
||||
pl_name = plugin.kt.name
|
||||
if self.plugins.get(pl_name) is not None:
|
||||
raise NameError(f'Having plugins with identical names is not allowed; '
|
||||
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
|
||||
|
||||
plugin.open = plugin.kt.open
|
||||
is_coro_func = inspect.iscoroutinefunction
|
||||
self.plugins.update(
|
||||
{
|
||||
pl_name: {
|
||||
"plugin": plugin,
|
||||
"load": {
|
||||
"func": plugin.load,
|
||||
"async": is_coro_func(plugin.load)
|
||||
},
|
||||
"start": {
|
||||
"func": plugin.start,
|
||||
"async": is_coro_func(plugin.start)
|
||||
},
|
||||
"unload": {
|
||||
"func": plugin.unload,
|
||||
"async": is_coro_func(plugin.unload)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if self.plugins[pl_name]["load"]['async']:
|
||||
plugin.log.debug(f"I'm async")
|
||||
await plugin.load()
|
||||
else:
|
||||
plugin.log.debug(f"I'm sync")
|
||||
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
|
||||
th.start()
|
||||
th.join()
|
||||
self.loaded.append((pl_name, True))
|
||||
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
|
||||
return pl_name
|
||||
except Exception as e:
|
||||
self.loaded.append((file, False))
|
||||
self.log.error(i18n.plugins_error_loading.format(file, f"{e}"))
|
||||
self.log.exception(e)
|
||||
return False
|
||||
|
||||
async def load(self):
|
||||
self.log.debug("Loading plugins...")
|
||||
for file in os.listdir(self.plugins_dir):
|
||||
await self._load_by_file(file)
|
||||
|
||||
async def _unload_by_name(self, name, reload=False):
|
||||
t1 = time.monotonic()
|
||||
data = self.plugins.get(name)
|
||||
if not data:
|
||||
return False, name, None, None
|
||||
try:
|
||||
if reload:
|
||||
data['plugin'].kt._unload()
|
||||
self.loaded.remove((name, True))
|
||||
self.plugins.pop(name)
|
||||
if data['unload']['async']:
|
||||
self.log.debug(f"Unload async plugin: {name}")
|
||||
await data['unload']['func']()
|
||||
else:
|
||||
self.log.debug(f"Unload sync plugin: {name}")
|
||||
th = Thread(target=data['unload']['func'], name=f"Thread {name}")
|
||||
th.start()
|
||||
th.join()
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
return True, name, data['plugin'].kt._file, time.monotonic() - t1
|
||||
|
||||
async def start(self, _):
|
||||
for pl_name, pl_data in self.plugins.items():
|
||||
try:
|
||||
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, _):
|
||||
t = []
|
||||
for n in self.plugins.keys():
|
||||
await asyncio.sleep(0.01)
|
||||
t.append(self._unload_by_name(n))
|
||||
self.log.debug(await asyncio.gather(*t))
|
||||
self.log.debug("Plugins unloaded")
|
||||
|
||||
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
|
||||
682
src/modules/PluginsLoader/lua_plugins_loader.py
Normal file
682
src/modules/PluginsLoader/lua_plugins_loader.py
Normal file
@@ -0,0 +1,682 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
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_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):
|
||||
if event_name != "getTable":
|
||||
self.log.debug("request TriggerLocalEvent()")
|
||||
self.log.debug(f"Calling local lua event: '{event_name}{args}'")
|
||||
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 len(client) > 0 else None
|
||||
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(f"request TriggerClientEventJson({player_id, event_name, data})")
|
||||
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_player", cid=-1)[0]
|
||||
return self._lua.table_from([i.nick for i in 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):
|
||||
data = {}
|
||||
try:
|
||||
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)
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
data = json.dumps(data)
|
||||
self.log.debug(f"Encoded: {data}")
|
||||
return 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("_lua_plugins_get", lambda x: self.lua_plugins)
|
||||
ev.register("_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['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
|
||||
}
|
||||
}
|
||||
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}'
|
||||
try:
|
||||
_file = os.path.join(sys._MEIPASS, "add_in.lua")
|
||||
except AttributeError:
|
||||
_file = "modules/PluginsLoader/add_in.lua"
|
||||
with open(_file, "r") as f:
|
||||
lua.execute(f.read())
|
||||
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)
|
||||
|
||||
async 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))
|
||||
MP = data['lua'].globals().MP
|
||||
self.log.debug("gather")
|
||||
await asyncio.gather(*MP.tasks)
|
||||
self.log.debug("timers")
|
||||
for _, timer in MP._event_timers.items():
|
||||
timer.stop()
|
||||
self.log.debug("unloaded")
|
||||
@@ -1,196 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.PluginsLoader.plugins_loader.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
import types
|
||||
from contextlib import contextmanager
|
||||
from threading import Thread
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
# TODO: call_client_event, get_player, get_players, GetPlayerCount
|
||||
class KuiToi:
|
||||
_plugins_dir = ""
|
||||
|
||||
def __init__(self, name=None):
|
||||
if name is None:
|
||||
raise AttributeError("KuiToi: Name is required")
|
||||
self.log = get_logger(f"Plugin | {name}")
|
||||
self.__name = name
|
||||
self.__dir = os.path.join(self._plugins_dir, self.__name)
|
||||
if not os.path.exists(self.__dir):
|
||||
os.mkdir(self.__dir)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__name
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
# You chell not pass
|
||||
pass
|
||||
|
||||
@property
|
||||
def dir(self):
|
||||
return self.__dir
|
||||
|
||||
@dir.setter
|
||||
def dir(self, value):
|
||||
# You chell not pass
|
||||
pass
|
||||
|
||||
@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}"')
|
||||
# if not os.path.exists(path):
|
||||
# with open(path, 'x'): ...
|
||||
f = None
|
||||
try:
|
||||
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
|
||||
yield f
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
if f is not None:
|
||||
f.close()
|
||||
|
||||
def register_event(self, event_name, event_func):
|
||||
self.log.debug(f"Registering event {event_name}")
|
||||
ev.register_event(event_name, event_func)
|
||||
|
||||
def call_event(self, event_name, *data, **kwargs):
|
||||
self.log.debug(f"Called event {event_name}")
|
||||
ev.call_event(event_name, *data, **kwargs)
|
||||
|
||||
|
||||
class PluginsLoader:
|
||||
|
||||
def __init__(self, plugins_dir):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.plugins = {}
|
||||
self.plugins_tasks = []
|
||||
self.plugins_dir = plugins_dir
|
||||
self.log = get_logger("PluginsLoader")
|
||||
self.loaded_str = "Plugins: "
|
||||
ev.register_event("_plugins_start", self.start)
|
||||
ev.register_event("_plugins_unload", self.unload)
|
||||
console.add_command("plugins", lambda x: self.loaded_str[:-2])
|
||||
console.add_command("pl", lambda x: self.loaded_str[:-2])
|
||||
|
||||
async def load(self):
|
||||
self.log.debug("Loading plugins...")
|
||||
files = os.listdir(self.plugins_dir)
|
||||
for file in files:
|
||||
if file.endswith(".py"):
|
||||
try:
|
||||
self.log.debug(f"Loading plugin: {file[:-3]}")
|
||||
plugin = types.ModuleType(file[:-3])
|
||||
plugin.KuiToi = KuiToi
|
||||
plugin.KuiToi._plugins_dir = self.plugins_dir
|
||||
plugin.print = print
|
||||
file_path = os.path.join(self.plugins_dir, file)
|
||||
plugin.__file__ = file_path
|
||||
with open(f'{file_path}', 'r', encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
exec(code, plugin.__dict__)
|
||||
|
||||
ok = True
|
||||
try:
|
||||
isfunc = inspect.isfunction
|
||||
if not isfunc(plugin.load):
|
||||
self.log.error('Function "def load():" not found.')
|
||||
ok = False
|
||||
if not isfunc(plugin.start):
|
||||
self.log.error('Function "def start():" not found.')
|
||||
ok = False
|
||||
if not isfunc(plugin.unload):
|
||||
self.log.error('Function "def unload():" not found.')
|
||||
ok = False
|
||||
if type(plugin.kt) != KuiToi:
|
||||
self.log.error(f'Attribute "kt" isn\'t KuiToi class. Plugin file: "{file_path}"')
|
||||
ok = False
|
||||
except AttributeError:
|
||||
ok = False
|
||||
if not ok:
|
||||
self.log.error(f'Plugin file: "{file_path}" is not a valid KuiToi plugin.')
|
||||
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
|
||||
iscorfunc = inspect.iscoroutinefunction
|
||||
self.plugins.update(
|
||||
{
|
||||
pl_name: {
|
||||
"plugin": plugin,
|
||||
"load": {
|
||||
"func": plugin.load,
|
||||
"async": iscorfunc(plugin.load)
|
||||
},
|
||||
"start": {
|
||||
"func": plugin.start,
|
||||
"async": iscorfunc(plugin.start)
|
||||
},
|
||||
"unload": {
|
||||
"func": plugin.unload,
|
||||
"async": iscorfunc(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:
|
||||
# TODO: i18n
|
||||
self.loaded_str += f"{file}:no, "
|
||||
self.log.error(f"Error while loading plugin: {file}; Error: {e}")
|
||||
self.log.exception(e)
|
||||
|
||||
async def start(self, _):
|
||||
for pl_name, pl_data in self.plugins.items():
|
||||
try:
|
||||
if pl_data['start']['async']:
|
||||
self.log.debug(f"Start async plugin: {pl_name}")
|
||||
t = self.loop.create_task(pl_data['start']['func']())
|
||||
self.plugins_tasks.append(t)
|
||||
else:
|
||||
self.log.debug(f"Start sync plugin: {pl_name}")
|
||||
th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}")
|
||||
th.start()
|
||||
self.plugins_tasks.append(th)
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
|
||||
async def unload(self, _):
|
||||
for pl_name, pl_data in self.plugins.items():
|
||||
try:
|
||||
if pl_data['unload']['async']:
|
||||
self.log.debug(f"Unload async plugin: {pl_name}")
|
||||
await pl_data['unload']['func']()
|
||||
else:
|
||||
self.log.debug(f"Unload sync plugin: {pl_name}")
|
||||
th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}")
|
||||
th.start()
|
||||
th.join()
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
126
src/modules/RateLimiter/__init__.py
Normal file
126
src/modules/RateLimiter/__init__.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import asyncio
|
||||
import textwrap
|
||||
from collections import defaultdict, deque
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from core import utils
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
def __init__(self, max_calls: int, period: float, ban_time: float):
|
||||
self.log = utils.get_logger("RateLimiter")
|
||||
self.max_calls = max_calls
|
||||
self.period = timedelta(seconds=period)
|
||||
self.ban_time = timedelta(seconds=ban_time)
|
||||
self._calls = defaultdict(deque)
|
||||
self._banned_until = defaultdict(lambda: datetime.min)
|
||||
self._notified = {}
|
||||
|
||||
def parse_console(self, x):
|
||||
help_msg = textwrap.dedent("""\
|
||||
|
||||
RateLimiter menu:
|
||||
info - list banned ip's
|
||||
ban - put ip in banlist
|
||||
unban - force remove ip from banlist
|
||||
help - print that message""")
|
||||
_banned_ips = [i for i in self._banned_until if self.is_banned(i, False)]
|
||||
if len(x) > 0:
|
||||
match x[0]:
|
||||
case "info":
|
||||
self.log.info(f"Trigger {self.max_calls}req/{self.period}. IP will be banned for {self.ban_time}.")
|
||||
if len(_banned_ips) == 0:
|
||||
return "No one ip in banlist."
|
||||
else:
|
||||
_msg = f"Banned ip{'' if len(_banned_ips) == 1 else 's'}: "
|
||||
for ip in _banned_ips:
|
||||
_msg += f"{ip}; "
|
||||
return _msg
|
||||
case "unban":
|
||||
if len(x) == 2:
|
||||
ip = x[1]
|
||||
if ip in _banned_ips:
|
||||
self._notified[ip] = False
|
||||
self._calls[ip].clear()
|
||||
self._banned_until[ip] = datetime.now()
|
||||
return f"{ip} removed from banlist."
|
||||
return f"{ip} not banned."
|
||||
else:
|
||||
return 'rl unban <IP>'
|
||||
case "ban":
|
||||
if len(x) == 3:
|
||||
ip = x[1]
|
||||
sec = x[2]
|
||||
if not sec.isdigit():
|
||||
return f"{sec!r} is not digit."
|
||||
self._notified[ip] = False
|
||||
self._calls[ip].clear()
|
||||
self._banned_until[ip] = datetime.now() + timedelta(seconds=int(sec))
|
||||
return f"{ip} banned until {self._banned_until[ip]}"
|
||||
else:
|
||||
return 'rl ban <IP> <sec>'
|
||||
case _:
|
||||
return help_msg
|
||||
else:
|
||||
return help_msg
|
||||
|
||||
async def notify(self, ip, writer):
|
||||
if not self._notified[ip]:
|
||||
self._notified[ip] = True
|
||||
self.log.warning(f"{ip} banned until {self._banned_until[ip]}.")
|
||||
try:
|
||||
writer.write(b'\x0b\x00\x00\x00Eip banned.')
|
||||
await writer.drain()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def is_banned(self, ip: str, _add_call=True) -> bool:
|
||||
now = datetime.now()
|
||||
if now < self._banned_until[ip]:
|
||||
return True
|
||||
|
||||
if _add_call:
|
||||
self._calls[ip].append(now)
|
||||
while self._calls[ip] and self._calls[ip][0] + self.period < now:
|
||||
self._calls[ip].popleft()
|
||||
|
||||
if len(self._calls[ip]) > self.max_calls:
|
||||
self._banned_until[ip] = now + self.ban_time
|
||||
self._calls[ip].clear()
|
||||
return True
|
||||
|
||||
self._notified[ip] = False
|
||||
return False
|
||||
|
||||
|
||||
async def handle_request(ip: str, rate_limiter: RateLimiter):
|
||||
if rate_limiter.is_banned(ip):
|
||||
print(f"Request from {ip} is banned at {datetime.now()}")
|
||||
rate_limiter.parse_console(["info"])
|
||||
|
||||
|
||||
async def server_simulation():
|
||||
rate_limiter = RateLimiter(max_calls=5, period=10, ban_time=30)
|
||||
|
||||
# Симулируем несколько запросов от разных IP-адресов
|
||||
tasks = [
|
||||
handle_request("192.168.1.1", rate_limiter),
|
||||
handle_request("192.168.1.2", rate_limiter),
|
||||
handle_request("192.168.1.1", rate_limiter),
|
||||
handle_request("192.168.1.1", rate_limiter),
|
||||
handle_request("192.168.1.3", rate_limiter),
|
||||
handle_request("192.168.1.2", rate_limiter),
|
||||
handle_request("192.168.1.1", rate_limiter),
|
||||
handle_request("192.168.1.2", rate_limiter),
|
||||
handle_request("192.168.1.3", rate_limiter),
|
||||
handle_request("192.168.1.1", rate_limiter),
|
||||
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
|
||||
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
|
||||
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(server_simulation())
|
||||
@@ -1,2 +0,0 @@
|
||||
from .app import web_app
|
||||
from .app import _stop
|
||||
@@ -1,105 +0,0 @@
|
||||
import asyncio
|
||||
from asyncio import CancelledError
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette import status
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
import core.utils
|
||||
from . import utils
|
||||
|
||||
# from .models import SecretKey
|
||||
|
||||
web_app = FastAPI()
|
||||
log = core.utils.get_logger("web")
|
||||
|
||||
uvserver = None
|
||||
data_pool = []
|
||||
data_run = [True]
|
||||
|
||||
|
||||
def response(data=None, code=status.HTTP_200_OK, error_code=0, error_message=None):
|
||||
if 200 >= code <= 300:
|
||||
return JSONResponse(content={"result": data, "error": None}, status_code=code)
|
||||
return JSONResponse(
|
||||
content={"error": {"code": error_code if error_code else code, "message": f"{error_message}"}, "result": None},
|
||||
status_code=code)
|
||||
|
||||
|
||||
@web_app.get("/")
|
||||
async def index():
|
||||
log.debug("Request IndexPage;")
|
||||
return response("Index page")
|
||||
|
||||
|
||||
@web_app.get("/method/{method}")
|
||||
async def _method(method, secret_key: str = None):
|
||||
# log.debug(f"Request method; kwargs: {kwargs}")
|
||||
is_auth = secret_key == config.WebAPI["secret_key"]
|
||||
spl = method.split(".")
|
||||
if len(spl) != 2:
|
||||
raise StarletteHTTPException(405)
|
||||
api_class, api_method = spl
|
||||
match api_class:
|
||||
case "events":
|
||||
match api_method, is_auth:
|
||||
case "get", False:
|
||||
return response(data_pool)
|
||||
raise StarletteHTTPException(404)
|
||||
|
||||
|
||||
async def _stop():
|
||||
await asyncio.sleep(1)
|
||||
if uvserver is not None:
|
||||
uvserver.should_exit = True
|
||||
data_run[0] = False
|
||||
|
||||
|
||||
@web_app.get("/stop")
|
||||
async def stop(secret_key: str):
|
||||
log.debug(f"Request stop; secret key: {secret_key}")
|
||||
if secret_key == config.WebAPI["secret_key"]:
|
||||
log.info("Stopping Web server")
|
||||
asyncio.create_task(_stop())
|
||||
return response("Web server stopped")
|
||||
|
||||
|
||||
@web_app.exception_handler(HTTPException)
|
||||
async def default_exception_handler(request: Request, exc: HTTPException):
|
||||
return response(
|
||||
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
error_code=exc.status_code, error_message=f"Internal Server Error: {exc.status_code}"
|
||||
)
|
||||
|
||||
|
||||
@web_app.exception_handler(StarletteHTTPException)
|
||||
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
||||
code = exc.status_code
|
||||
if code == status.HTTP_405_METHOD_NOT_ALLOWED:
|
||||
return response(code=code, error_message="Method Not Allowed")
|
||||
if code == status.HTTP_404_NOT_FOUND:
|
||||
return response(code=code, error_message="Method not Found")
|
||||
return response(code=code, error_message="Unhandled error..")
|
||||
|
||||
|
||||
@web_app.exception_handler(RequestValidationError)
|
||||
async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
code = status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
return response(code=code, error_message="Request Validation Error")
|
||||
|
||||
|
||||
utils.hack_fastapi()
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
uvconfig = uvicorn.Config(web_app,
|
||||
host=config.WebAPI["server_ip"],
|
||||
port=config.WebAPI["server_port"],
|
||||
loop="asyncio")
|
||||
uvserver = uvicorn.Server(uvconfig)
|
||||
uvserver.run()
|
||||
except KeyboardInterrupt or CancelledError:
|
||||
pass
|
||||
@@ -1,126 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
import click
|
||||
import uvicorn.server as uvs
|
||||
from uvicorn.config import LOGGING_CONFIG
|
||||
|
||||
from uvicorn.lifespan import on
|
||||
|
||||
import core.utils
|
||||
|
||||
# logger = core.utils.get_logger("uvicorn")
|
||||
# uvs.logger = logger
|
||||
logger = uvs.logger
|
||||
|
||||
|
||||
def ev_log_started_message(self, listeners) -> None:
|
||||
cfg = self.config
|
||||
if cfg.fd is not None:
|
||||
sock = listeners[0]
|
||||
logger.info(i18n.web_start.format(sock.getsockname()))
|
||||
elif cfg.uds is not None:
|
||||
logger.info(i18n.web_start.format(cfg.uds))
|
||||
else:
|
||||
addr_format = "%s://%s:%d"
|
||||
host = "0.0.0.0" if cfg.host is None else cfg.host
|
||||
if ":" in host:
|
||||
addr_format = "%s://[%s]:%d"
|
||||
port = cfg.port
|
||||
if port == 0:
|
||||
port = listeners[0].getsockname()[1]
|
||||
protocol_name = "https" if cfg.ssl else "http"
|
||||
message = i18n.web_start.format(addr_format)
|
||||
color_message = (i18n.web_start.format(click.style(addr_format, bold=True)))
|
||||
logger.info(message, protocol_name, host, port, extra={"color_message": color_message})
|
||||
|
||||
|
||||
async def ev_shutdown(self, sockets=None) -> None:
|
||||
logger.debug("Shutting down")
|
||||
for server in self.servers:
|
||||
server.close()
|
||||
for sock in sockets or []:
|
||||
sock.close()
|
||||
for server in self.servers:
|
||||
await server.wait_closed()
|
||||
for connection in list(self.server_state.connections):
|
||||
connection.shutdown()
|
||||
await asyncio.sleep(0.1)
|
||||
try:
|
||||
await asyncio.wait_for(self._wait_tasks_to_complete(), timeout=self.config.timeout_graceful_shutdown)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded", len(self.server_state.tasks))
|
||||
for t in self.server_state.tasks:
|
||||
if sys.version_info < (3, 9):
|
||||
t.cancel()
|
||||
else:
|
||||
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
|
||||
if not self.force_exit:
|
||||
await self.lifespan.shutdown()
|
||||
|
||||
|
||||
async def on_startup(self) -> None:
|
||||
self.logger.debug("Waiting for application startup.")
|
||||
loop = asyncio.get_event_loop()
|
||||
main_lifespan_task = loop.create_task(self.main()) # noqa: F841
|
||||
startup_event = {"type": "lifespan.startup"}
|
||||
await self.receive_queue.put(startup_event)
|
||||
await self.startup_event.wait()
|
||||
if self.startup_failed or (self.error_occured and self.config.lifespan == "on"):
|
||||
self.logger.error("Application startup failed. Exiting.")
|
||||
self.should_exit = True
|
||||
else:
|
||||
self.logger.debug("Application startup complete.")
|
||||
|
||||
|
||||
async def on_shutdown(self) -> None:
|
||||
if self.error_occured:
|
||||
return
|
||||
self.logger.debug("Waiting for application shutdown.")
|
||||
shutdown_event = {"type": "lifespan.shutdown"}
|
||||
await self.receive_queue.put(shutdown_event)
|
||||
await self.shutdown_event.wait()
|
||||
if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"):
|
||||
self.logger.error("Application shutdown failed. Exiting.")
|
||||
self.should_exit = True
|
||||
else:
|
||||
self.logger.debug("Application shutdown complete.")
|
||||
|
||||
|
||||
def hack_fastapi():
|
||||
uvs.Server.shutdown = ev_shutdown
|
||||
uvs.Server._log_started_message = ev_log_started_message
|
||||
on.LifespanOn.startup = on_startup
|
||||
on.LifespanOn.shutdown = on_shutdown
|
||||
|
||||
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
|
||||
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format
|
||||
LOGGING_CONFIG["formatters"].update({
|
||||
"file_default": {
|
||||
"()": "logging.Formatter",
|
||||
"fmt": core.utils.log_format
|
||||
},
|
||||
"file_access": {
|
||||
"()": "logging.Formatter",
|
||||
"fmt": core.utils.log_format
|
||||
}
|
||||
})
|
||||
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
|
||||
LOGGING_CONFIG["handlers"].update({
|
||||
"file_default": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": "./logs/web.log",
|
||||
"encoding": "utf-8",
|
||||
"formatter": "file_default"
|
||||
},
|
||||
"file_access": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": "./logs/web_access.log",
|
||||
"encoding": "utf-8",
|
||||
"formatter": "file_access"
|
||||
}
|
||||
})
|
||||
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
|
||||
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.__init__.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
from .ConsoleSystem import Console
|
||||
from .ConfigProvider import ConfigProvider, Config
|
||||
from .i18n import MultiLanguage
|
||||
from .EventsSystem import EventsSystem
|
||||
from .ConsoleSystem import Console
|
||||
from .PluginsLoader import PluginsLoader
|
||||
from .WebAPISystem import web_app
|
||||
from .WebAPISystem import _stop as stop_web
|
||||
from .i18n import MultiLanguage
|
||||
from .RateLimiter import RateLimiter
|
||||
from .PermsSystem import PermsSystem
|
||||
|
||||
@@ -1,9 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.i18n.__init__.py
|
||||
# File modules.i18n
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Version 1.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
from .i18n import MultiLanguage
|
||||
import builtins
|
||||
import json
|
||||
import os
|
||||
from json import JSONDecodeError
|
||||
|
||||
import requests
|
||||
|
||||
from core.utils import get_logger
|
||||
|
||||
|
||||
class i18n:
|
||||
data = {}
|
||||
|
||||
def __init__(self, data):
|
||||
i18n.data = data
|
||||
|
||||
def __getattribute__(self, key):
|
||||
return i18n.data[key]
|
||||
|
||||
|
||||
class MultiLanguage:
|
||||
|
||||
def __init__(self, language: str = None, files_dir="translates/", encoding=None):
|
||||
if encoding is None:
|
||||
encoding = config.enc
|
||||
if language is None:
|
||||
language = "en"
|
||||
self.__data = {
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization completed.",
|
||||
"start": "Server started!",
|
||||
"stop": "Server stopped!",
|
||||
"auth_need_key": "BeamMP key is required to run!",
|
||||
"auth_empty_key": "BeamMP key is empty!",
|
||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||
"auth_use_link": "Use this link: {}",
|
||||
"GUI_yes": "Yes",
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "OK",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?",
|
||||
"GUI_enter_key_message": "Please enter the key:",
|
||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||
"core_bind_failed": "Failed to bind port. Error: {}",
|
||||
"core_direct_mode": "Server started in direct connection mode.",
|
||||
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
|
||||
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
|
||||
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
|
||||
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
|
||||
"core_auth_server_no_response": "Failed to authenticate the server.",
|
||||
"core_mods_loaded": "Loaded {} mods. {}Mb",
|
||||
"core_identifying_connection": "Processing new connection...",
|
||||
"core_player_kick_outdated": "Incorrect version of BeamMP.",
|
||||
"core_player_kick_bad_key": "Invalid key passed!",
|
||||
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
|
||||
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
|
||||
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
|
||||
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
|
||||
"core_player_kick_server_full": "Server is full.",
|
||||
"core_identifying_okay": "Successful login.",
|
||||
"game_welcome_message": "Welcome {}!",
|
||||
"client_mod_request": "Requested mod: {}",
|
||||
"client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)",
|
||||
"client_mod_sent_limit": " (limit {}Mb/s)",
|
||||
"client_mod_sent_error": "Error sending mod: {}",
|
||||
"client_sync_time": "Sync time {}sec.",
|
||||
"client_kicked": "Kicked for reason: \"{}\"",
|
||||
"client_event_invalid_data": "Invalid data returned from event: {}",
|
||||
"client_player_disconnected": "Left the server. Playtime: {} min",
|
||||
"events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...",
|
||||
"events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...",
|
||||
"events_calling_error": "Error calling \"{}\" in function \"{}\".",
|
||||
"events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.",
|
||||
"events_lua_local": "local ",
|
||||
"events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}",
|
||||
"plugins_not_found_load": "Function \"def load():\" not found.",
|
||||
"plugins_not_found_start": "Function \"def start():\" not found.",
|
||||
"plugins_not_found_unload": "Function \"def unload():\" not found.",
|
||||
"plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.",
|
||||
"plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.",
|
||||
"plugins_error_loading": "An error occurred while loading the plugin {}: {}",
|
||||
"plugins_lua_enabled": "You have enabled Lua plugin support.",
|
||||
"plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.",
|
||||
"plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.",
|
||||
"plugins_lua_legacy_config_create": "Creating it.",
|
||||
"plugins_lua_unload": "Stopping Lua plugin: {}",
|
||||
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Shows the help page for COMMAND.",
|
||||
"man_for": "Help page for",
|
||||
"man_message_not_found": "man: Help page not found.",
|
||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
||||
"man_message_help": "help - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.",
|
||||
"help_message_help": "Shows the names and brief descriptions of commands",
|
||||
"help_command": "Command",
|
||||
"help_message": "Text",
|
||||
"help_message_not_found": "No text found",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
self.__en_data = self.__data.copy()
|
||||
self.__i18n = None
|
||||
self.__encoding = encoding
|
||||
self.language = language
|
||||
if not os.path.exists(files_dir):
|
||||
os.makedirs(files_dir)
|
||||
if not os.path.exists(files_dir + "en.json"):
|
||||
with open(files_dir + "en.json", "w") as f:
|
||||
f.write(json.dumps(self.__en_data, indent=2))
|
||||
self.files_dir = files_dir
|
||||
self.log = get_logger("i18n")
|
||||
self.fi = False
|
||||
self.lang_url = "https://raw.githubusercontent.com/KuiToi/KuiToi-Server/Stable/src/translates/"
|
||||
|
||||
def set_language(self, language):
|
||||
if self.__i18n and language == self.language:
|
||||
return
|
||||
self.log.debug(f"set_language({language})")
|
||||
self.language = language
|
||||
self.open_file()
|
||||
self.__i18n = i18n(self.__data)
|
||||
|
||||
def open_file(self):
|
||||
self.log.debug("open_file")
|
||||
file = self.files_dir + self.language + ".json"
|
||||
try:
|
||||
with open(file, encoding=self.__encoding) as f:
|
||||
self.__data.update(json.load(f))
|
||||
return
|
||||
except JSONDecodeError:
|
||||
self.log.error(
|
||||
f"Localisation \"{file}\" have JsonDecodeError. Using default localisation: en.")
|
||||
except FileNotFoundError:
|
||||
r = requests.get(f"{self.lang_url}{self.language}.json")
|
||||
if r.status_code != 404:
|
||||
self.log.info(f"Downloaded new localisation: {self.language}")
|
||||
loc = r.json()
|
||||
with open(file, "w", encoding=self.__encoding) as f:
|
||||
json.dump(loc, f)
|
||||
self.__data.update(loc)
|
||||
return
|
||||
else:
|
||||
self.log.warning(f"Localisation \"{file}\" not found; Using default localisation: en.")
|
||||
self.set_language("en")
|
||||
|
||||
def builtins_hook(self) -> None:
|
||||
self.log.debug("used builtins_hook")
|
||||
builtins.i18n = self.__i18n
|
||||
builtins.i18n_data = self.__data
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Greetings from KuiToi Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization complete.",
|
||||
"start": "Server started!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"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",
|
||||
"GUI_yes": "Yes",
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "Ok",
|
||||
"GUI_cancel": "Cancel",
|
||||
"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 started at {} (Press CTRL+C to quit)",
|
||||
|
||||
"": "Command: man",
|
||||
"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 - 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": "Description",
|
||||
"help_message_not_found": "No description available.",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Привет из KuiToi-Server!",
|
||||
"config_path": "Используй {} для настройки.",
|
||||
"init_ok": "Инициализация окончена.",
|
||||
"start": "Сервер запущен!",
|
||||
"stop": "Сервер остановлен!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "Нужен BeamMP ключ для запуска!",
|
||||
"auth_empty_key": "BeamMP ключ пустой!",
|
||||
"auth_cannot_open_browser": "Не получилось открыть браузер: {}",
|
||||
"auth_use_link": "Используй эту ссылку: {}",
|
||||
|
||||
"": "GUI phases",
|
||||
"GUI_yes": "Да",
|
||||
"GUI_no": "Нет",
|
||||
"GUI_ok": "Окей",
|
||||
"GUI_cancel": "Отмена",
|
||||
"GUI_need_key_message": "Нужен BeamMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
|
||||
"GUI_enter_key_message": "Пожалуйста введите ключ:",
|
||||
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",
|
||||
|
||||
"": "Web phases",
|
||||
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
|
||||
|
||||
"": "Command: 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: Команда \"{}\" не найдена!",
|
||||
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - Показывает названия и краткое описание команд.\nИспользование: help [--raw]\nКоманда `help` выводит список всех доступных команд, и краткое описание для каждой команды.",
|
||||
"help_message_help": "Показывает названия и краткое описание команд",
|
||||
"help_command": "Команда",
|
||||
"help_message": "Текст",
|
||||
"help_message_not_found": "Нет текста",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Выключает сервер.\nИспользование: stop",
|
||||
"help_message_stop": "Выключает сервер.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Выключает сервер.\nИспользование: exit",
|
||||
"help_message_exit": "Выключает сервер."
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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"]
|
||||
|
||||
# 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"]
|
||||
|
||||
# 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"]
|
||||
|
||||
# Web phases
|
||||
web_start: str = data["web_start"]
|
||||
|
||||
# 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"]
|
||||
|
||||
# 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"]
|
||||
|
||||
# Command: stop
|
||||
man_message_stop: str = data["man_message_stop"]
|
||||
help_message_stop: str = data["help_message_stop"]
|
||||
|
||||
# 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."
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.i18n.i18n.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import builtins
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
|
||||
from core.utils import get_logger
|
||||
|
||||
|
||||
class i18n:
|
||||
|
||||
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"]
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class MultiLanguage:
|
||||
|
||||
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding="utf-8"):
|
||||
if language is None:
|
||||
language = "en"
|
||||
self.__data = {}
|
||||
self.__i18n = None
|
||||
self.__encoding = encoding
|
||||
self.language = language
|
||||
self.files_dir = files_dir
|
||||
self.log = get_logger("i18n")
|
||||
self.set_language(language)
|
||||
|
||||
def set_language(self, language):
|
||||
if language is None:
|
||||
language = "en"
|
||||
self.log.debug(f"set_language({language})")
|
||||
self.language = language
|
||||
if language != "en":
|
||||
self.open_file()
|
||||
else:
|
||||
# noinspection PyDictDuplicateKeys
|
||||
self.__data = {
|
||||
"": "Basic phases",
|
||||
"hello": "Greetings from KuiToi Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization complete.",
|
||||
"start": "Server started!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"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",
|
||||
"GUI_yes": "Yes",
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "Ok",
|
||||
"GUI_cancel": "Cancel",
|
||||
"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 started at {} (Press CTRL+C to quit)",
|
||||
|
||||
"": "Command: man",
|
||||
"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 - 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": "Description",
|
||||
"help_message_not_found": "No description available.",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
self.__i18n = i18n(self.__data)
|
||||
|
||||
def open_file(self):
|
||||
self.log.debug("open_file")
|
||||
file = self.files_dir + self.language + ".json"
|
||||
try:
|
||||
with open(file, encoding=self.__encoding) as f:
|
||||
self.__data.update(json.load(f))
|
||||
return
|
||||
except JSONDecodeError:
|
||||
self.log.error(
|
||||
f"Localisation \"{self.language}.json\" have JsonDecodeError. Using default localisation: en.")
|
||||
except FileNotFoundError:
|
||||
self.log.warning(f"Localisation \"{self.language}.json\" not found; Using default localisation: en.")
|
||||
self.set_language("en")
|
||||
|
||||
def builtins_hook(self) -> None:
|
||||
self.log.debug("used builtins_hook")
|
||||
builtins.i18n = self.__i18n
|
||||
builtins.i18n_data = self.__data
|
||||
112
src/modules/i18n/readme.md
Normal file
112
src/modules/i18n/readme.md
Normal file
@@ -0,0 +1,112 @@
|
||||
### Builtins
|
||||
|
||||
```python
|
||||
class i18n:
|
||||
# Basic phases
|
||||
hello: str
|
||||
config_path: str
|
||||
init_ok: str
|
||||
start: str
|
||||
stop: str
|
||||
|
||||
# Server auth
|
||||
auth_need_key: str
|
||||
auth_empty_key: str
|
||||
auth_cannot_open_browser: str
|
||||
auth_use_link: str
|
||||
|
||||
# GUI phases
|
||||
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
|
||||
|
||||
# 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_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
|
||||
help_message_man: str
|
||||
man_for: str
|
||||
man_message_not_found: str
|
||||
man_command_not_found: str
|
||||
|
||||
# Command: help
|
||||
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
|
||||
help_message_stop: str
|
||||
|
||||
# Command: exit
|
||||
man_message_exit: str
|
||||
help_message_exit: str
|
||||
```
|
||||
103
src/translates/cn.json
Normal file
103
src/translates/cn.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"": "基本阶段",
|
||||
"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请使用此链接:{}",
|
||||
|
||||
"": "核心短语",
|
||||
"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_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": "停止服务器。"
|
||||
}
|
||||
103
src/translates/en.json
Normal file
103
src/translates/en.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization completed.",
|
||||
"start": "Server started!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BeamMP key is required to run!",
|
||||
"auth_empty_key": "BeamMP key is empty!",
|
||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||
"auth_use_link": "Use this link: {}",
|
||||
|
||||
"": "GUI phases",
|
||||
"GUI_yes": "Yes",
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "OK",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?",
|
||||
"GUI_enter_key_message": "Please enter the key:",
|
||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||
|
||||
"": "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_identifying_okay": "Successful login.",
|
||||
|
||||
"": "In-game phrases",
|
||||
"game_welcome_message": "Welcome {}!",
|
||||
|
||||
"": "Client class phrases",
|
||||
"client_mod_request": "Requested mod: {}",
|
||||
"client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)",
|
||||
"client_mod_sent_limit": " (limit {}Mb/s)",
|
||||
"client_mod_sent_error": "Error sending mod: {}",
|
||||
"client_sync_time": "Sync time {}sec.",
|
||||
"client_kicked": "Kicked for reason: \"{}\"",
|
||||
"client_event_invalid_data": "Invalid data returned from event: {}",
|
||||
"client_player_disconnected": "Left the server. Playtime: {} min",
|
||||
|
||||
"": "Events system",
|
||||
|
||||
"events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...",
|
||||
"events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...",
|
||||
"events_calling_error": "Error calling \"{}\" in function \"{}\".",
|
||||
"events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.",
|
||||
"events_lua_local": "local ",
|
||||
"events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}",
|
||||
|
||||
"": "Plugins loader",
|
||||
|
||||
"plugins_not_found_load": "Function \"def load():\" not found.",
|
||||
"plugins_not_found_start": "Function \"def start():\" not found.",
|
||||
"plugins_not_found_unload": "Function \"def unload():\" not found.",
|
||||
"plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.",
|
||||
"plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.",
|
||||
"plugins_error_loading": "An error occurred while loading the plugin {}: {}",
|
||||
|
||||
"": "Lua plugins loader",
|
||||
|
||||
"plugins_lua_enabled": "You have enabled Lua plugin support.",
|
||||
"plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.",
|
||||
"plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.",
|
||||
"plugins_lua_legacy_config_create": "Creating it.",
|
||||
"plugins_lua_unload": "Stopping Lua plugin: {}",
|
||||
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Shows the help page for COMMAND.",
|
||||
"man_for": "Help page for",
|
||||
"man_message_not_found": "man: Help page not found.",
|
||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
||||
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.",
|
||||
"help_message_help": "Shows the names and brief descriptions of commands",
|
||||
"help_command": "Command",
|
||||
"help_message": "Text",
|
||||
"help_message_not_found": "No text found",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
103
src/translates/ru.json
Normal file
103
src/translates/ru.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Привет из KuiToi-Server!",
|
||||
"config_path": "Используй {} для настройки.",
|
||||
"init_ok": "Инициализация окончена.",
|
||||
"start": "Сервер запущен!",
|
||||
"stop": "Сервер остановлен!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "Нужен BeamMP ключ для запуска!",
|
||||
"auth_empty_key": "BeamMP ключ пустой!",
|
||||
"auth_cannot_open_browser": "Не получилось открыть браузер: {}",
|
||||
"auth_use_link": "Используй эту ссылку: {}",
|
||||
|
||||
"": "GUI phases",
|
||||
"GUI_yes": "Да",
|
||||
"GUI_no": "Нет",
|
||||
"GUI_ok": "Окей",
|
||||
"GUI_cancel": "Отмена",
|
||||
"GUI_need_key_message": "Нужен BeamMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
|
||||
"GUI_enter_key_message": "Пожалуйста введите ключ:",
|
||||
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",
|
||||
|
||||
"": "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_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.",
|
||||
"man_for": "Страничка помощи для",
|
||||
"man_message_not_found": "man: Страничка помощи не найдена.",
|
||||
"man_command_not_found": "man: Команда \"{}\" не найдена!",
|
||||
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - Показывает названия и краткое описание команд.\nИспользование: help [--raw]\nКоманда `help` выводит список всех доступных команд, и краткое описание для каждой команды.",
|
||||
"help_message_help": "Показывает названия и краткое описание команд",
|
||||
"help_command": "Команда",
|
||||
"help_message": "Текст",
|
||||
"help_message_not_found": "Нет текста",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Выключает сервер.\nИспользование: stop",
|
||||
"help_message_stop": "Выключает сервер.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Выключает сервер.\nИспользование: exit",
|
||||
"help_message_exit": "Выключает сервер."
|
||||
}
|
||||
73
win/auto.json
Normal file
73
win/auto.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"version": "auto-py-to-exe-configuration_v1",
|
||||
"pyinstallerOptions": [
|
||||
{
|
||||
"optionDest": "noconfirm",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "filenames",
|
||||
"value": "../src/main.py"
|
||||
},
|
||||
{
|
||||
"optionDest": "onefile",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "console",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "icon_file",
|
||||
"value": "./icon.ico"
|
||||
},
|
||||
{
|
||||
"optionDest": "name",
|
||||
"value": "KuiToi-Server"
|
||||
},
|
||||
{
|
||||
"optionDest": "clean_build",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "strip",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "noupx",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "disable_windowed_traceback",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "version_file",
|
||||
"value": "./metadata.txt"
|
||||
},
|
||||
{
|
||||
"optionDest": "uac_admin",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "uac_uiaccess",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "argv_emulation",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "bootloader_ignore_signals",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "datas",
|
||||
"value": "C:/Users/SantaSpeen/PycharmProjects/KuiToi-Server/src/modules/PluginsLoader/add_in.lua;."
|
||||
}
|
||||
],
|
||||
"nonPyinstallerOptions": {
|
||||
"increaseRecursionLimit": true,
|
||||
"manualArguments": ""
|
||||
}
|
||||
}
|
||||
BIN
win/icon.ico
Normal file
BIN
win/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
44
win/metadata.txt
Normal file
44
win/metadata.txt
Normal file
@@ -0,0 +1,44 @@
|
||||
# UTF-8
|
||||
#
|
||||
# For more details about fixed file info 'ffi' see:
|
||||
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
||||
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
||||
# Set not needed items to zero 0. Must always contain 4 elements.
|
||||
filevers=(0,4,5,0),
|
||||
prodvers=(0,4,5,0),
|
||||
# Contains a bitmask that specifies the valid bits 'flags'r
|
||||
mask=0x3f,
|
||||
# Contains a bitmask that specifies the Boolean attributes of the file.
|
||||
flags=0x0,
|
||||
# The operating system for which this file was designed.
|
||||
# 0x4 - NT and there is no need to change it.
|
||||
OS=0x40004,
|
||||
# The general type of file.
|
||||
# 0x1 - the file is an application.
|
||||
fileType=0x1,
|
||||
# The function of the file.
|
||||
# 0x0 - the function is not defined for this fileType
|
||||
subtype=0x0,
|
||||
# Creation date and time stamp.
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
u'040904B0',
|
||||
[StringStruct(u'CompanyName', u'KuiToi'),
|
||||
StringStruct(u'FileDescription', u'KuiToi Server'),
|
||||
StringStruct(u'FileVersion', u'0.4.5.0'),
|
||||
StringStruct(u'InternalName', u'KuiToi Server'),
|
||||
StringStruct(u'LegalCopyright', u'© Maxim Khomutov'),
|
||||
StringStruct(u'OriginalFilename', u'KuiToi-Server.exe'),
|
||||
StringStruct(u'ProductName', u'KuiToi Server'),
|
||||
StringStruct(u'ProductVersion', u'0.4.5.0')])
|
||||
]),
|
||||
VarFileInfo([VarStruct(u'Translation', [1049, 1251, 1033, 1251, 2052, 950])])
|
||||
]
|
||||
)
|
||||
Reference in New Issue
Block a user