22 Commits

Author SHA1 Message Date
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
54 changed files with 1382 additions and 1285 deletions

View File

@@ -13,81 +13,11 @@ I didn't like writing plugins in Lua after using Python; it was very inconvenien
## TODOs ## TODOs
- [x] Server core: [TODO.md](./TODO.md)
- [x] BeamMP System:
- [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher)
- [X] Player authentication
- [x] TCP Server part:
- [x] Handle code
- [x] Understanding BeamMP header
- [x] Upload mods
- [x] Connecting to the world
- [x] Chat
- [x] Players online counter
- [x] Packets handled (Recursive finding second packet)
- [x] Client events
- [x] Car synchronizations:
- [x] State packets
- [x] Spawn cars
- [x] Delete cars
- [x] Edit cars
- [x] Reset cars
- [x] "ABG": (compressed data)
- [x] Decompress data
- [x] Compress data
- [x] UDP Server part:
- [x] Ping
- [x] Position synchronizations
- [x] Additional:
- [x] Logger:
- [x] Just logging
- [x] Log in file
- [x] Log history (.1.log, .2.log, ...)
- [x] Console:
- [x] Tabulation
- [x] History
- [x] Autocomplete
- [x] Events System:
- [x] Call events
- [x] Create custom events
- [x] Return from events
- [x] Async support
- [x] Add all events
- [x] MultiLanguage: (i18n support)
- [x] Core
- [x] Console
- [x] WebAPI
- [x] Plugins supports:
- [x] Python part:
- [x] Load Python plugins
- [x] Async support
- [x] KuiToi class
- [x] Client (Player) class
- [x] Lua part: (Original BeamMP compatibility)
- [x] Load Lua plugins
- [x] MP Class
- [x] Util class
- [x] FS class
- [ ] HTTP API Server: (fastapi)
- [x] Stop and Start with core
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] RCON System:
- [x] Serving
- [ ] Handle commands
- [x] Client
- [x] AES encryption
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] [Documentation](./docs)
## Installation ## Installation
1. Install **Python 3.10** 1. Install **Python 3.12**+
2. Clone the repository in a location of your choice with: `git clone -b Stable https://github.com/kuitoi/kuitoi-Server.git`. 2. Clone the repository in a location of your choice with: `git clone -b Stable https://github.com/kuitoi/kuitoi-Server.git`.
3. Change directory into the KuiToi-Server: `cd KuiToi-Server`. 3. Change directory into the KuiToi-Server: `cd KuiToi-Server`.
4. Install requirements: `pip install -r requirements.txt`. 4. Install requirements: `pip install -r requirements.txt`.

73
TODO.md Normal file
View File

@@ -0,0 +1,73 @@
## 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] WebAPI
- [x] Plugins supports:
- [x] Python part:
- [x] Load Python plugins
- [x] Async support
- [x] KuiToi class
- [x] Client (Player) class
- [x] Lua part: (Original BeamMP compatibility)
- [x] Load Lua plugins
- [x] MP Class
- [x] Util class
- [x] FS class
- [ ] HTTP API Server: (fastapi)
- [x] Stop and Start with core
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] RCON System:
- [x] Serving
- [ ] Handle commands
- [x] Client
- [x] AES encryption
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] [Documentation](./docs)

View File

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

View File

@@ -1,3 +1,13 @@
# 所有可用事件列表 # 所有可用事件列表
大多数事件将包含`pl = data ['kwargs'] ['player']`,可以在[这里](./classes.md)找到描述。 大多数事件将包含`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) json.dump(config, f)
cgf = config cgf = config
log.info(cgf) log.info(cgf)
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")

View File

@@ -21,7 +21,7 @@ def load():
json.dump(config, f) json.dump(config, f)
cgf = config cgf = config
log.info(cgf) log.info(cgf)
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")

View File

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

View File

@@ -34,7 +34,7 @@ Auth:
private: true private: true
Game: Game:
map: gridmap_v2 map: gridmap_v2
max_cars: 1 cars: 1
players: 8 players: 8
Options: Options:
debug: false debug: false
@@ -64,7 +64,7 @@ WebAPI:
### Game ### Game
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。 * `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
* `max_cars` - 每个玩家的最大汽车数量 * `cars` - 每个玩家的最大汽车数量
* `players` - 最大玩家数 * `players` - 最大玩家数
### Options ### Options

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 # Passed Classes
## Worth looking at ## Worth looking at
@@ -147,7 +23,7 @@ Returns the directory of the plugin
_Parameters are the same as for open()_\ _Parameters are the same as for open()_\
Opens a file in kt.dir Opens a file in kt.dir
### kt.register_event(event_name: str, event_func: function) ### kt.register(event_name: str, event_func: function)
_`event_name: str` -> The name of the event that `event_func` will be called on._\ _`event_name: str` -> The name of the event that `event_func` will be called on._\
_`event_func: function` -> The function that will be called._ _`event_func: function` -> The function that will be called._

View File

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

View File

@@ -21,7 +21,7 @@ async def load():
json.dump(config, f) json.dump(config, f)
cgf = config cgf = config
log.info(cgf) log.info(cgf)
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")

View File

@@ -21,7 +21,7 @@ def load():
json.dump(config, f) json.dump(config, f)
cgf = config cgf = config
log.info(cgf) log.info(cgf)
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")

View File

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

View File

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

View File

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

View File

