46 Commits

Author SHA1 Message Date
7464a4095d [+] Warning on udp_addr != main_addr 2024-08-12 18:43:42 +03:00
3dc2232db2 [+] Fix onModsSending 2024-08-05 18:40:20 +03:00
a16e2e39d9 [~] go home... 2024-08-02 18:18:53 +03:00
ac2aba4b27 [~] Basic events 2024-08-02 16:33:13 +03:00
66db0aa9f7 [~] Minor 2024-08-02 16:17:52 +03:00
2197a32354 [+] PluginConsole
[+] ev.unregister_by_id
[+] Completes for PluginsLoader
[~] asyncio.to_thread
[~] console.legacy_mode
2024-08-02 16:03:14 +03:00
1e95a7519b [+] PluginConsole
[+] ev.unregister_by_id
[+] Completes for PluginsLoader
[~] asyncio.to_thread
[~] console.legacy_mode
2024-08-02 16:03:11 +03:00
16d5d06881 [+] db 2024-08-02 09:04:52 +03:00
e43dc69b5c [+] onCarSpawned
[+] onCarDeleted
[~] change onChatReceive handler
[~] Minor
2024-08-02 09:03:50 +03:00
1e685e69ed [+] on_none
[+] WIP PermsSystem
2024-08-02 09:01:44 +03:00
633e235342 [+] sent counters
[+] Colored TPS
[~] Minor
2024-08-01 18:06:55 +03:00
cbb3fc8b29 [+] ANSI 2024-08-01 18:05:17 +03:00
613dfb741a [-] print TPS every sec 2024-07-31 18:07:04 +03:00
c2159fc523 [~] Relogic onChatReceive
[~] minor
2024-07-31 18:06:45 +03:00
180ab2421e [~] Minor 2024-07-31 18:06:12 +03:00
25c3f503bf [+] call_as_events 2024-07-31 18:05:57 +03:00
d2c856fd90 del 2024-07-31 18:05:42 +03:00
189af0d773 [~] kick 2024-07-31 15:51:34 +03:00
d003601b58 [!] FIX Legacy mode
[+] MyNestedCompleter
[+] players_completer
2024-07-31 15:50:37 +03:00
72035c226b minor 2024-07-31 12:14:58 +03:00
8ed5671995 bump version 2024-07-30 01:30:43 +03:00
b64c449065 [+] PPS
[+] Clients ticks
[>] UDP handler to Client class
[~] Minor
2024-07-30 00:52:01 +03:00
f2de91d0f1 [~] FIX add_in.lua 2024-07-29 15:21:44 +03:00
243177ee6f [~] console.del_command
[!] FIX plugins load | reload
2024-07-29 15:20:35 +03:00
7796e3970d [~] Minor 2024-07-29 03:09:16 +03:00
2bf1c07041 [+] plugins command 2024-07-29 03:09:02 +03:00
2b4c0bf4d0 [~] FastFix 2024-07-27 19:01:16 +03:00
2ee74c310d [+] TPS
[!] FIX tcp. Againg?...
2024-07-27 17:00:27 +03:00
b0303f3e6d [!] Fix closing connection 2024-07-27 12:21:29 +03:00
a406956080 [~] Minor 2024-07-27 12:12:41 +03:00
027c239424 [!] Fix 'add_in.lua' 2024-07-27 05:16:31 +03:00
9e86c41a6a [!] Fix events
[~] Minor updates
2024-07-27 05:04:49 +03:00
bf1c6d2c41 [-] WebAPI 2024-07-27 05:03:27 +03:00
71ec0c7aed [-] WebAPI 2024-07-27 05:03:23 +03:00
85475a49be [-] WebAPI 2024-07-27 03:45:59 +03:00
8cbe3d07e3 bump build 2024-07-26 17:56:33 +03:00
b2a608d369 [+] Add supports for pip packets 2024-07-26 17:07:32 +03:00
c12a91bf86 [+] preparations for pip 2024-07-25 18:58:53 +03:00
209004c9cb [+] pip-packets 2024-07-25 18:30:36 +03:00
2af4681082 [+] Support pip packages 2024-07-25 17:32:00 +03:00
f1f80cc94c [!] FIX unicycle
[!] FIX BeamMP bug
[~] Minor
2024-07-25 17:00:09 +03:00
06942e8a71 [!] Fix open 2024-07-25 12:36:20 +03:00
51867d526d [+] onPlayerReady
[+] ResetCar
2024-07-25 05:07:35 +03:00
ff58e2a994 [minor] 2024-07-25 03:33:46 +03:00
4c6a240f96 [!] Fastfix
[+] call_async_event onPlayerAuthenticated
2024-07-25 02:54:20 +03:00
666a76201e [!] Fastfix 2024-07-25 01:01:42 +03:00
36 changed files with 1258 additions and 834 deletions

4
.gitignore vendored
View File

@@ -142,4 +142,6 @@ logs/
*.toml *.toml
/win-ver_info.txt /win-ver_info.txt
/output/ /output/
pip-packets/
users.db3

View File

@@ -44,7 +44,6 @@
- [x] MultiLanguage: (i18n support) - [x] MultiLanguage: (i18n support)
- [x] Core - [x] Core
- [x] Console - [x] Console
- [x] WebAPI
- [x] Plugins supports: - [x] Plugins supports:
- [x] Python part: - [x] Python part:
- [x] Load Python plugins - [x] Load Python plugins

View File

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

View File

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

View File

