2023-05-30 18:20:45 +00:00
|
|
|
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
|
2023-06-14 16:15:14 +00:00
|
|
|
from module.base.utils import area_size, random_rectangle_vector_opted
|
2023-05-30 18:20:45 +00:00
|
|
|
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
|
|
|
|
"""
|
2023-06-14 16:15:14 +00:00
|
|
|
drag_vector = (0.65, 0.85)
|
2023-05-30 18:20:45 +00:00
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
name,
|
|
|
|
keyword_class,
|
|
|
|
ocr_class,
|
|
|
|
search_button: ButtonWrapper,
|
2023-06-25 16:07:52 +00:00
|
|
|
check_row_order: bool = True,
|
2023-06-29 16:32:33 +00:00
|
|
|
active_color: tuple[int, int, int] = (190, 175, 124),
|
|
|
|
drag_direction: str = "down"
|
2023-05-30 18:20:45 +00:00
|
|
|
):
|
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
name:
|
|
|
|
keyword_class: Keyword
|
|
|
|
search_button:
|
2023-06-29 16:32:33 +00:00
|
|
|
drag_direction: Default drag direction to higher index
|
2023-05-30 18:20:45 +00:00
|
|
|
"""
|
|
|
|
self.name = name
|
|
|
|
self.keyword_class = keyword_class
|
|
|
|
self.ocr_class = ocr_class
|
2023-06-14 16:15:14 +00:00
|
|
|
if isinstance(keyword_class, list):
|
|
|
|
keyword_class = keyword_class[0]
|
2023-05-30 18:20:45 +00:00
|
|
|
self.known_rows = list(keyword_class.instances.values())
|
|
|
|
self.search_button = search_button
|
2023-06-25 16:07:52 +00:00
|
|
|
self.check_row_order = check_row_order
|
2023-06-19 00:39:41 +00:00
|
|
|
self.active_color = active_color
|
2023-06-29 16:32:33 +00:00
|
|
|
self.drag_direction = drag_direction
|
2023-05-30 18:20:45 +00:00
|
|
|
|
|
|
|
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:
|
2023-06-14 16:15:14 +00:00
|
|
|
# logger.warning(f'Row "{row}" does not belong to {self}')
|
2023-05-30 18:20:45 +00:00
|
|
|
return 0
|
|
|
|
|
2023-07-10 14:20:35 +00:00
|
|
|
def keyword2button(self, row: Keyword, show_warning=True) -> Optional[OcrResultButton]:
|
2023-05-30 18:20:45 +00:00
|
|
|
for button in self.cur_buttons:
|
|
|
|
if button == row:
|
|
|
|
return button
|
|
|
|
|
2023-07-10 14:20:35 +00:00
|
|
|
if show_warning:
|
|
|
|
logger.warning(f'Keyword {row} is not in current rows of {self}')
|
|
|
|
logger.warning(f'Current rows: {self.cur_buttons}')
|
2023-05-30 18:20:45 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
def load_rows(self, main: ModuleBase):
|
|
|
|
"""
|
|
|
|
Parse current rows to get list position.
|
|
|
|
"""
|
|
|
|
self.cur_buttons = self.ocr_class(self.search_button) \
|
2023-06-14 16:15:14 +00:00
|
|
|
.matched_ocr(main.device.image, self.keyword_class)
|
2023-05-30 18:20:45 +00:00
|
|
|
# Get indexes
|
2023-06-19 00:39:41 +00:00
|
|
|
indexes = [self.keyword2index(row.matched_keyword)
|
|
|
|
for row in self.cur_buttons]
|
2023-05-30 18:20:45 +00:00
|
|
|
indexes = [index for index in indexes if index]
|
|
|
|
# Check row order
|
2023-06-25 16:07:52 +00:00
|
|
|
if self.check_row_order and len(indexes) >= 2:
|
2023-05-30 18:20:45 +00:00
|
|
|
if not np.all(np.diff(indexes) > 0):
|
2023-06-19 00:39:41 +00:00
|
|
|
logger.warning(
|
|
|
|
f'Rows given to {self} are not ascending sorted')
|
2023-05-30 18:20:45 +00:00
|
|
|
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}')
|
|
|
|
|
2023-06-14 16:15:14 +00:00
|
|
|
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)
|
2023-05-30 18:20:45 +00:00
|
|
|
width, height = area_size(self.search_button.button)
|
2023-06-14 16:15:14 +00:00
|
|
|
if direction == 'up':
|
2023-05-30 18:20:45 +00:00
|
|
|
vector = (0, vector * height)
|
2023-06-14 16:15:14 +00:00
|
|
|
elif direction == 'down':
|
2023-05-30 18:20:45 +00:00
|
|
|
vector = (0, -vector * height)
|
|
|
|
elif direction == 'left':
|
|
|
|
vector = (vector * width, 0)
|
2023-06-14 16:15:14 +00:00
|
|
|
elif direction == 'right':
|
|
|
|
vector = (-vector * width, 0)
|
2023-05-30 18:20:45 +00:00
|
|
|
else:
|
|
|
|
logger.warning(f'Unknown drag direction: {direction}')
|
|
|
|
return
|
2023-06-14 16:15:14 +00:00
|
|
|
|
|
|
|
p1, p2 = random_rectangle_vector_opted(vector, box=self.search_button.button)
|
|
|
|
main.device.drag(p1, p2, name=f'{self.name}_DRAG')
|
2023-05-30 18:20:45 +00:00
|
|
|
|
2023-06-29 16:32:33 +00:00
|
|
|
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'
|
|
|
|
|
2024-10-14 16:50:26 +00:00
|
|
|
def wait_bottom_appear(self, main: ModuleBase, skip_first_screenshot=True):
|
|
|
|
"""
|
|
|
|
Returns:
|
|
|
|
bool: If waited
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
2023-05-30 18:20:45 +00:00
|
|
|
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}')
|
2023-09-12 10:36:24 +00:00
|
|
|
last_buttons: set[OcrResultButton] = None
|
2024-04-11 14:24:49 +00:00
|
|
|
bottom_check = Timer(3, count=5).start()
|
2023-05-30 18:20:45 +00:00
|
|
|
while 1:
|
|
|
|
if skip_first_screenshot:
|
|
|
|
skip_first_screenshot = False
|
|
|
|
else:
|
|
|
|
main.device.screenshot()
|
|
|
|
|
|
|
|
self.load_rows(main=main)
|
|
|
|
|
|
|
|
# End
|
2023-08-02 16:51:58 +00:00
|
|
|
if self.cur_buttons and self.cur_min <= row_index <= self.cur_max:
|
2023-05-30 18:20:45 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
# Drag pages
|
|
|
|
if row_index < self.cur_min:
|
2023-06-29 16:32:33 +00:00
|
|
|
self.drag_page(self.reverse_direction(self.drag_direction), main=main)
|
2023-05-30 18:20:45 +00:00
|
|
|
elif self.cur_max < row_index:
|
2023-06-29 16:32:33 +00:00
|
|
|
self.drag_page(self.drag_direction, main=main)
|
|
|
|
|
2023-05-30 18:20:45 +00:00
|
|
|
# Wait for bottoming out
|
2024-10-14 16:50:26 +00:00
|
|
|
self.wait_bottom_appear(main, skip_first_screenshot=False)
|
|
|
|
main.wait_until_stable(
|
|
|
|
self.search_button, timer=Timer(0, count=0),
|
|
|
|
timeout=Timer(1.5, count=5)
|
|
|
|
)
|
2023-05-30 18:20:45 +00:00
|
|
|
skip_first_screenshot = True
|
2023-09-15 05:29:22 +00:00
|
|
|
if self.cur_buttons and last_buttons == set(self.cur_buttons):
|
2024-04-11 14:24:49 +00:00
|
|
|
if bottom_check.reached():
|
|
|
|
logger.warning(f'No more rows in {self}')
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
bottom_check.reset()
|
2023-09-12 10:36:24 +00:00
|
|
|
last_buttons = set(self.cur_buttons)
|
2023-05-30 18:20:45 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2023-06-27 18:25:54 +00:00
|
|
|
def is_row_selected(self, button: OcrResultButton, main: ModuleBase) -> bool:
|
2023-05-30 18:20:45 +00:00
|
|
|
# Having gold letters
|
2023-06-19 00:39:41 +00:00
|
|
|
if main.image_color_count(button, color=self.active_color, threshold=221, count=50):
|
2023-05-30 18:20:45 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
2023-07-02 07:33:30 +00:00
|
|
|
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):
|
2023-05-30 18:20:45 +00:00
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
row:
|
|
|
|
main:
|
2023-07-02 07:33:30 +00:00
|
|
|
insight: If call `insight_row()` before selecting
|
2023-05-30 18:20:45 +00:00
|
|
|
skip_first_screenshot:
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
If success
|
|
|
|
"""
|
2023-07-02 07:33:30 +00:00
|
|
|
if insight:
|
|
|
|
result = self.insight_row(
|
|
|
|
row, main=main, skip_first_screenshot=skip_first_screenshot)
|
|
|
|
if not result:
|
|
|
|
return False
|
|
|
|
|
2023-05-30 18:20:45 +00:00
|
|
|
logger.info(f'Select row: {row}')
|
|
|
|
skip_first_screenshot = True
|
|
|
|
interval = Timer(5)
|
2023-07-01 15:06:46 +00:00
|
|
|
skip_first_load_rows = True
|
|
|
|
load_rows_interval = Timer(1)
|
2023-05-30 18:20:45 +00:00
|
|
|
while 1:
|
|
|
|
if skip_first_screenshot:
|
|
|
|
skip_first_screenshot = False
|
|
|
|
else:
|
|
|
|
main.device.screenshot()
|
2023-07-01 15:06:46 +00:00
|
|
|
|
|
|
|
if skip_first_load_rows:
|
|
|
|
skip_first_load_rows = False
|
2023-07-06 14:41:16 +00:00
|
|
|
load_rows_interval.reset()
|
2023-07-01 15:06:46 +00:00
|
|
|
else:
|
|
|
|
if load_rows_interval.reached():
|
|
|
|
self.load_rows(main=main)
|
|
|
|
load_rows_interval.reset()
|
2023-05-30 18:20:45 +00:00
|
|
|
|
2023-06-30 04:56:20 +00:00
|
|
|
button = self.keyword2button(row)
|
|
|
|
if not button:
|
|
|
|
return False
|
|
|
|
|
2023-05-30 18:20:45 +00:00
|
|
|
# End
|
2023-06-27 18:25:54 +00:00
|
|
|
if self.is_row_selected(button, main=main):
|
2023-07-06 14:41:16 +00:00
|
|
|
logger.info(f'Row selected at {row}')
|
2023-06-27 18:25:54 +00:00
|
|
|
return True
|
2023-05-30 18:20:45 +00:00
|
|
|
|
|
|
|
# Click
|
|
|
|
if interval.reached():
|
2023-06-27 18:25:54 +00:00
|
|
|
main.device.click(button)
|
2023-05-30 18:20:45 +00:00
|
|
|
interval.reset()
|