[~] 0.4.5 > 0.4.6

[+] Recreate config (if empty)
[+] Hidden config
[!] FIX rl
[!] Update getting loop
[-] core_player_set_id
[~] Update copyrights
[~] Minor
This commit is contained in:
2024-07-16 17:18:49 +03:00
parent 2eb7e8801b
commit a923dbea1f
13 changed files with 136 additions and 103 deletions

View File

@@ -19,7 +19,7 @@ class Client:
def __init__(self, reader, writer, core):
self.__reader = reader
self.__writer = writer
self.__Core = core
self._core = core
self.__alive = True
self.__packets_queue = []
self.__tasks = []
@@ -38,7 +38,7 @@ class Client:
self._identifiers = []
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
self._focus_car = -1
self._snowman = {"id": -1, "packet": ""}
self._unicycle = {"id": -1, "packet": ""}
self._connect_time = 0
self._last_position = {}
self._lock = Lock()
@@ -155,7 +155,7 @@ class Client:
if to_all:
code = chr(data[0])
for client in self.__Core.clients:
for client in self._core.clients:
if not client or (client is self and not to_self):
continue
if not to_udp or code in ['V', 'W', 'Y', 'E']:
@@ -174,8 +174,7 @@ class Client:
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
if to_udp:
udp_sock = self._udp_sock[0]
udp_addr = self._udp_sock[1]
udp_sock, udp_addr = self._udp_sock
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
if udp_sock and udp_addr:
try:
@@ -297,7 +296,7 @@ class Client:
file = data[1:].decode(config.enc)
self.log.info(i18n.client_mod_request.format(repr(file)))
size = -1
for mod in self.__Core.mods_list:
for mod in self._core.mods_list:
if type(mod) == int:
continue
if mod.get('path') == file:
@@ -318,9 +317,9 @@ class Client:
await self.kick("Missing download socket")
return
if config.Options['use_queue']:
while self.__Core.lock_upload:
while self._core.lock_upload:
await asyncio.sleep(.2)
self.__Core.lock_upload = True
self._core.lock_upload = True
speed = config.Options["speed_limit"]
if speed:
speed = speed / 2
@@ -332,8 +331,8 @@ class Client:
]
sl0, sl1 = await asyncio.gather(*uploads)
tr = (time.monotonic() - t) or 0.0001
if self.__Core.lock_upload:
self.__Core.lock_upload = False
if self._core.lock_upload:
self._core.lock_upload = False
msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
if speed:
msg += i18n.client_mod_sent_limit.format(int(speed * 2))
@@ -349,7 +348,7 @@ class Client:
elif data.startswith(b"SR"):
path_list = ''
size_list = ''
for mod in self.__Core.mods_list:
for mod in self._core.mods_list:
if type(mod) == int:
continue
path_list += f"{mod['path']};"
@@ -391,7 +390,7 @@ class Client:
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:
if self._unicycle['id'] != -1:
cars_count -= 1 # -1 for unicycle
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
car_json = {}
@@ -416,12 +415,12 @@ class Client:
snowman = car_json.get("jbm") == "unicycle"
if allow and config.Game['cars'] > cars_count or (snowman and allow_snowman) or over_spawn:
if snowman:
unicycle_id = self._snowman['id']
unicycle_id = self._unicycle['id']
if unicycle_id != -1:
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}")
self.log.debug(f"Delete old unicycle: car_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._unicycle = {"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}")
@@ -469,8 +468,8 @@ class Client:
car = self._cars[car_id]
if car['snowman']:
self.log.debug(f"Snowman found")
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
unicycle_id = self._unicycle['id']
self._unicycle['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)
@@ -482,7 +481,7 @@ class Client:
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)
client = self._core.get_client(cid=cid)
if client:
car = client._cars[car_id]
new_car_json = {}
@@ -506,8 +505,8 @@ class Client:
if cid == self.cid or allow or admin_allow:
if car['snowman']:
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
unicycle_id = self._unicycle['id']
self._unicycle['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
@@ -593,7 +592,7 @@ class Client:
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
await self._send(f"J{i18n.game_welcome_message.format(self.nick)}", to_all=True) # Hello message
for client in self.__Core.clients:
for client in self._core.clients:
if not client:
continue
for car in client._cars:
@@ -656,20 +655,18 @@ class Client:
self.__alive = False
return
# Codes: V W X Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
return
_bytes = False
try:
data = data.decode()
except UnicodeDecodeError:
_bytes = True
self.log.error(f"UnicodeDecodeError: {data}")
self.log.info("Some things are skipping...")
# Codes: p, Z in udp_server.py
if data[0] in ['V', 'W', 'Y', 'E', 'N']:
await self._send(data, to_all=True, to_self=False)
return
# Codes: p, Z, X in udp_server.py
match data[0]: # At data[0] code
case "H": # Map load, client ready
await self._connected_handler()
@@ -700,8 +697,8 @@ class Client:
ev.call_lua_event(event_name, self.cid, even_data)
ev.call_event(event_name, data=even_data, player=self)
await ev.call_async_event(event_name, data=even_data, player=self)
case "N":
await self._send(data, to_all=True, to_self=False)
case _:
self.log.warning(f"TCP [{self.cid}] Unknown code: {data[0]}; {data}")
async def _looper(self):
ev.call_lua_event("onPlayerConnecting", self.cid)
@@ -728,7 +725,7 @@ class Client:
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):
self._core.clients_by_nick.get(self.nick):
for i, car in enumerate(self._cars):
if not car:
continue
@@ -746,9 +743,9 @@ class Client:
round((time.monotonic() - self._connect_time) / 60, 2)
)
)
self.__Core.clients[self.cid] = None
del self.__Core.clients_by_id[self.cid]
del self.__Core.clients_by_nick[self.nick]
self._core.clients[self.cid] = None
del self._core.clients_by_id[self.cid]
del self._core.clients_by_nick[self.nick]
else:
self.log.debug(f"Removing client; Closing connection...")
try:

View File

@@ -25,7 +25,7 @@ class Client:
self._log = utils.get_logger("client(id: )")
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
self._loop = asyncio.get_event_loop()
self.__Core: Core = core
self._core: Core = core
self._cid: int = -1
self._key: str = None
self.nick: str = None
@@ -37,7 +37,7 @@ class Client:
self._focus_car = -1
self._identifiers = []
self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = []
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
self._unicycle: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
self._last_position = {}
self._lock = Lock()

View File

@@ -9,12 +9,12 @@
__title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.4.5'
__build__ = 2303 # Я это считаю лог файлами
__version__ = '0.4.6'
__build__ = 2421 # Я это считаю лог файлами
__author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su'
__license__ = "FPA"
__copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
__copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)'
import asyncio
import builtins
@@ -43,7 +43,7 @@ config_path = "kuitoi.yml"
if args.config:
config_path = args.config
config_provider = ConfigProvider(config_path)
config = config_provider.open_config()
config = config_provider.read()
builtins.config = config
config.enc = config.Options['encoding']
if config.Options['debug'] is True:
@@ -60,6 +60,7 @@ ml.builtins_hook()
log.debug("Initializing EventsSystem...")
ev = EventsSystem()
ev.builtins_hook()
ev.register("get_version", lambda _: {"version": __version__, "build": __build__})
log.info(i18n.hello)
log.info(i18n.config_path.format(config_path))
@@ -68,7 +69,7 @@ log.debug("Initializing BeamMP Server system...")
# Key handler..
if not config.Auth['private'] and not config.Auth['key']:
log.warn(i18n.auth_need_key)
url = "https://beammp.com/k/keys"
url = "https://keymaster.beammp.com/login"
if shortcuts.yes_no_dialog(
title='BeamMP Server Key',
text=i18n.GUI_need_key_message,
@@ -90,7 +91,7 @@ if not config.Auth['private'] and not config.Auth['key']:
text=i18n.GUI_enter_key_message,
ok_text=i18n.GUI_ok,
cancel_text=i18n.GUI_cancel).run()
config_provider.save_config()
config_provider.save()
if not config.Auth['private'] and not config.Auth['key']:
log.error(i18n.auth_empty_key)
log.info(i18n.stop)

View File