@@ -2,4 +2,12 @@
В большинство ивентов будет приходить `pl = data['kwargs']['player']`, описание можно найти [тут](./classes.md) В большинство ивентов будет приходить `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) json.dump(config, f)
cgf = config cgf = config
log.info(cgf) log.info(cgf)
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")

View File

@@ -21,7 +21,7 @@ def load():
json.dump(config, f) json.dump(config, f)
cgf = config cgf = config
log.info(cgf) log.info(cgf)
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")

View File

@@ -28,7 +28,7 @@ def my_event_handler(event_data):
def load(): def load():
# Инициализация плагина # Инициализация плагина
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")
@@ -46,7 +46,7 @@ def unload():
Так же более обширный пример можно найти в [example.py](examples/example.py) Так же более обширный пример можно найти в [example.py](examples/example.py)
* Рекомендуется использовать `open()` после `load()`, иначе стоит использовать `kt.load()` - Создаёт файл в папке `plugin/<plugin_name>/<filename>` * Рекомендуется использовать `open()` после `load()`, иначе стоит использовать `kt.load()` - Создаёт файл в папке `plugin/<plugin_name>/<filename>`
* Создание своего ивента : `kt.register_event("my_event", my_event_function)` - * Создание своего ивента : `kt.register("my_event", my_event_function)` -
* Вызов ивента: `kt.call_event("my_event")` * Вызов ивента: `kt.call_event("my_event")`
* Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)` * Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)`
* Базовые ивенты: озже напишу_ * Базовые ивенты: озже напишу_
@@ -71,7 +71,7 @@ async def my_event_handler(event_data):
async def load(): async def load():
# Инициализация плагина # Инициализация плагина
ev.register_event("my_event", my_event_handler) ev.register("my_event", my_event_handler)
log.info("Плагин загружен успешно.") log.info("Плагин загружен успешно.")
@@ -90,7 +90,7 @@ async def unload():
Так же более обширный пример можно найти в [async_example.py](examples/async_example.py) Так же более обширный пример можно найти в [async_example.py](examples/async_example.py)
* Создание своего ивента: `kt.register_event("my_event", my_event_function)` (в register_event стоит проверка на функцию) * Создание своего ивента: `kt.register("my_event", my_event_function)` (в register стоит проверка на функцию)
* Вызов async ивента: `kt.call_async_event("my_event")` * Вызов async ивента: `kt.call_async_event("my_event")`
* Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)` * Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)`
* Базовые async ивенты: озже напишу_ * Базовые async ивенты: озже напишу_

View File

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

View File

@@ -1,12 +1,14 @@
PyYAML~=6.0 PyYAML~=6.0
prompt-toolkit~=3.0.38 prompt-toolkit~=3.0.38
aiohttp~=3.8.4 aiohttp~=3.9.5
uvicorn~=0.22.0 uvicorn~=0.22.0
fastapi~=0.100.0 fastapi~=0.109.1
starlette~=0.27.0 starlette~=0.36.2
pydantic~=2.0.2 pydantic~=2.0.2
click~=8.1.4 click~=8.1.4
lupa~=2.0 lupa~=2.0
toml~=0.10.2 toml~=0.10.2
colorama~=0.4.6 colorama~=0.4.6
cryptography~=41.0.2 cryptography~=42.0.4
prompt_toolkit~=3.0.47
requests~=2.32.3

View File

@@ -9,6 +9,7 @@ import json
import math import math
import time import time
import zlib import zlib
from asyncio import Lock
from core import utils from core import utils
@@ -18,7 +19,7 @@ class Client:
def __init__(self, reader, writer, core): def __init__(self, reader, writer, core):
self.__reader = reader self.__reader = reader
self.__writer = writer self.__writer = writer
self.__Core = core self._core = core
self.__alive = True self.__alive = True
self.__packets_queue = [] self.__packets_queue = []
self.__tasks = [] self.__tasks = []
@@ -32,13 +33,15 @@ class Client:
self.nick = None self.nick = None
self.roles = None self.roles = None
self._guest = True self._guest = True
self._synced = False
self._ready = False self._ready = False
self._identifiers = [] self._identifiers = []
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
self._focus_car = -1 self._focus_car = -1
self._snowman = {"id": -1, "packet": ""} self._unicycle = {"id": -1, "packet": ""}
self._connect_time = 0 self._connect_time = 0
self._last_position = {} self._last_position = {}
self._lock = Lock()
@property @property
def _writer(self): def _writer(self):
@@ -72,6 +75,10 @@ class Client:
def guest(self): def guest(self):
return self._guest return self._guest
@property
def synced(self):
return self._synced
@property @property
def ready(self): def ready(self):
return self._ready return self._ready
@@ -148,7 +155,7 @@ class Client:
if to_all: if to_all:
code = chr(data[0]) code = chr(data[0])
for client in self.__Core.clients: for client in self._core.clients:
if not client or (client is self and not to_self): if not client or (client is self and not to_self):
continue continue
if not to_udp or code in ['V', 'W', 'Y', 'E']: if not to_udp or code in ['V', 'W', 'Y', 'E']:
@@ -167,8 +174,7 @@ class Client:
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION) data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
if to_udp: if to_udp:
udp_sock = self._udp_sock[0] udp_sock, udp_addr = self._udp_sock
udp_addr = self._udp_sock[1]
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}') # self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
if udp_sock and udp_addr: if udp_sock and udp_addr:
try: try:
@@ -188,7 +194,6 @@ class Client:
writer.write(header + data) writer.write(header + data)
await writer.drain() await writer.drain()
return True return True
except Exception as e: except Exception as e:
self.log.debug(f'[TCP] Disconnected: {e}') self.log.debug(f'[TCP] Disconnected: {e}')
self.__alive = False self.__alive = False
@@ -291,7 +296,7 @@ class Client:
file = data[1:].decode(config.enc) file = data[1:].decode(config.enc)
self.log.info(i18n.client_mod_request.format(repr(file))) self.log.info(i18n.client_mod_request.format(repr(file)))
size = -1 size = -1
for mod in self.__Core.mods_list: for mod in self._core.mods_list:
if type(mod) == int: if type(mod) == int:
continue continue
if mod.get('path') == file: if mod.get('path') == file:
@@ -312,9 +317,9 @@ class Client:
await self.kick("Missing download socket") await self.kick("Missing download socket")
return return
if config.Options['use_queue']: if config.Options['use_queue']:
while self.__Core.lock_upload: while self._core.lock_upload:
await asyncio.sleep(.2) await asyncio.sleep(.2)
self.__Core.lock_upload = True self._core.lock_upload = True
speed = config.Options["speed_limit"] speed = config.Options["speed_limit"]
if speed: if speed:
speed = speed / 2 speed = speed / 2
@@ -326,8 +331,8 @@ class Client:
] ]
sl0, sl1 = await asyncio.gather(*uploads) sl0, sl1 = await asyncio.gather(*uploads)
tr = (time.monotonic() - t) or 0.0001 tr = (time.monotonic() - t) or 0.0001
if self.__Core.lock_upload: if self._core.lock_upload:
self.__Core.lock_upload = False self._core.lock_upload = False
msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr)) msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
if speed: if speed:
msg += i18n.client_mod_sent_limit.format(int(speed * 2)) msg += i18n.client_mod_sent_limit.format(int(speed * 2))
@@ -343,7 +348,7 @@ class Client:
elif data.startswith(b"SR"): elif data.startswith(b"SR"):
path_list = '' path_list = ''
size_list = '' size_list = ''
for mod in self.__Core.mods_list: for mod in self._core.mods_list:
if type(mod) == int: if type(mod) == int:
continue continue
path_list += f"{mod['path']};" path_list += f"{mod['path']};"
@@ -385,7 +390,7 @@ class Client:
car_data = data[2:] car_data = data[2:]
car_id = next((i for i, car in enumerate(self._cars) if car is None), len(self._cars)) car_id = next((i for i, car in enumerate(self._cars) if car is None), len(self._cars))
cars_count = len(self._cars) - self._cars.count(None) cars_count = len(self._cars) - self._cars.count(None)
if self._snowman['id'] != -1: if self._unicycle['id'] != -1:
cars_count -= 1 # -1 for unicycle cars_count -= 1 # -1 for unicycle
self.log.debug(f"car_id={car_id}, cars_count={cars_count}") self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
car_json = {} car_json = {}
@@ -394,7 +399,7 @@ class Client:
except Exception as e: except Exception as e:
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}") self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
allow = True allow = True
allow_snowman = True allow_unicycle = True
over_spawn = False over_spawn = False
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):]) lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
if 1 in lua_data: if 1 in lua_data:
@@ -407,15 +412,15 @@ class Client:
# TODO: handle event onCarSpawn # TODO: handle event onCarSpawn
pass pass
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}" pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
snowman = car_json.get("jbm") == "unicycle" unicycle = car_json.get("jbm") == "unicycle"
if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn: if allow and config.Game['cars'] > cars_count or (unicycle and allow_unicycle) or over_spawn:
if snowman: if unicycle:
unicycle_id = self._snowman['id'] unicycle_id = self._unicycle['id']
if unicycle_id != -1: if unicycle_id != -1:
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}") self.log.debug(f"Delete old unicycle: car_id={unicycle_id}")
self._cars[unicycle_id] = None self._cars[unicycle_id] = None
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True) await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._snowman = {"id": car_id, "packet": pkt} self._unicycle = {"id": car_id, "packet": pkt}
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}") self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
else: else:
self.log.debug(f"Car spawn accepted: car_id={car_id}") self.log.debug(f"Car spawn accepted: car_id={car_id}")
@@ -424,8 +429,8 @@ class Client:
"packet": pkt, "packet": pkt,
"json": car_json, "json": car_json,
"json_ok": bool(car_json), "json_ok": bool(car_json),
"snowman": snowman, "unicycle": unicycle,
"over_spawn": (snowman and allow_snowman) or over_spawn, "over_spawn": (unicycle and allow_unicycle) or over_spawn,
"pos": {} "pos": {}
} }
await self._send(pkt, to_all=True, to_self=True) await self._send(pkt, to_all=True, to_self=True)
@@ -461,10 +466,10 @@ class Client:
if cid == self.cid or admin_allow: if cid == self.cid or admin_allow:
await self._send(raw_data, to_all=True, to_self=True) await self._send(raw_data, to_all=True, to_self=True)
car = self._cars[car_id] car = self._cars[car_id]
if car['snowman']: if car['unicycle']:
self.log.debug(f"Snowman found") self.log.debug("unicycle found")
unicycle_id = self._snowman['id'] unicycle_id = self._unicycle['id']
self._snowman['id'] = -1 self._unicycle['id'] = -1
self._cars[unicycle_id] = None self._cars[unicycle_id] = None
self._cars[car_id] = None self._cars[car_id] = None
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True) await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
@@ -476,7 +481,7 @@ class Client:
async def _edit_car(self, raw_data, data): async def _edit_car(self, raw_data, data):
cid, car_id = self._get_cid_vid(raw_data) cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and self._cars[car_id]: if car_id != -1 and self._cars[car_id]:
client = self.__Core.get_client(cid=cid) client = self._core.get_client(cid=cid)
if client: if client:
car = client._cars[car_id] car = client._cars[car_id]
new_car_json = {} new_car_json = {}
@@ -500,8 +505,8 @@ class Client:
if cid == self.cid or allow or admin_allow: if cid == self.cid or allow or admin_allow:
if car['snowman']: if car['snowman']:
unicycle_id = self._snowman['id'] unicycle_id = self._unicycle['id']
self._snowman['id'] = -1 self._unicycle['id'] = -1
self.log.debug(f"Delete snowman") self.log.debug(f"Delete snowman")
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True) await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._cars[unicycle_id] = None self._cars[unicycle_id] = None
@@ -587,7 +592,7 @@ class Client:
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
await self._send(f"J{i18n.game_welcome_message.format(self.nick)}", to_all=True) # Hello message await self._send(f"J{i18n.game_welcome_message.format(self.nick)}", to_all=True) # Hello message
for client in self.__Core.clients: for client in self._core.clients:
if not client: if not client:
continue continue
for car in client._cars: for car in client._cars:
@@ -617,6 +622,8 @@ class Client:
ev_data_list.extend(d2) ev_data_list.extend(d2)
need_send = True need_send = True
for ev_data in ev_data_list: for ev_data in ev_data_list:
if ev_data is None:
continue
try: try:
message = ev_data["message"] message = ev_data["message"]
to_all = ev_data.get("to_all") to_all = ev_data.get("to_all")
@@ -634,7 +641,9 @@ class Client:
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}") self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}")
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer) await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False need_send = False
except KeyError | AttributeError: except KeyError:
self.log.error(i18n.client_event_invalid_data.format(ev_data))
except AttributeError:
self.log.error(i18n.client_event_invalid_data.format(ev_data)) self.log.error(i18n.client_event_invalid_data.format(ev_data))
if need_send: if need_send:
if config.Options['log_chat']: if config.Options['log_chat']:
@@ -646,20 +655,18 @@ class Client:
self.__alive = False self.__alive = False
return return
# Codes: V W X Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
return
_bytes = False _bytes = False
try: try:
data = data.decode() data = data.decode()
except UnicodeDecodeError: except UnicodeDecodeError:
_bytes = True _bytes = True
self.log.error(f"UnicodeDecodeError: {data}") self.log.error(f"UnicodeDecodeError: {data}")
self.log.info("Some things are skipping...")
# Codes: p, Z in udp_server.py if data[0] in ['V', 'W', 'Y', 'E', 'N']:
await self._send(data, to_all=True, to_self=False)
return
# Codes: p, Z, X in udp_server.py
match data[0]: # At data[0] code match data[0]: # At data[0] code
case "H": # Map load, client ready case "H": # Map load, client ready
await self._connected_handler() await self._connected_handler()
@@ -687,11 +694,11 @@ class Client:
self.log.error(f"Received event in invalid format (missing ':'), got: {data}") self.log.error(f"Received event in invalid format (missing ':'), got: {data}")
event_name = data[2:sep] event_name = data[2:sep]
even_data = data[sep + 1:] even_data = data[sep + 1:]
ev.call_lua_event(event_name, even_data) ev.call_lua_event(event_name, self.cid, even_data)
ev.call_event(event_name, data=even_data, player=self) ev.call_event(event_name, data=even_data, player=self)
await ev.call_async_event(event_name, data=even_data, player=self) await ev.call_async_event(event_name, data=even_data, player=self)
case "N": case _:
await self._send(data, to_all=True, to_self=False) self.log.warning(f"TCP [{self.cid}] Unknown code: {data[0]}; {data}")
async def _looper(self): async def _looper(self):
ev.call_lua_event("onPlayerConnecting", self.cid) ev.call_lua_event("onPlayerConnecting", self.cid)
@@ -702,6 +709,7 @@ class Client:
tasks = self.__tasks tasks = self.__tasks
recv = asyncio.create_task(self._recv()) recv = asyncio.create_task(self._recv())
tasks.append(recv) tasks.append(recv)
self._synced = True
while self.__alive: while self.__alive:
if len(self.__packets_queue) > 0: if len(self.__packets_queue) > 0:
for index, packet in enumerate(self.__packets_queue): for index, packet in enumerate(self.__packets_queue):
@@ -717,7 +725,7 @@ class Client:
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
self.__alive = False self.__alive = False
if (self.cid > 0 or self.nick is not None) and \ if (self.cid > 0 or self.nick is not None) and \
self.__Core.clients_by_nick.get(self.nick): self._core.clients_by_nick.get(self.nick):
for i, car in enumerate(self._cars): for i, car in enumerate(self._cars):
if not car: if not car:
continue continue
@@ -735,9 +743,9 @@ class Client:
round((time.monotonic() - self._connect_time) / 60, 2) round((time.monotonic() - self._connect_time) / 60, 2)
) )
) )
self.__Core.clients[self.cid] = None self._core.clients[self.cid] = None
del self.__Core.clients_by_id[self.cid] del self._core.clients_by_id[self.cid]
del self.__Core.clients_by_nick[self.nick] del self._core.clients_by_nick[self.nick]
else: else:
self.log.debug(f"Removing client; Closing connection...") self.log.debug(f"Removing client; Closing connection...")
try: try:

View File

@@ -5,7 +5,7 @@
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock
from logging import Logger from logging import Logger
from typing import Tuple, List, Dict, Optional, Union, Any from typing import Tuple, List, Dict, Optional, Union, Any
@@ -25,19 +25,22 @@ class Client:
self._log = utils.get_logger("client(id: )") self._log = utils.get_logger("client(id: )")
self._addr: Tuple[str, int] = writer.get_extra_info("sockname") self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
self._loop = asyncio.get_event_loop() self._loop = asyncio.get_event_loop()
self.__Core: Core = core self._core: Core = core
self._cid: int = -1 self._cid: int = -1
self._key: str = None self._key: str = None
self.nick: str = None self.nick: str = None
self.roles: str = None self.roles: str = None
self._guest = True self._guest = True
self.__alive = True self.__alive = True
self._synced = False
self._ready = False self._ready = False
self._focus_car = -1 self._focus_car = -1
self._identifiers = [] self._identifiers = []
self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = [] self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = []
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""} self._unicycle: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
self._last_position = {} self._last_position = {}
self._lock = Lock()
async def __gracefully_kick(self): ... async def __gracefully_kick(self): ...
@property @property
def _writer(self) -> StreamWriter: ... def _writer(self) -> StreamWriter: ...
@@ -55,6 +58,8 @@ class Client:
@property @property
def guest(self) -> bool: ... def guest(self) -> bool: ...
@property @property
def synced(self) -> bool: ...
@property
def ready(self) -> bool: ... def ready(self) -> bool: ...
@property @property
def identifiers(self) -> list: ... def identifiers(self) -> list: ...

View File

@@ -4,18 +4,17 @@
# Version 1.5 # Version 1.5
# Core version: 0.4.5 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2024
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
__title__ = 'KuiToi-Server' __title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.' __description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server' __url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.4.5' __version__ = '0.4.6'
__build__ = 2300 # Я это считаю лог файлами __build__ = 2421 # Я это считаю лог файлами
__author__ = 'SantaSpeen' __author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su' __author_email__ = 'admin@kuitoi.su'
__license__ = "FPA" __license__ = "FPA"
__copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)' __copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)'
import asyncio import asyncio
import builtins import builtins
@@ -44,15 +43,14 @@ config_path = "kuitoi.yml"
if args.config: if args.config:
config_path = args.config config_path = args.config
config_provider = ConfigProvider(config_path) config_provider = ConfigProvider(config_path)
config = config_provider.open_config() config = config_provider.read()
builtins.config = config builtins.config = config
config.enc = config.Options['encoding'] config.enc = config.Options['encoding']
if config.Options['debug'] is True: if config.Options['debug'] is True:
utils.set_debug_status() utils.set_debug_status()
log.info("Debug enabled!")
log = get_logger("core.init") log = get_logger("core.init")
log.debug("Debug mode enabled!") log.info("Debug mode enabled!")
log.debug(f"Server config: {config}") log.debug(f"Server config: {config}")
# i18n init # i18n init
log.debug("Initializing i18n...") log.debug("Initializing i18n...")
ml = MultiLanguage() ml = MultiLanguage()
@@ -62,6 +60,7 @@ ml.builtins_hook()
log.debug("Initializing EventsSystem...") log.debug("Initializing EventsSystem...")
ev = EventsSystem() ev = EventsSystem()
ev.builtins_hook() ev.builtins_hook()
ev.register("get_version", lambda _: {"version": __version__, "build": __build__})
log.info(i18n.hello) log.info(i18n.hello)
log.info(i18n.config_path.format(config_path)) log.info(i18n.config_path.format(config_path))
@@ -70,7 +69,7 @@ log.debug("Initializing BeamMP Server system...")
# Key handler.. # Key handler..
if not config.Auth['private'] and not config.Auth['key']: if not config.Auth['private'] and not config.Auth['key']:
log.warn(i18n.auth_need_key) log.warn(i18n.auth_need_key)
url = "https://beammp.com/k/keys" url = "https://keymaster.beammp.com/login"
if shortcuts.yes_no_dialog( if shortcuts.yes_no_dialog(
title='BeamMP Server Key', title='BeamMP Server Key',
text=i18n.GUI_need_key_message, text=i18n.GUI_need_key_message,
@@ -92,7 +91,7 @@ if not config.Auth['private'] and not config.Auth['key']:
text=i18n.GUI_enter_key_message, text=i18n.GUI_enter_key_message,
ok_text=i18n.GUI_ok, ok_text=i18n.GUI_ok,
cancel_text=i18n.GUI_cancel).run() cancel_text=i18n.GUI_cancel).run()
config_provider.save_config() config_provider.save()
if not config.Auth['private'] and not config.Auth['key']: if not config.Auth['private'] and not config.Auth['key']:
log.error(i18n.auth_empty_key) log.error(i18n.auth_empty_key)
log.info(i18n.stop) log.info(i18n.stop)

View File

@@ -27,7 +27,8 @@ class Core:
def __init__(self): def __init__(self):
self.log = utils.get_logger("core") self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop() self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.start_time = time.monotonic() self.start_time = time.monotonic()
self.run = False self.run = False
self.direct = False self.direct = False
@@ -47,17 +48,17 @@ class Core:
self.lock_upload = False self.lock_upload = False
self.client_major_version = "2.0" self.client_major_version = "2.0"
self.BeamMP_version = "3.1.1" # 20.07.2023 self.BeamMP_version = "3.4.1" # 16.07.2024
ev.register_event("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")])) ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
ev.register_event("_get_player", lambda x: self.get_client(**x['kwargs'])) ev.register("_get_player", lambda x: self.get_client(**x['kwargs']))
def get_client(self, cid=None, nick=None): def get_client(self, cid=None, nick=None):
if cid is None and nick is None: if cid is None and nick is None:
return None return None
if cid is not None: if cid is not None:
if cid == -1: if cid == -1:
return [i for i in self.clients if i is not None] return [i for i in self.clients if i is not None and i.synced]
return self.clients_by_id.get(cid) return self.clients_by_id.get(cid)
if nick: if nick:
return self.clients_by_nick.get(nick) return self.clients_by_nick.get(nick)
@@ -103,6 +104,7 @@ class Core:
return out return out
async def check_alive(self): async def check_alive(self):
self.log.debug("Starting alive checker.")
maxp = config.Game['players'] maxp = config.Game['players']
try: try:
while self.run: while self.run:
@@ -138,6 +140,8 @@ class Core:
uvserver.run() uvserver.run()
async def stop_me(self): async def stop_me(self):
if not config.WebAPI['enabled']:
return
while webapp.data_run[0]: while webapp.data_run[0]:
await asyncio.sleep(1) await asyncio.sleep(1)
self.run = False self.run = False
@@ -145,31 +149,44 @@ class Core:
# noinspection SpellCheckingInspection,PyPep8Naming # noinspection SpellCheckingInspection,PyPep8Naming
async def heartbeat(self, test=False): async def heartbeat(self, test=False):
if config.Auth["private"] or self.direct: try:
if test: self.log.debug("Starting heartbeat.")
self.log.info(i18n.core_direct_mode) if config.Auth["private"] or self.direct:
self.direct = True if test:
return self.log.info(i18n.core_direct_mode)
self.direct = True
return
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"] BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
modlist = "" _map = config.Game['map'] if "/" in config.Game['map'] else f"/levels/{config.Game['map']}/info.json"
for mod in self.mods_list: tags = config.Server['tags'].replace(", ", ";").replace(",", ";")
if type(mod) == int: if tags and tags[-1:] != ";":
continue tags += ";"
modlist += f"/{os.path.basename(mod['path'])};" modlist = "".join(f"/{os.path.basename(mod['path'])};" for mod in self.mods_list[1:])
modstotalsize = self.mods_list[0] modstotalsize = self.mods_list[0]
modstotal = len(self.mods_list) - 1 modstotal = len(self.mods_list) - 1
while self.run: while self.run:
try: playerslist = "".join(f"{client.nick};" for client in self.clients if client and client.alive)
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id), data = {
"maxplayers": config.Game["players"], "port": config.Server["server_port"], "uuid": config.Auth["key"],
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'], "players": len(self.clients_by_id),
"version": self.BeamMP_version, "clientversion": self.client_major_version, "maxplayers": config.Game["players"],
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize, "port": config.Server["server_port"],
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False} "map": _map,
"private": config.Auth['private'],
"version": self.BeamMP_version,
"clientversion": self.client_major_version,
"name": config.Server["name"],
"tags": tags,
"guests": not config.Auth["private"],
"modlist": modlist,
"modstotalsize": modstotalsize,
"modstotal": modstotal,
"playerslist": playerslist,
"desc": config.Server['description'],
"pass": False
}
# Sentry?
ok = False
body = {} body = {}
for server_url in BEAM_backend: for server_url in BEAM_backend:
url = "https://" + server_url + "/heartbeat" url = "https://" + server_url + "/heartbeat"
@@ -177,14 +194,15 @@ class Core:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, data=data, headers={"api-v": "2"}) as response: async with session.post(url, data=data, headers={"api-v": "2"}) as response:
code = response.status code = response.status
# text = await response.text()
# self.log.debug(f"[HB] res={text}")
body = await response.json() body = await response.json()
ok = True
break break
except Exception as e: except Exception as e:
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`") self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
continue continue
if ok: if body:
if not (body.get("status") is not None and if not (body.get("status") is not None and
body.get("code") is not None and body.get("code") is not None and
body.get("msg") is not None): body.get("msg") is not None):
@@ -216,11 +234,11 @@ class Core:
# raise KeyboardInterrupt # raise KeyboardInterrupt
if test: if test:
return ok return bool(body)
await asyncio.sleep(5) await asyncio.sleep(5)
except Exception as e: except Exception as e:
self.log.error(f"Error in heartbeat: {e}") self.log.error(f"Error in heartbeat: {e}")
async def kick_cmd(self, args): async def kick_cmd(self, args):
if not len(args) > 0: if not len(args) > 0:

View File

@@ -36,7 +36,7 @@ class Core:
self.web_stop: Callable = lambda: None self.web_stop: Callable = lambda: None
self.lock_upload = False self.lock_upload = False
self.client_major_version = "2.0" self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0" self.BeamMP_version = "3.4.1"
def get_client(self, cid=None, nick=None) -> Client | None: ... def get_client(self, cid=None, nick=None) -> Client | None: ...
async def insert_client(self, client: Client) -> None: ... async def insert_client(self, client: Client) -> None: ...
def create_client(self, *args, **kwargs) -> Client: ... def create_client(self, *args, **kwargs) -> Client: ...

View File

@@ -10,6 +10,7 @@ import traceback
import aiohttp import aiohttp
from core import utils from core import utils
from modules import RateLimiter
# noinspection PyProtectedMember # noinspection PyProtectedMember
@@ -21,6 +22,9 @@ class TCPServer:
self.host = host self.host = host
self.port = port self.port = port
self.run = False self.run = False
self.rl = RateLimiter(50, 10, 300)
console.add_command("rl", self.rl.parse_console, None, "RateLimiter menu",
{"rl": {"info": None, "unban": None, "ban": None, "help": None}})
async def auth_client(self, reader, writer): async def auth_client(self, reader, writer):
client = self.Core.create_client(reader, writer) client = self.Core.create_client(reader, writer)
@@ -31,7 +35,8 @@ class TCPServer:
await client.kick(i18n.core_player_kick_outdated) await client.kick(i18n.core_player_kick_outdated)
return False, client return False, client
else: else:
await client._send(b"S") # Accepted client version # await client._send(b"S") # Accepted client version
await client._send(b"A") # Accepted client version
data = await client._recv(True) data = await client._recv(True)
self.log.debug(f"Key: {data}") self.log.debug(f"Key: {data}")
@@ -53,10 +58,13 @@ class TCPServer:
client.roles = res["roles"] client.roles = res["roles"]
client._guest = res["guest"] client._guest = res["guest"]
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]} client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
if not client._identifiers.get("ip"):
client._identifiers["ip"] = client._addr[0]
# noinspection PyProtectedMember # noinspection PyProtectedMember
client._update_logger() client._update_logger()
except Exception as e: except Exception as e:
self.log.error(f"Auth error: {e}") self.log.error("Auth error.")
self.log.exception(e)
await client.kick(i18n.core_player_kick_auth_server_fail) await client.kick(i18n.core_player_kick_auth_server_fail)
return False, client return False, client
@@ -72,9 +80,9 @@ class TCPServer:
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers) lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
for data in lua_data: for data in lua_data:
if 1 == data: if 1 == data:
allow = True allow = False
elif isinstance(data, str): elif isinstance(data, str):
allow = True allow = False
reason = data reason = data
if not allow: if not allow:
await client.kick(reason) await client.kick(reason)
@@ -86,9 +94,8 @@ class TCPServer:
await client.kick(i18n.core_player_kick_server_full) await client.kick(i18n.core_player_kick_server_full)
return False, client return False, client
else: else:
self.log.info(i18n.core_identifying_okay)
await self.Core.insert_client(client) await self.Core.insert_client(client)
client.log.info(i18n.core_player_set_id.format(client.pid)) client.log.info(i18n.core_identifying_okay)
return True, client return True, client
@@ -101,7 +108,7 @@ class TCPServer:
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!") self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
else: else:
writer.close() writer.close()
self.log.debug(f"Unknown client id:{cid} - HandleDownload") self.log.debug(f"Unknown client <nick>:{cid} - HandleDownload")
finally: finally:
return return
@@ -127,6 +134,11 @@ class TCPServer:
async def handle_client(self, reader, writer): async def handle_client(self, reader, writer):
while True: while True:
try: try:
ip = writer.get_extra_info('peername')[0]
if self.rl.is_banned(ip):
await self.rl.notify(ip, writer)
writer.close()
break
data = await reader.read(1) data = await reader.read(1)
if not data: if not data:
break break
@@ -137,7 +149,6 @@ class TCPServer:
_, cl = await self.handle_code(code, reader, writer) _, cl = await self.handle_code(code, reader, writer)
if cl: if cl:
await cl._remove_me() await cl._remove_me()
del cl
break break
except Exception as e: except Exception as e:
self.log.error("Error while handling connection...") self.log.error("Error while handling connection...")