@@ -49,11 +49,6 @@ Server:
name: KuiToi-Server name: KuiToi-Server
server_ip: 0.0.0.0 server_ip: 0.0.0.0
server_port: 30814 server_port: 30814
WebAPI:
enabled: false
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
server_ip: 127.0.0.1
server_port: 8433
``` ```
### Auth ### Auth
@@ -83,6 +78,3 @@ WebAPI:
* `name` - BeamMP 启动器的服务器名称 * `name` - BeamMP 启动器的服务器名称
* `server_ip` - 分配给服务器的 IP 地址(仅适用于有经验的用户,默认为 0.0.0.0 * `server_ip` - 分配给服务器的 IP 地址(仅适用于有经验的用户,默认为 0.0.0.0
* `server_port` - 服务器将在哪个端口上工作 * `server_port` - 服务器将在哪个端口上工作
### WebAPI
##### _文档尚未准备好_

View File

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

View File

@@ -1,15 +0,0 @@
Here's the translation of the readme.txt content:
# WebAPI for the server
## Available endpoints
* `/stop`:
* Required parameters:
* `secret_key` - The key specified in the server configuration
* `/event.get`
* The endpoint is not yet ready
* Required parameters:
* `secret_key` - The key specified in the server configuration

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.py # File core.tcp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.5 # Core version: 0.4.8
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -9,7 +9,7 @@ import json
import math import math
import time import time
import zlib import zlib
from asyncio import Lock from asyncio import Queue
from core import utils from core import utils
@@ -21,7 +21,27 @@ class Client:
self.__writer = writer self.__writer = writer
self._core = core self._core = core
self.__alive = True self.__alive = True
self.__packets_queue = []
self.__queue_tpc = Queue()
self.__queue_udp = Queue()
self._tpc_count_recv = 0
self._udp_count_recv = 0
self._tpc_count_total_recv = 0
self._udp_count_total_recv = 0
self._udp_size_total_recv = 0.1
self._tpc_size_total_recv = 0.1
# self._tpc_count_sent = 0
# self._udp_count_sent = 0
self._tpc_count_total_sent = 0
self._udp_count_total_sent = 0
self._udp_size_total_sent = 0.1
self._tpc_size_total_sent = 0.1
self.tcp_pps = 0
self.udp_pps = 0
self.__tasks = [] self.__tasks = []
self._down_sock = (None, None) self._down_sock = (None, None)
self._udp_sock = (None, None) self._udp_sock = (None, None)
@@ -41,7 +61,8 @@ class Client:
self._unicycle = {"id": -1, "packet": ""} self._unicycle = {"id": -1, "packet": ""}
self._connect_time = 0 self._connect_time = 0
self._last_position = {} self._last_position = {}
self._lock = Lock() self._last_recv = time.monotonic()
self.__tpt_id = 0
@property @property
def _writer(self): def _writer(self):
@@ -130,17 +151,24 @@ class Client:
if not message: if not message:
message = "no message" message = "no message"
to_all = False to_all = False
await self._send(f"C:{message!r}", to_all=to_all) if "\n" in message:
ms = message.split("\n")
for m in ms:
await self.send_message(m, to_all)
return
await self._send(f"C:{message}", to_all=to_all)
async def send_event(self, event_name, event_data, to_all=True): async def send_event(self, event_name, event_data, to_all=False):
self.log.debug(f"send_event: {event_name}:{event_data}; {to_all=}")
if not self.ready:
self.log.debug(f"Client not ready. {event_data=}")
return
if isinstance(event_data, (list, tuple, dict)): if isinstance(event_data, (list, tuple, dict)):
event_data = json.dumps(event_data, separators=(',', ':')) event_data = json.dumps(event_data, separators=(',', ':'))
else: if len(event_data) > 99 * MB:
event_data = f"{event_data!r}" self.log.error(f"Error while preparing event {event_name!r}: data too big! data>99MB")
if len(event_data) > 104857599:
self.log.error("Client data too big! >=104857599")
return return
await self._send(f"E:{event_name}:{event_data}", to_all=to_all) await self._send(f"E:{event_name}:{event_data}", to_all, True)
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None): async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
@@ -179,7 +207,9 @@ class Client:
if udp_sock and udp_addr: if udp_sock and udp_addr:
try: try:
if not udp_sock.is_closing(): if not udp_sock.is_closing():
# self.log.debug(f'[UDP] {data!r}') # self.log.debug(f'[UDP] {data!r}; {udp_addr}')
self._udp_count_total_sent += 1
self._udp_size_total_sent += len(data)
udp_sock.sendto(data, udp_addr) udp_sock.sendto(data, udp_addr)
except OSError: except OSError:
self.log.debug("[UDP] Error sending") self.log.debug("[UDP] Error sending")
@@ -189,13 +219,16 @@ class Client:
return return
header = len(data).to_bytes(4, "little", signed=True) header = len(data).to_bytes(4, "little", signed=True)
# self.log.debug(f'[TCP] {header + data!r}') data = header + data
# self.log.debug(f'[TCP] {data!r}')
try: try:
writer.write(header + data) self._tpc_count_total_sent += 1
self._tpc_size_total_sent += len(data)
writer.write(data)
await writer.drain() await writer.drain()
return True return True
except Exception as e: except Exception as e:
self.log.debug(f'[TCP] Disconnected: {e}') self.log.debug(f'[TCP] Disconnected: {e}; {writer=}')
self.__alive = False self.__alive = False
await self._remove_me() await self._remove_me()
return False return False
@@ -218,12 +251,11 @@ class Client:
self.is_disconnected() self.is_disconnected()
if self.__alive: if self.__alive:
if header == b"": if header == b"":
self.__packets_queue.append(None) await self._tpc_put(None)
self.__alive = False self.__alive = False
continue continue
self.log.error(f"Header: {header}") self.log.error(f"Header: {header}")
await self.kick("Invalid packet - header negative") await self.kick("Invalid packet - header negative")
self.__packets_queue.append(None)
continue continue
if int_header > 100 * MB: if int_header > 100 * MB:
@@ -231,7 +263,6 @@ class Client:
self.log.warning("Client sent header of >100MB - " self.log.warning("Client sent header of >100MB - "
"assuming malicious intent and disconnecting the client.") "assuming malicious intent and disconnecting the client.")
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}") self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
self.__packets_queue.append(None)
continue continue
data = b"" data = b""
@@ -249,31 +280,39 @@ class Client:
if one: if one:
return data return data
self.__packets_queue.append(data) await self._tpc_put(data)
except ConnectionError: except ConnectionError:
self.__alive = False self.__alive = False
self.__packets_queue.append(None) await self._tpc_put(None)
async def _split_load(self, start, end, d_sock, filename, speed_limit=None): async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
real_size = end - start size = end - start
writer = self._down_sock[1] if d_sock else self.__writer writer = self._down_sock[1] if d_sock else self.__writer
who = 'dwn' if d_sock else 'srv' who = 'DSock' if d_sock else 'MSock'
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}") self.log.debug(f"[{who}] Started; start,end={(start, end)}")
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
f.seek(start) f.seek(start)
total_sent = 0 total_sent = 0
start_time = time.monotonic() start_time = time.monotonic()
while total_sent < real_size: while total_sent < size:
data = f.read(min(MB, real_size - total_sent)) # read data in chunks of 1MB or less if (size - total_sent) == 0:
break
data = f.read(min(MB, size - total_sent)) # read data in chunks of 1MB or less
try: try:
writer.write(data) writer.write(data)
await writer.drain() async with asyncio.timeout(120): # ~100kb/s
await writer.drain()
# self.log.debug(f"[{who}] Sent {len(data)} bytes.") # self.log.debug(f"[{who}] Sent {len(data)} bytes.")
except ConnectionError: except TimeoutError:
self.log.debug(f"[{who}] TimeoutError; Sock: {writer}")
self.log.error("TimeoutError")
self.__alive = False
break
except ConnectionError:
self.log.debug(f"[{who}] Disconnected; Sock: {writer}")
self.__alive = False self.__alive = False
self.log.debug(f"[{who}] Disconnected.")
break break
total_sent += len(data) total_sent += len(data)
@@ -283,7 +322,7 @@ class Client:
expected_time = total_sent / (speed_limit * MB) expected_time = total_sent / (speed_limit * MB)
if expected_time > elapsed_time: if expected_time > elapsed_time:
await asyncio.sleep(expected_time - elapsed_time) await asyncio.sleep(expected_time - elapsed_time)
self.log.debug(f"[{who}] Ready. {total_sent=}")
return total_sent return total_sent
async def _sync_resources(self): async def _sync_resources(self):
@@ -303,10 +342,10 @@ class Client:
size = mod['size'] size = mod['size']
self.log.debug("File is accept.") self.log.debug("File is accept.")
break break
self.log.debug(f"Mode size: {size}") # self.log.debug(f"Mod size: {size}")
if size == -1: if size == -1:
await self._send(b"CO") await self._send(b"CO")
await self.kick(f"Not allowed mod: " + file) await self.kick(f"Requested not allowed file: " + file)
return return
await self._send(b"AG") await self._send(b"AG")
t = 0 t = 0
@@ -314,22 +353,22 @@ class Client:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
t += 1 t += 1
if t > 50: if t > 50:
await self.kick("Missing download socket") await self.kick("Error (Missing DSock)")
return return
if config.Options['use_queue']: if config.Options['use_queue']:
while self._core.lock_upload: while self._core.lock_upload:
await asyncio.sleep(.2) await asyncio.sleep(.2)
self._core.lock_upload = True self._core.lock_upload = True
speed = config.Options["speed_limit"] speed = config.Options["speed_limit"] or 25*B
if speed: if speed:
speed = speed / 2 speed = speed / 2
half_size = math.floor(size / 2) half_size = size // 2
t = time.monotonic() t = time.monotonic()
uploads = [ self.log.debug(f"Sending: {size=}; sl0={(0, half_size)}; sl1={(half_size, size)}")
self._split_load(0, half_size, False, file, speed), async with asyncio.TaskGroup() as tg:
self._split_load(half_size, size, True, file, speed) sl0 = tg.create_task(self._split_load(0, half_size, False, file, speed))
] sl1 = tg.create_task(self._split_load(half_size, size, True, file, speed))
sl0, sl1 = await asyncio.gather(*uploads) sl0, sl1 = sl0.result(), sl1.result()
tr = (time.monotonic() - t) or 0.0001 tr = (time.monotonic() - t) or 0.0001
if self._core.lock_upload: if self._core.lock_upload:
self._core.lock_upload = False self._core.lock_upload = False
@@ -340,17 +379,17 @@ class Client:
sent = sl0 + sl1 sent = sl0 + sl1
ok = sent == size ok = sent == size
lost = size - sent lost = size - sent
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}") self.log.debug(f"Sent; sl_0: {sl0}; sl_1: {sl1}; size==sent is {ok}: {size}-{sent}={lost}")
if not ok: if not ok:
self.__alive = False self.__alive = False
self.log.error(i18n.client_mod_sent_error.format(repr(file))) e = i18n.client_mod_sent_error.format(repr(file))
await self._send(f"E{e}")
self.log.error(e)
return return
elif data.startswith(b"SR"): elif data.startswith(b"SR"):
path_list = '' path_list = ''
size_list = '' size_list = ''
for mod in self._core.mods_list: for mod in self._core.mods_list[1:]:
if type(mod) == int:
continue
path_list += f"{mod['path']};" path_list += f"{mod['path']};"
size_list += f"{mod['size']};" size_list += f"{mod['size']};"
mod_list = path_list + size_list mod_list = path_list + size_list
@@ -360,6 +399,7 @@ class Client:
else: else:
await self._send(mod_list) await self._send(mod_list)
elif data == b"Done": elif data == b"Done":
self.log.debug("recv Done")
await self._send(f"M/levels/{config.Game['map']}/info.json") await self._send(f"M/levels/{config.Game['map']}/info.json")
break break
return return
@@ -404,10 +444,8 @@ class Client:
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):]) lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
if 1 in lua_data: if 1 in lua_data:
allow = False allow = False
ev_data_list = ev.call_event("onCarSpawn", data=car_json, car_id=car_id, player=self) event_data = await ev.call_as_events("onCarSpawn", data=car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarSpawn", data=car_json, car_id=car_id, player=self) for ev_data in event_data:
ev_data_list.extend(d2)
for ev_data in ev_data_list:
self.log.debug(ev_data) self.log.debug(ev_data)
# TODO: handle event onCarSpawn # TODO: handle event onCarSpawn
pass pass
@@ -434,6 +472,9 @@ class Client:
"pos": {} "pos": {}
} }
await self._send(pkt, to_all=True, to_self=True) await self._send(pkt, to_all=True, to_self=True)
if self.focus_car == -1:
self._focus_car = car_id
await ev.call_as_events("onCarSpawned", data=car_json, car_id=car_id, player=self)
else: else:
await self._send(pkt) await self._send(pkt)
des = f"Od:{self.cid}-{car_id}" des = f"Od:{self.cid}-{car_id}"
@@ -452,13 +493,9 @@ class Client:
if car_id != -1 and self._cars[car_id]: if car_id != -1 and self._cars[car_id]:
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
admin_allow = False # Delete from admin, for example... admin_allow = False # Delete from admin, for example...
ev_data_list = ev.call_event("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self) event_data = await ev.call_as_events("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self) for ev_data in event_data:
ev_data_list.extend(d2)
for ev_data in ev_data_list:
self.log.debug(ev_data) self.log.debug(ev_data)
# TODO: handle event onCarDelete # TODO: handle event onCarDelete
pass pass
@@ -473,8 +510,9 @@ class Client:
self._cars[unicycle_id] = None self._cars[unicycle_id] = None
self._cars[car_id] = None self._cars[car_id] = None
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True) await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
await ev.call_as_events("onCarDeleted", data=self._cars[car_id], car_id=car_id, player=self)
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
self.log.debug(f"Deleted car: car_id={car_id}") self.log.debug(f"Deleted car: car_id={car_id}")
else: else:
self.log.debug(f"Invalid car: car_id={car_id}") self.log.debug(f"Invalid car: car_id={car_id}")
@@ -504,10 +542,10 @@ class Client:
pass pass
if cid == self.cid or allow or admin_allow: if cid == self.cid or allow or admin_allow:
if car['snowman']: if car['unicycle']:
unicycle_id = self._unicycle['id'] unicycle_id = self._unicycle['id']
self._unicycle['id'] = -1 self._unicycle['id'] = -1
self.log.debug(f"Delete snowman") self.log.debug(f"Delete unicycle")
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True) await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._cars[unicycle_id] = None self._cars[unicycle_id] = None
else: else:
@@ -521,10 +559,11 @@ class Client:
self.log.debug(f"Invalid car: car_id={car_id}") self.log.debug(f"Invalid car: car_id={car_id}")
async def reset_car(self, car_id, x, y, z, rot=None): async def reset_car(self, car_id, x, y, z, rot=None):
# TODO: reset_car self.log.debug(f"Resetting car from plugin {x, y, z}; {rot=}")
self.log.debug(f"Resetting car from plugin") jpkt = {"pos": {"y": float(y), "x": float(x), "z": float(z)}, "rot": {"y": 0, "w": 0, "x": 0, "z": 0}}
if rot is None: if rot:
rot = {"y": 0, "w": 0, "x": 0, "z": 0} jpkt['rot'] = rot
await self._send(f"Or:{self.cid}-{car_id}:{json.dumps(jpkt)}", True)
async def _reset_car(self, raw_data): async def _reset_car(self, raw_data):
cid, car_id = self._get_cid_vid(raw_data) cid, car_id = self._get_cid_vid(raw_data)
@@ -545,6 +584,7 @@ class Client:
async def _handle_car_codes(self, raw_data): async def _handle_car_codes(self, raw_data):
if len(raw_data) < 6: if len(raw_data) < 6:
return return
self.log.debug(f"[car] {raw_data}")
sub_code = raw_data[1] sub_code = raw_data[1]
data = raw_data[3:] data = raw_data[3:]
match sub_code: match sub_code:
@@ -602,6 +642,9 @@ class Client:
self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2))) self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2)))
self._ready = True self._ready = True
self._synced = True
ev.call_event("onPlayerReady", player=self)
await ev.call_async_event("onPlayerReady", player=self)
async def _chat_handler(self, data): async def _chat_handler(self, data):
sup = data.find(":", 2) sup = data.find(":", 2)
@@ -611,56 +654,62 @@ class Client:
if not msg: if not msg:
self.log.debug("Tried to send an empty event, ignoring") self.log.debug("Tried to send an empty event, ignoring")
return return
to_ev = {"message": msg, "player": self}
lua_data = ev.call_lua_event("onChatMessage", self.cid, self.nick, msg) lua_data = ev.call_lua_event("onChatMessage", self.cid, self.nick, msg)
if 1 in lua_data: if 1 in lua_data:
if config.Options['log_chat']: if config.Options['log_chat']:
self.log.info(f"{self.nick}: {msg}") self.log.info(f"{self.nick}: {msg}")
return return
ev_data_list = ev.call_event("onChatReceive", **to_ev) event_data = await ev.call_as_events("onChatReceive", message=msg, player=self)
d2 = await ev.call_async_event("onChatReceive", **to_ev)
ev_data_list.extend(d2)
need_send = True need_send = True
for ev_data in ev_data_list: for event in event_data:
if ev_data is None: if event is None:
continue continue
try: try:
message = ev_data["message"]
to_all = ev_data.get("to_all")
if to_all is None:
to_all = True
to_self = ev_data.get("to_self")
if to_self is None:
to_self = True
to_client = ev_data.get("to_client")
writer = None writer = None
if to_client: to_all = True
# noinspection PyProtectedMember to_self = True
writer = to_client._writer message = f"{self.nick}: {msg}"
match event:
case False | 0:
need_send = False
continue
case {"message": message, **setting}:
message = message
to_all = setting.get("to_all", True)
to_self = setting.get("to_self", True)
to_client = setting.get("to")
if to_client:
writer = to_client._writer
case _:
self.log.error(f"[onChatReceive] Bad data returned from event: {event}")
if config.Options['log_chat']: if config.Options['log_chat']:
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}") self.log.info(f"[local] {message}" if not to_all else message)
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer) await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False need_send = False
except KeyError: except KeyError:
self.log.error(i18n.client_event_invalid_data.format(ev_data)) self.log.error(i18n.client_event_invalid_data.format(event))
except AttributeError: except AttributeError:
self.log.error(i18n.client_event_invalid_data.format(ev_data)) self.log.error(i18n.client_event_invalid_data.format(event))
if need_send: if need_send:
if config.Options['log_chat']: if config.Options['log_chat']:
self.log.info(f"{self.nick}: {msg}") self.log.info(f"{self.nick}: {msg}")
await self._send(data, to_all=True) await self._send(data, to_all=True)
async def _handle_codes(self, data): async def _handle_codes_tcp(self, data):
if not data: if data is None:
self.__alive = False self.__alive = False
return return
if len(data) == 0:
await self.kick("Bad data from client")
return
_bytes = False _bytes = False
try: try:
data = data.decode() data = data.decode()
except UnicodeDecodeError: except UnicodeDecodeError:
_bytes = True _bytes = True
self.log.error(f"UnicodeDecodeError: {data}") self.log.error("UnicodeDecodeError")
if data[0] in ['V', 'W', 'Y', 'E', 'N']: if data[0] in ['V', 'W', 'Y', 'E', 'N']:
await self._send(data, to_all=True, to_self=False) await self._send(data, to_all=True, to_self=False)
@@ -670,17 +719,14 @@ class Client:
match data[0]: # At data[0] code match data[0]: # At data[0] code
case "H": # Map load, client ready case "H": # Map load, client ready
await self._connected_handler() await self._connected_handler()
case "C": # Chat handler case "C": # Chat handler
if _bytes: if _bytes:
return return
await self._chat_handler(data) await self._chat_handler(data)
case "O": # Cars handler case "O": # Cars handler
if _bytes: if _bytes:
return return
await self._handle_car_codes(data) await self._handle_car_codes(data)
case "E": # Client events handler case "E": # Client events handler
if len(data) < 2: if len(data) < 2:
self.log.debug("Tried to send an empty event, ignoring.") self.log.debug("Tried to send an empty event, ignoring.")
@@ -698,7 +744,75 @@ class Client:
ev.call_event(event_name, data=even_data, player=self) ev.call_event(event_name, data=even_data, player=self)
await ev.call_async_event(event_name, data=even_data, player=self) await ev.call_async_event(event_name, data=even_data, player=self)
case _: case _:
self.log.warning(f"TCP [{self.cid}] Unknown code: {data[0]}; {data}") self.log.warning(f"TCP Unknown code: {data[0]}; {data}")
async def _handle_codes_udp(self, data):
code = data[2:3].decode()
data = data[2:].decode()
match code:
case "p": # Ping packet
ev.call_event("onSentPing", player=self)
await self._send(b"p", to_udp=True)
case "Z": # Position packet
sub = data.find("{", 1)
last_pos = data[sub:]
try:
_, car_id = self._get_cid_vid(data)
if self._cars[car_id]:
last_pos = json.loads(last_pos)
self._last_position = last_pos
self._cars[car_id]['pos'] = last_pos
ev.call_event("onChangePosition", data, player=self, pos=last_pos)
except Exception as e:
self.log.warning(f"Cannot parse position packet: {e}")
self.log.debug(f"data: '{data}', sub: {sub}")
self.log.debug(f"last_pos ({type(last_pos)}): {last_pos}")
await self._send(data, True, False, True)
case "X":
await self._send(data, True, False, True)
case _:
self.log.warning(f"UDP Unknown code: {code}; {data}")
def _tick_pps(self, _):
self.tcp_pps = self._tpc_count_recv
self.udp_pps = self._udp_count_recv
self._tpc_count_recv = 0
self._udp_count_recv = 0
if self.tcp_pps > self._core.target_tps or self.udp_pps > self._core.target_tps:
self.log.warning(f"PPS > TPS; PPS: TPC: {self.tcp_pps}, UDP: {self.udp_pps}")
async def __tick_player_tcp(self, _):
try:
if self.__queue_tpc.qsize() > 0:
packet = await self.__queue_tpc.get()
if packet is None:
return await self._remove_me()
await self._handle_codes_tcp(packet)
except Exception as e:
self.log.error(f'[TPC] Error while ticking player:')
self.log.exception(e)
async def __tick_player_udp(self, _):
try:
if self.__queue_udp.qsize() > 0:
packet = await self.__queue_udp.get()
await self._handle_codes_udp(packet)
except Exception as e:
self.log.error(f'[UDP] Error while ticking player:')
self.log.exception(e)
async def _tpc_put(self, packet):
if packet:
self._tpc_count_recv += 1
self._tpc_count_total_recv += 1
self._tpc_size_total_recv += len(packet)
await self.__queue_tpc.put(packet)
async def _udp_put(self, packet):
self._udp_count_recv += 1
self._udp_count_total_recv += 1
self._udp_size_total_recv += len(packet)
await self.__queue_udp.put(packet)
async def _looper(self): async def _looper(self):
ev.call_lua_event("onPlayerConnecting", self.cid) ev.call_lua_event("onPlayerConnecting", self.cid)
@@ -706,56 +820,52 @@ class Client:
await self._send(f"P{self.cid}") # Send clientID await self._send(f"P{self.cid}") # Send clientID
await self._sync_resources() await self._sync_resources()
ev.call_lua_event("onPlayerJoining", self.cid) ev.call_lua_event("onPlayerJoining", self.cid)
tasks = self.__tasks self.__tpt_id = ev.register("serverTick", self.__tick_player_tcp)
recv = asyncio.create_task(self._recv()) self.__tpu_id = ev.register("serverTick", self.__tick_player_udp)
tasks.append(recv) self.__tpp_id = ev.register("serverTick_1s", self._tick_pps)
self._synced = True await self._recv()
while self.__alive:
if len(self.__packets_queue) > 0:
for index, packet in enumerate(self.__packets_queue):
# self.log.debug(f"Packet: {packet}")
del self.__packets_queue[index]
task = self._loop.create_task(self._handle_codes(packet))
tasks.append(task)
else:
await asyncio.sleep(0.1)
await asyncio.gather(*tasks)
async def _remove_me(self): async def _remove_me(self):
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
self.__alive = False self.__alive = False
if (self.cid > 0 or self.nick is not None) and \ if self._core.clients_by_nick.get(self.nick):
self._core.clients_by_nick.get(self.nick):
for i, car in enumerate(self._cars): for i, car in enumerate(self._cars):
if not car: if not car:
continue continue
self.log.debug(f"Removing car: car_id={i}") self.log.debug(f"Removing car: car_id={i}")
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False) await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
if self.ready: if self.ready:
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected. await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False)
self.log.debug(f"Removing client") self.log.debug(f"Removing client")
ev.call_lua_event("onPlayerDisconnect", self.cid) ev.call_lua_event("onPlayerDisconnect", self.cid)
ev.call_event("onPlayerDisconnect", player=self) ev.call_event("onPlayerDisconnect", player=self)
await ev.call_async_event("onPlayerDisconnect", player=self) await ev.call_async_event("onPlayerDisconnect", player=self)
if self.__tpt_id:
self.log.info( ev.unregister_by_id(self.__tpt_id) # self.__tick_player_tcp
i18n.client_player_disconnected.format( ev.unregister_by_id(self.__tpu_id) # self.__tick_player_udp
round((time.monotonic() - self._connect_time) / 60, 2) ev.unregister_by_id(self.__tpp_id) # self._tick_pps
) gt = round((time.monotonic() - self._connect_time) / 60, 2)
) self.log.info(i18n.client_player_disconnected.format(gt))
self._core.clients[self.cid] = None self._core.clients[self.cid] = None
del self._core.clients_by_id[self.cid] del self._core.clients_by_id[self.cid]
del self._core.clients_by_nick[self.nick] del self._core.clients_by_nick[self.nick]
self.log.debug(f"TPC: "
f"Recv: {self._tpc_count_total_recv}; {self._tpc_size_total_recv / KB:.4f}kb; "
f"Sent: {self._tpc_count_total_sent}; {self._tpc_size_total_sent / KB:.4f}kb;")
self.log.debug(f"UDP: "
f"Recv: {self._udp_count_total_recv}; {self._udp_size_total_recv / KB:.4f}kb; "
f"Sent: {self._udp_count_total_sent}; {self._udp_size_total_sent / KB:.4f}kb;")
else: else:
self.log.debug(f"Removing client; Closing connection...") self.log.debug(f"Removing client; Closing connection...")
await asyncio.sleep(0.001)
try: try:
if not self.__writer.is_closing(): self.__writer.close()
self.__writer.close() await self.__writer.wait_closed()
except Exception as e: except Exception as e:
self.log.debug(f"Error while closing writer: {e}") self.log.debug(f"Error while closing writer: {e}")
try: try:
_, down_w = self._down_sock _, down_w = self._down_sock
if down_w and not down_w.is_closing(): down_w.close()
down_w.close() await down_w.wait_closed()
except Exception as e: except Exception as e:
self.log.debug(f"Error while closing download writer: {e}") self.log.debug(f"Error while closing download writer: {e}")

