mirror of
https://github.com/TheFunny/ArisuAutoSweeper
synced 2026-06-09 20:04:52 +00:00
Upload code
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from module.base.base import ModuleBase
|
||||
from module.base.button import ButtonWrapper
|
||||
from module.base.timer import Timer
|
||||
from module.base.utils import area_size, random_rectangle_vector_opted
|
||||
from module.logger import logger
|
||||
from module.ocr.keyword import Keyword
|
||||
from module.ocr.ocr import OcrResultButton
|
||||
|
||||
|
||||
class DraggableList:
|
||||
"""
|
||||
A wrapper to handle draggable lists like
|
||||
- Simulated Universe
|
||||
- Calyx (Golden)
|
||||
- Calyx (Crimson)
|
||||
- Stagnant Shadow
|
||||
- Cavern of Corrosion
|
||||
"""
|
||||
drag_vector = (0.65, 0.85)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
keyword_class,
|
||||
ocr_class,
|
||||
search_button: ButtonWrapper,
|
||||
check_row_order: bool = True,
|
||||
active_color: tuple[int, int, int] = (190, 175, 124),
|
||||
drag_direction: str = "down"
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
name:
|
||||
keyword_class: Keyword
|
||||
search_button:
|
||||
drag_direction: Default drag direction to higher index
|
||||
"""
|
||||
self.name = name
|
||||
self.keyword_class = keyword_class
|
||||
self.ocr_class = ocr_class
|
||||
if isinstance(keyword_class, list):
|
||||
keyword_class = keyword_class[0]
|
||||
self.known_rows = list(keyword_class.instances.values())
|
||||
self.search_button = search_button
|
||||
self.check_row_order = check_row_order
|
||||
self.active_color = active_color
|
||||
self.drag_direction = drag_direction
|
||||
|
||||
self.row_min = 1
|
||||
self.row_max = len(self.known_rows)
|
||||
self.cur_min = 1
|
||||
self.cur_max = 1
|
||||
self.cur_buttons: list[OcrResultButton] = []
|
||||
|
||||
def __str__(self):
|
||||
return f'DraggableList({self.name})'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __eq__(self, other):
|
||||
return str(self) == str(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def keyword2index(self, row: Keyword) -> int:
|
||||
try:
|
||||
return self.known_rows.index(row) + 1
|
||||
except ValueError:
|
||||
# logger.warning(f'Row "{row}" does not belong to {self}')
|
||||
return 0
|
||||
|
||||
def keyword2button(self, row: Keyword, show_warning=True) -> Optional[OcrResultButton]:
|
||||
for button in self.cur_buttons:
|
||||
if button == row:
|
||||
return button
|
||||
|
||||
if show_warning:
|
||||
logger.warning(f'Keyword {row} is not in current rows of {self}')
|
||||
logger.warning(f'Current rows: {self.cur_buttons}')
|
||||
return None
|
||||
|
||||
def load_rows(self, main: ModuleBase):
|
||||
"""
|
||||
Parse current rows to get list position.
|
||||
"""
|
||||
self.cur_buttons = self.ocr_class(self.search_button) \
|
||||
.matched_ocr(main.device.image, self.keyword_class)
|
||||
# Get indexes
|
||||
indexes = [self.keyword2index(row.matched_keyword)
|
||||
for row in self.cur_buttons]
|
||||
indexes = [index for index in indexes if index]
|
||||
# Check row order
|
||||
if self.check_row_order and len(indexes) >= 2:
|
||||
if not np.all(np.diff(indexes) > 0):
|
||||
logger.warning(
|
||||
f'Rows given to {self} are not ascending sorted')
|
||||
if not indexes:
|
||||
logger.warning(f'No valid rows loaded into {self}')
|
||||
return
|
||||
|
||||
self.cur_min = min(indexes)
|
||||
self.cur_max = max(indexes)
|
||||
logger.attr(self.name, f'{self.cur_min} - {self.cur_max}')
|
||||
|
||||
def drag_page(self, direction: str, main: ModuleBase, vector=None):
|
||||
"""
|
||||
Args:
|
||||
direction: up, down, left, right
|
||||
main:
|
||||
vector (tuple[float, float]): Specific `drag_vector`, None by default to use `self.drag_vector`
|
||||
"""
|
||||
if vector is None:
|
||||
vector = self.drag_vector
|
||||
vector = np.random.uniform(*vector)
|
||||
width, height = area_size(self.search_button.button)
|
||||
if direction == 'up':
|
||||
vector = (0, vector * height)
|
||||
elif direction == 'down':
|
||||
vector = (0, -vector * height)
|
||||
elif direction == 'left':
|
||||
vector = (vector * width, 0)
|
||||
elif direction == 'right':
|
||||
vector = (-vector * width, 0)
|
||||
else:
|
||||
logger.warning(f'Unknown drag direction: {direction}')
|
||||
return
|
||||
|
||||
p1, p2 = random_rectangle_vector_opted(vector, box=self.search_button.button)
|
||||
main.device.drag(p1, p2, name=f'{self.name}_DRAG')
|
||||
|
||||
def reverse_direction(self, direction):
|
||||
if direction == 'up':
|
||||
return 'down'
|
||||
if direction == 'down':
|
||||
return 'up'
|
||||
if direction == 'left':
|
||||
return 'right'
|
||||
if direction == 'right':
|
||||
return 'left'
|
||||
|
||||
def insight_row(self, row: Keyword, main: ModuleBase, skip_first_screenshot=True) -> bool:
|
||||
"""
|
||||
Args:
|
||||
row:
|
||||
main:
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
If success
|
||||
"""
|
||||
row_index = self.keyword2index(row)
|
||||
if not row_index:
|
||||
logger.warning(f'Insight row {row} but index unknown')
|
||||
return False
|
||||
|
||||
logger.info(f'Insight row: {row}, index={row_index}')
|
||||
last_buttons: set[OcrResultButton] = None
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
self.load_rows(main=main)
|
||||
|
||||
# End
|
||||
if self.cur_buttons and self.cur_min <= row_index <= self.cur_max:
|
||||
break
|
||||
|
||||
# Drag pages
|
||||
if row_index < self.cur_min:
|
||||
self.drag_page(self.reverse_direction(self.drag_direction), main=main)
|
||||
elif self.cur_max < row_index:
|
||||
self.drag_page(self.drag_direction, main=main)
|
||||
|
||||
# Wait for bottoming out
|
||||
main.wait_until_stable(self.search_button, timer=Timer(
|
||||
0, count=0), timeout=Timer(1.5, count=5))
|
||||
skip_first_screenshot = True
|
||||
if self.cur_buttons and last_buttons == set(self.cur_buttons):
|
||||
logger.warning(f'No more rows in {self}')
|
||||
return False
|
||||
last_buttons = set(self.cur_buttons)
|
||||
|
||||
return True
|
||||
|
||||
def is_row_selected(self, button: OcrResultButton, main: ModuleBase) -> bool:
|
||||
# Having gold letters
|
||||
if main.image_color_count(button, color=self.active_color, threshold=221, count=50):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_selected_row(self, main: ModuleBase) -> Optional[OcrResultButton]:
|
||||
"""
|
||||
`load_rows()` must be called before `get_selected_row()`.
|
||||
"""
|
||||
for row in self.cur_buttons:
|
||||
if self.is_row_selected(row, main=main):
|
||||
return row
|
||||
return None
|
||||
|
||||
def select_row(self, row: Keyword, main: ModuleBase, insight=True, skip_first_screenshot=True):
|
||||
"""
|
||||
Args:
|
||||
row:
|
||||
main:
|
||||
insight: If call `insight_row()` before selecting
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
If success
|
||||
"""
|
||||
if insight:
|
||||
result = self.insight_row(
|
||||
row, main=main, skip_first_screenshot=skip_first_screenshot)
|
||||
if not result:
|
||||
return False
|
||||
|
||||
logger.info(f'Select row: {row}')
|
||||
skip_first_screenshot = True
|
||||
interval = Timer(5)
|
||||
skip_first_load_rows = True
|
||||
load_rows_interval = Timer(1)
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
if skip_first_load_rows:
|
||||
skip_first_load_rows = False
|
||||
load_rows_interval.reset()
|
||||
else:
|
||||
if load_rows_interval.reached():
|
||||
self.load_rows(main=main)
|
||||
load_rows_interval.reset()
|
||||
|
||||
button = self.keyword2button(row)
|
||||
if not button:
|
||||
return False
|
||||
|
||||
# End
|
||||
if self.is_row_selected(button, main=main):
|
||||
logger.info(f'Row selected at {row}')
|
||||
return True
|
||||
|
||||
# Click
|
||||
if interval.reached():
|
||||
main.device.click(button)
|
||||
interval.reset()
|
||||
@@ -0,0 +1,198 @@
|
||||
import numpy as np
|
||||
|
||||
from module.base.base import ModuleBase
|
||||
from module.base.button import Button, ButtonWrapper
|
||||
from module.base.timer import Timer
|
||||
from module.base.utils import color_similarity_2d, random_rectangle_point
|
||||
from module.logger import logger
|
||||
|
||||
|
||||
class Scroll:
|
||||
color_threshold = 221
|
||||
drag_threshold = 0.05
|
||||
edge_threshold = 0.05
|
||||
edge_add = (0.3, 0.5)
|
||||
|
||||
def __init__(self, area, color, is_vertical=True, name='Scroll'):
|
||||
"""
|
||||
Args:
|
||||
area (Button, tuple): A button or area of the whole scroll.
|
||||
color (tuple): RGB of the scroll
|
||||
is_vertical (bool): True if vertical, false if horizontal.
|
||||
name (str):
|
||||
"""
|
||||
if isinstance(area, (Button, ButtonWrapper)):
|
||||
# name = area.name
|
||||
area = area.area
|
||||
self.area = area
|
||||
self.color = color
|
||||
self.is_vertical = is_vertical
|
||||
self.name = name
|
||||
|
||||
if self.is_vertical:
|
||||
self.total = self.area[3] - self.area[1]
|
||||
else:
|
||||
self.total = self.area[2] - self.area[0]
|
||||
# Just default value, will change in match_color()
|
||||
self.length = self.total / 2
|
||||
self.drag_interval = Timer(1, count=2)
|
||||
self.drag_timeout = Timer(5, count=10)
|
||||
|
||||
def match_color(self, main):
|
||||
"""
|
||||
Args:
|
||||
main (ModuleBase):
|
||||
|
||||
Returns:
|
||||
np.ndarray: Shape (n,), dtype bool.
|
||||
"""
|
||||
image = main.image_crop(self.area)
|
||||
image = color_similarity_2d(image, color=self.color)
|
||||
mask = np.max(image, axis=1 if self.is_vertical else 0) > self.color_threshold
|
||||
self.length = np.sum(mask)
|
||||
return mask
|
||||
|
||||
def cal_position(self, main):
|
||||
"""
|
||||
Args:
|
||||
main (ModuleBase):
|
||||
|
||||
Returns:
|
||||
float: 0 to 1.
|
||||
"""
|
||||
mask = self.match_color(main)
|
||||
middle = np.mean(np.where(mask)[0])
|
||||
|
||||
position = (middle - self.length / 2) / (self.total - self.length)
|
||||
position = position if position > 0 else 0.0
|
||||
position = position if position < 1 else 1.0
|
||||
logger.attr(self.name, f'{position:.2f} ({middle}-{self.length / 2})/({self.total}-{self.length})')
|
||||
return position
|
||||
|
||||
def position_to_screen(self, position, random_range=(-0.05, 0.05)):
|
||||
"""
|
||||
Convert scroll position to screen coordinates.
|
||||
Call cal_position() or match_color() to get length, before calling this.
|
||||
|
||||
Args:
|
||||
position (int, float):
|
||||
random_range (tuple):
|
||||
|
||||
Returns:
|
||||
tuple[int]: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y)
|
||||
"""
|
||||
position = np.add(position, random_range)
|
||||
middle = position * (self.total - self.length) + self.length / 2
|
||||
middle = middle.astype(int)
|
||||
if self.is_vertical:
|
||||
middle += self.area[1]
|
||||
while np.max(middle) >= 720:
|
||||
middle -= 2
|
||||
while np.min(middle) <= 0:
|
||||
middle += 2
|
||||
area = (self.area[0], middle[0], self.area[2], middle[1])
|
||||
else:
|
||||
middle += self.area[0]
|
||||
while np.max(middle) >= 1280:
|
||||
middle -= 2
|
||||
while np.min(middle) <= 0:
|
||||
middle += 2
|
||||
area = (middle[0], self.area[1], middle[1], self.area[3])
|
||||
return area
|
||||
|
||||
def appear(self, main):
|
||||
"""
|
||||
Args:
|
||||
main (ModuleBase):
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
return np.mean(self.match_color(main)) > 0.1
|
||||
|
||||
def at_top(self, main):
|
||||
return self.cal_position(main) < self.edge_threshold
|
||||
|
||||
def at_bottom(self, main):
|
||||
return self.cal_position(main) > 1 - self.edge_threshold
|
||||
|
||||
def set(self, position, main, random_range=(-0.05, 0.05), distance_check=True, skip_first_screenshot=True):
|
||||
"""
|
||||
Set scroll to a specific position.
|
||||
|
||||
Args:
|
||||
position (float, int): 0 to 1.
|
||||
main (ModuleBase):
|
||||
random_range (tuple(int, float)):
|
||||
distance_check (bool): Whether to drop short swipes
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
bool: If dragged.
|
||||
"""
|
||||
logger.info(f'{self.name} set to {position}')
|
||||
self.drag_interval.clear()
|
||||
self.drag_timeout.reset()
|
||||
dragged = 0
|
||||
if position <= self.edge_threshold:
|
||||
random_range = np.subtract(0, self.edge_add)
|
||||
if position >= 1 - self.edge_threshold:
|
||||
random_range = self.edge_add
|
||||
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
current = self.cal_position(main)
|
||||
if abs(position - current) < self.drag_threshold:
|
||||
break
|
||||
if self.length:
|
||||
self.drag_timeout.reset()
|
||||
else:
|
||||
if self.drag_timeout.reached():
|
||||
logger.warning('Scroll disappeared, assume scroll set')
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
if self.drag_interval.reached():
|
||||
p1 = random_rectangle_point(self.position_to_screen(current), n=1)
|
||||
p2 = random_rectangle_point(self.position_to_screen(position, random_range=random_range), n=1)
|
||||
main.device.swipe(p1, p2, name=self.name, distance_check=distance_check)
|
||||
self.drag_interval.reset()
|
||||
dragged += 1
|
||||
|
||||
return dragged
|
||||
|
||||
def set_top(self, main, random_range=(-0.05, 0.05), skip_first_screenshot=True):
|
||||
return self.set(0.00, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
||||
|
||||
def set_bottom(self, main, random_range=(-0.05, 0.05), skip_first_screenshot=True):
|
||||
return self.set(1.00, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
||||
|
||||
def drag_page(self, page, main, random_range=(-0.05, 0.05), skip_first_screenshot=True):
|
||||
"""
|
||||
Drag scroll forward or backward.
|
||||
|
||||
Args:
|
||||
page (int, float): Relative position to drag. 1.0 means next page, -1.0 means previous page.
|
||||
main (ModuleBase):
|
||||
random_range (tuple[float]):
|
||||
skip_first_screenshot:
|
||||
"""
|
||||
if not skip_first_screenshot:
|
||||
main.device.screenshot()
|
||||
current = self.cal_position(main)
|
||||
|
||||
multiply = self.length / (self.total - self.length)
|
||||
target = current + page * multiply
|
||||
target = round(min(max(target, 0), 1), 3)
|
||||
self.set(target, main=main, random_range=random_range, skip_first_screenshot=True)
|
||||
|
||||
def next_page(self, main, page=0.8, random_range=(-0.01, 0.01), skip_first_screenshot=True):
|
||||
self.drag_page(page, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
||||
|
||||
def prev_page(self, main, page=0.8, random_range=(-0.01, 0.01), skip_first_screenshot=True):
|
||||
self.drag_page(-page, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
||||
@@ -0,0 +1,167 @@
|
||||
from module.base.base import ModuleBase
|
||||
from module.base.button import Button
|
||||
from module.base.timer import Timer
|
||||
from module.exception import ScriptError
|
||||
from module.logger import logger
|
||||
|
||||
|
||||
class Switch:
|
||||
"""
|
||||
A wrapper to handle switches in game, switch among states with retries.
|
||||
|
||||
Examples:
|
||||
# Definitions
|
||||
submarine_hunt = Switch('Submarine_hunt', offset=120)
|
||||
submarine_hunt.add_state('on', check_button=SUBMARINE_HUNT_ON)
|
||||
submarine_hunt.add_state('off', check_button=SUBMARINE_HUNT_OFF)
|
||||
|
||||
# Change state to ON
|
||||
submarine_view.set('on', main=self)
|
||||
"""
|
||||
|
||||
def __init__(self, name='Switch', is_selector=False):
|
||||
"""
|
||||
Args:
|
||||
name (str):
|
||||
is_selector (bool): True if this is a multi choice, click to choose one of the switches.
|
||||
For example: | [Daily] | Urgent | -> click -> | Daily | [Urgent] |
|
||||
False if this is a switch, click the switch itself, and it changed in the same position.
|
||||
For example: | [ON] | -> click -> | [OFF] |
|
||||
"""
|
||||
self.name = name
|
||||
self.is_choice = is_selector
|
||||
self.state_list = []
|
||||
|
||||
def add_state(self, state, check_button, click_button=None):
|
||||
"""
|
||||
Args:
|
||||
state (str):
|
||||
check_button (Button):
|
||||
click_button (Button):
|
||||
"""
|
||||
self.state_list.append({
|
||||
'state': state,
|
||||
'check_button': check_button,
|
||||
'click_button': click_button if click_button is not None else check_button,
|
||||
})
|
||||
|
||||
def appear(self, main):
|
||||
"""
|
||||
Args:
|
||||
main (ModuleBase):
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
for data in self.state_list:
|
||||
if main.appear(data['check_button']):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get(self, main):
|
||||
"""
|
||||
Args:
|
||||
main (ModuleBase):
|
||||
|
||||
Returns:
|
||||
str: state name or 'unknown'.
|
||||
"""
|
||||
for data in self.state_list:
|
||||
if main.appear(data['check_button']):
|
||||
return data['state']
|
||||
|
||||
return 'unknown'
|
||||
|
||||
def click(self, state, main):
|
||||
"""
|
||||
Args:
|
||||
state (str):
|
||||
main (ModuleBase):
|
||||
"""
|
||||
button = self.get_data(state)['click_button']
|
||||
main.device.click(button)
|
||||
|
||||
def get_data(self, state):
|
||||
"""
|
||||
Args:
|
||||
state (str):
|
||||
|
||||
Returns:
|
||||
dict: Dictionary in add_state
|
||||
|
||||
Raises:
|
||||
ScriptError: If state invalid
|
||||
"""
|
||||
for row in self.state_list:
|
||||
if row['state'] == state:
|
||||
return row
|
||||
|
||||
logger.warning(f'Switch {self.name} received an invalid state {state}')
|
||||
raise ScriptError(f'Switch {self.name} received an invalid state {state}')
|
||||
|
||||
def handle_additional(self, main):
|
||||
"""
|
||||
Args:
|
||||
main (ModuleBase):
|
||||
|
||||
Returns:
|
||||
bool: If handled
|
||||
"""
|
||||
return False
|
||||
|
||||
def set(self, state, main, skip_first_screenshot=True):
|
||||
"""
|
||||
Args:
|
||||
state:
|
||||
main (ModuleBase):
|
||||
skip_first_screenshot (bool):
|
||||
|
||||
Returns:
|
||||
bool: If clicked
|
||||
"""
|
||||
logger.info(f'{self.name} set to {state}')
|
||||
self.get_data(state)
|
||||
|
||||
counter = 0
|
||||
changed = False
|
||||
warning_show_timer = Timer(5, count=10).start()
|
||||
click_timer = Timer(1, count=3)
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
# Detect
|
||||
current = self.get(main=main)
|
||||
logger.attr(self.name, current)
|
||||
|
||||
# Handle additional popups
|
||||
if self.handle_additional(main=main):
|
||||
continue
|
||||
|
||||
# End
|
||||
if current == state:
|
||||
return changed
|
||||
|
||||
# Warning
|
||||
if current == 'unknown':
|
||||
if warning_show_timer.reached():
|
||||
logger.warning(f'Unknown {self.name} switch')
|
||||
warning_show_timer.reset()
|
||||
if counter >= 1:
|
||||
logger.warning(f'{self.name} switch {state} asset has evaluated to unknown too many times, '
|
||||
f'asset should be re-verified')
|
||||
return False
|
||||
counter += 1
|
||||
continue
|
||||
|
||||
# Click
|
||||
if click_timer.reached():
|
||||
click_state = state if self.is_choice else current
|
||||
self.click(click_state, main=main)
|
||||
click_timer.reset()
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
Reference in New Issue
Block a user