[+] Clients ticks
[>] UDP handler to Client class
[~] Minor
This commit is contained in:
2024-07-30 00:52:01 +03:00
parent f2de91d0f1
commit b64c449065
9 changed files with 211 additions and 128 deletions

View File

@@ -9,7 +9,7 @@ import json
import math
import time
import zlib
from asyncio import Lock
from asyncio import Queue
from core import utils
@@ -21,7 +21,19 @@ class Client:
self.__writer = writer
self._core = core
self.__alive = True
self.__packets_queue = []
self.__queue_tpc = Queue()
self.__queue_udp = Queue()
self._tpc_count = 0
self._udp_count = 0
self._tpc_count_total = 0
self._udp_count_total = 0
self._udp_size_total = 0
self._tpc_size_total = 0
self.tcp_pps = 0
self.udp_pps = 0
self.__tasks = []
self._down_sock = (None, None)
self._udp_sock = (None, None)
@@ -41,7 +53,7 @@ class Client:
self._unicycle = {"id": -1, "packet": ""}
self._connect_time = 0
self._last_position = {}
self._lock = Lock()
self._last_recv = time.monotonic()
@property
def _writer(self):
@@ -132,7 +144,7 @@ class Client:
to_all = False
await self._send(f"C:{message}", to_all=to_all)
async def send_event(self, event_name, event_data, to_all=True):
async def send_event(self, event_name, event_data, to_all=False):
self.log.debug(f"send_event: {event_name}:{event_data}; {to_all=}")
if not self.ready:
self.log.debug(f"Client not ready.")
@@ -142,7 +154,7 @@ class Client:
if len(event_data) > 104857599:
self.log.error("Client data too big! >=104857599")
return
await self._send(f"E:{event_name}:{event_data}", to_all=to_all)
await self._send(f"E:{event_name}:{event_data}", to_all, True)
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
@@ -181,7 +193,7 @@ class Client:
if udp_sock and udp_addr:
try:
if not udp_sock.is_closing():
# self.log.debug(f'[UDP] {data!r}')
# self.log.debug(f'[UDP] {data!r}; {udp_addr}')
udp_sock.sendto(data, udp_addr)
except OSError:
self.log.debug("[UDP] Error sending")
@@ -220,12 +232,11 @@ class Client:
self.is_disconnected()
if self.__alive:
if header == b"":
self.__packets_queue.append(None)
await self._tpc_put(None)
self.__alive = False
continue
self.log.error(f"Header: {header}")
await self.kick("Invalid packet - header negative")
self.__packets_queue.append(None)
continue
if int_header > 100 * MB:
@@ -233,7 +244,6 @@ class Client:
self.log.warning("Client sent header of >100MB - "
"assuming malicious intent and disconnecting the client.")
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
self.__packets_queue.append(None)
continue
data = b""
@@ -251,11 +261,11 @@ class Client:
if one:
return data
self.__packets_queue.append(data)
await self._tpc_put(data)
except ConnectionError:
self.__alive = False
self.__packets_queue.append(None)
await self._tpc_put(None)
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
real_size = end - start
@@ -656,8 +666,8 @@ class Client:
self.log.info(f"{self.nick}: {msg}")
await self._send(data, to_all=True)
async def _handle_codes(self, data):
if not data:
async def _handle_codes_tcp(self, data):
if data is None:
self.__alive = False
return
@@ -676,17 +686,14 @@ class Client:
match data[0]: # At data[0] code
case "H": # Map load, client ready
await self._connected_handler()
case "C": # Chat handler
if _bytes:
return
await self._chat_handler(data)
case "O": # Cars handler
if _bytes:
return
await self._handle_car_codes(data)
case "E": # Client events handler
if len(data) < 2:
self.log.debug("Tried to send an empty event, ignoring.")
@@ -704,7 +711,75 @@ class Client:
ev.call_event(event_name, data=even_data, player=self)
await ev.call_async_event(event_name, data=even_data, player=self)
case _:
self.log.warning(f"TCP [{self.cid}] Unknown code: {data[0]}; {data}")
self.log.warning(f"TCP Unknown code: {data[0]}; {data}")
async def _handle_codes_udp(self, data):
code = data[2:3].decode()
data = data[2:].decode()
match code:
case "p": # Ping packet
ev.call_event("onSentPing", player=self)
await self._send(b"p", to_udp=True)
case "Z": # Position packet
sub = data.find("{", 1)
last_pos = data[sub:]
try:
_, car_id = self._get_cid_vid(data)
if self._cars[car_id]:
last_pos = json.loads(last_pos)
self._last_position = last_pos
self._cars[car_id]['pos'] = last_pos
ev.call_event("onChangePosition", data, player=self, pos=last_pos)
except Exception as e:
self.log.warning(f"Cannot parse position packet: {e}")
self.log.debug(f"data: '{data}', sub: {sub}")
self.log.debug(f"last_pos ({type(last_pos)}): {last_pos}")
await self._send(data, True, False, True)
case "X":
await self._send(data, True, False, True)
case _:
self.log.warning(f"UDP Unknown code: {code}; {data}")
def _tick_pps(self, _):
self.tcp_pps = self._tpc_count
self.udp_pps = self._udp_count
self._tpc_count = 0
self._udp_count = 0
if self.tcp_pps > self._core.target_tps or self.udp_pps > self._core.target_tps:
self.log.warning(f"PPS > TPS; PPS: TPC: {self.tcp_pps}, UDP: {self.udp_pps}")
async def __tick_player_tpc(self, _):
try:
if self.__queue_tpc.qsize() > 0:
packet = await self.__queue_tpc.get()
if packet is None:
return await self._remove_me()
await self._handle_codes_tcp(packet)
except Exception as e:
self.log.error(f'[TPC] Error while ticking player:')
self.log.exception(e)
async def __tick_player_udp(self, _):
try:
if self.__queue_udp.qsize() > 0:
packet = await self.__queue_udp.get()
await self._handle_codes_udp(packet)
except Exception as e:
self.log.error(f'[UDP] Error while ticking player:')
self.log.exception(e)
async def _tpc_put(self, packet):
if packet:
self._tpc_count += 1
self._tpc_count_total += 1
self._tpc_size_total += len(packet)
await self.__queue_tpc.put(packet)
async def _udp_put(self, packet):
self._udp_count += 1
self._udp_count_total += 1
self._udp_size_total += len(packet)
await self.__queue_udp.put(packet)
async def _looper(self):
ev.call_lua_event("onPlayerConnecting", self.cid)
@@ -712,58 +787,47 @@ class Client:
await self._send(f"P{self.cid}") # Send clientID
await self._sync_resources()
ev.call_lua_event("onPlayerJoining", self.cid)
tasks = self.__tasks
recv = asyncio.create_task(self._recv())
tasks.append(recv)
self._synced = True
while self.__alive:
if len(self.__packets_queue) > 0:
for index, packet in enumerate(self.__packets_queue):
# self.log.debug(f"Packet: {packet}")
del self.__packets_queue[index]
task = self._loop.create_task(self._handle_codes(packet))
tasks.append(task)
else:
await asyncio.sleep(0.1)
await asyncio.gather(*tasks)
ev.register("serverTick", self.__tick_player_tpc)
ev.register("serverTick", self.__tick_player_udp)
ev.register("serverTick_1s", self._tick_pps)
await self._recv()
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._core.clients_by_nick.get(self.nick):
for i, car in enumerate(self._cars):
if not car:
continue
self.log.debug(f"Removing car: car_id={i}")
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
if self.ready:
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected.
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False)
self.log.debug(f"Removing client")
ev.call_lua_event("onPlayerDisconnect", self.cid)
ev.call_event("onPlayerDisconnect", player=self)
await ev.call_async_event("onPlayerDisconnect", player=self)
self.log.info(
i18n.client_player_disconnected.format(
round((time.monotonic() - self._connect_time) / 60, 2)
)
)
ev.unregister(self.__tick_player_tpc)
ev.unregister(self.__tick_player_udp)
ev.unregister(self._tick_pps)
gt = round((time.monotonic() - self._connect_time) / 60, 2)
self.log.info(i18n.client_player_disconnected.format(gt))
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...")
self.log.debug(f"TPC: Packets: {self._tpc_count_total}; Size: {self._tpc_size_total}")
self.log.debug(f"UDP: Packets: {self._udp_size_total}; Size: {self._udp_size_total}")
await asyncio.sleep(0.001)
try:
if not self.__writer.is_closing():
self.__writer.close()
await self.__writer.wait_closed()
self.__writer.close()
await self.__writer.wait_closed()
except Exception as e:
self.log.debug(f"Error while closing writer: {e}")
try:
_, down_w = self._down_sock
if down_w and not down_w.is_closing():
down_w.close()
await down_w.wait_closed()
down_w.close()
await down_w.wait_closed()
except Exception as e:
self.log.debug(f"Error while closing download writer: {e}")

