Compare commits

..

7 Commits

Author SHA1 Message Date
a5202edf83 Relocate open() in plugins 2023-07-15 02:17:00 +03:00
64ce662d04 Add methods in KuiToi class 2023-07-15 02:15:11 +03:00
08e4f0fcba Update TODOs 2023-07-14 23:11:03 +03:00
99f40eadb0 Add returns from ev.call_events 2023-07-14 22:29:37 +03:00
5a40ab8b05 Add TODOs to code. 2023-07-14 22:27:23 +03:00
4f688d7c02 Update LICENCE 2023-07-14 21:55:19 +03:00
c4fe201b86 Move class Client to self file. 2023-07-14 21:52:24 +03:00
9 changed files with 389 additions and 346 deletions

View File

@ -6,6 +6,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions: Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change. - Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -40,8 +40,9 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Events System - [x] Events System
- [x] Call events - [x] Call events
- [x] Create custom events - [x] Create custom events
- [ ] Return from events - [x] Return from events
- [x] Plugins support - [x] Plugins support
- [ ] KuiToi class
- [x] Load Python plugins - [x] Load Python plugins
- [ ] Load Lua plugins (Original BeamMP compatibility) - [ ] Load Lua plugins (Original BeamMP compatibility)
- [x] MultiLanguage (i18n support) - [x] MultiLanguage (i18n support)
@ -78,6 +79,6 @@ Copyright (c) 2023 SantaSpeen (Maxim Khomutov)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions: Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change. - Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
``` ```

245
src/core/Client.py Normal file
View File

