Skip to content

Commit

Permalink
Merge pull request #228 from Avasam/2.0.1
Browse files Browse the repository at this point in the history
Hotfixes and documentation updates
  • Loading branch information
Avasam authored Apr 26, 2023
2 parents 48b025d + d273ca3 commit a64877d
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 77 deletions.
53 changes: 36 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Easy to use image comparison based auto splitter for speedrunning on console or

This program can be used to automatically start, split, and reset your preferred speedrun timer by comparing images to a capture region. This allows you to focus more on your speedrun and less on managing your timer. It also improves the accuracy of your splits. It can be used in tandem with any speedrun timer that accepts hotkeys (LiveSplit, wsplit, etc.), and can be integrated with LiveSplit.

![Example](res/2.0.0_gif.gif)
![Example](/docs/2.0.0_gif.gif)

# TUTORIAL

Expand All @@ -24,13 +24,8 @@ This program can be used to automatically start, split, and reset your preferred

### Compatibility

- Python 3.9+
- Windows 10 and 11.

### Building

(This is not required for normal use)
Refer to the [build instructions](build%20instructions.md) if you'd like to build the application yourself or run it directly in Python.
- Python 3.9+ (Not requried for normal use. Refer to the [build instructions](/docs/build%20instructions.md) if you'd like run the application directly in Python).

## OPTIONS

Expand Down Expand Up @@ -84,11 +79,11 @@ Refer to the [build instructions](build%20instructions.md) if you'd like to buil
It can record OpenGL and Hardware Accelerated windows.
About 10-15x slower than BitBlt. Not affected by window size.
overlapping windows will show up and can't record across displays.
This option may not be available for hybrid GPU laptops, see [D3DDD-Note-Laptops.md](/D3DDD-Note-Laptops.md) for a solution.
This option may not be available for hybrid GPU laptops, see [D3DDD-Note-Laptops.md](/docs/D3DDD-Note-Laptops.md) for a solution.
- **Force Full Content Rendering** (very slow, can affect rendering)
Uses BitBlt behind the scene, but passes a special flag to PrintWindow to force rendering the entire desktop.
About 10-15x slower than BitBlt based on original window size and can mess up some applications' rendering pipelines.
- **Video Capture Device**
- **Video Capture Device**
Uses a Video Capture Device, like a webcam, virtual cam, or capture card.
If you want to use this with OBS' Virtual Camera, use the [Virtualcam plugin](https://github.com/Avasam/obs-virtual-cam/releases) instead.

Expand Down Expand Up @@ -144,11 +139,6 @@ If this option is enabled, when the last split meets the threshold and splits, A
If this option is disabled, when the last split meets the threshold and splits, AutoSplit will stop running comparisons.
This option does not loop single, specific images. See the Custom Split Image Settings section above for this feature.

#### Auto Start On Reset

If this option is enabled, when the reset hotkey is hit, the reset button is pressed, or the reset split image meets its threshold, AutoSplit will reset and automatically start again back at the first split image.
If this option is disabled, when the reset hotkey is hit, the reset button is pressed, or the reset split image meets its threshold, AutoSplit will stop running comparisons.

### Custom Split Image Settings

- Each split image can have different thresholds, pause times, delay split times, loop amounts, and can be flagged.
Expand Down Expand Up @@ -179,7 +169,7 @@ Masked images are very useful if only a certain part of the capture region is co

The best way to create a masked image is to set your capture region as the entire game screen, take a screenshot, and use a program like [paint.net](https://www.getpaint.net/) to "erase" (make transparent) everything you don't want the program to compare. More on creating images with transparency using paint.net can be found in [this tutorial](https://www.youtube.com/watch?v=v53kkUYFVn8). For visualization, here is what the capture region compared to a masked split image looks like if you would want to split on "Shine Get!" text in Super Mario Sunshine:

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

### Reset image

Expand Down Expand Up @@ -226,15 +216,44 @@ The AutoSplit LiveSplit Component will directly connect AutoSplit with LiveSplit

- For many games, it will be difficult to find a split image for the last split of the run.
- The window of the capture region cannot be minimized.
- OBS' integrated Virtual Camera does not work and makes AutoSplit crash.

## Resources

Still need help?

- [Check if your issue already exists or open a new one](../../issues)
<!-- open issues sorted by reactions -->
- [Check if your issue already exists](../../issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc)
- If it does, upvote it 👍
- If it doesn't, create a new one
- Join the [AutoSplit Discord
![AutoSplit Discord](https://badgen.net/discord/members/Qcbxv9y)](https://discord.gg/Qcbxv9y)

## Contributing

See [CONTRIBUTING.md](/docs/CONTRIBUTING.md) for our contributing standards.
Refer to the [build instructions](/docs/build%20instructions.md) if you're interested in building the application yourself or running it in Python.

Not a developper? You can still help through the following methods:
<!-- open enhancements sorted by reactions -->
- Donating (see link below)
- [Upvoting feature requests](../../issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement) you are interested in
- Sharing AutoSplit with other speedrunners
- Upvoting the following upstream issues in libraries and tools we use:
- <https://github.com/opencv/opencv/issues/23539>
- <https://github.com/opencv/opencv/issues/14590>
- <https://github.com/opencv/opencv/issues/23537>
- <https://github.com/opencv/opencv/issues/23158>
- <https://github.com/opencv/opencv/issues/22632>
- <https://github.com/pywinrt/python-winsdk/issues/11>
- <https://github.com/adamchainz/pre-commit-dprint/issues/4>
- <https://github.com/mhammond/pywin32/issues/1913>
- <https://github.com/microsoft/vscode/issues/40239>
- <https://github.com/hhatto/autopep8/issues/675>
- <https://github.com/boppreh/keyboard/issues/171>
- <https://github.com/boppreh/keyboard/issues/516>
- <https://github.com/boppreh/keyboard/issues/216>
- <https://github.com/boppreh/keyboard/issues/161>

## Credits

- Created by [Toufool](https://twitter.com/Toufool) and [Faschz](https://twitter.com/faschz).
Expand Down
File renamed without changes
33 changes: 33 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!-- This has to in the repository's root, `docs/`, or `.github/` directory to be picked by github. Read more at: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors#about-contributing-guidelines -->

# Contributing guidelines

## Python Setup and Building

Refer to the [build instructions](/docs/build%20instructions.md) if you're interested in building the application yourself or running it in Python.

## Linting and formatting

The project is setup to automatically configure VSCode witht he proper extensions and settings. Linters and formatters will be run on save.
If you use a different IDE or for some reason cannot / don't want to use the recommended extensions, you can run `scripts/lint.ps1`.

If you like to use pre-commit hooks, `.pre-commit-config.yaml` is setup for such uses.

The CI will automatically fix and commit any autofixable issue your changes may have.

## Pull Request Guidelines

If your pull request is meant to address an open issue, please link it as part of your Pull Request description. If it would close said issue, please use a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).

Try not to Force Push once your Pull Request is open, unless absolutely necessary. It is easier for reviewers to keep track of reviewed and new changes if you don't. The Pull Request should be squashed-merged anyway.

Your Pull Request has to pass all checks ot be accepted. If it is still a work-in-progress, please [mark it as draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#converting-a-pull-request-to-a-draft).

## Coding Standards

Most coding standards will be enforced by automated tooling.
As time goes on, project-specific standards and "gotchas" in the frameworks we use will be listed here.

## Testing

None 😦 Please help us create test suites, we lack the time, but we really want (need!) them. <https://github.com/Toufool/AutoSplit/issues/216>
File renamed without changes.
14 changes: 8 additions & 6 deletions build instructions.md → docs/build instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

## Install and Build steps

- Read [requirements.txt](/scripts/requirements.txt) for more information on how to install, run and build the python code.
- Run `./scripts/install.ps1` to install all dependencies.
- Run the app directly with `./scripts/start.ps1 [--auto-controlled]`.
- Or debug by pressing `F5` in VSCode
- Run `./scripts/build.ps1` or press `CTRL+Shift+B` in VSCode to build an executable.
- Recompile resources after modifications by running `./scripts/compile_resources.ps1`.
- Run `./scripts/install.ps1` to install all dependencies.
- If you're having issues with the PySide generated code, you might want to first run `pip uninstall -y shiboken6 PySide PySide-Essentials`
- Run the app directly with `./scripts/start.ps1 [--auto-controlled]`.
- Or debug by pressing `F5` in VSCode.
- The `--auto-controlled` flag is passed when AutoSplit is started by LiveSplit.
- Run `./scripts/build.ps1` or press `CTRL+Shift+B` in VSCode to build an executable.
- Optional: Recompile resources after modifications by running `./scripts/compile_resources.ps1`.
- This should be done automatically by other scripts
File renamed without changes
9 changes: 1 addition & 8 deletions scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
# Requirements file for AutoSplit
#
# Python: CPython 3.9+
#
# Usage: ./scripts/install.ps1
#
# If you're having issues with the libraries, you might want to first run:
# pip uninstall -y -r ./scripts/requirements-dev.txt
#
# Creating an AutoSplit executable with PyInstaller: ./scripts/build.ps1
# Read /docs/build%20instructions.md for more information on how to install, run and build the python code.
#
# Dependencies:
certifi
Expand Down
59 changes: 34 additions & 25 deletions src/AutoSplit.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow): # pylint: disable=too-many-
split_image_number = 0
split_images_and_loop_number: list[tuple[AutoSplitImage, int]] = []
split_groups: list[list[int]] = []
capture_method = CaptureMethodBase()
capture_method = CaptureMethodBase(None)
is_running = False

# Last loaded settings empty and last successful loaded settings file path to None until we try to load them
Expand Down Expand Up @@ -206,8 +206,8 @@ def __init__(self, parent: QWidget | None = None): # pylint: disable=too-many-s
self.pause_signal.connect(self.pause)

# live image checkbox
self.timer_live_image.timeout.connect(self.__live_image_function)
self.timer_live_image.start(int(1000 / 60))
self.timer_live_image.timeout.connect(lambda: self.__update_live_image_details(None, True))
self.timer_live_image.start(int(1000 / self.settings_dict["fps_limit"]))

# Automatic timer start
self.timer_start_image.timeout.connect(self.__start_image_function)
Expand Down Expand Up @@ -244,16 +244,26 @@ def __browse(self):
self.split_image_folder_input.setText(f"{new_split_image_directory}/")
self.load_start_image_signal.emit()

def __live_image_function(self):
def __update_live_image_details(self, capture: cv2.Mat | None, called_from_timer: bool = False):
# HACK: Since this is also called in __get_capture_for_comparison,
# we don't need to update anything if the app is running
if called_from_timer:
if self.is_running or self.start_image:
return
else:
capture, _ = self.capture_method.get_frame(self)

# Update title from target window or Capture Device name
capture_region_window_label = self.settings_dict["capture_device_name"] \
if self.settings_dict["capture_method"] == CaptureMethodEnum.VIDEO_CAPTURE_DEVICE \
else self.settings_dict["captured_window_title"]
self.capture_region_window_label.setText(capture_region_window_label)

# Simply clear if "live capture region" setting is off
if not (self.settings_dict["live_capture_region"] and capture_region_window_label):
self.live_image.clear()
return
# Set live image in UI
capture, _ = self.capture_method.get_frame(self)

set_preview_image(self.live_image, capture, False)

def __load_start_image(self, started_by_button: bool = False, wait_for_delay: bool = True):
Expand Down Expand Up @@ -337,23 +347,24 @@ def __start_image_function(self):
self.timer_start_image.stop()
self.split_below_threshold = False

# delay start image if needed
if self.start_image.get_delay_time(self) > 0:
self.start_image_status_value_label.setText("delaying start...")
delay_start_time = time()
start_delay = self.start_image.get_delay_time(self) / 1000
time_delta = 0
while time_delta < start_delay:
delay_time_left = start_delay - time_delta
self.current_split_image.setText(
f"Delayed Before Starting:\n {seconds_remaining_text(delay_time_left)}",
)
# Wait 0.1s. Doesn't need to be shorter as we only show 1 decimal
QTest.qWait(100)
time_delta = time() - delay_start_time
if not self.start_image.check_flag(DUMMY_FLAG):
# Delay start image if needed
if self.start_image.get_delay_time(self) > 0:
self.start_image_status_value_label.setText("delaying start...")
delay_start_time = time()
start_delay = self.start_image.get_delay_time(self) / 1000
time_delta = 0
while time_delta < start_delay:
delay_time_left = start_delay - time_delta
self.current_split_image.setText(
f"Delayed Before Starting:\n {seconds_remaining_text(delay_time_left)}",
)
# Wait 0.1s. Doesn't need to be shorter as we only show 1 decimal
QTest.qWait(100)
time_delta = time() - delay_start_time
send_command(self, "start")

self.start_image_status_value_label.setText("started")
send_command(self, "start")
self.start_auto_splitter()

# update x, y, width, height when spinbox values are changed
Expand Down Expand Up @@ -506,10 +517,7 @@ def __check_for_reset_state_update_ui(self):
Check if AutoSplit is started, if not either restart (loop splits) or update the GUI
"""
if not self.is_running:
if self.settings_dict["loop_splits"]:
self.start_auto_splitter_signal.emit()
else:
self.gui_changes_on_reset(True)
self.gui_changes_on_reset(True)
return True
return False

Expand Down Expand Up @@ -803,6 +811,7 @@ def __get_capture_for_comparison(self):
if recovered:
capture, _ = self.capture_method.get_frame(self)

self.__update_live_image_details(capture)
return capture, is_old_image

def __reset_if_should(self, capture: cv2.Mat | None):
Expand Down
8 changes: 5 additions & 3 deletions src/capture_method/BitBltCaptureMethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from win32 import win32gui

from capture_method.CaptureMethodBase import CaptureMethodBase
from utils import get_window_bounds, is_valid_hwnd
from utils import get_window_bounds, is_valid_hwnd, try_delete_dc

if TYPE_CHECKING:
from AutoSplit import AutoSplit
Expand Down Expand Up @@ -58,10 +58,12 @@ def get_frame(self, autosplit: AutoSplit) -> tuple[cv2.Mat | None, bool]:
image = np.frombuffer(cast(bytes, bitmap.GetBitmapBits(True)), dtype=np.uint8)
image.shape = (selection["height"], selection["width"], 4)
except (win32ui.error, pywintypes.error):
# Invalid handle or the window was closed while it was being manipulated
return None, False

# Cleanup DC and handle
dc_object.DeleteDC()
compatible_dc.DeleteDC()
try_delete_dc(dc_object)
try_delete_dc(compatible_dc)
win32gui.ReleaseDC(hwnd, window_dc)
win32gui.DeleteObject(bitmap.GetHandle())
return image, False
Expand Down
2 changes: 1 addition & 1 deletion src/capture_method/CaptureMethodBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


class CaptureMethodBase():
def __init__(self, autosplit: AutoSplit | None = None):
def __init__(self, autosplit: AutoSplit | None):
# Some capture methods don't need an initialization process
pass

Expand Down
4 changes: 2 additions & 2 deletions src/capture_method/DesktopDuplicationCaptureMethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@


class DesktopDuplicationCaptureMethod(BitBltCaptureMethod): # pylint: disable=too-few-public-methods
def __init__(self):
super().__init__()
def __init__(self, autosplit: AutoSplit | None):
super().__init__(autosplit)
# Must not set statically as some laptops will throw an error
self.desktop_duplication = d3dshot.create(capture_output="numpy")

Expand Down
14 changes: 9 additions & 5 deletions src/capture_method/VideoCaptureDeviceCaptureMethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ def __read_loop(self, autosplit: AutoSplit):
result, image = self.capture_device.read()
except cv2.error as error:
if not (
error.code == cv2.Error.STS_ERROR and error.msg.endswith(
"in function 'cv::VideoCapture::grab'\n",
error.code == cv2.Error.STS_ERROR
and (
# Likely means the camera is occupied
error.msg.endswith("in function 'cv::VideoCapture::grab'\n")
# Some capture cards we cannot use directly
# https://github.com/opencv/opencv/issues/23539
or error.msg.endswith("in function 'cv::VideoCapture::retrieve'\n")
)
):
raise
# STS_ERROR most likely means the camera is occupied
result = False
image = None
if not result:
Expand All @@ -68,7 +72,7 @@ def __read_loop(self, autosplit: AutoSplit):
)

def __init__(self, autosplit: AutoSplit):
super().__init__()
super().__init__(autosplit)
filter_graph = dshow_graph.FilterGraph()
filter_graph.add_video_input_device(autosplit.settings_dict["capture_device_id"])
width, height = filter_graph.get_input_device().get_current_format()
Expand All @@ -80,8 +84,8 @@ def __init__(self, autosplit: AutoSplit):
try:
self.capture_device.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.capture_device.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# Some cameras don't allow changing the resolution
except cv2.error:
# Some cameras don't allow changing the resolution
pass
self.stop_thread = Event()
self.capture_thread = Thread(target=lambda: self.__read_loop(autosplit))
Expand Down
Loading

0 comments on commit a64877d

Please sign in to comment.