@@ -27,7 +27,8 @@ class Core:
def __init__(self):
self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.start_time = time.monotonic()
self.run = False
self.direct = False
@@ -47,7 +48,7 @@ class Core:
self.lock_upload = False
self.client_major_version = "2.0"
self.BeamMP_version = "3.4.1" # 20.07.2023
self.BeamMP_version = "3.4.1" # 16.07.2024
ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
ev.register("_get_player", lambda x: self.get_client(**x['kwargs']))
@@ -103,6 +104,7 @@ class Core:
return out
async def check_alive(self):
self.log.debug("Starting alive checker.")
maxp = config.Game['players']
try:
while self.run:
@@ -138,6 +140,8 @@ class Core:
uvserver.run()
async def stop_me(self):
if not config.WebAPI['enabled']:
return
while webapp.data_run[0]:
await asyncio.sleep(1)
self.run = False
@@ -145,31 +149,44 @@ class Core:
# noinspection SpellCheckingInspection,PyPep8Naming
async def heartbeat(self, test=False):
if config.Auth["private"] or self.direct:
if test:
self.log.info(i18n.core_direct_mode)
self.direct = True
return
try:
self.log.debug("Starting heartbeat.")
if config.Auth["private"] or self.direct:
if test:
self.log.info(i18n.core_direct_mode)
self.direct = True
return
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
modlist = ""
for mod in self.mods_list:
if type(mod) == int:
continue
modlist += f"/{os.path.basename(mod['path'])};"
modstotalsize = self.mods_list[0]
modstotal = len(self.mods_list) - 1
while self.run:
try:
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id),
"maxplayers": config.Game["players"], "port": config.Server["server_port"],
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'],
"version": self.BeamMP_version, "clientversion": self.client_major_version,
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
_map = config.Game['map'] if "/" in config.Game['map'] else f"/levels/{config.Game['map']}/info.json"
tags = config.Server['tags'].replace(", ", ";").replace(",", ";")
if tags and tags[-1:] != ";":
tags += ";"
modlist = "".join(f"/{os.path.basename(mod['path'])};" for mod in self.mods_list[1:])
modstotalsize = self.mods_list[0]
modstotal = len(self.mods_list) - 1
while self.run:
playerslist = "".join(f"{client.nick};" for client in self.clients if client and client.alive)
data = {
"uuid": config.Auth["key"],
"players": len(self.clients_by_id),
"maxplayers": config.Game["players"],
"port": config.Server["server_port"],
"map": _map,
"private": config.Auth['private'],
"version": self.BeamMP_version,
"clientversion": self.client_major_version,
"name": config.Server["name"],
"tags": tags,
"guests": not config.Auth["private"],
"modlist": modlist,
"modstotalsize": modstotalsize,
"modstotal": modstotal,
"playerslist": playerslist,
"desc": config.Server['description'],
"pass": False
}
# Sentry?
ok = False
body = {}
for server_url in BEAM_backend:
url = "https://" + server_url + "/heartbeat"
@@ -177,14 +194,15 @@ class Core:
async with aiohttp.ClientSession() as session:
async with session.post(url, data=data, headers={"api-v": "2"}) as response:
code = response.status
# text = await response.text()
# self.log.debug(f"[HB] res={text}")
body = await response.json()
ok = True
break
except Exception as e:
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
continue
if ok:
if body:
if not (body.get("status") is not None and
body.get("code") is not None and
body.get("msg") is not None):
@@ -216,11 +234,11 @@ class Core:
# raise KeyboardInterrupt
if test:
return ok
return bool(body)
await asyncio.sleep(5)
except Exception as e:
self.log.error(f"Error in heartbeat: {e}")
except Exception as e:
self.log.error(f"Error in heartbeat: {e}")
async def kick_cmd(self, args):
if not len(args) > 0:

View File

@@ -94,9 +94,8 @@ class TCPServer:
await client.kick(i18n.core_player_kick_server_full)
return False, client
else:
self.log.info(i18n.core_identifying_okay)
await self.Core.insert_client(client)
client.log.info(i18n.core_player_set_id.format(client.pid))
client.log.info(i18n.core_identifying_okay)
return True, client
@@ -137,7 +136,8 @@ class TCPServer:
try:
ip = writer.get_extra_info('peername')[0]
if self.rl.is_banned(ip):
self.rl.notify(ip, writer)
await self.rl.notify(ip, writer)
writer.close()
break
data = await reader.read(1)
if not data:

View File

@@ -18,7 +18,7 @@ class UDPServer(asyncio.DatagramTransport):
super().__init__()
self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop()
self.Core = core
self._core = core
self.host = host
self.port = port
self.run = False
@@ -33,8 +33,10 @@ class UDPServer(asyncio.DatagramTransport):
code = data[2:3].decode()
data = data[2:].decode()
client = self.Core.get_client(cid=cid)
client = self._core.get_client(cid=cid)
if client:
if not client.alive:
self.log.debug(f"{client.nick}:{cid} still sending UDP data: {data}")
match code:
case "p": # Ping packet
ev.call_event("onSentPing")
@@ -45,24 +47,26 @@ class UDPServer(asyncio.DatagramTransport):
self.log.debug(f"Set UDP Sock for CID: {cid}")
ev.call_event("onChangePosition", data=data)
sub = data.find("{", 1)
last_pos_data = data[sub:]
last_pos = data[sub:]
try:
last_pos = json.loads(last_pos_data)
client._last_position = last_pos
_, car_id = client._get_cid_vid(data)
client._cars[car_id]['pos'] = last_pos
if client._cars[car_id]:
last_pos = json.loads(last_pos)
client._last_position = last_pos
client._cars[car_id]['pos'] = last_pos
except Exception as e:
self.log.debug(f"Cannot parse position packet: {e}")
self.log.debug(f"data: {data}, sup: {sub}")
self.log.debug(f"last_pos_data: {last_pos_data}")
self.log.warning(f"Cannot parse position packet: {e}")
self.log.debug(f"data: '{data}', sup: {sub}")
self.log.debug(f"last_pos ({type(last_pos)}): {last_pos}")
await client._send(data, to_all=True, to_self=False, to_udp=True)
case "X":
await client._send(data, to_all=True, to_self=False, to_udp=True)
case _:
self.log.debug(f"[{cid}] Unknown code: {code}")
self.log.warning(f" UDP [{cid}] Unknown code: {code}; {data}")
else:
self.log.debug(f"[{cid}] Client not found.")
except Exception as e:
self.log.error(f"Error handle_datagram: {e}")
def datagram_received(self, *args, **kwargs):
@@ -81,14 +85,14 @@ class UDPServer(asyncio.DatagramTransport):
async def _start(self):
self.log.debug("Starting UDP server.")
while self.Core.run:
while self._core.run:
try:
await asyncio.sleep(0.2)
d = UDPServer
self.transport, p = await self.loop.create_datagram_endpoint(
lambda: d(self.Core),
lambda: d(self._core),
local_addr=(self.host, self.port)
)
d.transport = self.transport

