mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2025-08-17 08:15:42 +00:00
Compare commits
7 Commits
a7a9f367c5
...
a5202edf83
Author | SHA1 | Date | |
---|---|---|---|
a5202edf83 | |||
64ce662d04 | |||
08e4f0fcba | |||
99f40eadb0 | |||
5a40ab8b05 | |||
4f688d7c02 | |||
c4fe201b86 |
2
LICENCE
2
LICENCE
@ -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:
|
||||
|
||||
- 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.
|
@ -40,8 +40,9 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
|
||||
- [x] Events System
|
||||
- [x] Call events
|
||||
- [x] Create custom events
|
||||
- [ ] Return from events
|
||||
- [x] Return from events
|
||||
- [x] Plugins support
|
||||
- [ ] KuiToi class
|
||||
- [x] Load Python plugins
|
||||
- [ ] Load Lua plugins (Original BeamMP compatibility)
|
||||
- [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:
|
||||
- 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:
|
||||
- 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.
|
||||
```
|
||||
|
245
src/core/Client.py
Normal file
245
src/core/Client.py
Normal 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
33
src/core/Client.pyi
Normal 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: ...
|
297
src/core/core.py
297
src/core/core.py
@ -5,296 +5,20 @@
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import zlib
|
||||
from threading import Thread
|
||||
|
||||
import aiohttp
|
||||
import uvicorn
|
||||
|
||||
from core import utils
|
||||
from core.Client import Client
|
||||
from core.tcp_server import TCPServer
|
||||
from core.udp_server import UDPServer
|
||||
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:
|
||||
|
||||
def __init__(self):
|
||||
@ -397,6 +121,7 @@ class Core:
|
||||
async def heartbeat(self, test=False):
|
||||
if config.Auth["private"] or self.direct:
|
||||
if test:
|
||||
# TODO: i18n
|
||||
self.log.info(f"Server runnig in Direct connect mode.")
|
||||
self.direct = True
|
||||
return
|
||||
@ -447,6 +172,7 @@ class Core:
|
||||
status = body.get("status")
|
||||
msg = body.get("msg")
|
||||
if status == "2000":
|
||||
# TODO: i18n
|
||||
self.log.info(f"Authenticated! {msg}")
|
||||
elif status == "200":
|
||||
self.log.info(f"Resumed authenticated session. {msg}")
|
||||
@ -462,6 +188,7 @@ class Core:
|
||||
if not config.Auth['private']:
|
||||
raise KeyboardInterrupt
|
||||
if test:
|
||||
# TODO: i18n
|
||||
self.log.info(f"Server still runnig, but only in Direct connect mode.")
|
||||
|
||||
if test:
|
||||
@ -502,24 +229,24 @@ class Core:
|
||||
self.mods_list.append({"path": path, "size": size})
|
||||
self.mods_list[0] += size
|
||||
self.log.debug(f"mods_list: {self.mods_list}")
|
||||
lmods = len(self.mods_list) - 1
|
||||
if lmods > 0:
|
||||
self.log.info(f"Loaded {lmods} mods: {round(self.mods_list[0] / MB, 2)}mb")
|
||||
len_mods = len(self.mods_list) - 1
|
||||
if len_mods > 0:
|
||||
# TODO: i18n
|
||||
self.log.info(f"Loaded {len_mods} mods: {round(self.mods_list[0] / MB, 2)}mb")
|
||||
|
||||
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)
|
||||
tasks = []
|
||||
# self.udp.start,
|
||||
nrtasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
||||
for task in nrtasks:
|
||||
f_tasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
||||
for task in f_tasks:
|
||||
tasks.append(asyncio.create_task(task()))
|
||||
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
||||
|
||||
self.log.info(i18n.start)
|
||||
ev.call_event("on_started")
|
||||
await t
|
||||
# Wait the end.
|
||||
await t # Wait end.
|
||||
except Exception as e:
|
||||
self.log.error(f"Exception: {e}")
|
||||
self.log.exception(e)
|
||||
|
@ -5,42 +5,15 @@
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import StreamWriter, StreamReader
|
||||
from threading import Thread
|
||||
from typing import Callable, List, Dict, Tuple
|
||||
from typing import Callable, List, Dict
|
||||
|
||||
from core import utils
|
||||
from .Client import Client
|
||||
from .tcp_server import TCPServer
|
||||
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:
|
||||
def __init__(self):
|
||||
self.log = utils.get_logger("core")
|
||||
|
@ -23,10 +23,12 @@ class TCPServer:
|
||||
|
||||
async def auth_client(self, reader, writer):
|
||||
client = self.Core.create_client(reader, writer)
|
||||
# TODO: i18n
|
||||
self.log.info(f"Identifying new ClientConnection...")
|
||||
data = await client.recv()
|
||||
self.log.debug(f"Version: {data}")
|
||||
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
|
||||
# TODO: i18n
|
||||
await client.kick("Outdated Version.")
|
||||
return False, client
|
||||
else:
|
||||
@ -35,6 +37,7 @@ class TCPServer:
|
||||
data = await client.recv()
|
||||
self.log.debug(f"Key: {data}")
|
||||
if len(data) > 50:
|
||||
# TODO: i18n
|
||||
await client.kick("Invalid Key (too long)!")
|
||||
return False, client
|
||||
client.key = data.decode("utf-8")
|
||||
@ -46,6 +49,7 @@ class TCPServer:
|
||||
res = await response.json()
|
||||
self.log.debug(f"res: {res}")
|
||||
if res.get("error"):
|
||||
# TODO: i18n
|
||||
await client.kick('Invalid key! Please restart your game.')
|
||||
return False, client
|
||||
client.nick = res["username"]
|
||||
@ -54,6 +58,7 @@ class TCPServer:
|
||||
# noinspection PyProtectedMember
|
||||
client._update_logger()
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error(f"Auth error: {e}")
|
||||
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
|
||||
return False, client
|
||||
@ -62,15 +67,18 @@ class TCPServer:
|
||||
if not _client:
|
||||
continue
|
||||
if _client.nick == client.nick and _client.guest == client.guest:
|
||||
# TODO: i18n
|
||||
await client.kick('Stale Client (replaced by new client)')
|
||||
return False, client
|
||||
|
||||
ev.call_event("auth_ok", client)
|
||||
|
||||
if len(self.Core.clients_by_id) > config.Game["players"]:
|
||||
# TODO: i18n
|
||||
await client.kick("Server full!")
|
||||
return False, client
|
||||
else:
|
||||
# TODO: i18n
|
||||
self.log.info("Identification success")
|
||||
await self.Core.insert_client(client)
|
||||
|
||||
@ -103,6 +111,7 @@ class TCPServer:
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
case _:
|
||||
# TODO: i18n
|
||||
self.log.error(f"Unknown code: {code}")
|
||||
writer.close()
|
||||
return False, None
|
||||
@ -123,6 +132,7 @@ class TCPServer:
|
||||
del cl
|
||||
break
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error("Error while connecting..")
|
||||
self.log.exception(e)
|
||||
traceback.print_exc()
|
||||
@ -139,6 +149,7 @@ class TCPServer:
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
except OSError as e:
|
||||
# TODO: i18n
|
||||
self.log.error("Cannot bind port")
|
||||
raise e
|
||||
except BaseException as e:
|
||||
|
@ -3,13 +3,16 @@ import builtins
|
||||
from core import get_logger
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
class EventsSystem:
|
||||
|
||||
def __init__(self):
|
||||
# TODO: default events
|
||||
self.__events = {
|
||||
"on_started": [self.on_started],
|
||||
"on_stop": [self.on_stop],
|
||||
"on_auth": [self.on_auth]
|
||||
"on_started": [],
|
||||
"auth_sent_key": [],
|
||||
"auth_ok": [],
|
||||
"chat_receive": [],
|
||||
}
|
||||
self.log = get_logger("EventsSystem")
|
||||
|
||||
@ -20,6 +23,7 @@ class EventsSystem:
|
||||
def register_event(self, event_name, event_func):
|
||||
self.log.debug(f"register_event({event_name}, {event_func}):")
|
||||
if not callable(event_func):
|
||||
# TODO: i18n
|
||||
self.log.error(f"Cannot add event '{event_name}'. "
|
||||
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
|
||||
return
|
||||
@ -28,19 +32,20 @@ class EventsSystem:
|
||||
else:
|
||||
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}'")
|
||||
funcs_data = []
|
||||
|
||||
if event_name in self.__events.keys():
|
||||
for event in self.__events[event_name]:
|
||||
event(*data)
|
||||
for func in self.__events[event_name]:
|
||||
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:
|
||||
# TODO: i18n
|
||||
self.log.warning(f"Event {event_name} does not exist. Just skipping it...")
|
||||
|
||||
def on_started(self):
|
||||
pass
|
||||
|
||||
def on_stop(self):
|
||||
pass
|
||||
|
||||
def on_auth(self, client):
|
||||
pass
|
||||
return funcs_data
|
||||
|
@ -1,19 +1,55 @@
|
||||
import os
|
||||
import types
|
||||
from contextlib import contextmanager
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
# TODO: call_client_event, get_player, get_players, GetPlayerCount
|
||||
class KuiToi:
|
||||
_plugins_dir = ""
|
||||
|
||||
def __init__(self, name=None):
|
||||
if name is None:
|
||||
raise Exception("BeamMP: Name is required")
|
||||
self.log = get_logger(f"PluginsLoader | {name}")
|
||||
self.name = name
|
||||
raise AttributeError("KuiToi: Name is required")
|
||||
self.log = get_logger(f"Plugin | {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):
|
||||
self.name = name
|
||||
@property
|
||||
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
|
||||
def register_event(event_name, event_func):
|
||||
@ -27,26 +63,38 @@ class KuiToi:
|
||||
class PluginsLoader:
|
||||
|
||||
def __init__(self, plugins_dir):
|
||||
self.__plugins = {}
|
||||
self.__plugins_dir = plugins_dir
|
||||
self.plugins = {}
|
||||
self.plugins_dir = plugins_dir
|
||||
self.log = get_logger("PluginsLoader")
|
||||
|
||||
def load_plugins(self):
|
||||
self.log.debug("Loading plugins...")
|
||||
files = os.listdir(self.__plugins_dir)
|
||||
files = os.listdir(self.plugins_dir)
|
||||
for file in files:
|
||||
if file.endswith(".py"):
|
||||
try:
|
||||
self.log.debug(f"Loading plugin: {file}")
|
||||
plugin = types.ModuleType('plugin')
|
||||
self.log.debug(f"Loading plugin: {file[:-3]}")
|
||||
plugin = types.ModuleType(file[:-3])
|
||||
plugin.KuiToi = KuiToi
|
||||
plugin.KuiToi._plugins_dir = self.plugins_dir
|
||||
plugin.kt = None
|
||||
plugin.print = print
|
||||
file = os.path.join(self.__plugins_dir, file)
|
||||
with open(f'{file}', 'r') as f:
|
||||
code = f.read().replace("import KuiToi\n", "")
|
||||
file_path = os.path.join(self.plugins_dir, file)
|
||||
plugin.__file__ = file_path
|
||||
with open(f'{file_path}', 'r') as f:
|
||||
code = f.read()
|
||||
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()
|
||||
self.__plugins.update({file[:-3]: plugin})
|
||||
self.plugins.update({pl_name: plugin})
|
||||
self.log.debug(f"Plugin loaded: {file}")
|
||||
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user