Compare commits

..

7 Commits

Author SHA1 Message Date
a5a7a5dfc9 Add events onCarSpawn, onCarEdited 2023-07-19 22:59:30 +03:00
f6ff018b03 connect_time 2023-07-19 21:53:08 +03:00
1829113ae5 Add upload use_queue 2023-07-19 21:22:29 +03:00
e72c371e20 Minor updates 2023-07-19 21:10:26 +03:00
57b7cebeca Minor updates 2023-07-19 21:10:04 +03:00
2a2d55946e Update config_provider 2023-07-19 21:04:05 +03:00
ea2d715cae DO Speed limiter;
Minor fixes;
2023-07-19 20:23:16 +03:00
13 changed files with 154 additions and 76 deletions

View File

@ -7,6 +7,7 @@
import asyncio
import json
import math
import time
import zlib
from core import utils
@ -33,6 +34,7 @@ class Client:
self._guest = True
self._ready = False
self._cars = []
self._connect_time: float = 0
@property
def _writer(self):
@ -89,9 +91,15 @@ class Client:
return
# TODO: i18n
self.log.info(f"Kicked with reason: \"{reason}\"")
await self._send(b"K" + bytes(reason, "utf-8"))
await self._send(f"K{reason}")
self.__alive = False
async def send_message(self, message, to_all=True):
pass
async def send_event(self, event_name, event_data):
pass
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
# TNetwork.cpp; Line: 383
@ -101,7 +109,7 @@ class Client:
# size data
if type(data) == str:
data = bytes(data, "utf-8")
data = bytes(data, config.enc)
if to_all:
code = chr(data[0])
@ -198,7 +206,7 @@ class Client:
self.log.warning("Client sent header of >100MB - "
"assuming malicious intent and disconnecting the client.")
self.__packets_queue.append(None)
self.log.debug(f"Last recv: {await self.__reader.read(100 * MB)}")
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
continue
data = await self.__reader.read(int_header)
@ -222,31 +230,42 @@ class Client:
self.__alive = False
self.__packets_queue.append(None)
async def _split_load(self, start, end, d_sock, filename):
# TODO: Speed limiter
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
real_size = end - start
writer = self._down_sock[1] if d_sock else self.__writer
who = 'dwn' if d_sock else 'srv'
if config.Server["debug"]:
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
with open(filename, 'rb') as f:
f.seek(start)
data = f.read(end)
try:
writer.write(data)
await writer.drain()
self.log.debug(f"[{who}] File sent.")
except ConnectionError:
self.__alive = False
self.log.debug(f"[{who}] Disconnected.")
return real_size
total_sent = 0
start_time = time.monotonic()
while total_sent < real_size:
data = f.read(min(MB, real_size - total_sent)) # read data in chunks of 1MB or less
try:
writer.write(data)
await writer.drain()
self.log.debug(f"[{who}] Sent {len(data)} bytes.")
except ConnectionError:
self.__alive = False
self.log.debug(f"[{who}] Disconnected.")
break
total_sent += len(data)
# Calculate delay based on speed limit
if speed_limit:
elapsed_time = time.monotonic() - start_time
expected_time = total_sent / (speed_limit * MB)
if expected_time > elapsed_time:
await asyncio.sleep(expected_time - elapsed_time)
return total_sent
async def _sync_resources(self):
while self.__alive:
data = await self._recv(True)
if data.startswith(b"f"):
file = data[1:].decode("utf-8")
file = data[1:].decode(config.enc)
# TODO: i18n
self.log.info(f"Requested mode: {file!r}")
size = -1
@ -270,13 +289,28 @@ class Client:
if t > 50:
await self.kick("Missing download socket")
return
if config.Options['use_queue']:
while self.__Core.lock_upload:
await asyncio.sleep(.2)
self.__Core.lock_upload = True
speed = config.Options["speed_limit"]
if speed:
speed = speed / 2
half_size = math.floor(size / 2)
t = time.monotonic()
uploads = [
self._split_load(0, half_size, False, file),
self._split_load(half_size, size, True, file)
self._split_load(0, half_size, False, file, speed),
self._split_load(half_size, size, True, file, speed)
]
sl0, sl1 = await asyncio.gather(*uploads)
tr = time.monotonic() - t
if self.__Core.lock_upload:
self.__Core.lock_upload = False
# TODO: i18n
msg = f"Mod sent: Size {round(size / MB, 3)}mb Speed {math.ceil(size / tr / MB)}Mb/s ({int(tr)}s)"
if speed:
msg += f" of limit {int(speed * 2)}Mb/s"
self.log.info(msg)
sent = sl0 + sl1
ok = sent == size
lost = size - sent
@ -284,7 +318,7 @@ class Client:
if not ok:
self.__alive = False
# TODO: i18n
self.log.error(f"Error while sending.")
self.log.error(f"Error while sending: {file!r}")
return
elif data.startswith(b"SR"):
path_list = ''
@ -299,11 +333,11 @@ class Client:
if len(mod_list) == 0:
await self._send(b"-")
else:
await self._send(bytes(mod_list, "utf-8"))
await self._send(mod_list)
elif data == b"Done":
for c in range(int(config.Game['max_cars'] * 2.3)):
self._cars.append(None)
await self._send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
await self._send(f"M/levels/{config.Game['map']}/info.json")
break
return
@ -312,7 +346,8 @@ class Client:
s = data[sep:sep + 3]
id_sep = s.find('-')
if id_sep == -1:
self.log.debug(f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}'")
self.log.debug(
f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}'")
return -1, -1
cid = s[:id_sep]
vid = s[id_sep + 1:]
@ -349,9 +384,14 @@ class Client:
car_json = json.loads(data[data.find("{"):])
except Exception as e:
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
allow = True
# allow = True
over_spawn = False
# TODO: Call event onCarSpawn
ev_data = ev.call_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
ev_data.extend(d2)
for d in ev_data:
# TODO: handle event onCarSpawn
pass
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
unicycle = car_json.get("jbm") == "unicycle"
# FIXME: unicycle
@ -388,29 +428,39 @@ class Client:
self.log.debug(f"Unknown car: car_id={car_id}")
case "c": # Edit car
self.log.debug("Trying to edit car")
allow = True
# TODO: Call event onCarEdited
cid, car_id = self._get_cid_vid(dta)
if car_id != -1 and cid == self.cid:
try:
car = self.cars[car_id]
if car['unicycle']:
self._cars.pop(car_id)
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
elif allow:
await self._send(dta, to_all=True, to_self=False)
if car['json_ok']:
old_car_json = car['json']
try:
new_car_json = json.loads(data[data.find("{"):])
old_car_json.update(new_car_json)
car['json'] = new_car_json
self.log.debug(f"Updated car: car_id={car_id}")
except Exception as e:
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {data}")
try:
client = self.__Core.get_client(cid=cid)
if client:
car = client.cars[car_id]
new_car_json = {}
try:
new_car_json = json.loads(data[data.find("{"):])
except Exception as e:
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {data}")
allow = False
ev_data = ev.call_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
ev_data.extend(d2)
for d in ev_data:
# TODO: handle event onCarEdited
pass
if car_id != -1 and cid == self.cid or allow:
if car['unicycle']:
self._cars.pop(car_id)
await self._send(f"Od:{cid}-{car_id}", to_all=True, to_self=True)
else:
await self._send(dta, to_all=True, to_self=False)
if car['json_ok']:
old_car_json = car['json']
old_car_json.update(new_car_json)
car['json'] = old_car_json
self.log.debug(f"Updated car: car_id={car_id}")
except IndexError:
self.log.debug(f"Unknown car: car_id={car_id}")
except IndexError:
self.log.debug(f"Unknown car: car_id={car_id}")
case "r": # Reset car
self.log.debug("Trying to reset car")
cid, car_id = self._get_cid_vid(dta)
@ -442,6 +492,7 @@ class Client:
# Codes: p, Z in udp_server.py
match code:
case "H":
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s")
# Client connected
ev.call_event("onPlayerJoin", player=self)
await ev.call_async_event("onPlayerJoin", player=self)
@ -462,7 +513,7 @@ class Client:
sup = data.find(":", 2)
if sup == -1:
await self._send("C:Server: Invalid message.")
msg = data[sup+2:]
msg = data[sup + 2:]
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
return
@ -484,6 +535,7 @@ class Client:
to_client = ev_data.get("to_client")
writer = None
if to_client:
# noinspection PyProtectedMember
writer = to_client._writer
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False
@ -503,6 +555,7 @@ class Client:
await self._send(data, to_all=True, to_self=False)
async def _looper(self):
self._connect_time = time.monotonic()
await self._send(f"P{self.cid}") # Send clientID
await self._sync_resources()
tasks = self.__tasks
@ -533,7 +586,7 @@ class Client:
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected.
self.log.debug(f"Removing client")
# TODO: i18n
self.log.info("Disconnected")
self.log.info(f"Disconnected, online time: {round((time.monotonic() - self._connect_time) / 60, 2)}min.")
self.__Core.clients[self.cid] = None
self.__Core.clients_by_id.pop(self.cid)
self.__Core.clients_by_nick.pop(self.nick)

