diff --git a/clashroyalebuildabot/actions/generic/action.py b/clashroyalebuildabot/actions/generic/action.py index 8ddc613..595dd2e 100644 --- a/clashroyalebuildabot/actions/generic/action.py +++ b/clashroyalebuildabot/actions/generic/action.py @@ -12,9 +12,6 @@ def __init__(self, index, tile_x, tile_y): self.tile_x = tile_x self.tile_y = tile_y - if self.CARD is None: - raise ValueError("Each action must set the CARD attribute") - def __repr__(self): return f"{self.CARD.name} at ({self.tile_x}, {self.tile_y})" diff --git a/clashroyalebuildabot/bot/bot.py b/clashroyalebuildabot/bot/bot.py index 5eedbca..e954fa3 100644 --- a/clashroyalebuildabot/bot/bot.py +++ b/clashroyalebuildabot/bot/bot.py @@ -23,6 +23,7 @@ from clashroyalebuildabot.emulator.emulator import Emulator from clashroyalebuildabot.namespaces import Screens from clashroyalebuildabot.visualizer import Visualizer +from error_handling import WikifiedError pause_event = threading.Event() pause_event.set() @@ -42,7 +43,9 @@ def __init__(self, actions, config): cards = [action.CARD for action in actions] if len(cards) != 8: - raise ValueError(f"Must provide 8 cards but was given: {cards}") + raise WikifiedError( + "005", f"Must provide 8 cards but {len(cards)} was given" + ) self.cards_to_actions = dict(zip(cards, actions)) self.visualizer = Visualizer(**config["visuals"]) @@ -148,7 +151,7 @@ def play_action(self, action): self.emulator.click(*card_centre) self.emulator.click(*tile_centre) - def step(self): + def _handle_play_pause_in_step(self): if not pause_event.is_set(): if not Bot.is_paused_logged: logger.info("Bot paused.") @@ -159,6 +162,8 @@ def step(self): logger.info("Bot resumed.") Bot.is_resumed_logged = True + def step(self): + self._handle_play_pause_in_step() old_screen = self.state.screen if self.state else None self.set_state() new_screen = self.state.screen @@ -184,6 +189,9 @@ def step(self): self._log_and_wait("Starting game", 2) return + self._handle_game_step() + + def _handle_game_step(self): actions = self.get_actions() if not actions: self._log_and_wait("No actions available", self.play_action_delay) diff --git a/clashroyalebuildabot/detectors/card_detector.py b/clashroyalebuildabot/detectors/card_detector.py index 6fee375..7afc41d 100644 --- a/clashroyalebuildabot/detectors/card_detector.py +++ b/clashroyalebuildabot/detectors/card_detector.py @@ -7,6 +7,7 @@ from clashroyalebuildabot.constants import CARD_CONFIG from clashroyalebuildabot.constants import IMAGES_DIR from clashroyalebuildabot.namespaces.cards import Cards +from error_handling import WikifiedError class CardDetector: @@ -53,15 +54,19 @@ def _calculate_card_hashes(self): ), dtype=np.float32, ) - for i, card in enumerate(self.cards): - path = os.path.join(IMAGES_DIR, "cards", f"{card.name}.jpg") - pil_image = Image.open(path) - - multi_hash = self._calculate_multi_hash(pil_image) - card_hashes[i] = np.tile( - np.expand_dims(multi_hash, axis=2), (1, 1, self.HAND_SIZE) - ) + try: + for i, card in enumerate(self.cards): + path = os.path.join(IMAGES_DIR, "cards", f"{card.name}.jpg") + pil_image = Image.open(path) + multi_hash = self._calculate_multi_hash(pil_image) + card_hashes[i] = np.tile( + np.expand_dims(multi_hash, axis=2), (1, 1, self.HAND_SIZE) + ) + except Exception as e: + raise WikifiedError( + "005", "Can't load cards and their images." + ) from e return card_hashes def _detect_cards(self, image): diff --git a/clashroyalebuildabot/detectors/detector.py b/clashroyalebuildabot/detectors/detector.py index d999b76..d4f6cdb 100644 --- a/clashroyalebuildabot/detectors/detector.py +++ b/clashroyalebuildabot/detectors/detector.py @@ -10,6 +10,7 @@ from clashroyalebuildabot.detectors.screen_detector import ScreenDetector from clashroyalebuildabot.detectors.unit_detector import UnitDetector from clashroyalebuildabot.namespaces import State +from error_handling import WikifiedError class Detector: @@ -17,8 +18,8 @@ class Detector: def __init__(self, cards): if len(cards) != self.DECK_SIZE: - raise ValueError( - f"You must specify all {self.DECK_SIZE} of your cards" + raise WikifiedError( + "005", f"You must specify all {self.DECK_SIZE} of your cards" ) self.cards = deepcopy(cards) diff --git a/clashroyalebuildabot/emulator/emulator.py b/clashroyalebuildabot/emulator/emulator.py index 6efb13c..e402056 100644 --- a/clashroyalebuildabot/emulator/emulator.py +++ b/clashroyalebuildabot/emulator/emulator.py @@ -18,6 +18,7 @@ from clashroyalebuildabot.constants import EMULATOR_DIR from clashroyalebuildabot.constants import SCREENSHOT_HEIGHT from clashroyalebuildabot.constants import SCREENSHOT_WIDTH +from error_handling import WikifiedError class Emulator: @@ -67,7 +68,9 @@ def _get_valid_device_serial(self): ] if not available_devices: - raise RuntimeError("No connected devices found") from e + raise WikifiedError( + "006", "No connected devices found" + ) from e fallback_device_serial = available_devices[0] logger.info( @@ -78,8 +81,8 @@ def _get_valid_device_serial(self): logger.error( f"Failed to execute adb devices: {str(adb_error)}" ) - raise RuntimeError( - "Could not find a valid device to connect to." + raise WikifiedError( + "006", "Could not find a valid device to connect to." ) from adb_error def _start_recording(self): @@ -151,11 +154,13 @@ def _run_command(self, command): logger.error(str(e)) logger.error(f"stdout: {e.stdout}") logger.error(f"stderr: {e.stderr}") - raise + raise WikifiedError("007", "ADB command failed.") from e if result.returncode != 0: logger.error(f"Error executing command: {result.stderr}") - raise RuntimeError("ADB command failed") + raise WikifiedError( + "007", "ADB command failed." + ) from RuntimeError(result.stderr) return result.stdout @@ -171,35 +176,37 @@ def _update_frame(self): logger.debug("Starting to update frames...") for line in iter(self.video_thread.stdout.readline, b""): try: - if not line: + last_frame = self._get_last_frame(line) + if not last_frame: continue - if self.os_name == "windows": - line = line.replace(b"\r\n", b"\n") - - packets = self.codec.parse(line) - if not packets: - continue - - frames = self.codec.decode(packets[-1]) - if not frames: - continue - - self.frame = ( - frames[-1] - .reformat( - width=SCREENSHOT_WIDTH, - height=SCREENSHOT_HEIGHT, - format="rgb24", - ) - .to_image() - ) + self.frame = last_frame.reformat( + width=SCREENSHOT_WIDTH, + height=SCREENSHOT_HEIGHT, + format="rgb24", + ).to_image() except av.AVError as av_error: logger.error(f"Error while decoding video stream: {av_error}") except Exception as e: logger.error(f"Unexpected error in frame update: {str(e)}") + def _get_last_frame(self, line): + if not line: + return None + + if self.os_name == "windows": + line = line.replace(b"\r\n", b"\n") + + packets = self.codec.parse(line) + if not packets: + return None + + frames = self.codec.decode(packets[-1]) + if not frames: + return None + return frames[-1] + def _start_updating_frame(self): self.frame_thread = threading.Thread(target=self._update_frame) self.frame_thread.daemon = True diff --git a/clashroyalebuildabot/gui/main_window.py b/clashroyalebuildabot/gui/main_window.py index 1fd59ea..c628173 100644 --- a/clashroyalebuildabot/gui/main_window.py +++ b/clashroyalebuildabot/gui/main_window.py @@ -17,36 +17,40 @@ from clashroyalebuildabot.gui.styles import set_styles from clashroyalebuildabot.utils.logger import colorize_log from clashroyalebuildabot.utils.logger import setup_logger +from error_handling import WikifiedError class MainWindow(QMainWindow): def __init__(self, config, actions): - super().__init__() - self.config = config - self.actions = actions - self.bot = None - self.bot_thread = None - self.is_running = False + try: + super().__init__() + self.config = config + self.actions = actions + self.bot = None + self.bot_thread = None + self.is_running = False - self.setWindowTitle(" ") - self.setGeometry(100, 100, 900, 600) + self.setWindowTitle(" ") + self.setGeometry(100, 100, 900, 600) - transparent_pixmap = QPixmap(1, 1) - transparent_pixmap.fill(Qt.GlobalColor.transparent) - self.setWindowIcon(QIcon(transparent_pixmap)) + transparent_pixmap = QPixmap(1, 1) + transparent_pixmap.fill(Qt.GlobalColor.transparent) + self.setWindowIcon(QIcon(transparent_pixmap)) - main_widget = QWidget(self) - self.setCentralWidget(main_widget) - main_layout = QVBoxLayout(main_widget) + main_widget = QWidget(self) + self.setCentralWidget(main_widget) + main_layout = QVBoxLayout(main_widget) - top_bar = setup_top_bar(self) - tab_widget = setup_tabs(self) + top_bar = setup_top_bar(self) + tab_widget = setup_tabs(self) - main_layout.addWidget(top_bar) - main_layout.addWidget(tab_widget) + main_layout.addWidget(top_bar) + main_layout.addWidget(tab_widget) - set_styles(self) - start_play_button_animation(self) + set_styles(self) + start_play_button_animation(self) + except Exception as e: + raise WikifiedError("004", "Error in GUI initialization.") from e def log_handler_function(self, message): formatted_message = colorize_log(message) @@ -140,10 +144,12 @@ def bot_task(self): ) self.bot.run() self.stop_bot() - except Exception as e: - logger.error(f"Bot crashed: {e}") + except WikifiedError: self.stop_bot() raise + except Exception as e: + self.stop_bot() + raise WikifiedError("003", "Bot crashed.") from e def append_log(self, message): self.log_display.append(message) diff --git a/clashroyalebuildabot/gui/utils.py b/clashroyalebuildabot/gui/utils.py index 6b6bcfa..9433f41 100644 --- a/clashroyalebuildabot/gui/utils.py +++ b/clashroyalebuildabot/gui/utils.py @@ -1,20 +1,18 @@ import os -from loguru import logger import yaml from clashroyalebuildabot.constants import SRC_DIR +from error_handling import WikifiedError def load_config(): - config = None try: config_path = os.path.join(SRC_DIR, "config.yaml") with open(config_path, encoding="utf-8") as file: - config = yaml.safe_load(file) + return yaml.safe_load(file) except Exception as e: - logger.error(f"Can't parse config, stacktrace: {e}") - return config + raise WikifiedError("002", "Can't parse config.") from e def save_config(config): @@ -23,4 +21,4 @@ def save_config(config): with open(config_path, "w", encoding="utf-8") as file: yaml.dump(config, file) except Exception as e: - logger.error(f"Can't save config, stacktrace: {e}") + raise WikifiedError("000", "Can't save config.") from e diff --git a/clashroyalebuildabot/utils/error_handling.py b/clashroyalebuildabot/utils/error_handling.py new file mode 100644 index 0000000..f3dade0 --- /dev/null +++ b/clashroyalebuildabot/utils/error_handling.py @@ -0,0 +1,4 @@ +def get_wikified_error_message(error_code: str, reason: str) -> str: + return f"Error #E{str(error_code)}: {reason}.\ + See https://github.com/Pbatch/ClashRoyaleBuildABot/wiki/Troubleshooting#error-e{str(error_code)} for more information.\ + Full error message below:\n\n" diff --git a/clashroyalebuildabot/utils/git_utils.py b/clashroyalebuildabot/utils/git_utils.py index e8b67e0..95fd9aa 100644 --- a/clashroyalebuildabot/utils/git_utils.py +++ b/clashroyalebuildabot/utils/git_utils.py @@ -1,9 +1,10 @@ import subprocess -import sys from time import sleep from loguru import logger +from error_handling import WikifiedError + def _is_branch_late() -> bool: subprocess.run( @@ -42,4 +43,6 @@ def check_and_pull_updates() -> None: sleep(3) return logger.error(f"Error while checking / pulling updates: {e.stderr}") - sys.exit(1) + raise WikifiedError( + "000", "Error while checking / pulling updates" + ) from e diff --git a/error_handling/__init__.py b/error_handling/__init__.py new file mode 100644 index 0000000..a1a935b --- /dev/null +++ b/error_handling/__init__.py @@ -0,0 +1,3 @@ +from .wikify_error import WikifiedError + +__all__ = ["WikifiedError"] diff --git a/error_handling/wikify_error.py b/error_handling/wikify_error.py new file mode 100644 index 0000000..af6c0e5 --- /dev/null +++ b/error_handling/wikify_error.py @@ -0,0 +1,14 @@ +def get_wikified_error_message(error_code: str, reason: str) -> str: + err_str = f"\u26A0 Error #E{str(error_code)}: {reason}" + link = "https://github.com/Pbatch/ClashRoyaleBuildABot/wiki/" + link += "Troubleshooting#error-e{str(error_code)}" + err_str += f" See {link} for more information." + err_str += " You might find more context above this error.\n\n" + return err_str + + +class WikifiedError(Exception): + def __init__(self, error_code: str, reason: str): + self.error_code = error_code + self.reason = reason + super().__init__(get_wikified_error_message(error_code, reason)) diff --git a/main.py b/main.py index d76e64b..b5d1e12 100644 --- a/main.py +++ b/main.py @@ -1,21 +1,26 @@ -import signal -import sys - -from loguru import logger -from PyQt6.QtWidgets import QApplication - -from clashroyalebuildabot.actions import ArchersAction -from clashroyalebuildabot.actions import BabyDragonAction -from clashroyalebuildabot.actions import CannonAction -from clashroyalebuildabot.actions import GoblinBarrelAction -from clashroyalebuildabot.actions import KnightAction -from clashroyalebuildabot.actions import MinipekkaAction -from clashroyalebuildabot.actions import MusketeerAction -from clashroyalebuildabot.actions import WitchAction -from clashroyalebuildabot.gui.main_window import MainWindow -from clashroyalebuildabot.gui.utils import load_config -from clashroyalebuildabot.utils.git_utils import check_and_pull_updates -from clashroyalebuildabot.utils.logger import setup_logger +from error_handling import WikifiedError + +try: + import signal + import sys + + from loguru import logger + from PyQt6.QtWidgets import QApplication + + from clashroyalebuildabot.actions import ArchersAction + from clashroyalebuildabot.actions import BabyDragonAction + from clashroyalebuildabot.actions import CannonAction + from clashroyalebuildabot.actions import GoblinBarrelAction + from clashroyalebuildabot.actions import KnightAction + from clashroyalebuildabot.actions import MinipekkaAction + from clashroyalebuildabot.actions import MusketeerAction + from clashroyalebuildabot.actions import WitchAction + from clashroyalebuildabot.gui.main_window import MainWindow + from clashroyalebuildabot.gui.utils import load_config + from clashroyalebuildabot.utils.git_utils import check_and_pull_updates + from clashroyalebuildabot.utils.logger import setup_logger +except Exception as e: + raise WikifiedError("001", "Missing imports.") from e def main(): @@ -33,12 +38,14 @@ def main(): try: config = load_config() - app = QApplication(sys.argv) + app = QApplication([]) window = MainWindow(config, actions) setup_logger(window, config) window.show() sys.exit(app.exec()) + except WikifiedError: + raise except Exception as e: logger.error(f"An error occurred in main loop: {e}") sys.exit(1)