View File

@@ -18,7 +18,7 @@ class UDPServer(asyncio.DatagramTransport):
def __init__(self, core: Core, host=None, port=None, transport=None):
self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop()
self.Core = core
self._core = core
self.host = host
self.port = port
self.run = False

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import copy
# Developed by KuiToi Dev
# File modules.ConfigProvider
# Written by: SantaSpeen
@@ -16,12 +16,12 @@ class Config:
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "tags": "Freroam",
"server_ip": "0.0.0.0", "server_port": 30814}
self.Options = options or {"language": "en", "speed_limit": 0, "use_queue": False,
"use_lua": False, "log_chat": True}
self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383,
"password": secrets.token_hex(16)}
self.Options = options or {"language": "en", "encoding": "utf-8", "speed_limit": 0, "use_queue": False,
"debug": False, "use_lua": False, "log_chat": True}
"password": secrets.token_hex(6)}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"access_token": secrets.token_hex(16)}
@@ -36,7 +36,7 @@ class ConfigProvider:
self.config_path = config_path
self.config = Config()
def open_config(self):
def read(self, _again=False):
if not os.path.exists(self.config_path):
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.config, f)
@@ -47,9 +47,26 @@ class ConfigProvider:
print("You have errors in the YAML syntax.")
print("Stopping server.")
exit(1)
if not self.config:
if _again:
print("Error: empty configuration.")
exit(1)
print("Empty config?..")
os.remove(self.config_path)
self.config = Config()
return self.read(True)
if not self.config.Options.get("debug"):
self.config.Options['debug'] = False
if not self.config.Options.get("encoding"):
self.config.Options['encoding'] = "utf-8"
return self.config
def save_config(self):
def save(self):
_config = copy.deepcopy(self.config)
del _config.enc
del _config.Options['debug']
del _config.Options['encoding']
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.config, f)
yaml.dump(_config, f)

View File

@@ -40,6 +40,7 @@ class RateLimiter:
if len(x) == 2:
ip = x[1]
if ip in _banned_ips:
self._notified[ip] = False
self._calls[ip].clear()
self._banned_until[ip] = datetime.now()
return f"{ip} removed from banlist."
@@ -52,6 +53,7 @@ class RateLimiter:
sec = x[2]
if not sec.isdigit():
return f"{sec!r} is not digit."
self._notified[ip] = False
self._calls[ip].clear()
self._banned_until[ip] = datetime.now() + timedelta(seconds=int(sec))
return f"{ip} banned until {self._banned_until[ip]}"
@@ -69,7 +71,6 @@ class RateLimiter:
try:
writer.write(b'\x0b\x00\x00\x00Eip banned.')
await writer.drain()
writer.close()
except Exception:
pass

View File

@@ -45,7 +45,6 @@ class i18n:
core_player_kick_stale: str
core_player_kick_no_allowed_default_reason: str
core_player_kick_server_full: str
core_player_set_id: str
core_identifying_okay: str
# In-game phrases
@@ -110,5 +109,4 @@ class i18n:
# Command: exit
man_message_exit: str
help_message_exit: str
```

View File

@@ -41,7 +41,6 @@
"core_player_kick_stale": "过时的客户端。(由新连接替换)",
"core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。",
"core_player_kick_server_full": "服务器已满。",
"core_player_set_id": "玩家设置ID {}",
"core_identifying_okay": "成功登录。",
"": "游戏内短语",

View File

@@ -41,7 +41,6 @@
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
"core_player_kick_server_full": "Server is full.",
"core_player_set_id": "Player set ID {}",
"core_identifying_okay": "Successful login.",
"": "In-game phrases",

View File

@@ -41,7 +41,6 @@
"core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)",
"core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.",
"core_player_kick_server_full": "Сервер полон.",
"core_player_set_id": "Игрок получил ID {}",
"core_identifying_okay": "Успешный вход.",
"": "In-game phrases",