Compare commits

...

4 Commits

Author SHA1 Message Date
acdb32d900 Update TODOs 2023-07-17 14:25:07 +03:00
50b1e7b176 Recreate cars system;
Move code "O" to client._handle_vehicle_codes();
2023-07-17 14:18:52 +03:00
c9e6a0a9cd Change evens naming semantic 2023-07-16 16:14:16 +03:00
cd098571d9 Change evens naming semantic 2023-07-16 16:14:00 +03:00
6 changed files with 197 additions and 139 deletions

View File

@ -18,9 +18,11 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Connecting to the world - [x] Connecting to the world
- [x] Chat - [x] Chat
- [x] Players online counter - [x] Players online counter
- [ ] Packets handled (One or more)
- [ ] Car synchronizations: - [ ] Car synchronizations:
- [x] State packets
- [x] Spawn cars - [x] Spawn cars
- [ ] Edit cars - [x] Edit cars
- [ ] Delete cars - [ ] Delete cars
- [ ] Reset cars - [ ] Reset cars
- [ ] "ABG:" (compressed data) - [ ] "ABG:" (compressed data)

View File

@ -110,8 +110,11 @@ class Client:
continue continue
if not to_udp or code in ['V', 'W', 'Y', 'E']: if not to_udp or code in ['V', 'W', 'Y', 'E']:
if code in ['O', 'T'] or len(data) > 1000: if code in ['O', 'T'] or len(data) > 1000:
# TODO: Compress data if len(data) > 400:
await client._send(data) # TODO: Compress data
await client._send(data)
else:
await client._send(data)
else: else:
await client._send(data) await client._send(data)
else: else:
@ -159,7 +162,7 @@ class Client:
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: {data}") self.log.debug(f"ABG Packet: {len(data)}")
return data return data
return data return data
except ConnectionError: except ConnectionError:
@ -247,127 +250,179 @@ class Client:
await self._send(bytes(mod_list, "utf-8")) await self._send(bytes(mod_list, "utf-8"))
elif data == b"Done": elif data == b"Done":
await self._send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json") await self._send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
for c in range(config.Game['max_cars']):
self._cars.append(None)
break break
return return
def _get_cid_vid(self, data: str):
s = data[data.find(":", 1)+1:]
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: '{s}'")
return -1, -1
cid = s[:id_sep]
vid = s[id_sep + 1:]
if cid.isdigit() and vid.isdigit():
try:
cid = int(cid)
vid = int(vid)
return cid, vid
except ValueError:
self.log.debug(f"Invalid packet: Could not parse cid/vid from packet, as one or both are not valid "
f"numbers: '{s}'")
return -1, -1
self.log.debug(f"Invalid packet: Could not parse pid/vid from packet: '{s}'")
return -1, -1
async def _handle_vehicle_codes(self, data):
if len(data) < 6:
return
sub_code = data[1]
data = 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 with ID {car_id}")
car_data = data[2:]
car_json = {}
try:
car_json = json.loads(data[5:])
except Exception as e:
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
# TODO: Call event onCarSpawn
allow_spawn = True
over_spawn = False
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
unicycle = car_json.get("jbm") == "unicycle"
if (allow_spawn and (config.Game['max_cars'] > car_id or unicycle)) or over_spawn:
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)
else:
await self._send(pkt)
des = f"Od:{self.cid}-{car_id}"
await self._send(des)
case "d": # Delete car
self.log.debug("Trying to delete car")
cid, car_id = self._get_cid_vid(data)
if car_id != -1 and cid == self.cid:
# TODO: Call event onCarDelete
await self._send(data, to_all=True, to_self=True)
try:
self._cars[car_id] = None
await self._send(f"Od:{self.cid}-{car_id}")
self.log.debug(f"Deleted car with car_id: {car_id}")
except IndexError:
self.log.debug(f"Unknown car: car_id={car_id}")
case "c": # Edit car
self.log.debug("Trying to edit car")
# TODO: edit car
cid, car_id = self._get_cid_vid(data)
if car_id != -1 and cid == self.cid:
car = self.cars[car_id]
if car['unicycle']:
pass
case "r": # Reset car
self.log.debug("Trying to reset car")
# TODO: reset car
cid, car_id = self._get_cid_vid(data)
if car_id != -1 and cid == self.cid:
pass
case "t" | "m":
pass
async def _handle_codes(self, data):
if not data:
self.__alive = False
return
# Codes: V W X Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
data = data.decode()
code = data[0]
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:
await self._send(car['packet'])
case "C": # Chat handler
msg = data[4 + len(self.nick):]
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:
if need_send:
need_send = False
to_all = True
if to_all:
if need_send:
need_send = False
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)
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
await self._handle_vehicle_codes(data)
case "E": # Client events handler
# TODO: HandleEvent
pass
case "N":
await self._send(data, to_all=True, to_self=False)
async def _looper(self): async def _looper(self):
await self._send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID await self._send(f"P{self.cid}") # Send clientID
await self._sync_resources() await self._sync_resources()
while self.__alive: while self.__alive:
data = await self._recv() data = await self._recv()
if not data: self._loop.create_task(self._handle_codes(data))
self.__alive = False
break
# Codes: V W X Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
data = data.decode('utf-8')
code = data[0]
self.log.debug(f"Received code: {code}, data: {data}")
match code:
case "H":
# Client connected
ev.call_event("player_join", player=self)
await ev.call_async_event("player_join", 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:
await self._send(car)
case "C": # Chat handler
msg = data[4 + len(self.nick):]
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
continue
self.log.info(f"Received message: {msg}")
# TODO: Handle chat event
to_ev = {"message": msg, "player": self}
ev_data_list = ev.call_event("chat_receive", **to_ev)
d2 = await ev.call_async_event("chat_receive", **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:
if need_send:
need_send = False
to_all = True
if to_all:
if need_send:
need_send = False
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)
except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}")
if need_send:
await self._send(data, to_all=True)
case "O": # Vehicle info handler
if len(data) < 6:
continue
sub_code = data[1]
data = data[3:]
vid = -1
pid = -1
match sub_code:
case "s": # Spawn car
if data[0] == "0":
car_id = len(self._cars)
self.log.debug(f"Created a car with ID {car_id}")
car_data = data[2:]
car_json = {}
try:
car_json = json.loads(data[5:])
except Exception as e:
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
# TODO: Call event onVehicleSpawn
spawn = True
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
if spawn and (config.Game['max_cars'] > car_id or car_json.get("jbm") == "unicycle"):
self.log.debug(f"Car spawn accepted.")
self._cars.append(car_data)
await self._send(pkt, to_all=True)
else:
await self._send(pkt)
des = f"Od:{self.cid}-{car_id}"
await self._send(des)
case "c": # Edit car
# TODO: edit car
pass
case "d": # Delete car
# TODO: delete car
pass
case "r": # Reset car
# TODO: reset car
pass
case "t" | "m":
pass
case "E": # Client events handler
# TODO: HandleEvent
pass
case "N":
# TODO: N
pass
async def _remove_me(self): async def _remove_me(self):
await asyncio.sleep(0.3) await asyncio.sleep(0.3)

