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
10 changes: 8 additions & 2 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
- Custom split image settings are handled in the filename. See how [here](#custom-split-image-settings).
- To create split images, it is recommended to use AutoSplit's Take Screenshot button for accuracy. However, images can be created using any method including Print Screen and [Snipping Tool](https://support.microsoft.com/en-us/help/4027213/windows-10-open-snipping-tool-and-take-a-screenshot).

#### Reload Images

- Images are loaded from your Split Image Folder ahead of time, not when the run starts. This avoids any delay right after your timer starts.
- Click "Reload Images" after adding, removing, or editing images in your Split Image Folder. This reloads all Split Images, [Reset Images](#reset-images), and the [Start Image](#start-image).
- Images are also reloaded automatically when you select a new Split Image Folder, load a [profile](#profiles), and after each run ends or is reset.

#### Capture Region

- This is the region that your split images are compared to. Usually, this is going to be the full game screen.
Expand Down Expand Up @@ -178,13 +184,13 @@ The best way to create a masked image is to set your capture region as the entir

![Mask Example](/docs/mask_example_image.png)

### Reset Image
### Reset Images

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

The Start Image is similar to the Reset Image. You can only have one Start Image with the keyword `start_auto_splitter`.You can reload the image using the "`Reload Start Image`" button. The pause time is the amount of seconds AutoSplit will wait before starting comparisons of the first split image. Delay times will be used to delay starting your timer after the threshold is met.
The Start Image is similar to the Reset Image. You can only have one Start Image with the keyword `start_auto_splitter`. The pause time is the amount of seconds AutoSplit will wait before starting comparisons of the first split image. Delay times will be used to delay starting your timer after the threshold is met.

### Text Recognition / Optical Character Recognition (OCR) ⚠️EXPERIMENTAL⚠️

Expand Down
6 changes: 3 additions & 3 deletions res/design.ui
Original file line number Diff line number Diff line change
Expand Up @@ -1015,12 +1015,12 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="reload_start_image_button">
<widget class="QPushButton" name="reload_images_button">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>Reload Start Image</string>
<string>Reload Images</string>
</property>
</widget>
</item>
Expand Down Expand Up @@ -1216,7 +1216,7 @@ ClickableLabel:hover { background-color: palette(midlight); }</string>
</widget>
<widget class="QDockWidget" name="log_dock">
<property name="styleSheet">
<string notr="true">QDockWidget::title { padding: 6px; }</string>
<string notr="true">QDockWidget::title { padding: 6px; }</string>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFeature::NoDockWidgetFeatures</set>
Expand Down
107 changes: 49 additions & 58 deletions src/AutoSplit.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def do_nothing(*_): ...
import error_messages
import user_profile
from AutoControlledThread import AutoControlledThread
from AutoSplitImage import START_KEYWORD, AutoSplitImage, ImageType
from AutoSplitImage import AutoSplitImage, ImageType
from capture_method import CaptureMethodEnum
from capture_method.CaptureMethodBase import CaptureMethodBase
from gen import about, design, settings, update_checker
Expand All @@ -136,13 +136,7 @@ def do_nothing(*_): ...
view_help,
)
from region_selection import align_region, select_region, select_window
from split_parser import (
BELOW_FLAG,
DUMMY_FLAG,
PAUSE_FLAG,
parse_and_validate_images,
validate_before_parsing,
)
from split_parser import BELOW_FLAG, DUMMY_FLAG, PAUSE_FLAG, parse_and_validate_images
from user_profile import DEFAULT_PROFILE
from utils import (
ALPHA_CHANNEL_COUNT,
Expand Down Expand Up @@ -187,7 +181,7 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow):
screenshot_signal = QtCore.Signal()
after_setting_hotkey_signal = QtCore.Signal()
update_checker_widget_signal = QtCore.Signal(str, bool)
reload_start_image_signal = QtCore.Signal(bool, bool)
reload_images_signal = QtCore.Signal()
# Use this signal when trying to show an error from outside the main thread
show_error_signal = QtCore.Signal(FunctionType)

Expand Down Expand Up @@ -297,9 +291,7 @@ def _show_error_signal_slot(error_message_box: Callable[..., object]):
)
self.align_region_button.clicked.connect(lambda: align_region(self))
self.select_window_button.clicked.connect(lambda: select_window(self))
self.reload_start_image_button.clicked.connect(
lambda: self.__reload_start_image(started_by_button=True)
)
self.reload_images_button.clicked.connect(self.__reload_images)
self.action_check_for_updates_on_open.changed.connect(
lambda: user_profile.QT_SETTINGS.setValue(
"check_for_updates_on_open", self.action_check_for_updates_on_open.isChecked()
Expand Down Expand Up @@ -329,7 +321,7 @@ def _update_checker_widget_signal_slot(latest_version: str, check_on_open: bool)
return open_update_checker(self, latest_version, check_on_open=check_on_open)

self.update_checker_widget_signal.connect(_update_checker_widget_signal_slot)
self.reload_start_image_signal.connect(self.__reload_start_image)
self.reload_images_signal.connect(self.__reload_images)
self.reset_signal.connect(self.reset)
self.skip_split_signal.connect(self.skip_split)
self.undo_split_signal.connect(self.undo_split)
Expand Down Expand Up @@ -467,7 +459,7 @@ def __browse(self):
# set the split image folder line to the directory text
self.settings_dict["split_image_directory"] = new_split_image_directory
self.split_image_folder_input.setText(f"{new_split_image_directory}/")
self.reload_start_image_signal.emit(False, True)
self.reload_images_signal.emit()

def __update_live_image_details(
self,
Expand Down Expand Up @@ -497,62 +489,56 @@ def __update_live_image_details(
else:
set_preview_image(self.live_image, capture)

def __reload_start_image(self, *, started_by_button: bool = False, wait_for_delay: bool = True):
def __reload_images(self):
"""
Not thread safe (if triggered by LiveSplit for example).
Use `reload_start_image_signal.emit` instead.
Use `reload_images_signal.emit` instead.

1. Stops the automated start check and clear the current Split Image.
2. Reloads the Start Image from disk and validate.
2. Reloads all images from disk and validate.
3. If validation passed:
-
- Updates the shown Split Image and Start Image text
- Reinitialise values
- Restart the automated start check
"""
if self.is_running:
raise RuntimeError("Start Image should never be reloaded whilst running!")
raise RuntimeError("Images should never be reloaded whilst running!")

self.timer_start_image.stop()
self.current_image_file_label.setText("-")
self.start_image_status_value_label.setText("not found")
set_preview_image(self.current_split_image, None)

if not (
validate_before_parsing(self, show_error=started_by_button)
and parse_and_validate_images(self)
):
if not parse_and_validate_images(self):
QApplication.processEvents()
return

if not self.start_image:
if started_by_button:
error_messages.no_keyword_image(START_KEYWORD)
QApplication.processEvents()
return

if not wait_for_delay and self.start_image.get_pause_time(self) > 0:
self.start_image_status_value_label.setText("paused")
self.table_current_image_highest_label.setText("-")
self.table_current_image_threshold_label.setText("-")
else:
self.start_image_status_value_label.setText("ready")
if self.start_image:
self.__update_split_image(self.start_image)
# If there is no start hotkey set but a Start Image is present,
# and is not auto controlled, throw an error.
if not (self.settings_dict["split_hotkey"] or self.is_auto_controlled):
self.start_image_status_value_label.setText("not ready")
error_messages.load_start_image()
else:
self.start_image_status_value_label.setText("ready")
self.timer_start_image.start(int(ONE_SECOND / self.start_image.get_fps_limit(self)))

self.split_image_number = 0
self.highest_similarity = 0.0
self.reset_highest_similarity = 0.0
self.split_below_threshold = False

self.timer_start_image.start(int(ONE_SECOND / self.start_image.get_fps_limit(self)))

QApplication.processEvents()

def __compare_capture_for_auto_start(self):
if not self.start_image:
raise ValueError("There are no Start Image. How did we even get here?")

self.start_image_status_value_label.setText("ready")
# Keep comparing without a Start hotkey, so it starts working as soon as one is set
can_start = bool(self.settings_dict["split_hotkey"] or self.is_auto_controlled)
self.start_image_status_value_label.setText("ready" if can_start else "not ready")
self.__update_split_image(self.start_image)

capture = self.__get_capture_for_comparison()
Expand Down Expand Up @@ -583,6 +569,8 @@ def __compare_capture_for_auto_start(self):
and similarity_diff < 0
and is_valid_image(capture)
) or (not below_flag and similarity_diff >= 0):
if not can_start:
return
self.timer_start_image.stop()
self.split_below_threshold = False

Expand Down Expand Up @@ -666,8 +654,10 @@ def __check_fps(self):
self.fps_value_label.clear()
error_messages.region()
return
if not (validate_before_parsing(self) and parse_and_validate_images(self)):
# Use the images loaded by __reload_images rather than re-parsing them from disk
if not self.split_images:
self.fps_value_label.clear()
error_messages.no_split_image()
return

images = [
Expand Down Expand Up @@ -775,35 +765,36 @@ def start_auto_splitter(self):
return

start_label = self.start_image_status_value_label.text()
if start_label.endswith(("ready", "paused")):
if start_label == "ready":
self.start_image_status_value_label.setText("not ready")

self.start_auto_splitter_signal.emit()

def __check_for_reset_state_update_ui(self):
"""Check if AutoSplit is started, if not then update the GUI."""
if not self.is_running:
self.gui_changes_on_reset(safe_to_reload_start_image=True)
self.gui_changes_on_reset(safe_to_reload_images=True)
return True
return False

def __auto_splitter(self): # noqa: C901,PLR0911,PLR0912,PLR0915
def __auto_splitter(self): # noqa: C901, PLR0911, PLR0912, PLR0915
# Set start time as early as possible to avoid any delay caused by additional validation
self.run_start_time = time()

if not self.settings_dict["split_hotkey"] and not self.is_auto_controlled:
self.gui_changes_on_reset(safe_to_reload_start_image=True)
self.gui_changes_on_reset(safe_to_reload_images=True)
error_messages.split_hotkey()
return

# Set start time before parsing the images as it's a heavy operation that will cause delays
self.run_start_time = time()

if not self.capture_method.check_selected_region_exists():
self.gui_changes_on_reset(safe_to_reload_start_image=True)
self.gui_changes_on_reset(safe_to_reload_images=True)
error_messages.region()
return
if not (validate_before_parsing(self) and parse_and_validate_images(self)):
# `safe_to_reload_start_image=False` because __reload_start_image also does this check,
# we don't want to double a Start/Reset Image error message
self.gui_changes_on_reset(safe_to_reload_start_image=False)

# Images failed to load (or were never loaded).
# Reloading (done automatically on reset after failed start) will show the reason.
if not self.split_images:
self.gui_changes_on_reset(safe_to_reload_images=True)
return

# Construct a list of images + loop count tuples.
Expand Down Expand Up @@ -910,7 +901,7 @@ def __auto_splitter(self): # noqa: C901,PLR0911,PLR0912,PLR0915

# loop breaks to here when the last image splits
self.is_running = False
self.gui_changes_on_reset(safe_to_reload_start_image=True)
self.gui_changes_on_reset(safe_to_reload_images=True)

def __similarity_threshold_loop(
self,
Expand Down Expand Up @@ -1026,7 +1017,7 @@ def gui_changes_on_start(self):
self.timer_start_image.stop()
self.start_auto_splitter_button.setText("Running...")
self.split_image_folder_button.setEnabled(False)
self.reload_start_image_button.setEnabled(False)
self.reload_images_button.setEnabled(False)
self.action_load_profile.setEnabled(False)
self.previous_image_button.setEnabled(True)
self.next_image_button.setEnabled(True)
Expand All @@ -1045,7 +1036,7 @@ def gui_changes_on_start(self):

QApplication.processEvents()

def gui_changes_on_reset(self, *, safe_to_reload_start_image: bool = False):
def gui_changes_on_reset(self, *, safe_to_reload_images: bool = False):
self.start_auto_splitter_button.setText("Start Auto Splitter")
self.image_loop_value_label.setText("N/A")
self.current_split_image.clear()
Expand All @@ -1057,7 +1048,7 @@ def gui_changes_on_reset(self, *, safe_to_reload_start_image: bool = False):
self.table_reset_image_highest_label.setText("-")
self.table_reset_image_threshold_label.setText("-")
self.split_image_folder_button.setEnabled(True)
self.reload_start_image_button.setEnabled(True)
self.reload_images_button.setEnabled(True)
self.action_load_profile.setEnabled(True)
self.previous_image_button.setEnabled(False)
self.next_image_button.setEnabled(False)
Expand All @@ -1075,8 +1066,8 @@ def gui_changes_on_reset(self, *, safe_to_reload_start_image: bool = False):
self.skip_split_button.setEnabled(False)

QApplication.processEvents()
if safe_to_reload_start_image:
self.reload_start_image_signal.emit(False, False)
if safe_to_reload_images:
self.reload_images_signal.emit()

def __get_capture_for_comparison(self):
"""Grab capture region and resize for comparison."""
Expand Down Expand Up @@ -1236,7 +1227,7 @@ def _apply_split_controls_layout(self) -> None:
widgets = (
self.image_loop_row,
self.start_image_status_row,
self.reload_start_image_button,
self.reload_images_button,
self.action_row,
self.reset_button,
self.start_auto_splitter_button,
Expand All @@ -1250,7 +1241,7 @@ def _apply_split_controls_layout(self) -> None:
else:
layout.addWidget(self.image_loop_row, 0, 0)
layout.addWidget(self.start_image_status_row, 1, 0)
layout.addWidget(self.reload_start_image_button, 2, 0)
layout.addWidget(self.reload_images_button, 2, 0)
layout.addWidget(self.action_row, 0, 1)
layout.addWidget(self.reset_button, 1, 1)
layout.addWidget(self.start_auto_splitter_button, 2, 1)
Expand Down
3 changes: 2 additions & 1 deletion src/capture_method/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ def get_all_video_capture_devices():

def get_camera_info(index: int, device_name: str):
backend = ""
# Probing freezes some devices (like GV-USB2 and AverMedia) if already in use. See #169
# Probing freezes some devices (like GV-USB2 and AverMedia) if already in use.
# See Toufool/AutoSplit#169
# FIXME: Maybe offer the option to the user to obtain more info about their devices?
# Off by default. With a tooltip to explain the risk.
# video_capture = cv2.VideoCapture(index)
Expand Down
19 changes: 5 additions & 14 deletions src/error_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
)


def __exit_program():
def __exit_program() -> NoReturn:
# stop main thread (which is probably blocked reading input) via an interrupt signal
os.kill(os.getpid(), signal.SIGINT)
sys.exit(1)
Expand Down Expand Up @@ -114,13 +114,13 @@ def region():


def split_hotkey():
_set_text_message("No split hotkey has been set.")
_set_text_message("No Start/Split hotkey has been set.")


def pause_hotkey():
_set_text_message(
"Your Split Image Folder contains an image filename with a pause flag {p}, "
+ "but no pause hotkey is set."
+ "but no Pause hotkey is set."
)


Expand All @@ -132,18 +132,12 @@ def alignment_not_matched():
_set_text_message("No area in capture region matched reference image. Alignment failed.")


def no_keyword_image(keyword: str):
_set_text_message(
f"Your Split Image Folder does not contain an image with the keyword {keyword!r}."
)


def multiple_keyword_images(keyword: str):
_set_text_message(f"Only one image with the keyword {keyword!r} is allowed.")


def reset_hotkey():
_set_text_message("Your Split Image Folder contains a Reset Image, but no reset hotkey is set.")
_set_text_message("Your Split Image Folder contains a Reset Image, but no Reset hotkey is set.")


def old_version_settings_file():
Expand Down Expand Up @@ -183,10 +177,7 @@ def check_for_updates():


def load_start_image():
_set_text_message(
"Start Image found, but cannot be loaded unless Start hotkey is set. "
+ "Please set the hotkey, and then click the Reload Start Image button."
)
_set_text_message("Start Image found, but cannot be used until Start/Split hotkey is set.")


def stdin_lost():
Expand Down
Loading
Loading