Fix: Retry when joystick contact lost

This commit is contained in:
LmeSzinc 2023-09-20 01:20:52 +08:00
parent d314bab51c
commit a800bdb214
5 changed files with 87 additions and 30 deletions

View File

@ -152,20 +152,21 @@ class ModuleBase:
prev_image = image prev_image = image
timer.reset() timer.reset()
def image_crop(self, button): def image_crop(self, button, copy=True):
"""Extract the area from image. """Extract the area from image.
Args: Args:
button(Button, tuple): Button instance or area tuple. button(Button, tuple): Button instance or area tuple.
copy:
""" """
if isinstance(button, Button): if isinstance(button, Button):
return crop(self.device.image, button.area) return crop(self.device.image, button.area, copy=copy)
elif isinstance(button, ButtonWrapper): elif isinstance(button, ButtonWrapper):
return crop(self.device.image, button.area) return crop(self.device.image, button.area, copy=copy)
elif hasattr(button, 'area'): elif hasattr(button, 'area'):
return crop(self.device.image, button.area) return crop(self.device.image, button.area, copy=copy)
else: else:
return crop(self.device.image, button) return crop(self.device.image, button, copy=copy)
def image_color_count(self, button, color, threshold=221, count=50): def image_color_count(self, button, color, threshold=221, count=50):
""" """
@ -178,9 +179,14 @@ class ModuleBase:
Returns: Returns:
bool: bool:
""" """
image = self.image_crop(button) if isinstance(button, np.ndarray):
mask = color_similarity_2d(image, color=color) > threshold image = button
return np.sum(mask) > count else:
image = self.image_crop(button, copy=False)
mask = color_similarity_2d(image, color=color)
cv2.inRange(mask, threshold, 255, dst=mask)
sum_ = cv2.countNonZero(mask)
return sum_ > count
def image_color_button(self, area, color, color_threshold=250, encourage=5, name='COLOR_BUTTON'): def image_color_button(self, area, color, color_threshold=250, encourage=5, name='COLOR_BUTTON'):
""" """

View File

@ -106,7 +106,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
continue continue
if self.appear(COMBAT_TEAM_PREPARE): if self.appear(COMBAT_TEAM_PREPARE):
self.interval_reset(COMBAT_PREPARE) self.interval_reset(COMBAT_PREPARE)
self._map_A_timer.reset() self.map_A_timer.reset()
if self.appear(COMBAT_PREPARE, interval=2): if self.appear(COMBAT_PREPARE, interval=2):
if not self.handle_combat_prepare(): if not self.handle_combat_prepare():
return False return False

View File

@ -248,7 +248,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
area = area_offset((-50, -150, 0, 0), offset=self.config.ASSETS_RESOLUTION) area = area_offset((-50, -150, 0, 0), offset=self.config.ASSETS_RESOLUTION)
skip_first_screenshot = True skip_first_screenshot = True
self._map_A_timer.reset() self.map_A_timer.reset()
handled = False handled = False
while 1: while 1:
if skip_first_screenshot: if skip_first_screenshot:
@ -265,7 +265,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
logger.info(f'No destructible object') logger.info(f'No destructible object')
if not handled: if not handled:
break break
if self._map_A_timer.reached(): if self.map_A_timer.reached():
break break
return handled return handled

View File

@ -130,7 +130,7 @@ class MapControl(MapControlJoystick):
if allow_straight_run and diff < 15: if allow_straight_run and diff < 15:
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run') logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run')
direction_interval = Timer(0.2) direction_interval = Timer(0.2)
self._map_2x_run_timer.reset() self.map_2x_run_timer.reset()
allow_straight_run = False allow_straight_run = False
if allow_run and diff < 7: if allow_run and diff < 7:
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run') logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run')
@ -244,7 +244,7 @@ class MapControl(MapControlJoystick):
if __name__ == '__main__': if __name__ == '__main__':
# Control test in Himeko trail # Control test in Himeko trail
# Must manually enter Himeko trail first and dismiss popup # Must manually enter Himeko trail first and dismiss popup
self = MapControl('alas') self = MapControl('src')
self.minimap.set_plane('Jarilo_BackwaterPass', floor='F1') self.minimap.set_plane('Jarilo_BackwaterPass', floor='F1')
self.device.screenshot() self.device.screenshot()
self.minimap.init_position((519, 359)) self.minimap.init_position((519, 359))

View File

