diff --git a/.env.example b/.env.example index 83b804c..c97e1cb 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,8 @@ -DEV_MODE=False \ No newline at end of file +# Enable or disable the development mode +DEV_MODE=False + +# Enable or disable plugins in development mode +PLUGINS_DEV_MODE=True + +# Rather or not to use WebSockets +USE_WEBSOCKET=True diff --git a/README.md b/README.md index 18f7744..04554dd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +> [!WARNING] +> DO NOT USE THIS BRANCH! +> WEBSOCKETS ARE IN EARLY BETA AND REQUIRE A SPECIAL AUTHENTICATION ON THE API +> LAUNCHING THIS VERSION MIGHT RESULT IN A BAN FROM THE API +> +> PLEASE USE THE `main` BRANCH INSTEAD! +
Polsu @@ -10,8 +17,8 @@ A Hypixel Bedwars Overlay in Python, 100% free and open source! Wakatime
- - + + # ๐Ÿ“– Table of Contents - [๐Ÿ“ About](#-about) diff --git a/dev.py b/dev.py index 726d944..0ba1ade 100644 --- a/dev.py +++ b/dev.py @@ -87,6 +87,15 @@ else: logger.critical("You are running the development version of the overlay! However, you are not in development mode.") - print("You are running the development version of the overlay! However, you are not in development mode.") + print("\n") + print("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“") + print("โ”ƒ โ”ƒ") + print("โ”ƒ WARNING ! โ”ƒ") + print("โ”ƒ โ”ƒ") + print("โ”ƒ You are running the development version of the overlay! However, you are not in development mode. โ”ƒ") + print("โ”ƒ Please set the DEV_MODE variable to True in the environment file! Or run the main.py file. โ”ƒ") + print("โ”ƒ โ”ƒ") + print("โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”›") + print("\n") sys.exit(1) diff --git a/main.py b/main.py index 2bf85d9..ab3c915 100644 --- a/main.py +++ b/main.py @@ -52,7 +52,7 @@ if getattr(sys, 'frozen', False): - import pyi_splash + import pyi_splash # type: ignore def run(window: Updater, logger: Logger) -> None: diff --git a/src/PolsuAPI/api.py b/src/PolsuAPI/api.py index 0512e38..837b5ea 100644 --- a/src/PolsuAPI/api.py +++ b/src/PolsuAPI/api.py @@ -42,7 +42,7 @@ import traceback -from aiohttp import ClientSession, ContentTypeError +from aiohttp import ClientSession, ContentTypeError, WSMsgType from json import load @@ -144,6 +144,124 @@ async def login(self) -> User: self.logger.error(f"An error occurred while logging in!\n\nTraceback: {traceback.format_exc()} | {e}") return None + + async def WebSocketConnection(self, setWebSocket, callback, closed) -> None: + """ + Create a WebSocket connection + + :param setWebSocket: A function to set the WebSocket + :param callback: A function to call when a player is received + :param closed: A function to call when the WebSocket is closed + """ + self.logger.info(f"[WS] Creating a WebSocket connection...") + + # Wait for messages + expired = False + + try: + # Create a WebSocket connection + async with ClientSession() as session: + async with session.ws_connect( + url=f"{self.api}/internal/overlay/websocket", + headers=self.polsuHeaders, + timeout=1800, + receive_timeout=1800, + heartbeat=1800, + autoping=True, + autoclose=False + ) as ws: + if not ws.closed: + self.logger.info(f"[WS] Starting websocket connection...") + self.logger.debug(f"[WS] Sending handshake...") + + # Send a handshake + await ws.send_json( + { + "protocol": "PolsuWebSocketProtocol - 1.0", + "handshake": self.key + } + ) + + self.logger.debug(f"[WS] Handshake sent!") + + if not ws.closed: + # Wait for the handshake response + async for msg in ws: + try: + data = msg.json() + if data.get('success', False) and data.get('data', {}) == "Connection established.": + self.logger.info(f"[WS] New WebSocket connection established!") + break + else: + self.logger.debug(f"[WS] Couldn't create a WebSocket connection! {msg}") + break + except: + self.logger.debug(f"[WS] Couldn't create a WebSocket connection! {msg}") + break + + if not ws.closed: + # Set the WebSocket so the overlay can send messages + await setWebSocket(ws) + + self.logger.info(f"[WS] WebSocket connection established!") + + async for msg in ws: + if msg.type in (WSMsgType.CLOSED, WSMsgType.ERROR): + return + else: + try: + data = msg.json() + + if DEV_MODE: + self.logger.debug(f"[WS] Received data: {data}") + + if data.get("data", {}) != {}: + if data.get("success", False): + player = Player(data.get("data")) + player.manual = data.get("data", {}).get("player", {}).get("manual", False) + player.websocket = True + await callback(player) + else: + if isinstance(data.get("data", {}), str): + if data.get("data", {}) == "Expired websocket!": + self.logger.warning(f"[WS] WebSocket connection expired!") + expired = True + + raise Exception("Expired websocket!") + elif data.get("data", {}) == "Packet limit reached!": + self.logger.error(f"[WS] Packet limit reached!") + expired = True + + raise Exception("Packet limit reached!") + elif data.get("data", {}) == "Malformed JSON" or data.get("data", {}) == "Missing query" or data.get("data", {}) == "Invalid query": + self.logger.error(f"[WS] An error occurred while sending some data: {data.get('data', {})}") + elif data.get("data", {}) == "Too many players": + self.logger.error(f"[WS] Oops we reached the maximum amount of players!") + else: + self.logger.error(f"[WS] An error occurred with the websocket connection: {data.get('data', {})}") + else: + if data.get("cause", None).startswith("Rate Limited"): + self.logger.error(f"[WS] Rate limited! {data.get('cause', None)}") + + raise Exception("Rate limited!") + else: + f = open(f"{resource_path('src/PolsuAPI')}/schemas/nicked.json", mode="r", encoding="utf-8") + player = Player(load(f)) + player.username = data.get("data", {}).get("player", {}).get("username") + player.rank = f"ยงc{data.get('data', {}).get('player', {}).get('username')}" + player.manual = data.get("data", {}).get("player", {}).get("manual", False) + player.nicked = True + player.websocket = True + await callback(player) + except Exception as e: + self.logger.error(f"An error occurred while receiving a WebSocket message!\n\nTraceback: {traceback.format_exc()} | {e}") + except Exception as e: + self.logger.error(f"An error occurred while creating a WebSocket connection!\n\nTraceback: {traceback.format_exc()} | {e}") + + closed(expired) + + self.logger.info(f"[WS] WebSocket connection closed!") + async def get_stats(self, player: str) -> Player: """ @@ -322,15 +440,19 @@ async def load_skin(self, player: Player) -> None: if DEV_MODE: self.logger.info(f"GET skins.mcstats.com/face/{player.uuid}") + headers = { + "User-Agent": __header__["User-Agent"] + } + async with ClientSession() as session: - async with session.get(f"https://skins.mcstats.com/face/{player.uuid}", headers=__header__) as response: + async with session.get(f"https://skins.mcstats.com/face/{player.uuid}", headers=headers) as response: if response.status == 200: return await response.read() else: self.logger.error(f"An error occurred while getting the skin of {player.uuid} ({response.status})!") raise APIError except ContentTypeError: - raise APIError + return None except APIError: return None except Exception as e: diff --git a/src/PolsuAPI/client.py b/src/PolsuAPI/client.py index 05c9c3c..9d425b0 100644 --- a/src/PolsuAPI/client.py +++ b/src/PolsuAPI/client.py @@ -169,3 +169,15 @@ async def loadSkin(self, player: Pl) -> None: return await self.client.load_skin(player) except asyncio.TimeoutError: raise APIError + + + async def WebSocket(self, setWebSocket, callback, closed) -> None: + """ + Get a Player stats with a WebSocket connection + + :param setWebSocket: A setWebSocket function + :param callback: A callback function + :param closed: A closed function + :return: An instance of Pl, representing the Player stats + """ + await self.client.WebSocketConnection(setWebSocket, callback, closed) diff --git a/src/PolsuAPI/objects/player.py b/src/PolsuAPI/objects/player.py index 1c59eb7..5fbdb31 100644 --- a/src/PolsuAPI/objects/player.py +++ b/src/PolsuAPI/objects/player.py @@ -515,6 +515,9 @@ def __init__(self, data: dict) -> None: self._ping = Ping(data.get('ping')) self._local = None + self._manual = False + self._websocket = False + @property def data(self) -> dict: @@ -643,6 +646,38 @@ def local(self, value: LocalBlacklisted) -> None: """ self._local = value + + @property + def manual(self) -> bool: + """ + Whether the Player is manually requested or not + """ + return self._manual + + + @manual.setter + def manual(self, value: bool) -> None: + """ + Set whether the Player is manually requested or not + """ + self._manual = value + + + @property + def websocket(self) -> bool: + """ + Whether the Player was requested through the WebSocket or not + """ + return self._websocket + + + @websocket.setter + def websocket(self, value: bool) -> None: + """ + Set whether the Player was requested through the WebSocket or not + """ + self._websocket = value + def __repr__(self) -> str: return f"" diff --git a/src/__init__.py b/src/__init__.py index 927ad2d..a206c4f 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -41,7 +41,7 @@ __title__ = "PolsuOverlay" __author__ = "Polsulpicien" __license__ = "GPL-3.0 License" -__version__ = "2.0.7" +__version__ = "2.0.8" __description__ = "Polsu's Overlay" __module__ = getcwd() @@ -67,6 +67,8 @@ load_dotenv('.env') DEV_MODE = True if environ.get("DEV_MODE", "False") == "True" else False +PLUGINS_DEV_MODE = True if environ.get("PLUGINS_DEV_MODE", "False") == "True" else False +USE_WEBSOCKET = True if environ.get("USE_WEBSOCKET", "False") == "True" else True # This will only change the local cache, not the API cache diff --git a/src/components/logger.py b/src/components/logger.py index ee8ba51..84f3f90 100644 --- a/src/components/logger.py +++ b/src/components/logger.py @@ -64,8 +64,6 @@ def __init__(self): handler.setFormatter(formatter) self.logger.addHandler(handler) - self.logger.info('Logger Initialised\n\n') - def info(self, message: str) -> None: """ diff --git a/src/components/logs.py b/src/components/logs.py index bd27ac6..0f274ea 100644 --- a/src/components/logs.py +++ b/src/components/logs.py @@ -63,9 +63,9 @@ def __init__(self, win) -> None: """ self.win = win - self.oldString = "" + self.lastReadPosition = 0 self.timeIconIndex = 1 - self.error_sent = False + self.errorSent = False self.waitingForGame = False self.isInGame = False @@ -151,19 +151,21 @@ def leftGame(self) -> None: self.hideOverlayTimer = 0 - def who(self) -> None: + def who(self, force: bool = False) -> None: """ Runs /who + + :param force: A boolean representing if the /who should be forced """ - if not self.autoWho: + if force or not self.autoWho: self.leftGame() self.reset() - if self.win.configWho: + if force or self.win.configWho: self.autoWho = True active = get_active_window_title() - if any(client in active for client in CLIENT_NAMES): + if force or active and any(client in active for client in CLIENT_NAMES): keyboard.press_and_release('t') sleep(0.2) keyboard.write('/who') @@ -171,7 +173,7 @@ def who(self) -> None: keyboard.press_and_release('enter') else: self.autoWho = False - + self.waitingForGame = True @@ -179,7 +181,8 @@ def task(self) -> None: """ The main task which reads the log file """ - if self.oldString == "": + # Check if the log file has been read before or not + if self.lastReadPosition == 0: self.readLogFile() else: try: @@ -188,24 +191,33 @@ def task(self) -> None: self.win.logger.error(f"Error while reading logs.\n\nTraceback: {traceback.format_exc()}") - def readLogFile(self) -> str: + def readLogFile(self) -> list[str]: """ Function which returns the new lines of the log file :return: A string containing the new lines of the log file """ try: - with open(self.win.configLogPath, "r+") as logFile: - contents = logFile.read() + new_lines = [] + with open(self.win.configLogPath, "r") as logFile: + logFile.seek(self.lastReadPosition) + + if self.lastReadPosition > 0: + for line in logFile: + lastNewlineIdx = line.rfind('\n') + cleaned = line[:lastNewlineIdx] + line[lastNewlineIdx + 1:] + new_lines.append(self.rawLine(cleaned)) + else: + for line in logFile: + new_lines.append(line) - new = contents[len(self.oldString):] - self.oldString = contents + self.lastReadPosition = logFile.tell() - self.error_sent = False + self.errorSent = False - return new + return new_lines except FileNotFoundError: - if not self.error_sent: + if not self.errorSent: self.win.notif.send( title="Warning!", message="The log file you are currently using isn't valid.\nGo to: Settings -> Client, and choose a valid client.", @@ -213,10 +225,10 @@ def readLogFile(self) -> str: ) # To avoid multiple notifications - self.error_sent = True - return "" + self.errorSent = True + return [] except: - return "" + return [] def rawLine(self, string: str) -> str: @@ -243,8 +255,7 @@ def readLogs(self) -> None: Function which detected players in the new lines added in the log file Automatically add them to the queue, to get their stats and display them on the overlay """ - line: str = self.readLogFile() - lines = self.rawLine(line).splitlines() + lines: list[str] = self.readLogFile() for l in lines: line = l.replace(" [System] ", "") @@ -279,7 +290,7 @@ def readLogs(self) -> None: # If it's the first line after a player connects to Hypixel, the delivery command is executed elif self.connecting and "[CHAT] " in line: active = get_active_window_title() - if any(client in active for client in CLIENT_NAMES): + if active and any(client in active for client in CLIENT_NAMES): sleep(0.5) keyboard.press_and_release('t') sleep(0.3) diff --git a/src/components/player.py b/src/components/player.py index 5ad1bc7..9d7064f 100644 --- a/src/components/player.py +++ b/src/components/player.py @@ -31,13 +31,12 @@ โ”ƒ โ”ƒ โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”› """ -from src import CACHE, DEV_MODE +from src import CACHE, DEV_MODE, USE_WEBSOCKET from ..PolsuAPI import Polsu from ..PolsuAPI.exception import APIError, InvalidAPIKeyError from ..PolsuAPI.objects.player import Player as Pl from ..utils.colours import Colours - import asyncio import traceback @@ -70,6 +69,14 @@ def __init__(self, win) -> None: self.loading = [] + if USE_WEBSOCKET: + self.websocket = WebSocket(self.client) + self.websocket.playerObject.connect(self.update) + self.websocket.start() + else: + self.websocket = None + + def getPlayer(self, players: list, manual: bool = False) -> None: """ Get a player from the API @@ -118,40 +125,65 @@ def getPlayer(self, players: list, manual: bool = False) -> None: if len(new) == 1: self.win.logger.info(f"Requesting: {new[0]}.") - try: - self.threads[cleaned] = Worker(self.client, new[0], manual) - self.threads[cleaned].playerObject.connect(self.update) - self.threads[cleaned].start() + if self.websocket and self.websocket.websocket: + asyncio.run( + self.websocket.query( + [ + { + "player": new[0], + "manual": manual + } + ] + ) + ) + else: + try: + self.threads[cleaned] = Worker(self.client, new[0], manual) + self.threads[cleaned].playerObject.connect(self.update) + self.threads[cleaned].start() - self.win.plugins.broadcast("on_player_load", new[0]) - except: - self.win.logger.error(f"Error while loading a player ({new[0]}).\n\nTraceback: {traceback.format_exc()}") + self.win.plugins.broadcast("on_player_load", new[0]) + except: + self.win.logger.error(f"Error while loading a player ({new[0]}).\n\nTraceback: {traceback.format_exc()}") else: - if len(new) > 40: - nb_slice = 10 - elif len(new) > 20: - nb_slice = 6 + if self.websocket and self.websocket.websocket: + asyncio.run( + self.websocket.query( + [ + { + "player": p, + "manual": manual + } + for p in new + ] + ) + ) else: - nb_slice = 3 + if len(new) > 40: + nb_slice = 10 + elif len(new) > 20: + nb_slice = 6 + else: + nb_slice = 3 - slices = [new[i : i+nb_slice] for i in range(0, len(new), nb_slice)] + slices = [new[i : i+nb_slice] for i in range(0, len(new), nb_slice)] - for s in slices: - self.win.logger.info(f"Requesting: {s}.") + for s in slices: + self.win.logger.info(f"Requesting: {s}.") - uuid = str(uuid4()) - while uuid in self.threads: uuid = str(uuid4()) + while uuid in self.threads: + uuid = str(uuid4()) - try: - self.threads[uuid] = Worker(self.client, s, manual) - self.threads[uuid].playerObject.connect(self.update) - self.threads[uuid].start() + try: + self.threads[uuid] = Worker(self.client, s, manual) + self.threads[uuid].playerObject.connect(self.update) + self.threads[uuid].start() - for p in s: - self.win.plugins.broadcast("on_player_load", p) - except: - self.win.logger.error(f"Error while loading a player slice ({s}).\n\nTraceback: {traceback.format_exc()}") + for p in s: + self.win.plugins.broadcast("on_player_load", p) + except: + self.win.logger.error(f"Error while loading a player slice ({s}).\n\nTraceback: {traceback.format_exc()}") def loadPlayer(self, player: str, uuid: str) -> None: @@ -195,6 +227,22 @@ def setRPCPlayer(self, player: Pl) -> None: self.update(player) + def deleteWorker(self, player: str) -> None: + """ + Delete the worker + + :param player: The player to delete + """ + cleaned = player.lower() + + if cleaned in self.threads: + try: + self.threads[cleaned].terminate() + self.threads.pop(cleaned) + except: + self.win.logger.error(f"An error occurred while deleting the worker of {cleaned}!\n\nTraceback: {traceback.format_exc()}") + + def update(self, player: Pl, cache: bool = True) -> None: """ Insert a player into the table @@ -269,6 +317,10 @@ def update(self, player: Pl, cache: bool = True) -> None: self.win.settings.update("APIKey", "") + if not isinstance(player, bool): + self.deleteWorker(player.cleaned) + + def getCache(self, player: str) -> Union[Pl, None]: """ Get a player from the cache @@ -316,7 +368,7 @@ def __init__(self, client, query: Union[list, str], manual: bool = False) -> Non self.client = client self.query = query self.manual = manual - + def run(self) -> None: """ @@ -346,3 +398,74 @@ def run(self) -> None: self.playerObject.emit(None) except: self.playerObject.emit(False) + + +class WebSocket(QThread): + """ + The worker class, used to get players from the API + """ + playerObject = pyqtSignal(object) + + def __init__(self, client) -> None: + """ + Initialise the class + + :param client: The Polsu client + :param query: The query to use + :param manual: If the player is manually added + """ + super(QThread, self).__init__() + self.client = client + self.websocket = None + + + def run(self) -> None: + """ + Run the thread + """ + asyncio.run(self.client.player.WebSocket(self.setWebSocket, self.update, self.closed)) + + + async def setWebSocket(self, ws) -> None: + """ + Get a player from the websocket + + :param ws: The websocket + """ + self.websocket = ws + + + async def query(self, query: list) -> None: + """ + Query a player + + :param query: The query to use + """ + if self.websocket: + await self.websocket.send_json( + { + "query": query + } + ) + + + async def update(self, player: Pl) -> None: + """ + Insert a player into the table + + :param player: The player to update + """ + self.playerObject.emit(player) + + + def closed(self, expired: bool) -> None: + """ + Called when the websocket is closed + + :param expired: If the websocket expired + """ + self.websocket = None + + if expired: + self.client.logger.debug("The websocket expired! Creating a new one...") + self.start() diff --git a/src/overlay.py b/src/overlay.py index 16d2e53..b9f4587 100644 --- a/src/overlay.py +++ b/src/overlay.py @@ -32,7 +32,7 @@ โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”› """ from .PolsuAPI import User -from src import Menu, Notif, Settings, Logs, Player, loadThemes, openSettings, __version__, DEV_MODE +from src import Menu, Notif, Settings, Logs, Player, loadThemes, openSettings, __version__, DEV_MODE, PLUGINS_DEV_MODE from .components.theme import ThemeStyle from .components.logger import Logger from .components.rpc import openRPC, startRPC @@ -128,6 +128,7 @@ def __init__(self, logger: Logger) -> None: self.logger.debug("Loading the Settings...") self.settings = Settings(self) conf = self.settings.loadConfig() + conf["APIKey"] = f"{conf.get('APIKey', '')[0:4]}XXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" self.logger.debug(f"Settings: {conf}") @@ -184,7 +185,7 @@ def __init__(self, logger: Logger) -> None: # Check Logs Task self.logger.debug("Loading the Check Logs Task...") checkLogsTask = QTimer(self) - checkLogsTask.setInterval(700) #1000 -> 1 sec | 0.7 sec + checkLogsTask.setInterval(100) #1000 -> 1 sec | 0.7 sec checkLogsTask.timeout.connect(self.logs.task) checkLogsTask.start() self.logger.debug(f"Check Logs Task active: {'yes' if checkLogsTask.isActive() else 'no'}") @@ -238,13 +239,21 @@ def __init__(self, logger: Logger) -> None: PluginLogs(self.logs), PluginAPI(self.player.client), PluginSettings(self.settings), - PluginWindow(self.ask), + PluginWindow( + self.ask, + self.mini, + self.maxi, + self.window + ), PluginPlayer(self.player), ) - self.plugins.load_plugins(self.pluginsConfig) - self.logger.info(f"There are {len(self.plugins.getPlugins())} plugins loaded.") - self.logger.debug(f"Plugins: {', '.join([plugin.__name__ for plugin in self.plugins.getPlugins()])}") - self.logger.debug("Plugins loaded!") + if PLUGINS_DEV_MODE: + self.plugins.load_plugins(self.pluginsConfig) + self.logger.info(f"There are {len(self.plugins.getPlugins())} plugins loaded.") + self.logger.debug(f"Plugins: {', '.join([plugin.__name__ for plugin in self.plugins.getPlugins()])}") + self.logger.debug("Plugins loaded!") + else: + self.logger.warning("You have disabled the plugins development mode! No plugins loaded.") def loginEnded(self, user: User) -> None: diff --git a/src/plugins/logs.py b/src/plugins/logs.py index 4cb138c..a489448 100644 --- a/src/plugins/logs.py +++ b/src/plugins/logs.py @@ -42,4 +42,5 @@ def __init__(self, logs: Logs) -> None: self.isWaitingForGame = logs.isWaitingForGame self.isInAParty = logs.isInAParty self.getPartyMembers = logs.getPartyMembers + self.who = logs.who \ No newline at end of file diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py index 3c2bb7e..5620921 100644 --- a/src/plugins/plugin.py +++ b/src/plugins/plugin.py @@ -96,35 +96,35 @@ def __init__(self) -> None: Initialise the plugin """ pass - + def on_load(self) -> None: """ Called when the plugin is loaded """ raise NotImplementedError("on_load() is not implemented!") - + def on_unload(self) -> None: """ Called when the plugin is unloaded """ raise NotImplementedError("on_unload() is not implemented!") - + def on_login(self, user: User) -> None: """ Called when the user logs in """ raise NotImplementedError("on_login() is not implemented!") - + def on_logout(self, user: User) -> None: """ Called when the user logs out """ raise NotImplementedError("on_logout() is not implemented!") - + def on_search(self, player: str) -> None: """ @@ -138,7 +138,7 @@ def on_player_load(self, player: str) -> None: Called when the player is loaded """ raise NotImplementedError("on_player_load() is not implemented!") - + def on_player_insert(self, player: Player) -> None: """ @@ -152,56 +152,56 @@ def on_player_message(self, message: str) -> None: Called when the player sends a message """ raise NotImplementedError("on_player_message() is not implemented!") - + def on_message(self, message: str) -> None: """ Called when a message is sent """ raise NotImplementedError("on_message() is not implemented!") - + def on_join(self, player: str) -> None: """ Called when a player joins the game """ raise NotImplementedError("on_join() is not implemented!") - + def on_leave(self, player: str) -> None: """ Called when a player leaves the game """ raise NotImplementedError("on_leave() is not implemented!") - + def on_final_kill(self, player: str) -> None: """ Called when a player gets a final kill """ raise NotImplementedError("on_final_kill() is not implemented!") - + def on_final_death(self, player: str) -> None: """ Called when a player gets final killed """ raise NotImplementedError("on_final_death() is not implemented!") - + def on_who(self) -> None: """ Called when a player uses /who """ raise NotImplementedError("on_who() is not implemented!") - + def on_list(self) -> None: """ Called when a player uses /list """ raise NotImplementedError("on_list() is not implemented!") - + def on_game_start(self) -> None: """ diff --git a/src/plugins/window.py b/src/plugins/window.py index 76912bb..a77945a 100644 --- a/src/plugins/window.py +++ b/src/plugins/window.py @@ -32,5 +32,8 @@ โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”› """ class PluginWindow: - def __init__(self, ask) -> None: + def __init__(self, ask, minimize, maximize, window) -> None: self.ask = ask + self.minimize = minimize + self.maximize = maximize + self.window = window diff --git a/src/utils/log.py b/src/utils/log.py index eb4f9c0..8fa09bc 100644 --- a/src/utils/log.py +++ b/src/utils/log.py @@ -55,7 +55,7 @@ def __init__(self, key: str, logger) -> None: """ super(QThread, self).__init__() self.client = Polsu(key, logger) - + def run(self): """ diff --git a/src/utils/quickbuy.py b/src/utils/quickbuy.py index 205d1fc..028af09 100644 --- a/src/utils/quickbuy.py +++ b/src/utils/quickbuy.py @@ -96,7 +96,21 @@ def run(self, player) -> None: self.win.logger.error(f"An error occurred while loading the quickbuy image!\n\nTraceback: {traceback.format_exc()}") - def setPixmap(self, player, pixmap: QPixmap, cache: bool = True) -> None: + def deleteWorker(self, player: Player) -> None: + """ + Delete the Worker + + :param player: The player + """ + if player.username in self.threads: + try: + self.threads[player.username].terminate() + self.threads.pop(player.username) + except: + self.win.logger.error(f"An error occurred while deleting the worker of {player.username}!\n\nTraceback: {traceback.format_exc()}") + + + def setPixmap(self, player: Player, pixmap: QPixmap, cache: bool = True) -> None: """ Callback function to set the pixmap of the quickbuy image @@ -121,6 +135,8 @@ def setPixmap(self, player, pixmap: QPixmap, cache: bool = True) -> None: self.win.quickbuyWindow.resize(pixmap.size().width(), pixmap.size().height()) self.win.quickbuyWindow.setFixedSize(pixmap.size().width(), pixmap.size().height()) self.win.quickbuyWindow.show() + + self.deleteWorker(player) else: self.win.notif.send( title="Error...", diff --git a/src/utils/skin.py b/src/utils/skin.py index 9aa493a..a59892a 100644 --- a/src/utils/skin.py +++ b/src/utils/skin.py @@ -74,8 +74,8 @@ def loadSkin(self, player: Player, count: int) -> None: :param count: The position of the player in the table """ try: - if player.username in self.cache: - self.setSkin(self.cache[player.username], player, count) + if player.uuid in self.cache: + self.setSkin(self.cache[player.uuid], player, count) else: button = QPushButton(self.table) button.setIcon(self.default) @@ -91,9 +91,13 @@ def loadSkin(self, player: Player, count: int) -> None: self.table.setCellWidget(row, 0, button) self.table.setItem(row, 0, TableSortingItem(count)) - self.threads[player.username] = Worker(player, self.win.player.client, self.default, count) - self.threads[player.username].update.connect(self.setSkin) - self.threads[player.username].start() + # FIXME: This is a temporary fix for the skin loading issue with players loaded via the websocket + #if player.websocket: + # return + + self.threads[player.uuid] = Worker(player, self.win.player.client, self.default, count) + self.threads[player.uuid].update.connect(self.setSkin) + self.threads[player.uuid].start() except: self.win.logger.critical(f"An error occurred while loading the skin of {player.username}!\n\nTraceback: {traceback.format_exc()}") @@ -101,6 +105,20 @@ def loadSkin(self, player: Player, count: int) -> None: def rgbaToHex(self, rgba): rgba = tuple(int(x) for x in rgba) return "#{:02X}{:02X}{:02X}{:02X}".format(*rgba) + + + def deleteWorker(self, uuid: str) -> None: + """ + Delete the worker + + :param uuid: The UUID of the player + """ + if uuid in self.threads: + try: + self.threads[uuid].terminate() + self.threads.pop(uuid) + except: + self.win.logger.error(f"An error occurred while deleting the worker of {uuid}!\n\nTraceback: {traceback.format_exc()}") def setSkin(self, icon: QIcon, player: Player, count: int, cache: bool = True) -> None: @@ -114,7 +132,7 @@ def setSkin(self, icon: QIcon, player: Player, count: int, cache: bool = True) - """ try: if cache: - self.cache[player.username] = icon + self.cache[player.uuid] = icon button = QPushButton(self.table) button.setIcon(icon) @@ -136,6 +154,10 @@ def setSkin(self, icon: QIcon, player: Player, count: int, cache: bool = True) - color.setAlpha(50) item = self.table.item(row, 0) item.setBackground(color) + + self.deleteWorker(player.uuid) + + self.win.logger.info(f"Skin of {player.username} has been set.") except: self.win.logger.critical(f"An error occurred while setting the skin of {player.username}!\n\nTraceback: {traceback.format_exc()}") @@ -153,6 +175,7 @@ def __init__(self, player, client: Polsu, default: QIcon, count: int) -> None: :param player: The player to load the skin :param client: The client to request the API :param default: The default icon + :param count: The position of the player in the table """ super(QThread, self).__init__() self.player = player @@ -160,8 +183,6 @@ def __init__(self, player, client: Polsu, default: QIcon, count: int) -> None: self.default = default self.count = count - self.icon = QIcon() - def run(self) -> None: """ @@ -180,5 +201,4 @@ def run(self) -> None: except: icon = self.default - self.icon = icon self.update.emit(icon, self.player, self.count) diff --git a/version.rc b/version.rc index 036d137..f0c78d5 100644 --- a/version.rc +++ b/version.rc @@ -2,8 +2,8 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(2, 0, 7, 1), - prodvers=(2, 0, 7, 1), + filevers=(2, 0, 8, 1), + prodvers=(2, 0, 8, 1), mask=0x3f, flags=0x0, OS=0x4, @@ -18,7 +18,7 @@ VSVersionInfo( u'040904B0', [StringStruct(u'CompanyName', u'Polsu Development'), StringStruct(u'FileDescription', u'Polsu Overlay'), - StringStruct(u'FileVersion', u'2.0.7.1'), + StringStruct(u'FileVersion', u'2.0.8.1'), StringStruct(u'InternalName', u'Polsu Overlay'), StringStruct(u'LegalCopyright', u'Copyright ยฉ 2022 - 2024 Polsu Development'), StringStruct(u'OriginalFilename', u'Polsu Overlay.exe'),