Add: salvage relic (#40)

* Add: ItemUI

* Add: Salvage relic

* Upd: reuse CONFIRM_POPUP

* Add: wait_until_tab_loaded

* Add: wait_until_tab_loaded

* Fix: Return to item page when finish salvaging

* Add: Salvage relics to daily quest

* Fix typo

* Add: all item tabs' navigation

* Upd: remove unused asset

* Upd: remove item tab keywords' underscore
This commit is contained in:
Hengyu 2023-07-14 12:35:56 +08:00 committed by GitHub
parent 1c304f2baa
commit 211314aaa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 440 additions and 49 deletions

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 373 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -244,6 +244,9 @@ class KeywordExtract:
self.generate_assignment_keywords()
self.generate_forgotten_hall_stages()
self.generate_map_planes()
self.load_keywords(['养成材料', '光锥', '遗器', '其他材料', '消耗品', '任务', '贵重物'])
self.write_keywords(keyword_class='ItemTab', text_convert=lambda name: name.replace(' ', ''),
output_file='./tasks/item/keywords/tab.py')
if __name__ == '__main__':

View File

@ -6,12 +6,13 @@ from module.ocr.ocr import Ocr, OcrResultButton
from module.ocr.utils import split_and_pair_buttons
from tasks.daily.assets.assets_daily_reward import *
from tasks.daily.camera import CameraUI
from tasks.daily.consumable_usage import ConsumableUsageUI
from tasks.daily.keywords import DailyQuest, DailyQuestState, KEYWORDS_DAILY_QUEST, KEYWORDS_DAILY_QUEST_STATE
from tasks.daily.synthesize import SynthesizeConsumablesUI, SynthesizeMaterialUI
from tasks.daily.use_technique import UseTechniqueUI
from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB
from tasks.dungeon.ui import DungeonUI
from tasks.item.consumable_usage import ConsumableUsageUI
from tasks.item.relics import RelicsUI
class DailyQuestOcr(Ocr):
@ -190,6 +191,9 @@ class DailyQuestUI(DungeonUI):
if KEYWORDS_DAILY_QUEST.Use_Technique_2_times in quests:
UseTechniqueUI(self.config, self.device).use_technique(2)
done += 1
if KEYWORDS_DAILY_QUEST.Salvage_any_Relic in quests:
if RelicsUI(self.config, self.device).salvage_relic():
done += 1
return done

View File

@ -3,40 +3,20 @@ from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
ITEM_CONSUMABLE_CHECK = ButtonWrapper(
name='ITEM_CONSUMABLE_CHECK',
share=Button(
file='./assets/share/daily/consumable_usage/ITEM_CONSUMABLE_CHECK.png',
area=(39, 476, 78, 499),
search=(19, 456, 98, 519),
color=(137, 138, 139),
button=(39, 476, 78, 499),
),
)
ITEM_CONSUMABLE_SCROLL = ButtonWrapper(
name='ITEM_CONSUMABLE_SCROLL',
share=Button(
file='./assets/share/daily/consumable_usage/ITEM_CONSUMABLE_SCROLL.png',
file='./assets/share/item/consumable_usage/ITEM_CONSUMABLE_SCROLL.png',
area=(837, 89, 843, 615),
search=(817, 69, 863, 635),
color=(118, 117, 121),
button=(837, 89, 843, 615),
),
)
ITEM_GOTO_CONSUMABLE = ButtonWrapper(
name='ITEM_GOTO_CONSUMABLE',
share=Button(
file='./assets/share/daily/consumable_usage/ITEM_GOTO_CONSUMABLE.png',
area=(38, 476, 80, 499),
search=(18, 456, 100, 519),
color=(104, 106, 117),
button=(38, 476, 80, 499),
),
)
SIMPLE_PROTECTIVE_GEAR = ButtonWrapper(
name='SIMPLE_PROTECTIVE_GEAR',
share=Button(
file='./assets/share/daily/consumable_usage/SIMPLE_PROTECTIVE_GEAR.png',
file='./assets/share/item/consumable_usage/SIMPLE_PROTECTIVE_GEAR.png',
area=(164, 496, 236, 552),
search=(148, 87, 770, 579),
color=(93, 110, 115),
@ -46,7 +26,7 @@ SIMPLE_PROTECTIVE_GEAR = ButtonWrapper(
SIMPLE_PROTECTIVE_GEAR_CHECK = ButtonWrapper(
name='SIMPLE_PROTECTIVE_GEAR_CHECK',
share=Button(
file='./assets/share/daily/consumable_usage/SIMPLE_PROTECTIVE_GEAR_CHECK.png',
file='./assets/share/item/consumable_usage/SIMPLE_PROTECTIVE_GEAR_CHECK.png',
area=(1059, 190, 1177, 278),
search=(1039, 170, 1197, 298),
color=(94, 114, 121),
@ -56,7 +36,7 @@ SIMPLE_PROTECTIVE_GEAR_CHECK = ButtonWrapper(
USE_CONSUMABLE = ButtonWrapper(
name='USE_CONSUMABLE',
cn=Button(
file='./assets/cn/daily/consumable_usage/USE_CONSUMABLE.png',
file='./assets/cn/item/consumable_usage/USE_CONSUMABLE.png',
area=(1042, 647, 1084, 669),
search=(1022, 627, 1104, 689),
color=(158, 158, 159),

View File

@ -0,0 +1,45 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
FIRST_RELIC = ButtonWrapper(
name='FIRST_RELIC',
share=Button(
file='./assets/share/item/relics/FIRST_RELIC.png',
area=(434, 160, 530, 260),
search=(414, 140, 550, 280),
color=(72, 92, 124),
button=(434, 160, 530, 260),
),
)
GOTO_SALVAGE = ButtonWrapper(
name='GOTO_SALVAGE',
share=Button(
file='./assets/share/item/relics/GOTO_SALVAGE.png',
area=(668, 652, 690, 674),
search=(648, 632, 710, 694),
color=(155, 156, 156),
button=(668, 652, 690, 674),
),
)
REVERSE_ORDER = ButtonWrapper(
name='REVERSE_ORDER',
share=Button(
file='./assets/share/item/relics/REVERSE_ORDER.png',
area=(720, 649, 738, 667),
search=(700, 629, 758, 687),
color=(179, 180, 180),
button=(720, 649, 738, 667),
),
)
SALVAGE = ButtonWrapper(
name='SALVAGE',
share=Button(
file='./assets/share/item/relics/SALVAGE.png',
area=(1038, 646, 1062, 670),
search=(1018, 626, 1082, 690),
color=(122, 115, 98),
button=(1038, 646, 1062, 670),
),
)

View File

@ -0,0 +1,145 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CONSUMABLE_CHECK = ButtonWrapper(
name='CONSUMABLE_CHECK',
share=Button(
file='./assets/share/item/ui/CONSUMABLE_CHECK.png',
area=(39, 476, 78, 499),
search=(19, 456, 98, 519),
color=(137, 138, 139),
button=(39, 476, 78, 499),
),
)
CONSUMABLE_CLICK = ButtonWrapper(
name='CONSUMABLE_CLICK',
share=Button(
file='./assets/share/item/ui/CONSUMABLE_CLICK.png',
area=(38, 476, 80, 499),
search=(18, 456, 100, 519),
color=(104, 106, 117),
button=(38, 476, 80, 499),
),
)
LIGHT_CONE_CHECK = ButtonWrapper(
name='LIGHT_CONE_CHECK',
share=Button(
file='./assets/share/item/ui/LIGHT_CONE_CHECK.png',
area=(43, 207, 73, 228),
search=(23, 187, 93, 248),
color=(191, 193, 193),
button=(43, 207, 73, 228),
),
)
LIGHT_CONE_CLICK = ButtonWrapper(
name='LIGHT_CONE_CLICK',
share=Button(
file='./assets/share/item/ui/LIGHT_CONE_CLICK.png',
area=(42, 205, 72, 226),
search=(22, 185, 92, 246),
color=(48, 50, 50),
button=(42, 205, 72, 226),
),
)
MISSIONS_CHECK = ButtonWrapper(
name='MISSIONS_CHECK',
share=Button(
file='./assets/share/item/ui/MISSIONS_CHECK.png',
area=(42, 570, 75, 588),
search=(22, 550, 95, 608),
color=(91, 92, 92),
button=(42, 570, 75, 588),
),
)
MISSIONS_CLICK = ButtonWrapper(
name='MISSIONS_CLICK',
share=Button(
file='./assets/share/item/ui/MISSIONS_CLICK.png',
area=(43, 569, 74, 589),
search=(23, 549, 94, 609),
color=(115, 117, 117),
button=(43, 569, 74, 589),
),
)
OTHER_MATERIALS_CHECK = ButtonWrapper(
name='OTHER_MATERIALS_CHECK',
share=Button(
file='./assets/share/item/ui/OTHER_MATERIALS_CHECK.png',
area=(41, 385, 77, 408),
search=(21, 365, 97, 428),
color=(122, 123, 123),
button=(41, 385, 77, 408),
),
)
OTHER_MATERIALS_CLICK = ButtonWrapper(
name='OTHER_MATERIALS_CLICK',
share=Button(
file='./assets/share/item/ui/OTHER_MATERIALS_CLICK.png',
area=(40, 387, 78, 407),
search=(20, 367, 98, 427),
color=(92, 94, 94),
button=(40, 387, 78, 407),
),
)
RELICS_CHECK = ButtonWrapper(
name='RELICS_CHECK',
share=Button(
file='./assets/share/item/ui/RELICS_CHECK.png',
area=(38, 297, 80, 317),
search=(18, 277, 100, 337),
color=(101, 103, 103),
button=(38, 297, 80, 317),
),
)
RELICS_CLICK = ButtonWrapper(
name='RELICS_CLICK',
share=Button(
file='./assets/share/item/ui/RELICS_CLICK.png',
area=(36, 298, 78, 319),
search=(16, 278, 98, 339),
color=(121, 124, 129),
button=(36, 298, 78, 319),
),
)
UPGRADE_MATERIAL_CHECK = ButtonWrapper(
name='UPGRADE_MATERIAL_CHECK',
share=Button(
file='./assets/share/item/ui/UPGRADE_MATERIAL_CHECK.png',
area=(39, 117, 78, 139),
search=(19, 97, 98, 159),
color=(195, 199, 198),
button=(39, 117, 78, 139),
),
)
UPGRADE_MATERIAL_CLICK = ButtonWrapper(
name='UPGRADE_MATERIAL_CLICK',
share=Button(
file='./assets/share/item/ui/UPGRADE_MATERIAL_CLICK.png',
area=(40, 120, 78, 139),
search=(20, 100, 98, 159),
color=(114, 118, 125),
button=(40, 120, 78, 139),
),
)
VALUABLES_CHECK = ButtonWrapper(
name='VALUABLES_CHECK',
share=Button(
file='./assets/share/item/ui/VALUABLES_CHECK.png',
area=(44, 641, 73, 653),
search=(24, 621, 93, 673),
color=(91, 86, 92),
button=(44, 641, 73, 653),
),
)
VALUABLES_CLICK = ButtonWrapper(
name='VALUABLES_CLICK',
share=Button(
file='./assets/share/item/ui/VALUABLES_CLICK.png',
area=(45, 640, 72, 655),
search=(25, 620, 92, 675),
color=(123, 125, 125),
button=(45, 640, 72, 655),
),
)

View File

@ -1,16 +1,18 @@
from module.ocr.ocr import *
from module.ui.scroll import Scroll
from tasks.base.page import page_item
from tasks.base.ui import UI
from tasks.daily.synthesize import SynthesizeConsumablesUI
from tasks.base.assets.assets_base_popup import CONFIRM_POPUP
from tasks.daily.assets.assets_daily_consumable_usage import *
from tasks.base.page import page_item
from tasks.daily.assets.assets_daily_synthesize_consumable import \
SIMPLE_PROTECTIVE_GEAR as SYNTHESIZE_SIMPLE_PROTECTIVE_GEAR, \
SIMPLE_PROTECTIVE_GEAR_CHECK as SYNTHESIZE_SIMPLE_PROTECTIVE_GEAR_CHECK
from tasks.daily.synthesize import SynthesizeConsumablesUI
from tasks.item.assets.assets_item_consumable_usage import *
from tasks.item.assets.assets_item_ui import CONSUMABLE_CHECK
from tasks.item.keywords import KEYWORD_ITEM_TAB
from tasks.item.ui import ItemUI
class ConsumableUsageUI(UI):
class ConsumableUsageUI(ItemUI):
def use_consumable(self, synthesize_or_not: bool = True) -> bool:
"""
Args:
@ -26,7 +28,7 @@ class ConsumableUsageUI(UI):
"""
logger.hr('Use consumable', level=2)
self.ui_ensure(page_item)
self._switch_tag_to_consumables()
self.item_goto(KEYWORD_ITEM_TAB.Consumables)
if self._search_and_select_consumable():
self._click_use()
self._confirm_use()
@ -36,34 +38,35 @@ class ConsumableUsageUI(UI):
if not synthesize_or_not:
return False
if SynthesizeConsumablesUI(self.config, self.device).synthesize_consumables(
SYNTHESIZE_SIMPLE_PROTECTIVE_GEAR, SYNTHESIZE_SIMPLE_PROTECTIVE_GEAR_CHECK
SYNTHESIZE_SIMPLE_PROTECTIVE_GEAR, SYNTHESIZE_SIMPLE_PROTECTIVE_GEAR_CHECK
):
return self.use_consumable(synthesize_or_not=False)
else:
return False
def _switch_tag_to_consumables(self, skip_first_screenshot=True):
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# Scroll bar delay appears, need an interval here
if self.appear(ITEM_CONSUMABLE_CHECK, interval=1):
logger.info('Item consumables page appear')
break
# Switch to consumables subpage
if self.appear_then_click(ITEM_GOTO_CONSUMABLE):
logger.info('Switch to consumables subpage')
continue
# def _switch_tag_to_consumables(self, skip_first_screenshot=True):
# while 1:
# if skip_first_screenshot:
# skip_first_screenshot = False
# else:
# self.device.screenshot()
#
# # Scroll bar delay appears, need an interval here
# if self.appear(ITEM_CONSUMABLE_CHECK, interval=1):
# logger.info('Item consumables page appear')
# break
# # Switch to consumables subpage
# if self.appear_then_click(ITEM_GOTO_CONSUMABLE):
# logger.info('Switch to consumables subpage')
# continue
def _search_and_select_consumable(self, skip_first_screenshot=True) -> bool:
logger.info('Search consumable')
# If the default subpage is the consumables page, it is necessary to screenshot and check subpage again,
# because in this scenario, scroll bar delay appears and the previous screenshot was
# taken after clicking on the "item" to determine whether to enter the "item", which may be inaccurate
self._switch_tag_to_consumables(False)
# self._switch_tag_to_consumables(False)
self.item_goto(KEYWORD_ITEM_TAB.Consumables)
# Determine if there is a scroll bar. If there is a scroll bar,
# pull it down and check if the consumable to be used can be found
@ -83,6 +86,8 @@ class ConsumableUsageUI(UI):
return True
if scroll.at_bottom(main=self) and not self.appear(SIMPLE_PROTECTIVE_GEAR, similarity=0.7):
logger.info('Can not find the consumable which to be used, just skip')
from PIL import Image
Image.fromarray(self.device.image).save("./screenshots/image3.png")
return False
if self.appear_then_click(SIMPLE_PROTECTIVE_GEAR, similarity=0.7):
logger.info('Select the consumable which to be used')
@ -137,7 +142,7 @@ class ConsumableUsageUI(UI):
else:
self.device.screenshot()
if self.appear(ITEM_CONSUMABLE_CHECK):
if self.appear(CONSUMABLE_CHECK):
logger.info('Complete using consumables')
break
# If there is already consumable effect, a confirmation box will pop up again,

View File

@ -0,0 +1,2 @@
import tasks.item.keywords.tab as KEYWORD_ITEM_TAB
from tasks.item.keywords.classes import ItemTab

View File

@ -0,0 +1,9 @@
from dataclasses import dataclass
from typing import ClassVar
from module.ocr.keyword import Keyword
@dataclass(repr=False)
class ItemTab(Keyword):
instances: ClassVar = {}

View File

@ -0,0 +1,61 @@
from .classes import ItemTab
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.keyword_extract ```
UpgradeMaterials = ItemTab(
id=1,
name='UpgradeMaterials',
cn='养成材料',
cht='養成素材',
en='Upgrade Materials',
jp='育成素材',
)
LightCone = ItemTab(
id=2,
name='LightCone',
cn='光锥',
cht='光錐',
en='Light Cone',
jp='光円錐',
)
Relics = ItemTab(
id=3,
name='Relics',
cn='遗器',
cht='遺器',
en='Relics',
jp='遺物',
)
OtherMaterials = ItemTab(
id=4,
name='OtherMaterials',
cn='其他材料',
cht='其他素材',
en='Other Materials',
jp='その他',
)
Consumables = ItemTab(
id=5,
name='Consumables',
cn='消耗品',
cht='消耗品',
en='Consumables',
jp='消耗品',
)
Missions = ItemTab(
id=6,
name='Missions',
cn='任务',
cht='任務',
en='Missions',
jp='クエスト',
)
Valuables = ItemTab(
id=7,
name='Valuables',
cn='贵重物',
cht='貴重物',
en='Valuables',
jp='貴重品',
)

69
tasks/item/relics.py Normal file
View File

@ -0,0 +1,69 @@
from module.base.timer import Timer
from module.logger import logger
from tasks.base.assets.assets_base_page import CLOSE
from tasks.base.assets.assets_base_popup import CONFIRM_POPUP
from tasks.item.assets.assets_item_relics import *
from tasks.item.keywords import KEYWORD_ITEM_TAB
from tasks.item.ui import ItemUI
class RelicsUI(ItemUI):
def salvage_relic(self, skip_first_screenshot=True) -> bool:
logger.hr('Salvage Relic', level=2)
self.item_goto(KEYWORD_ITEM_TAB.Relics, wait_until_stable=False)
interval = Timer(1)
while 1: # relic tab -> salvage
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(REVERSE_ORDER):
break
if self.appear_then_click(GOTO_SALVAGE):
continue
skip_first_screenshot = True
while 1: # salvage -> first relic selected
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear_then_click(REVERSE_ORDER): # this should judge before break condition
continue
if self.appear(SALVAGE):
break
if interval.reached() and self.image_color_count(FIRST_RELIC, (233, 192, 108)):
self.device.click(FIRST_RELIC)
interval.reset()
continue
skip_first_screenshot = True
while 1: # selected -> rewards claimed
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.handle_reward():
logger.info("Relic salvaged")
break
if self.appear_then_click(SALVAGE):
continue
if self.appear_then_click(CONFIRM_POPUP):
continue
skip_first_screenshot = True
while 1: # rewards claimed -> relic tab page
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(GOTO_SALVAGE):
logger.info("Salvage page exited")
break
if self.appear_then_click(CLOSE, interval=1):
continue
return True

68
tasks/item/ui.py Normal file
View File

@ -0,0 +1,68 @@
from module.logger import logger
from module.ui.switch import Switch
from tasks.base.page import page_item
from tasks.base.ui import UI
from tasks.item.assets.assets_item_consumable_usage import SIMPLE_PROTECTIVE_GEAR
from tasks.item.assets.assets_item_ui import *
from tasks.item.keywords import KEYWORD_ITEM_TAB
SWITCH_ITEM_TAB = Switch('ItemTab', is_selector=True)
SWITCH_ITEM_TAB.add_state(
KEYWORD_ITEM_TAB.UpgradeMaterials,
check_button=UPGRADE_MATERIAL_CHECK,
click_button=UPGRADE_MATERIAL_CLICK
)
SWITCH_ITEM_TAB.add_state(
KEYWORD_ITEM_TAB.LightCone,
check_button=LIGHT_CONE_CHECK,
click_button=LIGHT_CONE_CLICK
)
SWITCH_ITEM_TAB.add_state(
KEYWORD_ITEM_TAB.Relics,
check_button=RELICS_CHECK,
click_button=RELICS_CLICK
)
SWITCH_ITEM_TAB.add_state(
KEYWORD_ITEM_TAB.OtherMaterials,
check_button=OTHER_MATERIALS_CHECK,
click_button=OTHER_MATERIALS_CLICK
)
SWITCH_ITEM_TAB.add_state(
KEYWORD_ITEM_TAB.Consumables,
check_button=CONSUMABLE_CHECK,
click_button=CONSUMABLE_CLICK,
)
SWITCH_ITEM_TAB.add_state(
KEYWORD_ITEM_TAB.Missions,
check_button=MISSIONS_CHECK,
click_button=MISSIONS_CLICK
)
SWITCH_ITEM_TAB.add_state(
KEYWORD_ITEM_TAB.Valuables,
check_button=VALUABLES_CHECK,
click_button=VALUABLES_CLICK
)
class ItemUI(UI):
def item_goto(self, state: KEYWORD_ITEM_TAB, wait_until_stable=True):
"""
Args:
state:
wait_until_stable: if subsequent actions are manipulating items, or any other thing that needs to load
inside the tab, wait_until_stable should set to True
Returns:
self = ItemUI('alas')
self.device.screenshot()
self.item_goto(KEYWORD_ITEM_TAB.Relics)
self.item_goto(KEYWORD_ITEM_TAB.Consumables)
"""
logger.hr('Item tab goto', level=2)
self.ui_ensure(page_item)
SWITCH_ITEM_TAB.set(state, main=self)
if wait_until_stable:
logger.info(f'Tab goto {state}, wait until loaded')
self.wait_until_stable(SIMPLE_PROTECTIVE_GEAR)
else:
logger.info(f'Tab goto {state}')