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
- [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)
[TODO.md](./TODO.md)
## Installation
1. Install **Python 3.10**
1. Install **Python 3.12**+
2. Clone the repository in a location of your choice with: `git clone -b Stable https://github.com/kuitoi/kuitoi-Server.git`.
3. Change directory into the KuiToi-Server: `cd KuiToi-Server`.
4. Install requirements: `pip install -r requirements.txt`.

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()参数相同_\
在kt.dir中打开文件
### kt.register_event(event_name: str, event_func: function)
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> 作为`event_func`调用的事件名称._\
_`event_func: function` -> 要调用的函数._

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ Auth:
private: true
Game:
map: gridmap_v2
max_cars: 1
cars: 1
players: 8
Options:
debug: false
@@ -64,7 +64,7 @@ WebAPI:
### Game
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
* `max_cars` - 每个玩家的最大汽车数量
* `cars` - 每个玩家的最大汽车数量
* `players` - 最大玩家数
### 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
## Worth looking at
@@ -147,7 +23,7 @@ Returns the directory of the plugin
_Parameters are the same as for open()_\
Opens a file in kt.dir
### kt.register_event(event_name: str, event_func: function)
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> The name of the event that `event_func` will be called on._\
_`event_func: function` -> The function that will be called._

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,72 @@
# -*- coding: utf-8 -*-
import copy
# Developed by KuiToi Dev
# File modules.config_provider.__init__.py
# File modules.ConfigProvider
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
from .config_provider import ConfigProvider, Config
import os
import secrets
import yaml
class Config:
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, 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:
Auth: Dict[str, object]
Game: Dict[str, object]
@@ -12,3 +12,4 @@ class Config:
def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class config (Config): ...
```

View File

@@ -1,9 +1,271 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.console.__init__.py
# File modules.ConsoleSystem
# Written by: SantaSpeen
# Version 1.0
# Version 1.2
# Licence: FPA
# (c) kuitoi.su 2023
from .console_system import Console
import builtins
import inspect
import logging
from typing import AnyStr
from prompt_toolkit import PromptSession, print_formatted_text, HTML
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
from typing import AnyStr
from core import get_logger
### Builtins
```python
class RCONSystem:
console = None
def __init__(self, key, host, port):
self.log = get_logger("RCON")
self.key = key
self.host = host
self.port = port
def __init__(self, key, host, port): ...
async def start(self): ...
async def stop(self): ...
@@ -35,3 +27,4 @@ class console:
def write(s: str) -> None: ...
@staticmethod
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:
@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
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
@staticmethod
@@ -11,3 +11,4 @@ class EventsSystem:
@staticmethod
def call_lua_event(event_name, *data) -> list[Any]: ...
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:
self.log.debug("request MP.RegisterEvent()")
ev.register_event(event_name, function_name, lua=self._lua)
ev.register(event_name, function_name, lua=self._lua)
if event_name not in self._local_events:
self._local_events.update({str(event_name): [function_name]})
else:
@@ -109,8 +109,9 @@ class MP:
del self._event_timers[event_name]
def TriggerLocalEvent(self, event_name, *args):
self.log.debug("request TriggerLocalEvent()")
self.log.debug(f"Calling local lua event: '{event_name}'")
if event_name != "getTable":
self.log.debug("request TriggerLocalEvent()")
self.log.debug(f"Calling local lua event: '{event_name}{args}'")
funcs_data = []
if event_name in self._local_events.keys():
for func_name in self._local_events[event_name]:
@@ -157,7 +158,7 @@ class MP:
to_all = False
if player_id < 0:
to_all = True
client = client[0]
client = client[0] if len(client) > 0 else None
if client and event_name and data:
t = self.loop.create_task(client.send_event(event_name, data, to_all=to_all))
self.tasks.append(t)
@@ -168,7 +169,7 @@ class MP:
return False, "Can't found event_name or data"
def TriggerClientEventJson(self, player_id, event_name, data):
self.log.debug("request TriggerClientEventJson()")
self.log.debug(f"request TriggerClientEventJson({player_id, event_name, data})")
data = self._lua.globals().Util.JsonEncode(data)
self.TriggerClientEvent(player_id, event_name, data)
@@ -232,8 +233,8 @@ class MP:
def GetPlayers(self):
self.log.debug("request GetPlayers()")
clients = ev.call_event("_get_players", cid=-1)
return self._lua.table_from(clients)
clients = ev.call_event("_get_player", cid=-1)[0]
return self._lua.table_from([i.nick for i in clients])
def IsPlayerGuest(self, player_id) -> bool:
self.log.debug("request IsPlayerGuest()")
@@ -312,12 +313,18 @@ class Util:
return {k: v for k, v in new_dict.items() if v is not None}
def JsonEncode(self, table):
self.log.debug("requesting JsonEncode()")
if all(isinstance(k, int) for k in table.keys()):
data = self._recursive_list_encode(table)
else:
data = self._recursive_dict_encode(table)
return json.dumps(data)
data = {}
try:
self.log.debug("requesting JsonEncode()")
if all(isinstance(k, int) for k in table.keys()):
data = self._recursive_list_encode(table)
else:
data = self._recursive_dict_encode(table)
except Exception as e:
self.log.exception(e)
data = json.dumps(data)
self.log.debug(f"Encoded: {data}")
return data
def JsonDecode(self, string):
self.log.debug("requesting JsonDecode()")
@@ -570,8 +577,8 @@ class LuaPluginsLoader:
self.lua_dirs = set()
self.log = get_logger("LuaPluginsLoader")
self.loaded_str = "Lua plugins: "
ev.register_event("_lua_plugins_get", lambda x: self.lua_plugins)
ev.register_event("_lua_plugins_unload", self.unload)
ev.register("_lua_plugins_get", lambda x: self.lua_plugins)
ev.register("_lua_plugins_unload", self.unload)
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
console.add_command("lua_pl", lambda x: self.loaded_str[:-2])
@@ -591,7 +598,7 @@ class LuaPluginsLoader:
"LogChat": config.Options['log_chat'],
"Debug": config.Options['debug'],
"Private": config.Auth['private'],
"MaxCars": config.Game['max_cars'],
"MaxCars": config.Game['cars'],
"MaxPlayers": config.Game['players'],
"Map": f"/levels/{config.Game['map']}/info.json",
"Description": config.Server['description'],

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 .WebAPISystem import web_app
from .WebAPISystem import _stop as stop_web
from .RateLimiter import RateLimiter