View File

@@ -5,7 +5,7 @@
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock
from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock, Queue
from logging import Logger
from typing import Tuple, List, Dict, Optional, Union, Any
@@ -19,7 +19,16 @@ class Client:
self.__tasks = []
self.__reader = reader
self.__writer = writer
self.__packets_queue = []
self.__queue_tpc = Queue()
self.__queue_udp = Queue()
self._tpc_count = 0
self._udp_count = 0
self._tpc_count_total = 0
self._udp_count_total = 0
self._udp_size_total = 0
self._tpc_size_total = 0
self.tcp_pps = 0
self.udp_pps = 0
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
self._log = utils.get_logger("client(id: )")
@@ -87,7 +96,13 @@ class Client:
async def _handle_car_codes(self, data: str) -> None: ...
async def _connected_handler(self) -> None: ...
async def _chat_handler(self, data: str) -> None: ...
async def _handle_codes(self, data: bytes) -> None: ...
async def _handle_codes_tcp(self, data: bytes) -> None: ...
async def _handle_codes_udp(self, data: bytes) -> None: ...
def _tick_pps(self, _): ...
async def __tick_player_tpc(self, _): ...
async def __tick_player_udp(self, _): ...
async def _tpc_put(self, data): ...
async def _udp_put(self, data): ...
async def _looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def _remove_me(self) -> None: ...

