From dd73247a07186e9729ff0add12a53c69a492ae0e Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 11 Jun 2024 22:02:25 +0800 Subject: [PATCH 1/5] Chore: match_template_luma --- module/base/button.py | 16 ++++++++-------- tasks/base/ui.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/module/base/button.py b/module/base/button.py index 4c206838d..5b5dc1507 100644 --- a/module/base/button.py +++ b/module/base/button.py @@ -57,12 +57,12 @@ class Button(Resource): return load_image(self.file, self.area) @cached_property - def image_binary(self): - return rgb2gray(self.image) + def image_luma(self): + return rgb2luma(self.image) def resource_release(self): del_cached_property(self, 'image') - del_cached_property(self, 'image_binary') + del_cached_property(self, 'image_luma') self.clear_offset() def __str__(self): @@ -119,7 +119,7 @@ class Button(Resource): self._button_offset = np.array(point) + self.search[:2] - self.area[:2] return sim > similarity - def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool: + def match_template_luma(self, image, similarity=0.85, direct_match=False) -> bool: """ Detects assets by template matching. @@ -135,8 +135,8 @@ class Button(Resource): """ if not direct_match: image = crop(image, self.search, copy=False) - image = rgb2gray(image) - res = cv2.matchTemplate(self.image_binary, image, cv2.TM_CCOEFF_NORMED) + image = rgb2luma(image) + res = cv2.matchTemplate(self.image_luma, image, cv2.TM_CCOEFF_NORMED) _, sim, _, point = cv2.minMaxLoc(res) self._button_offset = np.array(point) + self.search[:2] - self.area[:2] @@ -254,9 +254,9 @@ class ButtonWrapper(Resource): return True return False - def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool: + def match_template_luma(self, image, similarity=0.85, direct_match=False) -> bool: for assets in self.buttons: - if assets.match_template_binary(image, similarity=similarity, direct_match=direct_match): + if assets.match_template_luma(image, similarity=similarity, direct_match=direct_match): self._matched_button = assets return True return False diff --git a/tasks/base/ui.py b/tasks/base/ui.py index 185c36ce1..ffcaebd5e 100644 --- a/tasks/base/ui.py +++ b/tasks/base/ui.py @@ -314,11 +314,11 @@ class UI(MainPage): return False appear = False - if MAIN_GOTO_CHARACTER.match_template_binary(self.device.image): + if MAIN_GOTO_CHARACTER.match_template_luma(self.device.image): if self.image_color_count(MAIN_GOTO_CHARACTER, color=(235, 235, 235), threshold=234, count=400): appear = True if not appear: - if MAP_EXIT.match_template_binary(self.device.image): + if MAP_EXIT.match_template_luma(self.device.image): if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50): appear = True @@ -334,7 +334,7 @@ class UI(MainPage): return False appear = False - if MAP_EXIT.match_template_binary(self.device.image): + if MAP_EXIT.match_template_luma(self.device.image): if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50): appear = True From d6d61bb6fc92c78283120bf6b76b29d0afd82f75 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 12 Jun 2024 02:43:38 +0800 Subject: [PATCH 2/5] Dev: Fix missing dungeon_id on Credict --- dev_tools/keywords/item.py | 3 +++ tasks/planner/keywords/item_currency.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dev_tools/keywords/item.py b/dev_tools/keywords/item.py index bd8e20dd1..87fc9758f 100644 --- a/dev_tools/keywords/item.py +++ b/dev_tools/keywords/item.py @@ -66,6 +66,8 @@ class GenerateItemBase(GenerateKeyword): continue dic.setdefault(item_id, dungeon_id) + # Credict + dic.setdefault(2, 1003) return dic @@ -79,6 +81,7 @@ class GenerateItemCurrency(GenerateItemBase): if data['subtype'] == 'Virtual' and data['item_id'] < 100: if data['item_id'] not in self.whitelist: continue + data['dungeon_id'] = self.dict_itemid_to_dungeonid.get(data['item_id'], -1) yield data diff --git a/tasks/planner/keywords/item_currency.py b/tasks/planner/keywords/item_currency.py index 07ac9a841..e5c28fbf1 100644 --- a/tasks/planner/keywords/item_currency.py +++ b/tasks/planner/keywords/item_currency.py @@ -14,7 +14,7 @@ Credit = ItemCurrency( rarity='Rare', item_id=2, item_group=0, - dungeon_id=-1, + dungeon_id=1003, ) Trailblaze_EXP = ItemCurrency( id=2, From 33eedc2c631763ee28bc78e7acc6f449a8e4ac5f Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 12 Jun 2024 03:20:57 +0800 Subject: [PATCH 3/5] Add: Display overall progress of character planner --- config/template.json | 1 + module/config/argument/args.json | 12 +- module/config/argument/argument.yaml | 9 +- module/config/argument/gui.yaml | 2 + module/config/argument/stored.json | 27 ++- module/config/config_generated.py | 3 +- module/config/i18n/en-US.json | 7 +- module/config/i18n/es-ES.json | 7 +- module/config/i18n/ja-JP.json | 7 +- module/config/i18n/zh-CN.json | 7 +- module/config/i18n/zh-TW.json | 7 +- module/config/stored/classes.py | 5 + module/config/stored/stored_generated.py | 2 + module/webui/app.py | 5 + module/webui/widgets.py | 14 +- tasks/planner/model.py | 226 ++++++++++++++++++----- tasks/planner/scan.py | 1 + 17 files changed, 277 insertions(+), 65 deletions(-) diff --git a/config/template.json b/config/template.json index f15b5a042..8ef17f84a 100644 --- a/config/template.json +++ b/config/template.json @@ -47,6 +47,7 @@ "ServerUpdate": "04:00" }, "Planner": { + "PlannerOverall": {}, "Item_Credit": {}, "Item_Trailblaze_EXP": {}, "Item_Traveler_Guide": {}, diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 915b789f2..f849355a8 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -220,6 +220,14 @@ } }, "Planner": { + "PlannerOverall": { + "type": "stored", + "value": {}, + "display": true, + "stored": "StoredPlannerOverall", + "order": 4, + "color": "#85e7f2" + }, "Item_Credit": { "type": "planner", "value": {}, @@ -729,9 +737,7 @@ "type": "stored", "value": {}, "display": "hide", - "stored": "StoredEchoOfWar", - "order": 4, - "color": "#85e7f2" + "stored": "StoredEchoOfWar" }, "SimulatedUniverse": { "type": "stored", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 39430f202..1a7052d33 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -129,15 +129,18 @@ DungeonStorage: stored: StoredDungeonDouble EchoOfWar: stored: StoredEchoOfWar - order: 4 - color: "#85e7f2" SimulatedUniverse: stored: StoredSimulatedUniverse order: 6 color: "#8fb5fe" SupportReward: Collect: true -Planner: {} +Planner: + PlannerOverall: + stored: StoredPlannerOverall + display: true + order: 4 + color: "#85e7f2" # Items will be injected in config updater Weekly: diff --git a/module/config/argument/gui.yaml b/module/config/argument/gui.yaml index 419167b0c..0a8837328 100644 --- a/module/config/argument/gui.yaml +++ b/module/config/argument/gui.yaml @@ -61,6 +61,8 @@ Dashboard: HoursAgo: DaysAgo: LongTimeAgo: + # Planner + EtaDays: AddAlas: PopupTitle: diff --git a/module/config/argument/stored.json b/module/config/argument/stored.json index ced444313..b15996aa5 100644 --- a/module/config/argument/stored.json +++ b/module/config/argument/stored.json @@ -38,15 +38,15 @@ "order": 3, "color": "#79dbc4" }, - "EchoOfWar": { - "name": "EchoOfWar", - "path": "Dungeon.DungeonStorage.EchoOfWar", - "i18n": "DungeonStorage.EchoOfWar.name", - "stored": "StoredEchoOfWar", + "PlannerOverall": { + "name": "PlannerOverall", + "path": "Dungeon.Planner.PlannerOverall", + "i18n": "Planner.PlannerOverall.name", + "stored": "StoredPlannerOverall", "attrs": { "time": "2020-01-01 00:00:00", - "total": 3, - "value": 0 + "comment": " Output: value = values.pop("value", "") total = values.pop("total", "") time_ = values.pop("time", "") + comment = values.pop("comment", "") if value != "" and total != "": # 0 / 100 @@ -353,6 +354,12 @@ def put_arg_stored(kwargs: T_Output_Kwargs) -> Output: put_text(value).style("--dashboard-value--"), put_text(f" / {total}").style("--dashboard-time--"), ])] + elif value != "" and comment != "": + # 88% <1.2d + rows = [put_scope(f"dashboard-value-{name}", [ + put_text(value).style("--dashboard-value--"), + put_text(f" {comment}").style("--dashboard-time--"), + ])] elif value != "": # 100 rows = [put_scope(f"dashboard-value-{name}", [ @@ -393,6 +400,11 @@ def put_arg_planner(kwargs: T_Output_Kwargs) -> Output | None: except KeyError: # Hide items not needed by the planner return None + eta = values.get("eta", 0) + if eta > 0: + eta = f" - {t('Gui.Dashboard.EtaDays', time=eta)}" + else: + eta = "" value = values.pop('value', 0) if isinstance(value, dict): @@ -402,7 +414,7 @@ def put_arg_planner(kwargs: T_Output_Kwargs) -> Output | None: total = tuple(total.values()) row = put_scope(f"arg_stored-stored-value-{name}", [ - put_text(f"{progress:.2f}%").style("--dashboard-bold--"), + put_text(f"{progress:.2f}%{eta}").style("--dashboard-bold--"), put_text(f"{value} / {total}").style("--dashboard-time--"), ]) diff --git a/tasks/planner/model.py b/tasks/planner/model.py index 535ccb868..14e4f07cd 100644 --- a/tasks/planner/model.py +++ b/tasks/planner/model.py @@ -1,8 +1,9 @@ +import math import typing as t from datetime import datetime -from functools import partial +from functools import cached_property as functools_cached_property, partial -from pydantic import BaseModel, ValidationError, WrapValidator, field_validator, model_validator +from pydantic import BaseModel, ValidationError, WrapValidator, computed_field, field_validator, model_validator from module.base.decorator import cached_property, del_cached_property from module.config.stored.classes import now @@ -74,16 +75,37 @@ class MultiValue(BaseModelWithFallback): self.blue += other.blue self.purple += other.purple + def __sub__(self, other): + green = max(self.green - other.green, 0) + blue = max(self.blue - other.blue, 0) + purple = max(self.purple - other.purple, 0) + return MultiValue(green=green, blue=blue, purple=purple) + def equivalent_green(self): return self.green + self.blue * 3 + self.purple * 9 + def clear(self): + self.green = 0 + self.blue = 0 + self.purple = 0 + + +SET_ROW_EXCLUDE = { + 'drop_equivalent_green', + 'combat_cost', + 'progress_remain', + 'progress_total', + 'progress_current', +} + class StoredPlannerProxy(BaseModelWithFallback): item: ITEM_TYPES value: int | MultiValue = 0 total: int | MultiValue = 0 synthesize: int | MultiValue = 0 - progress: float = 0. + # progress: float = 0. + # eta: float = 0. time: datetime = DEFAULT_TIME @field_validator('item', mode='before') @@ -110,6 +132,12 @@ class StoredPlannerProxy(BaseModelWithFallback): self.synthesize = 0 return self + def clear(self): + if self.item.has_group_base: + self.value.clear() + else: + self.value = 0 + def update_synthesize(self): if self.item.has_group_base: green = self.value.green - self.total.green @@ -147,6 +175,82 @@ class StoredPlannerProxy(BaseModelWithFallback): else: self.value.blue += self.synthesize.purple * 3 + @computed_field(repr=False) + @functools_cached_property + def drop_equivalent_green(self) -> float: + # Tracks_of_Destiny + if self.item.dungeon is None: + return 1 + if self.item.dungeon.is_Calyx_Golden_Treasures: + return 24000 + if self.item.dungeon.is_Calyx_Golden_Memories: + # purple, blue, green = 5, 1, 0 + return 48 + if self.item.dungeon.is_Calyx_Golden_Aether: + # purple, blue, green = 1, 2, 2.5 + return 17.5 + if self.item.is_ItemAscension: + return 3 + if self.item.is_ItemTrace: + # purple, blue, green = 0.155, 1, 1.25 + return 5.645 + if self.item.is_ItemWeekly: + return 3 + raise ScriptError(f'{self} has no drop_equivalent_green defined') + + @computed_field(repr=False) + @functools_cached_property + def combat_cost(self) -> int: + # Tracks_of_Destiny + if self.item.dungeon is None: + return 30 + if self.item.dungeon.is_Calyx_Golden: + return 10 + if self.item.is_ItemAscension: + return 30 + if self.item.is_ItemTrace: + return 10 + if self.item.is_ItemWeekly: + return 30 + raise ScriptError(f'{self} has no stamina_pre_combat defined') + + @computed_field(repr=False) + @functools_cached_property + def progress_remain(self) -> float: + if self.item.has_group_base: + remain = self.total - self.value - self.synthesize + return remain.equivalent_green() + else: + remain = max(self.total - self.value, 0) + return remain + + @computed_field(repr=False) + @functools_cached_property + def progress_total(self) -> float: + if self.item.has_group_base: + return self.total.equivalent_green() + else: + return self.total + + @computed_field(repr=False) + @functools_cached_property + def progress_current(self) -> float: + if self.item.has_group_base: + current = self.progress_total - self.progress_remain + current = min(max(current, 0), self.progress_total) + return current + else: + current = self.value + current = min(max(current, 0), self.total) + return current + + @computed_field + @functools_cached_property + def progress(self) -> float: + # 0 to 100 + progress = self.progress_current / self.progress_total * 100 + return round(min(max(progress, 0), 100), 2) + def is_approaching_total(self, wave_done: int = 0): """ Args: @@ -157,46 +261,47 @@ class StoredPlannerProxy(BaseModelWithFallback): """ wave_done = max(wave_done, 0) # Items with a static drop rate will have `AVG * (wave_done + 1) - if self.item.dungeon.is_Calyx_Golden_Treasures: - return self.value + 24000 * (wave_done + 12) >= self.total - if self.item.dungeon.is_Calyx_Golden_Memories: - # purple, blue, green = 5, 1, 0 - value = self.value.equivalent_green() - total = self.total.equivalent_green() - return value + 48 * (wave_done + 12) >= total - if self.item.dungeon.is_Calyx_Golden_Aether: - # purple, blue, green = 1, 2, 2.5 - value = self.value.equivalent_green() - total = self.total.equivalent_green() - return value + 17.5 * (wave_done + 12) >= total - if self.item.is_ItemAscension: - return self.value + 3 * (wave_done + 1) >= self.total - if self.item.is_ItemTrace: - # purple, blue, green = 0.155, 1, 1.25 - value = self.value.equivalent_green() - total = self.total.equivalent_green() - return value + 5.645 * (wave_done + 12) >= total - if self.item.is_ItemWeekly: - return self.value + 3 * (wave_done + 1) >= self.total - return False - - def update_progress(self): - if self.item.has_group_base: - total = self.total.equivalent_green() - green = min(self.value.green, self.total.green) - blue = min(self.value.blue + self.synthesize.blue, self.total.blue) - purple = min(self.value.purple + self.synthesize.purple, self.total.purple) - value = green + blue * 3 + purple * 9 - progress = value / total * 100 - self.progress = round(min(max(progress, 0), 100), 2) + remain = self.progress_remain + cost = self.combat_cost + drop = self.drop_equivalent_green + if cost == 10: + return remain <= drop * (wave_done + 12) else: - progress = self.value / self.total * 100 - self.progress = round(min(max(progress, 0), 100), 2) + return remain <= drop * (wave_done + 1) + + @computed_field + @functools_cached_property + def eta(self) -> float: + """ + Estimate remaining days to farm + """ + if not self.need_farm(): + return 0. + if self.item.dungeon is None: + return 0. + + remain = self.progress_remain + cost = self.combat_cost + drop = self.drop_equivalent_green + + if self.item.is_ItemWeekly: + weeks = math.ceil(remain / drop / 3) + return weeks * 7 + else: + stamina = math.ceil(remain / drop) * cost + return round(stamina / 240, 1) + + def update(self, time=False): + for attr in SET_ROW_EXCLUDE: + del_cached_property(self, attr) + del_cached_property(self, 'progress') + del_cached_property(self, 'eta') - def update(self): self.update_synthesize() - self.update_progress() - self.time = now() + _ = self.progress + _ = self.eta + if time: + self.time = now() def load_value_total(self, item: ItemBase, value=None, total=None, synthesize=None): """ @@ -344,9 +449,11 @@ class PlannerProgressParser: def from_config(self, data): self.rows = {} - for row in data.values(): + for name, row in data.items(): if not row: continue + if name == 'PlannerOverall': + continue try: row = StoredPlannerProxy(**row) except (ScriptError, ValidationError) as e: @@ -355,8 +462,7 @@ class PlannerProgressParser: if not row.item.is_group_base: logger.error(f'from_config: item is not group base {row}') continue - row.update_synthesize() - row.update_progress() + row.update(time=False) self.rows[row.item.name] = row return self @@ -372,17 +478,37 @@ class PlannerProgressParser: self.rows[name] = row for row in self.rows.values(): - row.update() + row.update(time=True) def to_config(self) -> dict: data = {} for row in self.rows.values(): name = f'Item_{row.item.name}' - dic = row.model_dump() + dic = row.model_dump(exclude=SET_ROW_EXCLUDE) dic['item'] = row.item.name data[name] = dic return data + def get_overall(self): + """ + Calculate overall progress + Note that this method will clear all values + + Returns: + float: Progress percentage + float: ETA in days + """ + eta = 0. + progress_current = 0. + progress_total = 0. + for row in self.rows.values(): + eta += row.eta + progress_current += row.progress_current + progress_total += row.progress_total + + progress = round(progress_current / progress_total * 100, 2) + return progress, eta + def iter_row_to_farm(self, need_farm=True) -> t.Iterable[StoredPlannerProxy]: """ Args: @@ -494,6 +620,7 @@ class PlannerMixin(UI): planner = self.planner data = planner.to_config() + progress, eta = planner.get_overall() with self.config.multi_set(): # Set value @@ -506,5 +633,14 @@ class PlannerMixin(UI): remove.append(key) for key in remove: self.config.cross_set(f'Dungeon.Planner.{key}', {}) + print(progress, eta) + # Set overall + self.config.stored.PlannerOverall.value = f'{progress:.2f}%' + self.config.stored.PlannerOverall.comment = f'<{eta:.1f}d' del_cached_property(self, 'planner') + + +if __name__ == '__main__': + self = PlannerMixin('src') + self.planner_write(self.planner) diff --git a/tasks/planner/scan.py b/tasks/planner/scan.py index ddda92178..bca9b6fd8 100644 --- a/tasks/planner/scan.py +++ b/tasks/planner/scan.py @@ -22,6 +22,7 @@ DETAIL_TITLE.load_search(RESULT_CHECK.search) class OcrItemName(Ocr): def after_process(self, result): result = result.replace('念火之心', '忿火之心') + result = re.sub('^火之心', '忿火之心', result) result = re.sub('工造机$', '工造机杼', result) result = re.sub('工造迥?轮', '工造迴轮', result) result = re.sub('月狂[療撩]?牙', '月狂獠牙', result) From 365b1744e5dbbc840e0ca668aabb9df425a94fa4 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:43:20 +0800 Subject: [PATCH 4/5] Chore: Treat invalid staring page of planner scan as GamePageUnknownError so error image can be saved --- module/alas.py | 2 +- tasks/planner/scan.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/module/alas.py b/module/alas.py index cd6cfbc03..f03f0a18b 100644 --- a/module/alas.py +++ b/module/alas.py @@ -101,7 +101,7 @@ class AzurLaneAutoScript: self.device.sleep(10) return False except GamePageUnknownError: - logger.info('Game server may be under maintenance or network may be broken, check server status now') + # logger.info('Game server may be under maintenance or network may be broken, check server status now') self.checker.check_now() if self.checker.is_available(): logger.critical('Game page unknown') diff --git a/tasks/planner/scan.py b/tasks/planner/scan.py index bca9b6fd8..5aa40be5f 100644 --- a/tasks/planner/scan.py +++ b/tasks/planner/scan.py @@ -4,6 +4,7 @@ import cv2 from pponnxcr.predict_system import BoxedResult from module.base.utils import area_center, area_in_area +from module.exception import GamePageUnknownError from module.logger import logger from module.ocr.ocr import Ocr, OcrWhiteLetterOnComplexBackground from module.ui.scroll import AdaptiveScroll @@ -173,7 +174,7 @@ class PlannerScan(SynthesizeUI, PlannerMixin): logger.hr('Parse planner result', level=2) if not self.ui_page_appear(page_planner): logger.error('Not in page_planner, game must in the planner result page before scanning') - return [] + raise GamePageUnknownError scroll = AdaptiveScroll(RESULT_SCROLL.button, name=RESULT_SCROLL.name) scroll.drag_threshold = 0.1 From 1f734b494d730c0ef485fc043c0926d57f390692 Mon Sep 17 00:00:00 2001 From: Schwarze-Katze <68176965+Schwarze-Katze@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:29:51 +0800 Subject: [PATCH 5/5] Fix login task stuck when render error (#503) * modify similarity limit for login task * Revert modification to similarity and add asset for render error * Apply luma match for `LOGIN_CONFIRM` * cancel color match for login confirm --- tasks/base/ui.py | 20 ++++++++++++++++++-- tasks/login/login.py | 3 ++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tasks/base/ui.py b/tasks/base/ui.py index ffcaebd5e..88e62b249 100644 --- a/tasks/base/ui.py +++ b/tasks/base/ui.py @@ -96,7 +96,8 @@ class UI(MainPage): if self.handle_popup_confirm(): timeout.reset() continue - if self.appear_then_click(LOGIN_CONFIRM, interval=5): + if self.is_in_login_confirm(interval=5): + self.device.click(LOGIN_CONFIRM) timeout.reset() continue if self.appear(MAP_LOADING, interval=5): @@ -169,7 +170,8 @@ class UI(MainPage): continue if self.handle_popup_confirm(): continue - if self.appear_then_click(LOGIN_CONFIRM, interval=5): + if self.is_in_login_confirm(interval=5): + self.device.click(LOGIN_CONFIRM) continue # Reset connection @@ -327,6 +329,20 @@ class UI(MainPage): return appear + def is_in_login_confirm(self, interval=0): + self.device.stuck_record_add(LOGIN_CONFIRM) + + if interval and not self.interval_is_reached(LOGIN_CONFIRM, interval=interval): + return False + + appear = LOGIN_CONFIRM.match_template_luma(self.device.image) + + if appear and interval: + self.interval_reset(LOGIN_CONFIRM, interval=interval) + + return appear + + def is_in_map_exit(self, interval=0): self.device.stuck_record_add(MAP_EXIT) diff --git a/tasks/login/login.py b/tasks/login/login.py index f694af679..88d5ca733 100644 --- a/tasks/login/login.py +++ b/tasks/login/login.py @@ -56,7 +56,8 @@ class Login(UI, LoginAndroidCloud): orientation_timer.reset() # Login - if self.appear_then_click(LOGIN_CONFIRM): + if self.is_in_login_confirm(interval=5): + self.device.click(LOGIN_CONFIRM) login_success = True continue if self.appear_then_click(USER_AGREEMENT_ACCEPT):