View File

@@ -10,6 +10,7 @@ from typing import Tuple
from core import utils, Core from core import utils, Core
from core.Client import Client from core.Client import Client
from modules import RateLimiter
class TCPServer: class TCPServer:
@@ -20,6 +21,8 @@ class TCPServer:
self.host = host self.host = host
self.port = port self.port = port
self.run = False self.run = False
self.rl = RateLimiter(50, 10, 15)
async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ... async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ... async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ...
async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ... async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...

View File

@@ -3,7 +3,7 @@
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.5 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2024
import asyncio import asyncio
import json import json
@@ -18,7 +18,7 @@ class UDPServer(asyncio.DatagramTransport):
super().__init__() super().__init__()
self.log = utils.get_logger("UDPServer") self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.Core = core self._core = core
self.host = host self.host = host
self.port = port self.port = port
self.run = False self.run = False
@@ -33,8 +33,10 @@ class UDPServer(asyncio.DatagramTransport):
code = data[2:3].decode() code = data[2:3].decode()
data = data[2:].decode() data = data[2:].decode()
client = self.Core.get_client(cid=cid) client = self._core.get_client(cid=cid)
if client: if client:
if not client.alive:
self.log.debug(f"{client.nick}:{cid} still sending UDP data: {data}")
match code: match code:
case "p": # Ping packet case "p": # Ping packet
ev.call_event("onSentPing") ev.call_event("onSentPing")
@@ -45,24 +47,26 @@ class UDPServer(asyncio.DatagramTransport):
self.log.debug(f"Set UDP Sock for CID: {cid}") self.log.debug(f"Set UDP Sock for CID: {cid}")
ev.call_event("onChangePosition", data=data) ev.call_event("onChangePosition", data=data)
sub = data.find("{", 1) sub = data.find("{", 1)
last_pos_data = data[sub:] last_pos = data[sub:]
try: try:
last_pos = json.loads(last_pos_data)
client._last_position = last_pos
_, car_id = client._get_cid_vid(data) _, car_id = client._get_cid_vid(data)
client._cars[car_id]['pos'] = last_pos if client._cars[car_id]:
last_pos = json.loads(last_pos)
client._last_position = last_pos
client._cars[car_id]['pos'] = last_pos
except Exception as e: except Exception as e:
self.log.debug(f"Cannot parse position packet: {e}") self.log.warning(f"Cannot parse position packet: {e}")
self.log.debug(f"data: {data}, sup: {sub}") self.log.debug(f"data: '{data}', sup: {sub}")
self.log.debug(f"last_pos_data: {last_pos_data}") self.log.debug(f"last_pos ({type(last_pos)}): {last_pos}")
await client._send(data, to_all=True, to_self=False, to_udp=True)
case "X":
await client._send(data, to_all=True, to_self=False, to_udp=True) await client._send(data, to_all=True, to_self=False, to_udp=True)
case _: case _:
self.log.debug(f"[{cid}] Unknown code: {code}") self.log.warning(f" UDP [{cid}] Unknown code: {code}; {data}")
else: else:
self.log.debug(f"[{cid}] Client not found.") self.log.debug(f"[{cid}] Client not found.")
except Exception as e: except Exception as e:
self.log.error(f"Error handle_datagram: {e}") self.log.error(f"Error handle_datagram: {e}")
def datagram_received(self, *args, **kwargs): def datagram_received(self, *args, **kwargs):
@@ -81,14 +85,14 @@ class UDPServer(asyncio.DatagramTransport):
async def _start(self): async def _start(self):
self.log.debug("Starting UDP server.") self.log.debug("Starting UDP server.")
while self.Core.run: while self._core.run:
try: try:
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
d = UDPServer d = UDPServer
self.transport, p = await self.loop.create_datagram_endpoint( self.transport, p = await self.loop.create_datagram_endpoint(
lambda: d(self.Core), lambda: d(self._core),
local_addr=(self.host, self.port) local_addr=(self.host, self.port)
) )
d.transport = self.transport d.transport = self.transport
@@ -109,4 +113,5 @@ class UDPServer(asyncio.DatagramTransport):
def _stop(self): def _stop(self):
self.log.debug("Stopping UDP server") self.log.debug("Stopping UDP server")
self.transport.close() if self.transport:
self.transport.close()