View File

@@ -5,7 +5,7 @@
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock, Queue
from logging import Logger from logging import Logger
from typing import Tuple, List, Dict, Optional, Union, Any from typing import Tuple, List, Dict, Optional, Union, Any
@@ -19,7 +19,22 @@ class Client:
self.__tasks = [] self.__tasks = []
self.__reader = reader self.__reader = reader
self.__writer = writer self.__writer = writer
self.__packets_queue = [] self.__queue_tpc = Queue()
self.__queue_udp = Queue()
self._tpc_count_recv = 0
self._udp_count_recv = 0
self._tpc_count_total_recv = 0
self._udp_count_total_recv = 0
self._udp_size_total_recv = 0.1
self._tpc_size_total_recv = 0.1
# self._tpc_count_sent = 0
# self._udp_count_sent = 0
self._tpc_count_total_sent = 0
self._udp_count_total_sent = 0
self._udp_size_total_sent = 0.1
self._tpc_size_total_sent = 0.1
self.tcp_pps = 0
self.udp_pps = 0
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None) self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None) self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
self._log = utils.get_logger("client(id: )") self._log = utils.get_logger("client(id: )")
@@ -40,6 +55,9 @@ class Client:
self._unicycle: Dict[str, Union[int, str]] = {"id": -1, "packet": ""} self._unicycle: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
self._last_position = {} self._last_position = {}
self._lock = Lock() self._lock = Lock()
self.__tpt_id = 0
self.__tpu_id = 0
self.__tpp_id = 0
async def __gracefully_kick(self): ... async def __gracefully_kick(self): ...
@property @property
@@ -73,11 +91,11 @@ class Client:
def is_disconnected(self) -> bool: ... def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ... async def kick(self, reason: str) -> None: ...
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:... async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
async def send_event(self, event_name: str, event_data: Any, to_all: bool = True) -> None: ... async def send_event(self, event_name: str, event_data: Any, to_all: bool = False) -> None: ...
async def _send(self, data: bytes | str, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ... async def _send(self, data: bytes | str, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ...
async def _sync_resources(self) -> None: ... async def _sync_resources(self) -> None: ...
async def _recv(self, one=False) -> bytes | None: ... async def _recv(self, one=False) -> bytes | None: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ... async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> int: ...
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ... async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
async def _spawn_car(self, data: str) -> None: ... async def _spawn_car(self, data: str) -> None: ...
async def delete_car(self, car_id: int) -> None: ... async def delete_car(self, car_id: int) -> None: ...
@@ -87,7 +105,13 @@ class Client:
async def _handle_car_codes(self, data: str) -> None: ... async def _handle_car_codes(self, data: str) -> None: ...
async def _connected_handler(self) -> None: ... async def _connected_handler(self) -> None: ...
async def _chat_handler(self, data: str) -> None: ... async def _chat_handler(self, data: str) -> None: ...
async def _handle_codes(self, data: bytes) -> None: ... async def _handle_codes_tcp(self, data: bytes) -> None: ...
async def _handle_codes_udp(self, data: bytes) -> None: ...
def _tick_pps(self, _): ...
async def __tick_player_tcp(self, _): ...
async def __tick_player_udp(self, _): ...
async def _tpc_put(self, data): ...
async def _udp_put(self, data): ...
async def _looper(self) -> None: ... async def _looper(self) -> None: ...
def _update_logger(self) -> None: ... def _update_logger(self) -> None: ...
async def _remove_me(self) -> None: ... async def _remove_me(self) -> None: ...

View File

@@ -2,22 +2,24 @@
# File core.__init__.py # File core.__init__.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.5 # Version 1.5
# Core version: 0.4.5 # Core version: 0.4.8
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2024 # (c) kuitoi.su 2024
__title__ = 'KuiToi-Server' __title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.' __description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server' __url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.4.6' __version__ = '0.4.8'
__build__ = 2421 # Я это считаю лог файлами __build__ = 2800 # Я это считаю лог файлами
__author__ = 'SantaSpeen' __author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su' __author_email__ = 'admin@anidev.ru'
__license__ = "FPA" __license__ = "FPA"
__copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)' __copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)'
import asyncio import asyncio
import builtins import builtins
import sys
import time
import webbrowser import webbrowser
import prompt_toolkit.shortcuts as shortcuts import prompt_toolkit.shortcuts as shortcuts
@@ -29,12 +31,16 @@ from modules import ConfigProvider, EventsSystem
from modules import Console from modules import Console
from modules import MultiLanguage from modules import MultiLanguage
builtins.Ts = time.monotonic()
args, _ = parser.parse_known_args() args, _ = parser.parse_known_args()
if args.version: if args.version:
print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}") print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}")
exit(0) exit(0)
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# loop.set_task_factory(asyncio.eager_task_factory)
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
log = get_logger("core.init") log = get_logger("core.init")