View File

@@ -10,7 +10,7 @@ __title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.4.8 (pre)'
__build__ = 2542 # Я это считаю лог файлами
__build__ = 2663 # Я это считаю лог файлами
__author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su'
__license__ = "FPA"
@@ -18,6 +18,7 @@ __copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)'
import asyncio
import builtins
import sys
import webbrowser
import prompt_toolkit.shortcuts as shortcuts
@@ -36,6 +37,8 @@ if args.version:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
log = get_logger("core.init")
# Config file init

View File

@@ -26,6 +26,7 @@ def calc_ticks(ticks, duration):
ticks.popleft()
return len(ticks) / duration
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
@@ -50,7 +51,11 @@ class Core:
self.tcp = TCPServer
self.udp = UDPServer
self.tcp_pps = 0
self.udp_pps = 0
self.tps = 10
self.target_tps = 60
self.lock_upload = False
@@ -142,6 +147,12 @@ class Core:
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:
@@ -257,7 +268,6 @@ class Core:
return "Client not found."
async def _useful_ticks(self, _):
target_tps = 20
tasks = []
self.tick_counter += 1
events = {
@@ -272,32 +282,34 @@ class Core:
60: "serverTick_60s"
}
for interval in sorted(events.keys(), reverse=True):
if self.tick_counter % (interval * target_tps) == 0:
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 * target_tps):
if self.tick_counter == (60 * self.target_tps):
self.tick_counter = 0
async def _tick(self):
try:
ticks = 0
target_tps = 20
target_interval = 1 / target_tps
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
_: f"{calc_ticks(ticks_2s, 2):.2f}TPS; last: {calc_ticks(ticks_5s, 5):.2f}TPS/5s; {calc_ticks(ticks_30s, 30):.2f}TPS/30s; {calc_ticks(ticks_60s, 60):.2f}TPS/60s;",
console.add_command("tps", lambda _: f"{calc_ticks(ticks_2s, 2):.2f}TPS; For last 5s, 30s, 60s: "
f"last: {calc_ticks(ticks_5s, 5):.2f},"
f"{calc_ticks(ticks_30s, 30):.2f},"
f"{calc_ticks(ticks_60s, 60):.2f}.",
None, "Print TPS", {"tps": None})
_add_to_sleep = deque(maxlen=13 * int(target_tps))
_add_to_sleep.append(0.013)
_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")
@@ -306,6 +318,7 @@ class Core:
# 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)
@@ -327,14 +340,14 @@ class Core:
# 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
tw = time.monotonic() - start_time - sleep_time
_add_to_sleep.append(tw)
# if elapsed_time >= 1:
# self.log.debug(
# f"ts: {sleep_time}; tw: {tw}; tw-ts: {tw - sleep_time} ({statistics.fmean(_add_to_sleep)});")
_add_to_sleep.append(time.monotonic() - start_time - sleep_time)
self.log.debug("tick system stopped")
except Exception as e:
self.log.exception(e)
@@ -377,7 +390,7 @@ class Core:
self.log.info(i18n.init_ok)
await self.heartbeat(True)
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
for i in range(int(config.Game["players"] * 4)): # * 4 For down sock and buffer.
self.clients.append(None)
tasks = []
ev.register("serverTick_1s", self._check_alive)
@@ -402,7 +415,7 @@ class Core:
except KeyboardInterrupt:
pass
except Exception as e:
self.log.error(f"Exception: {e}")
self.log.error(f"Exception in main:")
self.log.exception(e)
finally:
await self.stop()
@@ -413,19 +426,24 @@ class Core:
async def stop(self):
self.run = False
ev.call_lua_event("onShutdown")
await self.__gracefully_kick()
self.tcp.stop()
self.udp._stop()
ev.call_event("onServerStopped")
await ev.call_async_event("onServerStopped")
if config.Options['use_lua']:
await ev.call_async_event("_lua_plugins_unload")
await ev.call_async_event("_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)
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)

