From 34b2a8fead8fe0233850f29b4e8552451cb9d236 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 13 May 2026 12:05:11 +0800 Subject: [PATCH 1/2] ## Summary - Removes the broken NotImplementedError from pick-ball mode so the documented q command no longer exits the program Closes #291 Assisted-by: OpenAI Codex --- pooltool/ani/modes/pick_ball.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pooltool/ani/modes/pick_ball.py b/pooltool/ani/modes/pick_ball.py index ed1eb294..7e0e70ec 100644 --- a/pooltool/ani/modes/pick_ball.py +++ b/pooltool/ani/modes/pick_ball.py @@ -43,8 +43,6 @@ def exit(self): tasks.remove("pick_ball_task") def pick_ball_task(self, task): - raise NotImplementedError("Woops, this is in a broken state, don't press that") - if not self.keymap[Action.pick_ball]: Global.mode_mgr.change_mode(Mode.aim) return task.done From 9d7ed403170f9bdeeeab4edae85c0b1e82f8c5de Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 13 May 2026 12:41:44 +0800 Subject: [PATCH 2/2] fix(pick_ball): guard against None closest_ball in pick_ball_task Previously only the NotImplementedError was removed, but find_closest_ball() can return None when no balls are eligible (all pocketed or not in can_cue). This would crash when trying to call .get_node('pos') on None, or when the 'done' action fires with no ball selected. Changes: - Guard ball highlight setup with - Wrap the 'done' handler's ball selection logic in the same None check - Mode change to aim still happens on 'done' even if no ball was selected Closes #291 --- pooltool/ani/modes/pick_ball.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pooltool/ani/modes/pick_ball.py b/pooltool/ani/modes/pick_ball.py index 7e0e70ec..caed2a1a 100644 --- a/pooltool/ani/modes/pick_ball.py +++ b/pooltool/ani/modes/pick_ball.py @@ -53,19 +53,21 @@ def pick_ball_task(self, task): if closest != self.closest_ball: self.remove_ball_highlight() self.closest_ball = closest - self.ball_highlight = self.closest_ball.get_node("pos") - self.add_ball_highlight() + if self.closest_ball is not None: + self.ball_highlight = self.closest_ball.get_node("pos") + self.add_ball_highlight() if self.keymap["done"]: - self.remove_ball_highlight() - ball_id = self.closest_ball._ball.id - if ball_id is not None: - multisystem.active.cue.cue_ball_id = ball_id - visual.cue.init_focus(visual.balls[ball_id]) - Global.game.log.add_msg( - f"Now cueing the {multisystem.active.cue.cue_ball_id} ball", - sentiment="neutral", - ) + if self.closest_ball is not None: + self.remove_ball_highlight() + ball_id = self.closest_ball._ball.id + if ball_id is not None: + multisystem.active.cue.cue_ball_id = ball_id + visual.cue.init_focus(visual.balls[ball_id]) + Global.game.log.add_msg( + f"Now cueing the {multisystem.active.cue.cue_ball_id} ball", + sentiment="neutral", + ) Global.mode_mgr.change_mode(Mode.aim) return task.done