diff --git a/assets/cn/equipment/FLEET_DETAIL_CHECK.png b/assets/cn/equipment/FLEET_DETAIL_CHECK.png new file mode 100644 index 000000000..0bfc387c4 Binary files /dev/null and b/assets/cn/equipment/FLEET_DETAIL_CHECK.png differ diff --git a/assets/cn/handler/AUTO_SEARCH_SET_ALL_15.png b/assets/cn/handler/AUTO_SEARCH_SET_ALL_15.png new file mode 100644 index 000000000..99fa64ca0 Binary files /dev/null and b/assets/cn/handler/AUTO_SEARCH_SET_ALL_15.png differ diff --git a/assets/cn/handler/AUTO_SEARCH_SET_BOSS_15.png b/assets/cn/handler/AUTO_SEARCH_SET_BOSS_15.png new file mode 100644 index 000000000..21ff805e5 Binary files /dev/null and b/assets/cn/handler/AUTO_SEARCH_SET_BOSS_15.png differ diff --git a/assets/cn/handler/AUTO_SEARCH_SET_MOB_15.png b/assets/cn/handler/AUTO_SEARCH_SET_MOB_15.png new file mode 100644 index 000000000..5605e5d7b Binary files /dev/null and b/assets/cn/handler/AUTO_SEARCH_SET_MOB_15.png differ diff --git a/assets/cn/handler/AUTO_SEARCH_SET_STANDBY_15.png b/assets/cn/handler/AUTO_SEARCH_SET_STANDBY_15.png new file mode 100644 index 000000000..9953a2e6f Binary files /dev/null and b/assets/cn/handler/AUTO_SEARCH_SET_STANDBY_15.png differ diff --git a/assets/cn/handler/AUTO_SEARCH_SET_SUB_AUTO_15.png b/assets/cn/handler/AUTO_SEARCH_SET_SUB_AUTO_15.png new file mode 100644 index 000000000..f7c438d4e Binary files /dev/null and b/assets/cn/handler/AUTO_SEARCH_SET_SUB_AUTO_15.png differ diff --git a/assets/cn/handler/AUTO_SEARCH_SET_SUB_STANDBY_15.png b/assets/cn/handler/AUTO_SEARCH_SET_SUB_STANDBY_15.png new file mode 100644 index 000000000..6ad169195 Binary files /dev/null and b/assets/cn/handler/AUTO_SEARCH_SET_SUB_STANDBY_15.png differ diff --git a/assets/cn/handler/MOB_MOVE_1.png b/assets/cn/handler/MOB_MOVE_1.png new file mode 100644 index 000000000..de5971cf4 Binary files /dev/null and b/assets/cn/handler/MOB_MOVE_1.png differ diff --git a/assets/cn/handler/MOB_MOVE_2.png b/assets/cn/handler/MOB_MOVE_2.png new file mode 100644 index 000000000..e88b737ba Binary files /dev/null and b/assets/cn/handler/MOB_MOVE_2.png differ diff --git a/assets/cn/handler/MOB_MOVE_CANCEL.png b/assets/cn/handler/MOB_MOVE_CANCEL.png new file mode 100644 index 000000000..3692eb216 Binary files /dev/null and b/assets/cn/handler/MOB_MOVE_CANCEL.png differ diff --git a/assets/cn/template/TEMPLATE_ENEMY_CarrierSpecial.gif b/assets/cn/template/TEMPLATE_ENEMY_CarrierSpecial.gif new file mode 100644 index 000000000..ae5936c83 Binary files /dev/null and b/assets/cn/template/TEMPLATE_ENEMY_CarrierSpecial.gif differ diff --git a/assets/cn/template/TEMPLATE_MOB_MOVE_ICON.png b/assets/cn/template/TEMPLATE_MOB_MOVE_ICON.png new file mode 100644 index 000000000..2efbaf9ca Binary files /dev/null and b/assets/cn/template/TEMPLATE_MOB_MOVE_ICON.png differ diff --git a/assets/cn/template/TEMPLATE_SIREN_DidoIdol2.gif b/assets/cn/template/TEMPLATE_SIREN_DidoIdol2.gif new file mode 100644 index 000000000..32eac9a5f Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_DidoIdol2.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_Elizabeth3.gif b/assets/cn/template/TEMPLATE_SIREN_Elizabeth3.gif new file mode 100644 index 000000000..657edf629 Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_Elizabeth3.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_GascogneIdol.gif b/assets/cn/template/TEMPLATE_SIREN_GascogneIdol.gif new file mode 100644 index 000000000..377fc0be4 Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_GascogneIdol.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_Laffey6.gif b/assets/cn/template/TEMPLATE_SIREN_Laffey6.gif new file mode 100644 index 000000000..54094373f Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_Laffey6.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_RoonIdol2.gif b/assets/cn/template/TEMPLATE_SIREN_RoonIdol2.gif new file mode 100644 index 000000000..e24b7e40f Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_RoonIdol2.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.gif b/assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.gif new file mode 100644 index 000000000..c33810aa6 Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.png b/assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.png deleted file mode 100644 index 25445dd60..000000000 Binary files a/assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.png and /dev/null differ diff --git a/assets/cn/template/TEMPLATE_SIREN_TaihouIdol.gif b/assets/cn/template/TEMPLATE_SIREN_TaihouIdol.gif new file mode 100644 index 000000000..9346ec9de Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_TaihouIdol.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_TashkentIdol.gif b/assets/cn/template/TEMPLATE_SIREN_TashkentIdol.gif new file mode 100644 index 000000000..b54d0575a Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_TashkentIdol.gif differ diff --git a/assets/cn/template/TEMPLATE_SIREN_Z23_5.gif b/assets/cn/template/TEMPLATE_SIREN_Z23_5.gif new file mode 100644 index 000000000..38d890793 Binary files /dev/null and b/assets/cn/template/TEMPLATE_SIREN_Z23_5.gif differ diff --git a/assets/en/equipment/FLEET_DETAIL_CHECK.png b/assets/en/equipment/FLEET_DETAIL_CHECK.png new file mode 100644 index 000000000..472a4db06 Binary files /dev/null and b/assets/en/equipment/FLEET_DETAIL_CHECK.png differ diff --git a/assets/en/handler/MOB_MOVE_1.png b/assets/en/handler/MOB_MOVE_1.png new file mode 100644 index 000000000..de5971cf4 Binary files /dev/null and b/assets/en/handler/MOB_MOVE_1.png differ diff --git a/assets/en/handler/MOB_MOVE_2.png b/assets/en/handler/MOB_MOVE_2.png new file mode 100644 index 000000000..e88b737ba Binary files /dev/null and b/assets/en/handler/MOB_MOVE_2.png differ diff --git a/assets/en/template/TEMPLATE_SIREN_DidoIdol2.gif b/assets/en/template/TEMPLATE_SIREN_DidoIdol2.gif new file mode 100644 index 000000000..32eac9a5f Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_DidoIdol2.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_Elizabeth3.gif b/assets/en/template/TEMPLATE_SIREN_Elizabeth3.gif new file mode 100644 index 000000000..657edf629 Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_Elizabeth3.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_GascogneIdol.gif b/assets/en/template/TEMPLATE_SIREN_GascogneIdol.gif new file mode 100644 index 000000000..377fc0be4 Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_GascogneIdol.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_Laffey6.gif b/assets/en/template/TEMPLATE_SIREN_Laffey6.gif new file mode 100644 index 000000000..54094373f Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_Laffey6.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_RoonIdol2.gif b/assets/en/template/TEMPLATE_SIREN_RoonIdol2.gif new file mode 100644 index 000000000..e24b7e40f Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_RoonIdol2.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_SheffieldIdol.gif b/assets/en/template/TEMPLATE_SIREN_SheffieldIdol.gif new file mode 100644 index 000000000..c33810aa6 Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_SheffieldIdol.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_SheffieldIdol.png b/assets/en/template/TEMPLATE_SIREN_SheffieldIdol.png deleted file mode 100644 index 25445dd60..000000000 Binary files a/assets/en/template/TEMPLATE_SIREN_SheffieldIdol.png and /dev/null differ diff --git a/assets/en/template/TEMPLATE_SIREN_TaihouIdol.gif b/assets/en/template/TEMPLATE_SIREN_TaihouIdol.gif new file mode 100644 index 000000000..9346ec9de Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_TaihouIdol.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_TashkentIdol.gif b/assets/en/template/TEMPLATE_SIREN_TashkentIdol.gif new file mode 100644 index 000000000..b54d0575a Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_TashkentIdol.gif differ diff --git a/assets/en/template/TEMPLATE_SIREN_Z23_5.gif b/assets/en/template/TEMPLATE_SIREN_Z23_5.gif new file mode 100644 index 000000000..38d890793 Binary files /dev/null and b/assets/en/template/TEMPLATE_SIREN_Z23_5.gif differ diff --git a/assets/jp/equipment/FLEET_DETAIL_CHECK.png b/assets/jp/equipment/FLEET_DETAIL_CHECK.png new file mode 100644 index 000000000..da1eff331 Binary files /dev/null and b/assets/jp/equipment/FLEET_DETAIL_CHECK.png differ diff --git a/assets/jp/handler/MOB_MOVE_1.png b/assets/jp/handler/MOB_MOVE_1.png new file mode 100644 index 000000000..de5971cf4 Binary files /dev/null and b/assets/jp/handler/MOB_MOVE_1.png differ diff --git a/assets/jp/handler/MOB_MOVE_2.png b/assets/jp/handler/MOB_MOVE_2.png new file mode 100644 index 000000000..e88b737ba Binary files /dev/null and b/assets/jp/handler/MOB_MOVE_2.png differ diff --git a/assets/jp/handler/MOB_MOVE_CANCEL.png b/assets/jp/handler/MOB_MOVE_CANCEL.png new file mode 100644 index 000000000..1e4ad2b82 Binary files /dev/null and b/assets/jp/handler/MOB_MOVE_CANCEL.png differ diff --git a/assets/jp/template/TEMPLATE_SIREN_DidoIdol2.gif b/assets/jp/template/TEMPLATE_SIREN_DidoIdol2.gif new file mode 100644 index 000000000..32eac9a5f Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_DidoIdol2.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_Elizabeth3.gif b/assets/jp/template/TEMPLATE_SIREN_Elizabeth3.gif new file mode 100644 index 000000000..657edf629 Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_Elizabeth3.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_GascogneIdol.gif b/assets/jp/template/TEMPLATE_SIREN_GascogneIdol.gif new file mode 100644 index 000000000..377fc0be4 Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_GascogneIdol.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_Laffey6.gif b/assets/jp/template/TEMPLATE_SIREN_Laffey6.gif new file mode 100644 index 000000000..54094373f Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_Laffey6.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_RoonIdol2.gif b/assets/jp/template/TEMPLATE_SIREN_RoonIdol2.gif new file mode 100644 index 000000000..e24b7e40f Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_RoonIdol2.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_SheffieldIdol.gif b/assets/jp/template/TEMPLATE_SIREN_SheffieldIdol.gif new file mode 100644 index 000000000..c33810aa6 Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_SheffieldIdol.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_TaihouIdol.gif b/assets/jp/template/TEMPLATE_SIREN_TaihouIdol.gif new file mode 100644 index 000000000..9346ec9de Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_TaihouIdol.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_TashkentIdol.gif b/assets/jp/template/TEMPLATE_SIREN_TashkentIdol.gif new file mode 100644 index 000000000..b54d0575a Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_TashkentIdol.gif differ diff --git a/assets/jp/template/TEMPLATE_SIREN_Z23_5.gif b/assets/jp/template/TEMPLATE_SIREN_Z23_5.gif new file mode 100644 index 000000000..38d890793 Binary files /dev/null and b/assets/jp/template/TEMPLATE_SIREN_Z23_5.gif differ diff --git a/assets/mask/MASK_MAP_UI_W15.png b/assets/mask/MASK_MAP_UI_W15.png new file mode 100644 index 000000000..656bc39a9 Binary files /dev/null and b/assets/mask/MASK_MAP_UI_W15.png differ diff --git a/assets/shop/medal/SpecializedCore.png b/assets/shop/medal/SpecializedCore.png new file mode 100644 index 000000000..c2ec5fc29 Binary files /dev/null and b/assets/shop/medal/SpecializedCore.png differ diff --git a/assets/tw/equipment/FLEET_DETAIL_CHECK.png b/assets/tw/equipment/FLEET_DETAIL_CHECK.png new file mode 100644 index 000000000..ff18f0f7f Binary files /dev/null and b/assets/tw/equipment/FLEET_DETAIL_CHECK.png differ diff --git a/assets/tw/handler/MOB_MOVE_1.png b/assets/tw/handler/MOB_MOVE_1.png new file mode 100644 index 000000000..de5971cf4 Binary files /dev/null and b/assets/tw/handler/MOB_MOVE_1.png differ diff --git a/assets/tw/handler/MOB_MOVE_2.png b/assets/tw/handler/MOB_MOVE_2.png new file mode 100644 index 000000000..e88b737ba Binary files /dev/null and b/assets/tw/handler/MOB_MOVE_2.png differ diff --git a/assets/tw/template/TEMPLATE_SIREN_DidoIdol2.gif b/assets/tw/template/TEMPLATE_SIREN_DidoIdol2.gif new file mode 100644 index 000000000..32eac9a5f Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_DidoIdol2.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_Elizabeth3.gif b/assets/tw/template/TEMPLATE_SIREN_Elizabeth3.gif new file mode 100644 index 000000000..657edf629 Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_Elizabeth3.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_GascogneIdol.gif b/assets/tw/template/TEMPLATE_SIREN_GascogneIdol.gif new file mode 100644 index 000000000..377fc0be4 Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_GascogneIdol.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_Laffey6.gif b/assets/tw/template/TEMPLATE_SIREN_Laffey6.gif new file mode 100644 index 000000000..54094373f Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_Laffey6.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_RoonIdol2.gif b/assets/tw/template/TEMPLATE_SIREN_RoonIdol2.gif new file mode 100644 index 000000000..e24b7e40f Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_RoonIdol2.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_SheffieldIdol.gif b/assets/tw/template/TEMPLATE_SIREN_SheffieldIdol.gif new file mode 100644 index 000000000..c33810aa6 Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_SheffieldIdol.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_TaihouIdol.gif b/assets/tw/template/TEMPLATE_SIREN_TaihouIdol.gif new file mode 100644 index 000000000..9346ec9de Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_TaihouIdol.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_TashkentIdol.gif b/assets/tw/template/TEMPLATE_SIREN_TashkentIdol.gif new file mode 100644 index 000000000..b54d0575a Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_TashkentIdol.gif differ diff --git a/assets/tw/template/TEMPLATE_SIREN_Z23_5.gif b/assets/tw/template/TEMPLATE_SIREN_Z23_5.gif new file mode 100644 index 000000000..38d890793 Binary files /dev/null and b/assets/tw/template/TEMPLATE_SIREN_Z23_5.gif differ diff --git a/bin/MaaTouch/maatouch b/bin/MaaTouch/maatouch index 3673f3ff0..e164cdb89 100644 Binary files a/bin/MaaTouch/maatouch and b/bin/MaaTouch/maatouch differ diff --git a/campaign/Readme.md b/campaign/Readme.md index 3a2f5da98..de950e751 100644 --- a/campaign/Readme.md +++ b/campaign/Readme.md @@ -191,6 +191,5 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20240328 | raid 20240328 | From Zero to Hero | 从零开始的魔王讨伐之旅 | From Zero to Hero | ゼロから頑張る魔王討伐 | - | | 20240403 | event 20211111 cn | The Flame-Touched Dagger | - | - | - | 復刻杰諾瓦的焰火 | | 20240411 | event 20220224 cn | Abyssal Refrain Rerun | 复刻深度回音 | Abyssal Refrain Rerun | 鳴動せし星霜の淵(復刻) | - | -| 20240411 | event 20220224 cn | Abyssal Refrain Rerun | 复刻深度回音 | Abyssal Refrain Rerun | 鳴動せし星霜の淵(復刻) | - | -| 20240403 | event 20211111 cn | The Flame-Touched Dagger | - | - | - | 復刻杰諾瓦的焰火 | -| 20240411 | event 20220224 cn | Abyssal Refrain Rerun | 复刻深度回音 | Abyssal Refrain Rerun | 鳴動せし星霜の淵(復刻) | - | \ No newline at end of file +| 20240425 | event 20230817 cn | The Fool's Scales | - | - | - | 愚者的天平 | +| 20240425 | event 20240425 cn | Heart-Linking Harmony | 共鸣的PASSION | Heart-Linking Harmony | 共鳴のパッション | - | diff --git a/campaign/campaign_main/campaign_15_1.py b/campaign/campaign_main/campaign_15_1.py index d08a2ea3d..f98c6da49 100644 --- a/campaign/campaign_main/campaign_15_1.py +++ b/campaign/campaign_main/campaign_15_1.py @@ -1,20 +1,23 @@ -from module.campaign.campaign_base import CampaignBase +from module.logger import logger from module.map.map_base import CampaignMap from module.map.map_grids import SelectedGrids, RoadGrids -from module.logger import logger + +from .campaign_15_base import CampaignBase +from .campaign_15_base import Config as ConfigBase MAP = CampaignMap('15-1') MAP.shape = 'H7' -MAP.camera_data = ['D2', 'D5', 'E2', 'E5'] -MAP.camera_data_spawn_point = ['D5'] +MAP.camera_data = ['C2', 'C5', 'E2', 'E5'] +MAP.camera_data_spawn_point = ['C5'] +MAP.camera_sight = (-2, -1, 3, 2) MAP.map_data = """ - Me ME ME ++ ME MB ++ ++ - ME ME ME ME Me ME MB ++ - ++ ME ME ME Me ME ME MB - ++ ME ME ME ME __ Me ME - ME ME ME ME ME Me ME ME - ME ME ME ME ++ ME ME ME - ME SP SP ME Me ME ME ME + Me Me ME ++ ME MB ++ ++ + ME ME Me ME Me -- MB ++ + ++ Me ME -- Me -- -- MB + ++ ME -- -- -- __ Me -- + -- ME -- ME ME Me -- ME + ME -- -- ME ++ -- ME -- + -- SP SP -- Me -- ME -- """ MAP.weight_data = """ 50 50 50 50 50 50 50 50 @@ -26,7 +29,7 @@ MAP.weight_data = """ 50 50 50 50 50 50 50 50 """ MAP.spawn_data = [ - {'battle': 0, 'enemy': 2, 'mystery': 1}, + {'battle': 0, 'enemy': 5}, {'battle': 1, 'enemy': 2}, {'battle': 2, 'enemy': 1}, {'battle': 3}, @@ -44,34 +47,54 @@ A7, B7, C7, D7, E7, F7, G7, H7, \ = MAP.flatten() -class Config: +class Config(ConfigBase): # ===== Start of generated config ===== - MAP_SIREN_TEMPLATE = ['0'] - MOVABLE_ENEMY_TURN = (2,) - MAP_HAS_SIREN = False - MAP_HAS_MOVABLE_ENEMY = False + # MAP_SIREN_TEMPLATE = ['0'] + # MOVABLE_ENEMY_TURN = (2,) + # MAP_HAS_SIREN = True + # MAP_HAS_MOVABLE_ENEMY = True MAP_HAS_MAP_STORY = False MAP_HAS_FLEET_STEP = False MAP_HAS_AMBUSH = True - MAP_HAS_MYSTERY = True + # MAP_HAS_MYSTERY = True # ===== End of generated config ===== + MAP_WALK_USE_CURRENT_FLEET = True + class Campaign(CampaignBase): MAP = MAP - ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_function(self): + if self.config.MAP_CLEAR_ALL_THIS_TIME \ + and self.battle_count == 0 and not self.map_is_clear_mode: + func = self.FUNCTION_NAME_BASE + str(self.battle_count) + logger.info(f'Using function: {func}') + func = self.__getattribute__(func) + result = func() + return result + + return super().battle_function() def battle_0(self): - if self.clear_siren(): + if not self.map_is_clear_mode and self.map_has_mob_move: + self.mob_move(B3, C3) + if B1.is_accessible: + self.clear_chosen_enemy(B1) + return True + + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): return True + + return self.battle_default() + + def battle_1(self): if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): return True return self.battle_default() def battle_5(self): - if self.clear_siren(): - return True if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): return True diff --git a/campaign/campaign_main/campaign_15_2.py b/campaign/campaign_main/campaign_15_2.py index 14da35ce9..038ebf1a3 100644 --- a/campaign/campaign_main/campaign_15_2.py +++ b/campaign/campaign_main/campaign_15_2.py @@ -1,22 +1,24 @@ -from module.campaign.campaign_base import CampaignBase +from module.logger import logger from module.map.map_base import CampaignMap from module.map.map_grids import SelectedGrids, RoadGrids -from module.logger import logger +from .campaign_15_base import CampaignBase +from .campaign_15_base import Config as ConfigBase MAP = CampaignMap('15-2') MAP.shape = 'I8' -MAP.camera_data = ['D2', 'D6', 'F2', 'F6'] +MAP.camera_data = ['C2', 'C6', 'F2', 'F6'] MAP.camera_data_spawn_point = ['F2'] +MAP.camera_sight = (-2, -1, 3, 2) MAP.map_data = """ - ME ME ME ME Me ME ++ ++ ++ - ME ME ME ++ ME ME ME SP SP - ME ME ME Me ME ME ME ME ME - ME ME ME ME ME ME ME Me ME - ME ME ++ ME Me ME ME ME ME - Me ME ME __ ME ++ ++ ++ ME - ++ MB ME ME ME ++ ME MB ME - ++ MB ME ME ME ME ME ME ME + ME -- ME ME Me -- ++ ++ ++ + ME -- -- ++ ME ME ME SP SP + -- ME ME Me -- -- -- -- -- + -- -- -- -- -- ME ME Me -- + ME -- ++ ME Me -- ME ME -- + Me -- ME __ ME ++ ++ ++ Me + ++ MB -- ME ME ++ Me MB -- + ++ MB ME -- ME Me -- -- ME """ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 @@ -29,7 +31,7 @@ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 """ MAP.spawn_data = [ - {'battle': 0, 'enemy': 3, 'mystery': 1}, + {'battle': 0, 'enemy': 6}, {'battle': 1, 'enemy': 2}, {'battle': 2, 'enemy': 1}, {'battle': 3}, @@ -48,34 +50,53 @@ A8, B8, C8, D8, E8, F8, G8, H8, I8, \ = MAP.flatten() -class Config: +class Config(ConfigBase): # ===== Start of generated config ===== - MAP_SIREN_TEMPLATE = ['0'] - MOVABLE_ENEMY_TURN = (2,) - MAP_HAS_SIREN = False - MAP_HAS_MOVABLE_ENEMY = False + # MAP_SIREN_TEMPLATE = ['0'] + # MOVABLE_ENEMY_TURN = (2,) + # MAP_HAS_SIREN = True + # MAP_HAS_MOVABLE_ENEMY = True MAP_HAS_MAP_STORY = False MAP_HAS_FLEET_STEP = False MAP_HAS_AMBUSH = True - MAP_HAS_MYSTERY = True + # MAP_HAS_MYSTERY = True # ===== End of generated config ===== class Campaign(CampaignBase): MAP = MAP - ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_function(self): + if self.config.MAP_CLEAR_ALL_THIS_TIME \ + and self.battle_count == 0 and not self.map_is_clear_mode: + func = self.FUNCTION_NAME_BASE + str(self.battle_count) + logger.info(f'Using function: {func}') + func = self.__getattribute__(func) + result = func() + return result + + return super().battle_function() def battle_0(self): - if self.clear_siren(): + if not self.map_is_clear_mode and self.map_has_mob_move: + self.mob_move(I6, I7) + self.mob_move(I7, H7) + if G7.is_accessible: + self.clear_chosen_enemy(G7) + return True + + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): return True + + return self.battle_default() + + def battle_1(self): if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): return True return self.battle_default() def battle_5(self): - if self.clear_siren(): - return True if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): return True diff --git a/campaign/campaign_main/campaign_15_3.py b/campaign/campaign_main/campaign_15_3.py index 155e5495e..b5e3e1037 100644 --- a/campaign/campaign_main/campaign_15_3.py +++ b/campaign/campaign_main/campaign_15_3.py @@ -1,22 +1,25 @@ -from module.campaign.campaign_base import CampaignBase +from module.logger import logger from module.map.map_base import CampaignMap from module.map.map_grids import SelectedGrids, RoadGrids -from module.logger import logger +from .campaign_15_base import CampaignBase, W15GridInfo +from .campaign_15_base import Config as ConfigBase MAP = CampaignMap('15-3') +MAP.grid_class = W15GridInfo MAP.shape = 'J8' -MAP.camera_data = ['D2', 'D6', 'G2', 'G6'] +MAP.camera_data = ['C2', 'C6', 'G2', 'G6'] MAP.camera_data_spawn_point = ['G6'] +MAP.camera_sight = (-2, -1, 3, 2) MAP.map_data = """ - ME ME ++ ME ME ME ME ME ME ME - ME ME ++ Me ME Me ME Me ME ME - ME ME ME ME Me ME ++ ME ME ME - ++ ME ME ME ME Me ++ ++ __ ME - Me ME ME ++ Me ME ME ME ME Me - ME ME ME ME ME ME ME ME ME ME - Me ME __ ME ME ME ME ME ++ ++ - ++ ++ ++ Me ME ME SP SP ++ ++ + Me -- ++ ME ME ME -- ME -- ME + -- ME ++ Me -- Me ME Me ME -- + ME Me ME ME Me -- ++ MB -- ME + ++ -- -- ME ME Me ++ ++ __ ME + Me ME -- ++ Me -- ME MS -- Me + ME ME -- -- ME ME ME -- -- ME + Me -- __ -- -- ME -- -- ++ ++ + ++ ++ ++ Me -- -- SP SP ++ ++ """ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 @@ -29,11 +32,11 @@ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 """ MAP.spawn_data = [ - {'battle': 0, 'enemy': 3, 'mystery': 1}, + {'battle': 0, 'enemy': 5}, {'battle': 1, 'enemy': 2}, {'battle': 2, 'enemy': 1}, - {'battle': 3, 'enemy': 1}, - {'battle': 4}, + {'battle': 3, 'enemy': 1, 'siren': 1}, + {'battle': 4, 'enemy': 2}, {'battle': 5}, {'battle': 6, 'boss': 1}, ] @@ -48,34 +51,65 @@ A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, \ = MAP.flatten() -class Config: +class Config(ConfigBase): # ===== Start of generated config ===== - MAP_SIREN_TEMPLATE = ['0'] - MOVABLE_ENEMY_TURN = (2,) - MAP_HAS_SIREN = False - MAP_HAS_MOVABLE_ENEMY = False + # MAP_SIREN_TEMPLATE = ['BOSS'] + # MOVABLE_ENEMY_TURN = (2,) + # MAP_HAS_SIREN = True + # MAP_HAS_MOVABLE_ENEMY = True MAP_HAS_MAP_STORY = False MAP_HAS_FLEET_STEP = False MAP_HAS_AMBUSH = True - MAP_HAS_MYSTERY = True + # MAP_HAS_MYSTERY = True # ===== End of generated config ===== class Campaign(CampaignBase): MAP = MAP - ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_function(self): + if not self.config.MAP_CLEAR_ALL_THIS_TIME: + return super().battle_function() + + if self.battle_count == 3 \ + or (self.battle_count == 0 and not self.map_is_clear_mode): + func = self.FUNCTION_NAME_BASE + str(self.battle_count) + logger.info(f'Using function: {func}') + func = self.__getattribute__(func) + result = func() + return result + + return super().battle_function() def battle_0(self): - if self.clear_siren(): + if not self.map_is_clear_mode and self.map_has_mob_move: + self.mob_move(B3, B4) + if A1.is_accessible: + self.clear_chosen_enemy(A1) + return True + + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): return True + + return self.battle_default() + + def battle_1(self): + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_3(self): + self.clear_chosen_enemy(H5, expected='siren') + return True + + def battle_4(self): if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): return True return self.battle_default() def battle_5(self): - if self.clear_siren(): - return True if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): return True diff --git a/campaign/campaign_main/campaign_15_4.py b/campaign/campaign_main/campaign_15_4.py index 574a06150..ecdcf249f 100644 --- a/campaign/campaign_main/campaign_15_4.py +++ b/campaign/campaign_main/campaign_15_4.py @@ -1,23 +1,26 @@ -from module.campaign.campaign_base import CampaignBase +from module.logger import logger from module.map.map_base import CampaignMap from module.map.map_grids import SelectedGrids, RoadGrids -from module.logger import logger +from .campaign_15_base import CampaignBase, W15GridInfo +from .campaign_15_base import Config as ConfigBase MAP = CampaignMap('15-4') +MAP.grid_class = W15GridInfo MAP.shape = 'K9' -MAP.camera_data = ['D2', 'D6', 'D7', 'H2', 'H6', 'H7'] +MAP.camera_data = ['C2', 'C5', 'C7', 'F2', 'F5', 'F7', 'H2', 'H5', 'H7'] MAP.camera_data_spawn_point = ['H2'] +MAP.camera_sight = (-2, -1, 3, 2) MAP.map_data = """ - ME ME ME ME Me ME ME ++ ++ ME ME - ME ME ME ME ME ME ME ++ ++ ME ME - ++ ME ME ME ME ME ME SP SP ME Me - ++ ME ME ++ ++ ME ME ME ME ME ME - ME Me ME MA ++ ME ME ME ME ME ME - ME ME ME ME ME ME ME ++ ME ME Me - ME ME __ ME ME ME ME ME ME ME ++ - ME ME ++ ME Me ME ME ME ME ME ME - ME Me ME ME ME Me ++ ++ ++ ME ME + Me -- ME ME Me -- ME ++ ++ ME ME + ME -- -- -- -- ME -- ++ ++ -- ME + ++ -- -- MS -- -- ME SP SP ME Me + ++ ME -- ++ ++ -- -- -- -- ME -- + -- Me ME MA ++ ME -- MS -- -- ME + ME ME ME -- -- -- -- ++ ME -- Me + ME -- __ -- ME ME -- ME ME -- ++ + -- -- ++ -- Me -- ME ME ME Me ME + MB Me -- ME ME Me ++ ++ ++ -- Me """ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 50 @@ -31,14 +34,15 @@ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 50 """ MAP.spawn_data = [ - {'battle': 0, 'enemy': 5, 'mystery': 2}, + {'battle': 0, 'enemy': 8}, {'battle': 1, 'enemy': 1}, {'battle': 2, 'enemy': 1}, - {'battle': 3, 'enemy': 1}, - {'battle': 4}, + {'battle': 3, 'enemy': 1, 'siren': 1}, + {'battle': 4, 'enemy': 2}, {'battle': 5}, - {'battle': 6}, - {'battle': 7, 'boss': 1}, + {'battle': 6, 'siren': 1}, + {'battle': 7, 'enemy': 1}, + {'battle': 8, 'boss': 1}, ] A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, \ A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, \ @@ -52,38 +56,96 @@ A9, B9, C9, D9, E9, F9, G9, H9, I9, J9, K9, \ = MAP.flatten() -class Config: +class Config(ConfigBase): # ===== Start of generated config ===== - MAP_SIREN_TEMPLATE = ['0'] - MOVABLE_ENEMY_TURN = (2,) - MAP_HAS_SIREN = False - MAP_HAS_MOVABLE_ENEMY = False + # MAP_SIREN_TEMPLATE = ['BOSS'] + # MOVABLE_ENEMY_TURN = (2,) + # MAP_HAS_SIREN = True + # MAP_HAS_MOVABLE_ENEMY = True MAP_HAS_MAP_STORY = False MAP_HAS_FLEET_STEP = False MAP_HAS_AMBUSH = True - MAP_HAS_MYSTERY = True + # MAP_HAS_MYSTERY = True # ===== End of generated config ===== + MAP_SWIPE_MULTIPLY = (1.055, 1.075) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.020, 1.039) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.990, 1.008) + class Campaign(CampaignBase): MAP = MAP - ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_function(self): + if not self.config.MAP_CLEAR_ALL_THIS_TIME: + return super().battle_function() + + if self.battle_count in [3, 6] \ + or (self.battle_count in [0, 1] and not self.map_is_clear_mode): + func = self.FUNCTION_NAME_BASE + str(self.battle_count) + logger.info(f'Using function: {func}') + func = self.__getattribute__(func) + result = func() + return result + + return super().battle_function() def battle_0(self): - if self.clear_siren(): - return True - if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=2): - return True + if not self.map_is_clear_mode and self.map_has_mob_move: + self.mob_move(J8, K8) + if K9.is_accessible: + self.clear_chosen_enemy(K9) + return True - return self.battle_default() - - def battle_5(self): - if self.clear_siren(): - return True if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): return True return self.battle_default() + def battle_1(self): + if not self.map_is_clear_mode: + if A1.is_accessible: + self.clear_chosen_enemy(A1) + return True + + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_2(self): + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_3(self): + if not self.map_is_clear_mode: + self.fleet_boss.clear_chosen_enemy(H5, expected='siren') + self.fleet_1.switch_to() + return True + else: + self.pick_up_ammo() + self.clear_chosen_enemy(H5, expected='siren') + return True + + def battle_4(self): + self.pick_up_ammo() + + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + self.clear_chosen_enemy(D3, expected='siren') + return True + def battle_7(self): + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_8(self): return self.fleet_boss.clear_boss() diff --git a/campaign/campaign_main/campaign_15_base.py b/campaign/campaign_main/campaign_15_base.py new file mode 100644 index 000000000..ce400f6da --- /dev/null +++ b/campaign/campaign_main/campaign_15_base.py @@ -0,0 +1,218 @@ +from module.base.mask import Mask +from module.base.timer import Timer +from module.campaign.campaign_base import CampaignBase as CampaignBase_ +from module.handler.assets import STRATEGY_OPENED +from module.logger import logger +from module.map.map_grids import SelectedGrids +from module.map.utils import location_ensure +from module.map_detection.grid import GridInfo +from module.map_detection.utils_assets import ASSETS + +MASK_MAP_UI_W15 = Mask(file='./assets/mask/MASK_MAP_UI_W15.png') + + +class Config: + # Ambushes can be avoid by having more DDs. + MAP_WALK_OPTIMIZE = False + MAP_HAS_MYSTERY = False + MAP_ENEMY_TEMPLATE = ['Light', 'Main', 'Carrier', 'CarrierSpecial'] + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (80, 255 - 33), + 'width': (0.9, 10), + 'prominence': 10, + 'distance': 35, + } + MAP_SWIPE_MULTIPLY = (0.993, 1.011) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.960, 0.978) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.932, 0.949) + + +class W15GridInfo(GridInfo): + def merge(self, info, mode='normal'): + # Consider boss as siren + if info.is_boss: + if not self.is_land and self.may_siren: + self.is_siren = True + self.enemy_scale = 0 + self.enemy_genre = '' + return True + + return super().merge(info, mode=mode) + + +class CampaignBase(CampaignBase_): + ENEMY_FILTER = '1T > 1L > 1E > 1M > 2T > 2L > 2E > 2M > 3T > 3L > 3E > 3M' + + def map_data_init(self, map_): + super().map_data_init(map_) + # Patch ui_mask, get rid of supporting fleet + _ = ASSETS.ui_mask + ASSETS.ui_mask = MASK_MAP_UI_W15.image + + map_has_mob_move = True + + def strategy_set_execute(self, formation_index=None, sub_view=None, sub_hunt=None): + super().strategy_set_execute( + formation_index=formation_index, + sub_view=sub_view, + sub_hunt=sub_hunt, + ) + self.map_has_mob_move = (self.strategy_get_mob_move_remain() > 0) + logger.attr("Map has mob move", self.map_has_mob_move) + + def _map_swipe(self, vector, box=(239, 159, 1175, 628)): + # Left border to 239, avoid swiping on support fleet + return super()._map_swipe(vector, box=box) + + def mob_movable(self, location, target): + """ + Check if mob is movable from location to target. + This requires that: + 1. both location and target are grids in the map (not exceeding the boundaries) + 2. Manhattan distance between location and target is 1. + 3. location is a mob fleet + 4. target is a sea grid + + Args: + location (tuple): Location of mob. + target (tuple): Destination. + + Returns: + bool: if movable. + """ + location = location_ensure(location) + target = location_ensure(target) + movable = True + + try: + logger.info(f'location: {self.map[location]}, target: {self.map[target]}') + except KeyError as e: + logger.exception(f'Given coordinates are outside the map.') + raise e + + if abs(location[0] - target[0]) + abs(location[1] - target[1]) != 1: + logger.error(f'{self.map[target]} is not adjacent from {self.map[location]}.') + movable = False + + if not self.map[location].is_enemy: + logger.error(f'{self.map[location]} is not a mob fleet.') + movable = False + + if not self.map[target].is_sea: + logger.error(f'{self.map[target]} is not a sea grid.') + movable = False + + if not movable: + logger.error(f'Cannot move from {self.map[location]} to {self.map[target]}.') + + return movable + + def _mob_move(self, location, target): + """ + Move mob from location to target, and confirm if successfully moved. + + Args: + location (tuple, str, GridInfo): Location of mob. + target (tuple, str, GridInfo): Destination. + + Returns: + bool: If mob moved. + + Pages: + in: MOB_MOVE_CANCEL + out: STRATEGY_OPENED + """ + location = location_ensure(location) + target = location_ensure(target) + + view_target = SelectedGrids([self.map[location], self.map[target]]) \ + .sort_by_camera_distance(self.camera)[1] + self.in_sight(view_target) + origin_grid = self.convert_global_to_local(location) + origin_grid.__str__ = location + target_grid = self.convert_global_to_local(target) + target_grid.__str__ = target + + logger.info('Select mob to move') + skip_first_screenshot = True + interval = Timer(2, count=4) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.is_in_strategy_mob_move(): + self.view.update(image=self.device.image) + if origin_grid.predict_mob_move_icon(): + break + # Click + if interval.reached() and self.is_in_strategy_mob_move(): + self.device.click(origin_grid) + interval.reset() + continue + + logger.info('Select target grid') + skip_first_screenshot = True + interval = Timer(2, count=4) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.appear(STRATEGY_OPENED, offset=(120, 120)): + break + # Click + if interval.reached() and self.is_in_strategy_mob_move(): + self.device.click(target_grid) + interval.reset() + continue + if self.handle_popup_confirm('MOB_MOVE'): + continue + + def _mob_move_info_change(self, location, target): + location = location_ensure(location) + target = location_ensure(target) + self.map[target].enemy_scale = self.map[location].enemy_scale + self.map[location].enemy_scale = 0 + self.map[target].enemy_genre = self.map[location].enemy_genre + self.map[location].enemy_genre = None + self.map[target].is_boss = self.map[location].is_boss + self.map[location].is_boss = False + self.map[target].is_enemy = True + self.map[location].is_enemy = False + + def mob_move(self, location, target): + """ + Open strategy, move mob fleet from location to target, close strategy. + + Args: + location (tuple, str, GridInfo): Location of mob. + target (tuple, str, GridInfo): Destination. + + Returns: + bool: If mob moved + + Pages: + in: IN_MAP + out: IN_MAP + """ + if not self.mob_movable(location, target): + return False + + self.strategy_open() + remain = self.strategy_get_mob_move_remain() + if remain == 0: + logger.warning(f'No remain mob move trials, will abandon moving') + self.strategy_close() + return False + self.strategy_mob_move_enter() + self._mob_move(location, target) + self.strategy_close(skip_first_screenshot=False) + + self._mob_move_info_change(location, target) + self.find_path_initial() + self.map.show() diff --git a/campaign/event_20220224_cn/b1.py b/campaign/event_20220224_cn/b1.py index 0431dab06..4614e27aa 100644 --- a/campaign/event_20220224_cn/b1.py +++ b/campaign/event_20220224_cn/b1.py @@ -55,20 +55,6 @@ class Config: MAP_HAS_MYSTERY = False # ===== End of generated config ===== - INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { - 'height': (150, 255 - 17), - 'width': (0.9, 10), - 'prominence': 10, - 'distance': 35, - } - EDGE_LINES_FIND_PEAKS_PARAMETERS = { - 'height': (255 - 17, 255), - 'prominence': 10, - 'distance': 50, - # 'width': (0, 7), - 'wlen': 1000 - } - HOMO_EDGE_COLOR_RANGE = (0, 17) MAP_ENEMY_GENRE_DETECTION_SCALING = { 'DD': 1.111, 'CL': 1.111, @@ -76,6 +62,7 @@ class Config: 'BBred': 1.111, 'CV': 1.111, } + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom' MAP_SWIPE_MULTIPLY = (0.974, 0.992) MAP_SWIPE_MULTIPLY_MINITOUCH = (0.942, 0.959) MAP_SWIPE_MULTIPLY_MAATOUCH = (0.914, 0.931) diff --git a/campaign/event_20220224_cn/d1.py b/campaign/event_20220224_cn/d1.py index 4ec7eeb1f..12bc601c6 100644 --- a/campaign/event_20220224_cn/d1.py +++ b/campaign/event_20220224_cn/d1.py @@ -76,6 +76,7 @@ class Config: 'BBred': 1.111, 'CV': 1.111, } + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom' MAP_SWIPE_MULTIPLY = (0.974, 0.992) MAP_SWIPE_MULTIPLY_MINITOUCH = (0.942, 0.959) MAP_SWIPE_MULTIPLY_MAATOUCH = (0.914, 0.931) diff --git a/campaign/event_20240425_cn/campaign_base.py b/campaign/event_20240425_cn/campaign_base.py new file mode 100644 index 000000000..03ca49532 --- /dev/null +++ b/campaign/event_20240425_cn/campaign_base.py @@ -0,0 +1,56 @@ +from module.campaign.campaign_base import CampaignBase as CampaignBase_ +from module.logger import logger + + +class CampaignBase(CampaignBase_): + STAGE_INCREASE = [ + """ + SP1 > SP2 > SP3 > SP4 > SP5 + """, + """ + ISP1 > ISP2 > ISP3 > ISP4 > ISP5 > ISP6 + """, + ] + + def campaign_set_chapter_event(self, chapter, mode='normal'): + self.ui_goto_event() + self.campaign_ensure_chapter(index=chapter) + return True + + def _campaign_get_chapter_index(self, name): + """ + Args: + name (str, int): + + Returns: + int + """ + if name == 'sp': + return 1 + if name == 'isp': + return 2 + if name == 'ex_sp': + return 3 + if name == 'ex_ex': + return 4 + + return super(CampaignBase, CampaignBase)._campaign_get_chapter_index(name) + + @staticmethod + def _campaign_ocr_result_process(result): + result = CampaignBase_._campaign_ocr_result_process(result) + if result in ['usp', 'iisp']: + result = 'sp' + return result + + def is_event_animation(self): + # Blue banner + if self.image_color_count((1180, 285, 1280, 335), color=(140, 215, 255), count=1000): + logger.info('Live start!') + return True + # Red-black banner with white bottom border + if self.image_color_count((1193, 428, 1273, 436), color=(255, 255, 255), count=500): + logger.info('Live start!') + return True + + return False diff --git a/campaign/event_20240425_cn/isp1.py b/campaign/event_20240425_cn/isp1.py new file mode 100644 index 000000000..a1155dc66 --- /dev/null +++ b/campaign/event_20240425_cn/isp1.py @@ -0,0 +1,56 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('ISP1') +MAP.shape = 'E5' +MAP.camera_data = ['C2'] +MAP.camera_data_spawn_point = ['C2'] +MAP.map_data = """ + -- ++ ++ ++ -- + -- ++ MB ++ -- + -- ++ -- ++ -- + ++ -- SP -- ++ + -- ++ ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'boss': 1}, +] +A1, B1, C1, D1, E1, \ +A2, B2, C2, D2, E2, \ +A3, B3, C3, D3, E3, \ +A4, B4, C4, D4, E4, \ +A5, B5, C5, D5, E5, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + MAP_HAS_CLEAR_PERCENTAGE = False + FLEET_2 = 0 + MAP_IS_ONE_TIME_STAGE = True + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/isp2.py b/campaign/event_20240425_cn/isp2.py new file mode 100644 index 000000000..467df2a85 --- /dev/null +++ b/campaign/event_20240425_cn/isp2.py @@ -0,0 +1,53 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .isp1 import Config as ConfigBase + +MAP = CampaignMap('ISP2') +MAP.shape = 'E5' +MAP.camera_data = ['C2'] +MAP.camera_data_spawn_point = ['C2'] +MAP.map_data = """ + -- ++ ++ ++ -- + -- ++ MB ++ -- + -- ++ -- ++ -- + ++ -- SP -- ++ + -- ++ ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'boss': 1}, +] +A1, B1, C1, D1, E1, \ +A2, B2, C2, D2, E2, \ +A3, B3, C3, D3, E3, \ +A4, B4, C4, D4, E4, \ +A5, B5, C5, D5, E5, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/isp3.py b/campaign/event_20240425_cn/isp3.py new file mode 100644 index 000000000..7afccbdee --- /dev/null +++ b/campaign/event_20240425_cn/isp3.py @@ -0,0 +1,53 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .isp1 import Config as ConfigBase + +MAP = CampaignMap('ISP3') +MAP.shape = 'E5' +MAP.camera_data = ['C2'] +MAP.camera_data_spawn_point = ['C2'] +MAP.map_data = """ + -- ++ ++ ++ -- + -- ++ MB ++ -- + -- ++ -- ++ -- + ++ -- SP -- ++ + -- ++ ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'boss': 1}, +] +A1, B1, C1, D1, E1, \ +A2, B2, C2, D2, E2, \ +A3, B3, C3, D3, E3, \ +A4, B4, C4, D4, E4, \ +A5, B5, C5, D5, E5, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/isp4.py b/campaign/event_20240425_cn/isp4.py new file mode 100644 index 000000000..eaa0a32d6 --- /dev/null +++ b/campaign/event_20240425_cn/isp4.py @@ -0,0 +1,53 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .isp1 import Config as ConfigBase + +MAP = CampaignMap('ISP4') +MAP.shape = 'E5' +MAP.camera_data = ['C2'] +MAP.camera_data_spawn_point = ['C2'] +MAP.map_data = """ + -- ++ ++ ++ -- + -- ++ MB ++ -- + -- ++ -- ++ -- + ++ -- SP -- ++ + -- ++ ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'boss': 1}, +] +A1, B1, C1, D1, E1, \ +A2, B2, C2, D2, E2, \ +A3, B3, C3, D3, E3, \ +A4, B4, C4, D4, E4, \ +A5, B5, C5, D5, E5, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/isp5.py b/campaign/event_20240425_cn/isp5.py new file mode 100644 index 000000000..89ed824a0 --- /dev/null +++ b/campaign/event_20240425_cn/isp5.py @@ -0,0 +1,53 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .isp1 import Config as ConfigBase + +MAP = CampaignMap('ISP5') +MAP.shape = 'E5' +MAP.camera_data = ['C2'] +MAP.camera_data_spawn_point = ['C2'] +MAP.map_data = """ + -- ++ ++ ++ -- + -- ++ MB ++ -- + -- ++ -- ++ -- + ++ -- SP -- ++ + -- ++ ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'boss': 1}, +] +A1, B1, C1, D1, E1, \ +A2, B2, C2, D2, E2, \ +A3, B3, C3, D3, E3, \ +A4, B4, C4, D4, E4, \ +A5, B5, C5, D5, E5, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/isp6.py b/campaign/event_20240425_cn/isp6.py new file mode 100644 index 000000000..298ede101 --- /dev/null +++ b/campaign/event_20240425_cn/isp6.py @@ -0,0 +1,53 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .isp1 import Config as ConfigBase + +MAP = CampaignMap('ISP6') +MAP.shape = 'E5' +MAP.camera_data = ['C2'] +MAP.camera_data_spawn_point = ['C2'] +MAP.map_data = """ + -- ++ ++ ++ -- + -- ++ MB ++ -- + -- ++ -- ++ -- + ++ -- SP -- ++ + -- ++ ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 + 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'boss': 1}, +] +A1, B1, C1, D1, E1, \ +A2, B2, C2, D2, E2, \ +A3, B3, C3, D3, E3, \ +A4, B4, C4, D4, E4, \ +A5, B5, C5, D5, E5, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/sp.py b/campaign/event_20240425_cn/sp.py new file mode 100644 index 000000000..8777c132b --- /dev/null +++ b/campaign/event_20240425_cn/sp.py @@ -0,0 +1,114 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('SP') +MAP.shape = 'I9' +MAP.camera_data = ['C5', 'F5'] +MAP.camera_data_spawn_point = ['E7'] +MAP.camera_sight = (-2, -1, 3, 2) +MAP.map_data = """ + -- ++ ++ -- -- -- ++ ++ -- + -- ++ ++ -- MB -- ++ ++ -- + ++ ++ ++ ++ -- ++ ++ ++ ++ + -- ME ++ -- -- -- ++ ME -- + ME -- -- -- __ -- -- -- ME + -- ME ++ -- -- -- ++ ME -- + ++ ++ ++ ++ -- ++ ++ ++ ++ + -- ++ ++ -- -- -- ++ ++ -- + -- ++ ++ SP -- SP ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 6, 'siren': 4}, + {'battle': 1}, + {'battle': 2}, + {'battle': 3}, + {'battle': 4}, + {'battle': 5}, + {'battle': 6}, + {'battle': 7, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ +A9, B9, C9, D9, E9, F9, G9, H9, I9, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + MAP_HAS_SIREN = True + MAP_HAS_CLEAR_PERCENTAGE = False + MAP_IS_ONE_TIME_STAGE = True + HOMO_STORAGE = ((9, 6), [(165.083, 80.309), (1168.09, 80.309), (21.023, 612.379), (1325.484, 612.379)]) + + MAP_SWIPE_MULTIPLY = (1.009, 1.028) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.976, 0.994) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.948, 0.965) + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom' + MAP_WALK_USE_CURRENT_FLEET = True + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def map_data_init(self, map_): + super().map_data_init(map_) + D4.is_siren = True + D6.is_siren = True + F4.is_siren = True + F6.is_siren = True + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=2): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_7(self): + return self.fleet_boss.clear_boss() + + def is_event_animation(self): + # Red-black banner with white bottom border + if self.image_color_count((1193, 322, 1273, 329), color=(255, 255, 255), count=500): + logger.info('Live start!') + return True + + return False \ No newline at end of file diff --git a/campaign/event_20240425_cn/sp1.py b/campaign/event_20240425_cn/sp1.py new file mode 100644 index 000000000..582355652 --- /dev/null +++ b/campaign/event_20240425_cn/sp1.py @@ -0,0 +1,87 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('SP1') +MAP.shape = 'I8' +MAP.camera_data = ['C3', 'C6', 'E3', 'E6'] +MAP.camera_data_spawn_point = ['E3'] +MAP.camera_sight = (-2, -1, 3, 2) +MAP.map_data = """ + -- -- -- -- -- -- -- -- -- + -- -- ME ++ ++ ++ -- ++ -- + ++ ME -- ++ MB ++ ME -- ME + ++ -- -- ++ -- ++ -- -- -- + -- Me -- SP -- SP -- Me -- + ME -- -- -- __ -- -- -- ME + -- ++ ME -- MS -- ME ++ ++ + -- -- -- Me ++ Me -- -- ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Laffey6'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (80, 255 - 33), + 'width': (0.9, 10), + 'prominence': 10, + 'distance': 35, + } + MAP_SWIPE_MULTIPLY = (1.062, 1.082) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.027, 1.046) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.997, 1.015) + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom' + MAP_WALK_USE_CURRENT_FLEET = True + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/sp2.py b/campaign/event_20240425_cn/sp2.py new file mode 100644 index 000000000..33db6a645 --- /dev/null +++ b/campaign/event_20240425_cn/sp2.py @@ -0,0 +1,78 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .sp1 import Config as ConfigBase + +MAP = CampaignMap('SP2') +MAP.shape = 'I7' +MAP.camera_data = ['D2', 'D5', 'F5'] +MAP.camera_data_spawn_point = ['F2'] +MAP.camera_sight = (-2, -1, 3, 2) +MAP.map_data = """ + -- -- ME -- -- MS -- -- SP + -- ++ ++ Me __ Me MS -- -- + -- ++ ++ Me -- MS -- -- SP + -- -- ME -- -- -- ++ ++ ++ + -- ME -- -- ++ Me ++ MB ++ + ++ ++ ++ -- ME -- ++ -- ++ + -- -- ME -- -- -- -- -- ME +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 1}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['TashkentIdol', 'DidoIdol2'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + HOMO_STORAGE = ((7, 4), [(179.668, 262.579), (1042.431, 262.579), (89.435, 660.188), (1122.585, 660.188)]) + MAP_SWIPE_MULTIPLY = (1.246, 1.269) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.205, 1.227) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.170, 1.191) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/campaign/event_20240425_cn/sp3.py b/campaign/event_20240425_cn/sp3.py new file mode 100644 index 000000000..46e4a9a39 --- /dev/null +++ b/campaign/event_20240425_cn/sp3.py @@ -0,0 +1,81 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .sp1 import Config as ConfigBase + +MAP = CampaignMap('SP3') +MAP.shape = 'I8' +MAP.camera_data = ['C4', 'E4', 'E6'] +MAP.camera_data_spawn_point = ['C6'] +MAP.camera_sight = (-2, -1, 3, 2) +MAP.map_data = """ + ++ ++ ++ ++ -- ++ ++ ++ -- + -- ++ MB ++ ME -- ME -- -- + ME ++ -- ++ -- Me -- ME -- + -- Me -- Me -- ++ -- ME ++ + -- MS __ MS -- Me -- -- ++ + -- -- MS -- -- -- -- ME -- + ++ -- -- -- ++ ++ ME ME -- + ++ SP -- SP ++ ++ -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 2}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4}, + {'battle': 5, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['SheffieldIdol', 'RoonIdol2'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.069, 1.089) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.034, 1.053) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.004, 1.022) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/campaign/event_20240425_cn/sp4.py b/campaign/event_20240425_cn/sp4.py new file mode 100644 index 000000000..7b522cd11 --- /dev/null +++ b/campaign/event_20240425_cn/sp4.py @@ -0,0 +1,81 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .sp1 import Config as ConfigBase + +MAP = CampaignMap('SP4') +MAP.shape = 'I8' +MAP.camera_data = ['C3', 'C6', 'F3', 'F6'] +MAP.camera_data_spawn_point = ['D2'] +MAP.camera_sight = (-2, -1, 3, 2) +MAP.map_data = """ + ++ ++ ++ SP -- SP ++ ++ ++ + -- Me -- -- -- -- -- Me -- + ME -- MS -- MS -- MS -- ME + -- ++ -- Me __ Me -- ++ -- + -- Me -- ++ ++ ++ -- Me -- + ME -- -- ++ MB ++ -- -- ME + ++ ME -- ++ -- ++ -- ME ++ + ++ ME -- -- -- -- -- ME ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 2}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Z23_5', 'Elizabeth3'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.251, 1.275) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.210, 1.232) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.175, 1.196) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/campaign/event_20240425_cn/sp5.py b/campaign/event_20240425_cn/sp5.py new file mode 100644 index 000000000..42867b5db --- /dev/null +++ b/campaign/event_20240425_cn/sp5.py @@ -0,0 +1,93 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .sp1 import Config as ConfigBase + +MAP = CampaignMap('SP5') +MAP.shape = 'I9' +MAP.camera_data = ['C4', 'C7', 'F4', 'F7'] +MAP.camera_data_spawn_point = ['C7'] +MAP.camera_sight = (-2, -1, 3, 2) +MAP.map_data = """ + ++ ++ -- ++ ++ ++ -- ++ ++ + ++ ++ ME -- Me -- ME -- ++ + -- -- -- ME -- ME -- -- -- + -- ME -- ++ ++ ++ -- ME -- + ME ++ -- ++ MB ++ -- ++ ME + -- ++ -- ++ -- ++ -- ++ -- + -- -- MS -- MS -- MS -- -- + Me -- -- -- __ -- -- -- Me + -- Me ++ SP -- SP ++ Me -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 2}, + {'battle': 1, 'enemy': 2, 'siren': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5}, + {'battle': 6, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ +A9, B9, C9, D9, E9, F9, G9, H9, I9, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['GascogneIdol', 'TaihouIdol'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.012, 1.031) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.979, 0.997) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.950, 0.968) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_boss.clear_boss() diff --git a/deploy/AidLux/0.92/requirements.txt b/deploy/AidLux/0.92/requirements.txt index 78ea35953..6bdd5e1ab 100644 --- a/deploy/AidLux/0.92/requirements.txt +++ b/deploy/AidLux/0.92/requirements.txt @@ -13,6 +13,7 @@ onepush pillow prettytable==2.2.1 psutil==5.9.3 +pycryptodome==3.10.4 pydantic pypresence==4.2.1 pywebio==1.6.2 diff --git a/deploy/AidLux/requirements_generator.py b/deploy/AidLux/requirements_generator.py index a5bbda2f5..dc9eee768 100644 --- a/deploy/AidLux/requirements_generator.py +++ b/deploy/AidLux/requirements_generator.py @@ -45,6 +45,7 @@ def write_file(file, data): def aidlux_requirements_generate(requirements_in='requirements-in.txt'): logger.info('aidlux_requirements_generate') requirements = read_file(requirements_in) + requirements = dict(sorted(requirements.items())) for aidlux in iter_version(): logger.info(f'Generate requirements for AidLux {aidlux}') pre_installed = read_file(os.path.join(BASE_FOLDER, f'./{aidlux}/pre-installed.txt')) diff --git a/deploy/Windows/config.py b/deploy/Windows/config.py index 48b32e290..90ca2ae6c 100644 --- a/deploy/Windows/config.py +++ b/deploy/Windows/config.py @@ -1,6 +1,7 @@ import copy import os import subprocess +import sys from typing import Optional, Union from deploy.Windows.logger import logger @@ -80,12 +81,6 @@ class DeployConfig(ConfigModel): self.config_template = {} self.read() - # Bypass webui.config.DeployConfig.__setattr__() - # Don't write these into deploy.yaml - super().__setattr__('GitOverCdn', self.Repository in ['cn']) - if self.Repository in ['global', 'cn']: - super().__setattr__('Repository', 'https://github.com/LmeSzinc/StarRailCopilot') - self.write() self.show_config() @@ -109,9 +104,21 @@ class DeployConfig(ConfigModel): if hasattr(self, key): super().__setattr__(key, value) + self.config_redirect() + def write(self): poor_yaml_write(self.config, self.file) + def config_redirect(self): + """ + Redirect deploy config, must be called after each `read()` + """ + # Bypass webui.config.DeployConfig.__setattr__() + # Don't write these into deploy.yaml + super().__setattr__('GitOverCdn', self.Repository in ['cn']) + if self.Repository in ['global', 'cn']: + super().__setattr__('Repository', 'https://github.com/LmeSzinc/StarRailCopilot') + def filepath(self, path): """ Args: @@ -143,7 +150,7 @@ class DeployConfig(ConfigModel): if os.path.exists(exe): return exe - logger.warning(f'AdbExecutable: {exe} does not exists, use `adb` instead') + logger.warning(f'AdbExecutable: {exe} does not exist, use `adb` instead') return 'adb' @cached_property @@ -152,12 +159,18 @@ class DeployConfig(ConfigModel): if os.path.exists(exe): return exe - logger.warning(f'GitExecutable: {exe} does not exists, use `git` instead') + logger.warning(f'GitExecutable: {exe} does not exist, use `git` instead') return 'git' @cached_property def python(self) -> str: - return self.filepath(self.PythonExecutable) + exe = self.filepath(self.PythonExecutable) + if os.path.exists(exe): + return exe + + current = sys.executable.replace("\\", "/") + logger.warning(f'PythonExecutable: {exe} does not exist, use current python instead: {current}') + return current @cached_property def requirements_file(self) -> str: diff --git a/deploy/config.py b/deploy/config.py new file mode 100644 index 000000000..528a16078 --- /dev/null +++ b/deploy/config.py @@ -0,0 +1,191 @@ +import copy +from typing import Optional, Union + +from deploy.logger import logger +from deploy.utils import * + + +class ExecutionError(Exception): + pass + + +class ConfigModel: + # Git + Repository: str = "https://github.com/LmeSzinc/AzurLaneAutoScript" + Branch: str = "master" + GitExecutable: str = "./toolkit/Git/mingw64/bin/git.exe" + GitProxy: Optional[str] = None + SSLVerify: bool = False + AutoUpdate: bool = True + KeepLocalChanges: bool = False + + # Python + PythonExecutable: str = "./toolkit/python.exe" + PypiMirror: Optional[str] = None + InstallDependencies: bool = True + RequirementsFile: str = "requirements.txt" + + # Adb + AdbExecutable: str = "./toolkit/Lib/site-packages/adbutils/binaries/adb.exe" + ReplaceAdb: bool = True + AutoConnect: bool = True + InstallUiautomator2: bool = True + + # Ocr + UseOcrServer: bool = False + StartOcrServer: bool = False + OcrServerPort: int = 22268 + OcrClientAddress: str = "127.0.0.1:22268" + + # Update + EnableReload: bool = True + CheckUpdateInterval: int = 5 + AutoRestartTime: str = "03:50" + + # Misc + DiscordRichPresence: bool = False + + # Remote Access + EnableRemoteAccess: bool = False + SSHUser: Optional[str] = None + SSHServer: Optional[str] = None + SSHExecutable: Optional[str] = None + + # Webui + WebuiHost: str = "0.0.0.0" + WebuiPort: int = 22267 + Language: str = "en-US" + Theme: str = "default" + DpiScaling: bool = True + Password: Optional[str] = None + CDN: Union[str, bool] = False + Run: Optional[str] = None + + # Dynamic + GitOverCdn: bool = False + + +class DeployConfig(ConfigModel): + def __init__(self, file=DEPLOY_CONFIG): + """ + Args: + file (str): User deploy config. + """ + self.file = file + self.config = {} + self.read() + + self.write() + self.show_config() + + def show_config(self): + logger.hr("Show deploy config", 1) + for k, v in self.config.items(): + if k in ("Password", "SSHUser"): + continue + if self.config_template.get(k) == v: + continue + logger.info(f"{k}: {v}") + + logger.info(f"Rest of the configs are the same as default") + + def read(self): + self.config = poor_yaml_read(DEPLOY_TEMPLATE) + self.config_template = copy.deepcopy(self.config) + self.config.update(poor_yaml_read(self.file)) + + for key, value in self.config.items(): + if hasattr(self, key): + super().__setattr__(key, value) + + self.config_redirect() + + def write(self): + poor_yaml_write(self.config, self.file) + + def config_redirect(self): + """ + Redirect deploy config, must be called after each `read()` + """ + if self.Repository in [ + 'https://gitee.com/LmeSzinc/AzurLaneAutoScript', + 'https://gitee.com/lmeszinc/azur-lane-auto-script-mirror', + 'https://e.coding.net/llop18870/alas/AzurLaneAutoScript.git', + 'https://e.coding.net/saarcenter/alas/AzurLaneAutoScript.git', + 'https://git.saarcenter.com/LmeSzinc/AzurLaneAutoScript.git', + ]: + self.Repository = 'git://git.lyoko.io/AzurLaneAutoScript' + + # Bypass webui.config.DeployConfig.__setattr__() + # Don't write these into deploy.yaml + super().__setattr__( + 'GitOverCdn', + self.Repository == 'git://git.lyoko.io/AzurLaneAutoScript' and self.Branch == 'master' + ) + if self.Repository in ['global']: + super().__setattr__('Repository', 'https://github.com/LmeSzinc/AzurLaneAutoScript') + if self.Repository in ['cn']: + super().__setattr__('Repository', 'git://git.lyoko.io/AzurLaneAutoScript') + + def filepath(self, key): + """ + Args: + key (str): + + Returns: + str: Absolute filepath. + """ + return ( + os.path.abspath(os.path.join(self.root_filepath, self.config[key])) + .replace(r"\\", "/") + .replace("\\", "/") + .replace('"', '"') + ) + + @cached_property + def root_filepath(self): + return ( + os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) + .replace(r"\\", "/") + .replace("\\", "/") + .replace('"', '"') + ) + + def execute(self, command, allow_failure=False, output=True): + """ + Args: + command (str): + allow_failure (bool): + output(bool): + + Returns: + bool: If success. + Terminate installation if failed to execute and not allow_failure. + """ + command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + if not output: + command = command + ' >nul 2>nul' + logger.info(command) + error_code = os.system(command) + if error_code: + if allow_failure: + logger.info(f"[ allowed failure ], error_code: {error_code}") + return False + else: + logger.info(f"[ failure ], error_code: {error_code}") + self.show_error(command) + raise ExecutionError + else: + logger.info(f"[ success ]") + return True + + def show_error(self, command=None): + logger.hr("Update failed", 0) + self.show_config() + logger.info("") + logger.info(f"Last command: {command}") + logger.info( + "Please check your deploy settings in config/deploy.yaml " + "and re-open Alas.exe" + ) + logger.info("Take the screenshot of entire window if you need help") diff --git a/deploy/docker/requirements.txt b/deploy/docker/requirements.txt index eaa999932..27ad15099 100644 --- a/deploy/docker/requirements.txt +++ b/deploy/docker/requirements.txt @@ -1,31 +1,43 @@ -adbutils==0.11.0 -aiofiles -anyio==1.3.1 -av==10.0.0 -cnocr==1.2.2 -imageio==2.27.0 -inflection -jellyfish==0.11.2 -lz4 -mxnet==1.6.0 +# Image processing numpy==1.16.6 -onepush -opencv-python-headless -pillow -prettytable==2.2.1 -psutil==5.9.3 -pydantic -pypresence==4.2.1 -pywebio==1.6.2 -pyyaml -pyzmq==22.3.0 -retrying -rich==11.2.0 scipy==1.4.1 -starlette==0.14.2 -tqdm +pillow +opencv-python-headless +imageio==2.27.0 + +# Device connection +adbutils==0.11.0 uiautomator2==2.16.17 uiautomator2cache==0.3.0.1 -uvicorn[standard]==0.17.6 wrapt==1.13.1 -zerorpc==0.6.3 \ No newline at end of file +retrying +lz4 +av==10.0.0 +psutil==5.9.3 + +# Utils +rich==11.2.0 +tqdm +jellyfish==0.11.2 +pyyaml +inflection +pydantic +aiofiles +prettytable==2.2.1 +anyio==1.3.1 + +# Pushing +onepush==1.3.0 +pycryptodome==3.9.9 +pypresence==4.2.1 + +# Ocr +cnocr==1.2.2 +mxnet==1.6.0 + +# Webui +pywebio==1.6.2 +starlette==0.14.2 +uvicorn[standard]==0.17.6 +zerorpc==0.6.3 +pyzmq==22.3.0 \ No newline at end of file diff --git a/deploy/docker/requirements_generator.py b/deploy/docker/requirements_generator.py index 75290c7f4..9f5b4d17d 100644 --- a/deploy/docker/requirements_generator.py +++ b/deploy/docker/requirements_generator.py @@ -10,6 +10,8 @@ def read_file(file): out = {} with open(file, 'r', encoding='utf-8') as f: for line in f.readlines(): + if not line.strip(): + continue res = [s.strip() for s in line.split('==')] if len(res) > 1: name, version = res @@ -29,7 +31,9 @@ def write_file(file, data): lines.append(str(name)) with open(file, 'w', encoding='utf-8', newline='') as f: - f.write('\n'.join(lines)) + text = '\n'.join(lines) + text = text.replace('#', '\n#').strip() + f.write(text) def docker_requirements_generate(requirements_in='requirements-in.txt'): diff --git a/deploy/git.py b/deploy/git.py new file mode 100644 index 000000000..83572e769 --- /dev/null +++ b/deploy/git.py @@ -0,0 +1,115 @@ +from deploy.config import DeployConfig +from deploy.git_over_cdn.client import GitOverCdnClient +from deploy.logger import logger +from deploy.utils import * + + +class GitManager(DeployConfig): + @cached_property + def git(self): + return self.filepath('GitExecutable') + + @staticmethod + def remove(file): + try: + os.remove(file) + logger.info(f'Removed file: {file}') + except FileNotFoundError: + logger.info(f'File not found: {file}') + + def git_repository_init( + self, repo, source='origin', branch='master', + proxy='', ssl_verify=True, keep_changes=False + ): + logger.hr('Git Init', 1) + if not self.execute(f'"{self.git}" init', allow_failure=True): + self.remove('./.git/config') + self.remove('./.git/index') + self.remove('./.git/HEAD') + self.execute(f'"{self.git}" init') + + logger.hr('Set Git Proxy', 1) + if proxy: + self.execute(f'"{self.git}" config --local http.proxy {proxy}') + self.execute(f'"{self.git}" config --local https.proxy {proxy}') + else: + self.execute(f'"{self.git}" config --local --unset http.proxy', allow_failure=True) + self.execute(f'"{self.git}" config --local --unset https.proxy', allow_failure=True) + + if ssl_verify: + self.execute(f'"{self.git}" config --local http.sslVerify true', allow_failure=True) + else: + self.execute(f'"{self.git}" config --local http.sslVerify false', allow_failure=True) + + logger.hr('Set Git Repository', 1) + if not self.execute(f'"{self.git}" remote set-url {source} {repo}', allow_failure=True): + self.execute(f'"{self.git}" remote add {source} {repo}') + + logger.hr('Fetch Repository Branch', 1) + self.execute(f'"{self.git}" fetch {source} {branch}') + + logger.hr('Pull Repository Branch', 1) + # Remove git lock + for lock_file in [ + './.git/index.lock', + './.git/HEAD.lock', + './.git/refs/heads/master.lock', + ]: + if os.path.exists(lock_file): + logger.info(f'Lock file {lock_file} exists, removing') + os.remove(lock_file) + if keep_changes: + if self.execute(f'"{self.git}" stash', allow_failure=True): + self.execute(f'"{self.git}" pull --ff-only {source} {branch}') + if self.execute(f'"{self.git}" stash pop', allow_failure=True): + pass + else: + # No local changes to existing files, untracked files not included + logger.info('Stash pop failed, there seems to be no local changes, skip instead') + else: + logger.info('Stash failed, this may be the first installation, drop changes instead') + self.execute(f'"{self.git}" reset --hard {source}/{branch}') + self.execute(f'"{self.git}" pull --ff-only {source} {branch}') + else: + self.execute(f'"{self.git}" reset --hard {source}/{branch}') + self.execute(f'"{self.git}" pull --ff-only {source} {branch}') + + logger.hr('Show Version', 1) + self.execute(f'"{self.git}" --no-pager log --no-merges -1') + + @property + def goc_client(self): + client = GitOverCdnClient( + url='https://vip.123pan.cn/1818706573/pack/LmeSzinc_AzurLaneAutoScript_master', + folder=self.root_filepath, + source='origin', + branch='master', + git=self.git, + ) + client.logger = logger + return client + + def git_install(self): + logger.hr('Update Alas', 0) + + if not self.AutoUpdate: + logger.info('AutoUpdate is disabled, skip') + return + + if self.GitOverCdn: + if self.goc_client.update(keep_changes=self.KeepLocalChanges): + return + + self.git_repository_init( + repo=self.Repository, + source='origin', + branch=self.Branch, + proxy=self.GitProxy, + ssl_verify=self.SSLVerify, + keep_changes=self.KeepLocalChanges, + ) + + +if __name__ == '__main__': + self = GitManager() + self.goc_client.get_status() \ No newline at end of file diff --git a/deploy/headless/requirements.txt b/deploy/headless/requirements.txt index c563a82b7..b10cef4d5 100644 --- a/deploy/headless/requirements.txt +++ b/deploy/headless/requirements.txt @@ -1,31 +1,43 @@ -adbutils==0.11.0 -aiofiles==23.1.0 -anyio==1.3.1 -av==10.0.0 -cnocr==1.2.2 -imageio==2.27.0 -inflection==0.5.1 -jellyfish==0.11.2 -lz4==4.3.2 -mxnet==1.6.0 +# Image processing numpy==1.17.4 -onepush==1.2.0 -opencv-python-headless==4.7.0.72 -pillow==9.5.0 -prettytable==2.2.1 -psutil==5.9.3 -pydantic==1.10.9 -pypresence==4.2.1 -pywebio==1.6.2 -pyyaml==6.0 -pyzmq==22.3.0 -retrying==1.3.4 -rich==11.2.0 scipy==1.4.1 -starlette==0.14.2 -tqdm==4.65.0 +pillow==9.5.0 +opencv-python-headless==4.7.0.72 +imageio==2.27.0 + +# Device connection +adbutils==0.11.0 uiautomator2==2.16.17 uiautomator2cache==0.3.0.1 -uvicorn[standard]==0.17.6 wrapt==1.15.0 -zerorpc==0.6.3 \ No newline at end of file +retrying==1.3.4 +lz4==4.3.2 +av==10.0.0 +psutil==5.9.3 + +# Utils +rich==11.2.0 +tqdm==4.65.0 +jellyfish==0.11.2 +pyyaml==6.0 +inflection==0.5.1 +pydantic==1.10.9 +aiofiles==23.1.0 +prettytable==2.2.1 +anyio==1.3.1 + +# Pushing +onepush==1.3.0 +pycryptodome==3.9.9 +pypresence==4.2.1 + +# Ocr +cnocr==1.2.2 +mxnet==1.6.0 + +# Webui +pywebio==1.6.2 +starlette==0.14.2 +uvicorn[standard]==0.17.6 +zerorpc==0.6.3 +pyzmq==22.3.0 \ No newline at end of file diff --git a/deploy/headless/requirements_generator.py b/deploy/headless/requirements_generator.py index bacf1ec7a..9c3c1ddbd 100644 --- a/deploy/headless/requirements_generator.py +++ b/deploy/headless/requirements_generator.py @@ -10,6 +10,8 @@ def read_file(file): out = {} with open(file, 'r', encoding='utf-8') as f: for line in f.readlines(): + if not line.strip(): + continue res = [s.strip() for s in line.split('==')] if len(res) > 1: name, version = res @@ -29,7 +31,9 @@ def write_file(file, data): lines.append(str(name)) with open(file, 'w', encoding='utf-8', newline='') as f: - f.write('\n'.join(lines)) + text = '\n'.join(lines) + text = text.replace('#', '\n#').strip() + f.write(text) def headless_requirements_generate(requirements_in='requirements-in.txt'): @@ -41,7 +45,7 @@ def headless_requirements_generate(requirements_in='requirements-in.txt'): 'inflection': '0.5.1', 'lz4': '4.3.2', 'numpy': '1.17.4', - 'onepush': '1.2.0', + # 'onepush': '1.2.0', 'opencv-python': { 'name': 'opencv-python-headless', 'version': '4.7.0.72' diff --git a/dev_tools/button_extract.py b/dev_tools/button_extract.py index 1c8eb2dff..b966f1a77 100644 --- a/dev_tools/button_extract.py +++ b/dev_tools/button_extract.py @@ -174,6 +174,8 @@ class ModuleExtractor: exp.append(ImageExtractor(module=self.name, file=file).expression) continue + exp.sort() + logger.info('Module: %s(%s)' % (self.name, len(exp))) exp = IMPORT_EXP + exp return exp diff --git a/dev_tools/map_extractor.py b/dev_tools/map_extractor.py index be324c21e..7dcafcf03 100644 --- a/dev_tools/map_extractor.py +++ b/dev_tools/map_extractor.py @@ -248,6 +248,14 @@ DIC_SIREN_NAME_CHI_TO_ENG = { # Light-Chasing Sea of Stars 'sairenboss10': 'Sirenboss10', 'UDFsairen_baolei_2': 'UDFFortress2', + + # Heart-Linking Harmony + 'lafei_6': 'Laffey6', + 'tashigan_idol': 'TashkentIdol', + 'xiefeierde_idol': 'SheffieldIdol', + 'yilishabai_3': 'Elizabeth3', + 'jiasikenie_idol': 'GascogneIdol', + 'dafeng_idol': 'TaihouIdol', } diff --git a/module/base/base.py b/module/base/base.py index fc24743c7..587951663 100644 --- a/module/base/base.py +++ b/module/base/base.py @@ -10,12 +10,15 @@ from module.device.method.utils import HierarchyButton from module.logger import logger from module.map_detection.utils import fit_points from module.statistics.azurstats import AzurStats +from module.webui.setting import cached_class_property class ModuleBase: config: AzurLaneConfig device: Device + EARLY_OCR_IMPORT = False + def __init__(self, config, device=None, task=None): """ Args: @@ -49,6 +52,7 @@ class ModuleBase: self.device = device self.interval_timer = {} + self.early_ocr_import() @cached_property def stat(self) -> AzurStats: @@ -58,6 +62,57 @@ class ModuleBase: def emotion(self) -> Emotion: return Emotion(config=self.config) + def early_ocr_import(self): + """ + Start a thread to import cnocr and mxnet while the Alas instance just starting to take screenshots + The import is paralleled since taking screenshot is I/O-bound while importing is CPU-bound, + thus would speed up the startup 0.5 ~ 1.0s and even 5s on slow PCs. + """ + if ModuleBase.EARLY_OCR_IMPORT: + return + if not self.config.is_actual_task: + logger.info('No actual task bound, skip early_ocr_import') + return + + def do_ocr_import(): + # Wait first image + import time + while 1: + if self.device.has_cached_image: + break + time.sleep(0.01) + + logger.info('early_ocr_import start') + from module.ocr.al_ocr import AlOcr + _ = AlOcr + logger.info('early_ocr_import finish') + + logger.info('early_ocr_import call') + import threading + thread = threading.Thread(target=do_ocr_import, daemon=True) + thread.start() + ModuleBase.EARLY_OCR_IMPORT = True + + @cached_class_property + def worker(self): + """ + A thread pool to run things at background + + Examples: + ``` + def func(image): + logger.info('Update thread start') + with self.config.multi_set(): + self.dungeon_get_simuni_point(image) + self.dungeon_update_stamina(image) + ModuleBase.worker.submit(func, self.device.image) + ``` + """ + logger.hr('Creating worker') + from concurrent.futures import ThreadPoolExecutor + pool = ThreadPoolExecutor(1) + return pool + def ensure_button(self, button): if isinstance(button, str): button = HierarchyButton(self.device.hierarchy, button) diff --git a/module/campaign/campaign_ocr.py b/module/campaign/campaign_ocr.py index 78642c549..1929fdf00 100644 --- a/module/campaign/campaign_ocr.py +++ b/module/campaign/campaign_ocr.py @@ -59,7 +59,7 @@ class CampaignOcr(ModuleBase): name = name.strip('-') if name == 'sp': return 'ex_sp', '1' - elif name.startswith('extra'): + elif name.startswith('extra') or name == 'ex': return 'ex_ex', '1' elif '-' in name: return name.split('-') @@ -189,11 +189,16 @@ class CampaignOcr(ModuleBase): name_offset=(75, 9), name_size=(60, 16) ) # 2024.04.11 Game client bugged with random broken assets around TEMPLATE_STAGE_CLEAR - digits += self.campaign_match_multi( - TEMPLATE_STAGE_CLEAR_SMALL, - image, self._stage_image_gray, - name_offset=(53, 2), name_size=(60, 16) - ) + # digits += self.campaign_match_multi( + # TEMPLATE_STAGE_CLEAR_SMALL, + # image, self._stage_image_gray, + # name_offset=(53, 2), name_size=(60, 16) + # ) + # digits += self.campaign_match_multi( + # TEMPLATE_STAGE_HALF_PERCENT, + # image, self._stage_image_gray, + # name_offset=(48, 0), name_size=(60, 16) + # ) digits += self.campaign_match_multi( TEMPLATE_STAGE_PERCENT, image, self._stage_image_gray, diff --git a/module/campaign/gems_farming.py b/module/campaign/gems_farming.py index 775fe0e20..6cae8f5e8 100644 --- a/module/campaign/gems_farming.py +++ b/module/campaign/gems_farming.py @@ -160,6 +160,7 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): def _fleet_detail_enter_hard(self): from module.retire.retirement import Retirement + _retire_class = Retirement(config=self.config, device=self.device) self.campaign.ensure_campaign_ui(self.stage) button_area = self.campaign.ENTRANCE.button button = Button(name=str(self.stage), area=button_area, color=(0, 0, 0), button=button_area) @@ -170,7 +171,7 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): self.device.screenshot() if self.appear_then_click(MAP_PREPARATION): self.device.sleep(0.5) - if Retirement(config=self.config, device=self.device).handle_retirement(): + if _retire_class.handle_retirement(): continue if self.appear(button=FLEET_PREPARATION, offset=(50, 50)): return @@ -344,13 +345,20 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): scanner.set_limitation(fleet=0) + if self.config.GemsFarming_CommonDD == 'any': + return scanner.scan(self.device.image, output=False) + candidates = self.find_candidates(self.get_templates(self.config.GemsFarming_CommonDD), scanner) if candidates: return candidates - else: - logger.info('No specific DD was found, try reversed order.') - return candidates + + logger.info('No specific DD was found, try reversed order.') + self.dock_sort_method_dsc_set(False) + + candidates = self.find_candidates(self.get_templates(self.config.GemsFarming_CommonDD), scanner) + + return candidates def find_candidates(self, template, scanner): """ diff --git a/module/campaign/run.py b/module/campaign/run.py index 6db29f368..6efbd8030 100644 --- a/module/campaign/run.py +++ b/module/campaign/run.py @@ -197,6 +197,12 @@ class CampaignRun(CampaignEvent, ShopStatus): name = 'sp' if folder == 'event_20221124_cn' and name in ['asp', 'a.sp']: name = 'sp' + if folder == 'event_20240425_cn': + if name in ['μsp', 'usp', 'iisp']: + name = 'sp' + name = name.replace('lsp', 'isp').replace('1sp', 'isp') + if name == 'isp': + name = 'isp1' # Convert to chapter T convert = { 'a1': 't1', @@ -353,7 +359,7 @@ class CampaignRun(CampaignEvent, ShopStatus): # UI ensure self.device.stuck_record_clear() self.device.click_record_clear() - if not hasattr(self.device, 'image') or self.device.image is None: + if not self.device.has_cached_image: self.device.screenshot() self.campaign.device.image = self.device.image if self.campaign.is_in_map(): diff --git a/module/config/argument/args.json b/module/config/argument/args.json index d33005fdf..e07dbc2a2 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -128,8 +128,7 @@ "uiautomator2", "minitouch", "Hermit", - "MaaTouch", - "nemu_ipc" + "MaaTouch" ] }, "ScreenshotDedithering": { @@ -1898,17 +1897,18 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "display": "hide", "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -2232,16 +2232,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -2680,16 +2681,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -3128,16 +3130,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -3542,8 +3545,8 @@ "raid_20240328" ], "option_bold": [ - "raid_20240328", - "raid_20230629" + "raid_20230629", + "raid_20240328" ], "cn": "raid_20240328", "en": "raid_20240328", @@ -4522,16 +4525,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -4987,16 +4991,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -5452,16 +5457,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -5917,16 +5923,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -6372,16 +6379,17 @@ "event_20231026_cn", "event_20231123_cn", "event_20231221_cn", - "event_20240229_cn" + "event_20240229_cn", + "event_20240425_cn" ], "option_bold": [ - "event_20220224_cn", - "event_20211111_cn" + "event_20230817_cn", + "event_20240425_cn" ], - "cn": "event_20220224_cn", - "en": "event_20220224_cn", - "jp": "event_20220224_cn", - "tw": "event_20211111_cn" + "cn": "event_20240425_cn", + "en": "event_20240425_cn", + "jp": "event_20240425_cn", + "tw": "event_20230817_cn" }, "Mode": { "type": "select", @@ -6783,8 +6791,8 @@ "raid_20240328" ], "option_bold": [ - "raid_20240328", - "raid_20230629" + "raid_20230629", + "raid_20240328" ], "cn": "raid_20240328", "en": "raid_20240328", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 285355ee1..4e7d3262f 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -52,7 +52,6 @@ Emulator: minitouch, Hermit, MaaTouch, - nemu_ipc, ] ScreenshotDedithering: false AdbRestart: false diff --git a/module/config/config.py b/module/config/config.py index 31dee5151..0dae98828 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -189,6 +189,10 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher self.data, keys="Alas.Optimization.CloseGameDuringWait", default=False ) + @property + def is_actual_task(self): + return self.task.command.lower() not in ['alas', 'template'] + def get_next_task(self): """ Calculate tasks, set pending_task and waiting_task diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 53966296f..962210dbc 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -22,7 +22,7 @@ class GeneratedConfig: Emulator_PackageName = 'auto' # auto, com.bilibili.azurlane, com.YoStarEN.AzurLane, com.YoStarJP.AzurLane, com.hkmanjuu.azurlane.gp, com.bilibili.blhx.huawei, com.bilibili.blhx.mi, com.tencent.tmgp.bilibili.blhx, com.bilibili.blhx.baidu, com.bilibili.blhx.qihoo, com.bilibili.blhx.nearme.gamecenter, com.bilibili.blhx.vivo, com.bilibili.blhx.mz, com.bilibili.blhx.dl, com.bilibili.blhx.lenovo, com.bilibili.blhx.uc, com.bilibili.blhx.mzw, com.yiwu.blhx.yx15, com.bilibili.blhx.m4399, com.bilibili.blhx.bilibiliMove, com.hkmanjuu.azurlane.gp.mc Emulator_ServerName = 'disabled' # disabled, cn_android-0, cn_android-1, cn_android-2, cn_android-3, cn_android-4, cn_android-5, cn_android-6, cn_android-7, cn_android-8, cn_android-9, cn_android-10, cn_android-11, cn_android-12, cn_android-13, cn_android-14, cn_android-15, cn_android-16, cn_android-17, cn_android-18, cn_android-19, cn_android-20, cn_android-21, cn_android-22, cn_android-23, cn_ios-0, cn_ios-1, cn_ios-2, cn_ios-3, cn_ios-4, cn_ios-5, cn_ios-6, cn_ios-7, cn_ios-8, cn_ios-9, cn_ios-10, cn_channel-0, cn_channel-1, cn_channel-2, cn_channel-3, cn_channel-4, en-0, en-1, en-2, en-3, en-4, en-5, jp-0, jp-1, jp-2, jp-3, jp-4, jp-5, jp-6, jp-7, jp-8, jp-9, jp-10, jp-11, jp-12, jp-13, jp-14, jp-15, jp-16, jp-17 Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy, nemu_ipc - Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch, nemu_ipc + Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False diff --git a/module/config/config_manual.py b/module/config/config_manual.py index de3f6d7e6..9b9799274 100644 --- a/module/config/config_manual.py +++ b/module/config/config_manual.py @@ -143,7 +143,7 @@ class ManualConfig: MAP_HAS_MYSTERY = True MAP_MYSTERY_MAP_CLICK = True MAP_MYSTERY_HAS_CARRIER = False - MAP_GRID_CENTER_TOLERANCE = 0.1 + MAP_GRID_CENTER_TOLERANCE = 0.2 MOVABLE_ENEMY_FLEET_STEP = 2 MOVABLE_ENEMY_TURN = (2,) diff --git a/module/config/config_updater.py b/module/config/config_updater.py index fcd4a2ced..5700b9d52 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -447,7 +447,7 @@ class ConfigGenerator: latest = {} for server in ARCHIVES_PREFIX.keys(): latest[server] = deep_pop(self.args, keys=f'{task}.Campaign.Event.{server}', default='') - bold = list(set(latest.values())) + bold = sorted(set(latest.values())) deep_set(self.args, keys=f'{task}.Campaign.Event.option_bold', value=bold) for server, event in latest.items(): deep_set(self.args, keys=f'{task}.Campaign.Event.{server}', value=event) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 9ee65a908..92ca33742 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -427,8 +427,7 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch", - "nemu_ipc": "nemu_ipc" + "MaaTouch": "MaaTouch" }, "ScreenshotDedithering": { "name": "Image Color De-dithering", @@ -732,6 +731,7 @@ "event_20231123_cn": "The Ninja Scrolls: Azur Flash", "event_20231221_cn": "Light-Chasing Sea of Stars", "event_20240229_cn": "Snowrealm Peregrination", + "event_20240425_cn": "Heart-Linking Harmony", "raid_20200624": "Air Raid Drills with Essex Rerun", "raid_20210708": "Cross Wave rerun", "raid_20220127": "Mystery Investigation", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index c1f598955..d547caf95 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -427,8 +427,7 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch", - "nemu_ipc": "nemu_ipc" + "MaaTouch": "MaaTouch" }, "ScreenshotDedithering": { "name": "Emulator.ScreenshotDedithering.name", @@ -732,6 +731,7 @@ "event_20231123_cn": "蒼閃忍法帖", "event_20231221_cn": "光追う星の海", "event_20240229_cn": "銀界遊廻", + "event_20240425_cn": "共鳴のパッション", "raid_20200624": "特別演習超空強襲波(復刻)", "raid_20210708": "交錯する新たな波 (復刻)", "raid_20220127": "秘密事件調査", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 55f179591..9287dfeaf 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -427,8 +427,7 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch", - "nemu_ipc": "nemu_ipc" + "MaaTouch": "MaaTouch" }, "ScreenshotDedithering": { "name": "去除图片色彩抖动", @@ -732,6 +731,7 @@ "event_20231123_cn": "苍闪忍法帖", "event_20231221_cn": "星海逐光", "event_20240229_cn": "雪境迷踪", + "event_20240425_cn": "共鸣的PASSION", "raid_20200624": "复刻特别演习埃塞克斯级", "raid_20210708": "复刻穿越彼方的水线", "raid_20220127": "演习神秘事件调查", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index a58765efb..ed2feba61 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -427,8 +427,7 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch", - "nemu_ipc": "nemu_ipc" + "MaaTouch": "MaaTouch" }, "ScreenshotDedithering": { "name": "去除圖片色彩抖動", @@ -726,12 +725,13 @@ "event_20230223_cn": "湮燼塵墟", "event_20230525_cn": "Confluence of Nothingness", "event_20230803_cn": "奏響鳶尾之歌", - "event_20230817_cn": "The Fools Scales", + "event_20230817_cn": "愚者的天平", "event_20230914_cn": "Effulgence Before Eclipse", "event_20231026_cn": "Tempesta and the Fountain of Youth", "event_20231123_cn": "蒼閃忍法帖", "event_20231221_cn": "Light-Chasing Sea of Stars", "event_20240229_cn": "Snowrealm Peregrination", + "event_20240425_cn": "Heart-Linking Harmony", "raid_20200624": "特別演習埃塞克斯級(復刻)", "raid_20210708": "復刻穿越彼方的水線", "raid_20220127": "演習神秘事件調查", diff --git a/module/device/connection.py b/module/device/connection.py index bbd6ed0b4..71ebbdeef 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -4,6 +4,7 @@ import platform import re import socket import subprocess +import sys import time from functools import wraps @@ -84,10 +85,17 @@ class AdbDeviceWithStatus(AdbDevice): def __bool__(self): return True + @cached_property + def port(self) -> int: + try: + return int(self.serial.split(':')[1]) + except (IndexError, ValueError): + return 0 + @cached_property def may_mumu12_family(self): # 127.0.0.1:16XXX - return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16') + return 16384 <= self.port <= 17408 class Connection(ConnectionAttr): @@ -498,30 +506,51 @@ class Connection(ConnectionAttr): def adb_forward_remove(self, local): """ Equivalent to `adb -s forward --remove ` + No error raised when removing a non-existent forward + More about the commands send to ADB server, see: https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/SERVICES.TXT Args: local (str): Such as 'tcp:2437' """ - with self.adb_client._connect() as c: - list_cmd = f"host-serial:{self.serial}:killforward:{local}" - c.send_command(list_cmd) - c.check_okay() + try: + with self.adb_client._connect() as c: + list_cmd = f"host-serial:{self.serial}:killforward:{local}" + c.send_command(list_cmd) + c.check_okay() + except AdbError as e: + # No error raised when removing a non-existed forward + # adbutils.errors.AdbError: listener 'tcp:8888' not found + msg = str(e) + if re.search(r'listener .*? not found', msg): + logger.warning(f'{type(e).__name__}: {msg}') + else: + raise def adb_reverse_remove(self, local): """ Equivalent to `adb -s reverse --remove ` + No error raised when removing a non-existent reverse Args: local (str): Such as 'tcp:2437' """ - with self.adb_client._connect() as c: - c.send_command(f"host:transport:{self.serial}") - c.check_okay() - list_cmd = f"reverse:killforward:{local}" - c.send_command(list_cmd) - c.check_okay() + try: + with self.adb_client._connect() as c: + c.send_command(f"host:transport:{self.serial}") + c.check_okay() + list_cmd = f"reverse:killforward:{local}" + c.send_command(list_cmd) + c.check_okay() + except AdbError as e: + # No error raised when removing a non-existed forward + # adbutils.errors.AdbError: listener 'tcp:8888' not found + msg = str(e) + if re.search(r'listener .*? not found', msg): + logger.warning(f'{type(e).__name__}: {msg}') + else: + raise def adb_push(self, local, remote): """ @@ -798,8 +827,11 @@ class Connection(ConnectionAttr): # brute_force_connect if self.config.Emulator_Serial == 'auto' and available.count == 0: logger.warning(f'No available device found') - brute_force_connect() - continue + if sys.platform == 'win32': + brute_force_connect() + continue + else: + break else: break @@ -811,7 +843,7 @@ class Connection(ConnectionAttr): raise RequestHumanTakeover elif available.count == 1: logger.info(f'Auto device detection found only one device, using it') - self.serial = available[0].serial + self.config.Emulator_Serial = self.serial = available[0].serial del_cached_property(self, 'adb') elif available.count == 2 \ and available.select(serial='127.0.0.1:7555') \ @@ -820,7 +852,7 @@ class Connection(ConnectionAttr): # For MuMu12 serials like 127.0.0.1:7555 and 127.0.0.1:16384 # ignore 7555 use 16384 remain = available.select(may_mumu12_family=True).first_or_none() - self.serial = remain.serial + self.config.Emulator_Serial = self.serial = remain.serial del_cached_property(self, 'adb') else: logger.critical('Multiple devices found, auto device detection cannot decide which to choose, ' @@ -829,6 +861,7 @@ class Connection(ConnectionAttr): # Handle LDPlayer # LDPlayer serial jumps between `127.0.0.1:5555+{X}` and `emulator-5554+{X}` + # No config write since it's dynamic port_serial, emu_serial = get_serial_pair(self.serial) if port_serial and emu_serial: # Might be LDPlayer, check connected devices @@ -862,8 +895,7 @@ class Connection(ConnectionAttr): if mumu12.count == 1: emu_serial = mumu12.first_or_none().serial logger.warning(f'Redirect MuMu12 {self.serial} to {emu_serial}') - self.serial = emu_serial - self.config.Emulator_Serial = emu_serial + self.config.Emulator_Serial = self.serial = emu_serial break elif mumu12.count >= 2: logger.warning(f'Multiple MuMu12 serial found, cannot redirect') @@ -871,6 +903,8 @@ class Connection(ConnectionAttr): else: # Only 127.0.0.1:7555 if self.is_mumu_over_version_356: + # is_mumu_over_version_356 and nemud_app_keep_alive was cached + # Acceptable since it's the same device logger.warning(f'Device {self.serial} is MuMu12 but corresponding port not found') brute_force_connect() devices = self.list_device() @@ -885,6 +919,26 @@ class Connection(ConnectionAttr): # MuMu6 break + # MuMu12 uses 127.0.0.1:16385 if port 16384 is occupied, auto redirect + # No config write since it's dynamic + if self.is_mumu12_family: + matched = False + for device in available.select(may_mumu12_family=True): + if device.port == self.port: + # Exact match + matched = True + break + if not matched: + for device in available.select(may_mumu12_family=True): + if -2 <= device.port - self.port <= 2: + # Port switched + logger.info(f'MuMu12 port switches from {self.serial} to {device.serial}') + del_cached_property(self, 'port') + del_cached_property(self, 'is_mumu12_family') + del_cached_property(self, 'is_mumu_family') + self.serial = device.serial + break + @retry def list_package(self, show_log=True): """ diff --git a/module/device/connection_attr.py b/module/device/connection_attr.py index da16756d5..5fe64733a 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -142,6 +142,18 @@ class ConnectionAttr: def is_wsa(self): return bool(re.match(r'^wsa', self.serial)) + @cached_property + def port(self) -> int: + try: + return int(self.serial.split(':')[1]) + except (IndexError, ValueError): + return 0 + + @cached_property + def is_mumu12_family(self): + # 127.0.0.1:16XXX + return 16384 <= self.port <= 17408 + @cached_property def is_mumu_family(self): # 127.0.0.1:7555 @@ -149,9 +161,8 @@ class ConnectionAttr: return self.serial == '127.0.0.1:7555' or self.is_mumu12_family @cached_property - def is_mumu12_family(self): - # 127.0.0.1:16384 + 32*n - return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16') + def is_nox_family(self): + return 62001 <= self.port <= 63025 @cached_property def is_emulator(self): @@ -197,7 +208,8 @@ class ConnectionAttr: rf"SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config") as key: port = QueryValueEx(key, "BstAdbPort")[0] except FileNotFoundError: - logger.error(rf'Unable to find registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config') + logger.error( + rf'Unable to find registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config') logger.error('Please confirm that your are using BlueStack 4 hyper-v and not regular BlueStacks 4') logger.error(r'Please check if there is any other emulator instances under ' r'registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests') diff --git a/module/device/device.py b/module/device/device.py index 33efadf42..e745b55c9 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -1,6 +1,12 @@ import collections from datetime import datetime +# Patch pkg_resources before importing adbutils and uiautomator2 +from module.device.pkg_resources import get_distribution + +# Just avoid being removed by import optimization +_ = get_distribution + from module.base.timer import Timer from module.config.utils import get_server_next_update from module.device.app_control import AppControl @@ -92,6 +98,13 @@ class Device(Screenshot, Control, AppControl): if not self.config.is_template_config and self.config.Emulator_ScreenshotMethod == 'auto': self.run_simple_screenshot_benchmark() + # Early init + if self.config.is_actual_task: + if self.config.Emulator_ControlMethod == 'MaaTouch': + self.early_maatouch_init() + if self.config.Emulator_ControlMethod == 'minitouch': + self.early_minitouch_init() + def run_simple_screenshot_benchmark(self): """ Perform a screenshot method benchmark, test 3 times on each method. @@ -175,6 +188,16 @@ class Device(Screenshot, Control, AppControl): if self.config.Emulator_ScreenshotMethod == 'nemu_ipc': self.nemu_ipc_release() + def get_orientation(self): + """ + Callbacks when orientation changed. + """ + o = super().get_orientation() + + self.on_orientation_change_maatouch() + + return o + def stuck_record_add(self, button): self.detect_record.add(str(button)) diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index b1d6e3471..e9a171322 100644 --- a/module/device/method/maatouch.py +++ b/module/device/method/maatouch.py @@ -1,14 +1,15 @@ import socket +import threading from functools import wraps from adbutils.errors import AdbError -from module.base.decorator import cached_property, del_cached_property +from module.base.decorator import cached_property, del_cached_property, has_cached_property from module.base.timer import Timer from module.base.utils import * from module.device.connection import Connection from module.device.method.minitouch import CommandBuilder, insert_swipe -from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error +from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep from module.exception import RequestHumanTakeover from module.logger import logger @@ -36,20 +37,20 @@ def retry(func): def init(): self.adb_reconnect() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') # Emulator closed except ConnectionAbortedError as e: logger.error(e) def init(): self.adb_reconnect() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_reconnect() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') else: break # MaaTouchNotInstalledError: Received "Aborted" from MaaTouch @@ -58,12 +59,12 @@ def retry(func): def init(): self.maatouch_install() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') except BrokenPipeError as e: logger.error(e) def init(): - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') # Unknown, probably a trucked image except Exception as e: logger.exception(e) @@ -77,6 +78,19 @@ def retry(func): return retry_wrapper +class MaatouchBuilder(CommandBuilder): + def __init__(self, device, contact=0, handle_orientation=False): + """ + Args: + device (MaaTouch): + """ + + super().__init__(device, contact, handle_orientation) + + def send(self): + return self.device.maatouch_send(builder=self) + + class MaaTouchNotInstalledError(Exception): pass @@ -88,14 +102,55 @@ class MaaTouch(Connection): """ max_x: int max_y: int - _maatouch_stream = socket.socket + _maatouch_stream: socket.socket = None _maatouch_stream_storage = None + _maatouch_init_thread = None + _maatouch_orientation: int = None @cached_property - def maatouch_builder(self): + @retry + def _maatouch_builder(self): self.maatouch_init() - # Orientation is handled inside MaaTouch - return CommandBuilder(self, handle_orientation=False) + return MaatouchBuilder(self) + + @property + def maatouch_builder(self): + # Wait init thread + if self._maatouch_init_thread is not None: + self._maatouch_init_thread.join() + del self._maatouch_init_thread + self._maatouch_init_thread = None + + return self._maatouch_builder + + def early_maatouch_init(self): + """ + Start a thread to init maatouch connection while the Alas instance just starting to take screenshots + This would speed up the first click 0.2 ~ 0.4s. + """ + if has_cached_property(self, '_maatouch_builder'): + return + + def early_maatouch_init_func(): + _ = self._maatouch_builder + + thread = threading.Thread(target=early_maatouch_init_func, daemon=True) + self._maatouch_init_thread = thread + thread.start() + + def on_orientation_change_maatouch(self): + """ + MaaTouch caches devices orientation at its startup + A restart is required when orientation changed + """ + if self._maatouch_orientation is None: + return + if self.orientation == self._maatouch_orientation: + return + + logger.info(f'Orientation changed {self._maatouch_orientation} => {self.orientation}, re-init MaaTouch') + del_cached_property(self, '_maatouch_builder') + self.early_maatouch_init() def maatouch_init(self): logger.hr('MaaTouch init') @@ -103,6 +158,20 @@ class MaaTouch(Connection): max_contacts = 2 max_pressure = 50 + # Try to close existing stream + if self._maatouch_stream is not None: + try: + self._maatouch_stream.close() + except Exception as e: + logger.error(e) + del self._maatouch_stream + if self._maatouch_stream_storage is not None: + del self._maatouch_stream_storage + + # MaaTouch caches devices orientation at its startup + super(MaaTouch, self).get_orientation() + self._maatouch_orientation = self.orientation + # CLASSPATH=/data/local/tmp/maatouch app_process / com.shxyke.MaaTouch.App stream = self.adb_shell( ['CLASSPATH=/data/local/tmp/maatouch', 'app_process', '/', 'com.shxyke.MaaTouch.App'], @@ -166,14 +235,14 @@ class MaaTouch(Connection): ) ) - def maatouch_send(self): - content = self.maatouch_builder.to_minitouch() + def maatouch_send(self, builder: MaatouchBuilder): + content = builder.to_minitouch() # logger.info("send operation: {}".format(content.replace("\n", "\\n"))) byte_content = content.encode('utf-8') self._maatouch_stream.sendall(byte_content) self._maatouch_stream.recv(0) - self.sleep(self.maatouch_builder.delay / 1000 + self.maatouch_builder.DEFAULT_DELAY) - self.maatouch_builder.clear() + self.sleep(self.maatouch_builder.delay / 1000 + builder.DEFAULT_DELAY) + builder.clear() def maatouch_install(self): logger.hr('MaaTouch install') @@ -188,7 +257,7 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(x, y).commit() builder.up().commit() - self.maatouch_send() + builder.send() @retry def long_click_maatouch(self, x, y, duration=1.0): @@ -196,7 +265,7 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(x, y).commit().wait(duration) builder.up().commit() - self.maatouch_send() + builder.send() @retry def swipe_maatouch(self, p1, p2): @@ -204,14 +273,14 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(*points[0]).commit() - self.maatouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.maatouch_send() + builder.send() builder.up().commit() - self.maatouch_send() + builder.send() @retry def drag_maatouch(self, p1, p2, point_random=(-10, -10, 10, 10)): @@ -221,15 +290,15 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(*points[0]).commit() - self.maatouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.maatouch_send() + builder.send() builder.move(*p2).commit().wait(140) builder.move(*p2).commit().wait(140) - self.maatouch_send() + builder.send() builder.up().commit() - self.maatouch_send() + builder.send() diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index 70abd1a1c..30fdcc88c 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -1,7 +1,7 @@ import asyncio import json -import re import socket +import threading import time from functools import wraps from typing import List @@ -10,11 +10,11 @@ import websockets from adbutils.errors import AdbError from uiautomator2 import _Service -from module.base.decorator import Config, cached_property, del_cached_property +from module.base.decorator import Config, cached_property, del_cached_property, has_cached_property from module.base.timer import Timer from module.base.utils import * from module.device.connection import Connection -from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error +from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -184,7 +184,7 @@ class CommandBuilder: max_x = 1280 max_y = 720 - def __init__(self, device, handle_orientation=True): + def __init__(self, device, contact=0, handle_orientation=True): """ Args: device: @@ -192,6 +192,7 @@ class CommandBuilder: self.device = device self.commands = [] self.delay = 0 + self.contact = contact self.handle_orientation = handle_orientation @property @@ -243,21 +244,21 @@ class CommandBuilder: self.delay += ms return self - def up(self, contact=0): + def up(self): """ add minitouch command: 'u \n' """ - self.commands.append(Command('u', contact=contact)) + self.commands.append(Command('u', contact=self.contact)) return self - def down(self, x, y, contact=0, pressure=100): + def down(self, x, y, pressure=100): """ add minitouch command: 'd \n' """ x, y = self.convert(x, y) - self.commands.append(Command('d', x=x, y=y, contact=contact, pressure=pressure)) + self.commands.append(Command('d', x=x, y=y, contact=self.contact, pressure=pressure)) return self - def move(self, x, y, contact=0, pressure=100): + def move(self, x, y, pressure=100): """ add minitouch command: 'm \n' """ x, y = self.convert(x, y) - self.commands.append(Command('m', x=x, y=y, contact=contact, pressure=pressure)) + self.commands.append(Command('m', x=x, y=y, contact=self.contact, pressure=pressure)) return self def clear(self): @@ -271,6 +272,9 @@ class CommandBuilder: def to_atx_agent(self) -> List[str]: return [command.to_atx_agent(self.max_x, self.max_y) for command in self.commands] + def send(self): + return self.device.minitouch_send(builder=self) + class MinitouchNotInstalledError(Exception): pass @@ -310,12 +314,18 @@ def retry(func): def init(): self.adb_reconnect() + if self._minitouch_port: + self.adb_forward_remove(f'tcp:{self._minitouch_port}') + del_cached_property(self, '_minitouch_builder') # Emulator closed except ConnectionAbortedError as e: logger.error(e) def init(): self.adb_reconnect() + if self._minitouch_port: + self.adb_forward_remove(f'tcp:{self._minitouch_port}') + del_cached_property(self, '_minitouch_builder') # MinitouchNotInstalledError: Received empty data from minitouch except MinitouchNotInstalledError as e: logger.error(e) @@ -324,7 +334,7 @@ def retry(func): self.install_uiautomator2() if self._minitouch_port: self.adb_forward_remove(f'tcp:{self._minitouch_port}') - del_cached_property(self, 'minitouch_builder') + del_cached_property(self, '_minitouch_builder') # MinitouchOccupiedError: Timeout when connecting to minitouch except MinitouchOccupiedError as e: logger.error(e) @@ -333,19 +343,22 @@ def retry(func): self.restart_atx() if self._minitouch_port: self.adb_forward_remove(f'tcp:{self._minitouch_port}') - del_cached_property(self, 'minitouch_builder') + del_cached_property(self, '_minitouch_builder') # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_reconnect() + if self._minitouch_port: + self.adb_forward_remove(f'tcp:{self._minitouch_port}') + del_cached_property(self, '_minitouch_builder') else: break except BrokenPipeError as e: logger.error(e) def init(): - del_cached_property(self, 'minitouch_builder') + del_cached_property(self, '_minitouch_builder') # Unknown, probably a trucked image except Exception as e: logger.exception(e) @@ -361,23 +374,59 @@ def retry(func): class Minitouch(Connection): _minitouch_port: int = 0 - _minitouch_client: socket.socket + _minitouch_client: socket.socket = None _minitouch_pid: int _minitouch_ws: websockets.WebSocketClientProtocol max_x: int max_y: int + _minitouch_init_thread = None @cached_property - def minitouch_builder(self): + @retry + def _minitouch_builder(self): self.minitouch_init() return CommandBuilder(self) + @property + def minitouch_builder(self): + # Wait init thread + if self._minitouch_init_thread is not None: + self._minitouch_init_thread.join() + del self._minitouch_init_thread + self._minitouch_init_thread = None + + return self._minitouch_builder + + def early_minitouch_init(self): + """ + Start a thread to init minitouch connection while the Alas instance just starting to take screenshots + This would speed up the first click 0.05s. + """ + if has_cached_property(self, '_minitouch_builder'): + return + + def early_minitouch_init_func(): + _ = self._minitouch_builder + + thread = threading.Thread(target=early_minitouch_init_func, daemon=True) + self._minitouch_init_thread = thread + thread.start() + @Config.when(DEVICE_OVER_HTTP=False) def minitouch_init(self): logger.hr('MiniTouch init') max_x, max_y = 1280, 720 max_contacts = 2 max_pressure = 50 + + # Try to close existing stream + if self._minitouch_client is not None: + try: + self._minitouch_client.close() + except Exception as e: + logger.error(e) + del self._minitouch_client + self.get_orientation() self._minitouch_port = self.adb_forward("localabstract:minitouch") @@ -446,14 +495,14 @@ class Minitouch(Connection): ) @Config.when(DEVICE_OVER_HTTP=False) - def minitouch_send(self): - content = self.minitouch_builder.to_minitouch() + def minitouch_send(self, builder: CommandBuilder): + content = builder.to_minitouch() # logger.info("send operation: {}".format(content.replace("\n", "\\n"))) byte_content = content.encode('utf-8') self._minitouch_client.sendall(byte_content) self._minitouch_client.recv(0) - time.sleep(self.minitouch_builder.delay / 1000 + self.minitouch_builder.DEFAULT_DELAY) - self.minitouch_builder.clear() + time.sleep(self.minitouch_builder.delay / 1000 + builder.DEFAULT_DELAY) + builder.clear() @cached_property def _minitouch_loop(self): @@ -514,8 +563,8 @@ class Minitouch(Connection): self._minitouch_ws = self._minitouch_loop_run(connect()) @Config.when(DEVICE_OVER_HTTP=True) - def minitouch_send(self): - content = self.minitouch_builder.to_atx_agent() + def minitouch_send(self, builder: CommandBuilder): + content = builder.to_atx_agent() async def send(): for row in content: @@ -523,15 +572,15 @@ class Minitouch(Connection): await self._minitouch_ws.send(row) self._minitouch_loop_run(send()) - time.sleep(self.minitouch_builder.delay / 1000 + self.minitouch_builder.DEFAULT_DELAY) - self.minitouch_builder.clear() + time.sleep(builder.delay / 1000 + builder.DEFAULT_DELAY) + builder.clear() @retry def click_minitouch(self, x, y): builder = self.minitouch_builder builder.down(x, y).commit() builder.up().commit() - self.minitouch_send() + builder.send() @retry def long_click_minitouch(self, x, y, duration=1.0): @@ -539,7 +588,7 @@ class Minitouch(Connection): builder = self.minitouch_builder builder.down(x, y).commit().wait(duration) builder.up().commit() - self.minitouch_send() + builder.send() @retry def swipe_minitouch(self, p1, p2): @@ -547,14 +596,14 @@ class Minitouch(Connection): builder = self.minitouch_builder builder.down(*points[0]).commit() - self.minitouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.minitouch_send() + builder.send() builder.up().commit() - self.minitouch_send() + builder.send() @retry def drag_minitouch(self, p1, p2, point_random=(-10, -10, 10, 10)): @@ -564,15 +613,15 @@ class Minitouch(Connection): builder = self.minitouch_builder builder.down(*points[0]).commit() - self.minitouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.minitouch_send() + builder.send() builder.move(*p2).commit().wait(140) builder.move(*p2).commit().wait(140) - self.minitouch_send() + builder.send() builder.up().commit() - self.minitouch_send() + builder.send() diff --git a/module/device/method/nemu_ipc.py b/module/device/method/nemu_ipc.py index 90131962c..b79e1c322 100644 --- a/module/device/method/nemu_ipc.py +++ b/module/device/method/nemu_ipc.py @@ -2,7 +2,7 @@ import asyncio import ctypes import os import sys -from functools import wraps, partial +from functools import partial, wraps import cv2 import numpy as np @@ -147,6 +147,8 @@ class CaptureNemuIpc(CaptureStd): # MuMuVMMSVC.exe died # b'nemu_capture_display rpc error: 1726\r\n' # No idea how to handle yet + if b'error: 1722' in self.stderr or b'error: 1726' in self.stderr: + raise NemuIpcError('Emulator instance is probably dead') def retry(func): @@ -172,7 +174,7 @@ def retry(func): break # Function call timeout except asyncio.TimeoutError: - logger.warning(f'Func {func.__name__}() call timeout, retrying') + logger.warning(f'Func {func.__name__}() call timeout, retrying: {_}') def init(): self.reconnect() @@ -236,11 +238,10 @@ class NemuIpcImpl: if self.connect_id > 0: return - with CaptureNemuIpc(): - connect_id = self.ev_run_sync( - self.lib.nemu_connect, - self.nemu_folder, self.instance_id - ) + connect_id = self.ev_run_sync( + self.lib.nemu_connect, + self.nemu_folder, self.instance_id + ) if connect_id == 0: raise NemuIpcError( 'Connection failed, please check if nemu_folder is correct and emulator is running' @@ -253,11 +254,10 @@ class NemuIpcImpl: if self.connect_id == 0: return - with CaptureNemuIpc(): - self.ev_run_sync( - self.lib.nemu_disconnect, - self.connect_id - ) + self.ev_run_sync( + self.lib.nemu_disconnect, + self.connect_id + ) # logger.info(f'NemuIpc disconnected: {self.connect_id}') self.connect_id = 0 @@ -288,7 +288,9 @@ class NemuIpcImpl: asyncio.TimeoutError: If function call timeout """ func_wrapped = partial(func, *args, **kwargs) - result = await asyncio.wait_for(self._ev.run_in_executor(None, func_wrapped), timeout=0.05) + # Increased timeout for slow PCs + # Default screenshot interval is 0.2s, so a 0.15s timeout would have a fast retry without extra time costs + result = await asyncio.wait_for(self._ev.run_in_executor(None, func_wrapped), timeout=0.15) return result def ev_run_sync(self, func, *args, **kwargs): @@ -300,8 +302,24 @@ class NemuIpcImpl: Raises: asyncio.TimeoutError: If function call timeout + NemuIpcIncompatible: + NemuIpcError """ result = self._ev.run_until_complete(self.ev_run_async(func, *args, **kwargs)) + + err = False + if func.__name__ == 'nemu_connect': + if result == 0: + err = True + else: + if result > 0: + err = True + # Get to actual error message printed in std + if err: + logger.warning(f'Failed to call {func.__name__}, result={result}') + with CaptureNemuIpc(): + result = self._ev.run_until_complete(self.ev_run_async(func, *args, **kwargs)) + return result def get_resolution(self): @@ -315,11 +333,10 @@ class NemuIpcImpl: height_ptr = ctypes.pointer(ctypes.c_int(0)) nullptr = ctypes.POINTER(ctypes.c_int)() - with CaptureNemuIpc(): - ret = self.ev_run_sync( - self.lib.nemu_capture_display, - self.connect_id, self.display_id, 0, width_ptr, height_ptr, nullptr - ) + ret = self.ev_run_sync( + self.lib.nemu_capture_display, + self.connect_id, self.display_id, 0, width_ptr, height_ptr, nullptr + ) if ret > 0: raise NemuIpcError('nemu_capture_display failed during get_resolution()') self.width = width_ptr.contents.value @@ -335,18 +352,17 @@ class NemuIpcImpl: if self.connect_id == 0: self.connect() - with CaptureNemuIpc(): - self.get_resolution() + self.get_resolution() - width_ptr = ctypes.pointer(ctypes.c_int(self.width)) - height_ptr = ctypes.pointer(ctypes.c_int(self.height)) - length = self.width * self.height * 4 - pixels_pointer = ctypes.pointer((ctypes.c_ubyte * length)()) + width_ptr = ctypes.pointer(ctypes.c_int(self.width)) + height_ptr = ctypes.pointer(ctypes.c_int(self.height)) + length = self.width * self.height * 4 + pixels_pointer = ctypes.pointer((ctypes.c_ubyte * length)()) - ret = self.ev_run_sync( - self.lib.nemu_capture_display, - self.connect_id, self.display_id, length, width_ptr, height_ptr, pixels_pointer - ) + ret = self.ev_run_sync( + self.lib.nemu_capture_display, + self.connect_id, self.display_id, length, width_ptr, height_ptr, pixels_pointer + ) if ret > 0: raise NemuIpcError('nemu_capture_display failed during screenshot()') @@ -378,11 +394,10 @@ class NemuIpcImpl: x, y = self.convert_xy(x, y) - with CaptureNemuIpc(): - ret = self.ev_run_sync( - self.lib.nemu_input_event_touch_down, - self.connect_id, self.display_id, x, y - ) + ret = self.ev_run_sync( + self.lib.nemu_input_event_touch_down, + self.connect_id, self.display_id, x, y + ) if ret > 0: raise NemuIpcError('nemu_input_event_touch_down failed') @@ -394,11 +409,10 @@ class NemuIpcImpl: if self.connect_id == 0: self.connect() - with CaptureNemuIpc(): - ret = self.ev_run_sync( - self.lib.nemu_input_event_touch_up, - self.connect_id, self.display_id - ) + ret = self.ev_run_sync( + self.lib.nemu_input_event_touch_up, + self.connect_id, self.display_id + ) if ret > 0: raise NemuIpcError('nemu_input_event_touch_up failed') @@ -448,7 +462,9 @@ class NemuIpc(Platform): # Search emulator instance # with E:\ProgramFiles\MuMuPlayer-12.0\shell\MuMuPlayer.exe # installation path is E:\ProgramFiles\MuMuPlayer-12.0 - _ = self.emulator_instance + if self.emulator_instance is None: + logger.error('Unable to use NemuIpc because emulator instance not found') + raise RequestHumanTakeover try: return NemuIpcImpl( nemu_folder=self.emulator_instance.emulator.abspath('../'), @@ -486,44 +502,40 @@ class NemuIpc(Platform): def click_nemu_ipc(self, x, y): down = ensure_time((0.010, 0.020)) - with CaptureNemuIpc(): - self.nemu_ipc.down(x, y) - self.sleep(down) - self.nemu_ipc.up() - self.sleep(0.050 - down) + self.nemu_ipc.down(x, y) + self.sleep(down) + self.nemu_ipc.up() + self.sleep(0.050 - down) def long_click_nemu_ipc(self, x, y, duration=1.0): - with CaptureNemuIpc(): - self.nemu_ipc.down(x, y) - self.sleep(duration) - self.nemu_ipc.up() - self.sleep(0.050) + self.nemu_ipc.down(x, y) + self.sleep(duration) + self.nemu_ipc.up() + self.sleep(0.050) def swipe_nemu_ipc(self, p1, p2): points = insert_swipe(p0=p1, p3=p2) - with CaptureNemuIpc(): - for point in points: - self.nemu_ipc.down(*point) - self.sleep(0.010) + for point in points: + self.nemu_ipc.down(*point) + self.sleep(0.010) - self.nemu_ipc.up() - self.sleep(0.050) + self.nemu_ipc.up() + self.sleep(0.050) def drag_nemu_ipc(self, p1, p2, point_random=(-10, -10, 10, 10)): p1 = np.array(p1) - random_rectangle_point(point_random) p2 = np.array(p2) - random_rectangle_point(point_random) points = insert_swipe(p0=p1, p3=p2, speed=20) - with CaptureNemuIpc(): - for point in points: - self.nemu_ipc.down(*point) - self.sleep(0.010) + for point in points: + self.nemu_ipc.down(*point) + self.sleep(0.010) - self.nemu_ipc.down(*p2) - self.sleep(0.140) - self.nemu_ipc.down(*p2) - self.sleep(0.140) + self.nemu_ipc.down(*p2) + self.sleep(0.140) + self.nemu_ipc.down(*p2) + self.sleep(0.140) - self.nemu_ipc.up() - self.sleep(0.050) + self.nemu_ipc.up() + self.sleep(0.050) diff --git a/module/device/method/utils.py b/module/device/method/utils.py index 76103999b..4ae0104fe 100644 --- a/module/device/method/utils.py +++ b/module/device/method/utils.py @@ -253,7 +253,7 @@ def remove_suffix(s, suffix): Returns: str, bytes: """ - return s[:len(suffix)] if s.endswith(suffix) else s + return s[:-len(suffix)] if s.endswith(suffix) else s def remove_shell_warning(s): diff --git a/module/device/pkg_resources/__init__.py b/module/device/pkg_resources/__init__.py new file mode 100644 index 000000000..b685a6d9a --- /dev/null +++ b/module/device/pkg_resources/__init__.py @@ -0,0 +1,109 @@ +import os +import re +import sys + +from module.base.decorator import cached_property +from module.logger import logger + +""" +Importing pkg_resources is so slow, like 0.4 ~ 1.0s, just google it you will find it indeed really slow. +Since it was some kind of standard library there is no way to modify it or speed it up. +So here's a poor but fast implementation of pkg_resources returning the things in need. + +To patch: +``` +# Patch pkg_resources before importing adbutils and uiautomator2 +from module.device.pkg_resources import get_distribution +# Just avoid being removed by import optimization +_ = get_distribution +``` +""" +# Inject sys.modules, pretend we have pkg_resources imported +try: + sys.modules['pkg_resources'] = sys.modules['module.device.pkg_resources'] +except KeyError: + logger.error('Patch pkg_resources failed, patch module does not exists') + + +def remove_suffix(s, suffix): + """ + Remove suffix of a string or bytes like `string.removesuffix(suffix)`, which is on Python3.9+ + + Args: + s (str, bytes): + suffix (str, bytes): + + Returns: + str, bytes: + """ + return s[:-len(suffix)] if s.endswith(suffix) else s + + +class FakeDistributionObject: + def __init__(self, dist, version): + self.dist = dist + self.version = version + + def __str__(self): + return f'{self.__class__.__name__}({self.dist}={self.version})' + + __repr__ = __str__ + + +class PackageCache: + @cached_property + def site_packages(self): + # Just whatever library to locate the `site-packages` directory + import requests + path = os.path.abspath(os.path.join(requests.__file__, '../../')) + return path + + @cached_property + def dict_installed_packages(self): + """ + Returns: + dict: Key: str, package name + Value: FakeDistributionObject + """ + dic = {} + for file in os.listdir(self.site_packages): + # mxnet_cu101-1.6.0.dist-info + # adbutils-0.11.0-py3.7.egg-info + res = re.match(r'^([a-zA-Z0-9._]+)-([a-zA-Z0-9._]+)-', file) + if res: + version = remove_suffix(res.group(2), '.dist') + # version = res.group(2) + obj = FakeDistributionObject( + dist=res.group(1), + version=version, + ) + dic[obj.dist] = obj + + return dic + + +PACKAGE_CACHE = PackageCache() + + +def resource_filename(*args): + if args == ("adbutils", "binaries"): + path = os.path.abspath(os.path.join(PACKAGE_CACHE.site_packages, *args)) + return path + + +def get_distribution(dist): + """Return a current distribution object for a Requirement or string""" + if dist == 'adbutils': + return PACKAGE_CACHE.dict_installed_packages.get( + 'adbutils', + FakeDistributionObject('adbutils', '0.11.0'), + ) + if dist == 'uiautomator2': + return PACKAGE_CACHE.dict_installed_packages.get( + 'uiautomator2', + FakeDistributionObject('uiautomator2', '2.16.17'), + ) + + +class DistributionNotFound(Exception): + pass diff --git a/module/device/platform/emulator_base.py b/module/device/platform/emulator_base.py index ecd026ba1..394becc24 100644 --- a/module/device/platform/emulator_base.py +++ b/module/device/platform/emulator_base.py @@ -36,6 +36,21 @@ def get_serial_pair(serial): return None, None +def remove_duplicated_path(paths): + """ + Args: + paths (list[str]): + + Returns: + list[str]: + """ + paths = sorted(set(paths)) + dic = {} + for path in paths: + dic.setdefault(path.lower(), path) + return list(dic.values()) + + @dataclass class EmulatorInstanceBase: # Serial for adb connection @@ -90,7 +105,7 @@ class EmulatorInstanceBase: Returns: int: Instance ID, or None if this is not a MuMu 12 instance """ - res = re.search(r'MuMuPlayer-12.0-(\d+)', self.name) + res = re.search(r'MuMuPlayer(?:Global)?-12.0-(\d+)', self.name) if res: return int(res.group(1)) res = re.search(r'YXArkNights-12.0-(\d+)', self.name) @@ -205,6 +220,14 @@ class EmulatorBase: class EmulatorManagerBase: + @staticmethod + def iter_running_emulator(): + """ + Yields: + str: Path to emulator executables, may contains duplicate values + """ + return + @cached_property def all_emulators(self) -> t.List[EmulatorBase]: """ diff --git a/module/device/platform/emulator_windows.py b/module/device/platform/emulator_windows.py index bb873e690..f7a5e54bc 100644 --- a/module/device/platform/emulator_windows.py +++ b/module/device/platform/emulator_windows.py @@ -8,7 +8,8 @@ from dataclasses import dataclass # module/device/platform/emulator_base.py # module/device/platform/emulator_windows.py # Will be used in Alas Easy Install, they shouldn't import any Alas modules. -from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase +from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase, \ + remove_duplicated_path from module.device.platform.utils import cached_property, iter_folder @@ -321,7 +322,7 @@ class EmulatorManager(EmulatorManagerBase): Get recently executed programs in UserAssist https://github.com/forensicmatt/MonitorUserAssist - Returns: + Yields: str: Path to emulator executables, may contains duplicate values """ path = r'Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist' @@ -452,6 +453,31 @@ class EmulatorManager(EmulatorManagerBase): uninstall = res.group(1) if res else uninstall yield uninstall + @staticmethod + def iter_running_emulator(): + """ + Yields: + str: Path to emulator executables, may contains duplicate values + """ + try: + import psutil + except ModuleNotFoundError: + return + # Since this is a one-time-usage, we access psutil._psplatform.Process directly + # to bypass the call of psutil.Process.is_running(). + # This only costs about 0.017s. + for pid in psutil.pids(): + proc = psutil._psplatform.Process(pid) + try: + exe = proc.cmdline() + exe = exe[0].replace(r'\\', '/').replace('\\', '/') + except (psutil.AccessDenied, IndexError): + # psutil.AccessDenied + continue + + if Emulator.is_emulator(exe): + yield exe + @cached_property def all_emulators(self) -> t.List[Emulator]: """ @@ -479,7 +505,7 @@ class EmulatorManager(EmulatorManagerBase): exe.add(ld) # Uninstall registry - for uninstall in self.iter_uninstall_registry(): + for uninstall in EmulatorManager.iter_uninstall_registry(): # Find emulator executable from uninstaller for file in iter_folder(abspath(os.path.dirname(uninstall)), ext='.exe'): if Emulator.is_emulator(file) and os.path.exists(file): @@ -493,12 +519,14 @@ class EmulatorManager(EmulatorManagerBase): if Emulator.is_emulator(file) and os.path.exists(file): exe.add(file) + # Running + for file in EmulatorManager.iter_running_emulator(): + if os.path.exists(file): + exe.add(file) + + # De-redundancy exe = [Emulator(path).path for path in exe if Emulator.is_emulator(path)] - exe = sorted(set(exe)) - dic = {} - for path in exe: - dic.setdefault(path.lower(), path) - exe = [Emulator(path) for path in dic.values()] + exe = [Emulator(path) for path in remove_duplicated_path(exe)] return exe @cached_property diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index 16313f87d..d4c2dae04 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -5,7 +5,8 @@ from pydantic import BaseModel from module.base.decorator import cached_property, del_cached_property from module.device.connection import Connection -from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase +from module.device.method.utils import get_serial_pair +from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase, remove_duplicated_path from module.logger import logger from module.map.map_grids import SelectedGrids @@ -80,8 +81,14 @@ class PlatformBase(Connection, EmulatorManagerBase): path=data.path, name=data.name, ) + # Redirect emulator-5554 to 127.0.0.1:5555 + serial = self.serial + port_serial, _ = get_serial_pair(self.serial) + if port_serial is not None: + serial = port_serial + instance = self.find_emulator_instance( - serial=str(self.config.Emulator_Serial).strip(), + serial=serial, name=data.name, path=data.path, emulator=data.emulator, @@ -129,7 +136,7 @@ class PlatformBase(Connection, EmulatorManagerBase): # Search by serial select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instance with {search_args}') + logger.warning(f'No emulator instance with {search_args}, serial invalid') return None if select.count == 1: instance = select[0] @@ -142,9 +149,9 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['name'] = name select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, name invalid') + search_args.pop('name') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -155,9 +162,9 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['path'] = path select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, path invalid') + search_args.pop('path') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -168,9 +175,28 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['type'] = emulator select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, type invalid') + search_args.pop('type') + elif select.count == 1: + instance = select[0] + logger.hr('Emulator instance', level=2) + logger.info(f'Found emulator instance: {instance}') + return instance + + # Still too many instances, search from running emulators + running = remove_duplicated_path(list(self.iter_running_emulator())) + logger.info('Running emulators') + for exe in running: + logger.info(exe) + if len(running) == 1: + logger.info('Only one running emulator') + # Same as searching path + search_args['path'] = running[0] + select = instances.select(**search_args) + if select.count == 0: + logger.warning(f'No emulator instances with {search_args}, path invalid') + search_args.pop('path') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') diff --git a/module/device/screenshot.py b/module/device/screenshot.py index 71e2b0cf3..6032dfa63 100644 --- a/module/device/screenshot.py +++ b/module/device/screenshot.py @@ -72,6 +72,10 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy, NemuIpc): return self.image + @property + def has_cached_image(self): + return hasattr(self, 'image') and self.image is not None + def _handle_orientated_image(self, image): """ Args: diff --git a/module/dorm/dorm.py b/module/dorm/dorm.py index 1f7df2e6d..5c76eded7 100644 --- a/module/dorm/dorm.py +++ b/module/dorm/dorm.py @@ -108,12 +108,13 @@ class RewardDorm(UI): # Long tap to feed. This requires minitouch. timeout = Timer(count // 5 + 5).start() x, y = random_rectangle_point(button.button) - self.device.minitouch_builder.down(x, y).commit() - self.device.minitouch_send() + builder = self.device.minitouch_builder + builder.down(x, y).commit() + builder.send() while 1: - self.device.minitouch_builder.move(x, y).commit().wait(10) - self.device.minitouch_send() + builder.move(x, y).commit().wait(10) + builder.send() self.device.screenshot() if not self._dorm_has_food(button) \ @@ -124,19 +125,20 @@ class RewardDorm(UI): logger.warning('Wait dorm feed timeout') break - self.device.minitouch_builder.up().commit() - self.device.minitouch_send() + builder.up().commit() + builder.send() @Config.when(DEVICE_CONTROL_METHOD='MaaTouch') def _dorm_feed_long_tap(self, button, count): timeout = Timer(count // 5 + 5).start() x, y = random_rectangle_point(button.button) - self.device.maatouch_builder.down(x, y).commit() - self.device.maatouch_send() + builder = self.device.maatouch_builder + builder.down(x, y).commit() + builder.send() while 1: - self.device.maatouch_builder.move(x, y).commit().wait(10) - self.device.maatouch_send() + builder.move(x, y).commit().wait(10) + builder.send() self.device.screenshot() if not self._dorm_has_food(button) \ @@ -147,8 +149,8 @@ class RewardDorm(UI): logger.warning('Wait dorm feed timeout') break - self.device.maatouch_builder.up().commit() - self.device.maatouch_send() + builder.up().commit() + builder.send() @Config.when(DEVICE_CONTROL_METHOD='uiautomator2') def _dorm_feed_long_tap(self, button, count): diff --git a/module/equipment/assets.py b/module/equipment/assets.py index f5a567a82..30ebfd803 100644 --- a/module/equipment/assets.py +++ b/module/equipment/assets.py @@ -21,6 +21,10 @@ EQUIP_TAKE_ON_1 = Button(area={'cn': (907, 338, 991, 431), 'en': (907, 338, 991, EQUIP_TAKE_ON_2 = Button(area={'cn': (866, 511, 950, 604), 'en': (866, 511, 950, 604), 'jp': (866, 511, 950, 604), 'tw': (866, 511, 950, 604)}, color={'cn': (117, 118, 127), 'en': (117, 118, 127), 'jp': (117, 118, 127), 'tw': (117, 118, 127)}, button={'cn': (866, 511, 950, 604), 'en': (866, 511, 950, 604), 'jp': (866, 511, 950, 604), 'tw': (866, 511, 950, 604)}, file={'cn': './assets/cn/equipment/EQUIP_TAKE_ON_2.png', 'en': './assets/en/equipment/EQUIP_TAKE_ON_2.png', 'jp': './assets/jp/equipment/EQUIP_TAKE_ON_2.png', 'tw': './assets/tw/equipment/EQUIP_TAKE_ON_2.png'}) EQUIP_TAKE_ON_3 = Button(area={'cn': (129, 252, 213, 345), 'en': (129, 252, 213, 345), 'jp': (129, 252, 213, 345), 'tw': (129, 252, 213, 345)}, color={'cn': (105, 108, 115), 'en': (105, 108, 115), 'jp': (105, 108, 115), 'tw': (105, 108, 115)}, button={'cn': (129, 252, 213, 345), 'en': (129, 252, 213, 345), 'jp': (129, 252, 213, 345), 'tw': (129, 252, 213, 345)}, file={'cn': './assets/cn/equipment/EQUIP_TAKE_ON_3.png', 'en': './assets/en/equipment/EQUIP_TAKE_ON_3.png', 'jp': './assets/jp/equipment/EQUIP_TAKE_ON_3.png', 'tw': './assets/tw/equipment/EQUIP_TAKE_ON_3.png'}) EQUIP_TAKE_ON_4 = Button(area={'cn': (128, 432, 212, 525), 'en': (128, 432, 212, 525), 'jp': (128, 432, 212, 525), 'tw': (128, 432, 212, 525)}, color={'cn': (109, 111, 120), 'en': (109, 111, 120), 'jp': (109, 111, 120), 'tw': (109, 111, 120)}, button={'cn': (128, 432, 212, 525), 'en': (128, 432, 212, 525), 'jp': (128, 432, 212, 525), 'tw': (128, 432, 212, 525)}, file={'cn': './assets/cn/equipment/EQUIP_TAKE_ON_4.png', 'en': './assets/en/equipment/EQUIP_TAKE_ON_4.png', 'jp': './assets/jp/equipment/EQUIP_TAKE_ON_4.png', 'tw': './assets/tw/equipment/EQUIP_TAKE_ON_4.png'}) +FLEET_DETAIL = Button(area={'cn': (906, 644, 1008, 699), 'en': (906, 644, 1008, 699), 'jp': (906, 644, 1008, 699), 'tw': (906, 644, 1008, 699)}, color={'cn': (166, 170, 188), 'en': (166, 170, 188), 'jp': (166, 170, 188), 'tw': (166, 170, 188)}, button={'cn': (906, 644, 1008, 699), 'en': (906, 644, 1008, 699), 'jp': (906, 644, 1008, 699), 'tw': (906, 644, 1008, 699)}, file={'cn': './assets/cn/equipment/FLEET_DETAIL.png', 'en': './assets/en/equipment/FLEET_DETAIL.png', 'jp': './assets/jp/equipment/FLEET_DETAIL.png', 'tw': './assets/tw/equipment/FLEET_DETAIL.png'}) +FLEET_DETAIL_CHECK = Button(area={'cn': (915, 647, 980, 679), 'en': (915, 649, 980, 677), 'jp': (915, 647, 979, 679), 'tw': (915, 647, 980, 680)}, color={'cn': (234, 195, 154), 'en': (236, 200, 160), 'jp': (234, 195, 154), 'tw': (237, 196, 154)}, button={'cn': (915, 647, 980, 679), 'en': (915, 649, 980, 677), 'jp': (915, 647, 979, 679), 'tw': (915, 647, 980, 680)}, file={'cn': './assets/cn/equipment/FLEET_DETAIL_CHECK.png', 'en': './assets/en/equipment/FLEET_DETAIL_CHECK.png', 'jp': './assets/jp/equipment/FLEET_DETAIL_CHECK.png', 'tw': './assets/tw/equipment/FLEET_DETAIL_CHECK.png'}) +FLEET_DETAIL_ENTER = Button(area={'cn': (729, 314, 802, 438), 'en': (729, 314, 802, 438), 'jp': (729, 314, 802, 438), 'tw': (729, 314, 802, 438)}, color={'cn': (154, 149, 147), 'en': (154, 149, 147), 'jp': (154, 149, 147), 'tw': (154, 149, 147)}, button={'cn': (729, 314, 802, 438), 'en': (729, 314, 802, 438), 'jp': (729, 314, 802, 438), 'tw': (729, 314, 802, 438)}, file={'cn': './assets/cn/equipment/FLEET_DETAIL_ENTER.png', 'en': './assets/en/equipment/FLEET_DETAIL_ENTER.png', 'jp': './assets/jp/equipment/FLEET_DETAIL_ENTER.png', 'tw': './assets/tw/equipment/FLEET_DETAIL_ENTER.png'}) +FLEET_DETAIL_ENTER_FLAGSHIP = Button(area={'cn': (103, 327, 176, 437), 'en': (103, 327, 176, 437), 'jp': (103, 327, 176, 437), 'tw': (103, 327, 176, 437)}, color={'cn': (141, 135, 138), 'en': (141, 135, 138), 'jp': (141, 135, 138), 'tw': (141, 135, 138)}, button={'cn': (103, 327, 176, 437), 'en': (103, 327, 176, 437), 'jp': (103, 327, 176, 437), 'tw': (103, 327, 176, 437)}, file={'cn': './assets/cn/equipment/FLEET_DETAIL_ENTER_FLAGSHIP.png', 'en': './assets/en/equipment/FLEET_DETAIL_ENTER_FLAGSHIP.png', 'jp': './assets/jp/equipment/FLEET_DETAIL_ENTER_FLAGSHIP.png', 'tw': './assets/tw/equipment/FLEET_DETAIL_ENTER_FLAGSHIP.png'}) FLEET_ENTER = Button(area={'cn': (502, 474, 517, 489), 'en': (502, 474, 517, 489), 'jp': (502, 474, 517, 489), 'tw': (502, 474, 517, 489)}, color={'cn': (58, 62, 77), 'en': (58, 62, 77), 'jp': (58, 62, 77), 'tw': (58, 62, 77)}, button={'cn': (502, 474, 517, 489), 'en': (502, 474, 517, 489), 'jp': (502, 474, 517, 489), 'tw': (502, 474, 517, 489)}, file={'cn': './assets/cn/equipment/FLEET_ENTER.png', 'en': './assets/en/equipment/FLEET_ENTER.png', 'jp': './assets/jp/equipment/FLEET_ENTER.png', 'tw': './assets/tw/equipment/FLEET_ENTER.png'}) FLEET_ENTER_FLAGSHIP = Button(area={'cn': (577, 306, 604, 321), 'en': (577, 306, 604, 321), 'jp': (577, 306, 604, 321), 'tw': (577, 306, 604, 321)}, color={'cn': (95, 83, 74), 'en': (95, 83, 74), 'jp': (95, 83, 74), 'tw': (95, 83, 74)}, button={'cn': (577, 306, 604, 321), 'en': (577, 306, 604, 321), 'jp': (577, 306, 604, 321), 'tw': (577, 306, 604, 321)}, file={'cn': './assets/cn/equipment/FLEET_ENTER_FLAGSHIP.png', 'en': './assets/en/equipment/FLEET_ENTER_FLAGSHIP.png', 'jp': './assets/jp/equipment/FLEET_ENTER_FLAGSHIP.png', 'tw': './assets/tw/equipment/FLEET_ENTER_FLAGSHIP.png'}) FLEET_NEXT = Button(area={'cn': (1234, 327, 1254, 356), 'en': (1234, 327, 1254, 356), 'jp': (1234, 327, 1254, 356), 'tw': (1234, 327, 1254, 356)}, color={'cn': (72, 93, 125), 'en': (72, 93, 125), 'jp': (72, 93, 125), 'tw': (72, 93, 125)}, button={'cn': (1234, 327, 1254, 356), 'en': (1234, 327, 1254, 356), 'jp': (1234, 327, 1254, 356), 'tw': (1234, 327, 1254, 356)}, file={'cn': './assets/cn/equipment/FLEET_NEXT.png', 'en': './assets/en/equipment/FLEET_NEXT.png', 'jp': './assets/jp/equipment/FLEET_NEXT.png', 'tw': './assets/tw/equipment/FLEET_NEXT.png'}) diff --git a/module/gacha/gacha_reward.py b/module/gacha/gacha_reward.py index 70859ecae..1099a06f4 100644 --- a/module/gacha/gacha_reward.py +++ b/module/gacha/gacha_reward.py @@ -325,6 +325,9 @@ class RewardGacha(GachaUI, GeneralShop, Retirement): buy[0] = self.build_ticket_count # Calculate rolls allowed based on configurations and resources buy[1] = self.gacha_calculate(self.config.Gacha_Amount-self.build_ticket_count, gold_cost, cube_cost) + else: + LogRes(self.config).Cube = self.build_cube_count + self.config.update() # Submit 'buy_count' and execute if capable # Cannot use handle_popup_confirm, this window diff --git a/module/guild/operations.py b/module/guild/operations.py index 6be3bc40a..0df401e7d 100644 --- a/module/guild/operations.py +++ b/module/guild/operations.py @@ -202,7 +202,7 @@ class GuildOperations(GuildBase): p1, p2 = random_rectangle_vector( direction_vector, box=detection_area, random_range=(-50, -50, 50, 50), padding=20) self.device.drag(p1, p2, segments=2, shake=(0, 25), point_random=(0, 0, 0, 0), shake_random=(0, -5, 0, 5)) - self.device.sleep(0.3) + # self.device.sleep(0.3) logger.warning('Failed to find active operation dispatch') return False diff --git a/module/handler/assets.py b/module/handler/assets.py index 0c0ef5cba..567a609f1 100644 --- a/module/handler/assets.py +++ b/module/handler/assets.py @@ -73,6 +73,9 @@ MAP_WALK_OUT_OF_STEP = Button(area={'cn': (654, 312, 704, 335), 'en': (454, 314, MAP_WALK_SPEEDUP = Button(area={'cn': (1025, 406, 1055, 436), 'en': (1025, 406, 1055, 436), 'jp': (1025, 406, 1055, 436), 'tw': (1025, 406, 1055, 436)}, color={'cn': (62, 97, 72), 'en': (62, 97, 72), 'jp': (62, 97, 72), 'tw': (62, 97, 72)}, button={'cn': (1025, 406, 1055, 436), 'en': (1025, 406, 1055, 436), 'jp': (1025, 406, 1055, 436), 'tw': (1025, 406, 1055, 436)}, file={'cn': './assets/cn/handler/MAP_WALK_SPEEDUP.png', 'en': './assets/en/handler/MAP_WALK_SPEEDUP.png', 'jp': './assets/jp/handler/MAP_WALK_SPEEDUP.png', 'tw': './assets/tw/handler/MAP_WALK_SPEEDUP.png'}) MISSION_POPUP_ACK = Button(area={'cn': (432, 493, 543, 533), 'en': (413, 489, 566, 532), 'jp': (410, 482, 574, 539), 'tw': (413, 489, 566, 532)}, color={'cn': (181, 182, 184), 'en': (169, 170, 172), 'jp': (162, 164, 167), 'tw': (169, 170, 172)}, button={'cn': (432, 493, 543, 533), 'en': (413, 489, 566, 532), 'jp': (410, 482, 574, 539), 'tw': (413, 489, 566, 532)}, file={'cn': './assets/cn/handler/MISSION_POPUP_ACK.png', 'en': './assets/en/handler/MISSION_POPUP_ACK.png', 'jp': './assets/jp/handler/MISSION_POPUP_ACK.png', 'tw': './assets/tw/handler/MISSION_POPUP_ACK.png'}) MISSION_POPUP_GO = Button(area={'cn': (719, 493, 861, 534), 'en': (716, 488, 869, 533), 'jp': (711, 482, 874, 539), 'tw': (716, 488, 869, 533)}, color={'cn': (125, 164, 214), 'en': (89, 138, 201), 'jp': (93, 142, 204), 'tw': (89, 138, 201)}, button={'cn': (719, 493, 861, 534), 'en': (716, 488, 869, 533), 'jp': (711, 482, 874, 539), 'tw': (716, 488, 869, 533)}, file={'cn': './assets/cn/handler/MISSION_POPUP_GO.png', 'en': './assets/en/handler/MISSION_POPUP_GO.png', 'jp': './assets/jp/handler/MISSION_POPUP_GO.png', 'tw': './assets/tw/handler/MISSION_POPUP_GO.png'}) +MOB_MOVE_1 = Button(area={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, color={'cn': (118, 120, 127), 'en': (118, 120, 127), 'jp': (118, 120, 127), 'tw': (118, 120, 127)}, button={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, file={'cn': './assets/cn/handler/MOB_MOVE_1.png', 'en': './assets/en/handler/MOB_MOVE_1.png', 'jp': './assets/jp/handler/MOB_MOVE_1.png', 'tw': './assets/tw/handler/MOB_MOVE_1.png'}) +MOB_MOVE_2 = Button(area={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, color={'cn': (119, 121, 128), 'en': (119, 121, 128), 'jp': (119, 121, 128), 'tw': (119, 121, 128)}, button={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, file={'cn': './assets/cn/handler/MOB_MOVE_2.png', 'en': './assets/en/handler/MOB_MOVE_2.png', 'jp': './assets/jp/handler/MOB_MOVE_2.png', 'tw': './assets/tw/handler/MOB_MOVE_2.png'}) +MOB_MOVE_CANCEL = Button(area={'cn': (1162, 646, 1220, 674), 'en': (1162, 646, 1220, 674), 'jp': (1162, 644, 1222, 675), 'tw': (1162, 646, 1220, 674)}, color={'cn': (224, 176, 173), 'en': (224, 176, 173), 'jp': (207, 140, 136), 'tw': (224, 176, 173)}, button={'cn': (1162, 646, 1220, 674), 'en': (1162, 646, 1220, 674), 'jp': (1162, 644, 1222, 675), 'tw': (1162, 646, 1220, 674)}, file={'cn': './assets/cn/handler/MOB_MOVE_CANCEL.png', 'en': './assets/cn/handler/MOB_MOVE_CANCEL.png', 'jp': './assets/jp/handler/MOB_MOVE_CANCEL.png', 'tw': './assets/cn/handler/MOB_MOVE_CANCEL.png'}) MONTHLY_PASS_NOTICE = Button(area={'cn': (554, 505, 726, 561), 'en': (716, 488, 869, 533), 'jp': (554, 505, 726, 561), 'tw': (554, 505, 726, 561)}, color={'cn': (109, 153, 208), 'en': (89, 138, 201), 'jp': (109, 153, 208), 'tw': (109, 153, 208)}, button={'cn': (872, 152, 939, 196), 'en': (863, 173, 929, 217), 'jp': (872, 152, 939, 196), 'tw': (872, 152, 939, 196)}, file={'cn': './assets/cn/handler/MONTHLY_PASS_NOTICE.png', 'en': './assets/en/handler/MONTHLY_PASS_NOTICE.png', 'jp': './assets/cn/handler/MONTHLY_PASS_NOTICE.png', 'tw': './assets/cn/handler/MONTHLY_PASS_NOTICE.png'}) MYSTERY_ITEM = Button(area={'cn': (589, 294, 691, 427), 'en': (589, 294, 691, 427), 'jp': (589, 294, 691, 427), 'tw': (589, 294, 691, 427)}, color={'cn': (144, 127, 83), 'en': (144, 127, 83), 'jp': (144, 127, 83), 'tw': (144, 127, 83)}, button={'cn': (588, 478, 698, 496), 'en': (588, 478, 698, 496), 'jp': (588, 478, 698, 496), 'tw': (588, 478, 698, 496)}, file={'cn': './assets/cn/handler/MYSTERY_ITEM.png', 'en': './assets/en/handler/MYSTERY_ITEM.png', 'jp': './assets/jp/handler/MYSTERY_ITEM.png', 'tw': './assets/tw/handler/MYSTERY_ITEM.png'}) POPUP_CANCEL = Button(area={'cn': (453, 506, 525, 536), 'en': (407, 485, 574, 535), 'jp': (455, 515, 521, 546), 'tw': (454, 495, 525, 526)}, color={'cn': (196, 198, 199), 'en': (168, 169, 171), 'jp': (181, 183, 184), 'tw': (195, 196, 197)}, button={'cn': (453, 506, 525, 536), 'en': (407, 485, 574, 535), 'jp': (455, 515, 521, 546), 'tw': (454, 495, 525, 526)}, file={'cn': './assets/cn/handler/POPUP_CANCEL.png', 'en': './assets/en/handler/POPUP_CANCEL.gif', 'jp': './assets/jp/handler/POPUP_CANCEL.png', 'tw': './assets/tw/handler/POPUP_CANCEL.png'}) diff --git a/module/handler/fast_forward.py b/module/handler/fast_forward.py index 9678ba690..54da7fa90 100644 --- a/module/handler/fast_forward.py +++ b/module/handler/fast_forward.py @@ -99,7 +99,7 @@ class FastForwardHandler(AutoSearchHandler): > 12-1 > 12-2 > 12-3 > 12-4 > 13-1 > 13-2 > 13-3 > 13-4 > 14-1 > 14-2 > 14-3 > 14-4 - > 15-1 > 15-2 + > 15-1 > 15-2 > 15-3 > 15-4 """, 'A1 > A2 > A3', 'B1 > B2 > B3', diff --git a/module/handler/strategy.py b/module/handler/strategy.py index c70a09331..7462a72eb 100644 --- a/module/handler/strategy.py +++ b/module/handler/strategy.py @@ -20,6 +20,8 @@ submarine_view = Switch('Submarine_view', offset=(100, 200)) submarine_view.add_status('on', check_button=SUBMARINE_VIEW_ON) submarine_view.add_status('off', check_button=SUBMARINE_VIEW_OFF) +MOB_MOVE_OFFSET = (120, 120) + class StrategyHandler(InfoHandler): fleet_1_formation_fixed = False @@ -141,7 +143,7 @@ class StrategyHandler(InfoHandler): """ return self.appear(SUBMARINE_MOVE_CONFIRM, offset=(20, 20)) - def strategy_submarine_move_enter(self): + def strategy_submarine_move_enter(self, skip_first_screenshot=True): """ Pages: in: STRATEGY_OPENED, SUBMARINE_MOVE_ENTER @@ -149,15 +151,18 @@ class StrategyHandler(InfoHandler): """ logger.info('Submarine move enter') while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + if self.appear(SUBMARINE_MOVE_ENTER, offset=120, interval=5): self.device.click(SUBMARINE_MOVE_ENTER) if self.appear(SUBMARINE_MOVE_CONFIRM, offset=(20, 20)): break - self.device.screenshot() - - def strategy_submarine_move_confirm(self): + def strategy_submarine_move_confirm(self, skip_first_screenshot=True): """ Pages: in: SUBMARINE_MOVE_CONFIRM @@ -165,6 +170,11 @@ class StrategyHandler(InfoHandler): """ logger.info('Submarine move confirm') while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + if self.appear_then_click(SUBMARINE_MOVE_CONFIRM, offset=(20, 20), interval=5): pass if self.handle_popup_confirm('SUBMARINE_MOVE'): @@ -173,9 +183,7 @@ class StrategyHandler(InfoHandler): if self.appear(SUBMARINE_MOVE_ENTER, offset=120): break - self.device.screenshot() - - def strategy_submarine_move_cancel(self): + def strategy_submarine_move_cancel(self, skip_first_screenshot=True): """ Pages: in: SUBMARINE_MOVE_CONFIRM @@ -183,6 +191,11 @@ class StrategyHandler(InfoHandler): """ logger.info('Submarine move cancel') while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + if self.appear_then_click(SUBMARINE_MOVE_CANCEL, offset=(20, 20), interval=5): pass if self.handle_popup_confirm('SUBMARINE_MOVE'): @@ -191,4 +204,63 @@ class StrategyHandler(InfoHandler): if self.appear(SUBMARINE_MOVE_ENTER, offset=120): break - self.device.screenshot() + def is_in_strategy_mob_move(self): + """ + Returns: + bool: + """ + return self.appear(MOB_MOVE_CANCEL, offset=(20, 20)) + + def strategy_get_mob_move_remain(self): + """ + Pages: + in: STRATEGY_OPENED + out: STRATEGY_OPENED + """ + if self.appear(MOB_MOVE_2, offset=MOB_MOVE_OFFSET): + return 2 + elif self.appear(MOB_MOVE_1, offset=MOB_MOVE_OFFSET): + return 1 + else: + return 0 + + def strategy_mob_move_enter(self, skip_first_screenshot=True): + """ + Pages: + in: STRATEGY_OPENED, MOB_MOVE_1 or MOB_MOVE_2 + out: MOB_MOVE_CANCEL + """ + logger.info('Mob move enter') + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.appear(MOB_MOVE_CANCEL, offset=(20, 20)): + break + + if self.appear_then_click(MOB_MOVE_1, offset=MOB_MOVE_OFFSET, interval=5): + continue + if self.appear_then_click(MOB_MOVE_2, offset=MOB_MOVE_OFFSET, interval=5): + continue + + def strategy_mob_move_cancel(self, skip_first_screenshot=True): + """ + Pages: + in: MOB_MOVE_CANCEL + out: STRATEGY_OPENED, MOB_MOVE_1 or MOB_MOVE_2 + """ + logger.info('Mob move cancel') + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.appear(MOB_MOVE_1, offset=MOB_MOVE_OFFSET) \ + or self.appear(MOB_MOVE_2, offset=MOB_MOVE_OFFSET): + break + + if self.appear_then_click(MOB_MOVE_CANCEL, offset=(20, 20), interval=5): + continue diff --git a/module/map/camera.py b/module/map/camera.py index bb2b6fcbf..2e9660322 100644 --- a/module/map/camera.py +++ b/module/map/camera.py @@ -57,7 +57,8 @@ class Camera(MapOperation): vector = distance * vector vector = -vector self.device.swipe_vector(vector, name=name, box=box, whitelist_area=whitelist, blacklist_area=blacklist) - self.device.sleep(0.3) + # Donno why initial commit have a sleep here + # self.device.sleep(0.3) self.update() return True else: @@ -119,7 +120,8 @@ class Camera(MapOperation): self._view_init() try: if not self.is_in_map() \ - and not self.is_in_strategy_submarine_move(): + and not self.is_in_strategy_submarine_move()\ + and not self.is_in_strategy_mob_move(): logger.warning('Image to detect is not in_map') raise MapDetectionError('Image to detect is not in_map') self.view.load(self.device.image) @@ -194,7 +196,8 @@ class Camera(MapOperation): self.device.click(BACK_ARROW) return False elif not self.is_in_map() \ - and not self.is_in_strategy_submarine_move(): + and not self.is_in_strategy_submarine_move()\ + and not self.is_in_strategy_mob_move(): if self.appear(GAME_TIPS, offset=(20, 20)): logger.warning('Perspective error caused by game tips') self.device.click(GAME_TIPS) diff --git a/module/map/map_base.py b/module/map/map_base.py index 42b81d03b..68dcbb431 100644 --- a/module/map/map_base.py +++ b/module/map/map_base.py @@ -10,6 +10,7 @@ from module.map_detection.grid_info import GridInfo class CampaignMap: def __init__(self, name=None): self.name = name + self.grid_class = GridInfo self.grids = {} self._shape = (0, 0) self._map_data = '' @@ -68,7 +69,7 @@ class CampaignMap: self._shape = node2location(scale.upper()) for y in range(self._shape[1] + 1): for x in range(self._shape[0] + 1): - grid = GridInfo() + grid = self.grid_class() grid.location = (x, y) self.grids[(x, y)] = grid diff --git a/module/map_detection/grid_predictor.py b/module/map_detection/grid_predictor.py index eb15fd58b..fd3c4c5d2 100644 --- a/module/map_detection/grid_predictor.py +++ b/module/map_detection/grid_predictor.py @@ -288,6 +288,10 @@ class GridPredictor: # Detect the orange arrow in submarine movement mode. return self.relative_rgb_count((-0.5, -1, 0.5, 0), color=(231, 138, 49), shape=(60, 60)) > 200 + def predict_mob_move_icon(self): + image = rgb2gray(self.relative_crop(area=(-0.5, -0.5, 0.5, 0.5), shape=(60, 60))) + return TEMPLATE_MOB_MOVE_ICON.match(image) + @cached_property def _image_similar_piece(self): return rgb2gray(self.relative_crop(area=(-0.5, -0.5, 0.5, 0.5), shape=(60, 60))) diff --git a/module/ocr/al_ocr.py b/module/ocr/al_ocr.py index 5f6fee115..e913c42f3 100644 --- a/module/ocr/al_ocr.py +++ b/module/ocr/al_ocr.py @@ -13,14 +13,14 @@ from cnocr.cn_ocr import (check_model_name, data_dir, gen_network, load_module, read_charset) from cnocr.fit.ctc_metrics import CtcMetrics from cnocr.hyperparams.cn_hyperparams import CnHyperparams as Hyperparams +from module.device.pkg_resources import PACKAGE_CACHE def get_mxnet_context(): - import re - import pkg_resources - for pkg in pkg_resources.working_set: - if re.match(r'^mxnet-cu\d+$', pkg.key): - logger.info(f'MXNet gpu package: {pkg.key}=={pkg.version} found, using it') + for dist in PACKAGE_CACHE.dict_installed_packages.values(): + # mxnet_cu101 + if dist.dist.startswith('mxnet_cu'): + logger.info(f'MXNet gpu package: {dist.dist}=={dist.version} found, using it') return 'gpu' return 'cpu' diff --git a/module/os/config.py b/module/os/config.py index 5bc6a0e88..73444fd6b 100644 --- a/module/os/config.py +++ b/module/os/config.py @@ -9,7 +9,7 @@ class OSConfig: MAP_HAS_FLEET_STEP = True IGNORE_LOW_EMOTION_WARN = False - MAP_GRID_CENTER_TOLERANCE = 0.2 + MAP_GRID_CENTER_TOLERANCE = 0.3 MAP_SWIPE_MULTIPLY = (1.174, 1.200) MAP_SWIPE_MULTIPLY_MINITOUCH = (1.135, 1.160) MAP_SWIPE_MULTIPLY_MAATOUCH = (1.102, 1.126) diff --git a/module/os_handler/storage.py b/module/os_handler/storage.py index 91e203dd3..3bd91055e 100644 --- a/module/os_handler/storage.py +++ b/module/os_handler/storage.py @@ -38,6 +38,8 @@ class StorageHandler(GlobeOperation, ZoneManager): # A game bug that AUTO_SEARCH_REWARD from the last cleared zone popups if self.appear_then_click(AUTO_SEARCH_REWARD, offset=(50, 50), interval=3): continue + if self.handle_map_event(): + continue self.handle_info_bar() diff --git a/module/shop/base.py b/module/shop/base.py index 1206bef73..10f20628b 100644 --- a/module/shop/base.py +++ b/module/shop/base.py @@ -15,7 +15,7 @@ from module.ui.ui import UI FILTER_REGEX = re.compile( '^(array|book|box|bulin|cat' '|chip|coin|cube|drill|food' - '|plate|retrofit|pr|dr' + '|plate|retrofit|pr|dr|specializedcore' '|logger|tuning' '|hecombatplan|fragment' '|albacore|bataan|bearn|bluegill|carabiniere|casablanca|contedicavour|dukeofyork' diff --git a/module/shop/clerk.py b/module/shop/clerk.py index 3ab48e7a6..9653febf1 100644 --- a/module/shop/clerk.py +++ b/module/shop/clerk.py @@ -285,11 +285,11 @@ class ShopClerk(ShopBase, Retirement): if self.handle_retirement(): self.interval_reset(BACK_ARROW) continue - if self.handle_info_bar(): + if self.shop_obstruct_handle(): self.interval_reset(BACK_ARROW) success = True continue - if self.shop_obstruct_handle(): + if self.info_bar_count(): self.interval_reset(BACK_ARROW) success = True continue diff --git a/module/template/assets.py b/module/template/assets.py index 3604cee76..f4047fc1c 100644 --- a/module/template/assets.py +++ b/module/template/assets.py @@ -14,6 +14,7 @@ TEMPLATE_ENEMY_BOSS = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_ TEMPLATE_ENEMY_Carrier = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_Carrier.png', 'en': './assets/en/template/TEMPLATE_ENEMY_Carrier.png', 'jp': './assets/jp/template/TEMPLATE_ENEMY_Carrier.png', 'tw': './assets/tw/template/TEMPLATE_ENEMY_Carrier.png'}) TEMPLATE_ENEMY_CarrierInvertedOrthant = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_CarrierInvertedOrthant.png', 'en': './assets/en/template/TEMPLATE_ENEMY_CarrierInvertedOrthant.png', 'jp': './assets/jp/template/TEMPLATE_ENEMY_CarrierInvertedOrthant.png', 'tw': './assets/tw/template/TEMPLATE_ENEMY_CarrierInvertedOrthant.png'}) TEMPLATE_ENEMY_CarrierPurple = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_CarrierPurple.png', 'en': './assets/en/template/TEMPLATE_ENEMY_CarrierPurple.png', 'jp': './assets/jp/template/TEMPLATE_ENEMY_CarrierPurple.png', 'tw': './assets/tw/template/TEMPLATE_ENEMY_CarrierPurple.png'}) +TEMPLATE_ENEMY_CarrierSpecial = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_CarrierSpecial.gif', 'en': './assets/cn/template/TEMPLATE_ENEMY_CarrierSpecial.gif', 'jp': './assets/cn/template/TEMPLATE_ENEMY_CarrierSpecial.gif', 'tw': './assets/cn/template/TEMPLATE_ENEMY_CarrierSpecial.gif'}) TEMPLATE_ENEMY_L = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_L.png', 'en': './assets/en/template/TEMPLATE_ENEMY_L.png', 'jp': './assets/jp/template/TEMPLATE_ENEMY_L.png', 'tw': './assets/tw/template/TEMPLATE_ENEMY_L.png'}) TEMPLATE_ENEMY_Light = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_Light.png', 'en': './assets/en/template/TEMPLATE_ENEMY_Light.png', 'jp': './assets/jp/template/TEMPLATE_ENEMY_Light.png', 'tw': './assets/tw/template/TEMPLATE_ENEMY_Light.png'}) TEMPLATE_ENEMY_LightInvertedOrthant = Template(file={'cn': './assets/cn/template/TEMPLATE_ENEMY_LightInvertedOrthant.png', 'en': './assets/en/template/TEMPLATE_ENEMY_LightInvertedOrthant.png', 'jp': './assets/jp/template/TEMPLATE_ENEMY_LightInvertedOrthant.png', 'tw': './assets/tw/template/TEMPLATE_ENEMY_LightInvertedOrthant.png'}) @@ -30,6 +31,7 @@ TEMPLATE_FORMATION_1 = Template(file={'cn': './assets/cn/template/TEMPLATE_FORMA TEMPLATE_FORMATION_2 = Template(file={'cn': './assets/cn/template/TEMPLATE_FORMATION_2.png', 'en': './assets/en/template/TEMPLATE_FORMATION_2.png', 'jp': './assets/jp/template/TEMPLATE_FORMATION_2.png', 'tw': './assets/tw/template/TEMPLATE_FORMATION_2.png'}) TEMPLATE_FORMATION_3 = Template(file={'cn': './assets/cn/template/TEMPLATE_FORMATION_3.png', 'en': './assets/en/template/TEMPLATE_FORMATION_3.png', 'jp': './assets/jp/template/TEMPLATE_FORMATION_3.png', 'tw': './assets/tw/template/TEMPLATE_FORMATION_3.png'}) TEMPLATE_MAP_WALK_OUT_OF_STEP = Template(file={'cn': './assets/cn/template/TEMPLATE_MAP_WALK_OUT_OF_STEP.png', 'en': './assets/en/template/TEMPLATE_MAP_WALK_OUT_OF_STEP.png', 'jp': './assets/jp/template/TEMPLATE_MAP_WALK_OUT_OF_STEP.png', 'tw': './assets/tw/template/TEMPLATE_MAP_WALK_OUT_OF_STEP.png'}) +TEMPLATE_MOB_MOVE_ICON = Template(file={'cn': './assets/cn/template/TEMPLATE_MOB_MOVE_ICON.png', 'en': './assets/cn/template/TEMPLATE_MOB_MOVE_ICON.png', 'jp': './assets/cn/template/TEMPLATE_MOB_MOVE_ICON.png', 'tw': './assets/cn/template/TEMPLATE_MOB_MOVE_ICON.png'}) TEMPLATE_OPERATIONS_ADD = Template(file={'cn': './assets/cn/template/TEMPLATE_OPERATIONS_ADD.png', 'en': './assets/en/template/TEMPLATE_OPERATIONS_ADD.png', 'jp': './assets/jp/template/TEMPLATE_OPERATIONS_ADD.png', 'tw': './assets/tw/template/TEMPLATE_OPERATIONS_ADD.png'}) TEMPLATE_OPERATIONS_RED_DOT = Template(file={'cn': './assets/cn/template/TEMPLATE_OPERATIONS_RED_DOT.png', 'en': './assets/en/template/TEMPLATE_OPERATIONS_RED_DOT.png', 'jp': './assets/jp/template/TEMPLATE_OPERATIONS_RED_DOT.png', 'tw': './assets/tw/template/TEMPLATE_OPERATIONS_RED_DOT.png'}) TEMPLATE_OS_AllyCargo = Template(file={'cn': './assets/cn/template/TEMPLATE_OS_AllyCargo.png', 'en': './assets/en/template/TEMPLATE_OS_AllyCargo.png', 'jp': './assets/jp/template/TEMPLATE_OS_AllyCargo.png', 'tw': './assets/tw/template/TEMPLATE_OS_AllyCargo.png'}) @@ -49,48 +51,51 @@ TEMPLATE_SIREN_Asanagi = Template(file={'cn': './assets/cn/template/TEMPLATE_SIR TEMPLATE_SIREN_Atlanta = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Atlanta.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Atlanta.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Atlanta.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Atlanta.gif'}) TEMPLATE_SIREN_August = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_August.gif', 'en': './assets/en/template/TEMPLATE_SIREN_August.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_August.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_August.gif'}) TEMPLATE_SIREN_AzusaMiura = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_AzusaMiura.gif', 'en': './assets/en/template/TEMPLATE_SIREN_AzusaMiura.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_AzusaMiura.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_AzusaMiura.gif'}) -TEMPLATE_SIREN_BaltimoreIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_BaltimoreIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_BaltimoreIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_BaltimoreIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_BaltimoreIdol.gif'}) TEMPLATE_SIREN_BB = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_BB.gif', 'en': './assets/en/template/TEMPLATE_SIREN_BB.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_BB.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_BB.gif'}) TEMPLATE_SIREN_BBalchemist = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_BBalchemist.gif', 'en': './assets/en/template/TEMPLATE_SIREN_BBalchemist.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_BBalchemist.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_BBalchemist.gif'}) TEMPLATE_SIREN_BBlightning = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_BBlightning.gif', 'en': './assets/en/template/TEMPLATE_SIREN_BBlightning.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_BBlightning.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_BBlightning.gif'}) TEMPLATE_SIREN_BBpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_BBpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_BBpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_BBpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_BBpurple.gif'}) TEMPLATE_SIREN_BBred = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_BBred.gif', 'en': './assets/en/template/TEMPLATE_SIREN_BBred.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_BBred.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_BBred.gif'}) +TEMPLATE_SIREN_BaltimoreIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_BaltimoreIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_BaltimoreIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_BaltimoreIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_BaltimoreIdol.gif'}) TEMPLATE_SIREN_Bellona = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Bellona.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Bellona.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Bellona.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Bellona.gif'}) TEMPLATE_SIREN_CA = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CA.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CA.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CA.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CA.gif'}) TEMPLATE_SIREN_CAalchemist = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CAalchemist.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CAalchemist.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CAalchemist.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CAalchemist.gif'}) TEMPLATE_SIREN_CAgreen = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CAgreen.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CAgreen.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CAgreen.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CAgreen.gif'}) TEMPLATE_SIREN_CAlightning = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CAlightning.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CAlightning.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CAlightning.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CAlightning.gif'}) TEMPLATE_SIREN_CApurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CApurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CApurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CApurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CApurple.gif'}) -TEMPLATE_SIREN_Carabiniere = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Carabiniere.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Carabiniere.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Carabiniere.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Carabiniere.gif'}) TEMPLATE_SIREN_CAred = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CAred.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CAred.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CAred.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CAred.gif'}) +TEMPLATE_SIREN_CL = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CL.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CL.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CL.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CL.gif'}) +TEMPLATE_SIREN_CLalchemist = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CLalchemist.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CLalchemist.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CLalchemist.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CLalchemist.gif'}) +TEMPLATE_SIREN_CLpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CLpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CLpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CLpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CLpurple.gif'}) +TEMPLATE_SIREN_CV = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CV.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CV.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CV.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CV.gif'}) +TEMPLATE_SIREN_CValchemist = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CValchemist.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CValchemist.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CValchemist.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CValchemist.gif'}) +TEMPLATE_SIREN_CVlightning = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CVlightning.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CVlightning.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CVlightning.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CVlightning.gif'}) +TEMPLATE_SIREN_CVpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CVpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CVpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CVpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CVpurple.gif'}) +TEMPLATE_SIREN_Carabiniere = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Carabiniere.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Carabiniere.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Carabiniere.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Carabiniere.gif'}) TEMPLATE_SIREN_Champagne = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Champagne.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Champagne.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Champagne.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Champagne.gif'}) TEMPLATE_SIREN_ChihayaKisaragi = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_ChihayaKisaragi.gif', 'en': './assets/en/template/TEMPLATE_SIREN_ChihayaKisaragi.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_ChihayaKisaragi.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_ChihayaKisaragi.gif'}) TEMPLATE_SIREN_Chitose = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Chitose.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Chitose.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Chitose.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Chitose.gif'}) TEMPLATE_SIREN_Chiyoda = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Chiyoda.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Chiyoda.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Chiyoda.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Chiyoda.gif'}) TEMPLATE_SIREN_Choukai = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Choukai.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Choukai.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Choukai.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Choukai.gif'}) -TEMPLATE_SIREN_CL = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CL.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CL.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CL.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CL.gif'}) -TEMPLATE_SIREN_CLalchemist = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CLalchemist.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CLalchemist.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CLalchemist.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CLalchemist.gif'}) TEMPLATE_SIREN_ClevelandIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_ClevelandIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_ClevelandIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_ClevelandIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_ClevelandIdol.gif'}) -TEMPLATE_SIREN_CLpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CLpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CLpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CLpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CLpurple.gif'}) TEMPLATE_SIREN_Compiler = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Compiler.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Compiler.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Compiler.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Compiler.gif'}) -TEMPLATE_SIREN_CV = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CV.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CV.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CV.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CV.gif'}) -TEMPLATE_SIREN_CValchemist = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CValchemist.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CValchemist.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CValchemist.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CValchemist.gif'}) -TEMPLATE_SIREN_CVlightning = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CVlightning.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CVlightning.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CVlightning.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CVlightning.gif'}) -TEMPLATE_SIREN_CVpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_CVpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_CVpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_CVpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_CVpurple.gif'}) -TEMPLATE_SIREN_Dace = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dace.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dace.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dace.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dace.gif'}) TEMPLATE_SIREN_DD = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DD.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DD.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DD.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DD.gif'}) TEMPLATE_SIREN_DDalchemist = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DDalchemist.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DDalchemist.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DDalchemist.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DDalchemist.gif'}) TEMPLATE_SIREN_DDpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DDpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DDpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DDpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DDpurple.gif'}) +TEMPLATE_SIREN_Dace = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dace.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dace.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dace.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dace.gif'}) TEMPLATE_SIREN_Deutschland = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Deutschland.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Deutschland.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Deutschland.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Deutschland.gif'}) TEMPLATE_SIREN_Dewey = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dewey.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dewey.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dewey.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dewey.gif'}) TEMPLATE_SIREN_DidoIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DidoIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DidoIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DidoIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DidoIdol.gif'}) +TEMPLATE_SIREN_DidoIdol2 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DidoIdol2.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DidoIdol2.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DidoIdol2.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DidoIdol2.gif'}) TEMPLATE_SIREN_Dilloy = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dilloy.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dilloy.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dilloy.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dilloy.gif'}) TEMPLATE_SIREN_DogPink = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DogPink.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DogPink.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DogPink.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DogPink.gif'}) TEMPLATE_SIREN_Dorsetshire = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dorsetshire.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dorsetshire.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dorsetshire.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dorsetshire.gif'}) TEMPLATE_SIREN_DukeOfYork = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DukeOfYork.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DukeOfYork.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DukeOfYork.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DukeOfYork.gif'}) TEMPLATE_SIREN_ELpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_ELpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_ELpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_ELpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_ELpurple.gif'}) +TEMPLATE_SIREN_Elizabeth3 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Elizabeth3.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Elizabeth3.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Elizabeth3.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Elizabeth3.gif'}) TEMPLATE_SIREN_Formidable = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Formidable.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Formidable.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Formidable.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Formidable.gif'}) TEMPLATE_SIREN_Gascogne = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Gascogne.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Gascogne.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Gascogne.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Gascogne.gif'}) +TEMPLATE_SIREN_GascogneIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_GascogneIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_GascogneIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_GascogneIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_GascogneIdol.gif'}) TEMPLATE_SIREN_Gloucester = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Gloucester.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Gloucester.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Gloucester.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Gloucester.gif'}) TEMPLATE_SIREN_Gneisenau = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Gneisenau.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Gneisenau.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Gneisenau.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Gneisenau.gif'}) TEMPLATE_SIREN_GrafZeppelin = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_GrafZeppelin.gif', 'en': './assets/en/template/TEMPLATE_SIREN_GrafZeppelin.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_GrafZeppelin.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_GrafZeppelin.gif'}) @@ -128,10 +133,11 @@ TEMPLATE_SIREN_Kinu = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_ TEMPLATE_SIREN_Kirishima = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Kirishima.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Kirishima.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Kirishima.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Kirishima.gif'}) TEMPLATE_SIREN_Kongo = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Kongo.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Kongo.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Kongo.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Kongo.gif'}) TEMPLATE_SIREN_LaGalissonniere = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LaGalissonniere.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LaGalissonniere.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LaGalissonniere.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LaGalissonniere.gif'}) -TEMPLATE_SIREN_Leipzig = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Leipzig.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Leipzig.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Leipzig.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Leipzig.gif'}) +TEMPLATE_SIREN_Laffey6 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Laffey6.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Laffey6.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Laffey6.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Laffey6.gif'}) TEMPLATE_SIREN_LeMalinIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LeMalinIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LeMalinIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LeMalinIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LeMalinIdol.gif'}) TEMPLATE_SIREN_LeMars = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LeMars.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LeMars.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LeMars.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LeMars.gif'}) TEMPLATE_SIREN_LeMars_ghost = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LeMars_ghost.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LeMars_ghost.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LeMars_ghost.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LeMars_ghost.gif'}) +TEMPLATE_SIREN_Leipzig = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Leipzig.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Leipzig.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Leipzig.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Leipzig.gif'}) TEMPLATE_SIREN_Lexington = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Lexington.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Lexington.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Lexington.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Lexington.gif'}) TEMPLATE_SIREN_Littorio = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Littorio.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Littorio.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Littorio.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Littorio.gif'}) TEMPLATE_SIREN_Lover = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Lover.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Lover.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Lover.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Lover.gif'}) @@ -160,31 +166,32 @@ TEMPLATE_SIREN_Revenge = Template(file={'cn': './assets/cn/template/TEMPLATE_SIR TEMPLATE_SIREN_Rodney = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Rodney.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Rodney.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Rodney.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Rodney.gif'}) TEMPLATE_SIREN_Roon = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Roon.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Roon.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Roon.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Roon.gif'}) TEMPLATE_SIREN_RoonIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_RoonIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_RoonIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_RoonIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_RoonIdol.gif'}) +TEMPLATE_SIREN_RoonIdol2 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_RoonIdol2.gif', 'en': './assets/en/template/TEMPLATE_SIREN_RoonIdol2.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_RoonIdol2.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_RoonIdol2.gif'}) TEMPLATE_SIREN_Ryuuhou = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Ryuuhou.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Ryuuhou.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Ryuuhou.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Ryuuhou.gif'}) -TEMPLATE_SIREN_Sakawa = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sakawa.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sakawa.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sakawa.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sakawa.gif'}) -TEMPLATE_SIREN_SanDiego = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SanDiego.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SanDiego.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SanDiego.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SanDiego.gif'}) -TEMPLATE_SIREN_Scharnhorst = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Scharnhorst.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Scharnhorst.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Scharnhorst.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Scharnhorst.gif'}) -TEMPLATE_SIREN_Sheffield = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sheffield.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sheffield.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sheffield.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sheffield.gif'}) -TEMPLATE_SIREN_SheffieldIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.png', 'en': './assets/en/template/TEMPLATE_SIREN_SheffieldIdol.png', 'jp': './assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.png', 'tw': './assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.png'}) -TEMPLATE_SIREN_Shokaku = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Shokaku.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Shokaku.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Shokaku.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Shokaku.gif'}) -TEMPLATE_SIREN_shuguangjixie_huixing = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif', 'en': './assets/en/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif'}) -TEMPLATE_SIREN_shuguangjixie_shanliu = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif', 'en': './assets/en/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif'}) -TEMPLATE_SIREN_Sirenboss10 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sirenboss10.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sirenboss10.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sirenboss10.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sirenboss10.gif'}) -TEMPLATE_SIREN_SirenBoss15 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss15.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss15.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss15.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss15.gif'}) -TEMPLATE_SIREN_SirenBoss16 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss16.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss16.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss16.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss16.gif'}) -TEMPLATE_SIREN_SirenBoss18 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss18.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss18.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss18.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss18.gif'}) -TEMPLATE_SIREN_SirenBoss19 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss19.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss19.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss19.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss19.gif'}) TEMPLATE_SIREN_SK_BB = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SK_BB.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SK_BB.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SK_BB.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SK_BB.gif'}) TEMPLATE_SIREN_SK_CA = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SK_CA.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SK_CA.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SK_CA.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SK_CA.gif'}) TEMPLATE_SIREN_SK_CL = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SK_CL.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SK_CL.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SK_CL.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SK_CL.gif'}) TEMPLATE_SIREN_SK_CV = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SK_CV.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SK_CV.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SK_CV.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SK_CV.gif'}) TEMPLATE_SIREN_SK_DD = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SK_DD.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SK_DD.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SK_DD.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SK_DD.gif'}) +TEMPLATE_SIREN_SS = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SS.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SS.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SS.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SS.gif'}) +TEMPLATE_SIREN_Sakawa = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sakawa.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sakawa.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sakawa.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sakawa.gif'}) +TEMPLATE_SIREN_SanDiego = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SanDiego.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SanDiego.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SanDiego.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SanDiego.gif'}) +TEMPLATE_SIREN_Scharnhorst = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Scharnhorst.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Scharnhorst.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Scharnhorst.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Scharnhorst.gif'}) +TEMPLATE_SIREN_Sheffield = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sheffield.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sheffield.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sheffield.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sheffield.gif'}) +TEMPLATE_SIREN_SheffieldIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SheffieldIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SheffieldIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SheffieldIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SheffieldIdol.gif'}) +TEMPLATE_SIREN_Shokaku = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Shokaku.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Shokaku.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Shokaku.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Shokaku.gif'}) +TEMPLATE_SIREN_SirenBoss15 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss15.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss15.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss15.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss15.gif'}) +TEMPLATE_SIREN_SirenBoss16 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss16.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss16.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss16.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss16.gif'}) +TEMPLATE_SIREN_SirenBoss18 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss18.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss18.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss18.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss18.gif'}) +TEMPLATE_SIREN_SirenBoss19 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss19.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss19.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss19.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss19.gif'}) +TEMPLATE_SIREN_Sirenboss10 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sirenboss10.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sirenboss10.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sirenboss10.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sirenboss10.gif'}) TEMPLATE_SIREN_Soobrazitelny = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Soobrazitelny.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Soobrazitelny.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Soobrazitelny.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Soobrazitelny.gif'}) TEMPLATE_SIREN_Spee = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Spee.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Spee.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Spee.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Spee.gif'}) TEMPLATE_SIREN_SpeeIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SpeeIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SpeeIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SpeeIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SpeeIdol.gif'}) -TEMPLATE_SIREN_SS = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SS.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SS.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SS.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SS.gif'}) TEMPLATE_SIREN_Suzutsuki = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Suzutsuki.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Suzutsuki.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Suzutsuki.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Suzutsuki.gif'}) TEMPLATE_SIREN_Swordfish = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Swordfish.png', 'en': './assets/en/template/TEMPLATE_SIREN_Swordfish.png', 'jp': './assets/jp/template/TEMPLATE_SIREN_Swordfish.png', 'tw': './assets/tw/template/TEMPLATE_SIREN_Swordfish.png'}) +TEMPLATE_SIREN_TaihouIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_TaihouIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_TaihouIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_TaihouIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_TaihouIdol.gif'}) +TEMPLATE_SIREN_TashkentIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_TashkentIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_TashkentIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_TashkentIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_TashkentIdol.gif'}) TEMPLATE_SIREN_Tirpitz = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Tirpitz.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Tirpitz.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Tirpitz.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Tirpitz.gif'}) TEMPLATE_SIREN_Trento = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Trento.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Trento.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Trento.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Trento.gif'}) TEMPLATE_SIREN_U101 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_U101.gif', 'en': './assets/en/template/TEMPLATE_SIREN_U101.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_U101.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_U101.gif'}) @@ -205,10 +212,13 @@ TEMPLATE_SIREN_Yuudachi = Template(file={'cn': './assets/cn/template/TEMPLATE_SI TEMPLATE_SIREN_Z18 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z18.png', 'en': './assets/en/template/TEMPLATE_SIREN_Z18.png', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z18.png', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z18.png'}) TEMPLATE_SIREN_Z19 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z19.png', 'en': './assets/en/template/TEMPLATE_SIREN_Z19.png', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z19.png', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z19.png'}) TEMPLATE_SIREN_Z2 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z2.png', 'en': './assets/en/template/TEMPLATE_SIREN_Z2.png', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z2.png', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z2.png'}) +TEMPLATE_SIREN_Z23_5 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z23_5.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Z23_5.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z23_5.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z23_5.gif'}) TEMPLATE_SIREN_Z24 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z24.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Z24.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z24.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z24.gif'}) TEMPLATE_SIREN_Z46 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z46.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Z46.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z46.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z46.gif'}) TEMPLATE_SIREN_Zuiho = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Zuiho.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Zuiho.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Zuiho.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Zuiho.gif'}) TEMPLATE_SIREN_Zuikaku = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Zuikaku.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Zuikaku.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Zuikaku.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Zuikaku.gif'}) +TEMPLATE_SIREN_shuguangjixie_huixing = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif', 'en': './assets/en/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_shuguangjixie_huixing.gif'}) +TEMPLATE_SIREN_shuguangjixie_shanliu = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif', 'en': './assets/en/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_shuguangjixie_shanliu.gif'}) TEMPLATE_STAGE_BLUE_CLEAR = Template(file={'cn': './assets/cn/template/TEMPLATE_STAGE_BLUE_CLEAR.png', 'en': './assets/en/template/TEMPLATE_STAGE_BLUE_CLEAR.png', 'jp': './assets/jp/template/TEMPLATE_STAGE_BLUE_CLEAR.png', 'tw': './assets/tw/template/TEMPLATE_STAGE_BLUE_CLEAR.png'}) TEMPLATE_STAGE_BLUE_PERCENT = Template(file={'cn': './assets/cn/template/TEMPLATE_STAGE_BLUE_PERCENT.png', 'en': './assets/en/template/TEMPLATE_STAGE_BLUE_PERCENT.png', 'jp': './assets/jp/template/TEMPLATE_STAGE_BLUE_PERCENT.png', 'tw': './assets/tw/template/TEMPLATE_STAGE_BLUE_PERCENT.png'}) TEMPLATE_STAGE_CLEAR = Template(file={'cn': './assets/cn/template/TEMPLATE_STAGE_CLEAR.png', 'en': './assets/en/template/TEMPLATE_STAGE_CLEAR.png', 'jp': './assets/jp/template/TEMPLATE_STAGE_CLEAR.png', 'tw': './assets/tw/template/TEMPLATE_STAGE_CLEAR.png'}) diff --git a/module/ui/page.py b/module/ui/page.py index d07ad0f1e..efc9c0c87 100644 --- a/module/ui/page.py +++ b/module/ui/page.py @@ -3,8 +3,6 @@ import traceback from module.ui.assets import * from module.raid.assets import * -MAIN_CHECK = MAIN_GOTO_CAMPAIGN - class Page: # Key: str, page name like "page_main" @@ -71,6 +69,11 @@ class Page: self.links[destination] = button +""" +Define UI pages +""" +# Use MAIN_GOTO_FLEET instead of MAIN_GOTO_CAMPAIGN for faster switches with info_bar +MAIN_CHECK = MAIN_GOTO_FLEET # Main page_main = Page(MAIN_CHECK) page_campaign_menu = Page(CAMPAIGN_MENU_CHECK) diff --git a/module/ui/ui.py b/module/ui/ui.py index 10f8fadc6..1e2f3560b 100644 --- a/module/ui/ui.py +++ b/module/ui/ui.py @@ -146,7 +146,7 @@ class UI(InfoHandler): while 1: if skip_first_screenshot: skip_first_screenshot = False - if not hasattr(self.device, "image") or self.device.image is None: + if not self.device.has_cached_image: self.device.screenshot() else: self.device.screenshot() diff --git a/module/webui/config.py b/module/webui/config.py index fd4d6ef12..0df5f0527 100644 --- a/module/webui/config.py +++ b/module/webui/config.py @@ -37,6 +37,8 @@ class DeployConfig(_DeployConfig): if hasattr(self, key): super().__setattr__(key, value) + self.config_redirect() + def write(self): """ Write `self.config` into deploy config. diff --git a/requirements-in.txt b/requirements-in.txt index 6146e9795..fd69eec2e 100644 --- a/requirements-in.txt +++ b/requirements-in.txt @@ -1,32 +1,44 @@ -adbutils==0.11.0 -aiofiles -alas-webapp==0.3.7 -anyio==1.3.1 -av==10.0.0 -cnocr==1.2.2 -imageio==2.27.0 -inflection -jellyfish==0.11.2 -lz4 -mxnet==1.6.0 +# Image processing numpy==1.16.6 -onepush -opencv-python -pillow -prettytable==2.2.1 -psutil==5.9.3 -pydantic -pypresence==4.2.1 -pywebio==1.6.2 -pyyaml -pyzmq==22.3.0 -retrying -rich==11.2.0 scipy==1.4.1 -starlette==0.14.2 -tqdm +pillow +opencv-python +imageio==2.27.0 + +# Device connection +adbutils==0.11.0 uiautomator2==2.16.17 uiautomator2cache==0.3.0.1 -uvicorn[standard]==0.17.6 wrapt==1.13.1 -zerorpc==0.6.3 \ No newline at end of file +retrying +lz4 +av==10.0.0 +psutil==5.9.3 + +# Utils +rich==11.2.0 +tqdm +jellyfish==0.11.2 +pyyaml +inflection +pydantic +aiofiles +prettytable==2.2.1 +anyio==1.3.1 + +# Pushing +onepush==1.3.0 +pycryptodome==3.9.9 +pypresence==4.2.1 + +# Ocr +cnocr==1.2.2 +mxnet==1.6.0 + +# Webui +pywebio==1.6.2 +starlette==0.14.2 +uvicorn[standard]==0.17.6 +alas-webapp==0.3.7 +zerorpc==0.6.3 +pyzmq==22.3.0 diff --git a/requirements.txt b/requirements.txt index d26b1145e..87d8e66de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ matplotlib==3.4.3 # via gluoncv msgpack==1.0.3 # via zerorpc mxnet==1.6.0 # via -r requirements-in.txt, cnocr numpy==1.16.6 # via -r requirements-in.txt, cnocr, gluoncv, imageio, matplotlib, mxnet, opencv-python, scipy -onepush==1.2.0 # via -r requirements-in.txt +onepush==1.3.0 # via -r requirements-in.txt opencv-python==4.5.3.56 # via -r requirements-in.txt packaging==20.9 # via deprecation, uiautomator2 pillow==8.3.2 # via -r requirements-in.txt, cnocr, gluoncv, imageio, matplotlib, uiautomator2 @@ -57,6 +57,7 @@ progress==1.6 # via uiautomator2 psutil==5.9.3 # via -r requirements-in.txt py==1.10.0 # via retry pycparser==2.21 # via cffi +pycryptodome==3.9.9 # via onepush pydantic==1.10.2 # via -r requirements-in.txt pyelftools==0.27 # via apkutils2 pygments==2.12.0 # via rich