ok-script 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autohelper/AutoHelper.py +116 -0
- autohelper/__init__.py +0 -0
- autohelper/capture/BaseCaptureMethod.py +30 -0
- autohelper/capture/HwndWindow.py +138 -0
- autohelper/capture/__init__.py +0 -0
- autohelper/capture/adb/ADBCaptureMethod.py +29 -0
- autohelper/capture/adb/DeviceManager.py +209 -0
- autohelper/capture/adb/__init__.py +0 -0
- autohelper/capture/adb/bin/__init__.py +0 -0
- autohelper/capture/adb/targets.py +61 -0
- autohelper/capture/adb/vbox.py +140 -0
- autohelper/capture/windows/WindowsGraphicsCaptureMethod.py +228 -0
- autohelper/capture/windows/__init__.py +0 -0
- autohelper/capture/windows/d3d11.py +74 -0
- autohelper/capture/windows/utils.py +149 -0
- autohelper/capture/windows/window.py +36 -0
- autohelper/color/Color.py +41 -0
- autohelper/color/__init__.py +0 -0
- autohelper/config/Config.py +79 -0
- autohelper/config/InfoDict.py +16 -0
- autohelper/config/__init__.py +0 -0
- autohelper/feature/Box.py +199 -0
- autohelper/feature/Feature.py +20 -0
- autohelper/feature/FeatureSet.py +207 -0
- autohelper/feature/FindFeature.py +52 -0
- autohelper/feature/__init__.py +0 -0
- autohelper/gui/App.py +120 -0
- autohelper/gui/Communicate.py +22 -0
- autohelper/gui/MainWindow.py +69 -0
- autohelper/gui/TabTitles.py +56 -0
- autohelper/gui/__init__.py +5 -0
- autohelper/gui/debug/AspectRatioWidget.py +31 -0
- autohelper/gui/debug/DebugTab.py +37 -0
- autohelper/gui/debug/FrameWidget.py +125 -0
- autohelper/gui/debug/InfoWidget.py +40 -0
- autohelper/gui/debug/LoggerWidget.py +74 -0
- autohelper/gui/debug/__init__.py +0 -0
- autohelper/gui/i18n/__init__.py +0 -0
- autohelper/gui/i18n/path.py +3 -0
- autohelper/gui/icon/__init__.py +0 -0
- autohelper/gui/icon/icon.py +6 -0
- autohelper/gui/loading/LoadingWindow.py +142 -0
- autohelper/gui/loading/__init__.py +0 -0
- autohelper/gui/overlay/OverlayWindow.py +34 -0
- autohelper/gui/overlay/__init__.py +0 -0
- autohelper/gui/resources.py +485 -0
- autohelper/gui/tasks/ConfigItemFactory.py +14 -0
- autohelper/gui/tasks/StartButton.py +47 -0
- autohelper/gui/tasks/TaskOpButton.py +33 -0
- autohelper/gui/tasks/TaskTab.py +121 -0
- autohelper/gui/tasks/TooltipTableWidget.py +39 -0
- autohelper/gui/tasks/__init__.py +0 -0
- autohelper/gui/util/Alert.py +20 -0
- autohelper/gui/util/InitWorker.py +17 -0
- autohelper/gui/util/__init__.py +0 -0
- autohelper/gui/widget/ListTableWidgetItem.py +27 -0
- autohelper/gui/widget/NumericTableWidgetItem.py +33 -0
- autohelper/gui/widget/RoundCornerContainer.py +59 -0
- autohelper/gui/widget/TabWidget.py +30 -0
- autohelper/gui/widget/UpdateConfigWidgetItem.py +16 -0
- autohelper/gui/widget/YesNonWidgetItem.py +23 -0
- autohelper/gui/widget/__init__.py +0 -0
- autohelper/interaction/ADBInteraction.py +33 -0
- autohelper/interaction/BaseInteraction.py +36 -0
- autohelper/interaction/Win32Interaction.py +56 -0
- autohelper/interaction/__init__.py +0 -0
- autohelper/logging/Logger.py +103 -0
- autohelper/logging/__init__.py +0 -0
- autohelper/ocr/OCR.py +50 -0
- autohelper/ocr/__init__.py +0 -0
- autohelper/predict/__init__.py +0 -0
- autohelper/predict/predict.py +45 -0
- autohelper/rotypes/Windows/Foundation/Collections.py +60 -0
- autohelper/rotypes/Windows/Foundation/__init__.py +153 -0
- autohelper/rotypes/Windows/Globalization/__init__.py +25 -0
- autohelper/rotypes/Windows/Graphics/Capture/__init__.py +110 -0
- autohelper/rotypes/Windows/Graphics/DirectX/Direct3D11/__init__.py +41 -0
- autohelper/rotypes/Windows/Graphics/DirectX/__init__.py +167 -0
- autohelper/rotypes/Windows/Graphics/Imaging/__init__.py +34 -0
- autohelper/rotypes/Windows/Graphics/__init__.py +5 -0
- autohelper/rotypes/Windows/Media/Ocr/__init__.py +76 -0
- autohelper/rotypes/Windows/Media/__init__.py +0 -0
- autohelper/rotypes/Windows/Security/Cryptography/__init__.py +18 -0
- autohelper/rotypes/Windows/Security/__init__.py +0 -0
- autohelper/rotypes/Windows/Storage/Streams/__init__.py +27 -0
- autohelper/rotypes/Windows/Storage/__init__.py +0 -0
- autohelper/rotypes/Windows/__init__.py +0 -0
- autohelper/rotypes/__init__.py +4 -0
- autohelper/rotypes/delegate.py +105 -0
- autohelper/rotypes/export.py +21 -0
- autohelper/rotypes/idldsl.py +272 -0
- autohelper/rotypes/inspectable.py +79 -0
- autohelper/rotypes/roapi.py +30 -0
- autohelper/rotypes/types.py +77 -0
- autohelper/rotypes/winstring.py +44 -0
- autohelper/save/BlackBarProcessor.py +25 -0
- autohelper/save/PostProcessor.py +6 -0
- autohelper/save/SaveByInterval.py +30 -0
- autohelper/save/SaveByKeyPress.py +27 -0
- autohelper/save/SaveMethodBase.py +46 -0
- autohelper/save/__init__.py +0 -0
- autohelper/scene/FeatureScene.py +14 -0
- autohelper/scene/Scene.py +21 -0
- autohelper/scene/__init__.py +0 -0
- autohelper/stats/StreamStats.py +34 -0
- autohelper/stats/__init__.py +0 -0
- autohelper/task/BaseTask.py +109 -0
- autohelper/task/ExecutorOperation.py +119 -0
- autohelper/task/FindFeatureTask.py +6 -0
- autohelper/task/TaskExecutor.py +249 -0
- autohelper/task/__init__.py +0 -0
- autohelper/util/__init__.py +0 -0
- autohelper/util/json.py +27 -0
- autohelper/util/list.py +2 -0
- autohelper/util/path.py +37 -0
- autohelper/util/thread.py +12 -0
- autohelper/util/win32_process.py +86 -0
- autohelper/util/yaml.py +28 -0
- ok_script-0.0.7.dist-info/METADATA +64 -0
- ok_script-0.0.7.dist-info/RECORD +122 -0
- ok_script-0.0.7.dist-info/WHEEL +5 -0
- ok_script-0.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StreamStats:
|
|
7
|
+
last_frame_time = 0
|
|
8
|
+
sleep_padding = 0
|
|
9
|
+
|
|
10
|
+
def __init__(self, max_size=100):
|
|
11
|
+
self.max_size = max_size
|
|
12
|
+
self.data = []
|
|
13
|
+
|
|
14
|
+
def add_frame(self):
|
|
15
|
+
now = time.time()
|
|
16
|
+
if self.last_frame_time != 0:
|
|
17
|
+
if len(self.data) >= self.max_size:
|
|
18
|
+
self.data.pop(0)
|
|
19
|
+
self.data.append(int((now - self.last_frame_time - self.sleep_padding) * 1000))
|
|
20
|
+
self.last_frame_time = now
|
|
21
|
+
self.sleep_padding = 0
|
|
22
|
+
|
|
23
|
+
def add_sleep(self, seconds):
|
|
24
|
+
self.sleep_padding += seconds
|
|
25
|
+
|
|
26
|
+
def mean(self):
|
|
27
|
+
"""Calculate and return the mean of the numbers in the stream."""
|
|
28
|
+
return round(np.mean(self.data)) if self.data else 0
|
|
29
|
+
|
|
30
|
+
def percentile(self, percentile):
|
|
31
|
+
"""Calculate and return the specified percentile of the numbers in the stream."""
|
|
32
|
+
if not self.data:
|
|
33
|
+
return 0
|
|
34
|
+
return np.percentile(self.data, percentile)
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from autohelper.config.Config import Config
|
|
4
|
+
from autohelper.config.InfoDict import InfoDict
|
|
5
|
+
from autohelper.gui.Communicate import communicate
|
|
6
|
+
from autohelper.logging.Logger import get_logger
|
|
7
|
+
from autohelper.task.ExecutorOperation import ExecutorOperation
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseTask(ExecutorOperation):
|
|
13
|
+
_done = False
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
17
|
+
self.name = self.__class__.__name__
|
|
18
|
+
self.success_count = 0
|
|
19
|
+
self.error_count = 0
|
|
20
|
+
self.enabled = True
|
|
21
|
+
self.running = False
|
|
22
|
+
self.config = None
|
|
23
|
+
self.info = InfoDict()
|
|
24
|
+
self.default_config = {}
|
|
25
|
+
self.last_execute_time = 0
|
|
26
|
+
|
|
27
|
+
def log_info(self, message, notify=False):
|
|
28
|
+
self.logger.info(message)
|
|
29
|
+
self.info_set("Log", message)
|
|
30
|
+
if notify:
|
|
31
|
+
self.notification(message)
|
|
32
|
+
communicate.task_info.emit()
|
|
33
|
+
|
|
34
|
+
def log_error(self, message, exception=None, notify=False):
|
|
35
|
+
self.logger.error(message, exception)
|
|
36
|
+
if exception is not None:
|
|
37
|
+
message += exception.args[0]
|
|
38
|
+
self.info_set("Error", message)
|
|
39
|
+
if notify:
|
|
40
|
+
self.notification(message)
|
|
41
|
+
communicate.task_info.emit()
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def notification(message, title=None):
|
|
45
|
+
communicate.notification.emit(title, message)
|
|
46
|
+
|
|
47
|
+
def info_clear(self):
|
|
48
|
+
self.info.clear()
|
|
49
|
+
|
|
50
|
+
def info_incr(self, key, by=1):
|
|
51
|
+
# If the key is in the dictionary, get its value. If not, return 0.
|
|
52
|
+
value = self.info.get(key, 0)
|
|
53
|
+
# Increment the value
|
|
54
|
+
value += by
|
|
55
|
+
# Store the incremented value back in the dictionary
|
|
56
|
+
self.info[key] = value
|
|
57
|
+
|
|
58
|
+
def info_add_to_list(self, key, item):
|
|
59
|
+
value = self.info.get(key, [])
|
|
60
|
+
if isinstance(item, list):
|
|
61
|
+
value += item
|
|
62
|
+
else:
|
|
63
|
+
value.append(item)
|
|
64
|
+
self.info[key] = value
|
|
65
|
+
|
|
66
|
+
def info_set(self, key, value):
|
|
67
|
+
self.info[key] = value
|
|
68
|
+
|
|
69
|
+
def can_run(self):
|
|
70
|
+
return self.enabled and not self._done
|
|
71
|
+
|
|
72
|
+
def load_config(self, folder):
|
|
73
|
+
self.config = Config(self.default_config, folder, self.__class__.__name__)
|
|
74
|
+
|
|
75
|
+
def enable(self):
|
|
76
|
+
self.enabled = True
|
|
77
|
+
self.info_clear()
|
|
78
|
+
self.set_done(False)
|
|
79
|
+
communicate.tasks.emit()
|
|
80
|
+
|
|
81
|
+
def disable(self):
|
|
82
|
+
self.enabled = False
|
|
83
|
+
communicate.tasks.emit()
|
|
84
|
+
|
|
85
|
+
def get_status(self):
|
|
86
|
+
if not self.enabled:
|
|
87
|
+
return "Disabled"
|
|
88
|
+
elif self.done:
|
|
89
|
+
return "Done"
|
|
90
|
+
elif self.enabled and self.executor.paused:
|
|
91
|
+
return "Paused"
|
|
92
|
+
elif self.running or self.enabled and time.time() - self.last_execute_time < 3:
|
|
93
|
+
return "Running"
|
|
94
|
+
else:
|
|
95
|
+
return "In Queue"
|
|
96
|
+
|
|
97
|
+
def run_frame(self):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def reset(self):
|
|
101
|
+
self._done = False
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def done(self):
|
|
106
|
+
return self._done
|
|
107
|
+
|
|
108
|
+
def set_done(self, done=True):
|
|
109
|
+
self._done = done
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from autohelper.feature.Box import Box, find_box_by_name
|
|
2
|
+
from autohelper.gui.Communicate import communicate
|
|
3
|
+
from autohelper.logging.Logger import get_logger
|
|
4
|
+
|
|
5
|
+
logger = get_logger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExecutorOperation:
|
|
9
|
+
executor = None
|
|
10
|
+
|
|
11
|
+
def box_in_horizontal_center(self, box, off_percent=0.02):
|
|
12
|
+
center = self.executor.method.width / 2
|
|
13
|
+
left = center - box.x
|
|
14
|
+
right = box.x + box.width - center
|
|
15
|
+
if left > 0 and right > 0 and abs(left - right) / box.width < off_percent:
|
|
16
|
+
return True
|
|
17
|
+
else:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
def is_scene(self, the_scene):
|
|
21
|
+
return isinstance(self.executor.current_scene, the_scene)
|
|
22
|
+
|
|
23
|
+
def click(self, x, y):
|
|
24
|
+
self.executor.reset_scene()
|
|
25
|
+
self.executor.interaction.click(x, y)
|
|
26
|
+
|
|
27
|
+
def click_box_if_name_match(self, boxes, names, relative_x=0.5, relative_y=0.5):
|
|
28
|
+
"""
|
|
29
|
+
Clicks on a box from a list of boxes if the box's name matches one of the specified names.
|
|
30
|
+
The box to click is selected based on the order of names provided, with priority given
|
|
31
|
+
to the earliest match in the names list.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
- boxes (list): A list of box objects. Each box object must have a 'name' attribute.
|
|
35
|
+
- names (str or list): A string or a list of strings representing the name(s) to match against the boxes' names.
|
|
36
|
+
- relative_x (float, optional): The relative X coordinate within the box to click,
|
|
37
|
+
as a fraction of the box's width. Defaults to 0.5 (center).
|
|
38
|
+
- relative_y (float, optional): The relative Y coordinate within the box to click,
|
|
39
|
+
as a fraction of the box's height. Defaults to 0.5 (center).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
- box: the matched box
|
|
43
|
+
|
|
44
|
+
The method attempts to find and click on the highest-priority matching box. If no matches are found,
|
|
45
|
+
or if there are no boxes, the method returns False. This operation is case-sensitive.
|
|
46
|
+
"""
|
|
47
|
+
to_click = find_box_by_name(boxes, names)
|
|
48
|
+
if to_click is not None:
|
|
49
|
+
logger.info(f"click_box_if_name_match found {to_click}")
|
|
50
|
+
self.click_box(to_click, relative_x, relative_y)
|
|
51
|
+
return to_click
|
|
52
|
+
|
|
53
|
+
def box_of_screen(self, x, y, width, height, name=None):
|
|
54
|
+
if name is None:
|
|
55
|
+
name = f"{x} {y} {width} {height}"
|
|
56
|
+
return Box(int(x * self.executor.method.width), int(y * self.executor.method.height),
|
|
57
|
+
int(width * self.executor.method.width), int(height * self.executor.method.height),
|
|
58
|
+
name=name)
|
|
59
|
+
|
|
60
|
+
def click_relative(self, x, y):
|
|
61
|
+
self.executor.reset_scene()
|
|
62
|
+
self.executor.interaction.click_relative(x, y)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def height(self):
|
|
66
|
+
return self.executor.method.height
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def width(self):
|
|
70
|
+
return self.executor.method.width
|
|
71
|
+
|
|
72
|
+
def move_relative(self, x, y):
|
|
73
|
+
self.executor.reset_scene()
|
|
74
|
+
self.executor.interaction.move_relative(x, y)
|
|
75
|
+
|
|
76
|
+
def click_box(self, box, relative_x=0.5, relative_y=0.5, raise_if_not_found=True):
|
|
77
|
+
self.executor.reset_scene()
|
|
78
|
+
if isinstance(box, list):
|
|
79
|
+
if len(box) > 0:
|
|
80
|
+
box = box[0]
|
|
81
|
+
else:
|
|
82
|
+
logger.error(f"No box")
|
|
83
|
+
if box is None:
|
|
84
|
+
logger.error(f"click_box box is None")
|
|
85
|
+
if raise_if_not_found:
|
|
86
|
+
raise Exception(f"click_box box is None")
|
|
87
|
+
return
|
|
88
|
+
self.executor.interaction.click_box(box, relative_x, relative_y)
|
|
89
|
+
|
|
90
|
+
def wait_scene(self, scene_type=None, time_out=0, pre_action=None, post_action=None):
|
|
91
|
+
return self.executor.wait_scene(scene_type, time_out, pre_action, post_action)
|
|
92
|
+
|
|
93
|
+
def sleep(self, timeout):
|
|
94
|
+
self.executor.sleep(timeout)
|
|
95
|
+
|
|
96
|
+
def send_key(self, key, down_time=0.02):
|
|
97
|
+
self.executor.interaction.send_key(key, down_time)
|
|
98
|
+
|
|
99
|
+
def wait_until(self, condition, time_out=0, pre_action=None, post_action=None):
|
|
100
|
+
return self.executor.wait_condition(condition, time_out, pre_action, post_action)
|
|
101
|
+
|
|
102
|
+
def wait_click_box(self, condition, time_out=0, pre_action=None, post_action=None, raise_if_not_found=True):
|
|
103
|
+
target = self.wait_until(condition, time_out, pre_action, post_action)
|
|
104
|
+
self.click_box(target, raise_if_not_found=raise_if_not_found)
|
|
105
|
+
|
|
106
|
+
def next_frame(self):
|
|
107
|
+
return self.executor.next_frame()
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def scene(self):
|
|
111
|
+
return self.executor.current_scene
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def frame(self):
|
|
115
|
+
return self.executor.frame
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def draw_boxes(feature_name, boxes, color="red"):
|
|
119
|
+
communicate.draw_box.emit(feature_name, boxes, color)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
from autohelper.capture.adb.DeviceManager import DeviceManager
|
|
6
|
+
from autohelper.gui.Communicate import communicate
|
|
7
|
+
from autohelper.logging.Logger import get_logger
|
|
8
|
+
from autohelper.scene.Scene import Scene
|
|
9
|
+
from autohelper.stats.StreamStats import StreamStats
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskDisabledException(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskExecutor:
|
|
19
|
+
current_scene: Scene | None = None
|
|
20
|
+
last_scene: Scene | None = None
|
|
21
|
+
frame_stats = StreamStats()
|
|
22
|
+
_frame = None
|
|
23
|
+
paused = True
|
|
24
|
+
ocr = None
|
|
25
|
+
pause_start = time.time()
|
|
26
|
+
pause_end_time = time.time()
|
|
27
|
+
|
|
28
|
+
def __init__(self, device_manager: DeviceManager,
|
|
29
|
+
wait_until_timeout=10, wait_until_before_delay=1, wait_until_check_delay=1,
|
|
30
|
+
exit_event=threading.Event(), tasks=[], scenes=[], feature_set=None, ocr=None, config_folder=None):
|
|
31
|
+
self.device_manager = device_manager
|
|
32
|
+
self.feature_set = feature_set
|
|
33
|
+
self.wait_until_check_delay = wait_until_check_delay
|
|
34
|
+
self.wait_until_before_delay = wait_until_before_delay
|
|
35
|
+
self.wait_scene_timeout = wait_until_timeout
|
|
36
|
+
self.exit_event = exit_event
|
|
37
|
+
self.ocr = ocr
|
|
38
|
+
self.current_task = None
|
|
39
|
+
self.tasks = tasks
|
|
40
|
+
self.scenes = scenes
|
|
41
|
+
self.config_folder = config_folder or "config"
|
|
42
|
+
for scene in self.scenes:
|
|
43
|
+
scene.executor = self
|
|
44
|
+
scene.feature_set = self.feature_set
|
|
45
|
+
for task in self.tasks:
|
|
46
|
+
task.executor = self
|
|
47
|
+
task.feature_set = self.feature_set
|
|
48
|
+
task.load_config(self.config_folder)
|
|
49
|
+
self.thread = threading.Thread(target=self.execute, name="TaskExecutor")
|
|
50
|
+
self.thread.start()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def interaction(self):
|
|
54
|
+
return self.device_manager.interaction
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def method(self):
|
|
58
|
+
return self.device_manager.capture_method
|
|
59
|
+
|
|
60
|
+
def next_frame(self):
|
|
61
|
+
self.reset_scene()
|
|
62
|
+
start = time.time()
|
|
63
|
+
while not self.exit_event.is_set():
|
|
64
|
+
if self.method and self.interaction.should_capture():
|
|
65
|
+
self._frame = self.method.get_frame()
|
|
66
|
+
if self._frame is not None:
|
|
67
|
+
height, width = self._frame.shape[:2]
|
|
68
|
+
if height <= 0 or width <= 0:
|
|
69
|
+
logger.warning(f"captured wrong size frame: {width}x{height}")
|
|
70
|
+
self._frame = None
|
|
71
|
+
communicate.frame.emit(self._frame)
|
|
72
|
+
return self._frame
|
|
73
|
+
self.sleep(0.001)
|
|
74
|
+
if time.time() - start > self.wait_scene_timeout:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def frame(self):
|
|
79
|
+
if self._frame is None:
|
|
80
|
+
return self.next_frame()
|
|
81
|
+
else:
|
|
82
|
+
return self._frame
|
|
83
|
+
|
|
84
|
+
def sleep(self, timeout):
|
|
85
|
+
"""
|
|
86
|
+
Sleeps for the specified timeout, checking for an exit event every 100ms, with adjustments to prevent oversleeping.
|
|
87
|
+
|
|
88
|
+
:param timeout: The total time to sleep in seconds.
|
|
89
|
+
"""
|
|
90
|
+
self.frame_stats.add_sleep(timeout)
|
|
91
|
+
self.pause_end_time = time.time() + timeout
|
|
92
|
+
while True:
|
|
93
|
+
if self.exit_event.is_set():
|
|
94
|
+
logger.info("Exit event set. Exiting early.")
|
|
95
|
+
return
|
|
96
|
+
if self.current_task and not self.current_task.enabled:
|
|
97
|
+
raise TaskDisabledException()
|
|
98
|
+
if not (self.paused or not self.interaction.should_capture()):
|
|
99
|
+
to_sleep = self.pause_end_time - time.time()
|
|
100
|
+
if to_sleep <= 0:
|
|
101
|
+
return
|
|
102
|
+
time.sleep(0.001)
|
|
103
|
+
|
|
104
|
+
def pause(self):
|
|
105
|
+
self.paused = True
|
|
106
|
+
self.pause_start = time.time()
|
|
107
|
+
communicate.executor_paused.emit(self.paused)
|
|
108
|
+
communicate.tasks.emit()
|
|
109
|
+
|
|
110
|
+
def start(self):
|
|
111
|
+
can_run = False
|
|
112
|
+
for task in self.tasks:
|
|
113
|
+
if task.can_run():
|
|
114
|
+
can_run = True
|
|
115
|
+
break
|
|
116
|
+
if not can_run:
|
|
117
|
+
return False
|
|
118
|
+
self.pause_end_time += self.pause_start - time.time()
|
|
119
|
+
self.paused = False
|
|
120
|
+
communicate.executor_paused.emit(self.paused)
|
|
121
|
+
communicate.tasks.emit()
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def wait_scene(self, scene_type, time_out, pre_action, post_action):
|
|
125
|
+
return self.wait_condition(lambda: self.detect_scene(scene_type), time_out, pre_action, post_action)
|
|
126
|
+
|
|
127
|
+
def wait_condition(self, condition, time_out, pre_action, post_action):
|
|
128
|
+
self.sleep(self.wait_until_before_delay)
|
|
129
|
+
start = time.time()
|
|
130
|
+
if time_out == 0:
|
|
131
|
+
time_out = self.wait_scene_timeout
|
|
132
|
+
while not self.exit_event.is_set():
|
|
133
|
+
self.reset_scene()
|
|
134
|
+
if pre_action is not None:
|
|
135
|
+
pre_action()
|
|
136
|
+
self._frame = self.next_frame()
|
|
137
|
+
if self._frame is not None:
|
|
138
|
+
result = condition()
|
|
139
|
+
self.add_frame_stats()
|
|
140
|
+
result_str = list_or_obj_to_str(result)
|
|
141
|
+
if result:
|
|
142
|
+
logger.debug(f"found result {result_str}")
|
|
143
|
+
self.sleep(self.wait_until_check_delay)
|
|
144
|
+
return result
|
|
145
|
+
if post_action is not None:
|
|
146
|
+
post_action()
|
|
147
|
+
if time.time() - start > time_out:
|
|
148
|
+
logger.info(f"wait_until timeout {condition} {time_out} seconds")
|
|
149
|
+
break
|
|
150
|
+
self.sleep(0.01)
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def reset_scene(self):
|
|
154
|
+
self._frame = None
|
|
155
|
+
self.last_scene = self.current_scene
|
|
156
|
+
self.current_scene = None
|
|
157
|
+
|
|
158
|
+
def execute(self):
|
|
159
|
+
logger.info(f"start execute")
|
|
160
|
+
while not self.exit_event.is_set():
|
|
161
|
+
self._frame = self.next_frame()
|
|
162
|
+
start = time.time()
|
|
163
|
+
if self._frame is not None:
|
|
164
|
+
self.detect_scene()
|
|
165
|
+
task_executed = 0
|
|
166
|
+
for index, task in enumerate(self.tasks):
|
|
167
|
+
if task.done or not task.enabled:
|
|
168
|
+
continue
|
|
169
|
+
self.current_task = task
|
|
170
|
+
task.running = True
|
|
171
|
+
task.last_execute_time = start
|
|
172
|
+
try:
|
|
173
|
+
result = task.run_frame()
|
|
174
|
+
if result is not None:
|
|
175
|
+
if result:
|
|
176
|
+
task.success_count += 1
|
|
177
|
+
else:
|
|
178
|
+
task.error_count += 1
|
|
179
|
+
except TaskDisabledException:
|
|
180
|
+
logger.info(f"{task.name} is disabled, breaking")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
traceback.print_exc()
|
|
183
|
+
stack_trace_str = traceback.format_exc()
|
|
184
|
+
logger.error(f"{task.name} exception: {e}, traceback: {stack_trace_str}")
|
|
185
|
+
task.error_count += 1
|
|
186
|
+
self.current_task = None
|
|
187
|
+
task.running = False
|
|
188
|
+
processing_time = time.time() - start
|
|
189
|
+
task_executed += 1
|
|
190
|
+
if processing_time > 0.5:
|
|
191
|
+
logger.debug(
|
|
192
|
+
f"{task.__class__.__name__} taking too long get new frame {processing_time} {task_executed} {len(self.tasks)}")
|
|
193
|
+
self.next_frame()
|
|
194
|
+
start = time.time()
|
|
195
|
+
|
|
196
|
+
# if task_executed == 0:
|
|
197
|
+
# self.pause()
|
|
198
|
+
self.add_frame_stats()
|
|
199
|
+
self.sleep(0.001)
|
|
200
|
+
|
|
201
|
+
def add_frame_stats(self):
|
|
202
|
+
self.frame_stats.add_frame()
|
|
203
|
+
mean = self.frame_stats.mean()
|
|
204
|
+
if mean > 0:
|
|
205
|
+
communicate.frame_time.emit(mean)
|
|
206
|
+
communicate.fps.emit(round(1000 / mean))
|
|
207
|
+
scene = "None"
|
|
208
|
+
if self.current_scene is not None:
|
|
209
|
+
scene = self.current_scene.name or self.current_scene.__class__.__name__
|
|
210
|
+
communicate.scene.emit(scene)
|
|
211
|
+
|
|
212
|
+
def latest_scene(self):
|
|
213
|
+
return self.current_scene or self.last_scene
|
|
214
|
+
|
|
215
|
+
def detect_scene(self, scene_type=None):
|
|
216
|
+
latest_scene = self.latest_scene()
|
|
217
|
+
if latest_scene is not None:
|
|
218
|
+
# detect the last scene optimistically
|
|
219
|
+
if latest_scene.detect(self._frame):
|
|
220
|
+
self.current_scene = latest_scene
|
|
221
|
+
return latest_scene
|
|
222
|
+
for scene in self.scenes:
|
|
223
|
+
if scene_type is not None and not isinstance(scene, scene_type):
|
|
224
|
+
continue
|
|
225
|
+
if scene != latest_scene:
|
|
226
|
+
if scene.detect(self._frame):
|
|
227
|
+
self.current_scene = scene
|
|
228
|
+
logger.debug(f"TaskExecutor: scene changed {scene.__class__.__name__}")
|
|
229
|
+
return scene
|
|
230
|
+
if self.current_scene is not None:
|
|
231
|
+
logger.debug(f"TaskExecutor: scene changed to None")
|
|
232
|
+
self.current_scene = None
|
|
233
|
+
return self.current_scene
|
|
234
|
+
|
|
235
|
+
def stop(self):
|
|
236
|
+
self.exit_event.set()
|
|
237
|
+
|
|
238
|
+
def wait_until_done(self):
|
|
239
|
+
self.thread.join()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def list_or_obj_to_str(val):
|
|
243
|
+
if val is not None:
|
|
244
|
+
if isinstance(val, list):
|
|
245
|
+
return ', '.join(str(obj) for obj in val)
|
|
246
|
+
else:
|
|
247
|
+
return str(val)
|
|
248
|
+
else:
|
|
249
|
+
return None
|
|
File without changes
|
|
File without changes
|
autohelper/util/json.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from autohelper.util.path import ensure_dir_for_file
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_json_file(file_path) -> dict | None:
|
|
8
|
+
if not os.path.exists(file_path):
|
|
9
|
+
return None
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
13
|
+
data = json.load(f)
|
|
14
|
+
return data
|
|
15
|
+
except json.JSONDecodeError:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def write_json_file(file_path, data):
|
|
20
|
+
try:
|
|
21
|
+
ensure_dir_for_file(file_path)
|
|
22
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
23
|
+
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
24
|
+
return True
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"Error writing to JSON file: {e}")
|
|
27
|
+
return False
|
autohelper/util/list.py
ADDED
autohelper/util/path.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_path_relative_to_exe(*files):
|
|
6
|
+
if getattr(sys, 'frozen', False):
|
|
7
|
+
# The application is running as a bundled executable
|
|
8
|
+
application_path = os.path.abspath(sys.executable)
|
|
9
|
+
else:
|
|
10
|
+
# The application is running as a Python script
|
|
11
|
+
application_path = os.path.abspath(sys.argv[0])
|
|
12
|
+
the_dir = os.path.dirname(application_path)
|
|
13
|
+
|
|
14
|
+
# Join the directory with the file paths
|
|
15
|
+
path = os.path.join(the_dir, *files)
|
|
16
|
+
|
|
17
|
+
# Normalize the path
|
|
18
|
+
normalized_path = os.path.normpath(path)
|
|
19
|
+
|
|
20
|
+
return normalized_path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ensure_dir_for_file(file_path):
|
|
24
|
+
# Extract the directory from the file path
|
|
25
|
+
directory = os.path.dirname(file_path)
|
|
26
|
+
|
|
27
|
+
# Check if the directory exists
|
|
28
|
+
if not os.path.exists(directory):
|
|
29
|
+
# If the directory does not exist, create it (including any intermediate directories)
|
|
30
|
+
os.makedirs(directory)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_path_in_package(base, file):
|
|
34
|
+
the_dir = os.path.dirname(os.path.realpath(base))
|
|
35
|
+
|
|
36
|
+
# Get the path of the file relative to the script
|
|
37
|
+
return os.path.join(the_dir, file)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import msvcrt
|
|
5
|
+
import os
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def all_pids() -> list[int]:
|
|
10
|
+
pidbuffer = 512
|
|
11
|
+
bytes_written = ctypes.c_uint32()
|
|
12
|
+
while True:
|
|
13
|
+
pids = (ctypes.c_uint32 * pidbuffer)()
|
|
14
|
+
bufsize = ctypes.sizeof(pids)
|
|
15
|
+
if ctypes.windll.kernel32.K32EnumProcesses(pids, bufsize, ctypes.byref(bytes_written)) == 0:
|
|
16
|
+
return []
|
|
17
|
+
if bytes_written.value < bufsize:
|
|
18
|
+
break
|
|
19
|
+
pidbuffer *= 2
|
|
20
|
+
pidcount = bytes_written.value // 4
|
|
21
|
+
return list(pids[:pidcount])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
NtQuerySystemInformation = ctypes.windll.ntdll.NtQuerySystemInformation
|
|
25
|
+
NtQuerySystemInformation.argtypes = (ctypes.c_int32, ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32))
|
|
26
|
+
SystemProcessIdInformation = 0x58
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UNICODE_STRING(ctypes.Structure):
|
|
30
|
+
_fields_ = [
|
|
31
|
+
('Length', ctypes.c_ushort),
|
|
32
|
+
('MaximumLength', ctypes.c_ushort),
|
|
33
|
+
('Buffer', ctypes.c_wchar_p),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def create(cls, init_with: Union[str, int]):
|
|
38
|
+
buffer = ctypes.create_unicode_buffer(init_with)
|
|
39
|
+
bufsize = len(buffer)
|
|
40
|
+
if isinstance(init_with, str):
|
|
41
|
+
wchlen = bufsize - 1 # exclude null
|
|
42
|
+
else:
|
|
43
|
+
wchlen = 0
|
|
44
|
+
return cls(wchlen * 2, bufsize * 2, ctypes.cast(buffer, ctypes.c_wchar_p))
|
|
45
|
+
|
|
46
|
+
def __str__(self):
|
|
47
|
+
return ctypes.wstring_at(self.Buffer, self.Length // 2)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SYSTEM_PROCESS_ID_INFORMATION(ctypes.Structure):
|
|
51
|
+
_fields_ = [
|
|
52
|
+
('ProcessId', ctypes.c_size_t),
|
|
53
|
+
('ImageName', UNICODE_STRING),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resolve_image_name(pid: int):
|
|
58
|
+
"""Resolves the image name of a process, in NT device path (\\Device\\HarddiskVolumeX\\...) format."""
|
|
59
|
+
info = SYSTEM_PROCESS_ID_INFORMATION()
|
|
60
|
+
info.ProcessId = pid
|
|
61
|
+
status = NtQuerySystemInformation(SystemProcessIdInformation, ctypes.byref(info), ctypes.sizeof(info), None)
|
|
62
|
+
if status == -1073741820:
|
|
63
|
+
info.ImageName = UNICODE_STRING.create(info.ImageName.MaximumLength // 2)
|
|
64
|
+
status = NtQuerySystemInformation(SystemProcessIdInformation, ctypes.byref(info), ctypes.sizeof(info), None)
|
|
65
|
+
return str(info.ImageName)
|
|
66
|
+
else:
|
|
67
|
+
return ''
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
GetFinalPathNameByHandleW = ctypes.windll.kernel32.GetFinalPathNameByHandleW
|
|
71
|
+
GetFinalPathNameByHandleW.argtypes = (ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_uint32, ctypes.c_uint32)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_final_path(path: os.PathLike) -> str:
|
|
75
|
+
with open(path, 'rb', buffering=0) as f:
|
|
76
|
+
handle = msvcrt.get_osfhandle(f.fileno())
|
|
77
|
+
buf = ctypes.create_unicode_buffer(32768)
|
|
78
|
+
pathlen = GetFinalPathNameByHandleW(handle, buf, 32768, 2)
|
|
79
|
+
if pathlen == 0:
|
|
80
|
+
raise ctypes.WinError()
|
|
81
|
+
return buf[:pathlen]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _test():
|
|
85
|
+
for pid in all_pids():
|
|
86
|
+
print(pid, resolve_image_name(pid), sep='\t')
|