Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
69 changes: 43 additions & 26 deletions src/AutoSplit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -226,7 +229,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
Expand Down Expand Up @@ -663,11 +666,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()
Expand Down Expand Up @@ -988,7 +991,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

Expand Down Expand Up @@ -1092,31 +1095,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:
Expand Down
9 changes: 7 additions & 2 deletions src/menu_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "-"
)

Expand Down
56 changes: 37 additions & 19 deletions src/split_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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
):
Expand All @@ -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
Loading