View File

@@ -1,9 +1,160 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.i18n.__init__.py
# File modules.i18n
# Written by: SantaSpeen
# Version 1.0
# Version 1.3
# Licence: FPA
# (c) kuitoi.su 2023
from .i18n import MultiLanguage
import builtins
import json
import os
from json import JSONDecodeError
import requests
from core.utils import get_logger
class i18n:
data = {}
def __init__(self, data):
i18n.data = data
def __getattribute__(self, key):
return i18n.data[key]
class MultiLanguage:
def __init__(self, language: str = None, files_dir="translates/", encoding=None):
if encoding is None:
encoding = config.enc
if language is None:
language = "en"
self.__data = {
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization completed.",
"start": "Server started!",
"stop": "Server stopped!",
"auth_need_key": "BeamMP key is required to run!",
"auth_empty_key": "BeamMP key is empty!",
"auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}",
"GUI_yes": "Yes",
"GUI_no": "No",
"GUI_ok": "OK",
"GUI_cancel": "Cancel",
"GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?",
"GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"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:
# Basic phases
hello: str
@@ -42,7 +45,6 @@ class i18n:
core_player_kick_stale: str
core_player_kick_no_allowed_default_reason: str
core_player_kick_server_full: str
core_player_set_id: str
core_identifying_okay: str
# In-game phrases
@@ -107,3 +109,4 @@ class i18n:
# Command: exit
man_message_exit: str
help_message_exit: str
```

View File

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

View File

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

View File

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

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