View File

@@ -1,34 +1,43 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.core.py # File core.core.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version: 0.4.5 # Version: 0.4.8
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
import math import math
import os import os
import random import random
import statistics
import time import time
from threading import Thread from collections import deque
import aiohttp import aiohttp
import uvicorn
from core import utils, __version__ from core import utils, __version__
from core.Client import Client from core.Client import Client
from core.tcp_server import TCPServer from core.tcp_server import TCPServer
from core.udp_server import UDPServer from core.udp_server import UDPServer
from modules import PluginsLoader from modules import PluginsLoader, PermsSystem
from modules.WebAPISystem import app as webapp
def calc_ticks(ticks, duration):
while ticks and ticks[0] < time.monotonic() - duration:
ticks.popleft()
return len(ticks) / duration
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# noinspection PyProtectedMember # noinspection PyProtectedMember
class Core: class Core:
def __init__(self): def __init__(self):
self.tick_counter = 0
self.log = utils.get_logger("core") self.log = utils.get_logger("core")
self.loop = asyncio.new_event_loop() self.loop = asyncio.get_event_loop()
asyncio.set_event_loop(self.loop)
self.start_time = time.monotonic() self.start_time = time.monotonic()
self.run = False self.run = False
self.direct = False self.direct = False
@@ -41,9 +50,12 @@ class Core:
self.server_port = config.Server["server_port"] self.server_port = config.Server["server_port"]
self.tcp = TCPServer self.tcp = TCPServer
self.udp = UDPServer self.udp = UDPServer
self.web_thread = None
self.web_pool = webapp.data_pool self.tcp_pps = 0
self.web_stop = None self.udp_pps = 0
self.tps = 60
self.target_tps = 60
self.lock_upload = False self.lock_upload = False
@@ -53,8 +65,10 @@ class Core:
ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")])) ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
ev.register("_get_player", lambda x: self.get_client(**x['kwargs'])) ev.register("_get_player", lambda x: self.get_client(**x['kwargs']))
def get_client(self, cid=None, nick=None): def get_client(self, cid=None, nick=None, raw=False):
if cid is None and nick is None: if raw:
return self.clients_by_nick
if (cid, nick) == (None, None):
return None return None
if cid is not None: if cid is not None:
if cid == -1: if cid == -1:
@@ -103,24 +117,30 @@ class Core:
out = out[:-1] out = out[:-1]
return out return out
async def check_alive(self): async def _check_alive(self, _):
self.log.debug("Starting alive checker.") # self.log.debug("alive checker.")
maxp = config.Game['players']
try: try:
while self.run: for client in self.clients:
await asyncio.sleep(1) if not client:
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}" continue
for client in self.clients: if not client.ready:
if not client: client.is_disconnected()
continue continue
if not client.ready: if not client.alive:
client.is_disconnected() await client.kick("You are not alive!")
continue
if not client.alive:
await client.kick("You are not alive!")
await client._send(ca)
except Exception as e: except Exception as e:
self.log.error("Error in check_alive.") self.log.error("Error in _check_alive.")
self.log.exception(e)
async def _send_online(self, _):
try:
for client in self.clients:
ca = f"Ss{len(self.clients_by_id)}/{config.Game['players']}:{self.get_clients_list()}"
if not client or not client.alive or not client.ready:
continue
await client._send(ca)
except Exception as e:
self.log.error("Error in _send_online.")
self.log.exception(e) self.log.exception(e)
async def __gracefully_kick(self): async def __gracefully_kick(self):
@@ -129,23 +149,11 @@ class Core:
continue continue
await client.kick("Server shutdown!") await client.kick("Server shutdown!")
@staticmethod async def __gracefully_remove(self):
def start_web(): for client in self.clients:
uvconfig = uvicorn.Config("modules.WebAPISystem.app:web_app", if not client:
host=config.WebAPI["server_ip"], continue
port=config.WebAPI["server_port"], await client._remove_me()
loop="asyncio")
uvserver = uvicorn.Server(uvconfig)
webapp.uvserver = uvserver
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
raise KeyboardInterrupt
# noinspection SpellCheckingInspection,PyPep8Naming # noinspection SpellCheckingInspection,PyPep8Naming
async def heartbeat(self, test=False): async def heartbeat(self, test=False):
@@ -160,11 +168,16 @@ class Core:
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"] 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" _map = config.Game['map'] if "/" in config.Game['map'] else f"/levels/{config.Game['map']}/info.json"
tags = config.Server['tags'].replace(", ", ";").replace(",", ";") tags = config.Server['tags'].replace(", ", ";").replace(",", ";")
self.log.debug(f"[heartbeat] {_map=}")
self.log.debug(f"[heartbeat] {tags=}")
if tags and tags[-1:] != ";": if tags and tags[-1:] != ";":
tags += ";" tags += ";"
modlist = "".join(f"/{os.path.basename(mod['path'])};" for mod in self.mods_list[1:]) modlist = "".join(f"/{os.path.basename(mod['path'])};" for mod in self.mods_list[1:])
modstotalsize = self.mods_list[0] modstotalsize = self.mods_list[0]
modstotal = len(self.mods_list) - 1 modstotal = len(self.mods_list) - 1
self.log.debug(f"[heartbeat] {modlist=}")
self.log.debug(f"[heartbeat] {modstotalsize=}")
self.log.debug(f"[heartbeat] {modstotal=}")
while self.run: while self.run:
playerslist = "".join(f"{client.nick};" for client in self.clients if client and client.alive) playerslist = "".join(f"{client.nick};" for client in self.clients if client and client.alive)
data = { data = {
@@ -236,13 +249,13 @@ class Core:
if test: if test:
return bool(body) return bool(body)
await asyncio.sleep(5) await asyncio.sleep(15)
except Exception as e: except Exception as e:
self.log.error(f"Error in heartbeat: {e}") self.log.error(f"Error in heartbeat: {e}")
async def kick_cmd(self, args): async def _cmd_kick(self, args):
if not len(args) > 0: if not len(args) > 0:
return "\nUsage: kick <nick>|:<id> [reason]\nExamples:\n\tkick admin bad boy\n\tkick :0 bad boy" return "Usage: kick <nick>|:<id> [reason]\nExamples:\n\tkick admin bad boy\n\tkick :0 bad boy"
reason = "kicked by console." reason = "kicked by console."
if len(args) > 1: if len(args) > 1:
reason = " ".join(args[1:]) reason = " ".join(args[1:])
@@ -256,38 +269,142 @@ class Core:
else: else:
return "Client not found." return "Client not found."
async def _useful_ticks(self, _):
tasks = []
self.tick_counter += 1
events = {
0.5: "serverTick_0.5s",
1: "serverTick_1s",
2: "serverTick_2s",
3: "serverTick_3s",
4: "serverTick_4s",
5: "serverTick_5s",
10: "serverTick_10s",
30: "serverTick_30s",
60: "serverTick_60s"
}
for interval in sorted(events.keys(), reverse=True):
if self.tick_counter % (interval * self.target_tps) == 0:
ev.call_event(events[interval])
tasks.append(ev.call_async_event(events[interval]))
await asyncio.gather(*tasks)
if self.tick_counter == (60 * self.target_tps):
self.tick_counter = 0
def _get_color_tps(self, ticks, d):
tps = calc_ticks(ticks, d)
half = self.target_tps // 2
qw = self.target_tps // 4
if tps > half + qw:
return f"<green><b>{tps:.2f}</b></green>"
elif tps > half:
return f"<yellow><b>{tps:.2f}</b></yellow>"
elif half > tps:
return f"<red><b>{tps:.2f}</b></red>"
def _cmd_tps(self, ticks_2s, ticks_5s, ticks_30s, ticks_60s):
t = ["-, ", "-, ", "-."]
if len(ticks_5s) > 5 * self.target_tps:
t[0] = f"{self._get_color_tps(ticks_5s, 5)}, "
if len(ticks_30s) > 30 * self.target_tps:
t[1] = f"{self._get_color_tps(ticks_30s, 30)}, "
if len(ticks_60s) > 60 * self.target_tps:
t[2] = f"{self._get_color_tps(ticks_60s, 60)}."
return f"html:{self._get_color_tps(ticks_2s, 2)} TPS; For last 5s, 30s, 60s: " + "".join(t)
async def _tick(self):
try:
ticks = 0
target_tps = self.target_tps
last_tick_time = time.monotonic()
ev.register("serverTick", self._useful_ticks)
ticks_2s = deque(maxlen=2 * int(target_tps) + 1)
ticks_5s = deque(maxlen=5 * int(target_tps) + 1)
ticks_30s = deque(maxlen=30 * int(target_tps) + 1)
ticks_60s = deque(maxlen=60 * int(target_tps) + 1)
console.add_command("tps", lambda _: self._cmd_tps(ticks_2s, ticks_5s, ticks_30s, ticks_60s),
None, "Print TPS", {"tps": None})
_add_to_sleep = deque([0.0, 0.0, 0.0, ], maxlen=3 * int(target_tps))
# _t0 = []
self.log.debug("tick system started")
while self.run:
target_interval = 1 / self.target_tps
start_time = time.monotonic()
ev.call_event("serverTick")
await ev.call_async_event("serverTick")
# Calculate the time taken for this tick
end_time = time.monotonic()
tick_duration = end_time - start_time
# _t0.append(tick_duration)
# Calculate the time to sleep to maintain target TPS
sleep_time = target_interval - tick_duration - statistics.fmean(_add_to_sleep)
if sleep_time > 0:
await asyncio.sleep(sleep_time)
# Update tick count and time
ticks += 1
current_time = time.monotonic()
ticks_2s.append(current_time)
ticks_5s.append(current_time)
ticks_30s.append(current_time)
ticks_60s.append(current_time)
# Calculate TPS
elapsed_time = current_time - last_tick_time
if elapsed_time >= 1:
self.tps = ticks / elapsed_time
# if self.tps < 5:
# self.log.warning(f"Low TPS: {self.tps:.2f}")
# Reset for next calculation
# _t0s = max(_t0), min(_t0), statistics.fmean(_t0)
# _tw = max(_add_to_sleep), min(_add_to_sleep), statistics.fmean(_add_to_sleep)
# self.log.debug(f"[{'OK' if sleep_time > 0 else "CHECK"}] TPS: {self.tps:.2f}; Tt={_t0s}; Ts={sleep_time}; Tw={_tw}")
# _t0 = []
last_tick_time = current_time
ticks = 0
_add_to_sleep.append(time.monotonic() - start_time - sleep_time)
self.log.debug("tick system stopped")
except Exception as e:
self.log.exception(e)
async def _parse_chat(self, event):
player = event['kwargs']['player']
message = event['kwargs']['message']
async def main(self): async def main(self):
self.tcp = self.tcp(self, self.server_ip, self.server_port) self.tcp = self.tcp(self, self.server_ip, self.server_port)
self.udp = self.udp(self, self.server_ip, self.server_port) self.udp = self.udp(self, self.server_ip, self.server_port)
PermsSystem()
console.add_command( console.add_command(
"list", "list",
lambda x: f"Players list: {self.get_clients_list(True)}" lambda x: f"Players list: {self.get_clients_list(True)}"
) )
console.add_command("kick", self.kick_cmd) ev.call_event("add_perm_to_alias", "cmd.kick")
console.add_command("kick", self._cmd_kick, "kick - Kick user\n"
"Usage: kick NICK|:{ID} [REASON]\n"
"Examples:\n"
" <white>></white> <b><skyblue>kick admin bad boy</skyblue></b>\n"
" <white>></white> <b><skyblue>kick :0 bad boy</skyblue></b>",
"kick user", {"kick": "<playerlist>"})
ev.register("onChatReceive", self._parse_chat)
pl_dir = "plugins" pl_dir = "plugins"
self.log.debug("Initializing PluginsLoaders...") self.log.debug("Initializing PluginsLoaders...")
if not os.path.exists(pl_dir): if not os.path.exists(pl_dir):
os.mkdir(pl_dir) os.mkdir(pl_dir)
pl = PluginsLoader(pl_dir) pl = PluginsLoader(pl_dir)
await pl.load()
if config.Options['use_lua']: if config.Options['use_lua']:
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
lpl = LuaPluginsLoader(pl_dir) lpl = LuaPluginsLoader(pl_dir)
lpl.load() lpl.load()
await pl.load()
try: try:
# WebApi Start
if config.WebAPI["enabled"]:
self.log.debug("Initializing WebAPI...")
web_thread = Thread(target=self.start_web, name="WebApiThread")
web_thread.start()
self.log.debug(f"WebAPI started at new thread: {web_thread.name}")
self.web_thread = web_thread
# noinspection PyProtectedMember
self.web_stop = webapp._stop
await asyncio.sleep(.3)
# Mods handler # Mods handler
self.log.debug("Listing mods..") self.log.debug("Listing mods..")
if not os.path.exists(self.mods_dir): if not os.path.exists(self.mods_dir):
@@ -304,16 +421,19 @@ class Core:
self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2))) self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
self.log.info(i18n.init_ok) self.log.info(i18n.init_ok)
await self.heartbeat(True) await self.heartbeat(True) # Check
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
self.clients.append(None) self.clients = [None] * config.Game["players"] * 4 # * 4 For down sock and buffer.
tasks = [] tasks = []
# self.udp.start, ev.register("serverTick_1s", self._check_alive)
f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive] ev.register("serverTick_1s", self._send_online)
# ev.register("serverTick_5s", self.heartbeat)
f_tasks = [self.tcp.start, self.udp._start, console.start, self._tick, self.heartbeat]
if config.RCON['enabled']: if config.RCON['enabled']:
console.rcon.version = f"KuiToi {__version__}" self.log.warning("RCON not available. yet.")
rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port']) # console.rcon.version = f"KuiToi {__version__}"
f_tasks.append(rcon.start) # rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port'])
# f_tasks.append(rcon.start)
for task in f_tasks: for task in f_tasks:
tasks.append(asyncio.create_task(task())) tasks.append(asyncio.create_task(task()))
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
@@ -328,32 +448,35 @@ class Core:
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
except Exception as e: except Exception as e:
self.log.error(f"Exception: {e}") self.log.error(f"Exception in main:")
self.log.exception(e) self.log.exception(e)
finally: finally:
self.run = False
self.tcp.stop()
self.udp._stop()
await self.stop() await self.stop()
def start(self): def start(self):
asyncio.run(self.main()) asyncio.run(self.main())
async def stop(self): async def stop(self):
ev.call_lua_event("onShutdown")
ev.call_event("onServerStopped")
await ev.call_async_event("onServerStopped")
await self.__gracefully_kick()
if config.Options['use_lua']:
ev.call_event("_lua_plugins_unload")
await ev.call_async_event("_plugins_unload")
self.run = False self.run = False
if config.WebAPI["enabled"]: ev.call_lua_event("onShutdown")
asyncio.run(self.web_stop()) await ev.call_async_event("onServerStopped")
total_time = time.monotonic() - self.start_time ev.call_event("onServerStopped")
hours = int(total_time // 3600) try:
minutes = int((total_time % 3600) // 60) await self.__gracefully_kick()
seconds = math.ceil(total_time % 60) await self.__gracefully_remove()
t = f"{'' if not hours else f'{hours} hours, '}{'' if not hours else f'{minutes} min., '}{seconds} sec." self.tcp.stop()
self.log.info(f"Working time: {t}") self.udp._stop()
self.log.info(i18n.stop) await ev.call_async_event("_plugins_unload")
if config.Options['use_lua']:
await ev.call_async_event("_lua_plugins_unload")
self.run = False
total_time = time.monotonic() - self.start_time
hours = int(total_time // 3600)
minutes = int((total_time % 3600) // 60)
seconds = math.ceil(total_time % 60)
t = f"{'' if not hours else f'{hours} hours, '}{'' if not hours else f'{minutes} min., '}{seconds} sec."
self.log.info(f"Working time: {t}")
self.log.info(i18n.stop)
except Exception as e:
self.log.error("Error while stopping server:")
self.log.exception(e)

View File

@@ -17,6 +17,9 @@ from .udp_server import UDPServer
class Core: class Core:
def __init__(self): def __init__(self):
self.target_tps = 50
self.tick_counter = 0
self.tps = 10
self.start_time = time.monotonic() self.start_time = time.monotonic()
self.log = utils.get_logger("core") self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
@@ -41,13 +44,17 @@ class Core:
async def insert_client(self, client: Client) -> None: ... async def insert_client(self, client: Client) -> None: ...
def create_client(self, *args, **kwargs) -> Client: ... def create_client(self, *args, **kwargs) -> Client: ...
def get_clients_list(self, need_cid=False) -> str: ... def get_clients_list(self, need_cid=False) -> str: ...
async def check_alive(self) -> None: ... async def _check_alive(self) -> None: ...
async def _send_online(self) -> None: ...
async def _useful_ticks(self, _) -> None: ...
async def __gracefully_kick(self): ... async def __gracefully_kick(self): ...
@staticmethod async def __gracefully_remove(self): ...
def start_web() -> None: ... def _get_color_tps(self, ticks, d): ...
def stop_me(self) -> None: ... async def _cmd_tps(self, ticks_2s, ticks_5s, ticks_30s, ticks_60s) -> str: ...
def _tick(self) -> None: ...
async def heartbeat(self, test=False) -> None: ... async def heartbeat(self, test=False) -> None: ...
async def kick_cmd(self, args: list) -> None | str: ... async def _cmd_kick(self, args: list) -> None | str: ...
async def _parse_chat(self, event): ...
async def main(self) -> None: ... async def main(self) -> None: ...
def start(self) -> None: ... def start(self) -> None: ...
async def stop(self) -> None: ... async def stop(self) -> None: ...

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.py # File core.tcp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.5 # Core version: 0.4.8
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -22,6 +22,8 @@ class TCPServer:
self.host = host self.host = host
self.port = port self.port = port
self.run = False self.run = False
self._connections = set()
self.server = None
self.rl = RateLimiter(50, 10, 300) self.rl = RateLimiter(50, 10, 300)
console.add_command("rl", self.rl.parse_console, None, "RateLimiter menu", console.add_command("rl", self.rl.parse_console, None, "RateLimiter menu",
{"rl": {"info": None, "unban": None, "ban": None, "help": None}}) {"rl": {"info": None, "unban": None, "ban": None, "help": None}})
@@ -35,12 +37,11 @@ class TCPServer:
await client.kick(i18n.core_player_kick_outdated) await client.kick(i18n.core_player_kick_outdated)
return False, client return False, client
else: else:
# await client._send(b"S") # Accepted client version
await client._send(b"A") # Accepted client version await client._send(b"A") # Accepted client version
data = await client._recv(True) data = await client._recv(True)
self.log.debug(f"Key: {data}") self.log.debug(f"Key: {data}")
if len(data) > 50: if not data or len(data) > 50:
await client.kick(i18n.core_player_kick_bad_key) await client.kick(i18n.core_player_kick_bad_key)
return False, client return False, client
client._key = data.decode("utf-8") client._key = data.decode("utf-8")
@@ -56,6 +57,9 @@ class TCPServer:
return False, client return False, client
client.nick = res["username"] client.nick = res["username"]
client.roles = res["roles"] client.roles = res["roles"]
self.log.debug(f"{client.roles=} {client.nick=}")
if client.roles == "USER" and client.nick == "SantaSpeen":
client.roles = "ADM"
client._guest = res["guest"] client._guest = res["guest"]
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]} client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
if not client._identifiers.get("ip"): if not client._identifiers.get("ip"):
@@ -89,6 +93,10 @@ class TCPServer:
return False, client return False, client
ev.call_event("onPlayerAuthenticated", player=client) ev.call_event("onPlayerAuthenticated", player=client)
await ev.call_async_event("onPlayerAuthenticated", player=client)
if not client.alive:
await client.kick("Not accepted.")
return False, client
if len(self.Core.clients_by_id) > config.Game["players"]: if len(self.Core.clients_by_id) > config.Game["players"]:
await client.kick(i18n.core_player_kick_server_full) await client.kick(i18n.core_player_kick_server_full)
@@ -118,61 +126,55 @@ class TCPServer:
result, client = await self.auth_client(reader, writer) result, client = await self.auth_client(reader, writer)
if result: if result:
await client._looper() await client._looper()
return result, client return "U", client
case "D": case "D":
await self.set_down_rw(reader, writer) await self.set_down_rw(reader, writer)
return "D", None
case "P": case "P":
writer.write(b"P") writer.write(b"P")
await writer.drain() await writer.drain()
writer.close() writer.close()
case _: case _:
self.log.error(f"Unknown code: {code}") self.log.warning(f"Unknown code: {code}")
self.log.info("Report about that!") self.log.warning("Report about that!")
writer.close() writer.close()
return False, None return "E", None
async def handle_client(self, reader, writer): async def handle_client(self, reader, writer):
while True: self._connections.add(writer)
try: try:
ip = writer.get_extra_info('peername')[0] ip = writer.get_extra_info('peername')[0]
if self.rl.is_banned(ip): if self.rl.is_banned(ip):
await self.rl.notify(ip, writer) await self.rl.notify(ip, writer)
writer.close() writer.close()
break data = await reader.read(1)
data = await reader.read(1) if not data:
if not data: return
break code = data.decode()
code = data.decode() self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}") _type, cl = await self.handle_code(code, reader, writer)
# task = asyncio.create_task(self.handle_code(code, reader, writer)) self.log.debug(f"[{_type}] cl returned: {cl}")
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION) if cl:
_, cl = await self.handle_code(code, reader, writer) await cl._remove_me()
if cl: except Exception as e:
await cl._remove_me() self.log.error("Error while handling connection...")
break self.log.exception(e)
except Exception as e: traceback.print_exc()
self.log.error("Error while handling connection...")
self.log.exception(e)
traceback.print_exc()
break
async def start(self): async def start(self):
self.log.debug("Starting TCP server.") self.log.debug("Starting TCP server.")
self.run = True self.run = True
try: try:
server = await asyncio.start_server(self.handle_client, self.host, self.port, self.server = await asyncio.start_server(self.handle_client, self.host, self.port,
backlog=int(config.Game["players"] * 2.3)) backlog=int(config.Game["players"] * 4))
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}") async with self.server:
while True: self.log.debug(f"TCP server started on {self.server.sockets[0].getsockname()!r}")
async with server: await self.server.serve_forever()
await server.serve_forever()
except OSError as e: except OSError as e:
self.log.error(i18n.core_bind_failed.format(e)) self.log.error(i18n.core_bind_failed.format(e))
raise e raise e
except KeyboardInterrupt:
pass
except Exception as e: except Exception as e:
self.log.error(f"Error: {e}") self.log.exception(e)
raise e raise e
finally: finally:
self.run = False self.run = False
@@ -180,3 +182,16 @@ class TCPServer:
def stop(self): def stop(self):
self.log.debug("Stopping TCP server") self.log.debug("Stopping TCP server")
try:
if not self.server:
return
self.server.close()
for conn in self._connections:
self.log.debug(f"Closing {conn}")
try:
conn.close()
except ConnectionResetError:
self.log.debug("ConnectionResetError")
except Exception as e:
self.log.exception(e)
self.log.debug("Stopped.")

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.pyi # File core.tcp_server.pyi
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.5 # Core version: 0.4.8
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -15,11 +15,13 @@ from modules import RateLimiter
class TCPServer: class TCPServer:
def __init__(self, core: Core, host, port): def __init__(self, core: Core, host, port):
self.server = await asyncio.start_server(self.handle_client, "", 0, backlog=int(config.Game["players"] * 2.3))
self.log = utils.get_logger("TCPServer") self.log = utils.get_logger("TCPServer")
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.Core = core self.Core = core
self.host = host self.host = host
self.port = port self.port = port
self._connections = set()
self.run = False self.run = False
self.rl = RateLimiter(50, 10, 15) self.rl = RateLimiter(50, 10, 15)