@ -1,6 +1,9 @@
import math import math
from functools import cached_property from functools import cached_property
import cv2
import numpy as np
from module.base.timer import Timer from module.base.timer import Timer
from module.device.method.maatouch import MaatouchBuilder from module.device.method.maatouch import MaatouchBuilder
from module.device.method.minitouch import CommandBuilder, insert_swipe, random_normal_distribution from module.device.method.minitouch import CommandBuilder, insert_swipe, random_normal_distribution
@ -36,10 +39,8 @@ class JoystickContact:
Can not lift finger when: Can not lift finger when:
- Process is force terminated - Process is force terminated
""" """
builder = self.builder
if self.is_downed: if self.is_downed:
builder.up().commit() self.up()
builder.send()
logger.info('JoystickContact ends') logger.info('JoystickContact ends')
else: else:
logger.info('JoystickContact ends but it was never downed') logger.info('JoystickContact ends but it was never downed')
@ -98,6 +99,13 @@ class JoystickContact:
point = (int(round(point[0])), int(round(point[1]))) point = (int(round(point[0])), int(round(point[1])))
return point return point
def up(self):
builder = self.builder
if self.is_downed:
builder.up().commit()
builder.send()
self.prev_point = None
def set(self, direction, run=True): def set(self, direction, run=True):
""" """
Set joystick to given position Set joystick to given position
@ -110,6 +118,13 @@ class JoystickContact:
point = JoystickContact.direction2screen(direction, run=run) point = JoystickContact.direction2screen(direction, run=run)
builder = self.builder builder = self.builder
if self.is_downed and not self.main.joystick_speed():
if self.main.joystick_lost_timer.reached():
logger.warning(f'Joystick contact lost: {self.main.joystick_lost_timer}, re-down')
self.up()
else:
self.main.joystick_lost_timer.reset()
if self.is_downed: if self.is_downed:
points = insert_swipe(p0=self.prev_point, p3=point, speed=20) points = insert_swipe(p0=self.prev_point, p3=point, speed=20)
for point in points[1:]: for point in points[1:]:
@ -121,20 +136,56 @@ class JoystickContact:
# Character starts moving, RUN button is still unavailable in a short time. # Character starts moving, RUN button is still unavailable in a short time.
# Assume available in 0.3s # Assume available in 0.3s
# We still have reties if 0.3s is incorrect. # We still have reties if 0.3s is incorrect.
self.main._map_2x_run_timer.set_current(0.7) self.main.map_2x_run_timer.set_current(0.7)
self.main.joystick_lost_timer.reset()
self.prev_point = point self.prev_point = point
class MapControlJoystick(UI): class MapControlJoystick(UI):
_map_A_timer = Timer(1) map_A_timer = Timer(1)
_map_E_timer = Timer(1) map_E_timer = Timer(1)
_map_2x_run_timer = Timer(1) map_2x_run_timer = Timer(1)
joystick_lost_timer = Timer(1, count=2)
@cached_property @cached_property
def joystick_center(self) -> tuple[float, float]: def joystick_center(self) -> tuple[int, int]:
x1, y1, x2, y2 = JOYSTICK.area x1, y1, x2, y2 = JOYSTICK.area
return (x1 + x2) / 2, (y1 + y2) / 2 return int((x1 + x2) // 2), int((y1 + y2) // 2)
@cached_property
def DirectionRemapData(self):
d = JoystickContact.RADIUS_RUN[1] * 2
mx = np.zeros((d, d), dtype=np.float32)
my = np.zeros((d, d), dtype=np.float32)
for i in range(d):
for j in range(d):
mx[i, j] = d / 2 + i / 2 * np.cos(2 * np.pi * j / d)
my[i, j] = d / 2 + i / 2 * np.sin(2 * np.pi * j / d)
return mx, my
def joystick_speed(self) -> str:
"""
Returns:
str: 'run', 'walk', ''
"""
# About 1.5ms
x, y = self.joystick_center
radius = JoystickContact.RADIUS_RUN[1]
image = self.image_crop((x - radius, y - radius, x + radius, y + radius), copy=False)
image = cv2.remap(image, *self.DirectionRemapData, cv2.INTER_CUBIC)
# 190~205
run = image[185:210, :]
if self.image_color_count(run, color=(223, 199, 145), threshold=221, count=100):
return 'run'
# 90~100
walk = image[85:105, :]
if self.image_color_count(walk, color=(235, 235, 235), threshold=221, count=50):
return 'walk'
return ''
def map_get_technique_points(self): def map_get_technique_points(self):
""" """
@ -162,9 +213,9 @@ class MapControlJoystick(UI):
Returns: Returns:
bool: If clicked. bool: If clicked.
""" """
if self._map_A_timer.reached(): if self.map_A_timer.reached():
self.device.click(A_BUTTON) self.device.click(A_BUTTON)
self._map_A_timer.reset() self.map_A_timer.reset()
return True return True
return False return False
@ -177,9 +228,9 @@ class MapControlJoystick(UI):
Returns: Returns:
bool: If clicked. bool: If clicked.
""" """
if self._map_E_timer.reached(): if self.map_E_timer.reached():
self.device.click(E_BUTTON) self.device.click(E_BUTTON)
self._map_E_timer.reset() self.map_E_timer.reset()
return True return True
return False return False
@ -194,13 +245,13 @@ class MapControlJoystick(UI):
""" """
is_running = self.image_color_count(RUN_BUTTON, color=(208, 183, 138), threshold=221, count=100) is_running = self.image_color_count(RUN_BUTTON, color=(208, 183, 138), threshold=221, count=100)
if run and not is_running and self._map_2x_run_timer.reached(): if run and not is_running and self.map_2x_run_timer.reached():
self.device.click(RUN_BUTTON) self.device.click(RUN_BUTTON)
self._map_2x_run_timer.reset() self.map_2x_run_timer.reset()
return True return True
if not run and is_running and self._map_2x_run_timer.reached(): if not run and is_running and self.map_2x_run_timer.reached():
self.device.click(RUN_BUTTON) self.device.click(RUN_BUTTON)
self._map_2x_run_timer.reset() self.map_2x_run_timer.reset()
return True return True
return False return False