View File

@ -15,6 +15,7 @@ from core import Core, utils
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self._connect_time: float = None
self.__tasks = []
self.__reader = reader
self.__writer = writer
@ -51,11 +52,13 @@ class Client:
def cars(self) -> List[dict | None]: ...
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
async def send_event(self, event_name: str, event_data: str) -> 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 __handle_packet(self, data, int_header): ...
# async def __handle_packet(self, data, int_header): ...
async def _recv(self, one=False) -> bytes | None: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str) -> None: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ...
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
async def _handle_car_codes(self, data) -> None: ...
async def _handle_codes(self, data) -> None: ...

View File

@ -46,17 +46,17 @@ if args.config:
config_provider = ConfigProvider(config_path)
config = config_provider.open_config()
builtins.config = config
if config.Server['debug'] is True:
if config.Options['debug'] is True:
utils.set_debug_status()
log.info("Debug enabled!")
log = get_logger("core.init")
log.debug("Debug mode enabled!")
log.debug(f"Server config: {config}")
config.enc = config.Options['encoding']
# i18n init
log.debug("Initializing i18n...")
ml = MultiLanguage()
ml.set_language(args.language or config.Server['language'])
ml.set_language(args.language or config.Options['language'])
ml.builtins_hook()
log.debug("Initializing EventsSystem...")