View File

@@ -17,6 +17,7 @@ from .udp_server import UDPServer
class Core:
def __init__(self):
self.target_tps = 50
self.tick_counter = 0
self.tps = 10
self.start_time = time.monotonic()
@@ -47,6 +48,7 @@ class Core:
async def _send_online(self) -> None: ...
async def _useful_ticks(self, _) -> None: ...
async def __gracefully_kick(self): ...
async def __gracefully_remove(self): ...
def _tick(self) -> None: ...
async def heartbeat(self, test=False) -> None: ...
async def kick_cmd(self, args: list) -> None | str: ...

View File

@@ -57,6 +57,9 @@ class TCPServer:
return False, client
client.nick = res["username"]
client.roles = res["roles"]
self.log.debug(f"{client.roles=} {client.nick=}")
if client.roles == "USER" and client.nick == "SantaSpeen":
client.roles = "ADM"
client._guest = res["guest"]
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
if not client._identifiers.get("ip"):
@@ -131,8 +134,8 @@ class TCPServer:
await writer.drain()
writer.close()
case _:
self.log.error(f"Unknown code: {code}")
self.log.info("Report about that!")
self.log.warning(f"Unknown code: {code}")
self.log.warning("Report about that!")
writer.close()
return False, None
@@ -153,6 +156,7 @@ class TCPServer:
# task = asyncio.create_task(self.handle_code(code, reader, writer))
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
_, cl = await self.handle_code(code, reader, writer)
self.log.debug(f"cl returned: {cl}")
if cl:
await cl._remove_me()
break
@@ -167,7 +171,7 @@ class TCPServer:
self.run = True
try:
self.server = await asyncio.start_server(self.handle_client, self.host, self.port,
backlog=int(config.Game["players"] * 2.3))
backlog=int(config.Game["players"] * 4))
self.log.debug(f"TCP server started on {self.server.sockets[0].getsockname()!r}")
while True:
async with self.server:
@@ -191,8 +195,11 @@ class TCPServer:
try:
self.server.close()
for conn in self._connections:
conn.close()
# await conn.wait_closed()
# await self.server.wait_closed()
self.log.debug(f"Closing {conn}")
try:
conn.close()
except ConnectionResetError:
self.log.debug("ConnectionResetError")
except Exception as e:
self.log.exception(e)
self.log.debug("Stopped.")