View File

@@ -18,7 +18,7 @@ class UDPServer(asyncio.DatagramTransport):
def __init__(self, core: Core, host=None, port=None, transport=None): def __init__(self, core: Core, host=None, port=None, transport=None):
self.log = utils.get_logger("UDPServer") self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.Core = core self._core = core
self.host = host self.host = host
self.port = port self.port = port
self.run = False self.run = False

View File

@@ -1,9 +1,72 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File modules.config_provider.__init__.py # File modules.ConfigProvider
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.0 # Version 1.0
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
from .config_provider import ConfigProvider, Config import os
import secrets
import yaml
class Config:
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=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)}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"access_token": secrets.token_hex(16)}
def __repr__(self):
return 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}, WebAPI={self.WebAPI!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("Empty config?..")
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']
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(_config, f)

View File

@@ -1,53 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.config_provider.config_provider.py
# Written by: SantaSpeen
# Version 1.1
# Licence: FPA
# (c) kuitoi.su 2023
import os
import secrets
import yaml
class Config:
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
"server_ip": "0.0.0.0", "server_port": 30814}
self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383,
"password": secrets.token_hex(16)}
self.Options = options or {"language": "en", "encoding": "utf-8", "speed_limit": 0, "use_queue": False,
"debug": False, "use_lua": False, "log_chat": True}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"access_token": secrets.token_hex(16)}
def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class ConfigProvider:
def __init__(self, config_path):
self.config_path = config_path
self.config = Config()
def open_config(self):
if not os.path.exists(self.config_path):
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.config, f)
try:
with open(self.config_path, "r", encoding="utf-8") as f:
self.config = yaml.load(f.read(), yaml.Loader)
except yaml.YAMLError:
print("You have errors in the YAML syntax.")
print("Stopping server.")
exit(1)
return self.config
def save_config(self):
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.config, f)

View File

@@ -1,6 +1,6 @@
from typing import Dict ### Builtins
```python
class Config: class Config:
Auth: Dict[str, object] Auth: Dict[str, object]
Game: Dict[str, object] Game: Dict[str, object]
@@ -12,3 +12,4 @@ class Config:
def __repr__(self): def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server) return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class config (Config): ... class config (Config): ...
```

View File

@@ -1,9 +1,271 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File modules.console.__init__.py # File modules.ConsoleSystem
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.0 # Version 1.2
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
from .console_system import Console import builtins
import inspect
import logging
from typing import AnyStr
from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter
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 Console:
def __init__(self,
prompt_in="> ",
prompt_out="",
not_found="Command \"%s\" not found in alias.",
debug=False) -> None:
self.__logger = get_logger("console")
self.__is_run = False
self.no_cmd = False
self.__prompt_in = prompt_in
self.__prompt_out = prompt_out
self.__not_found = not_found
self.__is_debug = debug
self.__print = print
self.__func = dict()
self.__alias = dict()
self.__man = dict()
self.__desc = dict()
self.__print_logger = get_logger("print")
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
custom_completer={"man": {}})
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
custom_completer={"help": {"--raw": None}})
self.completer = NestedCompleter.from_nested_dict(self.__alias)
rcon = RCONSystem
rcon.console = self
self.rcon = rcon
def __debug(self, *x):
self.__logger.debug(f"{x}")
# if self.__is_debug:
# x = list(x)
# x.insert(0, "\r CONSOLE DEBUG:")
# self.__print(*x)
def __getitem__(self, item):
print(item)
@staticmethod
def __get_max_len(arg) -> int:
i = 0
arg = list(arg)
for a in arg:
ln = len(str(a))
if ln > i:
i = ln
return i
def __create_man_message(self, argv: list) -> AnyStr:
if len(argv) == 0:
return self.__man.get("man")
x = argv[0]
if self.__alias.get(x) is None:
return i18n.man_command_not_found.format(x)
man_message = self.__man.get(x)
if man_message:
return man_message
else:
return i18n.man_message_not_found
# noinspection PyStringFormat
def __create_help_message(self, argv: list) -> AnyStr:
self.__debug("creating help message")
raw = False
max_len_v = 0
if "--raw" in argv:
max_len_v = self.__get_max_len(self.__func.values())
print()
raw = True
message = "\n"
max_len = self.__get_max_len(self.__func.keys())
if max_len < 7:
max_len = 7
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
else:
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
for k, v in self.__func.items():
doc = self.__desc.get(k)
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
else:
if doc is None:
doc = i18n.help_message_not_found
message += f" %-{max_len}s : %s\n" % (k, doc)
return message
def __update_completer(self):
self.completer = NestedCompleter.from_nested_dict(self.__alias)
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
key = key.format(" ", "-")
if not isinstance(key, str):
raise TypeError("key must be string")
self.__debug(f"added user command: key={key}; func={func};")
self.__alias.update(custom_completer or {key: None})
self.__alias["man"].update({key: None})
self.__func.update({key: {"f": func}})
self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man}</seagreen>' if man else None})
self.__desc.update({key: desc})
self.__update_completer()
return self.__alias.copy()
def _write(self, t):
if self.no_cmd:
print(t)
return
try:
if t.startswith("html:"):
print_formatted_text(HTML(t[5:]))
else:
print_formatted_text(t)
except NoConsoleScreenBufferError:
print("Works in non cmd mode.")
self.no_cmd = True
print(t)
def write(self, s: AnyStr):
if isinstance(s, (list, tuple)):
for text in s:
self._write(text)
else:
self._write(s)
def log(self, s: AnyStr) -> None:
if isinstance(s, (list, tuple)):
for text in s:
self.__logger.info(f"{text}")
else:
self.__logger.info(f"{s}")
# self.write(s)
def __lshift__(self, s: AnyStr) -> None:
self.write(s)
@property
def alias(self) -> dict:
return self.__alias.copy()
def __builtins_print(self,
*values: object,
sep: str or None = " ",
end: str or None = None,
file: str or None = None,
flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
val = list(values)
if len(val) > 0:
if self.__is_run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else:
if end is None:
end = "\n"
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
def logger_hook(self) -> None:
self.__debug("used logger_hook")
def emit(cls, record):
try:
msg = cls.format(record)
if cls.stream.name == "<stderr>":
self.write(f"\r{msg}")
else:
cls.stream.write(msg + cls.terminator)
cls.flush()
except RecursionError:
raise
except Exception as e:
cls.handleError(record)
logging.StreamHandler.emit = emit
def builtins_hook(self) -> None:
self.__debug("used builtins_hook")
builtins.Console = Console
builtins.console = self
# builtins.print = self.__builtins_print
async def read_input(self):
session = PromptSession(history=FileHistory('./.cmdhistory'))
while True:
try:
with patch_stdout():
if self.no_cmd:
cmd_in = input(self.__prompt_in)
else:
try:
cmd_in = await session.prompt_async(
self.__prompt_in,
completer=self.completer,
auto_suggest=AutoSuggestFromHistory()
)
except NoConsoleScreenBufferError:
print("Works in non cmd mode.")
self.no_cmd = True
cmd_s = cmd_in.split(" ")
cmd = cmd_s[0]
if cmd == "":
continue
else:
found_in_lua = False
d = ev.call_lua_event("onConsoleInput", cmd_in)
if len(d) > 0:
for text in d:
if text is not None:
found_in_lua = True
self.log(text)
command_object = self.__func.get(cmd)
if command_object:
func = command_object['f']
if inspect.iscoroutinefunction(func):
out = await func(cmd_s[1:])
else:
out = func(cmd_s[1:])
if out:
self.log(out)
else:
if not found_in_lua:
self.log(self.__not_found % cmd)
except KeyboardInterrupt:
raise KeyboardInterrupt
except Exception as e:
print(f"Error in console.py: {e}")
self.__logger.exception(e)
async def start(self):
self.__is_run = True
await self.read_input()
def stop(self, *args, **kwargs):
self.__is_run = False
raise KeyboardInterrupt

View File

@@ -1,268 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.ConsoleSystem.console_system.py
# Written by: SantaSpeen
# Version 1.2
# Licence: FPA
# (c) kuitoi.su 2023
import builtins
import inspect
import logging
from typing import AnyStr
from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.history import FileHistory
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
from prompt_toolkit.patch_stdout import patch_stdout
from core import get_logger
from modules.ConsoleSystem.RCON import RCONSystem
class Console:
def __init__(self,
prompt_in="> ",
prompt_out="",
not_found="Command \"%s\" not found in alias.",
debug=False) -> None:
self.__logger = get_logger("console")
self.__is_run = False
self.no_cmd = False
self.__prompt_in = prompt_in
self.__prompt_out = prompt_out
self.__not_found = not_found
self.__is_debug = debug
self.__print = print
self.__func = dict()
self.__alias = dict()
self.__man = dict()
self.__desc = dict()
self.__print_logger = get_logger("print")
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
custom_completer={"man": {}})
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
custom_completer={"help": {"--raw": None}})
self.completer = NestedCompleter.from_nested_dict(self.__alias)
rcon = RCONSystem
rcon.console = self
self.rcon = rcon
def __debug(self, *x):
self.__logger.debug(f"{x}")
# if self.__is_debug:
# x = list(x)
# x.insert(0, "\r CONSOLE DEBUG:")
# self.__print(*x)
def __getitem__(self, item):
print(item)
@staticmethod
def __get_max_len(arg) -> int:
i = 0
arg = list(arg)
for a in arg:
ln = len(str(a))
if ln > i:
i = ln
return i
def __create_man_message(self, argv: list) -> AnyStr:
if len(argv) == 0:
return self.__man.get("man")
x = argv[0]
if self.__alias.get(x) is None:
return i18n.man_command_not_found.format(x)
man_message = self.__man.get(x)
if man_message:
return man_message
else:
return i18n.man_message_not_found
# noinspection PyStringFormat
def __create_help_message(self, argv: list) -> AnyStr:
self.__debug("creating help message")
raw = False
max_len_v = 0
if "--raw" in argv:
max_len_v = self.__get_max_len(self.__func.values())
print()
raw = True
message = "\n"
max_len = self.__get_max_len(self.__func.keys())
if max_len < 7:
max_len = 7
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
else:
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
for k, v in self.__func.items():
doc = self.__desc.get(k)
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
else:
if doc is None:
doc = i18n.help_message_not_found
message += f" %-{max_len}s : %s\n" % (k, doc)
return message
def __update_completer(self):
self.completer = NestedCompleter.from_nested_dict(self.__alias)
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
key = key.format(" ", "-")
if not isinstance(key, str):
raise TypeError("key must be string")
self.__debug(f"added user command: key={key}; func={func};")
self.__alias.update(custom_completer or {key: None})
self.__alias["man"].update({key: None})
self.__func.update({key: {"f": func}})
self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man}</seagreen>' if man else None})
self.__desc.update({key: desc})
self.__update_completer()
return self.__alias.copy()
def _write(self, t):
if self.no_cmd:
print(t)
return
try:
if t.startswith("html:"):
print_formatted_text(HTML(t[5:]))
else:
print_formatted_text(t)
except NoConsoleScreenBufferError:
print("Works in non cmd mode.")
self.no_cmd = True
print(t)
def write(self, s: AnyStr):
if isinstance(s, (list, tuple)):
for text in s:
self._write(text)
else:
self._write(s)
def log(self, s: AnyStr) -> None:
if isinstance(s, (list, tuple)):
for text in s:
self.__logger.info(f"{text}")
else:
self.__logger.info(f"{s}")
# self.write(s)
def __lshift__(self, s: AnyStr) -> None:
self.write(s)
@property
def alias(self) -> dict:
return self.__alias.copy()
def __builtins_print(self,
*values: object,
sep: str or None = " ",
end: str or None = None,
file: str or None = None,
flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
val = list(values)
if len(val) > 0:
if self.__is_run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else:
if end is None:
end = "\n"
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
def logger_hook(self) -> None:
self.__debug("used logger_hook")
def emit(cls, record):
try:
msg = cls.format(record)
if cls.stream.name == "<stderr>":
self.write(f"\r{msg}")
else:
cls.stream.write(msg + cls.terminator)
cls.flush()
except RecursionError:
raise
except Exception as e:
cls.handleError(record)
logging.StreamHandler.emit = emit
def builtins_hook(self) -> None:
self.__debug("used builtins_hook")
builtins.Console = Console
builtins.console = self
# builtins.print = self.__builtins_print
async def read_input(self):
session = PromptSession(history=FileHistory('./.cmdhistory'))
while True:
try:
with patch_stdout():
if self.no_cmd:
cmd_in = input(self.__prompt_in)
else:
try:
cmd_in = await session.prompt_async(
self.__prompt_in,
completer=self.completer,
auto_suggest=AutoSuggestFromHistory()
)
except NoConsoleScreenBufferError:
print("Works in non cmd mode.")
self.no_cmd = True
cmd_s = cmd_in.split(" ")
cmd = cmd_s[0]
if cmd == "":
continue
else:
found_in_lua = False
d = ev.call_lua_event("onConsoleInput", cmd_in)
if len(d) > 0:
for text in d:
if text is not None:
found_in_lua = True
self.log(text)
command_object = self.__func.get(cmd)
if command_object:
func = command_object['f']
if inspect.iscoroutinefunction(func):
out = await func(cmd_s[1:])
else:
out = func(cmd_s[1:])
if out:
self.log(out)
else:
if not found_in_lua:
self.log(self.__not_found % cmd)
except KeyboardInterrupt:
raise KeyboardInterrupt
except Exception as e:
print(f"Error in console.py: {e}")
self.__logger.exception(e)
async def start(self):
self.__is_run = True
await self.read_input()
def stop(self, *args, **kwargs):
self.__is_run = False
raise KeyboardInterrupt

View File

@@ -1,18 +1,10 @@
from logging import Logger ### Builtins
from typing import AnyStr
from core import get_logger
```python
class RCONSystem: class RCONSystem:
console = None console = None
def __init__(self, key, host, port): def __init__(self, key, host, port): ...
self.log = get_logger("RCON")
self.key = key
self.host = host
self.port = port
async def start(self): ... async def start(self): ...
async def stop(self): ... async def stop(self): ...
@@ -35,3 +27,4 @@ class console:
def write(s: str) -> None: ... def write(s: str) -> None: ...
@staticmethod @staticmethod
def __lshift__(s: AnyStr) -> None: ... def __lshift__(s: AnyStr) -> None: ...
```

View File

@@ -1 +1,163 @@
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
"onChatReceive": [], # (!) With handler
"onCarSpawn": [], # (!) With handler
"onCarDelete": [], # (!) With handler (admin allow)
"onCarEdited": [], # (!) With handler
"onCarReset": [], # No handler
"onCarChanged": [], # No handler
"onCarFocusMove": [], # No handler
"onSentPing": [], # Only sync, no handler
"onChangePosition": [], # Only sync, no handler
"onPlayerDisconnect": [], # No handler
"onServerStopped": [], # No handler
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onCarChanged": [],
"onCarFocusMove": [],
"onPlayerDisconnect": [],
"onServerStopped": []
}
self.__lua_events = {
"onInit": [], # onServerStarted
"onShutdown": [], # onServerStopped
"onPlayerAuth": [], # onPlayerAuthenticated
"onPlayerConnecting": [], # No
"onPlayerJoining": [], # No
"onPlayerJoin": [], # onPlayerJoin
"onPlayerDisconnect": [], # onPlayerDisconnect
"onChatMessage": [], # onChatReceive
"onVehicleSpawn": [], # onCarSpawn
"onVehicleEdited": [], # onCarEdited
"onVehicleDeleted": [], # onCarDelete
"onVehicleReset": [], # onCarReset
"onFileChanged": [], # TODO lua onFileChanged
"onConsoleInput": [], # kt.add_command
}
self.register_event = self.register
def builtins_hook(self):
self.log.debug("used builtins_hook")
builtins.ev = self
def is_event(self, event_name):
return (event_name in self.__async_events.keys() or
event_name in self.__events.keys() or
event_name in self.__lua_events.keys())
def register(self, event_name, event_func, async_event=False, lua=None):
self.log.debug(f"register(event_name='{event_name}', event_func='{event_func}', "
f"async_event={async_event}, lua_event={lua}):")
if lua:
if event_name not in self.__lua_events:
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
else:
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
self.log.debug("Register ok")
return
if not callable(event_func):
self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
return
if async_event or inspect.iscoroutinefunction(event_func):
if event_name not in self.__async_events:
self.__async_events.update({str(event_name): [event_func]})
else:
self.__async_events[event_name].append(event_func)
self.log.debug("Register ok")
else:
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
else:
self.__events[event_name].append(event_func)
self.log.debug("Register ok")
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling async event: '{event_name}'")
funcs_data = []
if event_name in self.__async_events.keys():
for func in self.__async_events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
data = await func(event_data)
funcs_data.append(data)
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
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, *args, **kwargs):
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():
for func in self.__events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
funcs_data.append(func(event_data))
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
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,162 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.EventsSystem.events_system.py
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import builtins
import inspect
from core import get_logger
# noinspection PyShadowingBuiltins
class EventsSystem:
def __init__(self):
# TODO: default events
self.log = get_logger("EventsSystem")
self.loop = asyncio.get_event_loop()
self.as_tasks = []
self.__events = {
"onServerStarted": [], # No handler
"onPlayerSentKey": [], # Only sync, no handler
"onPlayerAuthenticated": [], # (!) Only sync, With handler
"onPlayerJoin": [], # (!) With handler
"onChatReceive": [], # (!) With handler
"onCarSpawn": [], # (!) With handler
"onCarDelete": [], # (!) With handler (admin allow)
"onCarEdited": [], # (!) With handler
"onCarReset": [], # No handler
"onCarChanged": [], # No handler
"onCarFocusMove": [], # No handler
"onSentPing": [], # Only sync, no handler
"onChangePosition": [], # Only sync, no handler
"onPlayerDisconnect": [], # No handler
"onServerStopped": [], # No handler
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onCarChanged": [],
"onCarFocusMove": [],
"onPlayerDisconnect": [],
"onServerStopped": []
}
self.__lua_events = {
"onInit": [], # onServerStarted
"onShutdown": [], # onServerStopped
"onPlayerAuth": [], # onPlayerAuthenticated
"onPlayerConnecting": [], # No
"onPlayerJoining": [], # No
"onPlayerJoin": [], # onPlayerJoin
"onPlayerDisconnect": [], # onPlayerDisconnect
"onChatMessage": [], # onChatReceive
"onVehicleSpawn": [], # onCarSpawn
"onVehicleEdited": [], # onCarEdited
"onVehicleDeleted": [], # onCarDelete
"onVehicleReset": [], # onCarReset
"onFileChanged": [], # TODO lua onFileChanged
"onConsoleInput": [], # kt.add_command
}
def builtins_hook(self):
self.log.debug("used builtins_hook")
builtins.ev = self
def 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_event(self, event_name, event_func, async_event=False, lua=None):
self.log.debug(f"register_event(event_name='{event_name}', event_func='{event_func}', "
f"async_event={async_event}, lua_event={lua}):")
if lua:
if event_name not in self.__lua_events:
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
else:
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
self.log.debug("Register ok")
return
if not callable(event_func):
self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
return
if async_event or inspect.iscoroutinefunction(event_func):
if event_name not in self.__async_events:
self.__async_events.update({str(event_name): [event_func]})
else:
self.__async_events[event_name].append(event_func)
self.log.debug("Register ok")
else:
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
else:
self.__events[event_name].append(event_func)
self.log.debug("Register ok")
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling async event: '{event_name}'")
funcs_data = []
if event_name in self.__async_events.keys():
for func in self.__async_events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
data = await func(event_data)
funcs_data.append(data)
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
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, *args, **kwargs):
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():
for func in self.__events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
funcs_data.append(func(event_data))
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
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}'")
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,9 +1,9 @@
from typing import Any ### Builtins
```python
class EventsSystem: class EventsSystem:
@staticmethod @staticmethod
def register_event(event_name, event_func, async_event: bool = False, lua: bool | object = None): ... def register(event_name, event_func, async_event: bool = False, lua: bool | object = None): ...
@staticmethod @staticmethod
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ... async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
@staticmethod @staticmethod
@@ -11,3 +11,4 @@ class EventsSystem:
@staticmethod @staticmethod
def call_lua_event(event_name, *data) -> list[Any]: ... def call_lua_event(event_name, *data) -> list[Any]: ...
class ev(EventsSystem): ... class ev(EventsSystem): ...
```

