18 Commits

Author SHA1 Message Date
8139cbf8bc 0.4.0 -> 0.4.1 2023-07-20 04:10:51 +03:00
eec7c8129d Add snowman support (Only one per player);
Optimize getting car_id;
Minor fixes;
2023-07-20 04:07:39 +03:00
158599dfc5 Debug t and m codes 2023-07-20 02:22:20 +03:00
06bd50f0fa Add UDP events: onChangePosition, onSentPing 2023-07-20 01:59:30 +03:00
e086fea2e9 Update __build__; 2023-07-20 01:56:36 +03:00
b6038ee6d0 Move code from case to func;
Add events for cars: onCarDelete, onCarReset;
Minor updates;
2023-07-20 01:54:52 +03:00
147e76e089 Remove some debug info;
Optimize code.
2023-07-20 01:38:56 +03:00
56b9049dcb BeamMP_version 2023-07-20 01:37:52 +03:00
78d323644d Remove some debug data! 2023-07-20 01:34:39 +03:00
310c47162c FIX config.enc 2023-07-20 01:27:01 +03:00
27d49cf5cc Update TODOs 2023-07-20 01:15:32 +03:00
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
16 changed files with 370 additions and 258 deletions

View File

@@ -19,6 +19,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Chat
- [x] Players online counter
- [x] Packets handled (Recursive finding second packet)
- [ ] Client events
- [x] Car synchronizations:
- [x] State packets
- [x] Spawn cars
@@ -49,6 +50,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Create custom events
- [x] Return from events
- [x] Async support
- [ ] Add all events
- [x] Plugins support
- [ ] KuiToi class
- [ ] Client (Player) class
@@ -56,7 +58,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Async support
- [ ] Load Lua plugins (Original BeamMP compatibility)
- [x] MultiLanguage (i18n support)
- [x] Core
- [ ] Core
- [x] Console
- [x] WebAPI
- [ ] HTTP API Server (fastapi)

View File

