Compare commits

..

16 Commits

Author SHA1 Message Date
b80d519c8d Update TODOs 2023-07-20 06:48:08 +03:00
2ace3fcd17 Update TODOs 2023-07-20 06:47:26 +03:00
dbe8b14d7f Add lua loading; Not supporting for now... 2023-07-20 06:44:23 +03:00
752e981462 Is this LUAAAAA? No... 2023-07-20 05:21:00 +03:00
744a7347a3 Prepare for lua support;
Minor update;
2023-07-20 04:59:12 +03:00
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
17 changed files with 361 additions and 252 deletions

View File

@ -7,7 +7,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
## TODOs ## TODOs
- [x] Server core - [x] Server core
- [x] BeamMP System - [x] BeamMP System:
- [x] Private access (Without key, Direct connect) - [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher) - [x] Public access (With key, listing in Launcher)
- [X] Player authentication - [X] Player authentication
@ -19,6 +19,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Chat - [x] Chat
- [x] Players online counter - [x] Players online counter
- [x] Packets handled (Recursive finding second packet) - [x] Packets handled (Recursive finding second packet)
- [ ] Client events
- [x] Car synchronizations: - [x] Car synchronizations:
- [x] State packets - [x] State packets
- [x] Spawn cars - [x] Spawn cars
@ -49,14 +50,24 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Create custom events - [x] Create custom events
- [x] Return from events - [x] Return from events
- [x] Async support - [x] Async support
- [ ] Add all events
- [x] Plugins support - [x] Plugins support
- [ ] KuiToi class - [ ] Python part:
- [ ] Client (Player) class - [x] Load Python plugins
- [x] Load Python plugins - [x] Async support
- [x] Async support - [ ] KuiToi class
- [ ] Load Lua plugins (Original BeamMP compatibility) - [ ] Client (Player) class
- [ ] JavaScript part:
- [ ] Load JavaScript plugins
- [ ] KuiToi class
- [ ] Client (Player) class
- [ ] Lua part: (Original BeamMP compatibility)
- [x] Load Lua plugins
- [ ] MP Class
- [ ] Util class
- [ ] FS class
- [x] MultiLanguage (i18n support) - [x] MultiLanguage (i18n support)
- [x] Core - [ ] Core
- [x] Console - [x] Console
- [x] WebAPI - [x] WebAPI
- [ ] HTTP API Server (fastapi) - [ ] HTTP API Server (fastapi)

View File