View File

@@ -1 +1,219 @@
from .plugins_loader import PluginsLoader # -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.PluginsLoader
# 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(self, event_name, event_func):
self.log.debug(f"Registering event {event_name}")
ev.register(event_name, event_func)
def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called event {event_name}")
return ev.call_event(event_name, *args, **kwargs)
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called async event {event_name}")
return await ev.call_async_event(event_name, *args, **kwargs)
def call_lua_event(self, event_name, *args):
self.log.debug(f"Called lua event {event_name}")
return ev.call_lua_event(event_name, *args)
def get_player(self, pid=None, nick=None, cid=None):
self.log.debug("Requests get_player")
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
def get_players(self):
self.log.debug("Requests get_players")
return self.get_player(-1)
def players_counter(self):
self.log.debug("Requests players_counter")
return len(self.get_players())
def is_player_connected(self, pid=None, nick=None):
self.log.debug("Requests is_player_connected")
if pid < 0:
return False
return bool(self.get_player(cid=pid, nick=nick))
def add_command(self, key, func, man, desc, custom_completer) -> dict:
self.log.debug("Requests add_command")
return console.add_command(key, func, man, desc, custom_completer)
class PluginsLoader:
def __init__(self, plugins_dir):
self.loop = asyncio.get_event_loop()
self.plugins = {}
self.plugins_tasks = []
self.plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader")
self.loaded_str = "Plugins: "
ev.register("_plugins_start", self.start)
ev.register("_plugins_unload", self.unload)
ev.register("_plugins_get", lambda x: list(self.plugins.keys()))
console.add_command("plugins", lambda x: self.loaded_str[:-2])
console.add_command("pl", lambda x: self.loaded_str[:-2])
async def load(self):
self.log.debug("Loading plugins...")
for file in os.listdir(self.plugins_dir):
file_path = os.path.join(self.plugins_dir, file)
if os.path.isfile(file_path) and file.endswith(".py"):
try:
self.log.debug(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.print = print
plugin.__file__ = file_path
with open(f'{file_path}', 'r', encoding=config.enc) as f:
code = f.read()
exec(code, plugin.__dict__)
ok = True
try:
is_func = inspect.isfunction
if not is_func(plugin.load):
self.log.error(i18n.plugins_not_found_load)
ok = False
if not is_func(plugin.start):
self.log.error(i18n.plugins_not_found_start)
ok = False
if not is_func(plugin.unload):
self.log.error(i18n.plugins_not_found_unload)
ok = False
if type(plugin.kt) != KuiToi:
self.log.error(i18n.plugins_kt_invalid)
ok = False
except AttributeError:
ok = False
if not ok:
self.log.error(i18n.plugins_invalid.format(file_path))
return
pl_name = plugin.kt.name
if self.plugins.get(pl_name) is not None:
raise NameError(f'Having plugins with identical names is not allowed; '
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
plugin.open = plugin.kt.open
is_coro_func = inspect.iscoroutinefunction
self.plugins.update(
{
pl_name: {
"plugin": plugin,
"load": {
"func": plugin.load,
"async": is_coro_func(plugin.load)
},
"start": {
"func": plugin.start,
"async": is_coro_func(plugin.start)
},
"unload": {
"func": plugin.unload,
"async": is_coro_func(plugin.unload)
}
}
}
)
if self.plugins[pl_name]["load"]['async']:
plugin.log.debug(f"I'm async")
await plugin.load()
else:
plugin.log.debug(f"I'm sync")
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
th.start()
th.join()
self.loaded_str += f"{pl_name}:ok, "
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
except Exception as e:
self.loaded_str += f"{file}:no, "
self.log.error(i18n.plugins_error_loading.format(file, f"{e}"))
self.log.exception(e)
async def start(self, _):
for pl_name, pl_data in self.plugins.items():
try:
if pl_data['start']['async']:
self.log.debug(f"Start async plugin: {pl_name}")
t = self.loop.create_task(pl_data['start']['func']())
self.plugins_tasks.append(t)
else:
self.log.debug(f"Start sync plugin: {pl_name}")
th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}")
th.start()
self.plugins_tasks.append(th)
except Exception as e:
self.log.exception(e)
async def unload(self, _):
for pl_name, pl_data in self.plugins.items():
try:
if pl_data['unload']['async']:
self.log.debug(f"Unload async plugin: {pl_name}")
await pl_data['unload']['func']()
else:
self.log.debug(f"Unload sync plugin: {pl_name}")
th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}")
th.start()
th.join()
except Exception as e:
self.log.exception(e)

View File