@@ -1,12 +1,13 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import json
import math
import time
import zlib
from core import utils
@@ -32,7 +33,9 @@ class Client:
self.roles = None
self._guest = True
self._ready = False
self._cars = []
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
self._snowman = {"id": -1, "packet": ""}
self._connect_time = 0
@property
def _writer(self):
@@ -73,13 +76,11 @@ class Client:
def is_disconnected(self):
if not self.__alive:
return True
res = self.__writer.is_closing()
if res:
self.log.debug(f"Disconnected.")
if self.__writer.is_closing():
self.log.debug(f"is_d: Disconnected.")
self.__alive = False
return True
else:
self.log.debug(f"Alive.")
self.__alive = True
return False
@@ -89,9 +90,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 +108,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])
@@ -140,7 +147,7 @@ class Client:
return
header = len(data).to_bytes(4, "little", signed=True)
self.log.debug(f'[TCP] {header + data!r}')
# self.log.debug(f'[TCP] {header + data!r}')
try:
writer.write(header + data)
await writer.drain()
@@ -152,27 +159,6 @@ class Client:
await self._remove_me()
return False
# async def __handle_packet(self, data, int_header):
# self.log.debug(f"int_header: {int_header}; data: {data};")
# if len(data) != int_header:
# self.log.debug(f"WARN Expected to read {int_header} bytes, instead got {len(data)}")
#
# recv2 = data[int_header:]
# header2 = recv2[:4]
# data2 = recv2[4:]
# int_header2 = int.from_bytes(header2, byteorder='little', signed=True)
# t = asyncio.create_task(self.__handle_packet(data2, int_header2))
# self.__tasks.append(t)
# data = data[:4 + int_header]
#
# abg = b"ABG:"
# if len(data) > len(abg) and data.startswith(abg):
# data = zlib.decompress(data[len(abg):])
# self.log.debug(f"ABG Packet: {len(data)}")
#
# self.__packets_queue.append(data)
# self.log.debug(f"Packets in queue: {len(self.__packets_queue)}")
async def _recv(self, one=False):
while self.__alive:
try:
@@ -188,7 +174,7 @@ class Client:
self.__packets_queue.append(None)
self.__alive = False
continue
self.log.debug(f"Header: {header}")
self.log.error(f"Header: {header}")
await self.kick("Invalid packet - header negative")
self.__packets_queue.append(None)
continue
@@ -197,56 +183,62 @@ class Client:
await self.kick("Header size limit exceeded")
self.log.warning("Client sent header of >100MB - "
"assuming malicious intent and disconnecting the client.")
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
self.__packets_queue.append(None)
self.log.debug(f"Last recv: {await self.__reader.read(100 * MB)}")
continue
data = await self.__reader.read(int_header)
self.log.debug(f"int_header: {int_header}; data: `{data}`;")
# self.log.debug(f"int_header: {int_header}; data: `{data}`;")
abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):])
self.log.debug(f"ABG Packet: {len(data)}")
# self.log.debug(f"ABG Packet: {len(data)}")
if one:
# self.log.debug(f"int_header: `{int_header}`; data: `{data}`;")
return data
# FIXME
# else:
# t = asyncio.create_task(self.__handle_packet(data, int_header))
# self.__tasks.append(t)
self.__packets_queue.append(data)
except ConnectionError:
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 +262,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 +291,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 +306,9 @@ 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 +317,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:]
@@ -328,101 +334,213 @@ class Client:
self.log.debug(f"Invalid packet: Could not parse pid/vid from packet: '{data}'")
return -1, -1
async def _handle_car_codes(self, dta):
if len(dta) < 6:
async def _spawn_car(self, data):
car_data = data[2:]
car_id = next((i for i, car in enumerate(self.cars) if car is None), len(self.cars))
cars_count = len(self.cars) - self.cars.count(None)
if self._snowman['id'] != -1:
cars_count -= 1 # -1 for unicycle
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
car_json = {}
try:
car_json = json.loads(car_data[car_data.find("{"):])
except Exception as e:
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
allow = True
allow_snowman = True
over_spawn = False
ev_data_list = ev.call_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
# TODO: handle event onCarSpawn
pass
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
snowman = car_json.get("jbm") == "unicycle"
if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn:
if snowman:
unicycle_id = self._snowman['id']
if unicycle_id != -1:
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}")
self._cars[unicycle_id] = None
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._snowman = {"id": car_id, "packet": pkt}
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
else:
self.log.debug(f"Car spawn accepted: car_id={car_id}")
self._cars[car_id] = {
"packet": pkt,
"json": car_json,
"json_ok": bool(car_json),
"snowman": snowman,
"over_spawn": (snowman and allow_snowman) or over_spawn
}
await self._send(pkt, to_all=True, to_self=True)
else:
await self._send(pkt)
des = f"Od:{self.cid}-{car_id}"
await self._send(des)
async def _delete_car(self, raw_data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and self.cars[car_id]:
admin_allow = False # Delete from admin, for example...
ev_data_list = ev.call_event("onCarDelete", car=self.cars[car_id], car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarDelete", car=self.cars[car_id], car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
# TODO: handle event onCarDelete
pass
if cid == self.cid or admin_allow:
await self._send(raw_data, to_all=True, to_self=True)
car = self.cars[car_id]
if car['snowman']:
self.log.debug(f"Snowman found")
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
self._cars[unicycle_id] = None
self._cars[car_id] = None
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
self.log.debug(f"Deleted car: car_id={car_id}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def _edit_car(self, raw_data, data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and self.cars[car_id]:
client = self.__Core.get_client(cid=cid)
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
admin_allow = False
ev_data_list = ev.call_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
# TODO: handle event onCarEdited
pass
if cid == self.cid or allow or admin_allow:
if car['snowman']:
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
self.log.debug(f"Delete snowman")
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._cars[unicycle_id] = None
else:
await self._send(raw_data, 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}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def _reset_car(self, raw_data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and cid == self.cid and self.cars[car_id]:
await self._send(raw_data, to_all=True, to_self=False)
ev.call_event("onCarReset", car=self.cars[car_id], car_id=car_id, player=self)
await ev.call_async_event("onCarReset", car=self.cars[car_id], car_id=car_id, player=self)
self.log.debug(f"Car reset: car_id={car_id}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def _handle_car_codes(self, raw_data):
if len(raw_data) < 6:
return
sub_code = dta[1]
data = dta[3:]
sub_code = raw_data[1]
data = raw_data[3:]
match sub_code:
case "s": # Spawn car
self.log.debug("Trying to spawn car")
if data[0] == "0":
car_id = 0
for c in self.cars:
if c is None:
break
car_id += 1
self.log.debug(f"Created a car: car_id={car_id}")
car_data = data[2:]
car_json = {}
try:
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
over_spawn = False
# TODO: Call event onCarSpawn
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
unicycle = car_json.get("jbm") == "unicycle"
# FIXME: unicycle
# if (allow and (config.Game['max_cars'] > car_id or unicycle)) or over_spawn:
if config.Game['max_cars'] > car_id and not unicycle:
self.log.debug(f"Car spawn accepted.")
self._cars[car_id] = {
"packet": pkt,
"json": car_json,
"json_ok": bool(car_json),
"unicycle": unicycle,
"over_spawn": over_spawn or unicycle
}
await self._send(pkt, to_all=True, to_self=True)
else:
await self._send(pkt)
des = f"Od:{self.cid}-{car_id}"
await self._send(des)
await self._spawn_car(data)
case "d": # Delete car
self.log.debug("Trying to delete car")
cid, car_id = self._get_cid_vid(dta)
if car_id != -1 and cid == self.cid:
# TODO: Call event onCarDelete
await self._send(dta, to_all=True, to_self=True)
try:
car = self.cars[car_id]
if car['unicycle']:
self._cars.pop(car_id)
else:
self._cars[car_id] = None
await self._send(f"Od:{self.cid}-{car_id}")
self.log.debug(f"Deleted car: car_id={car_id}")
except IndexError:
self.log.debug(f"Unknown car: car_id={car_id}")
await self._delete_car(raw_data)
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}")
await self._edit_car(raw_data, data)
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)
if car_id != -1 and cid == self.cid:
# TODO: Call event onCarReset
await self._send(dta, to_all=True, to_self=False)
self.log.debug(f"Car reset: car_id={car_id}")
case "t":
self.log.debug(f"Received 'Ot' packet: {dta}")
await self._send(dta, to_all=True, to_self=False)
case "m":
await self._send(dta, to_all=True, to_self=True)
await self._reset_car(raw_data)
case "t": # Broken details
self.log.debug(f"Something changed/broken: {raw_data}")
await self._send(raw_data, to_all=True, to_self=False)
case "m": # Move focus cat
self.log.debug(f"Move focus to: {raw_data}")
await self._send(raw_data, to_all=True, to_self=True)
async def _connected_handler(self):
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s")
# Client connected
ev.call_event("onPlayerJoin", player=self)
await ev.call_async_event("onPlayerJoin", player=self)
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
await self._send(f"JWelcome {self.nick}!", to_all=True) # Hello message
self._ready = True
for client in self.__Core.clients:
if not client:
continue
for car in client.cars:
if not car:
continue
await self._send(car['packet'])
async def _chat_handler(self, data):
sup = data.find(":", 2)
if sup == -1:
await self._send("C:Server: Invalid message.")
msg = data[sup + 2:]
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
return
to_ev = {"message": msg, "player": self}
ev_data_list = ev.call_event("onChatReceive", **to_ev)
d2 = await ev.call_async_event("onChatReceive", **to_ev)
ev_data_list.extend(d2)
need_send = True
for ev_data in ev_data_list:
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
if to_client:
# noinspection PyProtectedMember
writer = to_client._writer
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}")
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False
except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}")
if need_send:
self.log.info(f"{self.nick}: {msg}")
await self._send(data, to_all=True)
async def _handle_codes(self, data):
if not data:
@@ -432,77 +550,34 @@ class Client:
# Codes: V W X Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
return
try:
data = data.decode()
except UnicodeDecodeError:
self.log.debug(f"UnicodeDecodeError: {data}")
self.log.error(f"UnicodeDecodeError: {data}")
return
code = data[0]
# Codes: p, Z in udp_server.py
match code:
case "H":
# Client connected
ev.call_event("onPlayerJoin", player=self)
await ev.call_async_event("onPlayerJoin", player=self)
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
await self._send(f"JWelcome {self.nick}!", to_all=True) # Hello message
self._ready = True
for client in self.__Core.clients:
if not client:
continue
for car in client.cars:
if not car:
continue
await self._send(car['packet'])
match data[0]: # At data[0] code
case "H": # Map load, client ready
await self._connected_handler()
case "C": # Chat handler
sup = data.find(":", 2)
if sup == -1:
await self._send("C:Server: Invalid message.")
msg = data[sup+2:]
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
return
self.log.info(f"{self.nick}: {msg}")
to_ev = {"message": msg, "player": self}
ev_data_list = ev.call_event("onChatReceive", **to_ev)
d2 = await ev.call_async_event("onChatReceive", **to_ev)
ev_data_list.extend(d2)
need_send = True
for ev_data in ev_data_list:
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
if to_client:
writer = to_client._writer
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False
except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}")
if need_send:
await self._send(data, to_all=True)
await self._chat_handler(data)
case "O": # Cars handler
await self._handle_car_codes(data)
case "E": # Client events handler
# TODO: HandleEvent
# TODO: Handle events from client
pass
case "N":
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
@@ -511,7 +586,7 @@ class Client:
while self.__alive:
if len(self.__packets_queue) > 0:
for index, packet in enumerate(self.__packets_queue):
self.log.debug(f"Packet: {packet}")
# self.log.debug(f"Packet: {packet}")
del self.__packets_queue[index]
task = self._loop.create_task(self._handle_codes(packet))
tasks.append(task)
@@ -533,7 +608,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

@@ -1,13 +1,13 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport
from logging import Logger
from typing import Tuple, List, Dict
from typing import Tuple, List, Dict, Optional, Union
from core import Core, utils
@@ -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 = 0.0
self.__tasks = []
self.__reader = reader
self.__writer = writer
@@ -32,7 +33,9 @@ class Client:
self._guest = True
self.__alive = True
self._ready = False
self._cars: List[dict | None] = []
self._cars: List[Optional[Dict[str, int]]] = []
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
@property
def _writer(self) -> StreamWriter: ...
@property
@@ -51,14 +54,21 @@ 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 _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: ...
async def _spawn_car(self, data: str) -> None: ...
async def _delete_car(self, raw_data: str) -> None: ...
async def _edit_car(self, raw_data: str, data: str) -> None: ...
async def _reset_car(self, raw_data: str) -> None: ...
async def _handle_car_codes(self, data: str) -> None: ...
async def _connected_handler(self) -> None: ...
async def _chat_handler(self, data: str) -> None: ...
async def _handle_codes(self, data: bytes) -> None: ...
async def _looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def _remove_me(self) -> None: ...

View File

@@ -2,7 +2,7 @@
# File core.__init__.py
# Written by: SantaSpeen
# Version 1.3
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
@@ -10,8 +10,8 @@
__title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.4.0'
__build__ = 1361 # Я это считаю лог файлами
__version__ = '0.4.1'
__build__ = 1486 # Я это считаю лог файлами
__author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su'
__license__ = "FPA"
@@ -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:
config.enc = config.Options['encoding']
if config.Options['debug'] is True:
utils.set_debug_status()
log.info("Debug enabled!")
log = get_logger("core.init")
log.debug("Debug mode enabled!")
log.debug(f"Server config: {config}")
# 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

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.core.py
# Written by: SantaSpeen
# Version: 0.4.0
# Version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -20,6 +20,7 @@ from modules import PluginsLoader
from modules.WebAPISystem import app as webapp
# noinspection PyProtectedMember
class Core:
def __init__(self):
@@ -40,8 +41,10 @@ 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"
self.BeamMP_version = "3.1.1" # 20.07.2023
ev.register_event("get_player", self.get_client)
@@ -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

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.core.pyi
# Written by: SantaSpeen
# Version 0.4.0
# Version 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -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

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -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

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.tcp_server.pyi
# Written by: SantaSpeen
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio

View File

@@ -1,15 +1,15 @@
# Developed by KuiToi Dev
# File core.udp_server.py
# Written by: SantaSpeen
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import traceback
from core import utils
# noinspection PyProtectedMember
class UDPServer(asyncio.DatagramTransport):
transport = None
@@ -21,11 +21,8 @@ class UDPServer(asyncio.DatagramTransport):
self.host = host
self.port = port
self.run = False
# self.transport = transport
def connection_made(self, transport):
self.log.debug("set connection transport")
# self.transport = self.transport()
def connection_made(self, transport): ...
async def handle_datagram(self, data, addr):
try:
@@ -34,16 +31,15 @@ class UDPServer(asyncio.DatagramTransport):
client = self.Core.get_client(cid=cid)
if client:
if client._udp_sock != (self.transport, addr):
client._udp_sock = (self.transport, addr)
self.log.debug(f"Set UDP Sock for CID: {cid}")
match code:
case "p":
self.log.debug(f"[{cid}] Send ping")
# TODO: Call event onSentPing
self.transport.sendto(b"p", addr) # Send ping
case "Z":
# TODO: Call event onChangePosition
case "p": # Ping packet
ev.call_event("onSentPing")
self.transport.sendto(b"p", addr)
case "Z": # Position packet
if client._udp_sock != (self.transport, addr):
client._udp_sock = (self.transport, addr)
self.log.debug(f"Set UDP Sock for CID: {cid}")
ev.call_event("onChangePosition")
if client:
await client._send(data[2:], to_all=True, to_self=False, to_udp=True)
case _:
@@ -87,7 +83,6 @@ class UDPServer(asyncio.DatagramTransport):
self.run = True
while not self.transport.is_closing():
await asyncio.sleep(0.2)
self.log.info("UDP сервер сдох 1")
except OSError as e:
self.log.error("Cannot bind port or other error")
self.log.exception(e)
@@ -95,7 +90,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

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.udp_server.py
# Written by: SantaSpeen
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio

View File

@@ -2,7 +2,7 @@
# File core.utils.py
# Written by: SantaSpeen
# Version 1.1
# Core version: 0.4.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import datetime
@@ -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,22 @@ class EventsSystem:
"onPlayerAuthenticated": [], # Only sync
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onSentPing": [], # Only sync
"onChangePosition": [], # Only sync
"onServerStopped": [],
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onServerStopped": []
}
@@ -78,7 +88,8 @@ class EventsSystem:
return funcs_data
def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling sync event: '{event_name}'")
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():

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 = {}