@ -5,4 +5,5 @@ uvicorn~=0.22.0
fastapi~=0.100.0 fastapi~=0.100.0
starlette~=0.27.0 starlette~=0.27.0
pydantic~=2.0.2 pydantic~=2.0.2
click~=8.1.4 click~=8.1.4
lupa~=2.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.0 # Core version: 0.4.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@ -33,8 +33,9 @@ class Client:
self.roles = None self.roles = None
self._guest = True self._guest = True
self._ready = False self._ready = False
self._cars = [] self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
self._connect_time: float = 0 self._snowman = {"id": -1, "packet": ""}
self._connect_time = 0
@property @property
def _writer(self): def _writer(self):
@ -75,13 +76,11 @@ class Client:
def is_disconnected(self): def is_disconnected(self):
if not self.__alive: if not self.__alive:
return True return True
res = self.__writer.is_closing() if self.__writer.is_closing():
if res: self.log.debug(f"is_d: Disconnected.")
self.log.debug(f"Disconnected.")
self.__alive = False self.__alive = False
return True return True
else: else:
self.log.debug(f"Alive.")
self.__alive = True self.__alive = True
return False return False
@ -148,7 +147,7 @@ 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}') # self.log.debug(f'[TCP] {header + data!r}')
try: try:
writer.write(header + data) writer.write(header + data)
await writer.drain() await writer.drain()
@ -160,27 +159,6 @@ class Client:
await self._remove_me() await self._remove_me()
return False 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): async def _recv(self, one=False):
while self.__alive: while self.__alive:
try: try:
@ -196,7 +174,7 @@ class Client:
self.__packets_queue.append(None) self.__packets_queue.append(None)
self.__alive = False self.__alive = False
continue continue
self.log.debug(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) self.__packets_queue.append(None)
continue continue
@ -205,25 +183,20 @@ class Client:
await self.kick("Header size limit exceeded") await self.kick("Header size limit exceeded")
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.__packets_queue.append(None)
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 = await self.__reader.read(int_header) 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:" abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg): if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(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: if one:
# self.log.debug(f"int_header: `{int_header}`; data: `{data}`;")
return data return data
# FIXME
# else:
# t = asyncio.create_task(self.__handle_packet(data, int_header))
# self.__tasks.append(t)
self.__packets_queue.append(data) self.__packets_queue.append(data)
except ConnectionError: except ConnectionError:
@ -335,8 +308,6 @@ class Client:
else: else:
await self._send(mod_list) await self._send(mod_list)
elif data == b"Done": elif data == b"Done":
for c in range(int(config.Game['max_cars'] * 2.3)):
self._cars.append(None)
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
@ -363,116 +334,213 @@ class Client:
self.log.debug(f"Invalid packet: Could not parse pid/vid from packet: '{data}'") self.log.debug(f"Invalid packet: Could not parse pid/vid from packet: '{data}'")
return -1, -1 return -1, -1
async def _handle_car_codes(self, dta): async def _spawn_car(self, data):
if len(dta) < 6: 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 return
sub_code = dta[1] sub_code = raw_data[1]
data = dta[3:] data = raw_data[3:]
match sub_code: match sub_code:
case "s": # Spawn car case "s": # Spawn car
self.log.debug("Trying to spawn car") self.log.debug("Trying to spawn car")
if data[0] == "0": if data[0] == "0":
car_id = 0 await self._spawn_car(data)
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
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
# 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)
case "d": # Delete car case "d": # Delete car
self.log.debug("Trying to delete car") self.log.debug("Trying to delete car")
cid, car_id = self._get_cid_vid(dta) await self._delete_car(raw_data)
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}")
case "c": # Edit car case "c": # Edit car
self.log.debug("Trying to edit car") self.log.debug("Trying to edit car")
cid, car_id = self._get_cid_vid(dta) await self._edit_car(raw_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}")
case "r": # Reset car case "r": # Reset car
self.log.debug("Trying to reset car") self.log.debug("Trying to reset car")
cid, car_id = self._get_cid_vid(dta) await self._reset_car(raw_data)
if car_id != -1 and cid == self.cid:
# TODO: Call event onCarReset case "t": # Broken details
await self._send(dta, to_all=True, to_self=False) self.log.debug(f"Something changed/broken: {raw_data}")
self.log.debug(f"Car reset: car_id={car_id}") await self._send(raw_data, to_all=True, to_self=False)
case "t":
self.log.debug(f"Received 'Ot' packet: {dta}") case "m": # Move focus cat
await self._send(dta, to_all=True, to_self=False) self.log.debug(f"Move focus to: {raw_data}")
case "m": await self._send(raw_data, to_all=True, to_self=True)
await self._send(dta, 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): async def _handle_codes(self, data):
if not data: if not data:
@ -482,73 +550,27 @@ class Client:
# Codes: V W X Y # Codes: V W X Y
if 89 >= data[0] >= 86: if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False) await self._send(data, to_all=True, to_self=False)
return
try: try:
data = data.decode() data = data.decode()
except UnicodeDecodeError: except UnicodeDecodeError:
self.log.debug(f"UnicodeDecodeError: {data}") self.log.error(f"UnicodeDecodeError: {data}")
return return
code = data[0]
# Codes: p, Z in udp_server.py # Codes: p, Z in udp_server.py
match code: match data[0]: # At data[0] code
case "H": case "H": # Map load, client ready
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s") await self._connected_handler()
# 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'])
case "C": # Chat handler case "C": # Chat handler
sup = data.find(":", 2) await self._chat_handler(data)
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:
# 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
except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}")
if need_send:
await self._send(data, to_all=True)
case "O": # Cars handler case "O": # Cars handler
await self._handle_car_codes(data) await self._handle_car_codes(data)
case "E": # Client events handler case "E": # Client events handler
# TODO: HandleEvent # TODO: Handle events from client
pass pass
case "N": case "N":
@ -564,7 +586,7 @@ class Client:
while self.__alive: while self.__alive:
if len(self.__packets_queue) > 0: if len(self.__packets_queue) > 0:
for index, packet in enumerate(self.__packets_queue): for index, packet in enumerate(self.__packets_queue):
self.log.debug(f"Packet: {packet}") # self.log.debug(f"Packet: {packet}")
del self.__packets_queue[index] del self.__packets_queue[index]
task = self._loop.create_task(self._handle_codes(packet)) task = self._loop.create_task(self._handle_codes(packet))
tasks.append(task) tasks.append(task)

