mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2025-07-02 15:55:29 +00:00
483 lines
19 KiB
Python
483 lines
19 KiB
Python
# Developed by KuiToi Dev
|
|
# File core.core.py
|
|
# Written by: SantaSpeen
|
|
# Version: 0.4.8
|
|
# Licence: FPA
|
|
# (c) kuitoi.su 2023
|
|
import asyncio
|
|
import math
|
|
import os
|
|
import random
|
|
import statistics
|
|
import time
|
|
from collections import deque
|
|
|
|
import aiohttp
|
|
|
|
from core import utils, __version__
|
|
from core.Client import Client
|
|
from core.tcp_server import TCPServer
|
|
from core.udp_server import UDPServer
|
|
from modules import PluginsLoader, PermsSystem
|
|
|
|
|
|
def calc_ticks(ticks, duration):
|
|
while ticks and ticks[0] < time.monotonic() - duration:
|
|
ticks.popleft()
|
|
return len(ticks) / duration
|
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
|
# noinspection PyProtectedMember
|
|
class Core:
|
|
|
|
def __init__(self):
|
|
self.tick_counter = 0
|
|
self.log = utils.get_logger("core")
|
|
self.loop = asyncio.get_event_loop()
|
|
self.start_time = time.monotonic()
|
|
self.run = False
|
|
self.direct = False
|
|
self.clients = []
|
|
self.clients_by_id = {}
|
|
self.clients_by_nick = {}
|
|
self.mods_dir = "./mods"
|
|
self.mods_list = [0, ]
|
|
self.server_ip = config.Server["server_ip"]
|
|
self.server_port = config.Server["server_port"]
|
|
self.tcp = TCPServer
|
|
self.udp = UDPServer
|
|
|
|
self.tcp_pps = 0
|
|
self.udp_pps = 0
|
|
|
|
self.tps = 60
|
|
self.target_tps = 60
|
|
|
|
self.lock_upload = False
|
|
|
|
self.client_major_version = "2.0"
|
|
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']))
|
|
|
|
def get_client(self, cid=None, nick=None, raw=False):
|
|
if raw:
|
|
return self.clients_by_nick
|
|
if (cid, nick) == (None, None):
|
|
return None
|
|
if cid is not None:
|
|
if cid == -1:
|
|
return [i for i in self.clients if i is not None and i.synced]
|
|
return self.clients_by_id.get(cid)
|
|
if nick:
|
|
return self.clients_by_nick.get(nick)
|
|
|
|
async def insert_client(self, client):
|
|
await asyncio.sleep(random.randint(3, 9) * 0.01)
|
|
cid = 0
|
|
for _client in self.clients:
|
|
if not _client:
|
|
break
|
|
if _client.cid == cid:
|
|
cid += 1
|
|
else:
|
|
break
|
|
await asyncio.sleep(random.randint(3, 9) * 0.01)
|
|
if not self.clients[cid]:
|
|
client._cid = cid
|
|
self.clients_by_nick.update({client.nick: client})
|
|
self.log.debug(f"Inserting client: {client.nick}:{client.cid}")
|
|
self.clients_by_id.update({client.cid: client})
|
|
self.clients[client.cid] = client
|
|
# noinspection PyProtectedMember
|
|
client._update_logger()
|
|
return
|
|
await self.insert_client(client)
|
|
|
|
def create_client(self, *args, **kwargs):
|
|
self.log.debug(f"Create client")
|
|
client = Client(core=self, *args, **kwargs)
|
|
return client
|
|
|
|
def get_clients_list(self, need_cid=False):
|
|
out = ""
|
|
for client in self.clients:
|
|
if not client:
|
|
continue
|
|
out += f"{client.nick}"
|
|
if need_cid:
|
|
out += f":{client.cid}"
|
|
out += ","
|
|
if out:
|
|
out = out[:-1]
|
|
return out
|
|
|
|
async def _check_alive(self, _):
|
|
# self.log.debug("alive checker.")
|
|
try:
|
|
for client in self.clients:
|
|
if not client:
|
|
continue
|
|
if not client.ready:
|
|
client.is_disconnected()
|
|
continue
|
|
if not client.alive:
|
|
await client.kick("You are not alive!")
|
|
except Exception as e:
|
|
self.log.error("Error in _check_alive.")
|
|
self.log.exception(e)
|
|
|
|
async def _send_online(self, _):
|
|
try:
|
|
for client in self.clients:
|
|
ca = f"Ss{len(self.clients_by_id)}/{config.Game['players']}:{self.get_clients_list()}"
|
|
if not client or not client.alive:
|
|
continue
|
|
await client._send(ca)
|
|
except Exception as e:
|
|
self.log.error("Error in _send_online.")
|
|
self.log.exception(e)
|
|
|
|
async def __gracefully_kick(self):
|
|
for client in self.clients:
|
|
if not client:
|
|
continue
|
|
await client.kick("Server shutdown!")
|
|
|
|
async def __gracefully_remove(self):
|
|
for client in self.clients:
|
|
if not client:
|
|
continue
|
|
await client._remove_me()
|
|
|
|
# noinspection SpellCheckingInspection,PyPep8Naming
|
|
async def heartbeat(self, test=False):
|
|
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"]
|
|
_map = config.Game['map'] if "/" in config.Game['map'] else f"/levels/{config.Game['map']}/info.json"
|
|
tags = config.Server['tags'].replace(", ", ";").replace(",", ";")
|
|
self.log.debug(f"[heartbeat] {_map=}")
|
|
self.log.debug(f"[heartbeat] {tags=}")
|
|
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
|
|
self.log.debug(f"[heartbeat] {modlist=}")
|
|
self.log.debug(f"[heartbeat] {modstotalsize=}")
|
|
self.log.debug(f"[heartbeat] {modstotal=}")
|
|
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
|
|
}
|
|
|
|
body = {}
|
|
for server_url in BEAM_backend:
|
|
url = "https://" + server_url + "/heartbeat"
|
|
try:
|
|
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()
|
|
break
|
|
except Exception as e:
|
|
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
|
|
continue
|
|
|
|
if body:
|
|
if not (body.get("status") is not None and
|
|
body.get("code") is not None and
|
|
body.get("msg") is not None):
|
|
self.log.error(i18n.core_auth_server_error)
|
|
return
|
|
|
|
status = body.get("status")
|
|
msg = body.get("msg")
|
|
if status == "2000":
|
|
if test:
|
|
self.log.debug(f"Authenticated! {msg}")
|
|
elif status == "200":
|
|
if test:
|
|
self.log.debug(f"Resumed authenticated session. {msg}")
|
|
else:
|
|
self.log.debug(f"Auth: data {data}")
|
|
self.log.debug(f"Auth: code {code}, body {body}")
|
|
|
|
self.log.error(i18n.core_auth_server_refused.format(
|
|
msg or i18n.core_auth_server_refused_no_reason))
|
|
self.log.info(i18n.core_auth_server_refused_direct_node)
|
|
self.direct = True
|
|
else:
|
|
self.direct = True
|
|
if test:
|
|
self.log.error(i18n.core_auth_server_no_response)
|
|
self.log.info(i18n.core_auth_server_refused_direct_node)
|
|
# if not config.Auth['private']:
|
|
# raise KeyboardInterrupt
|
|
|
|
if test:
|
|
return bool(body)
|
|
|
|
await asyncio.sleep(15)
|
|
except Exception as e:
|
|
self.log.error(f"Error in heartbeat: {e}")
|
|
|
|
async def _cmd_kick(self, args):
|
|
if not len(args) > 0:
|
|
return "Usage: kick <nick>|:<id> [reason]\nExamples:\n\tkick admin bad boy\n\tkick :0 bad boy"
|
|
reason = "kicked by console."
|
|
if len(args) > 1:
|
|
reason = " ".join(args[1:])
|
|
cl = args[0]
|
|
if cl.startswith(":") and cl[1:].isdigit():
|
|
client = self.get_client(cid=int(cl[1:]))
|
|
else:
|
|
client = self.get_client(nick=cl)
|
|
if client:
|
|
await client.kick(reason)
|
|
else:
|
|
return "Client not found."
|
|
|
|
async def _useful_ticks(self, _):
|
|
tasks = []
|
|
self.tick_counter += 1
|
|
events = {
|
|
0.5: "serverTick_0.5s",
|
|
1: "serverTick_1s",
|
|
2: "serverTick_2s",
|
|
3: "serverTick_3s",
|
|
4: "serverTick_4s",
|
|
5: "serverTick_5s",
|
|
10: "serverTick_10s",
|
|
30: "serverTick_30s",
|
|
60: "serverTick_60s"
|
|
}
|
|
for interval in sorted(events.keys(), reverse=True):
|
|
if self.tick_counter % (interval * self.target_tps) == 0:
|
|
ev.call_event(events[interval])
|
|
tasks.append(ev.call_async_event(events[interval]))
|
|
await asyncio.gather(*tasks)
|
|
if self.tick_counter == (60 * self.target_tps):
|
|
self.tick_counter = 0
|
|
|
|
def _get_color_tps(self, ticks, d):
|
|
tps = calc_ticks(ticks, d)
|
|
half = self.target_tps // 2
|
|
qw = self.target_tps // 4
|
|
if tps > half + qw:
|
|
return f"<green><b>{tps:.2f}</b></green>"
|
|
elif tps > half:
|
|
return f"<yellow><b>{tps:.2f}</b></yellow>"
|
|
elif half > tps:
|
|
return f"<red><b>{tps:.2f}</b></red>"
|
|
|
|
def _cmd_tps(self, ticks_2s, ticks_5s, ticks_30s, ticks_60s):
|
|
t = ["-, ", "-, ", "-."]
|
|
if len(ticks_5s) > 5 * self.target_tps:
|
|
t[0] = f"{self._get_color_tps(ticks_5s, 5)}, "
|
|
if len(ticks_30s) > 30 * self.target_tps:
|
|
t[1] = f"{self._get_color_tps(ticks_30s, 30)}, "
|
|
if len(ticks_60s) > 60 * self.target_tps:
|
|
t[2] = f"{self._get_color_tps(ticks_60s, 60)}."
|
|
return f"html:{self._get_color_tps(ticks_2s, 2)} TPS; For last 5s, 30s, 60s: " + "".join(t)
|
|
|
|
async def _tick(self):
|
|
try:
|
|
ticks = 0
|
|
target_tps = self.target_tps
|
|
last_tick_time = time.monotonic()
|
|
ev.register("serverTick", self._useful_ticks)
|
|
ticks_2s = deque(maxlen=2 * int(target_tps) + 1)
|
|
ticks_5s = deque(maxlen=5 * int(target_tps) + 1)
|
|
ticks_30s = deque(maxlen=30 * int(target_tps) + 1)
|
|
ticks_60s = deque(maxlen=60 * int(target_tps) + 1)
|
|
console.add_command("tps", lambda _: self._cmd_tps(ticks_2s, ticks_5s, ticks_30s, ticks_60s),
|
|
None, "Print TPS", {"tps": None})
|
|
_add_to_sleep = deque([0.0, 0.0, 0.0, ], maxlen=3 * int(target_tps))
|
|
# _t0 = []
|
|
|
|
self.log.debug("tick system started")
|
|
while self.run:
|
|
target_interval = 1 / self.target_tps
|
|
start_time = time.monotonic()
|
|
|
|
ev.call_event("serverTick")
|
|
await ev.call_async_event("serverTick")
|
|
|
|
# Calculate the time taken for this tick
|
|
end_time = time.monotonic()
|
|
tick_duration = end_time - start_time
|
|
# _t0.append(tick_duration)
|
|
|
|
# Calculate the time to sleep to maintain target TPS
|
|
sleep_time = target_interval - tick_duration - statistics.fmean(_add_to_sleep)
|
|
if sleep_time > 0:
|
|
await asyncio.sleep(sleep_time)
|
|
|
|
# Update tick count and time
|
|
ticks += 1
|
|
current_time = time.monotonic()
|
|
ticks_2s.append(current_time)
|
|
ticks_5s.append(current_time)
|
|
ticks_30s.append(current_time)
|
|
ticks_60s.append(current_time)
|
|
|
|
# Calculate TPS
|
|
elapsed_time = current_time - last_tick_time
|
|
if elapsed_time >= 1:
|
|
self.tps = ticks / elapsed_time
|
|
# if self.tps < 5:
|
|
# self.log.warning(f"Low TPS: {self.tps:.2f}")
|
|
# Reset for next calculation
|
|
# _t0s = max(_t0), min(_t0), statistics.fmean(_t0)
|
|
# _tw = max(_add_to_sleep), min(_add_to_sleep), statistics.fmean(_add_to_sleep)
|
|
# self.log.debug(f"[{'OK' if sleep_time > 0 else "CHECK"}] TPS: {self.tps:.2f}; Tt={_t0s}; Ts={sleep_time}; Tw={_tw}")
|
|
# _t0 = []
|
|
last_tick_time = current_time
|
|
ticks = 0
|
|
|
|
_add_to_sleep.append(time.monotonic() - start_time - sleep_time)
|
|
self.log.debug("tick system stopped")
|
|
except Exception as e:
|
|
self.log.exception(e)
|
|
|
|
async def _parse_chat(self, event):
|
|
player = event['kwargs']['player']
|
|
message = event['kwargs']['message']
|
|
|
|
async def main(self):
|
|
self.tcp = self.tcp(self, self.server_ip, self.server_port)
|
|
self.udp = self.udp(self, self.server_ip, self.server_port)
|
|
PermsSystem()
|
|
console.add_command(
|
|
"list",
|
|
lambda x: f"Players list: {self.get_clients_list(True)}"
|
|
)
|
|
ev.call_event("add_perm_to_alias", "cmd.kick")
|
|
console.add_command("kick", self._cmd_kick, "kick - Kick user\n"
|
|
"Usage: kick NICK|:{ID} [REASON]\n"
|
|
"Examples:\n"
|
|
" <white>></white> <b><skyblue>kick admin bad boy</skyblue></b>\n"
|
|
" <white>></white> <b><skyblue>kick :0 bad boy</skyblue></b>",
|
|
"kick user", {"kick": "<playerlist>"})
|
|
ev.register("onChatReceive", self._parse_chat)
|
|
|
|
pl_dir = "plugins"
|
|
self.log.debug("Initializing PluginsLoaders...")
|
|
if not os.path.exists(pl_dir):
|
|
os.mkdir(pl_dir)
|
|
pl = PluginsLoader(pl_dir)
|
|
if config.Options['use_lua']:
|
|
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
|
|
lpl = LuaPluginsLoader(pl_dir)
|
|
lpl.load()
|
|
await pl.load()
|
|
|
|
try:
|
|
# Mods handler
|
|
self.log.debug("Listing mods..")
|
|
if not os.path.exists(self.mods_dir):
|
|
os.mkdir(self.mods_dir)
|
|
for file in os.listdir(self.mods_dir):
|
|
path = os.path.join(self.mods_dir, file).replace("\\", "/")
|
|
if os.path.isfile(path) and path.endswith(".zip"):
|
|
size = os.path.getsize(path)
|
|
self.mods_list.append({"path": path, "size": size})
|
|
self.mods_list[0] += size
|
|
self.log.debug(f"mods_list: {self.mods_list}")
|
|
len_mods = len(self.mods_list) - 1
|
|
if len_mods > 0:
|
|
self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
|
|
self.log.info(i18n.init_ok)
|
|
|
|
await self.heartbeat(True) # Check
|
|
|
|
self.clients = [None] * config.Game["players"] * 4 # * 4 For down sock and buffer.
|
|
tasks = []
|
|
ev.register("serverTick_1s", self._check_alive)
|
|
ev.register("serverTick_1s", self._send_online)
|
|
# ev.register("serverTick_5s", self.heartbeat)
|
|
f_tasks = [self.tcp.start, self.udp._start, console.start, self._tick, self.heartbeat]
|
|
if config.RCON['enabled']:
|
|
self.log.warning("RCON not available. yet.")
|
|
# console.rcon.version = f"KuiToi {__version__}"
|
|
# rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port'])
|
|
# f_tasks.append(rcon.start)
|
|
for task in f_tasks:
|
|
tasks.append(asyncio.create_task(task()))
|
|
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
|
|
|
await ev.call_async_event("_plugins_start")
|
|
|
|
self.run = True
|
|
self.log.info(i18n.start)
|
|
ev.call_event("onServerStarted")
|
|
await ev.call_async_event("onServerStarted")
|
|
await t # Wait end.
|
|
except KeyboardInterrupt:
|
|
pass
|
|
except Exception as e:
|
|
self.log.error(f"Exception in main:")
|
|
self.log.exception(e)
|
|
finally:
|
|
await self.stop()
|
|
|
|
def start(self):
|
|
asyncio.run(self.main())
|
|
|
|
async def stop(self):
|
|
self.run = False
|
|
ev.call_lua_event("onShutdown")
|
|
await ev.call_async_event("onServerStopped")
|
|
ev.call_event("onServerStopped")
|
|
try:
|
|
await self.__gracefully_kick()
|
|
await self.__gracefully_remove()
|
|
self.tcp.stop()
|
|
self.udp._stop()
|
|
await ev.call_async_event("_plugins_unload")
|
|
if config.Options['use_lua']:
|
|
await ev.call_async_event("_lua_plugins_unload")
|
|
self.run = False
|
|
total_time = time.monotonic() - self.start_time
|
|
hours = int(total_time // 3600)
|
|
minutes = int((total_time % 3600) // 60)
|
|
seconds = math.ceil(total_time % 60)
|
|
t = f"{'' if not hours else f'{hours} hours, '}{'' if not hours else f'{minutes} min., '}{seconds} sec."
|
|
self.log.info(f"Working time: {t}")
|
|
self.log.info(i18n.stop)
|
|
except Exception as e:
|
|
self.log.error("Error while stopping server:")
|
|
self.log.exception(e)
|