View File

@@ -1,11 +1,12 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.udp_server.py # File core.udp_server
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.5 # Core version: 0.4.7
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2024 # (c) kuitoi.su 2024
import asyncio import asyncio
import json import json
import time
from core import utils from core import utils
@@ -27,45 +28,24 @@ class UDPServer(asyncio.DatagramTransport):
def pause_writing(self, *args, **kwargs): ... def pause_writing(self, *args, **kwargs): ...
def resume_writing(self, *args, **kwargs): ... def resume_writing(self, *args, **kwargs): ...
async def handle_datagram(self, data, addr): async def handle_datagram(self, packet, addr):
try: try:
cid = data[0] - 1 cid = packet[0] - 1
code = data[2:3].decode() if cid > config.Game['players'] * 4:
data = data[2:].decode() return
client = self._core.get_client(cid=cid) client = self._core.get_client(cid=cid)
if client: if client:
if not client.alive: if not client.alive:
self.log.debug(f"{client.nick}:{cid} still sending UDP data: {data}") client.log.debug(f"Still sending UDP data: {packet}")
match code: if client._udp_sock != (self.transport, addr):
case "p": # Ping packet self.log.debug(f"udp_addr={addr[0]}; main_addr={client.addr}")
ev.call_event("onSentPing") if addr[0] != client.addr:
self.transport.sendto(b"p", addr) self.log.warning(f"udp_addr != main_addr. Is this bug?")
case "Z": # Position packet client._udp_sock = (self.transport, addr)
if client._udp_sock != (self.transport, addr): self.log.debug(f"Set UDP Sock for CID: {cid}")
client._udp_sock = (self.transport, addr) await client._udp_put(packet)
self.log.debug(f"Set UDP Sock for CID: {cid}")
ev.call_event("onChangePosition", data=data)
sub = data.find("{", 1)
last_pos = data[sub:]
try:
_, car_id = client._get_cid_vid(data)
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.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.warning(f" UDP [{cid}] Unknown code: {code}; {data}")
else: else:
self.log.debug(f"[{cid}] Client not found.") self.log.debug(f"[{cid}] Client not found.")
except Exception as e: except Exception as e:
self.log.error(f"Error handle_datagram: {e}") self.log.error(f"Error handle_datagram: {e}")
@@ -87,7 +67,6 @@ class UDPServer(asyncio.DatagramTransport):
self.log.debug("Starting UDP server.") self.log.debug("Starting UDP server.")
while self._core.run: while self._core.run:
try: try:
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
d = UDPServer d = UDPServer
@@ -104,8 +83,8 @@ class UDPServer(asyncio.DatagramTransport):
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
except OSError as e: except OSError as e:
# self.run = False self.run = False
# self.Core.run = False self._core.run = False
self.log.error(f"Cannot bind port or other error: {e}") self.log.error(f"Cannot bind port or other error: {e}")
except Exception as e: except Exception as e:
self.log.error(f"Error: {e}") self.log.error(f"Error: {e}")