View File

@ -1,13 +1,13 @@
# 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.0 # Core version: 0.4.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport from asyncio import StreamReader, StreamWriter, DatagramTransport
from logging import Logger from logging import Logger
from typing import Tuple, List, Dict from typing import Tuple, List, Dict, Optional, Union
from core import Core, utils from core import Core, utils
@ -15,7 +15,7 @@ from core import Core, utils
class Client: class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client": def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self._connect_time: float = None self._connect_time: float = 0.0
self.__tasks = [] self.__tasks = []
self.__reader = reader self.__reader = reader
self.__writer = writer self.__writer = writer
@ -33,7 +33,9 @@ class Client:
self._guest = True self._guest = True
self.__alive = True self.__alive = True
self._ready = False 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 @property
def _writer(self) -> StreamWriter: ... def _writer(self) -> StreamWriter: ...
@property @property
@ -56,12 +58,17 @@ class Client:
async def send_event(self, event_name: str, event_data: str) -> 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 _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 __handle_packet(self, data, int_header): ...
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) -> None: ...
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ... async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
async def _handle_car_codes(self, data) -> None: ... async def _spawn_car(self, data: str) -> None: ...
async def _handle_codes(self, data) -> 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: ... 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,7 +2,7 @@
# File core.__init__.py # File core.__init__.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.3 # Version 1.3
# Core version: 0.4.0 # Core version: 0.4.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai) # Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
@ -10,8 +10,8 @@
__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.0' __version__ = '0.4.1'
__build__ = 1361 # Я это считаю лог файлами __build__ = 1486 # Я это считаю лог файлами
__author__ = 'SantaSpeen' __author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su' __author_email__ = 'admin@kuitoi.su'
__license__ = "FPA" __license__ = "FPA"
@ -46,13 +46,13 @@ if args.config:
config_provider = ConfigProvider(config_path) config_provider = ConfigProvider(config_path)
config = config_provider.open_config() config = config_provider.open_config()
builtins.config = config builtins.config = config
config.enc = config.Options['encoding']
if config.Options['debug'] is True: if config.Options['debug'] is True:
utils.set_debug_status() utils.set_debug_status()
log.info("Debug enabled!") log.info("Debug enabled!")
log = get_logger("core.init") log = get_logger("core.init")
log.debug("Debug mode enabled!") log.debug("Debug mode enabled!")
log.debug(f"Server config: {config}") log.debug(f"Server config: {config}")
config.enc = config.Options['encoding']
# i18n init # i18n init
log.debug("Initializing i18n...") log.debug("Initializing i18n...")
ml = MultiLanguage() ml = MultiLanguage()

View File