@ -0,0 +1,245 @@
import asyncio
import math
import zlib
from core import utils
class Client:
def __init__(self, reader, writer, core):
self.reader = reader
self.writer = writer
self.down_rw = (None, None)
self.log = utils.get_logger("client(None:0)")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid = -1
self.key = None
self.nick = None
self.roles = None
self.guest = True
self.alive = True
self.ready = False
def _update_logger(self):
self.log = utils.get_logger(f"{self.nick}:{self.cid})")
self.log.debug(f"Update logger")
def is_disconnected(self):
if not self.alive:
return True
res = self.writer.is_closing()
if res:
self.log.debug(f"Disconnected.")
self.alive = False
return True
else:
self.log.debug(f"Alive.")
self.alive = True
return False
async def kick(self, reason):
if not self.alive:
self.log.debug(f"Kick({reason}) skipped;")
return
# TODO: i18n
self.log.info(f"Kicked with reason: \"{reason}\"")
await self.tcp_send(b"K" + bytes(reason, "utf-8"))
self.alive = False
async def tcp_send(self, data, to_all=False, writer=None):
# TNetwork.cpp; Line: 383
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
# [][][][][][]...[]
# ^------^^---...-^
# size data
if writer is None:
writer = self.writer
if to_all:
for client in self.Core.clients:
if not client:
continue
await client.tcp_send(data)
return
if len(data) == 10:
data += b"."
header = len(data).to_bytes(4, "little", signed=True)
self.log.debug(f'len: {len(data)}; send: {header + data}')
try:
writer.write(header + data)
await writer.drain()
except ConnectionError:
self.log.debug('tcp_send: Disconnected')
self.alive = False
async def recv(self):
try:
header = await self.reader.read(4)
int_header = 0
for i in range(len(header)):
int_header += header[i]
if int_header <= 0:
await asyncio.sleep(0.1)
self.is_disconnected()
if self.alive:
self.log.debug(f"Header: {header}")
await self.kick("Invalid packet - header negative")
return b""
if int_header > 100 * MB:
await self.kick("Header size limit exceeded")
self.log.warn(f"Client {self.nick}:{self.cid} sent header of >100MB - "
f"assuming malicious intent and disconnecting the client.")
return b""
data = await self.reader.read(100 * MB)
self.log.debug(f"header: `{header}`; 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)}")
abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):])
self.log.debug(f"ABG: {data}")
return data
return data
except ConnectionError:
self.alive = False
return b""
async def _split_load(self, start, end, d_sock, filename):
real_size = end - start
writer = self.down_rw[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}")
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
async def sync_resources(self):
while self.alive:
data = await self.recv()
self.log.debug(f"data: {data!r}")
if data.startswith(b"f"):
file = data[1:].decode("utf-8")
# TODO: i18n
self.log.info(f"Requested mode: {file!r}")
size = -1
for mod in self.Core.mods_list:
if type(mod) == int:
continue
if mod.get('path') == file:
size = mod['size']
self.log.debug("File is accept.")
break
self.log.debug(f"Mode size: {size}")
if size == -1:
await self.tcp_send(b"CO")
await self.kick(f"Not allowed mod: " + file)
return
await self.tcp_send(b"AG")
t = 0
while not self.down_rw[0]:
await asyncio.sleep(0.1)
t += 1
if t > 50:
await self.kick("Missing download socket")
return
half_size = math.floor(size / 2)
uploads = [
self._split_load(0, half_size, False, file),
self._split_load(half_size, size, True, file)
]
sl0, sl1 = await asyncio.gather(*uploads)
sent = sl0 + sl1
ok = sent == size
lost = size - sent
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
if not ok:
self.alive = False
# TODO: i18n
self.log.error(f"Error while sending.")
return
elif data.startswith(b"SR"):
path_list = ''
size_list = ''
for mod in self.Core.mods_list:
if type(mod) == int:
continue
path_list += f"{mod['path']};"
size_list += f"{mod['size']};"
mod_list = path_list + size_list
self.log.debug(f"Mods List: {mod_list}")
if len(mod_list) == 0:
await self.tcp_send(b"-")
else:
await self.tcp_send(bytes(mod_list, "utf-8"))
elif data == b"Done":
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
break
return
async def looper(self):
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID
await self.sync_resources()
while self.alive:
data = await self.recv()
if data == b"":
if not self.alive:
break
else:
await asyncio.sleep(.1)
self.is_disconnected()
continue
code = data.decode()[0]
self.log.debug(f"Received code: {code}, data: {data}")
match code:
case "H":
# Client connected
self.ready = True
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"), to_all=True)
case "C":
# Chat
ev.call_event("chat_receive", f"{data}")
await self.tcp_send(data, to_all=True)
async def remove_me(self):
await asyncio.sleep(0.3)
self.alive = False
if (self.cid > 0 or self.nick is not None) and \
self.Core.clients_by_nick.get(self.nick):
# if self.ready:
# await self.tcp_send(b"", to_all=True) # I'm disconnected.
self.log.debug(f"Removing client {self.nick}:{self.cid}")
# TODO: i18n
self.log.info("Disconnected")
self.Core.clients[self.cid] = None
self.Core.clients_by_id.pop(self.cid)
self.Core.clients_by_nick.pop(self.nick)
else:
self.log.debug(f"Removing client; Closing connection...")
if not self.writer.is_closing():
self.writer.close()
_, down_w = self.down_rw
if down_w and not down_w.is_closing():
down_w.close()

33
src/core/Client.pyi Normal file
View File

@ -0,0 +1,33 @@
import asyncio
from asyncio import StreamReader, StreamWriter
from typing import Tuple
from core import Core, utils
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self.reader = reader
self.writer = writer
self.down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
self.log = utils.get_logger("client(id: )")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid: int = -1
self.key: str = None
self.nick: str = None
self.roles: str = None
self.guest = True
self.alive = True
self.ready = False
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def tcp_send(self, data: bytes, to_all:bool = False, writer: StreamWriter = None) -> None: ...
async def sync_resources(self) -> None: ...
async def recv(self) -> bytes: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str) -> None: ...
async def looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def remove_me(self) -> None: ...

View File

