Skip to content

Commit fc0f413

Browse files
committed
Wait for launched window stability before capture
1 parent 20cec03 commit fc0f413

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

ok/gui/StartController.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414

1515
class StartController(QObject):
16+
STARTED_WINDOW_MIN_SIZE = (100, 100)
17+
STARTED_WINDOW_STABLE_SECONDS = 10
18+
STARTED_WINDOW_POLL_INTERVAL = 0.2
19+
1620
def __init__(self, app_config, exit_event):
1721
super().__init__()
1822
self.config = app_config
@@ -119,6 +123,37 @@ def _wait_until_device_ready(self):
119123
time.sleep(2)
120124
return False
121125

126+
def _wait_until_started_window_stable(self):
127+
wait_until = time.monotonic() + self.start_timeout
128+
stable_size = None
129+
stable_since = None
130+
min_width, min_height = self.STARTED_WINDOW_MIN_SIZE
131+
132+
while not self.exit_event.is_set():
133+
hwnd_window = getattr(og.device_manager, 'hwnd_window', None)
134+
if hwnd_window is not None:
135+
hwnd_window.do_update_window_size()
136+
size = (hwnd_window.width, hwnd_window.height)
137+
if hwnd_window.hwnd and size[0] >= min_width and size[1] >= min_height:
138+
now = time.monotonic()
139+
if size != stable_size:
140+
logger.info(f'waiting for started window to stabilize, current size {size[0]}x{size[1]}')
141+
stable_size = size
142+
stable_since = now
143+
elif now - stable_since >= self.STARTED_WINDOW_STABLE_SECONDS:
144+
logger.info(f'started window size stable for {self.STARTED_WINDOW_STABLE_SECONDS}s: {size[0]}x{size[1]}')
145+
return True
146+
else:
147+
stable_size = None
148+
stable_since = None
149+
150+
remaining_time = wait_until - time.monotonic()
151+
if remaining_time <= 0:
152+
communicate.starting_emulator.emit(True, self.tr('Start game timeout!'), 0)
153+
return False
154+
time.sleep(self.STARTED_WINDOW_POLL_INTERVAL)
155+
return False
156+
122157
def start_device(self):
123158
device = og.device_manager.get_preferred_device()
124159
logger.info(f'start_device: {device}')
@@ -140,6 +175,8 @@ def start_device(self):
140175
if not execute(path, arguments=args):
141176
communicate.starting_emulator.emit(True, self.tr("Start game failed, please start game first"), 0)
142177
return False
178+
if device['device'] == "windows" and not self._wait_until_started_window_stable():
179+
return False
143180
if not self._wait_until_device_ready():
144181
return False
145182
else:

tests/test_start_controller.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import threading
2+
import unittest
3+
from types import SimpleNamespace
4+
from unittest.mock import Mock, patch
5+
6+
import ok.gui.StartController as start_controller_module
7+
from ok.gui.StartController import StartController
8+
9+
10+
class FakeClock:
11+
def __init__(self):
12+
self.value = 0
13+
14+
def monotonic(self):
15+
return self.value
16+
17+
def sleep(self, seconds):
18+
self.value += seconds
19+
20+
21+
class FakeWindow:
22+
def __init__(self, sizes):
23+
self.sizes = iter(sizes)
24+
self.hwnd = 0
25+
self.width = 0
26+
self.height = 0
27+
self.updates = 0
28+
29+
def do_update_window_size(self):
30+
self.width, self.height = next(self.sizes)
31+
self.hwnd = 1
32+
self.updates += 1
33+
34+
35+
class TestStartController(unittest.TestCase):
36+
def make_controller(self):
37+
controller = StartController.__new__(StartController)
38+
controller.exit_event = threading.Event()
39+
controller.start_timeout = 20
40+
controller.STARTED_WINDOW_STABLE_SECONDS = 2
41+
controller.STARTED_WINDOW_POLL_INTERVAL = 1
42+
return controller
43+
44+
def test_started_window_must_be_usable_and_stable_before_continuing(self):
45+
controller = self.make_controller()
46+
window = FakeWindow([(80, 80), (120, 120), (140, 120), (140, 120), (140, 120)])
47+
clock = FakeClock()
48+
fake_og = SimpleNamespace(device_manager=SimpleNamespace(hwnd_window=window))
49+
50+
with patch.object(start_controller_module, 'og', fake_og), \
51+
patch.object(start_controller_module.time, 'monotonic', clock.monotonic), \
52+
patch.object(start_controller_module.time, 'sleep', clock.sleep):
53+
self.assertTrue(controller._wait_until_started_window_stable())
54+
55+
self.assertEqual(5, window.updates)
56+
57+
def test_started_window_wait_does_not_restart_countdown(self):
58+
controller = self.make_controller()
59+
controller.STARTED_WINDOW_STABLE_SECONDS = 1
60+
controller.STARTED_WINDOW_POLL_INTERVAL = 0.2
61+
window = FakeWindow([(120, 120)] * 6)
62+
clock = FakeClock()
63+
fake_og = SimpleNamespace(device_manager=SimpleNamespace(hwnd_window=window))
64+
emit = Mock()
65+
fake_communicate = SimpleNamespace(starting_emulator=SimpleNamespace(emit=emit))
66+
67+
with patch.object(start_controller_module, 'og', fake_og), \
68+
patch.object(start_controller_module, 'communicate', fake_communicate), \
69+
patch.object(start_controller_module.time, 'monotonic', clock.monotonic), \
70+
patch.object(start_controller_module.time, 'sleep', clock.sleep):
71+
self.assertTrue(controller._wait_until_started_window_stable())
72+
73+
emit.assert_not_called()
74+
75+
def test_started_windows_wait_for_stability_before_capture_readiness(self):
76+
controller = self.make_controller()
77+
device_manager = Mock()
78+
device_manager.get_preferred_device.return_value = {
79+
'connected': False,
80+
'device': 'windows',
81+
}
82+
device_manager.get_exe_path.return_value = r'C:\game.exe'
83+
fake_og = SimpleNamespace(
84+
device_manager=device_manager,
85+
global_config=Mock(),
86+
)
87+
fake_og.global_config.get_config.return_value = None
88+
89+
call_order = []
90+
controller._wait_until_started_window_stable = Mock(
91+
side_effect=lambda: call_order.append('stable') or True)
92+
controller._wait_until_device_ready = Mock(
93+
side_effect=lambda: call_order.append('ready') or True)
94+
95+
with patch.object(start_controller_module, 'og', fake_og), \
96+
patch.object(start_controller_module, 'is_admin', return_value=True), \
97+
patch.object(start_controller_module, 'execute',
98+
side_effect=lambda *args, **kwargs: call_order.append('execute') or True):
99+
self.assertTrue(controller.start_device())
100+
101+
self.assertEqual(['execute', 'stable', 'ready'], call_order)
102+
103+
104+
if __name__ == '__main__':
105+
unittest.main()

0 commit comments

Comments
 (0)