@ -1,7 +1,7 @@
# 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.0 # Version: 0.4.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@ -16,7 +16,7 @@ from core import utils
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, LuaPluginsLoader
from modules.WebAPISystem import app as webapp from modules.WebAPISystem import app as webapp
@ -44,7 +44,7 @@ class Core:
self.lock_upload = False self.lock_upload = False
self.client_major_version = "2.0" self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0" self.BeamMP_version = "3.1.1" # 20.07.2023
ev.register_event("get_player", self.get_client) ev.register_event("get_player", self.get_client)
@ -217,11 +217,14 @@ class Core:
lambda x: f"Players list: {self.get_clients_list(True)}" lambda x: f"Players list: {self.get_clients_list(True)}"
) )
self.log.debug("Initializing PluginsLoader...") pl_dir = "plugins"
if not os.path.exists("plugins"): self.log.debug("Initializing PluginsLoaders...")
os.mkdir("plugins") if not os.path.exists(pl_dir):
pl = PluginsLoader("plugins") os.mkdir(pl_dir)
pl = PluginsLoader(pl_dir)
await pl.load() await pl.load()
lpl = LuaPluginsLoader(pl_dir)
await lpl.load()
try: try:
# WebApi Start # WebApi Start
@ -263,6 +266,7 @@ class Core:
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
await ev.call_async_event("_plugins_start") await ev.call_async_event("_plugins_start")
await ev.call_async_event("_lua_plugins_start")
self.run = True self.run = True
self.log.info(i18n.start) self.log.info(i18n.start)
@ -287,6 +291,7 @@ class Core:
ev.call_event("onServerStopped") ev.call_event("onServerStopped")
await ev.call_async_event("onServerStopped") await ev.call_async_event("onServerStopped")
await ev.call_async_event("_plugins_unload") await ev.call_async_event("_plugins_unload")
await ev.call_async_event("_lua_plugins_unload")
self.run = False self.run = False
self.log.info(i18n.stop) self.log.info(i18n.stop)
if config.WebAPI["enabled"]: if config.WebAPI["enabled"]:

View File

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

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.0 # Core version: 0.4.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio

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.0 # Core version: 0.4.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio

View File

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

View File

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

View File

@ -2,7 +2,7 @@
# File core.utils.py # File core.utils.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.1 # Version 1.1
# Core version: 0.4.0 # Core version: 0.4.1
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import datetime import datetime

View File

@ -28,7 +28,11 @@ class EventsSystem:
"onPlayerJoin": [], "onPlayerJoin": [],
"onChatReceive": [], "onChatReceive": [],
"onCarSpawn": [], "onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [], "onCarEdited": [],
"onCarReset": [],
"onSentPing": [], # Only sync
"onChangePosition": [], # Only sync
"onServerStopped": [], "onServerStopped": [],
} }
self.__async_events = { self.__async_events = {
@ -36,7 +40,9 @@ class EventsSystem:
"onPlayerJoin": [], "onPlayerJoin": [],
"onChatReceive": [], "onChatReceive": [],
"onCarSpawn": [], "onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [], "onCarEdited": [],
"onCarReset": [],
"onServerStopped": [] "onServerStopped": []
} }
@ -82,7 +88,8 @@ class EventsSystem:
return funcs_data return funcs_data
def call_event(self, event_name, *args, **kwargs): 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 = [] funcs_data = []
if event_name in self.__events.keys(): if event_name in self.__events.keys():

View File

@ -1 +1,2 @@
from .plugins_loader import PluginsLoader from .plugins_loader import PluginsLoader
from .lua_plugins_loader import LuaPluginsLoader

View File