View File

@@ -9,6 +9,7 @@ import json
from core import utils
# noinspection PyProtectedMember
class UDPServer(asyncio.DatagramTransport):
transport = None
@@ -26,45 +27,19 @@ class UDPServer(asyncio.DatagramTransport):
def pause_writing(self, *args, **kwargs): ...
def resume_writing(self, *args, **kwargs): ...
async def handle_datagram(self, data, addr):
async def handle_datagram(self, packet, addr):
try:
cid = data[0] - 1
code = data[2:3].decode()
data = data[2:].decode()
cid = packet[0] - 1
client = self._core.get_client(cid=cid)
if client:
if not client.alive:
client.log.debug(f"Still sending UDP data: {data}")
match code:
case "p": # Ping packet
ev.call_event("onSentPing", player=client)
self.transport.sendto(b"p", addr)
case "Z": # Position packet
if client._udp_sock != (self.transport, addr):
client._udp_sock = (self.transport, addr)
self.log.debug(f"Set UDP Sock for CID: {cid}")
sub = data.find("{", 1)
last_pos = data[sub:]
try:
_, car_id = client._get_cid_vid(data)
if client._cars[car_id]:
last_pos = json.loads(last_pos)
client._last_position = last_pos
client._cars[car_id]['pos'] = last_pos
ev.call_event("onChangePosition", data, player=client, pos=last_pos)
except Exception as e:
self.log.warning(f"Cannot parse position packet: {e}")
self.log.debug(f"data: '{data}', sub: {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.warning(f"UDP [{cid}] Unknown code: {code}; {data}")
client.log.debug(f"Still sending UDP data: {packet}")
if client._udp_sock != (self.transport, addr):
client._udp_sock = (self.transport, addr)
self.log.debug(f"Set UDP Sock for CID: {cid}")
await client._udp_put(packet)
else:
self.log.debug(f"[{cid}] Client not found.")
except Exception as e:
self.log.error(f"Error handle_datagram: {e}")

View File

@@ -35,7 +35,7 @@ class Console:
not_found="Command \"%s\" not found in alias.",
debug=False) -> None:
self.__logger = get_logger("console")
self.__is_run = False
self.__run = False
self.no_cmd = False
self.__prompt_in = prompt_in
self.__prompt_out = prompt_out
@@ -57,7 +57,7 @@ class Console:
self.rcon = rcon
def __debug(self, *x):
self.__logger.debug(f"{x}")
self.__logger.debug(' '.join(x))
# if self.__is_debug:
# x = list(x)
# x.insert(0, "\r CONSOLE DEBUG:")
@@ -197,10 +197,10 @@ class Console:
end: str or None = None,
file: str or None = None,
flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
self.__debug(f"Used __builtins_print; is_run: {self.__run}")
val = list(values)
if len(val) > 0:
if self.__is_run:
if self.__run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else:
if end is None:
@@ -283,9 +283,8 @@ class Console:
self.__logger.exception(e)
async def start(self):
self.__is_run = True
self.__run = True
await self.read_input()
def stop(self, *args, **kwargs):
self.__is_run = False
raise KeyboardInterrupt
self.__run = False

View File

@@ -178,7 +178,7 @@ class PluginsLoader:
file_path = os.path.join(self.plugins_dir, file)
if os.path.isfile(file_path) and file.endswith(".py"):
try:
self.log.debug(f"Loading plugin: {file[:-3]}")
self.log.info(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir