104 Commits

Author SHA1 Message Date
7464a4095d [+] Warning on udp_addr != main_addr 2024-08-12 18:43:42 +03:00
3dc2232db2 [+] Fix onModsSending 2024-08-05 18:40:20 +03:00
a16e2e39d9 [~] go home... 2024-08-02 18:18:53 +03:00
ac2aba4b27 [~] Basic events 2024-08-02 16:33:13 +03:00
66db0aa9f7 [~] Minor 2024-08-02 16:17:52 +03:00
2197a32354 [+] PluginConsole
[+] ev.unregister_by_id
[+] Completes for PluginsLoader
[~] asyncio.to_thread
[~] console.legacy_mode
2024-08-02 16:03:14 +03:00
1e95a7519b [+] PluginConsole
[+] ev.unregister_by_id
[+] Completes for PluginsLoader
[~] asyncio.to_thread
[~] console.legacy_mode
2024-08-02 16:03:11 +03:00
16d5d06881 [+] db 2024-08-02 09:04:52 +03:00
e43dc69b5c [+] onCarSpawned
[+] onCarDeleted
[~] change onChatReceive handler
[~] Minor
2024-08-02 09:03:50 +03:00
1e685e69ed [+] on_none
[+] WIP PermsSystem
2024-08-02 09:01:44 +03:00
633e235342 [+] sent counters
[+] Colored TPS
[~] Minor
2024-08-01 18:06:55 +03:00
cbb3fc8b29 [+] ANSI 2024-08-01 18:05:17 +03:00
613dfb741a [-] print TPS every sec 2024-07-31 18:07:04 +03:00
c2159fc523 [~] Relogic onChatReceive
[~] minor
2024-07-31 18:06:45 +03:00
180ab2421e [~] Minor 2024-07-31 18:06:12 +03:00
25c3f503bf [+] call_as_events 2024-07-31 18:05:57 +03:00
d2c856fd90 del 2024-07-31 18:05:42 +03:00
189af0d773 [~] kick 2024-07-31 15:51:34 +03:00
d003601b58 [!] FIX Legacy mode
[+] MyNestedCompleter
[+] players_completer
2024-07-31 15:50:37 +03:00
72035c226b minor 2024-07-31 12:14:58 +03:00
8ed5671995 bump version 2024-07-30 01:30:43 +03:00
b64c449065 [+] PPS
[+] Clients ticks
[>] UDP handler to Client class
[~] Minor
2024-07-30 00:52:01 +03:00
f2de91d0f1 [~] FIX add_in.lua 2024-07-29 15:21:44 +03:00
243177ee6f [~] console.del_command
[!] FIX plugins load | reload
2024-07-29 15:20:35 +03:00
7796e3970d [~] Minor 2024-07-29 03:09:16 +03:00
2bf1c07041 [+] plugins command 2024-07-29 03:09:02 +03:00
2b4c0bf4d0 [~] FastFix 2024-07-27 19:01:16 +03:00
2ee74c310d [+] TPS
[!] FIX tcp. Againg?...
2024-07-27 17:00:27 +03:00
b0303f3e6d [!] Fix closing connection 2024-07-27 12:21:29 +03:00
a406956080 [~] Minor 2024-07-27 12:12:41 +03:00
027c239424 [!] Fix 'add_in.lua' 2024-07-27 05:16:31 +03:00
9e86c41a6a [!] Fix events
[~] Minor updates
2024-07-27 05:04:49 +03:00
bf1c6d2c41 [-] WebAPI 2024-07-27 05:03:27 +03:00
71ec0c7aed [-] WebAPI 2024-07-27 05:03:23 +03:00
85475a49be [-] WebAPI 2024-07-27 03:45:59 +03:00
8cbe3d07e3 bump build 2024-07-26 17:56:33 +03:00
b2a608d369 [+] Add supports for pip packets 2024-07-26 17:07:32 +03:00
c12a91bf86 [+] preparations for pip 2024-07-25 18:58:53 +03:00
209004c9cb [+] pip-packets 2024-07-25 18:30:36 +03:00
2af4681082 [+] Support pip packages 2024-07-25 17:32:00 +03:00
f1f80cc94c [!] FIX unicycle
[!] FIX BeamMP bug
[~] Minor
2024-07-25 17:00:09 +03:00
06942e8a71 [!] Fix open 2024-07-25 12:36:20 +03:00
51867d526d [+] onPlayerReady
[+] ResetCar
2024-07-25 05:07:35 +03:00
ff58e2a994 [minor] 2024-07-25 03:33:46 +03:00
4c6a240f96 [!] Fastfix
[+] call_async_event onPlayerAuthenticated
2024-07-25 02:54:20 +03:00
666a76201e [!] Fastfix 2024-07-25 01:01:42 +03:00
b4b3953194 [!] FIX i18n 2024-07-16 18:32:08 +03:00
b73674f409 [+] requests 2024-07-16 18:08:54 +03:00
5330943d6f [>] snowman > unicycle 2024-07-16 18:08:39 +03:00
c533702cdc [+] Now localizations download from github 2024-07-16 17:49:21 +03:00
3b9cea5cae [-] core_player_set_id 2024-07-16 17:21:17 +03:00
a923dbea1f [~] 0.4.5 > 0.4.6
[+] Recreate config (if empty)
[+] Hidden config
[!] FIX rl
[!] Update getting loop
[-] core_player_set_id
[~] Update copyrights
[~] Minor
2024-07-16 17:18:49 +03:00
2eb7e8801b [+] rl.parse_console 2024-07-15 17:41:14 +03:00
c4a34c6630 [~] Minor
[>] main files > init.py
[+] RateLimiter
[~] Update BeamMP_version
2024-07-15 16:32:22 +03:00
8fbd2cc330 [~] Update versions in req.txt
[~] Rename register_event > register
2024-07-15 11:39:31 +03:00
c838d42dd8 [+] Basic events
[~] Remove double texts
2024-06-28 17:59:29 +03:00
274bdead2f [>] TODOs to md
[>] windows's blobs to win/
2024-06-28 17:51:12 +03:00
163b233306 Fix lua client handler #1 2023-08-17 23:54:07 +03:00
c6fba31c89 minor 2023-08-17 23:48:56 +03:00
59c113ade5 minor 2023-08-17 21:25:52 +03:00
a06c48dca5 Add _synced #1 2023-08-17 21:25:47 +03:00
42e4a8e05e Feature ev_data is None -> continue 2023-08-17 18:32:48 +03:00
074589da47 FIX GetPlayers #1 2023-08-17 18:27:16 +03:00
50c12f4b5c FIX _identifiers and e | e 2023-08-17 18:26:53 +03:00
f145048cc1 FIX #1 2023-08-16 05:11:48 +03:00
68bf7d0d00 More debug info 2023-08-16 05:02:51 +03:00
e9815cdfcf FIX _get_player #1 2023-08-16 05:02:39 +03:00
acb2b45626 linux fix 2023-08-16 01:03:02 +03:00
eb71fda356 0.4.4 -> 0.4.5 2023-08-15 22:56:43 +03:00
cc400d5a12 0.4.3 -> 0.4.4 2023-08-15 22:53:42 +03:00
67b3bd26bb 0.4.3 -> 0.4.4 2023-08-15 22:53:20 +03:00
77250561fb Update build 2023-08-15 22:35:42 +03:00
70dfb9b40c Move translations 2023-08-15 22:34:27 +03:00
3118c74990 Update docs 2023-08-15 22:34:07 +03:00
cb6adde7c2 prepare for .exe builds 2023-08-15 22:15:19 +03:00
422dd35a8f prepare for .exe build 2023-08-15 21:51:57 +03:00
2c2bd1cb4a n\f event fix 2023-08-15 19:31:39 +03:00
7eba3d5877 mod time fix 2023-08-15 16:56:41 +03:00
abbd64184e t fix 2023-07-31 22:33:51 +03:00
e5dd63579b RCON (WIP) 2023-07-31 21:38:36 +03:00
ef286b7e03 RCON (WIP) 2023-07-31 21:38:08 +03:00
3a42fa13e7 Work time 2023-07-31 21:37:48 +03:00
cdec0b9949 Minor fixes 2023-07-31 21:37:15 +03:00
de91d075b4 Protocol fixes 2023-07-27 02:05:49 +03:00
a7c02e0b52 Added kick command 2023-07-26 22:38:50 +03:00
6dd3de63a9 Added support async command 2023-07-26 22:37:32 +03:00
7e0c50a04e Update TODOs 2023-07-26 18:42:21 +03:00
f1ab07d49a Small fixes 2023-07-26 18:41:37 +03:00
cdeacc16bf Final of i18n 2023-07-26 18:40:44 +03:00
719e705bab Start adding RCON part! 2023-07-26 05:00:49 +03:00
21dd23cb55 Added Plugins loaders translations. 2023-07-26 04:28:57 +03:00
9295ed2b7a hotfix 2023-07-26 04:25:25 +03:00
7466f987ac hotfix 2023-07-26 01:52:08 +03:00
d29bb9de98 Ready Core translations! 2023-07-26 01:51:23 +03:00
189bd0cc30 Update TODOs 2023-07-26 01:21:17 +03:00
53dae25fcf Change Client events handler 2023-07-26 01:21:01 +03:00
e241a7da4c Added Client events handler 2023-07-26 01:20:05 +03:00
13be12a7a1 Added event onCarChanged; 2023-07-26 01:09:28 +03:00
e348ffecc3 Fix lua colors 2023-07-26 00:56:49 +03:00
336aa31732 Added new translations;
Added new event;
2023-07-26 00:39:53 +03:00
d8c667ff51 Added new translations;
Added new event;
2023-07-26 00:32:11 +03:00
658f9ed9c6 Add some colors ^) 2023-07-25 21:19:13 +03:00
c1cb8dcdba Minor update 2023-07-25 21:19:00 +03:00
f52c73ab76 Non CMD mode. 2023-07-25 19:28:49 +03:00
76 changed files with 3241 additions and 2141 deletions

5
.gitignore vendored
View File

@@ -140,3 +140,8 @@ dmypy.json
logs/
*.yml
*.toml
/win-ver_info.txt
/output/
pip-packets/
users.db3

View File

@@ -13,80 +13,11 @@ I didn't like writing plugins in Lua after using Python; it was very inconvenien
## 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)
- [ ] 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
- [ ] Add all events
- [x] MultiLanguage: (i18n support)
- [ ] Core
- [x] Console
- [x] WebAPI
- [x] Plugins supports:
- [x] Python part:
- [x] Load Python plugins
- [x] Async support
- [x] KuiToi class
- [x] Client (Player) class
- [x] Lua part: (Original BeamMP compatibility)
- [x] Load Lua plugins
- [x] MP Class
- [x] Util class
- [x] FS class
- [ ] HTTP API Server: (fastapi)
- [x] Stop and Start with core
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] RCON System:
- [ ] Serving
- [ ] Client
- [ ] SSL encryption
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] [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
View 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)

View File

@@ -21,9 +21,6 @@
"GUI_enter_key_message": "请输入密钥:",
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接{}",
"": "Web阶段",
"web_start": "WebAPI已启动{}CTRL+C停止",
"": "命令man",
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法man COMMAND",
"help_message_man": "显示COMMAND的帮助页面。",

View File

@@ -1,3 +1,3 @@
# 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) 的副本。如果你想将其翻译成以前未翻译过的语言,或者更新现有的翻译,我将很高兴接受你的拉取请求。

View File

@@ -23,7 +23,7 @@ _常量_\
_与open()参数相同_\
在kt.dir中打开文件
### kt.register_event(event_name: str, event_func: function)
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> 作为`event_func`调用的事件名称._\
_`event_func: function` -> 要调用的函数._

View File

@@ -1,3 +1,13 @@
# 所有可用事件列表
大多数事件将包含`pl = data ['kwargs'] ['player']`,可以在[这里](./classes.md)找到描述。
* onPlayerJoin
* onPlayerDisconnect
* playerLoaded
* onChatReceive
* onCarSpawn
* onCarDelete
* onCarEdited
* onCarChanged
* ...

View File

@@ -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("Плагин загружен успешно.")

View File

@@ -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("Плагин загружен успешно.")

View File

@@ -28,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("插件已成功加载。")
@@ -47,7 +47,7 @@ def unload():
您还可以在[example.py](examples/example.py)中找到更广泛的示例。
* 建议在`load()`后使用`open()`,否则应使用`kt.load()`-在`plugin/<plugin_name>/<filename>`文件夹中创建一个文件
* 创建自己的事件:`kt.register_event("my_event", my_event_function)`-
* 创建自己的事件:`kt.register("my_event", my_event_function)`-
* 调用事件:`kt.call_event("my_event")`
* 使用数据调用事件:`kt.call_event("my_event", data, data2=data2)`
* 基本事件_稍后会写_
@@ -72,7 +72,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("插件已成功加载。")
@@ -91,7 +91,7 @@ async def unload():
您还可以在[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事件_稍后会写_

View File

@@ -6,5 +6,4 @@
2. 插件和事件系统 - [这里](./plugins)
3. Lua的细微差别 - [这里](./plugins/lua)
4. 多语言支持 - [这里](./multilanguage)
5. KuiToi WebAPI - [这里](./web)
6. 将会有新的内容...
5. 将会有新的内容...

View File

@@ -34,7 +34,7 @@ Auth:
private: true
Game:
map: gridmap_v2
max_cars: 1
cars: 1
players: 8
Options:
debug: false
@@ -49,11 +49,6 @@ Server:
name: KuiToi-Server
server_ip: 0.0.0.0
server_port: 30814
WebAPI:
enabled: false
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
server_ip: 127.0.0.1
server_port: 8433
```
### Auth
@@ -64,7 +59,7 @@ WebAPI:
### Game
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
* `max_cars` - 每个玩家的最大汽车数量
* `cars` - 每个玩家的最大汽车数量
* `players` - 最大玩家数
### Options
@@ -83,6 +78,3 @@ WebAPI:
* `name` - BeamMP 启动器的服务器名称
* `server_ip` - 分配给服务器的 IP 地址(仅适用于有经验的用户,默认为 0.0.0.0
* `server_port` - 服务器将在哪个端口上工作
### WebAPI
##### _文档尚未准备好_

View File

@@ -1,13 +0,0 @@
# 服务器的 WebAPI
## 可用的端点
* `/stop`:
* 必需参数:
* `secret_key` - 在服务器配置中指定的密钥
* `/event.get`
* 这个端点还没有准备好
* 必需参数:
* `secret_key` - 在服务器配置中指定的密钥

View File

@@ -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.

View File

@@ -1,127 +1,3 @@
Sure, here's a translation of the text:
# Passed Classes
## Worth looking at
1. What are `*args` and `**kwargs`? -> [Post on Habr (RU)](https://habr.com/ru/companies/ruvds/articles/482464/)
## KuiToi
_`kt = KuiToi("PluginName"")`_
### kt.log
_Constant_\
Returns a pre-configured logger
### kt.name
_Constant_\
Returns the name of the plugin
### kt.dir
_Constant_\
Returns the directory of the plugin
### kt.open()
_Parameters are the same as for open()_\
Opens a file in kt.dir
### kt.register_event(event_name: str, event_func: function)
_`event_name: str` -> The name of the event that `event_func` will be called on._\
_`event_func: function` -> The function that will be called._
In `event_func`, you can pass both regular functions and async functions - you don't need to make them async beforehand.\
You can also create your own events with your own names.\
You can register an unlimited number of events.
### kt.call_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> The name of the event to call._\
_`*args, **kwargs` -> Arguments to be passed to the function._
### **async** kt.call_async_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> The name of the event to call._\
_`*args, **kwargs` -> Arguments to be passed to the function._\
_Must be called with `await`_
###### _I recommend familiarizing yourself with *args, **kwargs_, there is a link at the beginning
Data is passed to all events in the form of: `{"event_name": event_name, "args": args, "kwargs": kwargs}`\
`args: list` -> Represents an array of data passed to the event\
`kwargs: dict` -> Represents a dictionary of data passed to the event
The data will be returned from all successful attempts in an array.
### kt.call_lua_event(event_name: str, *args) -> list:
_`event_name: str` -> The name of the event to call._\
_`*args` -> Arguments to be passed to the function._
Added to support backward compatibility.\
The lua function is called with a direct transmission of arguments `lua_func(*args)`
### kt.get_player([pid: int], [nick: str]) -> Player | None:
_`pid: int` -> Player ID - The identifier of the player._\
_`nick: str` -> Player Nickname - The name of the player._
The method returns a player object by their `pid` or `nick`.\
If the player cannot be found, `None` will be returned.
### kt.get_players() -> List[Player] | list:
The method returns an array with all players.\
The array will be empty if there are no players.
### kt.players_counter() -> int:
The method returns the number of players currently online.
### kt.is_player_connected([pid: int], [nick: str]) -> bool:
_`pid: int` -> Player ID - The identifier of the player._\
_`nick: str` -> Player Nickname - The name of the player._
The method returns a player object by their `pid` or `nick`.
## Player (or Client)
_`pl = kt.get_player()`_\
_`pl = event_data['kwargs']['player']`_
### pl.log -> Logger
_Constant_\
Returns a pre-configured logger
### pl.addr -> str
_Constant_\
Returns the IP address of the player
### pl.pid -> int
### pl.cid -> int
_Constant_\
Returns the client ID _(pid: PlayerId = cid: ClientId)_
### pl.key -> str
_Constant_\
Returns the key passed during authentication
### pl.nick -> str
_Variable_\
The nickname passed during authentication from the BeamMP server, can be changed, consequences are untested
### pl.roles -> str
_Variable_\
The role passed during authentication from the BeamMP server, can be changed (if an incorrect role is set, unexpected things may happen.)
### pl.guest -> bool
_Constant_\
Returns whether the player is a guest, passed during authentication from the BeamMP server
### pl.identifiers -> dict
_Constant_\
Identifiers passed during authentication from the BeamMP server.
### pl.ready -> bool
_Constant, changed by the core_\
Returns a bool value, if True -> the player has downloaded all resources, loaded on the map
### pl.cars -> dict
_Constant, changed by the core_\
Returns a dictionary of cars like thisSure, here's the translation:
# Passed Classes
## Worth looking at
@@ -147,7 +23,7 @@ Returns the directory of the plugin
_Parameters are the same as for open()_\
Opens a file in kt.dir
### kt.register_event(event_name: str, event_func: function)
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> The name of the event that `event_func` will be called on._\
_`event_func: function` -> The function that will be called._

View File

@@ -2,3 +2,12 @@
Most events will receive `pl = data['kwargs']['player']`, you can find a description [here](./classes.md)
* onPlayerJoin
* onPlayerDisconnect
* playerLoaded
* onChatReceive
* onCarSpawn
* onCarDelete
* onCarEdited
* onCarChanged
* ...

View File

@@ -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("Плагин загружен успешно.")

View File

@@ -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("Плагин загружен успешно.")

View File

@@ -31,7 +31,7 @@ def my_event_handler(event_data):
def load():
# Plugin initialization
kt.register_event("my_event", my_event_handler)
kt.register("my_event", my_event_handler)
log.info("Plugin loaded successfully.")
@@ -50,7 +50,7 @@ def unload():
A more comprehensive example can also be found in [example.py](examples/example.py)
* It is recommended to use `open()` after `load()`, otherwise use `kt.load()` - It creates a file in the `plugin/<plugin_name>/<filename>` folder.
* Creating your own event: `kt.register_event("my_event", my_event_function)` -
* Creating your own event: `kt.register("my_event", my_event_function)` -
* Calling an event: `kt.call_event("my_event")`
* Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
* Base events: _To be added later_
@@ -75,7 +75,7 @@ async def my_event_handler(event_data):
async def load():
# Plugin initialization
kt.register_event("my_event", my_event_handler)
kt.register("my_event", my_event_handler)
log.info("Plugin loaded successfully.")
@@ -94,7 +94,7 @@ async def unload():
A more comprehensive example can also be found in [async_example.py](examples/async_example.py)
* Creating your own event: `kt.register_event("my_event", my_event_function)` (register_event has a function check)
* Creating your own event: `kt.register("my_event", my_event_function)` (register has a function check)
* Calling an async event: `kt.call_async_event("my_event")`
* Calling an async event with data: `kt.call_async_event("my_event", data, data2=data2)`
* Base async events: _To be added later_

View File

@@ -34,7 +34,7 @@ Auth:
private: true
Game:
map: gridmap_v2
max_cars: 1
cars: 1
players: 8
Options:
debug: false
@@ -64,7 +64,7 @@ WebAPI:
### Game
* `map` is only the name of the map, i.e. open the mod with the map in `map.zip/levels` - the name of the map will be there, that's what we insert.
* `max_cars` - Maximum number of cars per player
* `cars` - Maximum number of cars per player
* `players` - Maximum number of players
### Options

View File

@@ -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

View File

@@ -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)\
Если есть желание перевести на не переведённый ранее язык, или обновить уже существующий перевод буду рад вашим пул реквестам.

View File

@@ -23,7 +23,7 @@ _Константа_\
араметры как у open()_\
Открывает файл в kt.dir
### kt.register_event(event_name: str, event_func: function)
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> Имя ивента, по которому будет вызвана `event_func`._\
_`event_func: function` -> Функция, которая будет вызвана._

View File

@@ -2,4 +2,12 @@
В большинство ивентов будет приходить `pl = data['kwargs']['player']`, описание можно найти [тут](./classes.md)
###
* onPlayerJoin
* onPlayerDisconnect
* playerLoaded
* onChatReceive
* onCarSpawn
* onCarDelete
* onCarEdited
* onCarChanged
* ...

View File

@@ -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("Плагин загружен успешно.")

View File

@@ -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("Плагин загружен успешно.")

View File

@@ -28,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("Плагин загружен успешно.")
@@ -46,7 +46,7 @@ def unload():
Так же более обширный пример можно найти в [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)`
* Базовые ивенты: озже напишу_
@@ -71,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("Плагин загружен успешно.")
@@ -90,7 +90,7 @@ async def unload():
Так же более обширный пример можно найти в [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 ивенты: озже напишу_

View File

@@ -34,7 +34,7 @@ Auth:
private: true
Game:
map: gridmap_v2
max_cars: 1
cars: 1
players: 8
Options:
debug: false
@@ -64,7 +64,7 @@ WebAPI:
### Game
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
* `max_cars` - Максимальное количество машин на игрока
* `cars` - Максимальное количество машин на игрока
* `players` - Максимально количество игроков
### Options

View File

@@ -1,14 +0,0 @@
# WebAPI для сервера
## Доступные endpoints
* `/stop`:
* Необходимые парамеры:
* `secret_key` - Ключ, который указан в конфигурации сервера
* `/event.get`
* Точка не готова
* Необходимые парамеры:
* `secret_key` - Ключ, который указан в конфигурации сервера

View File

@@ -1,10 +1,10 @@
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
Pygments~=2.18.0

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.3
# Core version: 0.4.8
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -9,6 +9,7 @@ import json
import math
import time
import zlib
from asyncio import Queue
from core import utils
@@ -18,9 +19,29 @@ class Client:
def __init__(self, reader, writer, core):
self.__reader = reader
self.__writer = writer
self.__Core = core
self._core = core
self.__alive = True
self.__packets_queue = []
self.__queue_tpc = Queue()
self.__queue_udp = Queue()
self._tpc_count_recv = 0
self._udp_count_recv = 0
self._tpc_count_total_recv = 0
self._udp_count_total_recv = 0
self._udp_size_total_recv = 0.1
self._tpc_size_total_recv = 0.1
# self._tpc_count_sent = 0
# self._udp_count_sent = 0
self._tpc_count_total_sent = 0
self._udp_count_total_sent = 0
self._udp_size_total_sent = 0.1
self._tpc_size_total_sent = 0.1
self.tcp_pps = 0
self.udp_pps = 0
self.__tasks = []
self._down_sock = (None, None)
self._udp_sock = (None, None)
@@ -32,18 +53,25 @@ class Client:
self.nick = None
self.roles = None
self._guest = True
self._synced = False
self._ready = False
self._identifiers = []
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
self._focus_car = -1
self._snowman = {"id": -1, "packet": ""}
self._unicycle = {"id": -1, "packet": ""}
self._connect_time = 0
self._last_position = {}
self._last_recv = time.monotonic()
self.__tpt_id = 0
@property
def _writer(self):
return self.__writer
@property
def alive(self):
return self.__alive
@property
def log(self):
return self._log
@@ -68,6 +96,10 @@ class Client:
def guest(self):
return self._guest
@property
def synced(self):
return self._synced
@property
def ready(self):
return self._ready
@@ -111,8 +143,7 @@ class Client:
if not self.__alive:
self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;")
return
# TODO: i18n
self.log.info(f"Kicked with reason: \"{reason}\"")
self.log.info(i18n.client_kicked.format(reason))
await self._send(f"K{reason}")
self.__alive = False
@@ -120,17 +151,24 @@ class Client:
if not message:
message = "no message"
to_all = False
await self._send(f"C:{message!r}", to_all=to_all)
if "\n" in message:
ms = message.split("\n")
for m in ms:
await self.send_message(m, to_all)
return
await self._send(f"C:{message}", to_all=to_all)
async def send_event(self, event_name, event_data, to_all=True):
async def send_event(self, event_name, event_data, to_all=False):
self.log.debug(f"send_event: {event_name}:{event_data}; {to_all=}")
if not self.ready:
self.log.debug(f"Client not ready. {event_data=}")
return
if isinstance(event_data, (list, tuple, dict)):
event_data = json.dumps(event_data, separators=(',', ':'))
else:
event_data = f"{event_data!r}"
if len(event_data) > 104857599:
self.log.error("Client data too big! >=104857599")
if len(event_data) > 99 * MB:
self.log.error(f"Error while preparing event {event_name!r}: data too big! data>99MB")
return
await self._send(f"E:{event_name}:{event_data}", to_all=to_all)
await self._send(f"E:{event_name}:{event_data}", to_all, True)
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
@@ -145,7 +183,7 @@ class Client:
if to_all:
code = chr(data[0])
for client in self.__Core.clients:
for client in self._core.clients:
if not client or (client is self and not to_self):
continue
if not to_udp or code in ['V', 'W', 'Y', 'E']:
@@ -164,13 +202,14 @@ class Client:
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
if to_udp:
udp_sock = self._udp_sock[0]
udp_addr = self._udp_sock[1]
udp_sock, udp_addr = self._udp_sock
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
if udp_sock and udp_addr:
try:
if not udp_sock.is_closing():
# self.log.debug(f'[UDP] {data!r}')
# self.log.debug(f'[UDP] {data!r}; {udp_addr}')
self._udp_count_total_sent += 1
self._udp_size_total_sent += len(data)
udp_sock.sendto(data, udp_addr)
except OSError:
self.log.debug("[UDP] Error sending")
@@ -180,14 +219,16 @@ class Client:
return
header = len(data).to_bytes(4, "little", signed=True)
# self.log.debug(f'[TCP] {header + data!r}')
data = header + data
# self.log.debug(f'[TCP] {data!r}')
try:
writer.write(header + data)
self._tpc_count_total_sent += 1
self._tpc_size_total_sent += len(data)
writer.write(data)
await writer.drain()
return True
except ConnectionError:
self.log.debug('[TCP] Disconnected')
except Exception as e:
self.log.debug(f'[TCP] Disconnected: {e}; {writer=}')
self.__alive = False
await self._remove_me()
return False
@@ -195,7 +236,13 @@ class Client:
async def _recv(self, one=False):
while self.__alive:
try:
header = await self.__reader.read(4)
header = b""
while len(header) < 4 and self.__alive:
h = await self.__reader.read(4)
if not h:
break
else:
header += h
int_header = int.from_bytes(header, byteorder='little', signed=True)
@@ -204,12 +251,11 @@ class Client:
self.is_disconnected()
if self.__alive:
if header == b"":
self.__packets_queue.append(None)
await self._tpc_put(None)
self.__alive = False
continue
self.log.error(f"Header: {header}")
await self.kick("Invalid packet - header negative")
self.__packets_queue.append(None)
continue
if int_header > 100 * MB:
@@ -217,12 +263,16 @@ class Client:
self.log.warning("Client sent header of >100MB - "
"assuming malicious intent and disconnecting the client.")
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
self.__packets_queue.append(None)
continue
data = await self.__reader.read(int_header)
data = b""
while len(data) < int_header and self.__alive:
buffer = await self.__reader.read(int_header - len(data))
if not buffer:
break
else:
data += buffer
# self.log.debug(f"int_header: {int_header}; data: `{data}`;")
abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):])
@@ -230,31 +280,39 @@ class Client:
if one:
return data
self.__packets_queue.append(data)
await self._tpc_put(data)
except ConnectionError:
self.__alive = False
self.__packets_queue.append(None)
await self._tpc_put(None)
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
real_size = end - start
size = end - start
writer = self._down_sock[1] if d_sock else self.__writer
who = 'dwn' if d_sock else 'srv'
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
who = 'DSock' if d_sock else 'MSock'
self.log.debug(f"[{who}] Started; start,end={(start, end)}")
with open(filename, 'rb') as f:
f.seek(start)
total_sent = 0
start_time = time.monotonic()
while total_sent < real_size:
data = f.read(min(MB, real_size - total_sent)) # read data in chunks of 1MB or less
while total_sent < size:
if (size - total_sent) == 0:
break
data = f.read(min(MB, size - total_sent)) # read data in chunks of 1MB or less
try:
writer.write(data)
async with asyncio.timeout(120): # ~100kb/s
await writer.drain()
self.log.debug(f"[{who}] Sent {len(data)} bytes.")
except ConnectionError:
# self.log.debug(f"[{who}] Sent {len(data)} bytes.")
except TimeoutError:
self.log.debug(f"[{who}] TimeoutError; Sock: {writer}")
self.log.error("TimeoutError")
self.__alive = False
break
except ConnectionError:
self.log.debug(f"[{who}] Disconnected; Sock: {writer}")
self.__alive = False
self.log.debug(f"[{who}] Disconnected.")
break
total_sent += len(data)
@@ -264,28 +322,30 @@ class Client:
expected_time = total_sent / (speed_limit * MB)
if expected_time > elapsed_time:
await asyncio.sleep(expected_time - elapsed_time)
self.log.debug(f"[{who}] Ready. {total_sent=}")
return total_sent
async def _sync_resources(self):
while self.__alive:
data = await self._recv(True)
if data is None:
await self._remove_me()
break
if data.startswith(b"f"):
file = data[1:].decode(config.enc)
# TODO: i18n
self.log.info(f"Requested mode: {file!r}")
self.log.info(i18n.client_mod_request.format(repr(file)))
size = -1
for mod in self.__Core.mods_list:
for mod in self._core.mods_list:
if type(mod) == int:
continue
if mod.get('path') == file:
size = mod['size']
self.log.debug("File is accept.")
break
self.log.debug(f"Mode size: {size}")
# self.log.debug(f"Mod size: {size}")
if size == -1:
await self._send(b"CO")
await self.kick(f"Not allowed mod: " + file)
await self.kick(f"Requested not allowed file: " + file)
return
await self._send(b"AG")
t = 0
@@ -293,45 +353,43 @@ class Client:
await asyncio.sleep(0.1)
t += 1
if t > 50:
await self.kick("Missing download socket")
await self.kick("Error (Missing DSock)")
return
if config.Options['use_queue']:
while self.__Core.lock_upload:
while self._core.lock_upload:
await asyncio.sleep(.2)
self.__Core.lock_upload = True
speed = config.Options["speed_limit"]
self._core.lock_upload = True
speed = config.Options["speed_limit"] or 25*B
if speed:
speed = speed / 2
half_size = math.floor(size / 2)
half_size = size // 2
t = time.monotonic()
uploads = [
self._split_load(0, half_size, False, file, speed),
self._split_load(half_size, size, True, file, speed)
]
sl0, sl1 = await asyncio.gather(*uploads)
tr = time.monotonic() - t
if self.__Core.lock_upload:
self.__Core.lock_upload = False
# TODO: i18n
msg = f"Mod sent: Size {round(size / MB, 3)}mb Speed {math.ceil(size / tr / MB)}Mb/s ({int(tr)}s)"
self.log.debug(f"Sending: {size=}; sl0={(0, half_size)}; sl1={(half_size, size)}")
async with asyncio.TaskGroup() as tg:
sl0 = tg.create_task(self._split_load(0, half_size, False, file, speed))
sl1 = tg.create_task(self._split_load(half_size, size, True, file, speed))
sl0, sl1 = sl0.result(), sl1.result()
tr = (time.monotonic() - t) or 0.0001
if self._core.lock_upload:
self._core.lock_upload = False
msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
if speed:
msg += f" of limit {int(speed * 2)}Mb/s"
msg += i18n.client_mod_sent_limit.format(int(speed * 2))
self.log.info(msg)
sent = sl0 + sl1
ok = sent == size
lost = size - sent
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
self.log.debug(f"Sent; sl_0: {sl0}; sl_1: {sl1}; size==sent is {ok}: {size}-{sent}={lost}")
if not ok:
self.__alive = False
# TODO: i18n
self.log.error(f"Error while sending: {file!r}")
e = i18n.client_mod_sent_error.format(repr(file))
await self._send(f"E{e}")
self.log.error(e)
return
elif data.startswith(b"SR"):
path_list = ''
size_list = ''
for mod in self.__Core.mods_list:
if type(mod) == int:
continue
for mod in self._core.mods_list[1:]:
path_list += f"{mod['path']};"
size_list += f"{mod['size']};"
mod_list = path_list + size_list
@@ -341,6 +399,7 @@ class Client:
else:
await self._send(mod_list)
elif data == b"Done":
self.log.debug("recv Done")
await self._send(f"M/levels/{config.Game['map']}/info.json")
break
return
@@ -351,7 +410,7 @@ class Client:
id_sep = s.find('-')
if id_sep == -1:
self.log.debug(
f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}'")
f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}', {s}")
return -1, -1
cid = s[:id_sep]
vid = s[id_sep + 1:]
@@ -371,7 +430,7 @@ class Client:
car_data = data[2:]
car_id = next((i for i, car in enumerate(self._cars) if car is None), len(self._cars))
cars_count = len(self._cars) - self._cars.count(None)
if self._snowman['id'] != -1:
if self._unicycle['id'] != -1:
cars_count -= 1 # -1 for unicycle
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
car_json = {}
@@ -380,27 +439,26 @@ class Client:
except Exception as e:
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
allow = True
allow_snowman = True
allow_unicycle = True
over_spawn = False
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
if 1 in lua_data:
allow = False
ev_data_list = ev.call_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
event_data = await ev.call_as_events("onCarSpawn", data=car_json, car_id=car_id, player=self)
for ev_data in event_data:
self.log.debug(ev_data)
# TODO: handle event onCarSpawn
pass
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
snowman = car_json.get("jbm") == "unicycle"
if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn:
if snowman:
unicycle_id = self._snowman['id']
unicycle = car_json.get("jbm") == "unicycle"
if allow and config.Game['cars'] > cars_count or (unicycle and allow_unicycle) or over_spawn:
if unicycle:
unicycle_id = self._unicycle['id']
if unicycle_id != -1:
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}")
self.log.debug(f"Delete old unicycle: car_id={unicycle_id}")
self._cars[unicycle_id] = None
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._snowman = {"id": car_id, "packet": pkt}
self._unicycle = {"id": car_id, "packet": pkt}
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
else:
self.log.debug(f"Car spawn accepted: car_id={car_id}")
@@ -409,11 +467,14 @@ class Client:
"packet": pkt,
"json": car_json,
"json_ok": bool(car_json),
"snowman": snowman,
"over_spawn": (snowman and allow_snowman) or over_spawn,
"unicycle": unicycle,
"over_spawn": (unicycle and allow_unicycle) or over_spawn,
"pos": {}
}
await self._send(pkt, to_all=True, to_self=True)
if self.focus_car == -1:
self._focus_car = car_id
await ev.call_as_events("onCarSpawned", data=car_json, car_id=car_id, player=self)
else:
await self._send(pkt)
des = f"Od:{self.cid}-{car_id}"
@@ -432,35 +493,33 @@ class Client:
if car_id != -1 and self._cars[car_id]:
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
admin_allow = False # Delete from admin, for example...
ev_data_list = ev.call_event("onCarDelete", car=self._cars[car_id], car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarDelete", car=self._cars[car_id], car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
event_data = await ev.call_as_events("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
for ev_data in event_data:
self.log.debug(ev_data)
# TODO: handle event onCarDelete
pass
if cid == self.cid or admin_allow:
await self._send(raw_data, to_all=True, to_self=True)
car = self._cars[car_id]
if car['snowman']:
self.log.debug(f"Snowman found")
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
if car['unicycle']:
self.log.debug("unicycle found")
unicycle_id = self._unicycle['id']
self._unicycle['id'] = -1
self._cars[unicycle_id] = None
self._cars[car_id] = None
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
await ev.call_as_events("onCarDeleted", data=self._cars[car_id], car_id=car_id, player=self)
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
self.log.debug(f"Deleted car: car_id={car_id}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def _edit_car(self, raw_data, data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and self._cars[car_id]:
client = self.__Core.get_client(cid=cid)
client = self._core.get_client(cid=cid)
if client:
car = client._cars[car_id]
new_car_json = {}
@@ -474,18 +533,19 @@ class Client:
lua_data = ev.call_lua_event("onVehicleEdited", self.cid, car_id, data[data.find("{"):])
if 1 in lua_data:
allow = False
ev_data_list = ev.call_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
ev_data_list = ev.call_event("onCarEdited", data=new_car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarEdited", data=new_car_json, car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
self.log.debug(ev_data)
# TODO: handle event onCarEdited
pass
if cid == self.cid or allow or admin_allow:
if car['snowman']:
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
self.log.debug(f"Delete snowman")
if car['unicycle']:
unicycle_id = self._unicycle['id']
self._unicycle['id'] = -1
self.log.debug(f"Delete unicycle")
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._cars[unicycle_id] = None
else:
@@ -498,6 +558,13 @@ class Client:
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def reset_car(self, car_id, x, y, z, rot=None):
self.log.debug(f"Resetting car from plugin {x, y, z}; {rot=}")
jpkt = {"pos": {"y": float(y), "x": float(x), "z": float(z)}, "rot": {"y": 0, "w": 0, "x": 0, "z": 0}}
if rot:
jpkt['rot'] = rot
await self._send(f"Or:{self.cid}-{car_id}:{json.dumps(jpkt)}", True)
async def _reset_car(self, raw_data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and cid == self.cid and self._cars[car_id]:
@@ -508,8 +575,8 @@ class Client:
car_json = json.loads(raw_data[raw_data.find("{"):])
except Exception as e:
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {raw_data}")
ev.call_event("onCarReset", car=car_json, car_id=car_id, player=self)
await ev.call_async_event("onCarReset", car=car_json, car_id=car_id, player=self)
ev.call_event("onCarReset", data=car_json, car_id=car_id, player=self)
await ev.call_async_event("onCarReset", data=car_json, car_id=car_id, player=self)
self.log.debug(f"Car reset: car_id={car_id}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
@@ -517,6 +584,7 @@ class Client:
async def _handle_car_codes(self, raw_data):
if len(raw_data) < 6:
return
self.log.debug(f"[car] {raw_data}")
sub_code = raw_data[1]
data = raw_data[3:]
match sub_code:
@@ -539,25 +607,32 @@ class Client:
case "t": # Broken details
self.log.debug(f"Something changed/broken: {raw_data}")
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and cid == self.cid and self._cars[car_id]:
data = raw_data[raw_data.find("{"):]
ev.call_event("onCarChanged", car_id=car_id, data=data)
await ev.call_async_event("onCarChanged", car_id=car_id, data=data)
await self._send(raw_data, to_all=True, to_self=False)
case "m": # Move focus car
self.log.debug(f"Move focus to: {raw_data}")
cid, car_id = self._get_cid_vid(raw_data[5:])
cid, car_id = self._get_cid_vid(raw_data[3:])
if car_id != -1 and cid == self.cid and self._cars[car_id]:
self._focus_car = car_id
data = raw_data[raw_data.find("{"):]
ev.call_event("onCarFocusMove", car_id=car_id, data=data)
await ev.call_async_event("onCarFocusMove", car_id=car_id, data=data)
await self._send(raw_data, to_all=True, to_self=True)
async def _connected_handler(self):
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s")
# Client connected
ev.call_event("onPlayerJoin", player=self)
await ev.call_async_event("onPlayerJoin", player=self)
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
await self._send(f"JWelcome {self.nick}!", to_all=True) # Hello message
await self._send(f"J{i18n.game_welcome_message.format(self.nick)}", to_all=True) # Hello message
for client in self.__Core.clients:
for client in self._core.clients:
if not client:
continue
for car in client._cars:
@@ -565,7 +640,11 @@ class Client:
continue
await self._send(car['packet'])
self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2)))
self._ready = True
self._synced = True
ev.call_event("onPlayerReady", player=self)
await ev.call_async_event("onPlayerReady", player=self)
async def _chat_handler(self, data):
sup = data.find(":", 2)
@@ -575,74 +654,165 @@ class Client:
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
return
to_ev = {"message": msg, "player": self}
lua_data = ev.call_lua_event("onChatMessage", self.cid, self.nick, msg)
if 1 in lua_data:
if config.Options['log_chat']:
self.log.info(f"{self.nick}: {msg}")
return
ev_data_list = ev.call_event("onChatReceive", **to_ev)
d2 = await ev.call_async_event("onChatReceive", **to_ev)
ev_data_list.extend(d2)
event_data = await ev.call_as_events("onChatReceive", message=msg, player=self)
need_send = True
for ev_data in ev_data_list:
for event in event_data:
if event is None:
continue
try:
message = ev_data["message"]
to_all = ev_data.get("to_all")
if to_all is None:
to_all = True
to_self = ev_data.get("to_self")
if to_self is None:
to_self = True
to_client = ev_data.get("to_client")
writer = None
to_all = True
to_self = True
message = f"{self.nick}: {msg}"
match event:
case False | 0:
need_send = False
continue
case {"message": message, **setting}:
message = message
to_all = setting.get("to_all", True)
to_self = setting.get("to_self", True)
to_client = setting.get("to")
if to_client:
# noinspection PyProtectedMember
writer = to_client._writer
case _:
self.log.error(f"[onChatReceive] Bad data returned from event: {event}")
if config.Options['log_chat']:
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}")
self.log.info(f"[local] {message}" if not to_all else message)
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False
except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}")
except KeyError:
self.log.error(i18n.client_event_invalid_data.format(event))
except AttributeError:
self.log.error(i18n.client_event_invalid_data.format(event))
if need_send:
if config.Options['log_chat']:
self.log.info(f"{self.nick}: {msg}")
await self._send(data, to_all=True)
async def _handle_codes(self, data):
if not data:
async def _handle_codes_tcp(self, data):
if data is None:
self.__alive = False
return
# Codes: V W X Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
if len(data) == 0:
await self.kick("Bad data from client")
return
_bytes = False
try:
data = data.decode()
except UnicodeDecodeError:
self.log.error(f"UnicodeDecodeError: {data}")
_bytes = True
self.log.error("UnicodeDecodeError")
if data[0] in ['V', 'W', 'Y', 'E', 'N']:
await self._send(data, to_all=True, to_self=False)
return
# Codes: p, Z in udp_server.py
# Codes: p, Z, X in udp_server.py
match data[0]: # At data[0] code
case "H": # Map load, client ready
await self._connected_handler()
case "C": # Chat handler
if _bytes:
return
await self._chat_handler(data)
case "O": # Cars handler
if _bytes:
return
await self._handle_car_codes(data)
case "E": # Client events handler
# TODO: Handle events from client
pass
if len(data) < 2:
self.log.debug("Tried to send an empty event, ignoring.")
return
if _bytes:
sep = data.find(b":", 2)
self.log.warning("Bytes event!")
else:
sep = data.find(":", 2)
if sep == -1:
self.log.error(f"Received event in invalid format (missing ':'), got: {data}")
event_name = data[2:sep]
even_data = data[sep + 1:]
ev.call_lua_event(event_name, self.cid, even_data)
ev.call_event(event_name, data=even_data, player=self)
await ev.call_async_event(event_name, data=even_data, player=self)
case _:
self.log.warning(f"TCP Unknown code: {data[0]}; {data}")
case "N":
await self._send(data, to_all=True, to_self=False)
async def _handle_codes_udp(self, data):
code = data[2:3].decode()
data = data[2:].decode()
match code:
case "p": # Ping packet
ev.call_event("onSentPing", player=self)
await self._send(b"p", to_udp=True)
case "Z": # Position packet
sub = data.find("{", 1)
last_pos = data[sub:]
try:
_, car_id = self._get_cid_vid(data)
if self._cars[car_id]:
last_pos = json.loads(last_pos)
self._last_position = last_pos
self._cars[car_id]['pos'] = last_pos
ev.call_event("onChangePosition", data, player=self, pos=last_pos)
except Exception as e:
self.log.warning(f"Cannot parse position packet: {e}")
self.log.debug(f"data: '{data}', sub: {sub}")
self.log.debug(f"last_pos ({type(last_pos)}): {last_pos}")
await self._send(data, True, False, True)
case "X":
await self._send(data, True, False, True)
case _:
self.log.warning(f"UDP Unknown code: {code}; {data}")
def _tick_pps(self, _):
self.tcp_pps = self._tpc_count_recv
self.udp_pps = self._udp_count_recv
self._tpc_count_recv = 0
self._udp_count_recv = 0
if self.tcp_pps > self._core.target_tps or self.udp_pps > self._core.target_tps:
self.log.warning(f"PPS > TPS; PPS: TPC: {self.tcp_pps}, UDP: {self.udp_pps}")
async def __tick_player_tcp(self, _):
try:
if self.__queue_tpc.qsize() > 0:
packet = await self.__queue_tpc.get()
if packet is None:
return await self._remove_me()
await self._handle_codes_tcp(packet)
except Exception as e:
self.log.error(f'[TPC] Error while ticking player:')
self.log.exception(e)
async def __tick_player_udp(self, _):
try:
if self.__queue_udp.qsize() > 0:
packet = await self.__queue_udp.get()
await self._handle_codes_udp(packet)
except Exception as e:
self.log.error(f'[UDP] Error while ticking player:')
self.log.exception(e)
async def _tpc_put(self, packet):
if packet:
self._tpc_count_recv += 1
self._tpc_count_total_recv += 1
self._tpc_size_total_recv += len(packet)
await self.__queue_tpc.put(packet)
async def _udp_put(self, packet):
self._udp_count_recv += 1
self._udp_count_total_recv += 1
self._udp_size_total_recv += len(packet)
await self.__queue_udp.put(packet)
async def _looper(self):
ev.call_lua_event("onPlayerConnecting", self.cid)
@@ -650,52 +820,52 @@ class Client:
await self._send(f"P{self.cid}") # Send clientID
await self._sync_resources()
ev.call_lua_event("onPlayerJoining", self.cid)
tasks = self.__tasks
recv = asyncio.create_task(self._recv())
tasks.append(recv)
while self.__alive:
if len(self.__packets_queue) > 0:
for index, packet in enumerate(self.__packets_queue):
# self.log.debug(f"Packet: {packet}")
del self.__packets_queue[index]
task = self._loop.create_task(self._handle_codes(packet))
tasks.append(task)
else:
await asyncio.sleep(0.1)
await asyncio.gather(*tasks)
self.__tpt_id = ev.register("serverTick", self.__tick_player_tcp)
self.__tpu_id = ev.register("serverTick", self.__tick_player_udp)
self.__tpp_id = ev.register("serverTick_1s", self._tick_pps)
await self._recv()
async def _remove_me(self):
await asyncio.sleep(0.3)
self.__alive = False
if (self.cid > 0 or self.nick is not None) and \
self.__Core.clients_by_nick.get(self.nick):
if self._core.clients_by_nick.get(self.nick):
for i, car in enumerate(self._cars):
if not car:
continue
self.log.debug(f"Removing car: car_id={i}")
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
if self.ready:
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected.
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False)
self.log.debug(f"Removing client")
ev.call_lua_event("onPlayerDisconnect", self.cid)
ev.call_event("onPlayerDisconnect", player=self)
await ev.call_async_event("onPlayerDisconnect", player=self)
# TODO: i18n
self.log.info(f"Disconnected, online time: {round((time.monotonic() - self._connect_time) / 60, 2)}min.")
self.__Core.clients[self.cid] = None
del self.__Core.clients_by_id[self.cid]
del self.__Core.clients_by_nick[self.nick]
if self.__tpt_id:
ev.unregister_by_id(self.__tpt_id) # self.__tick_player_tcp
ev.unregister_by_id(self.__tpu_id) # self.__tick_player_udp
ev.unregister_by_id(self.__tpp_id) # self._tick_pps
gt = round((time.monotonic() - self._connect_time) / 60, 2)
self.log.info(i18n.client_player_disconnected.format(gt))
self._core.clients[self.cid] = None
del self._core.clients_by_id[self.cid]
del self._core.clients_by_nick[self.nick]
self.log.debug(f"TPC: "
f"Recv: {self._tpc_count_total_recv}; {self._tpc_size_total_recv / KB:.4f}kb; "
f"Sent: {self._tpc_count_total_sent}; {self._tpc_size_total_sent / KB:.4f}kb;")
self.log.debug(f"UDP: "
f"Recv: {self._udp_count_total_recv}; {self._udp_size_total_recv / KB:.4f}kb; "
f"Sent: {self._udp_count_total_sent}; {self._udp_size_total_sent / KB:.4f}kb;")
else:
self.log.debug(f"Removing client; Closing connection...")
await asyncio.sleep(0.001)
try:
if not self.__writer.is_closing():
self.__writer.close()
await self.__writer.wait_closed()
except Exception as e:
self.log.debug(f"Error while closing writer: {e}")
try:
_, down_w = self._down_sock
if down_w and not down_w.is_closing():
down_w.close()
await down_w.wait_closed()
except Exception as e:
self.log.debug(f"Error while closing download writer: {e}")

View File

@@ -1,11 +1,11 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.3
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport
from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock, Queue
from logging import Logger
from typing import Tuple, List, Dict, Optional, Union, Any
@@ -19,29 +19,52 @@ class Client:
self.__tasks = []
self.__reader = reader
self.__writer = writer
self.__packets_queue = []
self.__queue_tpc = Queue()
self.__queue_udp = Queue()
self._tpc_count_recv = 0
self._udp_count_recv = 0
self._tpc_count_total_recv = 0
self._udp_count_total_recv = 0
self._udp_size_total_recv = 0.1
self._tpc_size_total_recv = 0.1
# self._tpc_count_sent = 0
# self._udp_count_sent = 0
self._tpc_count_total_sent = 0
self._udp_count_total_sent = 0
self._udp_size_total_sent = 0.1
self._tpc_size_total_sent = 0.1
self.tcp_pps = 0
self.udp_pps = 0
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
self._log = utils.get_logger("client(id: )")
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
self._loop = asyncio.get_event_loop()
self.__Core: Core = core
self._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[Optional[Dict[str, int]]] = []
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
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()
self.__tpt_id = 0
self.__tpu_id = 0
self.__tpp_id = 0
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]: ...
@@ -53,6 +76,8 @@ class Client:
@property
def guest(self) -> bool: ...
@property
def synced(self) -> bool: ...
@property
def ready(self) -> bool: ...
@property
def identifiers(self) -> list: ...
@@ -66,11 +91,11 @@ class Client:
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
async def send_event(self, event_name: str, event_data: Any, to_all: bool = True) -> None: ...
async def send_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, one=False) -> bytes | None: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> int: ...
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
async def _spawn_car(self, data: str) -> None: ...
async def delete_car(self, car_id: int) -> None: ...
@@ -80,7 +105,13 @@ class Client:
async def _handle_car_codes(self, data: str) -> None: ...
async def _connected_handler(self) -> None: ...
async def _chat_handler(self, data: str) -> None: ...
async def _handle_codes(self, data: bytes) -> None: ...
async def _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: ...

View File

@@ -1,24 +1,25 @@
# Developed by KuiToi Dev
# File core.__init__.py
# Written by: SantaSpeen
# Version 1.4
# Core version: 0.4.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.4.3'
__build__ = 2125 # Я это считаю лог файлами
__version__ = '0.4.8'
__build__ = 2800 # Я это считаю лог файлами
__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 time
import webbrowser
import prompt_toolkit.shortcuts as shortcuts
@@ -30,12 +31,16 @@ from modules import ConfigProvider, EventsSystem
from modules import Console
from modules import MultiLanguage
args = parser.parse_args()
builtins.Ts = time.monotonic()
args, _ = parser.parse_known_args()
if args.version:
print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}")
exit(0)
loop = asyncio.new_event_loop()
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# loop.set_task_factory(asyncio.eager_task_factory)
asyncio.set_event_loop(loop)
log = get_logger("core.init")
@@ -44,14 +49,13 @@ 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
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.info("Debug mode enabled!")
log.debug(f"Server config: {config}")
# i18n init
log.debug("Initializing i18n...")
@@ -62,6 +66,7 @@ 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 +75,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 +97,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)

View File

@@ -1,31 +1,44 @@
# Developed by KuiToi Dev
# File core.core.py
# Written by: SantaSpeen
# Version: 0.4.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 = []
@@ -37,24 +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.1.1" # 20.07.2023
self.BeamMP_version = "3.4.1" # 16.07.2024
ev.register_event("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
ev.register_event("_get_player", lambda x: self.get_client(**x['kwargs']))
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):
if cid is None and nick is None:
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]
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)
@@ -99,21 +117,30 @@ class Core:
out = out[:-1]
return out
async def check_alive(self):
maxp = config.Game['players']
async def _check_alive(self, _):
# self.log.debug("alive checker.")
try:
while self.run:
await asyncio.sleep(1)
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
for client in self.clients:
if not client:
continue
if not client.ready:
client.is_disconnected()
continue
if not client.alive:
await client.kick("You are not alive!")
except Exception as e:
self.log.error("Error in _check_alive.")
self.log.exception(e)
async def _send_online(self, _):
try:
for client in self.clients:
ca = f"Ss{len(self.clients_by_id)}/{config.Game['players']}:{self.get_clients_list()}"
if not client or not client.alive or not client.ready:
continue
await client._send(ca)
except Exception as e:
self.log.error("Error in check_alive.")
self.log.error("Error in _send_online.")
self.log.exception(e)
async def __gracefully_kick(self):
@@ -122,50 +149,57 @@ class Core:
continue
await client.kick("Server shutdown!")
@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 stop_me(self):
while webapp.data_run[0]:
await asyncio.sleep(1)
self.run = False
raise KeyboardInterrupt
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):
try:
self.log.debug("Starting heartbeat.")
if config.Auth["private"] or self.direct:
if test:
# TODO: i18n
self.log.info(f"Server runnig in Direct connect mode.")
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'])};"
_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:
try:
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id),
"maxplayers": config.Game["players"], "port": config.Server["server_port"],
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'],
"version": self.BeamMP_version, "clientversion": self.client_major_version,
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
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"
@@ -173,83 +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()
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
status = body.get("status")
msg = body.get("msg")
if status == "2000":
if test:
# TODO: i18n
self.log.info(f"Authenticated! {msg}")
self.log.debug(f"Authenticated! {msg}")
elif status == "200":
if test:
self.log.info(f"Resumed authenticated session. {msg}")
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(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.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:
# TODO: i18n
self.log.error("Cannot authenticate server.")
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)
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)
pl_dir = "plugins"
self.log.debug("Initializing PluginsLoaders...")
if not os.path.exists(pl_dir):
os.mkdir(pl_dir)
pl = PluginsLoader(pl_dir)
await pl.load()
if config.Options['use_lua']:
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
lpl = LuaPluginsLoader(pl_dir)
lpl.load()
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):
@@ -263,22 +418,27 @@ 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, self.udp._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)
await ev.call_async_event("_plugins_start")
# await ev.call_async_event("_lua_plugins_start")
self.run = True
self.log.info(i18n.start)
@@ -288,26 +448,35 @@ class Core:
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_lua_event("onShutdown")
ev.call_event("onServerStopped")
await ev.call_async_event("onServerStopped")
await self.__gracefully_kick()
await ev.call_async_event("_plugins_unload")
ev.call_event("_lua_plugins_unload")
self.run = False
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)
if config.WebAPI["enabled"]:
asyncio.run(self.web_stop())
# exit(0)
except Exception as e:
self.log.error("Error while stopping server:")
self.log.exception(e)

View File

@@ -1,10 +1,11 @@
# Developed by KuiToi Dev
# File core.core.pyi
# Written by: SantaSpeen
# Core version: 0.4.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
@@ -34,17 +39,22 @@ class Core:
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: ...
async def _check_alive(self) -> None: ...
async def _send_online(self) -> None: ...
async def _useful_ticks(self, _) -> None: ...
async def __gracefully_kick(self): ...
@staticmethod
def start_web() -> None: ...
def stop_me(self) -> None: ...
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: ...

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.3
# Core version: 0.4.8
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -10,6 +10,7 @@ import traceback
import aiohttp
from core import utils
from modules import RateLimiter
# noinspection PyProtectedMember
@@ -21,25 +22,27 @@ 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...")
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(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("onPlayerSentKey", player=client)
@@ -50,53 +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"]
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)
allow = True
reason = "You are not allowed on the server!"
reason = i18n.core_player_kick_no_allowed_default_reason
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
for data in lua_data:
if 1 == data:
allow = True
allow = False
elif isinstance(data, str):
allow = True
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
@@ -109,7 +116,7 @@ class TCPServer:
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
@@ -119,59 +126,55 @@ class TCPServer:
result, client = await self.auth_client(reader, writer)
if result:
await client._looper()
return result, client
return "U", client
case "D":
await self.set_down_rw(reader, writer)
return "D", None
case "P":
writer.write(b"P")
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
return "E", None
async def handle_client(self, reader, writer):
while True:
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()
data = await reader.read(1)
if not data:
break
return
code = data.decode()
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
# 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)
_type, cl = await self.handle_code(code, reader, writer)
self.log.debug(f"[{_type}] cl returned: {cl}")
if cl:
await cl._remove_me()
del cl
break
except Exception as e:
# TODO: i18n
self.log.error("Error while handling connection...")
self.log.exception(e)
traceback.print_exc()
break
async def start(self):
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"] * 2.3))
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}")
while True:
async with server:
await server.serve_forever()
self.server = await asyncio.start_server(self.handle_client, self.host, self.port,
backlog=int(config.Game["players"] * 4))
async with self.server:
self.log.debug(f"TCP server started on {self.server.sockets[0].getsockname()!r}")
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 Exception as e:
self.log.error(f"Error: {e}")
self.log.exception(e)
raise e
finally:
self.run = False
@@ -179,3 +182,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.")

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.tcp_server.pyi
# Written by: SantaSpeen
# Core version: 0.4.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]: ...

View File

@@ -1,11 +1,12 @@
# Developed by KuiToi Dev
# File core.udp_server.py
# File core.udp_server
# Written by: SantaSpeen
# Core version: 0.4.3
# Core version: 0.4.7
# Licence: FPA
# (c) kuitoi.su 2023
# (c) kuitoi.su 2024
import asyncio
import json
import time
from core import utils
@@ -18,7 +19,7 @@ class UDPServer(asyncio.DatagramTransport):
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
@@ -27,42 +28,25 @@ class UDPServer(asyncio.DatagramTransport):
def pause_writing(self, *args, **kwargs): ...
def resume_writing(self, *args, **kwargs): ...
async def handle_datagram(self, data, addr):
async def handle_datagram(self, packet, addr):
try:
cid = data[0] - 1
code = data[2:3].decode()
data = data[2:].decode()
client = self.Core.get_client(cid=cid)
cid = packet[0] - 1
if cid > config.Game['players'] * 4:
return
client = self._core.get_client(cid=cid)
if client:
match code:
case "p": # Ping packet
ev.call_event("onSentPing")
self.transport.sendto(b"p", addr)
case "Z": # Position packet
if not client.alive:
client.log.debug(f"Still sending UDP data: {packet}")
if client._udp_sock != (self.transport, addr):
self.log.debug(f"udp_addr={addr[0]}; main_addr={client.addr}")
if addr[0] != client.addr:
self.log.warning(f"udp_addr != main_addr. Is this bug?")
client._udp_sock = (self.transport, addr)
self.log.debug(f"Set UDP Sock for CID: {cid}")
ev.call_event("onChangePosition", data=data)
sub = data.find("{", 1)
last_pos_data = data[sub:]
try:
last_pos = json.loads(last_pos_data)
client._last_position = last_pos
_, car_id = client._get_cid_vid(data)
client._cars[car_id]['pos'] = last_pos
except Exception as e:
self.log.debug(f"Cannot parse position packet: {e}")
self.log.debug(f"data: {data}, sup: {sub}")
self.log.debug(f"last_pos_data: {last_pos_data}")
await client._send(data, to_all=True, to_self=False, to_udp=True)
case _:
self.log.debug(f"[{cid}] Unknown code: {code}")
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 datagram_received(self, *args, **kwargs):
@@ -81,14 +65,13 @@ class UDPServer(asyncio.DatagramTransport):
async def _start(self):
self.log.debug("Starting UDP server.")
while self.Core.run:
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),
lambda: d(self._core),
local_addr=(self.host, self.port)
)
d.transport = self.transport
@@ -101,7 +84,7 @@ class UDPServer(asyncio.DatagramTransport):
except OSError as e:
self.run = False
self.Core.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}")
@@ -109,4 +92,5 @@ class UDPServer(asyncio.DatagramTransport):
def _stop(self):
self.log.debug("Stopping UDP server")
if self.transport:
self.transport.close()

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.udp_server.py
# Written by: SantaSpeen
# Core version: 0.4.3
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -18,13 +18,13 @@ class UDPServer(asyncio.DatagramTransport):
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
# 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: ...

View File

@@ -2,7 +2,7 @@
# File core.utils.py
# Written by: SantaSpeen
# Version 1.1
# Core version: 0.4.3
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import datetime

View File

@@ -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)

View File

@@ -1,17 +0,0 @@
import secrets
class Config:
def __init__(self, auth=None, game=None, server=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
"server_ip": "0.0.0.0", "server_port": 30814}
self.Options = options or {"language": "en", "encoding": "utf8", "speed_limit": 0, "use_queue": False,
"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 config (Config): ...

View File

@@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.config_provider.config_provider.py
# Written by: SantaSpeen
# Version 1.1
# Licence: FPA
# (c) kuitoi.su 2023
import os
import secrets
import yaml
class Config:
def __init__(self, auth=None, game=None, server=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
"server_ip": "0.0.0.0", "server_port": 30814}
self.Options = options or {"language": "en", "encoding": "utf-8", "speed_limit": 0, "use_queue": False,
"debug": False, "use_lua": False, "log_chat": True}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"secret_key": secrets.token_hex(16)}
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)

View 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): ...
```

View 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

View File

@@ -1,9 +1,394 @@
# -*- 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
@property
def legacy_mode(self):
return self.__legacy_mode
def __debug(self, *x):
self.__logger.debug(' '.join(x))
# if self.__is_debug:
# x = list(x)
# x.insert(0, "\r CONSOLE DEBUG:")
# self.__print(*x)
def __getitem__(self, item):
print(item)
@staticmethod
def __get_max_len(arg) -> int:
i = 0
arg = list(arg)
for a in arg:
ln = len(str(a))
if ln > i:
i = ln
return i
def __create_man_message(self, argv: list) -> AnyStr:
if len(argv) == 0:
return self.__man.get("man")
x = argv[0]
if x not in self.__alias:
return i18n.man_command_not_found.format(x)
return self.__man.get(x)
# noinspection PyStringFormat
def __create_help_message(self, argv: list) -> AnyStr:
self.__debug("creating help message")
raw = False
max_len_v = 0
if "--raw" in argv:
max_len_v = self.__get_max_len(self.__func.values())
print()
raw = True
message = "\n"
max_len = self.__get_max_len(self.__func.keys())
if max_len < 7:
max_len = 7
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
else:
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
for k, v in self.__func.items():
doc = self.__desc.get(k)
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
else:
if doc is None:
doc = i18n.help_message_not_found
message += f" %-{max_len}s : %s\n" % (k, doc)
return message
def del_command(self, func):
self.__debug(f"delete command: func={func};")
keys = []
for k, v in self.__func.items():
if v['f'] is func:
keys.append(k)
for key in keys:
self.__debug(f"{key=}")
self.__alias.pop(key)
self.__alias["man"].pop(key)
self.__func.pop(key)
self.__man.pop(key)
self.__desc.pop(key)
if keys:
self.__debug("Deleted.")
self.completer.load(self.__alias)
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
if not isinstance(key, str):
raise TypeError("key must be string")
key = key.replace(" ", "-")
self.__debug(f"added user command: key={key}; func={func};")
self.__alias.update(custom_completer or {key: None})
self.__alias["man"].update({key: None})
self.__func.update({key: {"f": func}})
self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man if man else "No page"}</seagreen>'})
self.__desc.update({key: desc})
self.completer.load(self.__alias)
return self.__alias.copy()
def _write(self, text):
# https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html#formatted-text
if self.__legacy_mode:
print(text)
return
assert isinstance(text, str)
_type = text.split(":")[0]
match _type:
case "html":
print_formatted_text(HTML(text[5:]))
case "ansi":
print_formatted_text(ANSI(text[5:]))
case _:
print_formatted_text(text)
def write(self, s: AnyStr):
if isinstance(s, (list, tuple)):
for text in s:
self._write(text)
else:
self._write(s)
def log(self, s: AnyStr) -> None:
# if isinstance(s, (list, tuple)):
# for text in s:
# self.__logger.info(f"{text}")
# else:
# self.__logger.info(f"{s}")
self.write(s)
def __lshift__(self, s: AnyStr) -> None:
self.write(s)
@property
def alias(self) -> dict:
return self.__alias.copy()
def __builtins_print(self,
*values: object,
sep: str or None = " ",
end: str or None = None,
file: str or None = None,
flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__run}")
val = list(values)
if len(val) > 0:
if self.__run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else:
if end is None:
end = "\n"
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
def logger_hook(self) -> None:
self.__debug("used logger_hook")
def emit(cls, record):
try:
msg = cls.format(record)
if cls.stream.name == "<stderr>":
self.write(f"\r{msg}")
else:
cls.stream.write(msg + cls.terminator)
cls.flush()
except RecursionError:
raise
except Exception as e:
print(e)
cls.handleError(record)
logging.StreamHandler.emit = emit
def builtins_hook(self) -> None:
self.__debug("used builtins_hook")
builtins.Console = Console
builtins.console = self
# builtins.print = self.__builtins_print
async def _parse_input(self, inp):
cmd_s = inp.split(" ")
cmd = cmd_s[0]
if cmd == "":
return True
else:
found_in_lua = False
d = ev.call_lua_event("onConsoleInput", inp)
if len(d) > 0:
for text in d:
if text is not None:
found_in_lua = True
self.log(text)
command_object = self.__func.get(cmd)
if command_object:
func = command_object['f']
if inspect.iscoroutinefunction(func):
out = await func(cmd_s[1:])
else:
out = func(cmd_s[1:])
if out:
self.log(out)
else:
if not found_in_lua:
self.log(self.__not_found % cmd)
async def _read_input(self):
with patch_stdout():
while self.__run:
try:
inp = await self.session.prompt_async(
self.__prompt_in, completer=self.completer, auto_suggest=AutoSuggestFromHistory()
)
if await self._parse_input(inp):
continue
except EOFError:
pass
except KeyboardInterrupt:
self.__run = False
except Exception as e:
self.__logger.error("Exception in console.py:")
self.__logger.exception(e)
async def _read_input_legacy(self):
while self.__run:
try:
inp = input(self.__prompt_in)
if await self._parse_input(inp):
continue
except UnicodeDecodeError:
self.__logger.error("UnicodeDecodeError")
self.__run = False
except KeyboardInterrupt:
self.__run = False
except Exception as e:
self.__logger.error("Exception in console.py:")
self.__logger.exception(e)
async def start(self):
ev.register("serverTick_0.5s", players_completer.tick_players)
# ev.register("get_players_completer", lambda _: players_completer)
self.__run = True
if self.__legacy_mode:
await self._read_input_legacy()
else:
await self._read_input()
self.__debug("Closing console.")
raise KeyboardInterrupt
def stop(self, *args, **kwargs):
self.__run = False