@ -5,296 +5,20 @@
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
import math
import os import os
import random import random
import zlib
from threading import Thread from threading import Thread
import aiohttp import aiohttp
import uvicorn import uvicorn
from core import utils from core import utils
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.WebAPISystem import app as webapp from modules.WebAPISystem import app as webapp
class Client:
def __init__(self, reader, writer, core):
self.reader = reader
self.writer = writer
self.down_rw = (None, None)
self.log = utils.get_logger("client(None:0)")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid = -1
self.key = None
self.nick = None
self.roles = None
self.guest = True
self.alive = True
self.ready = False
def _update_logger(self):
self.log = utils.get_logger(f"{self.nick}:{self.cid})")
self.log.debug(f"Update logger")
def is_disconnected(self):
if not self.alive:
return True
res = self.writer.is_closing()
if res:
self.log.debug(f"Client Disconnected")
self.alive = False
return True
else:
self.log.debug(f"Client Alive")
self.alive = True
return False
async def kick(self, reason):
if not self.alive:
self.log.debug(f"Kick({reason}) skipped;")
return
self.log.info(f"Kicked with reason: \"{reason}\"")
await self.tcp_send(b"K" + bytes(reason, "utf-8"))
self.alive = False
# await self.remove_me()
async def tcp_send(self, data, to_all=False, writer=None):
# TNetwork.cpp; Line: 383
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
# [][][][][][]...[]
# ^------^^---...-^
# size data
if writer is None:
writer = self.writer
if to_all:
for client in self.Core.clients:
if not client:
continue
await client.tcp_send(data)
return
# self.log.debug(f"tcp_send({data})")
if len(data) == 10:
data += b"."
header = len(data).to_bytes(4, "little", signed=True)
self.log.debug(f'len: {len(data)}; send: {header + data}')
try:
writer.write(header + data)
await writer.drain()
except ConnectionError:
self.log.debug('tcp_send: Disconnected')
self.alive = False
async def recv(self):
try:
header = await self.reader.read(4) # header: 4 bytes
int_header = 0
for i in range(len(header)):
int_header += header[i]
if int_header <= 0:
await asyncio.sleep(0.1)
self.is_disconnected()
if self.alive:
self.log.debug(f"Header: {header}")
await self.kick("Invalid packet - header negative")
return b""
if int_header > 100 * MB:
await self.kick("Header size limit exceeded")
self.log.warn(f"Client {self.nick}:{self.cid} sent header of >100MB - "
f"assuming malicious intent and disconnecting the client.")
return b""
data = await self.reader.read(100 * MB)
self.log.debug(f"header: `{header}`; 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)}")
abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):])
self.log.debug(f"ABG: {data}")
return data
return data
except ConnectionError:
self.alive = False
return b""
async def _split_load(self, start, end, d_sock, filename):
real_size = end - start
writer = self.down_rw[1] if d_sock else self.writer
who = 'dwn' if d_sock else 'srv'
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.")
# break
return real_size
# chunk_size = 125 * MB
# if chunk_size > real_size:
# chunk_size = real_size
# chunks = math.floor(real_size / chunk_size)
# self.log.debug(f"[{who}] s:{start}, e:{end}, c:{chunks}, cz:{chunk_size/MB}mb, rs:{real_size/MB}mb")
# dw = 0
# for chunk in range(1, chunks + 1):
# chunk_end = start + (chunk_size * chunk)
# chunk_start = chunk_end - chunk_size
# # if chunk_start != 0:
# # chunk_start -= 1
# real_size -= chunk_size
# if chunk_size > real_size:
# chunk_end = real_size
# self.log.debug(f"[{who}] Chunk: {chunk}; Start: {chunk_start}; End: {chunk_end/MB};")
# with open(filename, 'rb') as f:
# f.seek(chunk_start)
# data = f.read(chunk_end)
# try:
# writer.write(data)
# await writer.drain()
# except ConnectionError:
# self.alive = False
# self.log.debug(f"[{who}] Disconnected")
# break
# dw += len(data)
# del data
# self.log.debug(f"[{who}] File sent.")
# return dw
async def sync_resources(self):
while self.alive:
data = await self.recv()
self.log.debug(f"data: {data!r}")
if data.startswith(b"f"):
file = data[1:].decode("utf-8")
self.log.debug(f"Sending File: {file}")
size = -1
for mod in self.Core.mods_list:
if type(mod) == int:
continue
if mod.get('path') == file:
size = mod['size']
self.log.debug("File is accept.")
break
if size == -1:
await self.tcp_send(b"CO")
await self.kick(f"Not allowed mod: " + file)
return
await self.tcp_send(b"AG")
t = 0
while not self.down_rw[0]:
await asyncio.sleep(0.1)
t += 1
if t > 50:
await self.kick("Missing download socket")
return
self.log.info(f"Requested mode: {file!r}")
self.log.debug(f"Mode size: {size/MB}")
msize = math.floor(size / 2)
# uploads = [
# asyncio.create_task(self._split_load(0, msize, False, file)), # SplitLoad_0
# asyncio.create_task(self._split_load(msize, size, True, file)) # SplitLoad_1
# ]
# await asyncio.wait(uploads)
uploads = [
self._split_load(0, msize, False, file),
self._split_load(msize, size, True, file)
]
sl0, sl1 = await asyncio.gather(*uploads)
sent = sl0 + sl1
ok = sent == size
lost = size - sent
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
self.log.debug(f"SplitLoad_0: {sl0/MB}mb; "
f"SplitLoad_1: {sl1/MB}MB; At all ({ok}): Sent: {sent/MB}mb; Lost: {lost/MB}mb")
if not ok:
self.alive = False
self.log.error(f"Error while sending.")
return
elif data.startswith(b"SR"):
path_list = ''
size_list = ''
for mod in self.Core.mods_list:
if type(mod) == int:
continue
path_list += f"{mod['path']};"
size_list += f"{mod['size']};"
mod_list = path_list + size_list
self.log.debug(f"Mods List: {mod_list}")
if len(mod_list) == 0:
await self.tcp_send(b"-")
else:
await self.tcp_send(bytes(mod_list, "utf-8"))
elif data == b"Done":
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
break
return
async def looper(self):
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID
await self.sync_resources()
while self.alive:
data = await self.recv()
if data == b"":
if not self.alive:
break
else:
await asyncio.sleep(.2)
self.is_disconnected()
continue
code = data.decode()[0]
self.log.debug(f"Received code: {code}, data: {data}")
match code:
case "H":
# Client connected
self.ready = True
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"), to_all=True)
case "C":
# Chat
await self.tcp_send(data, to_all=True)
async def remove_me(self):
await asyncio.sleep(0.3)
self.alive = False
if (self.cid > 0 or self.nick is not None) and \
self.Core.clients_by_nick.get(self.nick):
# if self.ready:
# await self.tcp_send(b"", to_all=True) # I'm disconnected.
self.log.debug(f"Removing client {self.nick}:{self.cid}")
self.log.info("Disconnected")
self.Core.clients[self.cid] = None
self.Core.clients_by_id.pop(self.cid)
self.Core.clients_by_nick.pop(self.nick)
else:
self.log.debug(f"Removing client; Closing connection...")
if not self.writer.is_closing():
self.writer.close()
_, down_w = self.down_rw
if down_w and not down_w.is_closing():
down_w.close()
class Core: class Core:
def __init__(self): def __init__(self):
@ -397,6 +121,7 @@ class Core:
async def heartbeat(self, test=False): async def heartbeat(self, test=False):
if config.Auth["private"] or self.direct: if config.Auth["private"] or self.direct:
if test: if test:
# TODO: i18n
self.log.info(f"Server runnig in Direct connect mode.") self.log.info(f"Server runnig in Direct connect mode.")
self.direct = True self.direct = True
return return
@ -447,6 +172,7 @@ class Core:
status = body.get("status") status = body.get("status")
msg = body.get("msg") msg = body.get("msg")
if status == "2000": if status == "2000":
# TODO: i18n
self.log.info(f"Authenticated! {msg}") self.log.info(f"Authenticated! {msg}")
elif status == "200": elif status == "200":
self.log.info(f"Resumed authenticated session. {msg}") self.log.info(f"Resumed authenticated session. {msg}")
@ -462,6 +188,7 @@ class Core:
if not config.Auth['private']: if not config.Auth['private']:
raise KeyboardInterrupt raise KeyboardInterrupt
if test: if test:
# TODO: i18n
self.log.info(f"Server still runnig, but only in Direct connect mode.") self.log.info(f"Server still runnig, but only in Direct connect mode.")
if test: if test:
@ -502,24 +229,24 @@ class Core:
self.mods_list.append({"path": path, "size": size}) self.mods_list.append({"path": path, "size": size})
self.mods_list[0] += size self.mods_list[0] += size
self.log.debug(f"mods_list: {self.mods_list}") self.log.debug(f"mods_list: {self.mods_list}")
lmods = len(self.mods_list) - 1 len_mods = len(self.mods_list) - 1
if lmods > 0: if len_mods > 0:
self.log.info(f"Loaded {lmods} mods: {round(self.mods_list[0] / MB, 2)}mb") # TODO: i18n
self.log.info(f"Loaded {len_mods} mods: {round(self.mods_list[0] / MB, 2)}mb")
await self.heartbeat(True) await self.heartbeat(True)
for i in range(int(config.Game["players"] * 1.3)): for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
self.clients.append(None) self.clients.append(None)
tasks = [] tasks = []
# self.udp.start, # self.udp.start,
nrtasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive] f_tasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
for task in nrtasks: for task in f_tasks:
tasks.append(asyncio.create_task(task())) tasks.append(asyncio.create_task(task()))
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
self.log.info(i18n.start) self.log.info(i18n.start)
ev.call_event("on_started") ev.call_event("on_started")
await t await t # Wait end.
# Wait the end.
except Exception as e: except Exception as e:
self.log.error(f"Exception: {e}") self.log.error(f"Exception: {e}")
self.log.exception(e) self.log.exception(e)