View File

@@ -22,9 +22,9 @@ class UDPServer(asyncio.DatagramTransport):
self.host = host self.host = host
self.port = port self.port = port
self.run = False self.run = False
# self.transport: DatagramTransport = None
def connection_made(self, transport: DatagramTransport): ... def connection_made(self, transport: DatagramTransport): ...
async def handle_datagram(self, data: bytes, addr: Tuple[str, int]): async def handle_datagram(self, data: bytes, addr: Tuple[str, int]):
def datagram_received(self, data: bytes, addr: Tuple[str, int]): ... def datagram_received(self, data: bytes, addr: Tuple[str, int]): ...
async def _print_pps(self) -> None: ...
async def _start(self) -> None: ... async def _start(self) -> None: ...
async def _stop(self) -> None: ... async def _stop(self) -> None: ...

View File

@@ -13,7 +13,7 @@ import yaml
class Config: class Config:
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=None): def __init__(self, auth=None, game=None, server=None, rcon=None, options=None):
self.Auth = auth or {"key": None, "private": True} self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "cars": 1} 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", self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "tags": "Freroam",
@@ -22,12 +22,10 @@ class Config:
"use_lua": False, "log_chat": True} "use_lua": False, "log_chat": True}
self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383, self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383,
"password": secrets.token_hex(6)} "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): def __repr__(self):
return f"{self.__class__.__name__}(Auth={self.Auth!r}, Game={self.Game!r}, Server={self.Server!r}, " \ 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})" f"RCON={self.RCON!r}, Options={self.Options!r})")
class ConfigProvider: class ConfigProvider:
@@ -51,7 +49,7 @@ class ConfigProvider:
if _again: if _again:
print("Error: empty configuration.") print("Error: empty configuration.")
exit(1) exit(1)
print("Empty config?..") print("Reconfig: empty configuration.")
os.remove(self.config_path) os.remove(self.config_path)
self.config = Config() self.config = Config()
return self.read(True) return self.read(True)
@@ -68,5 +66,6 @@ class ConfigProvider:
del _config.enc del _config.enc
del _config.Options['debug'] del _config.Options['debug']
del _config.Options['encoding'] del _config.Options['encoding']
os.remove(self.config_path)
with open(self.config_path, "w", encoding="utf-8") as f: with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(_config, f) yaml.dump(_config, f)

View File

@@ -7,7 +7,6 @@ class Config:
Server: Dict[str, object] Server: Dict[str, object]
RCON: Dict[str, object] RCON: Dict[str, object]
Options: Dict[str, object] Options: Dict[str, object]
WebAPI: Dict[str, object]
enc: str | None enc: str | None
def __repr__(self): def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server) return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)

View File

@@ -11,20 +11,106 @@ import inspect
import logging import logging
from typing import AnyStr from typing import AnyStr
from prompt_toolkit import PromptSession, print_formatted_text, HTML from prompt_toolkit import PromptSession, print_formatted_text, HTML, ANSI
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter from prompt_toolkit.completion import Completer, WordCompleter
from prompt_toolkit.document import Document
from prompt_toolkit.history import FileHistory from prompt_toolkit.history import FileHistory
try: try:
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
except AssertionError: except AssertionError:
class NoConsoleScreenBufferError(Exception): ... class NoConsoleScreenBufferError(Exception):
...
from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.patch_stdout import patch_stdout
from core import get_logger from core import get_logger
from modules.ConsoleSystem.RCON import RCONSystem from modules.ConsoleSystem.RCON import RCONSystem
class BadCompleter(Exception): ...
class MyNestedCompleter(Completer):
def __init__(self, options, ignore_case=True, on_none=None):
self.options = self._from_nested_dict(options)
self.ignore_case = ignore_case
self.on_none = on_none
def __repr__(self) -> str:
return f"MyNestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
@classmethod
def _from_nested_dict(cls, data, r=False):
options: dict[str, Completer | None] = {}
for key, value in data.items():
if isinstance(value, Completer):
options[key] = value
elif isinstance(value, dict):
options[key] = cls._from_nested_dict(value, True)
elif isinstance(value, set):
options[key] = cls._from_nested_dict({item: None for item in value}, True)
elif isinstance(value, bool):
if value:
options[key] = None
else:
if isinstance(value, str) and value == "<playerlist>":
options[key] = players_completer
else:
if value is not None:
raise BadCompleter(f"{value!r} for key {key!r} have not valid type.")
options[key] = None
if r:
return cls(options)
return options
def load(self, data):
self.options = self._from_nested_dict(data)
def get_completions(self, document, complete_event):
# Split document.
text = document.text_before_cursor.lstrip()
stripped_len = len(document.text_before_cursor) - len(text)
# If there is a space, check for the first term, and use a
# subcompleter.
if " " in text:
first_term = text.split()[0]
completer = self.options.get(first_term)
if completer is None:
completer = self.on_none
# If we have a sub completer, use this for the completions.
if completer is not None:
remaining_text = text[len(first_term):].lstrip()
move_cursor = len(text) - len(remaining_text) + stripped_len
new_document = Document(
remaining_text,
cursor_position=document.cursor_position - move_cursor,
)
yield from completer.get_completions(new_document, complete_event)
# No space in the input: behave exactly like `WordCompleter`.
else:
completer = WordCompleter(
list(self.options.keys()), ignore_case=self.ignore_case
)
yield from completer.get_completions(document, complete_event)
def tick_players(self, _):
clients = ev.call_event("_get_player", raw=True)[0]
self.options = {}
for k in clients.keys():
self.options[k] = None
players_completer = MyNestedCompleter({})
builtins.Completer = MyNestedCompleter
builtins.players_completer = players_completer
class Console: class Console:
def __init__(self, def __init__(self,
@@ -33,8 +119,12 @@ class Console:
not_found="Command \"%s\" not found in alias.", not_found="Command \"%s\" not found in alias.",
debug=False) -> None: debug=False) -> None:
self.__logger = get_logger("console") self.__logger = get_logger("console")
self.__is_run = False self.__run = False
self.no_cmd = False try:
self.session = PromptSession(history=FileHistory('./.cmdhistory'))
self.__legacy_mode = False
except NoConsoleScreenBufferError:
self.__legacy_mode = True
self.__prompt_in = prompt_in self.__prompt_in = prompt_in
self.__prompt_out = prompt_out self.__prompt_out = prompt_out
self.__not_found = not_found self.__not_found = not_found
@@ -45,17 +135,21 @@ class Console:
self.__man = dict() self.__man = dict()
self.__desc = dict() self.__desc = dict()
self.__print_logger = get_logger("print") self.__print_logger = get_logger("print")
self.completer = MyNestedCompleter(self.__alias)
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man, self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
custom_completer={"man": {}}) custom_completer={"man": {}})
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help, self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
custom_completer={"help": {"--raw": None}}) custom_completer={"help": {"--raw": False}})
self.completer = NestedCompleter.from_nested_dict(self.__alias)
rcon = RCONSystem rcon = RCONSystem
rcon.console = self rcon.console = self
self.rcon = rcon self.rcon = rcon
@property
def legacy_mode(self):
return self.__legacy_mode
def __debug(self, *x): def __debug(self, *x):
self.__logger.debug(f"{x}") self.__logger.debug(' '.join(x))
# if self.__is_debug: # if self.__is_debug:
# x = list(x) # x = list(x)
# x.insert(0, "\r CONSOLE DEBUG:") # x.insert(0, "\r CONSOLE DEBUG:")
@@ -77,15 +171,11 @@ class Console:
def __create_man_message(self, argv: list) -> AnyStr: def __create_man_message(self, argv: list) -> AnyStr:
if len(argv) == 0: if len(argv) == 0:
return self.__man.get("man") 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) x = argv[0]
if man_message: if x not in self.__alias:
return man_message return i18n.man_command_not_found.format(x)
else: return self.__man.get(x)
return i18n.man_message_not_found
# noinspection PyStringFormat # noinspection PyStringFormat
def __create_help_message(self, argv: list) -> AnyStr: def __create_help_message(self, argv: list) -> AnyStr:
@@ -120,36 +210,51 @@ class Console:
return message return message
def __update_completer(self): def del_command(self, func):
self.completer = NestedCompleter.from_nested_dict(self.__alias) self.__debug(f"delete command: func={func};")
keys = []
for k, v in self.__func.items():
if v['f'] is func:
keys.append(k)
for key in keys:
self.__debug(f"{key=}")
self.__alias.pop(key)
self.__alias["man"].pop(key)
self.__func.pop(key)
self.__man.pop(key)
self.__desc.pop(key)
if keys:
self.__debug("Deleted.")
self.completer.load(self.__alias)
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict: 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): if not isinstance(key, str):
raise TypeError("key must be string") raise TypeError("key must be string")
key = key.replace(" ", "-")
self.__debug(f"added user command: key={key}; func={func};") self.__debug(f"added user command: key={key}; func={func};")
self.__alias.update(custom_completer or {key: None}) self.__alias.update(custom_completer or {key: None})
self.__alias["man"].update({key: None}) self.__alias["man"].update({key: None})
self.__func.update({key: {"f": func}}) self.__func.update({key: {"f": func}})
self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man}</seagreen>' if man else None}) self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man if man else "No page"}</seagreen>'})
self.__desc.update({key: desc}) self.__desc.update({key: desc})
self.__update_completer() self.completer.load(self.__alias)
return self.__alias.copy() return self.__alias.copy()
def _write(self, t): def _write(self, text):
if self.no_cmd: # https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html#formatted-text
print(t) if self.__legacy_mode:
print(text)
return return
try: assert isinstance(text, str)
if t.startswith("html:"): _type = text.split(":")[0]
print_formatted_text(HTML(t[5:])) match _type:
else: case "html":
print_formatted_text(t) print_formatted_text(HTML(text[5:]))
except NoConsoleScreenBufferError: case "ansi":
print("Works in non cmd mode.") print_formatted_text(ANSI(text[5:]))
self.no_cmd = True case _:
print(t) print_formatted_text(text)
def write(self, s: AnyStr): def write(self, s: AnyStr):
if isinstance(s, (list, tuple)): if isinstance(s, (list, tuple)):
@@ -159,12 +264,12 @@ class Console:
self._write(s) self._write(s)
def log(self, s: AnyStr) -> None: def log(self, s: AnyStr) -> None:
if isinstance(s, (list, tuple)): # if isinstance(s, (list, tuple)):
for text in s: # for text in s:
self.__logger.info(f"{text}") # self.__logger.info(f"{text}")
else: # else:
self.__logger.info(f"{s}") # self.__logger.info(f"{s}")
# self.write(s) self.write(s)
def __lshift__(self, s: AnyStr) -> None: def __lshift__(self, s: AnyStr) -> None:
self.write(s) self.write(s)
@@ -179,10 +284,10 @@ class Console:
end: str or None = None, end: str or None = None,
file: str or None = None, file: str or None = None,
flush: bool = False) -> None: flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}") self.__debug(f"Used __builtins_print; is_run: {self.__run}")
val = list(values) val = list(values)
if len(val) > 0: if len(val) > 0:
if self.__is_run: if self.__run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}") self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else: else:
if end is None: if end is None:
@@ -203,6 +308,7 @@ class Console:
except RecursionError: except RecursionError:
raise raise
except Exception as e: except Exception as e:
print(e)
cls.handleError(record) cls.handleError(record)
logging.StreamHandler.emit = emit logging.StreamHandler.emit = emit
@@ -215,57 +321,74 @@ class Console:
# builtins.print = self.__builtins_print # builtins.print = self.__builtins_print
async def read_input(self): async def _parse_input(self, inp):
session = PromptSession(history=FileHistory('./.cmdhistory')) cmd_s = inp.split(" ")
while True: cmd = cmd_s[0]
try: if cmd == "":
with patch_stdout(): return True
if self.no_cmd: else:
cmd_in = input(self.__prompt_in) found_in_lua = False
else: d = ev.call_lua_event("onConsoleInput", inp)
try: if len(d) > 0:
cmd_in = await session.prompt_async( for text in d:
self.__prompt_in, if text is not None:
completer=self.completer, found_in_lua = True
auto_suggest=AutoSuggestFromHistory() self.log(text)
) command_object = self.__func.get(cmd)
except NoConsoleScreenBufferError: if command_object:
print("Works in non cmd mode.") func = command_object['f']
self.no_cmd = True if inspect.iscoroutinefunction(func):
cmd_s = cmd_in.split(" ") out = await func(cmd_s[1:])
cmd = cmd_s[0]
if cmd == "":
continue
else: else:
found_in_lua = False out = func(cmd_s[1:])
d = ev.call_lua_event("onConsoleInput", cmd_in) if out:
if len(d) > 0: self.log(out)
for text in d: else:
if text is not None: if not found_in_lua:
found_in_lua = True self.log(self.__not_found % cmd)
self.log(text)
command_object = self.__func.get(cmd) async def _read_input(self):
if command_object: with patch_stdout():
func = command_object['f'] while self.__run:
if inspect.iscoroutinefunction(func): try:
out = await func(cmd_s[1:]) inp = await self.session.prompt_async(
else: self.__prompt_in, completer=self.completer, auto_suggest=AutoSuggestFromHistory()
out = func(cmd_s[1:]) )
if out: if await self._parse_input(inp):
self.log(out) continue
else: except EOFError:
if not found_in_lua: pass
self.log(self.__not_found % cmd) except KeyboardInterrupt:
self.__run = False
except Exception as e:
self.__logger.error("Exception in console.py:")
self.__logger.exception(e)
async def _read_input_legacy(self):
while self.__run:
try:
inp = input(self.__prompt_in)
if await self._parse_input(inp):
continue
except UnicodeDecodeError:
self.__logger.error("UnicodeDecodeError")
self.__run = False
except KeyboardInterrupt: except KeyboardInterrupt:
raise KeyboardInterrupt self.__run = False
except Exception as e: except Exception as e:
print(f"Error in console.py: {e}") self.__logger.error("Exception in console.py:")
self.__logger.exception(e) self.__logger.exception(e)
async def start(self): async def start(self):
self.__is_run = True ev.register("serverTick_0.5s", players_completer.tick_players)
await self.read_input() # ev.register("get_players_completer", lambda _: players_completer)
self.__run = True
if self.__legacy_mode:
await self._read_input_legacy()
else:
await self._read_input()
self.__debug("Closing console.")
raise KeyboardInterrupt
def stop(self, *args, **kwargs): def stop(self, *args, **kwargs):
self.__is_run = False self.__run = False
raise KeyboardInterrupt