View File

@ -20,6 +20,7 @@ from modules import PluginsLoader
from modules.WebAPISystem import app as webapp
# noinspection PyProtectedMember
class Core:
def __init__(self):
@ -40,6 +41,8 @@ class Core:
self.web_pool = webapp.data_pool
self.web_stop = None
self.lock_upload = False
self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0"
@ -105,7 +108,7 @@ class Core:
if not client.ready:
client.is_disconnected()
continue
await client._send(bytes(ca, "utf-8"))
await client._send(ca)
except Exception as e:
self.log.error("Error in check_alive.")
self.log.exception(e)
@ -145,10 +148,10 @@ class Core:
modstotal = len(self.mods_list) - 1
while self.run:
try:
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id), "maxplayers": config.Game["players"],
"port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json",
"private": config.Auth['private'], "version": self.BeamMP_version,
"clientversion": self.client_major_version,
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id),
"maxplayers": config.Game["players"], "port": config.Server["server_port"],
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'],
"version": self.BeamMP_version, "clientversion": self.client_major_version,
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
self.log.debug(f"Auth: data {data}")

View File

@ -32,6 +32,7 @@ class Core:
self.udp = UDPServer
self.web_thread: Thread = None
self.web_stop: Callable = lambda: None
self.lock_upload = False
self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0"
def get_client(self, cid=None, nick=None) -> Client | None: ...

View File

@ -12,6 +12,7 @@ import aiohttp
from core import utils
# noinspection PyProtectedMember
class TCPServer:
def __init__(self, core, host, port):
self.log = utils.get_logger("TCPServer")

View File

@ -5,7 +5,6 @@
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import traceback
from core import utils
@ -95,7 +94,6 @@ class UDPServer(asyncio.DatagramTransport):
self.log.error(f"Error: {e}")
self.log.exception(e)
finally:
self.log.info("UDP сервер сдох 2")
self.run = False
self.Core.run = False

View File

@ -20,8 +20,8 @@ logging.basicConfig(level=log_level, format=log_format)
if not os.path.exists(log_dir):
os.mkdir(log_dir)
if os.path.exists(log_file):
mtime = os.path.getmtime(log_file)
gz_path = log_dir + datetime.datetime.fromtimestamp(mtime).strftime('%d.%m.%Y') + "-%s.tar.gz"
ftime = os.path.getmtime(log_file)
gz_path = log_dir + datetime.datetime.fromtimestamp(ftime).strftime('%d.%m.%Y') + "-%s.tar.gz"
index = 1
while True:
if not os.path.exists(gz_path % index):
@ -33,11 +33,15 @@ if os.path.exists(log_file):
if os.path.exists(file):
tar.add(file, os.path.basename(file))
os.remove(file)
fh = logging.FileHandler(log_file, encoding='utf-8')
fh = logging.FileHandler(log_file, encoding="utf-8")
fh.setFormatter(logging.Formatter(log_format))
def get_logger(name):
try:
fh.encoding = config.enc
except NameError:
fh.encoding = "utf-8"
log = logging.getLogger(name=name)
log.addHandler(fh)
log.level = log_level

View File

@ -1,8 +1,17 @@
import secrets
class Config:
Auth: dict
Game: dict
Server: dict
WebAPI: dict
def __init__(self, auth=None, game=None, server=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
"server_ip": "0.0.0.0", "server_port": 30814}
self.Options = options or {"language": "en", "encoding": "utf8", "speed_limit": 0, "use_queue": False,
"debug": False}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"secret_key": secrets.token_hex(16)}
def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class config (Config): ...

View File

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

View File

@ -27,12 +27,16 @@ class EventsSystem:
"onPlayerAuthenticated": [], # Only sync
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarEdited": [],
"onServerStopped": [],
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarEdited": [],
"onServerStopped": []
}

View File

@ -99,7 +99,7 @@ class PluginsLoader:
plugin.print = print
file_path = os.path.join(self.plugins_dir, file)
plugin.__file__ = file_path
with open(f'{file_path}', 'r', encoding="utf-8") as f:
with open(f'{file_path}', 'r', encoding=config.enc) as f:
code = f.read()
exec(code, plugin.__dict__)

View File

@ -68,7 +68,9 @@ class i18n:
class MultiLanguage:
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding="utf-8"):
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding=None):
if encoding is None:
encoding = config.enc
if language is None:
language = "en"
self.__data = {}