View File

@ -5,42 +5,15 @@
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
from asyncio import StreamWriter, StreamReader
from threading import Thread from threading import Thread
from typing import Callable, List, Dict, Tuple from typing import Callable, List, Dict
from core import utils from core import utils
from .Client import Client
from .tcp_server import TCPServer from .tcp_server import TCPServer
from .udp_server import UDPServer from .udp_server import UDPServer
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self.reader = reader
self.writer = writer
self.down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
self.log = utils.get_logger("client(id: )")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid: int = -1
self.key: str = None
self.nick: str = None
self.roles: str = None
self.guest = True
self.alive = True
self.ready = False
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def tcp_send(self, data: bytes, to_all:bool = False, writer: StreamWriter = None) -> None: ...
async def sync_resources(self) -> None: ...
async def recv(self) -> bytes: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str) -> None: ...
async def looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def remove_me(self) -> None: ...
class Core: class Core:
def __init__(self): def __init__(self):
self.log = utils.get_logger("core") self.log = utils.get_logger("core")

View File

@ -23,10 +23,12 @@ class TCPServer:
async def auth_client(self, reader, writer): async def auth_client(self, reader, writer):
client = self.Core.create_client(reader, writer) client = self.Core.create_client(reader, writer)
# TODO: i18n
self.log.info(f"Identifying new ClientConnection...") self.log.info(f"Identifying new ClientConnection...")
data = await client.recv() data = await client.recv()
self.log.debug(f"Version: {data}") self.log.debug(f"Version: {data}")
if data.decode("utf-8") != f"VC{self.Core.client_major_version}": if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
# TODO: i18n
await client.kick("Outdated Version.") await client.kick("Outdated Version.")
return False, client return False, client
else: else:
@ -35,6 +37,7 @@ class TCPServer:
data = await client.recv() data = await client.recv()
self.log.debug(f"Key: {data}") self.log.debug(f"Key: {data}")
if len(data) > 50: if len(data) > 50:
# TODO: i18n
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")
@ -46,6 +49,7 @@ class TCPServer:
res = await response.json() res = await response.json()
self.log.debug(f"res: {res}") self.log.debug(f"res: {res}")
if res.get("error"): if res.get("error"):
# TODO: i18n
await client.kick('Invalid key! Please restart your game.') await client.kick('Invalid key! Please restart your game.')
return False, client return False, client
client.nick = res["username"] client.nick = res["username"]
@ -54,6 +58,7 @@ class TCPServer:
# noinspection PyProtectedMember # noinspection PyProtectedMember
client._update_logger() client._update_logger()
except Exception as e: except Exception as e:
# TODO: i18n
self.log.error(f"Auth error: {e}") self.log.error(f"Auth error: {e}")
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.') await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
return False, client return False, client
@ -62,15 +67,18 @@ class TCPServer:
if not _client: if not _client:
continue continue
if _client.nick == client.nick and _client.guest == client.guest: if _client.nick == client.nick and _client.guest == client.guest:
# TODO: i18n
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", client) ev.call_event("auth_ok", client)
if len(self.Core.clients_by_id) > config.Game["players"]: if len(self.Core.clients_by_id) > config.Game["players"]:
# TODO: i18n
await client.kick("Server full!") await client.kick("Server full!")
return False, client return False, client
else: else:
# TODO: i18n
self.log.info("Identification success") self.log.info("Identification success")
await self.Core.insert_client(client) await self.Core.insert_client(client)
@ -103,6 +111,7 @@ class TCPServer:
await writer.drain() await writer.drain()
writer.close() writer.close()
case _: case _:
# TODO: i18n
self.log.error(f"Unknown code: {code}") self.log.error(f"Unknown code: {code}")
writer.close() writer.close()
return False, None return False, None
@ -123,6 +132,7 @@ class TCPServer:
del cl del cl
break break
except Exception as e: except Exception as e:
# TODO: i18n
self.log.error("Error while connecting..") self.log.error("Error while connecting..")
self.log.exception(e) self.log.exception(e)
traceback.print_exc() traceback.print_exc()
@ -139,6 +149,7 @@ class TCPServer:
async with server: async with server:
await server.serve_forever() await server.serve_forever()
except OSError as e: except OSError as e:
# TODO: i18n
self.log.error("Cannot bind port") self.log.error("Cannot bind port")
raise e raise e
except BaseException as e: except BaseException as e:

View File

@ -3,13 +3,16 @@ import builtins
from core import get_logger from core import get_logger
# noinspection PyShadowingBuiltins
class EventsSystem: class EventsSystem:
def __init__(self): def __init__(self):
# TODO: default events
self.__events = { self.__events = {
"on_started": [self.on_started], "on_started": [],
"on_stop": [self.on_stop], "auth_sent_key": [],
"on_auth": [self.on_auth] "auth_ok": [],
"chat_receive": [],
} }
self.log = get_logger("EventsSystem") self.log = get_logger("EventsSystem")
@ -20,6 +23,7 @@ class EventsSystem:
def register_event(self, event_name, event_func): def register_event(self, event_name, event_func):
self.log.debug(f"register_event({event_name}, {event_func}):") self.log.debug(f"register_event({event_name}, {event_func}):")
if not callable(event_func): if not callable(event_func):
# TODO: i18n
self.log.error(f"Cannot add event '{event_name}'. " self.log.error(f"Cannot add event '{event_name}'. "
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...") f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
return return
@ -28,19 +32,20 @@ class EventsSystem:
else: else:
self.__events[event_name].append(event_func) self.__events[event_name].append(event_func)
def call_event(self, event_name, *data): def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Using event '{event_name}'") self.log.debug(f"Using event '{event_name}'")
funcs_data = []
if event_name in self.__events.keys(): if event_name in self.__events.keys():
for event in self.__events[event_name]: for func in self.__events[event_name]:
event(*data) try:
funcs_data.append(func({"event_name": event_name, "args": args, "kwargs": kwargs}))
except Exception as e:
# TODO: i18n
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
self.log.exception(e)
else: else:
# TODO: i18n
self.log.warning(f"Event {event_name} does not exist. Just skipping it...") self.log.warning(f"Event {event_name} does not exist. Just skipping it...")
def on_started(self): return funcs_data
pass
def on_stop(self):
pass
def on_auth(self, client):
pass

View File

@ -1,19 +1,55 @@
import os import os
import types import types
from contextlib import contextmanager
from core import get_logger from core import get_logger
# TODO: call_client_event, get_player, get_players, GetPlayerCount
class KuiToi: class KuiToi:
_plugins_dir = ""
def __init__(self, name=None): def __init__(self, name=None):
if name is None: if name is None:
raise Exception("BeamMP: Name is required") raise AttributeError("KuiToi: Name is required")
self.log = get_logger(f"PluginsLoader | {name}") self.log = get_logger(f"Plugin | {name}")
self.name = name self.__name = name
self.__dir = os.path.join(self._plugins_dir, self.__name)
if not os.path.exists(self.__dir):
os.mkdir(self.__dir)
def set_name(self, name): @property
self.name = name def name(self):
return self.__name
@name.setter
def name(self, value):
# You chell not pass
pass
@property
def dir(self):
return self.__dir
@dir.setter
def dir(self, value):
# You chell not pass
pass
@contextmanager
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)
if not os.path.exists(path):
with open(path, 'x'): ...
f = None
try:
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
yield f
except Exception as e:
raise e
finally:
if f is not None:
f.close()
@staticmethod @staticmethod
def register_event(event_name, event_func): def register_event(event_name, event_func):
@ -27,26 +63,38 @@ class KuiToi:
class PluginsLoader: class PluginsLoader:
def __init__(self, plugins_dir): def __init__(self, plugins_dir):
self.__plugins = {} self.plugins = {}
self.__plugins_dir = plugins_dir self.plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader") self.log = get_logger("PluginsLoader")
def load_plugins(self): def load_plugins(self):
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"): if file.endswith(".py"):
try: try:
self.log.debug(f"Loading plugin: {file}") self.log.debug(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType('plugin') plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.kt = None
plugin.print = print plugin.print = print
file = os.path.join(self.__plugins_dir, file) file_path = os.path.join(self.plugins_dir, file)
with open(f'{file}', 'r') as f: plugin.__file__ = file_path
code = f.read().replace("import KuiToi\n", "") with open(f'{file_path}', 'r') as f:
code = f.read()
exec(code, plugin.__dict__) exec(code, plugin.__dict__)
if type(plugin.kt) != KuiToi:
raise AttributeError(f'Attribute "kt" isn\'t KuiToi class. Plugin file: "{file_path}"')
pl_name = plugin.kt.name
if self.plugins.get(pl_name) is not None:
raise NameError(f'Having plugins with identical names is not allowed; '
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
plugin.open = plugin.kt.open
plugin.load() plugin.load()
self.__plugins.update({file[:-3]: plugin}) self.plugins.update({pl_name: plugin})
self.log.debug(f"Plugin loaded: {file}") self.log.debug(f"Plugin loaded: {file}")
except Exception as e: except Exception as e:
self.log.error(f"Error loading plugin: {file}; Error: {e}") # TODO: i18n
self.log.error(f"Error while loading plugin: {file}; Error: {e}")
self.log.exception(e)