View File

@@ -26,6 +26,7 @@ class EventsSystem:
"onPlayerSentKey": [], # Only sync, no handler "onPlayerSentKey": [], # Only sync, no handler
"onPlayerAuthenticated": [], # (!) Only sync, With handler "onPlayerAuthenticated": [], # (!) Only sync, With handler
"onPlayerJoin": [], # (!) With handler "onPlayerJoin": [], # (!) With handler
"onPlayerReady": [], # No handler
"onChatReceive": [], # (!) With handler "onChatReceive": [], # (!) With handler
"onCarSpawn": [], # (!) With handler "onCarSpawn": [], # (!) With handler
"onCarDelete": [], # (!) With handler (admin allow) "onCarDelete": [], # (!) With handler (admin allow)
@@ -37,10 +38,23 @@ class EventsSystem:
"onChangePosition": [], # Only sync, no handler "onChangePosition": [], # Only sync, no handler
"onPlayerDisconnect": [], # No handler "onPlayerDisconnect": [], # No handler
"onServerStopped": [], # No handler "onServerStopped": [], # No handler
"onCarSpawned": [], # No handler
"onCarDeleted": [], # No handler
"serverTick": [],
"serverTick_0.5s": [],
"serverTick_1s": [],
"serverTick_2s": [],
"serverTick_3s": [],
"serverTick_4s": [],
"serverTick_5s": [],
"serverTick_10s": [],
"serverTick_30s": [],
"serverTick_60s": [],
} }
self.__async_events = { self.__async_events = {
"onServerStarted": [], "onServerStarted": [],
"onPlayerJoin": [], "onPlayerJoin": [],
"onPlayerReady": [],
"onChatReceive": [], "onChatReceive": [],
"onCarSpawn": [], "onCarSpawn": [],
"onCarDelete": [], "onCarDelete": [],
@@ -48,8 +62,20 @@ class EventsSystem:
"onCarReset": [], "onCarReset": [],
"onCarChanged": [], "onCarChanged": [],
"onCarFocusMove": [], "onCarFocusMove": [],
"onServerStopped": [],
"onPlayerDisconnect": [], "onPlayerDisconnect": [],
"onServerStopped": [] "onCarSpawned": [],
"onCarDeleted": [],
"serverTick": [],
"serverTick_0.5s": [],
"serverTick_1s": [],
"serverTick_2s": [],
"serverTick_3s": [],
"serverTick_4s": [],
"serverTick_5s": [],
"serverTick_10s": [],
"serverTick_30s": [],
"serverTick_60s": [],
} }
self.__lua_events = { self.__lua_events = {
@@ -74,13 +100,45 @@ class EventsSystem:
self.log.debug("used builtins_hook") self.log.debug("used builtins_hook")
builtins.ev = self builtins.ev = self
def unregister_by_id(self, _id):
self.log.debug(f"unregister_by_id '{_id}'")
if not isinstance(_id, int):
return
s = a = 0
for k, funcs in self.__events.items():
for f in funcs:
if id(f) == _id:
s += 1
self.__events[k].remove(f)
for k, funcs in self.__async_events.items():
for f in funcs:
if id(f) == _id:
a += 1
self.__async_events[k].remove(f)
self.log.debug(f"unregister in {s + a} events; S:{s}; A:{a};")
def unregister(self, func):
self.log.debug(f"unregister '{func.__name__}' id: {id(func)}")
s = a = 0
for k, funcs in self.__events.items():
for f in funcs:
if f == func:
s += 1
self.__events[k].remove(func)
for k, funcs in self.__async_events.items():
for f in funcs:
if f == func:
a += 1
self.__async_events[k].remove(func)
self.log.debug(f"unregister in {s + a} events; S:{s}; A:{a};")
def is_event(self, event_name): def is_event(self, event_name):
return (event_name in self.__async_events.keys() or return (event_name in self.__async_events.keys() or
event_name in self.__events.keys() or event_name in self.__events.keys() or
event_name in self.__lua_events.keys()) event_name in self.__lua_events.keys())
def register(self, event_name, event_func, async_event=False, lua=None): def register(self, event_name, event_func, async_event=False, lua=None, return_id=True):
self.log.debug(f"register(event_name='{event_name}', event_func='{event_func}', " self.log.debug(f"register(event_name='{event_name}', event_func='{event_func.__name__}'(id: {id(event_func)}), "
f"async_event={async_event}, lua_event={lua}):") f"async_event={async_event}, lua_event={lua}):")
if lua: if lua:
if event_name not in self.__lua_events: if event_name not in self.__lua_events:
@@ -95,19 +153,23 @@ class EventsSystem:
return return
if async_event or inspect.iscoroutinefunction(event_func): if async_event or inspect.iscoroutinefunction(event_func):
if event_name not in self.__async_events: if event_name not in self.__async_events:
self.__async_events.update({str(event_name): [event_func]}) self.__async_events[event_name] = []
else: self.__async_events[event_name].append(event_func)
self.__async_events[event_name].append(event_func)
self.log.debug("Register ok") self.log.debug("Register ok")
else: else:
if event_name not in self.__events: if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]}) self.__events[event_name] = []
else: self.__events[event_name].append(event_func)
self.__events[event_name].append(event_func)
self.log.debug("Register ok") self.log.debug("Register ok")
if return_id:
return id(event_func)
async def call_as_events(self, *args, **kwargs):
return await self.call_async_event(*args, **kwargs) + self.call_event(*args, **kwargs)
async def call_async_event(self, event_name, *args, **kwargs): async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling async event: '{event_name}'") if not event_name.startswith("serverTick"):
self.log.debug(f"Calling async event: '{event_name}'")
funcs_data = [] funcs_data = []
if event_name in self.__async_events.keys(): if event_name in self.__async_events.keys():
for func in self.__async_events[event_name]: for func in self.__async_events[event_name]:
@@ -123,8 +185,11 @@ class EventsSystem:
return funcs_data return funcs_data
def call_event(self, event_name, *args, **kwargs): def call_event(self, event_name: str, *args, **kwargs):
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events if event_name not in (
"onChangePosition", "onSentPing", # UDP events
"_get_player"
) and not event_name.startswith("serverTick"):
self.log.debug(f"Calling sync event: '{event_name}'") self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = [] funcs_data = []

View File

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