View File

@@ -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: ...

View File

@@ -1,242 +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, t):
if t.startswith("html:"):
print_formatted_text(HTML(t[5:]))
else:
print_formatted_text(t)
def write(self, s: AnyStr):
if isinstance(s, (list, tuple)):
for text in s:
self._write(text)
else:
self._write(s)
def log(self, s: AnyStr) -> None:
if isinstance(s, (list, tuple)):
for text in s:
self.__logger.info(f"{text}")
else:
self.__logger.info(f"{s}")
# self.write(s)
def __lshift__(self, s: AnyStr) -> None:
self.write(s)
@property
def alias(self) -> dict:
return self.__alias.copy()
def __builtins_print(self,
*values: object,
sep: str or None = " ",
end: str or None = None,
file: str or None = None,
flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
val = list(values)
if len(val) > 0:
if self.__is_run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else:
if end is None:
end = "\n"
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
def logger_hook(self) -> None:
self.__debug("used logger_hook")
def emit(cls, record):
try:
msg = cls.format(record)
if cls.stream.name == "<stderr>":
self.write(f"\r{msg}")
else:
cls.stream.write(msg + cls.terminator)
cls.flush()
except RecursionError:
raise
except Exception as e:
cls.handleError(record)
logging.StreamHandler.emit = emit
def builtins_hook(self) -> None:
self.__debug("used builtins_hook")
builtins.Console = Console
builtins.console = self
# builtins.print = self.__builtins_print
async def read_input(self):
session = PromptSession(history=FileHistory('./.cmdhistory'))
while True:
try:
with patch_stdout():
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:
found_in_lua = False
d = ev.call_lua_event("onConsoleInput", cmd_in)
if len(d) > 0:
for text in d:
if text is not None:
found_in_lua = True
self.log(text)
command_object = self.__func.get(cmd)
if command_object:
out = command_object['f'](cmd_s[1:])
if out:
self.log(out)
else:
if not found_in_lua:
self.log(self.__not_found % cmd)
except KeyboardInterrupt:
raise KeyboardInterrupt
except Exception as e:
print(f"Error in console.py: {e}")
self.__logger.exception(e)
async def start(self):
self.__is_run = True
await self.read_input()
def stop(self, *args, **kwargs):
self.__is_run = False
raise KeyboardInterrupt

View 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: ...
```

View File

@@ -1 +1,228 @@
from .events_system import EventsSystem
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.EventsSystem
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import builtins
import inspect
from core import get_logger
# noinspection PyShadowingBuiltins
class EventsSystem:
def __init__(self):
# TODO: default events
self.log = get_logger("EventsSystem")
self.loop = asyncio.get_event_loop()
self.as_tasks = []
self.__events = {
"onServerStarted": [], # No handler
"onPlayerSentKey": [], # Only sync, no handler
"onPlayerAuthenticated": [], # (!) Only sync, With handler
"onPlayerJoin": [], # (!) With handler
"onPlayerReady": [], # No handler
"onChatReceive": [], # (!) With handler
"onCarSpawn": [], # (!) With handler
"onCarDelete": [], # (!) With handler (admin allow)
"onCarEdited": [], # (!) With handler
"onCarReset": [], # No handler
"onCarChanged": [], # No handler
"onCarFocusMove": [], # No handler
"onSentPing": [], # Only sync, no handler
"onChangePosition": [], # Only sync, no handler
"onPlayerDisconnect": [], # No handler
"onServerStopped": [], # No handler
"onCarSpawned": [], # No handler
"onCarDeleted": [], # No handler
"serverTick": [],
"serverTick_0.5s": [],
"serverTick_1s": [],
"serverTick_2s": [],
"serverTick_3s": [],
"serverTick_4s": [],
"serverTick_5s": [],
"serverTick_10s": [],
"serverTick_30s": [],
"serverTick_60s": [],
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onPlayerReady": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onCarChanged": [],
"onCarFocusMove": [],
"onServerStopped": [],
"onPlayerDisconnect": [],
"onCarSpawned": [],
"onCarDeleted": [],
"serverTick": [],
"serverTick_0.5s": [],
"serverTick_1s": [],
"serverTick_2s": [],
"serverTick_3s": [],
"serverTick_4s": [],
"serverTick_5s": [],
"serverTick_10s": [],
"serverTick_30s": [],
"serverTick_60s": [],
}
self.__lua_events = {
"onInit": [], # onServerStarted
"onShutdown": [], # onServerStopped
"onPlayerAuth": [], # onPlayerAuthenticated
"onPlayerConnecting": [], # No
"onPlayerJoining": [], # No
"onPlayerJoin": [], # onPlayerJoin
"onPlayerDisconnect": [], # onPlayerDisconnect
"onChatMessage": [], # onChatReceive
"onVehicleSpawn": [], # onCarSpawn
"onVehicleEdited": [], # onCarEdited
"onVehicleDeleted": [], # onCarDelete
"onVehicleReset": [], # onCarReset
"onFileChanged": [], # TODO lua onFileChanged
"onConsoleInput": [], # kt.add_command
}
self.register_event = self.register
def builtins_hook(self):
self.log.debug("used builtins_hook")
builtins.ev = self
def unregister_by_id(self, _id):
self.log.debug(f"unregister_by_id '{_id}'")
if not isinstance(_id, int):
return
s = a = 0
for k, funcs in self.__events.items():
for f in funcs:
if id(f) == _id:
s += 1
self.__events[k].remove(f)
for k, funcs in self.__async_events.items():
for f in funcs:
if id(f) == _id:
a += 1
self.__async_events[k].remove(f)
self.log.debug(f"unregister in {s + a} events; S:{s}; A:{a};")
def unregister(self, func):
self.log.debug(f"unregister '{func.__name__}' id: {id(func)}")
s = a = 0
for k, funcs in self.__events.items():
for f in funcs:
if f == func:
s += 1
self.__events[k].remove(func)
for k, funcs in self.__async_events.items():
for f in funcs:
if f == func:
a += 1
self.__async_events[k].remove(func)
self.log.debug(f"unregister in {s + a} events; S:{s}; A:{a};")
def is_event(self, event_name):
return (event_name in self.__async_events.keys() or
event_name in self.__events.keys() or
event_name in self.__lua_events.keys())
def register(self, event_name, event_func, async_event=False, lua=None, return_id=True):
self.log.debug(f"register(event_name='{event_name}', event_func='{event_func.__name__}'(id: {id(event_func)}), "
f"async_event={async_event}, lua_event={lua}):")
if lua:
if event_name not in self.__lua_events:
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
else:
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
self.log.debug("Register ok")
return
if not callable(event_func):
self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
return
if async_event or inspect.iscoroutinefunction(event_func):
if event_name not in self.__async_events:
self.__async_events[event_name] = []
self.__async_events[event_name].append(event_func)
self.log.debug("Register ok")
else:
if event_name not in self.__events:
self.__events[event_name] = []
self.__events[event_name].append(event_func)
self.log.debug("Register ok")
if return_id:
return id(event_func)
async def call_as_events(self, *args, **kwargs):
return await self.call_async_event(*args, **kwargs) + self.call_event(*args, **kwargs)
async def call_async_event(self, event_name, *args, **kwargs):
if not event_name.startswith("serverTick"):
self.log.debug(f"Calling async event: '{event_name}'")
funcs_data = []
if event_name in self.__async_events.keys():
for func in self.__async_events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
data = await func(event_data)
funcs_data.append(data)
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
elif not self.is_event(event_name):
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()"))
return funcs_data
def call_event(self, event_name: str, *args, **kwargs):
if event_name not in (
"onChangePosition", "onSentPing", # UDP events
"_get_player"
) and not event_name.startswith("serverTick"):
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():
for func in self.__events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
funcs_data.append(func(event_data))
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
elif not self.is_event(event_name):
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()"))
return funcs_data
def call_lua_event(self, event_name, *args):
self.log.debug(f"Calling lua event: '{event_name}{args}'")
funcs_data = []
if event_name in self.__lua_events.keys():
for data in self.__lua_events[event_name]:
lua = data['lua']
func_name = data["func_name"]
try:
func = lua.globals()[func_name]
if not func:
self.log.warning(i18n.events_lua_function_not_found.format("", func_name))
continue
fd = func(*args)
funcs_data.append(fd)
except Exception as e:
self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
elif not self.is_event(event_name):
self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
return funcs_data

View File

@@ -1,166 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.EventsSystem.events_system.py
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import builtins
import inspect
from core import get_logger
# noinspection PyShadowingBuiltins
class EventsSystem:
def __init__(self):
# TODO: default events
self.log = get_logger("EventsSystem")
self.loop = asyncio.get_event_loop()
self.as_tasks = []
self.__events = {
"onServerStarted": [],
"onPlayerSentKey": [], # Only sync
"onPlayerAuthenticated": [], # Only sync
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onSentPing": [], # Only sync
"onChangePosition": [], # Only sync
"onPlayerDisconnect": [],
"onServerStopped": [],
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onPlayerDisconnect": [],
"onServerStopped": []
}
self.__lua_events = {
"onInit": [], # onServerStarted
"onShutdown": [], # onServerStopped
"onPlayerAuth": [], # onPlayerAuthenticated
"onPlayerConnecting": [], # TODO lua onPlayerConnecting
"onPlayerJoining": [], # TODO lua onPlayerJoining
"onPlayerJoin": [], # onPlayerJoin
"onPlayerDisconnect": [], # TODO lua onPlayerDisconnect
"onChatMessage": [], # onChatReceive
"onVehicleSpawn": [], # "onCarSpawn
"onVehicleEdited": [], # onCarEdited
"onVehicleDeleted": [], # onCarDelete
"onVehicleReset": [], # onCarReset
"onFileChanged": [], # TODO lua onFileChanged
}
def builtins_hook(self):
self.log.debug("used builtins_hook")
builtins.ev = self
def register_event(self, event_name, event_func, async_event=False, lua=None):
self.log.debug(f"register_event(event_name='{event_name}', event_func='{event_func}', "
f"async_event={async_event}, lua_event={lua}):")
if lua:
if event_name not in self.__lua_events:
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
else:
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
self.log.debug("Register ok")
return
if not callable(event_func):
# 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)
self.log.debug("Register ok")
else:
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
else:
self.__events[event_name].append(event_func)
self.log.debug("Register ok")
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling async event: '{event_name}'")
funcs_data = []
if event_name in self.__async_events.keys():
for func in self.__async_events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
data = await func(event_data)
funcs_data.append(data)
except Exception as e:
# 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):
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():
for func in self.__events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
funcs_data.append(func(event_data))
except Exception as e:
# 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
def call_lua_event(self, event_name, *args):
self.log.debug(f"Calling lua event: '{event_name}'")
funcs_data = []
if event_name in self.__lua_events.keys():
for data in self.__lua_events[event_name]:
lua = data['lua']
func_name = data["func_name"]
try:
func = lua.globals()[func_name]
if not func:
self.log.warning(f"Cannot trigger local event: '{func_name}' not found!")
continue
fd = func(*args)
funcs_data.append(fd)
except Exception as e:
# TODO: i18n
self.log.error(f'Error: "{e}" - while calling lua event "{event_name}" with arguments: {args} - '
f'in function: "{func_name}"')
# self.log.exception(e)
else:
# TODO: i18n
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_lua_event() or MP.Trigger<>Event()?. "
f"Just skipping it...")
return funcs_data

View File

@@ -1,13 +0,0 @@
from typing import Any
class EventsSystem:
@staticmethod
def register_event(event_name, event_func, async_event: bool = False, lua: bool | object = None): ...
@staticmethod
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
@staticmethod
def call_event(event_name, *data, **kwargs) -> list[Any]: ...
@staticmethod
def call_lua_event(event_name, *data) -> list[Any]: ...
class ev(EventsSystem): ...

View File

@@ -0,0 +1,19 @@
### Builtins
```python
class EventsSystem:
@staticmethod
def unregister_by_id(_id: int) -> None: ...
@staticmethod
def unregister(func: Callable | Awaitable): ...
@staticmethod
def register(event_name: str, event_func: Callable | Awaitable, async_event: bool = False, lua: bool | object = None) -> None | int: ...
@staticmethod
async def call_as_events(event_name: str, *args, **kwargs) -> list[Any]: ...
@staticmethod
async def call_async_event(event_name: str, *args, **kwargs) -> list[Any]: ...
@staticmethod
def call_event(event_name: str, *data, **kwargs) -> list[Any]: ...
@staticmethod
def call_lua_event(event_name: str, *data) -> list[Any]: ...
```

View 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)

View File

@@ -1 +1,384 @@
from .plugins_loader import PluginsLoader
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.PluginsLoader
# Written by: SantaSpeen
# Version 1.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import inspect
import os
import subprocess
import sys
import textwrap
import time
import types
from contextlib import contextmanager
from pathlib import Path
from threading import Thread
from prompt_toolkit import PromptSession, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import FileHistory
from prompt_toolkit.lexers import PygmentsLexer
try:
from pygments.lexers.python import Python3Lexer
except ImportError:
print("ImportError: Python3Lexer")
exit(1)
from core import get_logger
class KuiToi:
_plugins_dir = ""
_file = ""
def __init__(self, name):
if not name:
raise AttributeError("KuiToi: Name is required")
self.__log = get_logger(f"Plugin | {name}")
self.__name = name
self.__dir = Path(self._plugins_dir) / self.__name
os.makedirs(self.__dir, exist_ok=True)
self.__funcs = []
self.register_event = self.register
@property
def log(self):
return self.__log
@property
def name(self):
return self.__name
@property
def dir(self):
return self.__dir
@contextmanager
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
path = self.__dir / file
if str(self.__dir) in str(file):
path = file
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
# Really need?
# if not os.path.exists(path):
# with open(path, 'x'): ...
f = None
try:
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
yield f
except Exception as e:
raise e
finally:
if f is not None:
f.close()
def register(self, event_name, event_func):
self.log.debug(f"Registering event {event_name}")
_id = ev.register(event_name, event_func)
self.__funcs.append(_id)
def _unload(self):
for f in self.__funcs:
console.del_command(f)
ev.unregister_by_id(f)
def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called event {event_name}")
return ev.call_event(event_name, *args, **kwargs)
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called async event {event_name}")
return await ev.call_async_event(event_name, *args, **kwargs)
def call_lua_event(self, event_name, *args):
self.log.debug(f"Called lua event {event_name}")
return ev.call_lua_event(event_name, *args)
def get_player(self, pid=None, nick=None, cid=None):
# self.log.debug("Requests get_player")
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
def get_players(self):
# self.log.debug("Requests get_players")
return self.get_player(-1)
def players_counter(self):
self.log.debug("Requests players_counter")
return len(self.get_players())
def is_player_connected(self, pid=None, nick=None):
self.log.debug("Requests is_player_connected")
if pid < 0:
return False
return bool(self.get_player(cid=pid, nick=nick))
def add_command(self, key, func, man, desc, custom_completer) -> dict:
self.log.debug("Requests add_command")
self.__funcs.append(func)
return console.add_command(key, func, man, desc, custom_completer)
class PluginsLoader:
_pip_dir = str(Path("pip-packets").resolve())
def __init__(self, plugins_dir):
self.loop = asyncio.get_event_loop()
self.plugins = {}
self.plugins_tasks = []
self.plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader")
self.loaded = []
self.pl_completer = Completer({})
self.pl_files_completer = Completer({})
self._scan_dir(None)
ev.register("serverTick_5s", self._scan_dir)
ev.register("_plugins_start", self.start)
ev.register("_plugins_unload", self.unload)
ev.register("_plugins_get",
lambda _: "Plugins: " + ", ".join(f"{i[0]}:{'on' if i[1] else 'off'}" for i in self.loaded))
console.add_command("plugins", self._parse_console, None, "Plugins manipulations",
{"plugins": {
"reload": self.pl_completer,
"load": self.pl_files_completer,
"unload": self.pl_completer,
"list": None,
}})
console.add_command("plugin", self._plugin_console, None, "plugin console", {"plugin": self.pl_completer})
sys.path.append(self._pip_dir)
os.makedirs(self._pip_dir, exist_ok=True)
console.add_command("install", self._pip_install)
def _scan_dir(self, _):
_load = {}
for file in os.listdir(self.plugins_dir):
file_path = os.path.join(self.plugins_dir, file)
if os.path.isfile(file_path) and file.endswith(".py"):
_load[file] = None
self.pl_files_completer.load(_load)
async def _plugin_console(self, x):
usage = 'Usage: plugin <name>'
if not x:
return usage
if x[0] in self.plugins:
plugin = self.plugins[x[0]]['plugin']
kt: KuiToi = plugin.kt
work = True
session = None
if not console.legacy_mode:
session = PromptSession(history=FileHistory(f'{kt.dir}/.cmdhistory'), lexer=PygmentsLexer(Python3Lexer))
def bottom_toolbar():
x = lambda x: f'<b><style bg="ansired">{x}</style></b>'
c = lambda c: f'<style fg="#b3d6f4">{x(c)}</style>'
return HTML(f'[PluginConsole KuiToi@{x(kt.name)}] {c("^D")} Return to the main console {c("^C")} Exit ')
while work:
try:
if session:
inp = await session.prompt_async(">> ", auto_suggest=AutoSuggestFromHistory(), bottom_toolbar=bottom_toolbar)
else:
inp = input(f"@{kt.name} > ")
self.log.debug(f"[_plugin_console] {inp=}")
if inp == "exit":
return "Exited"
if not inp:
continue
if inp.split(' ')[0] in ['import', 'from']:
kt.log.warning("Imports not allowed here... Sorry bro.")
continue
code = textwrap.dedent(f"""\
async def _console():
try:
i = {inp}
if i:
print(f"{{i!r}}")
except Exception as e:
kt.log.exception(e)""")
exec(code, plugin.__dict__)
kt.log.debug(await plugin._console())
except SyntaxError as e:
kt.log.error(f"SyntaxError: {e.msg}")
except EOFError:
return
except KeyboardInterrupt as e:
raise e
except UnicodeDecodeError as e:
raise e
except Exception as e:
kt.log.exception(e)
return "Plugin not found"
async def _parse_console(self, x):
usage = 'Usage: plugins [reload <name> | load <file.py> | unload <name> | list]'
if not x:
return usage
match x[0]:
case 'reload':
if len(x) == 2:
t1 = time.monotonic()
ok, _, file, _ = await self._unload_by_name(x[1], True)
if ok:
if await self._load_by_file(file):
self.plugins[x[1]]['plugin'].start()
return f"Plugin reloaded ({time.monotonic() - t1:.1f}sec)"
return "Plugin not found"
return usage
case 'load':
if len(x) == 2:
name = await self._load_by_file(x[1])
if name:
self.plugins[name]['plugin'].start()
return "Plugin loaded"
return usage
case 'unload':
if len(x) == 2:
ok, _, _, _ = await self._unload_by_name(x[1], True)
if ok:
return "Plugin unloaded"
return usage
case 'list':
return ev.call_event("_plugins_get")[0]
return usage
def _pip_install(self, x):
self.log.debug(f"_pip_install {x}")
if len(x) > 0:
try:
subprocess.check_call(['pip', 'install', *x, '--target', self._pip_dir])
return "Success"
except subprocess.CalledProcessError as e:
self.log.debug(f"error: {e}")
return f"Failed to install packages"
else:
return "Invalid syntax"
async def _load_by_file(self, file):
file_path = os.path.join(self.plugins_dir, file)
if os.path.isfile(file_path) and file.endswith(".py"):
try:
self.log.info(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.KuiToi._file = file
plugin.print = print
plugin.__file__ = file_path
with open(f'{file_path}', 'r', encoding=config.enc) as f:
code = f.read()
exec(code, plugin.__dict__)
ok = True
try:
is_func = inspect.isfunction
if not is_func(plugin.load):
self.log.error(i18n.plugins_not_found_load)
ok = False
if not is_func(plugin.start):
self.log.error(i18n.plugins_not_found_start)
ok = False
if not is_func(plugin.unload):
self.log.error(i18n.plugins_not_found_unload)
ok = False
if type(plugin.kt) != KuiToi:
self.log.error(i18n.plugins_kt_invalid)
ok = False
except AttributeError:
ok = False
if not ok:
self.log.error(i18n.plugins_invalid.format(file_path))
return
pl_name = plugin.kt.name
if self.plugins.get(pl_name) is not None:
raise NameError(f'Having plugins with identical names is not allowed; '
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
plugin.open = plugin.kt.open
is_coro_func = inspect.iscoroutinefunction
self.plugins.update(
{
pl_name: {
"plugin": plugin,
"load": {
"func": plugin.load,
"async": is_coro_func(plugin.load)
},
"start": {
"func": plugin.start,
"async": is_coro_func(plugin.start)
},
"unload": {
"func": plugin.unload,
"async": is_coro_func(plugin.unload)
}
}
}
)
if self.plugins[pl_name]["load"]['async']:
plugin.log.debug(f"I'm async")
await plugin.load()
else:
plugin.log.debug(f"I'm sync")
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
th.start()
th.join()
self.pl_completer.options[pl_name] = None
self.loaded.append((pl_name, True))
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
return pl_name
except Exception as e:
self.loaded.append((file, False))
self.log.error(i18n.plugins_error_loading.format(file, f"{e}"))
self.log.exception(e)
return False
async def load(self):
self.log.debug("Loading plugins...")
for file in os.listdir(self.plugins_dir):
await self._load_by_file(file)
async def _unload_by_name(self, name, reload=False):
t1 = time.monotonic()
data = self.plugins.get(name)
if not data:
return False, name, None, None
try:
if reload:
data['plugin'].kt._unload()
self.loaded.remove((name, True))
self.plugins.pop(name)
if data['unload']['async']:
self.log.debug(f"Unload async plugin: {name}")
await data['unload']['func']()
else:
self.log.debug(f"Unload sync plugin: {name}")
th = Thread(target=data['unload']['func'], name=f"Thread {name}")
th.start()
th.join()
except Exception as e:
self.log.exception(e)
return True, name, data['plugin'].kt._file, time.monotonic() - t1
async def start(self, _):
for pl_name, pl_data in self.plugins.items():
try:
func = pl_data['start']['func']
if pl_data['start']['async']:
self.log.debug(f"Start async plugin: {pl_name}")
t = self.loop.create_task(func())
else:
self.log.debug(f"Start sync plugin: {pl_name}")
t = self.loop.create_task(asyncio.to_thread(func))
self.plugins_tasks.append(t)
except Exception as e:
self.log.exception(e)
async def unload(self, _):
t = []
for n in self.plugins.keys():
t.append(self._unload_by_name(n))
self.log.debug(await asyncio.gather(*t))
self.log.debug("Plugins unloaded")

View File

@@ -3,7 +3,9 @@ import json
import os
import platform
import random
import re
import shutil
import sys
import threading
import time
@@ -42,7 +44,7 @@ class EventTimer:
self.mp.TriggerLocalEvent(self.event_name)
# noinspection PyPep8Naming
# noinspection PyPep8Naming,PyProtectedMember
class MP:
def __init__(self, name: str, lua: LuaRuntime):
@@ -63,6 +65,12 @@ class MP:
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))
@@ -81,7 +89,7 @@ class MP:
def RegisterEvent(self, event_name: str, function_name: str) -> None:
self.log.debug("request MP.RegisterEvent()")
ev.register_event(event_name, function_name, lua=self._lua)
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:
@@ -102,27 +110,24 @@ class MP:
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}'")
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(f"Cannot trigger local event: '{func_name}' not found!")
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:
# TODO: i18n
self.log.error(f'Error: "{e}" - while calling lua event "{event_name}" with arguments: {args} - '
f'in function: "{func_name}"')
# self.log.exception(e)
self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
else:
# TODO: i18n
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_lua_event() or MP.Trigger<>Event()?. "
f"Just skipping it...")
self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
return self._lua.table_from(funcs_data)
@@ -154,7 +159,7 @@ class MP:
to_all = False
if player_id < 0:
to_all = True
client = client[0]
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)
@@ -165,7 +170,7 @@ class MP:
return False, "Can't found event_name or data"
def TriggerClientEventJson(self, player_id, event_name, data):
self.log.debug("request TriggerClientEventJson()")
self.log.debug(f"request TriggerClientEventJson({player_id, event_name, data})")
data = self._lua.globals().Util.JsonEncode(data)
self.TriggerClientEvent(player_id, event_name, data)
@@ -229,8 +234,8 @@ class MP:
def GetPlayers(self):
self.log.debug("request GetPlayers()")
clients = ev.call_event("_get_players", cid=-1)
return self._lua.table_from(clients)
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()")
@@ -309,12 +314,18 @@ class Util:
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)
return json.dumps(data)
except Exception as e:
self.log.exception(e)
data = json.dumps(data)
self.log.debug(f"Encoded: {data}")
return data
def JsonDecode(self, string):
self.log.debug("requesting JsonDecode()")
@@ -556,6 +567,7 @@ class FS:
return os.path.join(*args)
# noinspection PyProtectedMember
class LuaPluginsLoader:
def __init__(self, plugins_dir):
@@ -566,20 +578,17 @@ class LuaPluginsLoader:
self.lua_dirs = set()
self.log = get_logger("LuaPluginsLoader")
self.loaded_str = "Lua plugins: "
ev.register_event("_lua_plugins_get", lambda x: self.lua_plugins)
ev.register_event("_lua_plugins_unload", self.unload)
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...")
# TODO: i18n
self.log.info("You have enabled support for Lua plugins.")
self.log.warning("There are some nuances to working with KuiToi. "
"If you have a proposal for their solution, and it is related to KuiToi, "
"please contact the developer.")
self.log.warning("Some BeamMP plugins require a correctly configured ServerConfig.toml file to function.")
self.log.info("Creating it.")
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.",
@@ -590,7 +599,7 @@ class LuaPluginsLoader:
"LogChat": config.Options['log_chat'],
"Debug": config.Options['debug'],
"Private": config.Auth['private'],
"MaxCars": config.Game['max_cars'],
"MaxCars": config.Game['cars'],
"MaxPlayers": config.Game['players'],
"Map": f"/levels/{config.Game['map']}/info.json",
"Description": config.Server['description'],
@@ -600,14 +609,6 @@ class LuaPluginsLoader:
"ImScaredOfUpdates": False,
"SendErrorsShowMessage": False,
"SendErrors": False
},
"HTTP": {
"HTTPServerIP": config.WebAPI['server_ip'],
"HTTPServerPort": config.WebAPI['server_port'],
"SSLKeyPath": None,
"SSLCertPath": None,
"UseSSL": False,
"HTTPServerEnabled": config.WebAPI['enabled'],
}
}
with open("ServerConfig.toml", "w") as f:
@@ -640,7 +641,11 @@ class LuaPluginsLoader:
p0 = os.path.join(pa, name, "?.lua")
p1 = os.path.join(pa, name, "lua", "?.lua")
lua_globals.package.path += f';{p0};{p1}'
with open("modules/PluginsLoader/add_in.lua", "r") as f:
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)
@@ -663,10 +668,15 @@ class LuaPluginsLoader:
self.log.error(f"Exception onInit from `{name}`: {e}")
self.log.exception(e)
def unload(self, _):
async def unload(self, _):
self.log.debug("Unloading lua plugins")
for name, data in self.lua_plugins.items():
if data['ok']:
self.log.info(f"Unloading lua plugin: {name}")
for _, timer in data['lua'].globals().MP._event_timers.items():
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")

View File

@@ -1,216 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.PluginsLoader.plugins_loader.py
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import inspect
import os
import types
from contextlib import contextmanager
from threading import Thread
from core import get_logger
class KuiToi:
_plugins_dir = ""
def __init__(self, name):
if name is None:
raise AttributeError("KuiToi: Name is required")
self.__log = get_logger(f"Plugin | {name}")
self.__name = name
self.__dir = os.path.join(self._plugins_dir, self.__name)
if not os.path.exists(self.__dir):
os.mkdir(self.__dir)
@property
def log(self):
return self.__log
@property
def name(self):
return self.__name
@property
def dir(self):
return self.__dir
@contextmanager
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
path = os.path.join(self.__dir, file)
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
# Really need?
# if not os.path.exists(path):
# with open(path, 'x'): ...
f = None
try:
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
yield f
except Exception as e:
raise e
finally:
if f is not None:
f.close()
def register_event(self, event_name, event_func):
self.log.debug(f"Registering event {event_name}")
ev.register_event(event_name, event_func)
def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called event {event_name}")
return ev.call_event(event_name, *args, **kwargs)
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called async event {event_name}")
return await ev.call_async_event(event_name, *args, **kwargs)
def call_lua_event(self, event_name, *args):
self.log.debug(f"Called lua event {event_name}")
return ev.call_lua_event(event_name, *args)
def get_player(self, pid=None, nick=None, cid=None):
self.log.debug("Requests get_player")
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
def get_players(self):
self.log.debug("Requests get_players")
return self.get_player(-1)
def players_counter(self):
self.log.debug("Requests players_counter")
return len(self.get_players())
def is_player_connected(self, pid=None, nick=None):
self.log.debug("Requests is_player_connected")
if pid < 0:
return False
return bool(self.get_player(cid=pid, nick=nick))
class PluginsLoader:
def __init__(self, plugins_dir):
self.loop = asyncio.get_event_loop()
self.plugins = {}
self.plugins_tasks = []
self.plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader")
self.loaded_str = "Plugins: "
ev.register_event("_plugins_start", self.start)
ev.register_event("_plugins_unload", self.unload)
ev.register_event("_plugins_get", lambda x: list(self.plugins.keys()))
console.add_command("plugins", lambda x: self.loaded_str[:-2])
console.add_command("pl", lambda x: self.loaded_str[:-2])
async def load(self):
self.log.debug("Loading plugins...")
for file in os.listdir(self.plugins_dir):
file_path = os.path.join(self.plugins_dir, file)
if os.path.isfile(file_path) and file.endswith(".py"):
try:
self.log.debug(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.print = print
plugin.__file__ = file_path
with open(f'{file_path}', 'r', encoding=config.enc) as f:
code = f.read()
exec(code, plugin.__dict__)
ok = True
try:
is_func = inspect.isfunction
if not is_func(plugin.load):
self.log.error('Function "def load():" not found.')
ok = False
if not is_func(plugin.start):
self.log.error('Function "def start():" not found.')
ok = False
if not is_func(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
is_coro_func = inspect.iscoroutinefunction
self.plugins.update(
{
pl_name: {
"plugin": plugin,
"load": {
"func": plugin.load,
"async": is_coro_func(plugin.load)
},
"start": {
"func": plugin.start,
"async": is_coro_func(plugin.start)
},
"unload": {
"func": plugin.unload,
"async": is_coro_func(plugin.unload)
}
}
}
)
if self.plugins[pl_name]["load"]['async']:
plugin.log.debug(f"I'm async")
await plugin.load()
else:
plugin.log.debug(f"I'm sync")
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
th.start()
th.join()
self.loaded_str += f"{pl_name}:ok, "
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
except Exception as e:
# 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)

View 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())

View File

@@ -1,2 +0,0 @@
from .app import web_app
from .app import _stop

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -1,48 +0,0 @@
{
"": "基础阶段",
"hello": "来自KuiToi服务器的问候",
"config_path": "使用{}进行配置。",
"init_ok": "初始化完成。",
"start": "服务器已启动!",
"stop": "服务器已停止!",
"": "服务器认证",
"auth_need_key": "需要BeamMP密钥才能启动",
"auth_empty_key": "BeamMP密钥为空",
"auth_cannot_open_browser": "无法打开浏览器:{}",
"auth_use_link": "使用此链接:{}",
"": "GUI阶段",
"GUI_yes": "是",
"GUI_no": "否",
"GUI_ok": "确定",
"GUI_cancel": "取消",
"GUI_need_key_message": "需要BeamMP密钥才能启动\n是否在浏览器中打开链接以获取密钥",
"GUI_enter_key_message": "请输入密钥:",
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接{}",
"": "Web阶段",
"web_start": "WebAPI已启动{}CTRL+C停止",
"": "命令man",
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法man COMMAND",
"help_message_man": "显示COMMAND的帮助页面。",
"man_for": "帮助页面",
"man_message_not_found": "man找不到帮助页面。",
"man_command_not_found": "man找不到\"{}\"命令!",
"": "命令help",
"man_message_help": "help - 显示命令的名称和简短描述。\n用法help [--raw]\n命令`help`列出所有可用的命令,并为每个命令提供简短描述。",
"help_message_help": "显示命令的名称和简短描述。",
"help_command": "命令",
"help_message": "文本",
"help_message_not_found": "无文本",
"": "命令stop",
"man_message_stop": "stop - 关闭服务器。\n用法stop",
"help_message_stop": "关闭服务器。",
"": "命令exit",
"man_message_exit": "exit - 关闭服务器。\n用法exit",
"help_message_exit": "关闭服务器。"
}

View File

@@ -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."
}

View File

@@ -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": "Выключает сервер."
}

View File

@@ -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."
}

View File

@@ -1,160 +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=None):
if encoding is None:
encoding = config.enc
if language is None:
language = "en"
self.__data = {}
self.__i18n = None
self.__encoding = encoding
self.language = language
self.files_dir = files_dir
self.log = get_logger("i18n")
self.set_language(language)
def set_language(self, language):
if language is None:
language = "en"
self.log.debug(f"set_language({language})")
self.language = language
if language != "en":
self.open_file()
else:
# noinspection PyDictDuplicateKeys
self.__data = {
"": "Basic phases",
"hello": "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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

44
win/metadata.txt Normal file
View 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])])
]
)