@ -0,0 +1,67 @@
import asyncio
import os
# noinspection PyUnresolvedReferences
import lupa.lua53
from lupa.lua53 import LuaRuntime
from core import get_logger
class MP:
def __init__(self, name):
self.name = name
self.log = get_logger(f"LuaPlugin | {name}")
def log_info(self, *args):
s = ""
for i in args:
s += i
self.log.info(s)
class LuaPluginsLoader:
def __init__(self, plugins_dir):
self.loop = asyncio.get_event_loop()
self.plugins_dir = plugins_dir
self.lua_plugins = {}
self.lua_plugins_tasks = []
self.lua_dirs = []
self.log = get_logger("LuaPluginsLoader")
self.loaded_str = "Lua plugins: "
ev.register_event("_lua_plugins_start", self.start)
ev.register_event("_lua_plugins_unload", self.unload)
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
console.add_command("lua_pl", lambda x: self.loaded_str[:-2])
async def load(self):
self.log.debug("Loading Lua plugins...")
py_folders = ev.call_event("_plugins_get")[0]
for obj in os.listdir(self.plugins_dir):
path = os.path.join(self.plugins_dir, obj)
if os.path.isdir(path) and obj not in py_folders and obj not in "__pycache__":
if os.path.isfile(os.path.join(path, "main.lua")):
self.lua_dirs.append([path, obj])
self.log.debug(f"py_folders {py_folders}, lua_dirs {self.lua_dirs}")
for path, obj in self.lua_dirs:
lua = LuaRuntime(encoding=config.enc, source_encoding=config.enc)
mp = MP(obj)
lua.globals().MP = mp
lua.globals().print = mp.log_info
code = f'package.path = package.path.."' \
f';{self.plugins_dir}/{obj}/?.lua' \
f';{self.plugins_dir}/{obj}/lua/?.lua' \
f';modules/PluginsLoader/lua_libs/?.lua"'
with open(os.path.join(path, "main.lua"), 'r', encoding=config.enc) as f:
code += f.read()
lua.execute(code)
async def start(self, _):
...
async def unload(self, _):
...

View File

@ -33,20 +33,10 @@ class KuiToi:
def name(self): def name(self):
return self.__name return self.__name
@name.setter
def name(self, value):
# You chell not pass
pass
@property @property
def dir(self): def dir(self):
return self.__dir return self.__dir
@dir.setter
def dir(self, value):
# You chell not pass
pass
@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 = os.path.join(self.__dir, file)
@ -83,6 +73,7 @@ class PluginsLoader:
self.loaded_str = "Plugins: " self.loaded_str = "Plugins: "
ev.register_event("_plugins_start", self.start) ev.register_event("_plugins_start", self.start)
ev.register_event("_plugins_unload", self.unload) ev.register_event("_plugins_unload", self.unload)
ev.register_event("_plugins_get", lambda x: list(self.plugins.keys()))
console.add_command("plugins", lambda x: self.loaded_str[:-2]) console.add_command("plugins", lambda x: self.loaded_str[:-2])
console.add_command("pl", lambda x: self.loaded_str[:-2]) console.add_command("pl", lambda x: self.loaded_str[:-2])
@ -90,14 +81,14 @@ class PluginsLoader:
self.log.debug("Loading plugins...") self.log.debug("Loading plugins...")
files = os.listdir(self.plugins_dir) files = os.listdir(self.plugins_dir)
for file in files: for file in files:
if file.endswith(".py"): file_path = os.path.join(self.plugins_dir, file)
if os.path.isfile(file_path) and file.endswith(".py"):
try: try:
self.log.debug(f"Loading plugin: {file[:-3]}") self.log.debug(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType(file[:-3]) plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.print = print plugin.print = print
file_path = os.path.join(self.plugins_dir, file)
plugin.__file__ = file_path plugin.__file__ = file_path
with open(f'{file_path}', 'r', encoding=config.enc) as f: with open(f'{file_path}', 'r', encoding=config.enc) as f:
code = f.read() code = f.read()

View File

@ -11,5 +11,6 @@ from .ConfigProvider import ConfigProvider, Config
from .i18n import MultiLanguage from .i18n import MultiLanguage
from .EventsSystem import EventsSystem from .EventsSystem import EventsSystem
from .PluginsLoader import PluginsLoader from .PluginsLoader import PluginsLoader
from .PluginsLoader import LuaPluginsLoader
from .WebAPISystem import web_app from .WebAPISystem import web_app
from .WebAPISystem import _stop as stop_web from .WebAPISystem import _stop as stop_web