View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.PermsSystem
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2024
from core import get_logger
import sqlite3
class PermsSystem:
_db_name = "users.db3"
def __init__(self):
self.log = get_logger("PermsSystem")
self._create_base()
self._completer_permissions = Completer({})
# set <permission | group> | unset <permission | group>
self._completer_group = Completer({}) # <group_name> info | permission
_completer_after_user = Completer({
"info": None,
"permission": {"set": self._completer_permissions, "unset": self._completer_permissions}
})
self._completer_user = Completer({}, on_none=_completer_after_user) # <nick> info | permission
ev.register("add_perm_to_alias", lambda ev: self._completer_permissions.options.update({ev['args'][0]: None}))
ev.call_event("add_perm_to_alias", "cmd.perms")
console.add_command("perms", self._parse_console,
None,
"Permission module",
{"perms": {
"groups": {
"create": None,
"delete": None,
"list": None
},
"user": self._completer_user,
"group": self._completer_group,
"reload": None,
}})
ev.register("onChatReceive", self._parse_chat)
ev.register("onPlayerJoin", self._process_new_player)
def _create_base(self):
con = sqlite3.connect(self._db_name)
cursor = con.cursor()
# Create table for users
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mp_id INTEGER UNIQUE,
nick TEXT NOT NULL,
playtime INTEGER
)
''')
# Create table for perms
cursor.execute('''
CREATE TABLE IF NOT EXISTS perms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mp_id INTEGER,
rule TEXT,
`group` TEXT,
FOREIGN KEY(mp_id) REFERENCES users(mp_id)
)
''')
# Create table for groups
cursor.execute('''
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
rules TEXT NOT NULL
)
''')
con.commit()
con.close()
def _parse_console(self, x):
pass
def _parse_chat(self, ev):
pass
def add_player(self, player):
self._completer_user.options.update({player.nick: None})
self.log.debug(f'Added user: {player.nick}')
def have_permission(self, ev):
player = ev['kwargs']['player']
def _process_new_player(self, ev):
player = ev['kwargs']['player']
self.add_player(player)

View File

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

@@ -5,6 +5,7 @@ import platform
import random import random
import re import re
import shutil import shutil
import sys
import threading import threading
import time import time
@@ -608,14 +609,6 @@ class LuaPluginsLoader:
"ImScaredOfUpdates": False, "ImScaredOfUpdates": False,
"SendErrorsShowMessage": False, "SendErrorsShowMessage": False,
"SendErrors": False "SendErrors": False
},
"HTTP": {
"HTTPServerIP": config.WebAPI['server_ip'],
"HTTPServerPort": config.WebAPI['server_port'],
"SSLKeyPath": None,
"SSLCertPath": None,
"UseSSL": False,
"HTTPServerEnabled": config.WebAPI['enabled'],
} }
} }
with open("ServerConfig.toml", "w") as f: with open("ServerConfig.toml", "w") as f:
@@ -648,7 +641,11 @@ class LuaPluginsLoader:
p0 = os.path.join(pa, name, "?.lua") p0 = os.path.join(pa, name, "?.lua")
p1 = os.path.join(pa, name, "lua", "?.lua") p1 = os.path.join(pa, name, "lua", "?.lua")
lua_globals.package.path += f';{p0};{p1}' lua_globals.package.path += f';{p0};{p1}'
with open("modules/PluginsLoader/add_in.lua", "r") as f: try:
_file = os.path.join(sys._MEIPASS, "add_in.lua")
except AttributeError:
_file = "modules/PluginsLoader/add_in.lua"
with open(_file, "r") as f:
lua.execute(f.read()) lua.execute(f.read())
self.lua_plugins.update({name: {"lua": lua, "ok": False}}) self.lua_plugins.update({name: {"lua": lua, "ok": False}})
plugin_path = os.path.join(self.plugins_dir, name) plugin_path = os.path.join(self.plugins_dir, name)
@@ -671,10 +668,15 @@ class LuaPluginsLoader:
self.log.error(f"Exception onInit from `{name}`: {e}") self.log.error(f"Exception onInit from `{name}`: {e}")
self.log.exception(e) self.log.exception(e)
def unload(self, _): async def unload(self, _):
self.log.debug("Unloading lua plugins") self.log.debug("Unloading lua plugins")
for name, data in self.lua_plugins.items(): for name, data in self.lua_plugins.items():
if data['ok']: if data['ok']:
self.log.info(i18n.plugins_lua_unload.format(name)) self.log.info(i18n.plugins_lua_unload.format(name))
for _, timer in data['lua'].globals().MP._event_timers.items(): MP = data['lua'].globals().MP
self.log.debug("gather")
await asyncio.gather(*MP.tasks)
self.log.debug("timers")
for _, timer in MP._event_timers.items():
timer.stop() timer.stop()
self.log.debug("unloaded")

View File

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

View File

@@ -1,105 +0,0 @@
import asyncio
from asyncio import CancelledError
import uvicorn
from fastapi import FastAPI, Request, HTTPException
from fastapi.exceptions import RequestValidationError
from starlette import status
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse
import core.utils
from . import utils
# from .models import SecretKey
web_app = FastAPI()
log = core.utils.get_logger("web")
uvserver = None
data_pool = []
data_run = [True]
def response(data=None, code=status.HTTP_200_OK, error_code=0, error_message=None):
if 200 >= code <= 300:
return JSONResponse(content={"result": data, "error": None}, status_code=code)
return JSONResponse(
content={"error": {"code": error_code if error_code else code, "message": f"{error_message}"}, "result": None},
status_code=code)
@web_app.get("/")
async def index():
log.debug("Request IndexPage;")
return response("Index page")
@web_app.get("/method/{method}")
async def _method(method, secret_key: str = None):
# log.debug(f"Request method; kwargs: {kwargs}")
is_auth = secret_key == config.WebAPI["secret_key"]
spl = method.split(".")
if len(spl) != 2:
raise StarletteHTTPException(405)
api_class, api_method = spl
match api_class:
case "events":
match api_method, is_auth:
case "get", False:
return response(data_pool)
raise StarletteHTTPException(404)
async def _stop():
await asyncio.sleep(1)
if uvserver is not None:
uvserver.should_exit = True
data_run[0] = False
@web_app.get("/stop")
async def stop(secret_key: str):
log.debug(f"Request stop; secret key: {secret_key}")
if secret_key == config.WebAPI["secret_key"]:
log.info("Stopping Web server")
asyncio.create_task(_stop())
return response("Web server stopped")
@web_app.exception_handler(HTTPException)
async def default_exception_handler(request: Request, exc: HTTPException):
return response(
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
error_code=exc.status_code, error_message=f"Internal Server Error: {exc.status_code}"
)
@web_app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
code = exc.status_code
if code == status.HTTP_405_METHOD_NOT_ALLOWED:
return response(code=code, error_message="Method Not Allowed")
if code == status.HTTP_404_NOT_FOUND:
return response(code=code, error_message="Method not Found")
return response(code=code, error_message="Unhandled error..")
@web_app.exception_handler(RequestValidationError)
async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
code = status.HTTP_422_UNPROCESSABLE_ENTITY
return response(code=code, error_message="Request Validation Error")
utils.hack_fastapi()
if __name__ == '__main__':
try:
uvconfig = uvicorn.Config(web_app,
host=config.WebAPI["server_ip"],
port=config.WebAPI["server_port"],
loop="asyncio")
uvserver = uvicorn.Server(uvconfig)
uvserver.run()
except KeyboardInterrupt or CancelledError:
pass

View File

@@ -1,126 +0,0 @@
import asyncio
import sys
import click
import uvicorn.server as uvs
from uvicorn.config import LOGGING_CONFIG
from uvicorn.lifespan import on
import core.utils
# logger = core.utils.get_logger("uvicorn")
# uvs.logger = logger
logger = uvs.logger
def ev_log_started_message(self, listeners) -> None:
cfg = self.config
if cfg.fd is not None:
sock = listeners[0]
logger.info(i18n.web_start.format(sock.getsockname()))
elif cfg.uds is not None:
logger.info(i18n.web_start.format(cfg.uds))
else:
addr_format = "%s://%s:%d"
host = "0.0.0.0" if cfg.host is None else cfg.host
if ":" in host:
addr_format = "%s://[%s]:%d"
port = cfg.port
if port == 0:
port = listeners[0].getsockname()[1]
protocol_name = "https" if cfg.ssl else "http"
message = i18n.web_start.format(addr_format)
color_message = (i18n.web_start.format(click.style(addr_format, bold=True)))
logger.info(message, protocol_name, host, port, extra={"color_message": color_message})
async def ev_shutdown(self, sockets=None) -> None:
logger.debug("Shutting down")
for server in self.servers:
server.close()
for sock in sockets or []:
sock.close()
for server in self.servers:
await server.wait_closed()
for connection in list(self.server_state.connections):
connection.shutdown()
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(self._wait_tasks_to_complete(), timeout=self.config.timeout_graceful_shutdown)
except asyncio.TimeoutError:
logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded", len(self.server_state.tasks))
for t in self.server_state.tasks:
if sys.version_info < (3, 9):
t.cancel()
else:
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
if not self.force_exit:
await self.lifespan.shutdown()
async def on_startup(self) -> None:
self.logger.debug("Waiting for application startup.")
loop = asyncio.get_event_loop()
main_lifespan_task = loop.create_task(self.main()) # noqa: F841
startup_event = {"type": "lifespan.startup"}
await self.receive_queue.put(startup_event)
await self.startup_event.wait()
if self.startup_failed or (self.error_occured and self.config.lifespan == "on"):
self.logger.error("Application startup failed. Exiting.")
self.should_exit = True
else:
self.logger.debug("Application startup complete.")
async def on_shutdown(self) -> None:
if self.error_occured:
return
self.logger.debug("Waiting for application shutdown.")
shutdown_event = {"type": "lifespan.shutdown"}
await self.receive_queue.put(shutdown_event)
await self.shutdown_event.wait()
if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"):
self.logger.error("Application shutdown failed. Exiting.")
self.should_exit = True
else:
self.logger.debug("Application shutdown complete.")
def hack_fastapi():
uvs.Server.shutdown = ev_shutdown
uvs.Server._log_started_message = ev_log_started_message
on.LifespanOn.startup = on_startup
on.LifespanOn.shutdown = on_shutdown
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format
LOGGING_CONFIG["formatters"].update({
"file_default": {
"()": "logging.Formatter",
"fmt": core.utils.log_format
},
"file_access": {
"()": "logging.Formatter",
"fmt": core.utils.log_format
}
})
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
LOGGING_CONFIG["handlers"].update({
"file_default": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "./logs/web.log",
"encoding": "utf-8",
"formatter": "file_default"
},
"file_access": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "./logs/web_access.log",
"encoding": "utf-8",
"formatter": "file_access"
}
})
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")

View File

@@ -1,16 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File modules.__init__.py # File modules.__init__.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.1 # Version 1.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
from .ConsoleSystem import Console
from .ConfigProvider import ConfigProvider, Config from .ConfigProvider import ConfigProvider, Config
from .i18n import MultiLanguage
from .EventsSystem import EventsSystem from .EventsSystem import EventsSystem
from .ConsoleSystem import Console
from .PluginsLoader import PluginsLoader from .PluginsLoader import PluginsLoader
from .WebAPISystem import web_app from .i18n import MultiLanguage
from .WebAPISystem import _stop as stop_web
from .RateLimiter import RateLimiter from .RateLimiter import RateLimiter
from .PermsSystem import PermsSystem

View File

@@ -50,7 +50,6 @@ class MultiLanguage:
"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_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_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}", "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_bind_failed": "Failed to bind port. Error: {}",
"core_direct_mode": "Server started in direct connection mode.", "core_direct_mode": "Server started in direct connection mode.",
"core_auth_server_error": "Received invalid response from BeamMP authentication server.", "core_auth_server_error": "Received invalid response from BeamMP authentication server.",

View File

@@ -21,9 +21,6 @@
"GUI_enter_key_message": "请输入密钥:", "GUI_enter_key_message": "请输入密钥:",
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接{}", "GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接{}",
"": "Web阶段",
"web_start": "WebAPI已启动于{}按CTRL+C停止",
"": "核心短语", "": "核心短语",
"core_bind_failed": "无法绑定端口。错误:{}", "core_bind_failed": "无法绑定端口。错误:{}",
"core_direct_mode": "服务器以直接连接模式启动。", "core_direct_mode": "服务器以直接连接模式启动。",

View File

@@ -21,9 +21,6 @@
"GUI_enter_key_message": "Please enter the key:", "GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}", "GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"": "Web phases",
"web_start": "WebAPI started on {} (CTRL+C to stop)",
"": "Core phrases", "": "Core phrases",
"core_bind_failed": "Failed to bind port. Error: {}", "core_bind_failed": "Failed to bind port. Error: {}",
"core_direct_mode": "Server started in direct connection mode.", "core_direct_mode": "Server started in direct connection mode.",

View File

@@ -21,9 +21,6 @@
"GUI_enter_key_message": "Пожалуйста введите ключ:", "GUI_enter_key_message": "Пожалуйста введите ключ:",
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}", "GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",
"": "Web phases",
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
"": "Core phrases", "": "Core phrases",
"core_bind_failed": "Не получилось занять порт. Ошибка: {}", "core_bind_failed": "Не получилось занять порт. Ошибка: {}",
"core_direct_mode": "Сервер запушен в режиме прямого подключения.", "core_direct_mode": "Сервер запушен в режиме прямого подключения.",

View File

@@ -60,6 +60,10 @@
{ {
"optionDest": "bootloader_ignore_signals", "optionDest": "bootloader_ignore_signals",
"value": false "value": false
},
{
"optionDest": "datas",
"value": "C:/Users/SantaSpeen/PycharmProjects/KuiToi-Server/src/modules/PluginsLoader/add_in.lua;."
} }
], ],
"nonPyinstallerOptions": { "nonPyinstallerOptions": {