@@ -88,7 +88,7 @@ class MP:
def RegisterEvent(self, event_name: str, function_name: str) -> None: def RegisterEvent(self, event_name: str, function_name: str) -> None:
self.log.debug("request MP.RegisterEvent()") self.log.debug("request MP.RegisterEvent()")
ev.register_event(event_name, function_name, lua=self._lua) ev.register(event_name, function_name, lua=self._lua)
if event_name not in self._local_events: if event_name not in self._local_events:
self._local_events.update({str(event_name): [function_name]}) self._local_events.update({str(event_name): [function_name]})
else: else:
@@ -109,8 +109,9 @@ class MP:
del self._event_timers[event_name] del self._event_timers[event_name]
def TriggerLocalEvent(self, event_name, *args): def TriggerLocalEvent(self, event_name, *args):
self.log.debug("request TriggerLocalEvent()") if event_name != "getTable":
self.log.debug(f"Calling local lua event: '{event_name}'") self.log.debug("request TriggerLocalEvent()")
self.log.debug(f"Calling local lua event: '{event_name}{args}'")
funcs_data = [] funcs_data = []
if event_name in self._local_events.keys(): if event_name in self._local_events.keys():
for func_name in self._local_events[event_name]: for func_name in self._local_events[event_name]:
@@ -157,7 +158,7 @@ class MP:
to_all = False to_all = False
if player_id < 0: if player_id < 0:
to_all = True to_all = True
client = client[0] client = client[0] if len(client) > 0 else None
if client and event_name and data: if client and event_name and data:
t = self.loop.create_task(client.send_event(event_name, data, to_all=to_all)) t = self.loop.create_task(client.send_event(event_name, data, to_all=to_all))
self.tasks.append(t) self.tasks.append(t)
@@ -168,7 +169,7 @@ class MP:
return False, "Can't found event_name or data" return False, "Can't found event_name or data"
def TriggerClientEventJson(self, player_id, event_name, data): def TriggerClientEventJson(self, player_id, event_name, data):
self.log.debug("request TriggerClientEventJson()") self.log.debug(f"request TriggerClientEventJson({player_id, event_name, data})")
data = self._lua.globals().Util.JsonEncode(data) data = self._lua.globals().Util.JsonEncode(data)
self.TriggerClientEvent(player_id, event_name, data) self.TriggerClientEvent(player_id, event_name, data)
@@ -232,8 +233,8 @@ class MP:
def GetPlayers(self): def GetPlayers(self):
self.log.debug("request GetPlayers()") self.log.debug("request GetPlayers()")
clients = ev.call_event("_get_players", cid=-1) clients = ev.call_event("_get_player", cid=-1)[0]
return self._lua.table_from(clients) return self._lua.table_from([i.nick for i in clients])
def IsPlayerGuest(self, player_id) -> bool: def IsPlayerGuest(self, player_id) -> bool:
self.log.debug("request IsPlayerGuest()") self.log.debug("request IsPlayerGuest()")
@@ -312,12 +313,18 @@ class Util:
return {k: v for k, v in new_dict.items() if v is not None} return {k: v for k, v in new_dict.items() if v is not None}
def JsonEncode(self, table): def JsonEncode(self, table):
self.log.debug("requesting JsonEncode()") data = {}
if all(isinstance(k, int) for k in table.keys()): try:
data = self._recursive_list_encode(table) self.log.debug("requesting JsonEncode()")
else: if all(isinstance(k, int) for k in table.keys()):
data = self._recursive_dict_encode(table) data = self._recursive_list_encode(table)
return json.dumps(data) else:
data = self._recursive_dict_encode(table)
except Exception as e:
self.log.exception(e)
data = json.dumps(data)
self.log.debug(f"Encoded: {data}")
return data
def JsonDecode(self, string): def JsonDecode(self, string):
self.log.debug("requesting JsonDecode()") self.log.debug("requesting JsonDecode()")
@@ -570,8 +577,8 @@ class LuaPluginsLoader:
self.lua_dirs = set() self.lua_dirs = set()
self.log = get_logger("LuaPluginsLoader") self.log = get_logger("LuaPluginsLoader")
self.loaded_str = "Lua plugins: " self.loaded_str = "Lua plugins: "
ev.register_event("_lua_plugins_get", lambda x: self.lua_plugins) ev.register("_lua_plugins_get", lambda x: self.lua_plugins)
ev.register_event("_lua_plugins_unload", self.unload) ev.register("_lua_plugins_unload", self.unload)
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2]) console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
console.add_command("lua_pl", lambda x: self.loaded_str[:-2]) console.add_command("lua_pl", lambda x: self.loaded_str[:-2])
@@ -591,7 +598,7 @@ class LuaPluginsLoader:
"LogChat": config.Options['log_chat'], "LogChat": config.Options['log_chat'],
"Debug": config.Options['debug'], "Debug": config.Options['debug'],
"Private": config.Auth['private'], "Private": config.Auth['private'],
"MaxCars": config.Game['max_cars'], "MaxCars": config.Game['cars'],
"MaxPlayers": config.Game['players'], "MaxPlayers": config.Game['players'],
"Map": f"/levels/{config.Game['map']}/info.json", "Map": f"/levels/{config.Game['map']}/info.json",
"Description": config.Server['description'], "Description": config.Server['description'],

View File

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

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

@@ -13,3 +13,4 @@ from .EventsSystem import EventsSystem
from .PluginsLoader import PluginsLoader from .PluginsLoader import PluginsLoader
from .WebAPISystem import web_app from .WebAPISystem import web_app
from .WebAPISystem import _stop as stop_web from .WebAPISystem import _stop as stop_web
from .RateLimiter import RateLimiter

View File

@@ -1,9 +1,160 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File modules.i18n.__init__.py # File modules.i18n
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.0 # Version 1.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
from .i18n import MultiLanguage import builtins
import json
import os
from json import JSONDecodeError
import requests
from core.utils import get_logger
class i18n:
data = {}
def __init__(self, data):
i18n.data = data
def __getattribute__(self, key):
return i18n.data[key]
class MultiLanguage:
def __init__(self, language: str = None, files_dir="translates/", encoding=None):
if encoding is None:
encoding = config.enc
if language is None:
language = "en"
self.__data = {
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization completed.",
"start": "Server started!",
"stop": "Server stopped!",
"auth_need_key": "BeamMP key is required to run!",
"auth_empty_key": "BeamMP key is empty!",
"auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}",
"GUI_yes": "Yes",
"GUI_no": "No",
"GUI_ok": "OK",
"GUI_cancel": "Cancel",
"GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?",
"GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"web_start": "WebAPI started on {} (CTRL+C to stop)",
"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,152 +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
import os
from json import JSONDecodeError
from core.utils import get_logger
class i18n:
data = {}
def __init__(self, data):
i18n.data = data
def __getattribute__(self, key):
return i18n.data[key]
class MultiLanguage:
def __init__(self, language: str = None, files_dir="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: {}",
"web_start": "WebAPI started on {} (CTRL+C to stop)",
"core_bind_failed": "Failed to bind port. Error: {}",
"core_direct_mode": "Server started in direct connection mode.",
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
"core_auth_server_no_response": "Failed to authenticate the server.",
"core_mods_loaded": "Loaded {} mods. {}Mb",
"core_identifying_connection": "Processing new connection...",
"core_player_kick_outdated": "Incorrect version of BeamMP.",
"core_player_kick_bad_key": "Invalid key passed!",
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
"core_player_kick_server_full": "Server is full.",
"core_player_set_id": "Player set ID {}",
"core_identifying_okay": "Successful login.",
"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.set_language(language)
def set_language(self, language="en"):
if self.language == language and self.fi:
return
else:
self.fi = True
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:
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,3 +1,6 @@
### Builtins
```python
class i18n: class i18n:
# Basic phases # Basic phases
hello: str hello: str
@@ -42,7 +45,6 @@ class i18n:
core_player_kick_stale: str core_player_kick_stale: str
core_player_kick_no_allowed_default_reason: str core_player_kick_no_allowed_default_reason: str
core_player_kick_server_full: str core_player_kick_server_full: str
core_player_set_id: str
core_identifying_okay: str core_identifying_okay: str
# In-game phrases # In-game phrases
@@ -107,3 +109,4 @@ class i18n:
# Command: exit # Command: exit
man_message_exit: str man_message_exit: str
help_message_exit: str help_message_exit: str
```

View File

@@ -41,7 +41,6 @@
"core_player_kick_stale": "过时的客户端。(由新连接替换)", "core_player_kick_stale": "过时的客户端。(由新连接替换)",
"core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。", "core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。",
"core_player_kick_server_full": "服务器已满。", "core_player_kick_server_full": "服务器已满。",
"core_player_set_id": "玩家设置ID {}",
"core_identifying_okay": "成功登录。", "core_identifying_okay": "成功登录。",
"": "游戏内短语", "": "游戏内短语",

View File

@@ -41,7 +41,6 @@
"core_player_kick_stale": "Stale client. (Replaced by new connection)", "core_player_kick_stale": "Stale client. (Replaced by new connection)",
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.", "core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
"core_player_kick_server_full": "Server is full.", "core_player_kick_server_full": "Server is full.",
"core_player_set_id": "Player set ID {}",
"core_identifying_okay": "Successful login.", "core_identifying_okay": "Successful login.",
"": "In-game phrases", "": "In-game phrases",

View File

@@ -41,7 +41,6 @@
"core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)", "core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)",
"core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.", "core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.",
"core_player_kick_server_full": "Сервер полон.", "core_player_kick_server_full": "Сервер полон.",
"core_player_set_id": "Игрок получил ID {}",
"core_identifying_okay": "Успешный вход.", "core_identifying_okay": "Успешный вход.",
"": "In-game phrases", "": "In-game phrases",

View File

@@ -1,19 +0,0 @@
# pip install pyinstaller-versionfile
# create-version-file win-metadata.yml --outfile win-ver_info.txt
Version: 0.4.5
CompanyName: KuiToi
FileDescription: KuiToi Server
InternalName: KuiToi Server
LegalCopyright: © Maxim Khomutov
OriginalFilename: KuiToi-Server.exe
ProductName: KuiToi Server
Translation:
# ru-RU
- langID: 1049
charsetID: 1251
# en-US
- langID: 1033
charsetID: 1251
# zh-CN
- langID: 2052
charsetID: 950

View File

@@ -7,7 +7,7 @@
}, },
{ {
"optionDest": "filenames", "optionDest": "filenames",
"value": "C:/Users/Santa/PycharmProjects/kuitoi-Server/src/main.py" "value": "../src/main.py"
}, },
{ {
"optionDest": "onefile", "optionDest": "onefile",
@@ -19,16 +19,12 @@
}, },
{ {
"optionDest": "icon_file", "optionDest": "icon_file",
"value": "C:/Users/Santa/PycharmProjects/kuitoi-Server/win-logo.ico" "value": "./icon.ico"
}, },
{ {
"optionDest": "name", "optionDest": "name",
"value": "KuiToi-Server" "value": "KuiToi-Server"
}, },
{
"optionDest": "ascii",
"value": true
},
{ {
"optionDest": "clean_build", "optionDest": "clean_build",
"value": false "value": false
@@ -47,11 +43,7 @@
}, },
{ {
"optionDest": "version_file", "optionDest": "version_file",
"value": "C:/Users/Santa/PycharmProjects/kuitoi-Server/win-ver_info.txt" "value": "./metadata.txt"
},
{
"optionDest": "embed_manifest",
"value": true
}, },
{ {
"optionDest": "uac_admin", "optionDest": "uac_admin",
@@ -62,20 +54,12 @@
"value": false "value": false
}, },
{ {
"optionDest": "win_private_assemblies", "optionDest": "argv_emulation",
"value": false
},
{
"optionDest": "win_no_prefer_redirects",
"value": false "value": false
}, },
{ {
"optionDest": "bootloader_ignore_signals", "optionDest": "bootloader_ignore_signals",
"value": false "value": false
},
{
"optionDest": "argv_emulation",
"value": false
} }
], ],
"nonPyinstallerOptions": { "nonPyinstallerOptions": {

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

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