From a0cca0e1ffca47e930d671f3f6160c59e8a81506 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 8 Oct 2026 21:15:35 -0400 Subject: [PATCH 1/2] Allow multiple reset images --- docs/tutorial.md | 2 +- src/AutoSplit.py | 66 +++++++++++++++++++++++++++------------------ src/menu_bar.py | 9 +++++-- src/split_parser.py | 56 +++++++++++++++++++++++++------------- 4 files changed, 85 insertions(+), 48 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index d94cb5c0..7ea3b91b 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -180,7 +180,7 @@ The best way to create a masked image is to set your capture region as the entir ### Reset Image -You can have one (and only one) image with the keyword `reset` in its name. AutoSplit will press the reset button when it finds this image. This image will only be used for resets and it will not be tied to any split. You can set a threshold and pause time for it. The pause time is the amount of seconds AutoSplit will wait before checking for the Reset Image once the run starts. For example: `Reset_(0.95)_[10].png`. +You can have one or more images with the keyword `reset` in their name. AutoSplit will press the reset button when it matches any of these images. These images will only be used for resets and they will not be tied to any split. You can set a threshold and pause time for each of them. The pause time is the amount of seconds AutoSplit will wait before checking for that Reset Image once the run starts. For example: `Reset_(0.95)_[10].png`. When there is more than one Reset Image, the comparison table shows the one closest to its threshold. ### Start Image diff --git a/src/AutoSplit.py b/src/AutoSplit.py index f8d8610f..5a0e974c 100755 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -226,7 +226,7 @@ def __init__(self): # noqa: PLR0915 self.split_below_threshold = False self.run_start_time = 0.0 self.start_image: AutoSplitImage | None = None - self.reset_image: AutoSplitImage | None = None + self.reset_images: list[AutoSplitImage] = [] self.split_images: list[AutoSplitImage] = [] self.split_image: AutoSplitImage | None = None self.update_auto_control: AutoControlledThread | None = None @@ -663,11 +663,11 @@ def __check_fps(self): self.fps_value_label.clear() return - images = self.split_images - if self.start_image: - images.append(self.start_image) - if self.reset_image: - images.append(self.reset_image) + images = [ + *self.split_images, + *self.reset_images, + *([self.start_image] if self.start_image is not None else []), + ] # run X iterations of screenshotting capture region + comparison + displaying. t0 = time() @@ -988,7 +988,7 @@ def __pause_loop(self, stop_time: float, message: str): # This is done so that it can detect if user hit split/undo split while paused/delayed. pause_split_image_number = self.split_image_number while True: - # Calculate similarity for Reset Image + # Calculate similarity for Reset Images if self.__reset_if_should(self.__get_capture_for_comparison()): return True @@ -1092,31 +1092,45 @@ def __get_capture_for_comparison(self): def __reset_if_should(self, capture: MatLike | None): """Checks if we should reset, resets if it's the case, and returns the result.""" - if self.reset_image: + if self.reset_images: if self.settings_dict["enable_auto_reset"]: - similarity = self.reset_image.compare_with_capture(self, capture) - threshold = self.reset_image.get_similarity_threshold(self) - - pause_times = [self.reset_image.get_pause_time(self)] - if self.start_image: - pause_times.append(self.start_image.get_pause_time(self)) - paused = time() - self.run_start_time <= max(pause_times) - if paused: - should_reset = False - self.table_reset_image_live_label.setText("paused") - else: - should_reset = similarity >= threshold + elapsed_time = time() - self.run_start_time + start_pause_time = self.start_image.get_pause_time(self) if self.start_image else 0 + active_reset_images = [ + reset_image + for reset_image in self.reset_images + if elapsed_time > max(reset_image.get_pause_time(self), start_pause_time) + ] + if active_reset_images: + # Display the Reset Image that is the closest to its threshold + similarity, threshold = max( + ( + ( + reset_image.compare_with_capture(self, capture), + reset_image.get_similarity_threshold(self), + ) + for reset_image in active_reset_images + ), + key=lambda similarity_and_threshold: ( + similarity_and_threshold[0] - similarity_and_threshold[1] + ), + ) self.reset_highest_similarity = max(similarity, self.reset_highest_similarity) self.table_reset_image_highest_label.setText( decimal(self.reset_highest_similarity) ) self.table_reset_image_live_label.setText(decimal(similarity)) - - self.table_reset_image_threshold_label.setText(decimal(threshold)) - - if should_reset: - send_command(self, "reset") - self.reset() + self.table_reset_image_threshold_label.setText(decimal(threshold)) + if similarity >= threshold: + send_command(self, "reset") + self.reset() + else: + self.table_reset_image_live_label.setText("paused") + self.table_reset_image_threshold_label.setText( + decimal( + min(image.get_similarity_threshold(self) for image in self.reset_images) + ) + ) else: self.table_reset_image_live_label.setText("disabled") else: diff --git a/src/menu_bar.py b/src/menu_bar.py index 052aba6f..171d48e1 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -224,8 +224,13 @@ def __update_default_threshold(self, value: Any): else "-" ) self._autosplit_ref.table_reset_image_threshold_label.setText( - decimal(self._autosplit_ref.reset_image.get_similarity_threshold(self._autosplit_ref)) - if self._autosplit_ref.reset_image + decimal( + min( + reset_image.get_similarity_threshold(self._autosplit_ref) + for reset_image in self._autosplit_ref.reset_images + ) + ) + if self._autosplit_ref.reset_images else "-" ) diff --git a/src/split_parser.py b/src/split_parser.py index b84f76cc..6b20555a 100644 --- a/src/split_parser.py +++ b/src/split_parser.py @@ -7,12 +7,12 @@ from enum import IntEnum, auto from functools import partial from stat import UF_HIDDEN -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, Literal, TypeVar, overload import numpy as np import error_messages -from AutoSplitImage import RESET_KEYWORD, START_KEYWORD, AutoSplitImage, ImageType +from AutoSplitImage import START_KEYWORD, AutoSplitImage, ImageType from utils import ALPHA_CHANNEL_COUNT, MAXBYTE, ColorChannel, ImageShape, is_valid_image if sys.platform == "win32": @@ -234,13 +234,36 @@ def flags_from_filename(filename: str): return flags -def __pop_image_type(split_image: list[AutoSplitImage], image_type: ImageType): - for image in split_image: - if image.image_type == image_type: - split_image.remove(image) - return image - - return None +@overload +def __pop_image_type( + split_images: list[AutoSplitImage], image_type: Literal[ImageType.START] +) -> AutoSplitImage | None: ... +@overload +def __pop_image_type( + split_images: list[AutoSplitImage], image_type: Literal[ImageType.SPLIT, ImageType.RESET] +) -> list[AutoSplitImage]: ... +def __pop_image_type(split_images: list[AutoSplitImage], image_type: ImageType): + if image_type == ImageType.START: + # Only one Start Image is allowed. + # Any other is left in the list to be reported during validation. + for index, image in enumerate(split_images): + if image.image_type == image_type: + return split_images.pop(index) + return None + + images: list[AutoSplitImage] = [] + remaining_images: list[AutoSplitImage] = [] + for image in split_images: + (images if image.image_type == image_type else remaining_images).append(image) + split_images[:] = remaining_images + return images + + +def __set_reset_images(autosplit: AutoSplit, reset_images: list[AutoSplitImage]): + autosplit.reset_images = reset_images + autosplit.table_reset_image_label.setText( + f"Reset Images ({len(reset_images)})" if len(reset_images) > 1 else "Reset Image" + ) def validate_before_parsing(autosplit: AutoSplit, *, show_error: bool = True): @@ -298,7 +321,7 @@ def parse_and_validate_images(autosplit: AutoSplit): all_images = __get_images_from_directory(autosplit.settings_dict["split_image_directory"]) # Find non-split images and then remove them from the list start_image = __pop_image_type(all_images, ImageType.START) - reset_image = __pop_image_type(all_images, ImageType.RESET) + reset_images = __pop_image_type(all_images, ImageType.RESET) split_images = all_images error_message: Callable[[], object] | None = None @@ -318,7 +341,7 @@ def parse_and_validate_images(autosplit: AutoSplit): # If there is no reset hotkey set but a Reset Image is present, # and is not auto controlled, throw an error. elif ( - reset_image + reset_images and not autosplit.settings_dict["reset_hotkey"] and not autosplit.is_auto_controlled ): @@ -342,25 +365,20 @@ def parse_and_validate_images(autosplit: AutoSplit): error_message = error_messages.pause_hotkey break - # Check that there's only one Reset Image - if image.image_type == ImageType.RESET: - error_message = lambda: error_messages.multiple_keyword_images(RESET_KEYWORD) # noqa: E731 - break - # Check that there's only one Start Image if image.image_type == ImageType.START: - error_message = lambda: error_messages.multiple_keyword_images(START_KEYWORD) # noqa: E731 + error_message = partial(error_messages.multiple_keyword_images, START_KEYWORD) break if error_message: autosplit.start_image = None - autosplit.reset_image = None + __set_reset_images(autosplit, []) autosplit.split_images = [] autosplit.gui_changes_on_reset() error_message() return False autosplit.start_image = start_image - autosplit.reset_image = reset_image + __set_reset_images(autosplit, reset_images) autosplit.split_images = split_images return True From 111f87a55e64ad543221fac194613c360df07dd6 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 8 Oct 2026 21:44:51 -0400 Subject: [PATCH 2/2] Fix Levenshtein lazy import issue --- src/AutoSplit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 5a0e974c..09d1f618 100755 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -19,12 +19,15 @@ # (PySide6.support.signature.* vs shibokensupport.*). # - numpy: its self-check raises a bogus version conflict when imported # through shiboken6's patched __import__. + # - Levenshtein: its lazy "import rapidfuzz.distance.X as _X" statements recurse + # infinitely (RecursionError) when reified through shiboken6's patched __import__. _EAGER_INTERNALS = ( frozenset({ "PySide6", "shiboken6", "shibokensupport", "numpy", + "Levenshtein", }) | sys.stdlib_module_names )