View File

@ -7,7 +7,7 @@
import asyncio import asyncio
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from logging import Logger from logging import Logger
from typing import Tuple from typing import Tuple, List, Dict
from core import Core, utils from core import Core, utils
@ -29,7 +29,7 @@ class Client:
self._guest = True self._guest = True
self.__alive = True self.__alive = True
self._ready = False self._ready = False
self._cars = [] self._cars: List[dict | None] = []
@property @property
def _writer(self) -> StreamWriter: ... def _writer(self) -> StreamWriter: ...
@property @property
@ -45,13 +45,16 @@ class Client:
@property @property
def ready(self) -> bool: ... def ready(self) -> bool: ...
@property @property
def cars(self) -> list: ... def cars(self) -> List[dict | None]: ...
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(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) -> bytes: ... async def _recv(self) -> bytes: ...
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) -> None: ...
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
async def _handle_vehicle_codes(self, data) -> None: ...
async def _handle_codes(self, data) -> 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

@ -260,8 +260,8 @@ class Core:
self.run = True self.run = True
self.log.info(i18n.start) self.log.info(i18n.start)
ev.call_event("server_started") ev.call_event("onServerStarted")
await ev.call_async_event("server_started") await ev.call_async_event("onServerStarted")
await t # Wait end. await t # Wait end.
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
@ -278,8 +278,8 @@ class Core:
asyncio.run(self.main()) asyncio.run(self.main())
async def stop(self): async def stop(self):
ev.call_event("server_stopped") ev.call_event("onServerStopped")
await ev.call_async_event("server_stopped") await ev.call_async_event("onServerStopped")
await ev.call_async_event("_plugins_unload") await ev.call_async_event("_plugins_unload")
self.run = False self.run = False
self.log.info(i18n.stop) self.log.info(i18n.stop)

View File

@ -41,7 +41,7 @@ class TCPServer:
await client.kick("Invalid Key (too long)!") await client.kick("Invalid Key (too long)!")
return False, client return False, client
client._key = data.decode("utf-8") client._key = data.decode("utf-8")
ev.call_event("auth_sent_key", player=client) ev.call_event("onPlayerSentKey", player=client)
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
url = 'https://auth.beammp.com/pkToUser' url = 'https://auth.beammp.com/pkToUser'
@ -71,7 +71,7 @@ class TCPServer:
await client.kick('Stale Client (replaced by new client)') await client.kick('Stale Client (replaced by new client)')
return False, client return False, client
ev.call_event("auth_ok", player=client) ev.call_event("onPlayerAuthenticated", player=client)
if len(self.Core.clients_by_id) > config.Game["players"]: if len(self.Core.clients_by_id) > config.Game["players"]:
# TODO: i18n # TODO: i18n

View File

@ -22,20 +22,18 @@ class EventsSystem:
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.as_tasks = [] self.as_tasks = []
self.__events = { self.__events = {
"server_started": [], "onServerStarted": [],
"auth_sent_key": [], # Only sync "onPlayerSentKey": [], # Only sync
"auth_ok": [], # Only sync "onPlayerAuthenticated": [], # Only sync
"player_join": [], "onPlayerJoin": [],
"chat_receive": [], "onChatReceive": [],
"server_stopped": [], "onServerStopped": [],
} }
self.__async_events = { self.__async_events = {
"server_started": [], "onServerStarted": [],
"_plugins_start": [], "onPlayerJoin": [],
"_plugins_unload": [], "onChatReceive": [],
"player_join": [], "onServerStopped": []
"chat_receive": [],
"server_stopped": []
} }
def builtins_hook(self): def builtins_hook(self):