diff --git a/assets/cn/campaign/SWITCH_2_EX.png b/assets/cn/campaign/SWITCH_2_EX.png index 2b341509c..e98776244 100644 Binary files a/assets/cn/campaign/SWITCH_2_EX.png and b/assets/cn/campaign/SWITCH_2_EX.png differ diff --git a/assets/cn/dorm/DORM_MANAGE.png b/assets/cn/dorm/DORM_MANAGE.png new file mode 100644 index 000000000..a15de5da8 Binary files /dev/null and b/assets/cn/dorm/DORM_MANAGE.png differ diff --git a/assets/cn/handler/FORMATION_1.BUTTON.png b/assets/cn/handler/FORMATION_1.BUTTON.png new file mode 100644 index 000000000..621645902 Binary files /dev/null and b/assets/cn/handler/FORMATION_1.BUTTON.png differ diff --git a/assets/cn/handler/FORMATION_1.png b/assets/cn/handler/FORMATION_1.png index 621645902..309290142 100644 Binary files a/assets/cn/handler/FORMATION_1.png and b/assets/cn/handler/FORMATION_1.png differ diff --git a/assets/cn/handler/FORMATION_2.BUTTON.png b/assets/cn/handler/FORMATION_2.BUTTON.png new file mode 100644 index 000000000..bae9a30ca Binary files /dev/null and b/assets/cn/handler/FORMATION_2.BUTTON.png differ diff --git a/assets/cn/handler/FORMATION_2.png b/assets/cn/handler/FORMATION_2.png index bae9a30ca..51effe6c2 100644 Binary files a/assets/cn/handler/FORMATION_2.png and b/assets/cn/handler/FORMATION_2.png differ diff --git a/assets/cn/handler/FORMATION_3.BUTTON.png b/assets/cn/handler/FORMATION_3.BUTTON.png new file mode 100644 index 000000000..c43781a06 Binary files /dev/null and b/assets/cn/handler/FORMATION_3.BUTTON.png differ diff --git a/assets/cn/handler/FORMATION_3.png b/assets/cn/handler/FORMATION_3.png index c43781a06..abce4c396 100644 Binary files a/assets/cn/handler/FORMATION_3.png and b/assets/cn/handler/FORMATION_3.png differ diff --git a/assets/cn/handler/LOGIN_ANNOUNCE_2.BUTTON.png b/assets/cn/handler/LOGIN_ANNOUNCE_2.BUTTON.png new file mode 100644 index 000000000..4ec0d2990 Binary files /dev/null and b/assets/cn/handler/LOGIN_ANNOUNCE_2.BUTTON.png differ diff --git a/assets/cn/handler/LOGIN_ANNOUNCE_2.png b/assets/cn/handler/LOGIN_ANNOUNCE_2.png new file mode 100644 index 000000000..f13407e9a Binary files /dev/null and b/assets/cn/handler/LOGIN_ANNOUNCE_2.png differ diff --git a/assets/cn/handler/MISSION_POPUP_ACK.png b/assets/cn/handler/MISSION_POPUP_ACK.png index 86e66b428..d9c4038fd 100644 Binary files a/assets/cn/handler/MISSION_POPUP_ACK.png and b/assets/cn/handler/MISSION_POPUP_ACK.png differ diff --git a/assets/cn/handler/MISSION_POPUP_GO.png b/assets/cn/handler/MISSION_POPUP_GO.png index ac6ca0491..fff9a0874 100644 Binary files a/assets/cn/handler/MISSION_POPUP_GO.png and b/assets/cn/handler/MISSION_POPUP_GO.png differ diff --git a/assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png b/assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png new file mode 100644 index 000000000..e2d954d86 Binary files /dev/null and b/assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png differ diff --git a/assets/cn/raid/RPG_GOTO_STAGE.png b/assets/cn/raid/RPG_GOTO_STAGE.png new file mode 100644 index 000000000..44219b003 Binary files /dev/null and b/assets/cn/raid/RPG_GOTO_STAGE.png differ diff --git a/assets/cn/raid/RPG_GOTO_STORY.png b/assets/cn/raid/RPG_GOTO_STORY.png new file mode 100644 index 000000000..d7dd1a9b0 Binary files /dev/null and b/assets/cn/raid/RPG_GOTO_STORY.png differ diff --git a/assets/cn/raid/RPG_HOME.png b/assets/cn/raid/RPG_HOME.png new file mode 100644 index 000000000..8d84d8d24 Binary files /dev/null and b/assets/cn/raid/RPG_HOME.png differ diff --git a/assets/cn/raid/RPG_LEAVE_CITY.png b/assets/cn/raid/RPG_LEAVE_CITY.png new file mode 100644 index 000000000..7a434cef5 Binary files /dev/null and b/assets/cn/raid/RPG_LEAVE_CITY.png differ diff --git a/assets/cn/raid/RPG_RAID_EASY.png b/assets/cn/raid/RPG_RAID_EASY.png new file mode 100644 index 000000000..8b69b36bb Binary files /dev/null and b/assets/cn/raid/RPG_RAID_EASY.png differ diff --git a/assets/cn/raid/RPG_RAID_EX.png b/assets/cn/raid/RPG_RAID_EX.png new file mode 100644 index 000000000..b16b3f0f0 Binary files /dev/null and b/assets/cn/raid/RPG_RAID_EX.png differ diff --git a/assets/cn/raid/RPG_RAID_HARD.png b/assets/cn/raid/RPG_RAID_HARD.png new file mode 100644 index 000000000..28cba84ca Binary files /dev/null and b/assets/cn/raid/RPG_RAID_HARD.png differ diff --git a/assets/cn/raid/RPG_RAID_NORMAL.png b/assets/cn/raid/RPG_RAID_NORMAL.png new file mode 100644 index 000000000..216fcc9ee Binary files /dev/null and b/assets/cn/raid/RPG_RAID_NORMAL.png differ diff --git a/assets/cn/raid/RPG_STATUS_POPUP.png b/assets/cn/raid/RPG_STATUS_POPUP.png new file mode 100644 index 000000000..8dd75492d Binary files /dev/null and b/assets/cn/raid/RPG_STATUS_POPUP.png differ diff --git a/assets/cn/retire/TEMPLATE_AULICK.png b/assets/cn/retire/TEMPLATE_AULICK.png new file mode 100644 index 000000000..eb7f3f25c Binary files /dev/null and b/assets/cn/retire/TEMPLATE_AULICK.png differ diff --git a/assets/cn/retire/TEMPLATE_BOGUE.png b/assets/cn/retire/TEMPLATE_BOGUE.png index fd2125c99..f6fb84545 100644 Binary files a/assets/cn/retire/TEMPLATE_BOGUE.png and b/assets/cn/retire/TEMPLATE_BOGUE.png differ diff --git a/assets/cn/retire/TEMPLATE_CASSIN_1.png b/assets/cn/retire/TEMPLATE_CASSIN_1.png new file mode 100644 index 000000000..e1399ebe3 Binary files /dev/null and b/assets/cn/retire/TEMPLATE_CASSIN_1.png differ diff --git a/assets/cn/retire/TEMPLATE_CASSIN_2.png b/assets/cn/retire/TEMPLATE_CASSIN_2.png new file mode 100644 index 000000000..cffb88b89 Binary files /dev/null and b/assets/cn/retire/TEMPLATE_CASSIN_2.png differ diff --git a/assets/cn/retire/TEMPLATE_DOWNES_1.png b/assets/cn/retire/TEMPLATE_DOWNES_1.png new file mode 100644 index 000000000..9effcd06d Binary files /dev/null and b/assets/cn/retire/TEMPLATE_DOWNES_1.png differ diff --git a/assets/cn/retire/TEMPLATE_DOWNES_2.png b/assets/cn/retire/TEMPLATE_DOWNES_2.png new file mode 100644 index 000000000..61c58826c Binary files /dev/null and b/assets/cn/retire/TEMPLATE_DOWNES_2.png differ diff --git a/assets/cn/retire/TEMPLATE_FOOTE.png b/assets/cn/retire/TEMPLATE_FOOTE.png new file mode 100644 index 000000000..241f9a75b Binary files /dev/null and b/assets/cn/retire/TEMPLATE_FOOTE.png differ diff --git a/assets/cn/retire/TEMPLATE_HERMES.png b/assets/cn/retire/TEMPLATE_HERMES.png index a09399356..7b20af71d 100644 Binary files a/assets/cn/retire/TEMPLATE_HERMES.png and b/assets/cn/retire/TEMPLATE_HERMES.png differ diff --git a/assets/cn/retire/TEMPLATE_LANGLEY.png b/assets/cn/retire/TEMPLATE_LANGLEY.png index 8fb671a16..5440eaa72 100644 Binary files a/assets/cn/retire/TEMPLATE_LANGLEY.png and b/assets/cn/retire/TEMPLATE_LANGLEY.png differ diff --git a/assets/cn/retire/TEMPLATE_RANGER.png b/assets/cn/retire/TEMPLATE_RANGER.png index 4b77c19c8..9e75ea056 100644 Binary files a/assets/cn/retire/TEMPLATE_RANGER.png and b/assets/cn/retire/TEMPLATE_RANGER.png differ diff --git a/assets/cn/template/TEMPLATE_STAGE_CLEAR_SMALL.png b/assets/cn/template/TEMPLATE_STAGE_CLEAR_SMALL.png new file mode 100644 index 000000000..22e0c2bda Binary files /dev/null and b/assets/cn/template/TEMPLATE_STAGE_CLEAR_SMALL.png differ diff --git a/assets/cn/war_archives/TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD.png b/assets/cn/war_archives/TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD.png new file mode 100644 index 000000000..59e24d63c Binary files /dev/null and b/assets/cn/war_archives/TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD.png differ diff --git a/assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png b/assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png new file mode 100644 index 000000000..0a3a9b317 Binary files /dev/null and b/assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png differ diff --git a/assets/cn/war_archives/TEMPLATE_THE_SOLOMON_RANGER.png b/assets/cn/war_archives/TEMPLATE_THE_SOLOMON_RANGER.png new file mode 100644 index 000000000..3ca7b4821 Binary files /dev/null and b/assets/cn/war_archives/TEMPLATE_THE_SOLOMON_RANGER.png differ diff --git a/assets/cn/war_archives/TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT.png b/assets/cn/war_archives/TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT.png new file mode 100644 index 000000000..d68c3e1bd Binary files /dev/null and b/assets/cn/war_archives/TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT.png differ diff --git a/assets/en/combat/BATTLE_PREPARATION.gif b/assets/en/combat/BATTLE_PREPARATION.gif index dc0e69021..d368984ca 100644 Binary files a/assets/en/combat/BATTLE_PREPARATION.gif and b/assets/en/combat/BATTLE_PREPARATION.gif differ diff --git a/assets/en/dorm/DORM_FEED_ENTER.png b/assets/en/dorm/DORM_FEED_ENTER.png index b3c12eb9d..ea99ee495 100644 Binary files a/assets/en/dorm/DORM_FEED_ENTER.png and b/assets/en/dorm/DORM_FEED_ENTER.png differ diff --git a/assets/en/dorm/DORM_MANAGE.png b/assets/en/dorm/DORM_MANAGE.png new file mode 100644 index 000000000..a15de5da8 Binary files /dev/null and b/assets/en/dorm/DORM_MANAGE.png differ diff --git a/assets/en/handler/FORMATION_1.BUTTON.png b/assets/en/handler/FORMATION_1.BUTTON.png new file mode 100644 index 000000000..621645902 Binary files /dev/null and b/assets/en/handler/FORMATION_1.BUTTON.png differ diff --git a/assets/en/handler/FORMATION_1.png b/assets/en/handler/FORMATION_1.png index 621645902..309290142 100644 Binary files a/assets/en/handler/FORMATION_1.png and b/assets/en/handler/FORMATION_1.png differ diff --git a/assets/en/handler/FORMATION_2.BUTTON.png b/assets/en/handler/FORMATION_2.BUTTON.png new file mode 100644 index 000000000..bae9a30ca Binary files /dev/null and b/assets/en/handler/FORMATION_2.BUTTON.png differ diff --git a/assets/en/handler/FORMATION_2.png b/assets/en/handler/FORMATION_2.png index bae9a30ca..51effe6c2 100644 Binary files a/assets/en/handler/FORMATION_2.png and b/assets/en/handler/FORMATION_2.png differ diff --git a/assets/en/handler/FORMATION_3.BUTTON.png b/assets/en/handler/FORMATION_3.BUTTON.png new file mode 100644 index 000000000..c43781a06 Binary files /dev/null and b/assets/en/handler/FORMATION_3.BUTTON.png differ diff --git a/assets/en/handler/FORMATION_3.png b/assets/en/handler/FORMATION_3.png index c43781a06..abce4c396 100644 Binary files a/assets/en/handler/FORMATION_3.png and b/assets/en/handler/FORMATION_3.png differ diff --git a/assets/en/handler/LOGIN_ANNOUNCE_2.BUTTON.png b/assets/en/handler/LOGIN_ANNOUNCE_2.BUTTON.png new file mode 100644 index 000000000..4ec0d2990 Binary files /dev/null and b/assets/en/handler/LOGIN_ANNOUNCE_2.BUTTON.png differ diff --git a/assets/en/handler/LOGIN_ANNOUNCE_2.png b/assets/en/handler/LOGIN_ANNOUNCE_2.png new file mode 100644 index 000000000..f13407e9a Binary files /dev/null and b/assets/en/handler/LOGIN_ANNOUNCE_2.png differ diff --git a/assets/en/raid/RPG_GOTO_STAGE.png b/assets/en/raid/RPG_GOTO_STAGE.png new file mode 100644 index 000000000..44219b003 Binary files /dev/null and b/assets/en/raid/RPG_GOTO_STAGE.png differ diff --git a/assets/en/raid/RPG_GOTO_STORY.png b/assets/en/raid/RPG_GOTO_STORY.png new file mode 100644 index 000000000..d7dd1a9b0 Binary files /dev/null and b/assets/en/raid/RPG_GOTO_STORY.png differ diff --git a/assets/en/raid/RPG_HOME.png b/assets/en/raid/RPG_HOME.png new file mode 100644 index 000000000..8d84d8d24 Binary files /dev/null and b/assets/en/raid/RPG_HOME.png differ diff --git a/assets/en/raid/RPG_LEAVE_CITY.png b/assets/en/raid/RPG_LEAVE_CITY.png new file mode 100644 index 000000000..7a434cef5 Binary files /dev/null and b/assets/en/raid/RPG_LEAVE_CITY.png differ diff --git a/assets/en/raid/RPG_RAID_EASY.png b/assets/en/raid/RPG_RAID_EASY.png new file mode 100644 index 000000000..8b69b36bb Binary files /dev/null and b/assets/en/raid/RPG_RAID_EASY.png differ diff --git a/assets/en/raid/RPG_RAID_EX.png b/assets/en/raid/RPG_RAID_EX.png new file mode 100644 index 000000000..b16b3f0f0 Binary files /dev/null and b/assets/en/raid/RPG_RAID_EX.png differ diff --git a/assets/en/raid/RPG_RAID_HARD.png b/assets/en/raid/RPG_RAID_HARD.png new file mode 100644 index 000000000..28cba84ca Binary files /dev/null and b/assets/en/raid/RPG_RAID_HARD.png differ diff --git a/assets/en/raid/RPG_RAID_NORMAL.png b/assets/en/raid/RPG_RAID_NORMAL.png new file mode 100644 index 000000000..216fcc9ee Binary files /dev/null and b/assets/en/raid/RPG_RAID_NORMAL.png differ diff --git a/assets/en/raid/RPG_STATUS_POPUP.png b/assets/en/raid/RPG_STATUS_POPUP.png new file mode 100644 index 000000000..8dd75492d Binary files /dev/null and b/assets/en/raid/RPG_STATUS_POPUP.png differ diff --git a/assets/en/retire/TEMPLATE_AULICK.png b/assets/en/retire/TEMPLATE_AULICK.png new file mode 100644 index 000000000..eb7f3f25c Binary files /dev/null and b/assets/en/retire/TEMPLATE_AULICK.png differ diff --git a/assets/en/retire/TEMPLATE_BOGUE.png b/assets/en/retire/TEMPLATE_BOGUE.png index fd2125c99..f6fb84545 100644 Binary files a/assets/en/retire/TEMPLATE_BOGUE.png and b/assets/en/retire/TEMPLATE_BOGUE.png differ diff --git a/assets/en/retire/TEMPLATE_CASSIN_1.png b/assets/en/retire/TEMPLATE_CASSIN_1.png new file mode 100644 index 000000000..e1399ebe3 Binary files /dev/null and b/assets/en/retire/TEMPLATE_CASSIN_1.png differ diff --git a/assets/en/retire/TEMPLATE_CASSIN_2.png b/assets/en/retire/TEMPLATE_CASSIN_2.png new file mode 100644 index 000000000..cffb88b89 Binary files /dev/null and b/assets/en/retire/TEMPLATE_CASSIN_2.png differ diff --git a/assets/en/retire/TEMPLATE_DOWNES_1.png b/assets/en/retire/TEMPLATE_DOWNES_1.png new file mode 100644 index 000000000..9effcd06d Binary files /dev/null and b/assets/en/retire/TEMPLATE_DOWNES_1.png differ diff --git a/assets/en/retire/TEMPLATE_DOWNES_2.png b/assets/en/retire/TEMPLATE_DOWNES_2.png new file mode 100644 index 000000000..61c58826c Binary files /dev/null and b/assets/en/retire/TEMPLATE_DOWNES_2.png differ diff --git a/assets/en/retire/TEMPLATE_FOOTE.png b/assets/en/retire/TEMPLATE_FOOTE.png new file mode 100644 index 000000000..241f9a75b Binary files /dev/null and b/assets/en/retire/TEMPLATE_FOOTE.png differ diff --git a/assets/en/retire/TEMPLATE_HERMES.png b/assets/en/retire/TEMPLATE_HERMES.png index a09399356..7b20af71d 100644 Binary files a/assets/en/retire/TEMPLATE_HERMES.png and b/assets/en/retire/TEMPLATE_HERMES.png differ diff --git a/assets/en/retire/TEMPLATE_LANGLEY.png b/assets/en/retire/TEMPLATE_LANGLEY.png index 8fb671a16..5440eaa72 100644 Binary files a/assets/en/retire/TEMPLATE_LANGLEY.png and b/assets/en/retire/TEMPLATE_LANGLEY.png differ diff --git a/assets/en/retire/TEMPLATE_RANGER.png b/assets/en/retire/TEMPLATE_RANGER.png index 4b77c19c8..9e75ea056 100644 Binary files a/assets/en/retire/TEMPLATE_RANGER.png and b/assets/en/retire/TEMPLATE_RANGER.png differ diff --git a/assets/jp/dorm/DORM_MANAGE.png b/assets/jp/dorm/DORM_MANAGE.png new file mode 100644 index 000000000..a15de5da8 Binary files /dev/null and b/assets/jp/dorm/DORM_MANAGE.png differ diff --git a/assets/jp/handler/FORMATION_1.BUTTON.png b/assets/jp/handler/FORMATION_1.BUTTON.png new file mode 100644 index 000000000..621645902 Binary files /dev/null and b/assets/jp/handler/FORMATION_1.BUTTON.png differ diff --git a/assets/jp/handler/FORMATION_1.png b/assets/jp/handler/FORMATION_1.png index 621645902..309290142 100644 Binary files a/assets/jp/handler/FORMATION_1.png and b/assets/jp/handler/FORMATION_1.png differ diff --git a/assets/jp/handler/FORMATION_2.BUTTON.png b/assets/jp/handler/FORMATION_2.BUTTON.png new file mode 100644 index 000000000..bae9a30ca Binary files /dev/null and b/assets/jp/handler/FORMATION_2.BUTTON.png differ diff --git a/assets/jp/handler/FORMATION_2.png b/assets/jp/handler/FORMATION_2.png index bae9a30ca..51effe6c2 100644 Binary files a/assets/jp/handler/FORMATION_2.png and b/assets/jp/handler/FORMATION_2.png differ diff --git a/assets/jp/handler/FORMATION_3.BUTTON.png b/assets/jp/handler/FORMATION_3.BUTTON.png new file mode 100644 index 000000000..c43781a06 Binary files /dev/null and b/assets/jp/handler/FORMATION_3.BUTTON.png differ diff --git a/assets/jp/handler/FORMATION_3.png b/assets/jp/handler/FORMATION_3.png index c43781a06..abce4c396 100644 Binary files a/assets/jp/handler/FORMATION_3.png and b/assets/jp/handler/FORMATION_3.png differ diff --git a/assets/jp/handler/LOGIN_ANNOUNCE_2.BUTTON.png b/assets/jp/handler/LOGIN_ANNOUNCE_2.BUTTON.png new file mode 100644 index 000000000..4ec0d2990 Binary files /dev/null and b/assets/jp/handler/LOGIN_ANNOUNCE_2.BUTTON.png differ diff --git a/assets/jp/handler/LOGIN_ANNOUNCE_2.png b/assets/jp/handler/LOGIN_ANNOUNCE_2.png new file mode 100644 index 000000000..f13407e9a Binary files /dev/null and b/assets/jp/handler/LOGIN_ANNOUNCE_2.png differ diff --git a/assets/jp/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png b/assets/jp/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png new file mode 100644 index 000000000..e1fc3b293 Binary files /dev/null and b/assets/jp/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png differ diff --git a/assets/jp/raid/RPG_GOTO_STAGE.png b/assets/jp/raid/RPG_GOTO_STAGE.png new file mode 100644 index 000000000..44219b003 Binary files /dev/null and b/assets/jp/raid/RPG_GOTO_STAGE.png differ diff --git a/assets/jp/raid/RPG_GOTO_STORY.png b/assets/jp/raid/RPG_GOTO_STORY.png new file mode 100644 index 000000000..d7dd1a9b0 Binary files /dev/null and b/assets/jp/raid/RPG_GOTO_STORY.png differ diff --git a/assets/jp/raid/RPG_HOME.png b/assets/jp/raid/RPG_HOME.png new file mode 100644 index 000000000..8d84d8d24 Binary files /dev/null and b/assets/jp/raid/RPG_HOME.png differ diff --git a/assets/jp/raid/RPG_LEAVE_CITY.png b/assets/jp/raid/RPG_LEAVE_CITY.png new file mode 100644 index 000000000..7a434cef5 Binary files /dev/null and b/assets/jp/raid/RPG_LEAVE_CITY.png differ diff --git a/assets/jp/raid/RPG_RAID_EASY.png b/assets/jp/raid/RPG_RAID_EASY.png new file mode 100644 index 000000000..8b69b36bb Binary files /dev/null and b/assets/jp/raid/RPG_RAID_EASY.png differ diff --git a/assets/jp/raid/RPG_RAID_EX.png b/assets/jp/raid/RPG_RAID_EX.png new file mode 100644 index 000000000..b16b3f0f0 Binary files /dev/null and b/assets/jp/raid/RPG_RAID_EX.png differ diff --git a/assets/jp/raid/RPG_RAID_HARD.png b/assets/jp/raid/RPG_RAID_HARD.png new file mode 100644 index 000000000..28cba84ca Binary files /dev/null and b/assets/jp/raid/RPG_RAID_HARD.png differ diff --git a/assets/jp/raid/RPG_RAID_NORMAL.png b/assets/jp/raid/RPG_RAID_NORMAL.png new file mode 100644 index 000000000..216fcc9ee Binary files /dev/null and b/assets/jp/raid/RPG_RAID_NORMAL.png differ diff --git a/assets/jp/raid/RPG_STATUS_POPUP.png b/assets/jp/raid/RPG_STATUS_POPUP.png new file mode 100644 index 000000000..8dd75492d Binary files /dev/null and b/assets/jp/raid/RPG_STATUS_POPUP.png differ diff --git a/assets/jp/retire/TEMPLATE_AULICK.png b/assets/jp/retire/TEMPLATE_AULICK.png new file mode 100644 index 000000000..eb7f3f25c Binary files /dev/null and b/assets/jp/retire/TEMPLATE_AULICK.png differ diff --git a/assets/jp/retire/TEMPLATE_BOGUE.png b/assets/jp/retire/TEMPLATE_BOGUE.png index fd2125c99..f6fb84545 100644 Binary files a/assets/jp/retire/TEMPLATE_BOGUE.png and b/assets/jp/retire/TEMPLATE_BOGUE.png differ diff --git a/assets/jp/retire/TEMPLATE_CASSIN_1.png b/assets/jp/retire/TEMPLATE_CASSIN_1.png new file mode 100644 index 000000000..e1399ebe3 Binary files /dev/null and b/assets/jp/retire/TEMPLATE_CASSIN_1.png differ diff --git a/assets/jp/retire/TEMPLATE_CASSIN_2.png b/assets/jp/retire/TEMPLATE_CASSIN_2.png new file mode 100644 index 000000000..cffb88b89 Binary files /dev/null and b/assets/jp/retire/TEMPLATE_CASSIN_2.png differ diff --git a/assets/jp/retire/TEMPLATE_DOWNES_1.png b/assets/jp/retire/TEMPLATE_DOWNES_1.png new file mode 100644 index 000000000..9effcd06d Binary files /dev/null and b/assets/jp/retire/TEMPLATE_DOWNES_1.png differ diff --git a/assets/jp/retire/TEMPLATE_DOWNES_2.png b/assets/jp/retire/TEMPLATE_DOWNES_2.png new file mode 100644 index 000000000..61c58826c Binary files /dev/null and b/assets/jp/retire/TEMPLATE_DOWNES_2.png differ diff --git a/assets/jp/retire/TEMPLATE_FOOTE.png b/assets/jp/retire/TEMPLATE_FOOTE.png new file mode 100644 index 000000000..241f9a75b Binary files /dev/null and b/assets/jp/retire/TEMPLATE_FOOTE.png differ diff --git a/assets/jp/retire/TEMPLATE_HERMES.png b/assets/jp/retire/TEMPLATE_HERMES.png index a09399356..7b20af71d 100644 Binary files a/assets/jp/retire/TEMPLATE_HERMES.png and b/assets/jp/retire/TEMPLATE_HERMES.png differ diff --git a/assets/jp/retire/TEMPLATE_LANGLEY.png b/assets/jp/retire/TEMPLATE_LANGLEY.png index 8fb671a16..5440eaa72 100644 Binary files a/assets/jp/retire/TEMPLATE_LANGLEY.png and b/assets/jp/retire/TEMPLATE_LANGLEY.png differ diff --git a/assets/jp/retire/TEMPLATE_RANGER.png b/assets/jp/retire/TEMPLATE_RANGER.png index 4b77c19c8..9e75ea056 100644 Binary files a/assets/jp/retire/TEMPLATE_RANGER.png and b/assets/jp/retire/TEMPLATE_RANGER.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/dorm/DORM_MANAGE.png b/assets/tw/dorm/DORM_MANAGE.png new file mode 100644 index 000000000..a15de5da8 Binary files /dev/null and b/assets/tw/dorm/DORM_MANAGE.png differ diff --git a/assets/tw/handler/FORMATION_1.BUTTON.png b/assets/tw/handler/FORMATION_1.BUTTON.png new file mode 100644 index 000000000..621645902 Binary files /dev/null and b/assets/tw/handler/FORMATION_1.BUTTON.png differ diff --git a/assets/tw/handler/FORMATION_1.png b/assets/tw/handler/FORMATION_1.png index 621645902..309290142 100644 Binary files a/assets/tw/handler/FORMATION_1.png and b/assets/tw/handler/FORMATION_1.png differ diff --git a/assets/tw/handler/FORMATION_2.BUTTON.png b/assets/tw/handler/FORMATION_2.BUTTON.png new file mode 100644 index 000000000..bae9a30ca Binary files /dev/null and b/assets/tw/handler/FORMATION_2.BUTTON.png differ diff --git a/assets/tw/handler/FORMATION_2.png b/assets/tw/handler/FORMATION_2.png index bae9a30ca..51effe6c2 100644 Binary files a/assets/tw/handler/FORMATION_2.png and b/assets/tw/handler/FORMATION_2.png differ diff --git a/assets/tw/handler/FORMATION_3.BUTTON.png b/assets/tw/handler/FORMATION_3.BUTTON.png new file mode 100644 index 000000000..c43781a06 Binary files /dev/null and b/assets/tw/handler/FORMATION_3.BUTTON.png differ diff --git a/assets/tw/handler/FORMATION_3.png b/assets/tw/handler/FORMATION_3.png index c43781a06..abce4c396 100644 Binary files a/assets/tw/handler/FORMATION_3.png and b/assets/tw/handler/FORMATION_3.png differ diff --git a/assets/tw/handler/LOGIN_ANNOUNCE_2.BUTTON.png b/assets/tw/handler/LOGIN_ANNOUNCE_2.BUTTON.png new file mode 100644 index 000000000..4ec0d2990 Binary files /dev/null and b/assets/tw/handler/LOGIN_ANNOUNCE_2.BUTTON.png differ diff --git a/assets/tw/handler/LOGIN_ANNOUNCE_2.png b/assets/tw/handler/LOGIN_ANNOUNCE_2.png new file mode 100644 index 000000000..f13407e9a Binary files /dev/null and b/assets/tw/handler/LOGIN_ANNOUNCE_2.png differ diff --git a/assets/tw/map/MAP_OFFENSIVE.png b/assets/tw/map/MAP_OFFENSIVE.png index e1eb7c9cf..d7cb459cd 100644 Binary files a/assets/tw/map/MAP_OFFENSIVE.png and b/assets/tw/map/MAP_OFFENSIVE.png differ diff --git a/assets/tw/map/SWITCH_OVER.png b/assets/tw/map/SWITCH_OVER.png index e241cf716..739c8e5ac 100644 Binary files a/assets/tw/map/SWITCH_OVER.png and b/assets/tw/map/SWITCH_OVER.png differ diff --git a/assets/tw/raid/GORIZIA_OCR_REMAIN_EX.png b/assets/tw/raid/GORIZIA_OCR_REMAIN_EX.png new file mode 100644 index 000000000..5d68c39d8 Binary files /dev/null and b/assets/tw/raid/GORIZIA_OCR_REMAIN_EX.png differ diff --git a/assets/tw/raid/GORIZIA_RAID_EASY.png b/assets/tw/raid/GORIZIA_RAID_EASY.png new file mode 100644 index 000000000..3ccd79a16 Binary files /dev/null and b/assets/tw/raid/GORIZIA_RAID_EASY.png differ diff --git a/assets/tw/raid/GORIZIA_RAID_HARD.png b/assets/tw/raid/GORIZIA_RAID_HARD.png new file mode 100644 index 000000000..a76c178e2 Binary files /dev/null and b/assets/tw/raid/GORIZIA_RAID_HARD.png differ diff --git a/assets/tw/raid/GORIZIA_RAID_NORMAL.png b/assets/tw/raid/GORIZIA_RAID_NORMAL.png new file mode 100644 index 000000000..191215e39 Binary files /dev/null and b/assets/tw/raid/GORIZIA_RAID_NORMAL.png differ diff --git a/assets/tw/raid/RPG_GOTO_STAGE.png b/assets/tw/raid/RPG_GOTO_STAGE.png new file mode 100644 index 000000000..44219b003 Binary files /dev/null and b/assets/tw/raid/RPG_GOTO_STAGE.png differ diff --git a/assets/tw/raid/RPG_GOTO_STORY.png b/assets/tw/raid/RPG_GOTO_STORY.png new file mode 100644 index 000000000..d7dd1a9b0 Binary files /dev/null and b/assets/tw/raid/RPG_GOTO_STORY.png differ diff --git a/assets/tw/raid/RPG_HOME.png b/assets/tw/raid/RPG_HOME.png new file mode 100644 index 000000000..8d84d8d24 Binary files /dev/null and b/assets/tw/raid/RPG_HOME.png differ diff --git a/assets/tw/raid/RPG_LEAVE_CITY.png b/assets/tw/raid/RPG_LEAVE_CITY.png new file mode 100644 index 000000000..7a434cef5 Binary files /dev/null and b/assets/tw/raid/RPG_LEAVE_CITY.png differ diff --git a/assets/tw/raid/RPG_RAID_EASY.png b/assets/tw/raid/RPG_RAID_EASY.png new file mode 100644 index 000000000..8b69b36bb Binary files /dev/null and b/assets/tw/raid/RPG_RAID_EASY.png differ diff --git a/assets/tw/raid/RPG_RAID_EX.png b/assets/tw/raid/RPG_RAID_EX.png new file mode 100644 index 000000000..b16b3f0f0 Binary files /dev/null and b/assets/tw/raid/RPG_RAID_EX.png differ diff --git a/assets/tw/raid/RPG_RAID_HARD.png b/assets/tw/raid/RPG_RAID_HARD.png new file mode 100644 index 000000000..28cba84ca Binary files /dev/null and b/assets/tw/raid/RPG_RAID_HARD.png differ diff --git a/assets/tw/raid/RPG_RAID_NORMAL.png b/assets/tw/raid/RPG_RAID_NORMAL.png new file mode 100644 index 000000000..216fcc9ee Binary files /dev/null and b/assets/tw/raid/RPG_RAID_NORMAL.png differ diff --git a/assets/tw/raid/RPG_STATUS_POPUP.png b/assets/tw/raid/RPG_STATUS_POPUP.png new file mode 100644 index 000000000..8dd75492d Binary files /dev/null and b/assets/tw/raid/RPG_STATUS_POPUP.png differ diff --git a/assets/tw/retire/TEMPLATE_AULICK.png b/assets/tw/retire/TEMPLATE_AULICK.png new file mode 100644 index 000000000..eb7f3f25c Binary files /dev/null and b/assets/tw/retire/TEMPLATE_AULICK.png differ diff --git a/assets/tw/retire/TEMPLATE_BOGUE.png b/assets/tw/retire/TEMPLATE_BOGUE.png index fd2125c99..f6fb84545 100644 Binary files a/assets/tw/retire/TEMPLATE_BOGUE.png and b/assets/tw/retire/TEMPLATE_BOGUE.png differ diff --git a/assets/tw/retire/TEMPLATE_CASSIN_1.png b/assets/tw/retire/TEMPLATE_CASSIN_1.png new file mode 100644 index 000000000..e1399ebe3 Binary files /dev/null and b/assets/tw/retire/TEMPLATE_CASSIN_1.png differ diff --git a/assets/tw/retire/TEMPLATE_CASSIN_2.png b/assets/tw/retire/TEMPLATE_CASSIN_2.png new file mode 100644 index 000000000..cffb88b89 Binary files /dev/null and b/assets/tw/retire/TEMPLATE_CASSIN_2.png differ diff --git a/assets/tw/retire/TEMPLATE_DOWNES_1.png b/assets/tw/retire/TEMPLATE_DOWNES_1.png new file mode 100644 index 000000000..9effcd06d Binary files /dev/null and b/assets/tw/retire/TEMPLATE_DOWNES_1.png differ diff --git a/assets/tw/retire/TEMPLATE_DOWNES_2.png b/assets/tw/retire/TEMPLATE_DOWNES_2.png new file mode 100644 index 000000000..61c58826c Binary files /dev/null and b/assets/tw/retire/TEMPLATE_DOWNES_2.png differ diff --git a/assets/tw/retire/TEMPLATE_FOOTE.png b/assets/tw/retire/TEMPLATE_FOOTE.png new file mode 100644 index 000000000..241f9a75b Binary files /dev/null and b/assets/tw/retire/TEMPLATE_FOOTE.png differ diff --git a/assets/tw/retire/TEMPLATE_HERMES.png b/assets/tw/retire/TEMPLATE_HERMES.png index a09399356..7b20af71d 100644 Binary files a/assets/tw/retire/TEMPLATE_HERMES.png and b/assets/tw/retire/TEMPLATE_HERMES.png differ diff --git a/assets/tw/retire/TEMPLATE_LANGLEY.png b/assets/tw/retire/TEMPLATE_LANGLEY.png index 8fb671a16..5440eaa72 100644 Binary files a/assets/tw/retire/TEMPLATE_LANGLEY.png and b/assets/tw/retire/TEMPLATE_LANGLEY.png differ diff --git a/assets/tw/retire/TEMPLATE_RANGER.png b/assets/tw/retire/TEMPLATE_RANGER.png index 4b77c19c8..9e75ea056 100644 Binary files a/assets/tw/retire/TEMPLATE_RANGER.png and b/assets/tw/retire/TEMPLATE_RANGER.png differ diff --git a/assets/tw/ui/RAID_CHECK.png b/assets/tw/ui/RAID_CHECK.png index 87d471c94..bb74b18f7 100644 Binary files a/assets/tw/ui/RAID_CHECK.png and b/assets/tw/ui/RAID_CHECK.png differ diff --git a/campaign/Readme.md b/campaign/Readme.md index 7ab90fea2..318c74f10 100644 --- a/campaign/Readme.md +++ b/campaign/Readme.md @@ -13,7 +13,7 @@ To add a new event, add a new row in here, and run `python -m module.config.conf **CN, EN, JP, TW** Event names in GUI. If an event is not aired on some servers, use `-`. | Aired Date | Directory | Event Name | CN | EN | JP | TW | -| ---------- | ------------------------ | -------------------------------------------- | ---------------------- | -------------------------------------------- | ------------------------------------ | ------------------------ | +| :--------- | :----------------------- | :------------------------------------------- | :--------------------- | :------------------------------------------- | :----------------------------------- | :----------------------- | | 20170607 | war archives 20181020 en | Strive, Wish, and Strategize | 努力、希望和计划 | Strive, Wish, and Strategize | 努力、希望と計画 | 努力、希望和計劃 | | 20170802 | war archives 20191031 en | Divergent Chessboard | 异色格 | Divergent Chessboard | 鏡写されし異色 | 異色格 | | 20170928 | war archives 20190321 en | Visitors Dyed in Red | 红染的参访者 | Visitors Dyed in Red | 紅染の来訪者 | 紅染的參訪者 | @@ -22,24 +22,28 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20180119 | war archives 20191010 en | Encircling Graf Spee | 围剿斯佩伯爵 | Encircling Graf Spee | アドミラル・グラーフ・シュペー追撃戦 | 圍剿斯佩伯爵 | | 20180226 | war archives 20190221 en | Winter\'s Crown | 凛冬王冠 | Winter\'s Crown | 凛冽なりし冬の王冠 | 凜冬王冠 | | 20180607 | war archives 20180607 cn | Ink Stained Steel Sakura | 墨染的钢铁之花 | Ink Stained Steel Sakura | 墨染まりし鋼の桜 | 墨染的鋼鐵之花 | +| 20180717 | war archives 20190314 en | Prelude under the Moon | 月光下的序曲 | Prelude under the Moon | 月夜の開幕曲 | 月光下的序曲 | | 20180726 | war archives 20180726 cn | Iris of Light and Dark | 光与影的鸢尾之华 | Iris of Light and Dark | 光と影のアイリス | 光與影的鳶尾之華 | +| 20200312 | war archives 20200312 cn | The Solomon Ranger | 斯图尔特的硝烟 | The Solomon Ranger | 南洋に靡く硝煙 | 斯圖爾特的硝煙 | | 20210121 | war archives 20181227 cn | Crimson Echoes | 苍红的回响 | Crimson Echoes | 縹映る深緋の残響 | 蒼紅的迴響 | -| 20210513 | war archives 20200820 cn | Scherzo of Iron and Blood | 铁血音符誓言 | Scherzo of Iron and Blood | 黒鉄の楽章 誓いの海 | - | +| 20210513 | war archives 20200820 cn | Scherzo of Iron and Blood | 铁血音符誓言 | Scherzo of Iron and Blood | 黒鉄の楽章 誓いの海 | 鐵血音符誓言 | | 20211014 | war archives 20211014 cn | Crescendo of Polaris | 激奏的Polaris | Crescendo of Polaris | 激奏のポラリス | 激奏的Polaris | -| 20220113 | war archives 20190911 cn | Empyreal Tragicomedy | 神圣的悲喜剧 | Empyreal Tragicomedy | 悲歎せし焔海の詩 | - | -| 20220407 | war archives 20210325 cn | Ashen Simulacrum | 箱庭疗法 | Ashen Simulacrum | 開かれし紺碧の砂箱 | - | +| 20220113 | war archives 20190911 cn | Empyreal Tragicomedy | 神圣的悲喜剧 | Empyreal Tragicomedy | 悲歎せし焔海の詩 | 神聖的悲喜劇 | +| 20220407 | war archives 20210325 cn | Ashen Simulacrum | 箱庭疗法 | Ashen Simulacrum | 開かれし紺碧の砂箱 | 箱庭療法 | | 20220721 | war archives 20210624 cn | Swirling Cherry Blossoms | 浮樱影华 | Swirling Cherry Blossoms | 翳りし満ちる影の華 | 浮櫻影華 | -| 20220901 | war archives 20200806 cn | The Enigma and the Shark | 最重要的宝物 | The Enigma and the Shark | 鉄血鮫とエニグマ | - | -| 20221013 | war archives 20201029 cn | Universe in Unison | 激唱的UNIVERSE | Universe in Unison | 激唱のユニバース | - | +| 20220901 | war archives 20200806 cn | The Enigma and the Shark | 最重要的宝物 | The Enigma and the Shark | 鉄血鮫とエニグマ | 最重要的寶物 | +| 20221013 | war archives 20201029 cn | Universe in Unison | 激唱的UNIVERSE | Universe in Unison | 激唱のユニバース | 激唱的UNIVERSE | | 20221117 | war archives 20200903 cn | Stars of the Shimmering Fjord | 峡湾间的星辰 | Stars of the Shimmering Fjord | 輝ける峡湾の星 | 峽灣間的星辰 | | 20221117 | war archives 20210819 cn | Microlayer Medley | 微层混合 | Microlayer Medley | 闇靄払う銀翼 | 微層混合 | | 20211028 | war archives 20211028 cn | Skybound Oratorio | 穹顶下的圣咏曲 | Skybound Oratorio | 神穹を衝く聖歌 | 穹頂下的聖詠曲 | +| 20230309 | war archives 20200507 cn | The Way Home in the Night | 夜幕下的归途 | The Way Home in the Night | 帰路は海色の陰りへと | 夜幕下的歸途 | | 20230420 | war archives 20220210 cn | Northern Overture | 北境序曲 | Northern Overture | 凍絶の北海 | 北境序曲 | | 20230511 | war archives 20220414 cn | Aurora Noctis | 永夜幻光 | Aurora Noctis | 極夜照らす幻光 | 永夜幻光 | -| 20230831 | war archives 20201229 cn | Inverted Orthant | 负象限作战 | Inverted Orthant | 虚畳なりし限象 | - | -| 20240118 | war archives 20200917 cn | Dreamwaker's Butterfly | 蝶海梦花 | Dreamwaker's Butterfly | 刹那觀る胡蝶の夢 | - | -| 20240118 | war archives 20210527 cn | Mirror Involution | 镜位螺旋 | Mirror Involution | 照らす螺旋の鏡海 | - | -| 20240222 | war archives 20210225 cn | Khorovod of Dawn's Rime | 破晓冰华 | Khorovod of Dawn's Rime | 暁射す氷華の嵐 | - | +| 20230713 | war archives 20200603 cn | Counterattack Within the Fjord | 峡湾间的反击 | Counterattack Within the Fjord | 峡湾間の反撃 | 峽灣間的反擊 | +| 20230831 | war archives 20201229 cn | Inverted Orthant | 负象限作战 | Inverted Orthant | 虚畳なりし限象 | 負象限作戰 | +| 20240118 | war archives 20200917 cn | Dreamwaker's Butterfly | 蝶海梦花 | Dreamwaker's Butterfly | 刹那觀る胡蝶の夢 | 蝶海夢花 | +| 20240118 | war archives 20210527 cn | Mirror Involution | 镜位螺旋 | Mirror Involution | 照らす螺旋の鏡海 | 鏡位螺旋 | +| 20240222 | war archives 20210225 cn | Khorovod of Dawn's Rime | 破晓冰华 | Khorovod of Dawn's Rime | 暁射す氷華の嵐 | 破曉冰華 | | 20200227 | event 20200227 cn | Northern Overture | 北境序曲 | Northern Overture | 凍絶の北海 | - | | 20200312 | event 20200312 cn | The Solomon Ranger | 复刻斯图尔特的硝烟 | The Solomon Ranger Rerun | 南洋に靡く硝煙(復刻) | - | | 20200326 | event 20200326 cn | Microlayer Medley | 微层混合 | Microlayer Medley | 闇靄払う銀翼 | - | @@ -181,3 +185,9 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20240206 | raid 20230118 | Winter Pathfinder | - | - | - | 冬日的尋路人 | | 20240229 | event 20230223 cn | Revelations of Dust | - | - | - | 湮燼塵墟 | | 20240229 | event 20240229 cn | Snowrealm Peregrination | 雪境迷踪 | Snowrealm Peregrination | 銀界遊廻 | - | +| 20240314 | event 20210422 cn | Daedalian Hymn | - | - | - | 復刻復興的讚美詩 | +| 20240314 | event 20220324 cn | Virtual Tower Rerun | 复刻虚像构筑之塔 | Virtual Tower Rerun | 幻像の塔(復刻) | - | +| 20240321 | raid 20230629 | Reflections of the Oasis | - | - | - | 綠洲往事 | +| 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 | 鳴動せし星霜の淵(復刻) | - | \ No newline at end of file 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/war_archives_20190314_en/sp1.py b/campaign/war_archives_20190314_en/sp1.py new file mode 100644 index 000000000..a59b9c493 --- /dev/null +++ b/campaign/war_archives_20190314_en/sp1.py @@ -0,0 +1,59 @@ +from ..campaign_war_archives.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 = 'H5' +MAP.camera_data = ['D2', 'D3', 'E2', 'E3'] +MAP.camera_data_spawn_point = ['D3'] +MAP.map_data = """ + ++ ++ ++ -- ME ME MB ++ + -- -- -- Me -- ME -- -- + -- -- ++ -- Me ++ MB -- + SP SP -- ME -- ++ -- ME + SP SP -- -- 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ + = 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 + # ===== 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): + 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/war_archives_20190314_en/sp2.py b/campaign/war_archives_20190314_en/sp2.py new file mode 100644 index 000000000..c8193ce5f --- /dev/null +++ b/campaign/war_archives_20190314_en/sp2.py @@ -0,0 +1,66 @@ +from ..campaign_war_archives.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 = 'G7' +MAP.camera_data = ['D2', 'D5'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + ++ ++ -- ME -- ME MB + MM ++ -- -- ME -- MB + ME -- Me -- ++ ++ ++ + -- -- -- Me -- ME MB + -- -- ++ -- ++ ME -- + SP SP -- -- ++ -- ME + ++ SP -- 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'mystery': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, \ +A2, B2, C2, D2, E2, F2, G2, \ +A3, B3, C3, D3, E3, F3, G3, \ +A4, B4, C4, D4, E4, F4, G4, \ +A5, B5, C5, D5, E5, F5, G5, \ +A6, B6, C6, D6, E6, F6, G6, \ +A7, B7, C7, D7, E7, F7, G7, \ + = 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 = 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_0(self): + 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/war_archives_20190314_en/sp3.py b/campaign/war_archives_20190314_en/sp3.py new file mode 100644 index 000000000..dc2fdf6aa --- /dev/null +++ b/campaign/war_archives_20190314_en/sp3.py @@ -0,0 +1,64 @@ +from ..campaign_war_archives.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 = 'I6' +MAP.camera_data = ['D2', 'D4', 'F2', 'F4'] +MAP.camera_data_spawn_point = ['F4'] +MAP.map_data = """ + ++ MB -- ME -- Me -- ++ ++ + MB -- ME -- ++ -- ME ++ ++ + MB ME -- Me -- ME -- ME -- + ++ ++ ++ -- ME -- Me ++ -- + MB -- ME Me -- ++ -- -- SP + -- 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'enemy': 1, '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, \ + = 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 + # ===== 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): + 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/war_archives_20191031_en/b2.py b/campaign/war_archives_20191031_en/b2.py index 99710918a..a411e427d 100644 --- a/campaign/war_archives_20191031_en/b2.py +++ b/campaign/war_archives_20191031_en/b2.py @@ -54,6 +54,13 @@ class Config(ConfigBase): MAP_HAS_AMBUSH = False MAP_HAS_MYSTERY = True # ===== End of generated config ===== + HOMO_EDGE_COLOR_RANGE = (0, 12) + HOMO_EDGE_HOUGHLINES_THRESHOLD = 210 + MAP_SWIPE_MULTIPLY = (1.101, 1.122) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.065, 1.085) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.034, 1.053) + HOMO_STORAGE = ((6, 5), [(211, 175), (782, 175), (158, 569), (800, 569)]) + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'top' class Campaign(CampaignBase): diff --git a/campaign/war_archives_20191031_en/b4.py b/campaign/war_archives_20191031_en/b4.py index bb04914ab..0e8584c6a 100644 --- a/campaign/war_archives_20191031_en/b4.py +++ b/campaign/war_archives_20191031_en/b4.py @@ -58,6 +58,13 @@ class Config(ConfigBase): MAP_HAS_AMBUSH = False MAP_HAS_MYSTERY = False # ===== End of generated config ===== + HOMO_EDGE_COLOR_RANGE = (0, 12) + HOMO_EDGE_HOUGHLINES_THRESHOLD = 210 + MAP_SWIPE_MULTIPLY = (1.101, 1.122) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.065, 1.085) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.034, 1.053) + HOMO_STORAGE = ((6, 4), [(448, 180), (1051, 180), (426, 513), (1100, 513)]) + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'top' class Campaign(CampaignBase): diff --git a/campaign/war_archives_20191031_en/d2.py b/campaign/war_archives_20191031_en/d2.py index 5ab9f2114..dbda2c3f3 100644 --- a/campaign/war_archives_20191031_en/d2.py +++ b/campaign/war_archives_20191031_en/d2.py @@ -55,6 +55,13 @@ class Config(ConfigBase): MAP_HAS_AMBUSH = False MAP_HAS_MYSTERY = True # ===== End of generated config ===== + HOMO_EDGE_COLOR_RANGE = (0, 12) + HOMO_EDGE_HOUGHLINES_THRESHOLD = 210 + MAP_SWIPE_MULTIPLY = (1.101, 1.122) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.065, 1.085) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.034, 1.053) + HOMO_STORAGE = ((6, 5), [(211, 175), (782, 175), (158, 569), (800, 569)]) + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'top' class Campaign(CampaignBase): diff --git a/campaign/war_archives_20191031_en/d4.py b/campaign/war_archives_20191031_en/d4.py index 01258a03a..3736c9f85 100644 --- a/campaign/war_archives_20191031_en/d4.py +++ b/campaign/war_archives_20191031_en/d4.py @@ -58,6 +58,13 @@ class Config(ConfigBase): MAP_HAS_AMBUSH = False MAP_HAS_MYSTERY = False # ===== End of generated config ===== + HOMO_EDGE_COLOR_RANGE = (0, 12) + HOMO_EDGE_HOUGHLINES_THRESHOLD = 210 + MAP_SWIPE_MULTIPLY = (1.101, 1.122) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.065, 1.085) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.034, 1.053) + HOMO_STORAGE = ((6, 4), [(448, 180), (1051, 180), (426, 513), (1100, 513)]) + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'top' class Campaign(CampaignBase): diff --git a/campaign/war_archives_20200312_cn/sp1.py b/campaign/war_archives_20200312_cn/sp1.py new file mode 100644 index 000000000..297b40f54 --- /dev/null +++ b/campaign/war_archives_20200312_cn/sp1.py @@ -0,0 +1,59 @@ +from ..campaign_war_archives.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 = 'H5' +MAP.camera_data = ['D2', 'D3', 'E2', 'E3'] +MAP.camera_data_spawn_point = ['D3', 'D2'] +MAP.map_data = """ + ++ ++ ++ -- ME ME MB ++ + SP -- -- Me __ ME -- -- + -- ME ++ -- Me ++ MB -- + -- Me -- ME -- ++ -- ME + 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = True + MAP_HAS_MYSTERY = False + # ===== 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): + 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/war_archives_20200312_cn/sp2.py b/campaign/war_archives_20200312_cn/sp2.py new file mode 100644 index 000000000..d68dcf8c1 --- /dev/null +++ b/campaign/war_archives_20200312_cn/sp2.py @@ -0,0 +1,66 @@ +from ..campaign_war_archives.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 = 'G7' +MAP.camera_data = ['D2', 'D5'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + ++ ++ -- ME -- ME MB + -- ++ -- -- -- -- MB + ME -- Me -- ++ ++ ++ + -- -- Me __ -- ME MB + -- ME ++ -- ++ ME -- + SP -- -- -- ++ -- ME + ++ SP -- 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'mystery': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, \ +A2, B2, C2, D2, E2, F2, G2, \ +A3, B3, C3, D3, E3, F3, G3, \ +A4, B4, C4, D4, E4, F4, G4, \ +A5, B5, C5, D5, E5, F5, G5, \ +A6, B6, C6, D6, E6, F6, G6, \ +A7, B7, C7, D7, E7, F7, G7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = 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_0(self): + 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/war_archives_20200312_cn/sp3.py b/campaign/war_archives_20200312_cn/sp3.py new file mode 100644 index 000000000..392f05ba2 --- /dev/null +++ b/campaign/war_archives_20200312_cn/sp3.py @@ -0,0 +1,64 @@ +from ..campaign_war_archives.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 = 'I6' +MAP.camera_data = ['D2', 'D4', 'F2', 'F4'] +MAP.camera_data_spawn_point = ['F2', 'D4'] +MAP.map_data = """ + ++ MB -- ME -- Me -- ++ ++ + MB -- -- -- ++ -- ME ++ ++ + MB ME __ -- -- ME -- ME SP + ++ ++ ++ -- -- -- -- ++ -- + MB -- ME Me -- ++ -- Me -- + -- ME -- -- 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'enemy': 1, '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, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = True + MAP_HAS_MYSTERY = False + # ===== 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): + 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/war_archives_20200507_cn/sp1.py b/campaign/war_archives_20200507_cn/sp1.py new file mode 100644 index 000000000..6ed66faf3 --- /dev/null +++ b/campaign/war_archives_20200507_cn/sp1.py @@ -0,0 +1,65 @@ +from ..campaign_war_archives.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 = 'H7' +MAP.camera_data = ['D2', 'D5', 'E2', 'E5'] +MAP.camera_data_spawn_point = ['E5', 'D5'] +MAP.map_data = """ + -- ++ ++ MB MB ++ ++ ++ + ME ++ Me -- -- ME Me -- + -- -- __ -- ME __ -- -- + ME -- ME -- -- -- ME -- + -- -- ++ ME -- 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ + = 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 + # ===== 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): + 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/war_archives_20200507_cn/sp2.py b/campaign/war_archives_20200507_cn/sp2.py new file mode 100644 index 000000000..15f61b9a3 --- /dev/null +++ b/campaign/war_archives_20200507_cn/sp2.py @@ -0,0 +1,63 @@ +from ..campaign_war_archives.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 = 'K6' +MAP.camera_data = ['D2', 'D4', 'H2', 'H4'] +MAP.camera_data_spawn_point = ['D4'] +MAP.map_data = """ + -- -- ++ ++ -- Me -- ++ ++ ME -- + Me ME ++ ++ ME -- ME -- ME -- ME + -- -- MB -- MB -- -- -- -- -- -- + ++ -- -- MB -- __ Me ++ ++ ME ++ + SP -- Me -- ME -- -- ++ ME -- ME + SP SP ++ ++ ++ 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'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, J1, K1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, K5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, K6, \ + = 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 + # ===== 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): + 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/war_archives_20200507_cn/sp3.py b/campaign/war_archives_20200507_cn/sp3.py new file mode 100644 index 000000000..cec61e279 --- /dev/null +++ b/campaign/war_archives_20200507_cn/sp3.py @@ -0,0 +1,70 @@ +from ..campaign_war_archives.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 = 'H8' +MAP.camera_data = ['D2', 'D6', 'E2', 'E6'] +MAP.camera_data_spawn_point = ['D6'] +MAP.map_data = """ + ++ ++ ++ ME ME ++ MB MB + ME -- -- -- -- ME -- MB + -- -- Me Me -- __ -- ++ + ME -- ++ ++ -- Me -- ME + -- -- SP SP -- ++ -- ME + ME -- -- -- ME ++ ME -- + ++ ++ Me -- -- -- -- 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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 2, 'siren': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ +A8, B8, C8, D8, E8, F8, G8, H8, \ + = 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 + # ===== 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): + 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/war_archives_20200603_cn/sp1.py b/campaign/war_archives_20200603_cn/sp1.py new file mode 100644 index 000000000..fedbeea0f --- /dev/null +++ b/campaign/war_archives_20200603_cn/sp1.py @@ -0,0 +1,74 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.logger import logger +from module.map.map_base import CampaignMap +from module.map.map_grids import RoadGrids, SelectedGrids + +MAP = CampaignMap('SP1') +MAP.shape = 'K7' +MAP.camera_data = ['D2', 'D5', 'H2', 'H5'] +MAP.camera_data_spawn_point = ['H5'] +MAP.map_data = """ + ++ ++ ++ -- ME -- -- -- -- ++ -- + -- ME -- ++ -- ++ ++ ME -- ++ ME + ME -- -- Me -- ME ++ -- Me ++ -- + -- -- -- -- -- ++ ME -- -- -- SP + ME -- -- Me -- -- -- __ -- -- SP + -- -- ME ++ -- MS -- Me -- -- -- + -- -- -- ++ MB -- MB ++ ++ ++ ++ +""" +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 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, K5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, K6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, K7, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Z18'] + MOVABLE_ENEMY_TURN = (3,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.073, 1.093) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.038, 1.057) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.008, 1.026) + + +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/war_archives_20200603_cn/sp2.py b/campaign/war_archives_20200603_cn/sp2.py new file mode 100644 index 000000000..4d69a20a9 --- /dev/null +++ b/campaign/war_archives_20200603_cn/sp2.py @@ -0,0 +1,76 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.logger import logger +from module.map.map_base import CampaignMap +from module.map.map_grids import RoadGrids, SelectedGrids + +from .sp1 import Config as ConfigBase + +MAP = CampaignMap('SP2') +MAP.shape = 'K7' +MAP.camera_data = ['D2', 'D5', 'H2', 'H5'] +MAP.camera_data_spawn_point = ['D2', 'H2'] +MAP.map_data = """ + -- -- ++ ++ SP -- SP ++ ++ ++ ++ + -- ME ++ ++ -- -- -- -- -- ME ME + ME -- -- MS -- -- -- ME -- -- MB + ++ -- -- -- Me -- Me ++ __ -- MB + -- -- ++ ++ ++ MS -- ++ -- -- ME + -- ME -- 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 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, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, K5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, K6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, K7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Z2'] + MOVABLE_ENEMY_TURN = (3,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.135, 1.157) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.098, 1.118) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.066, 1.085) + + +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/war_archives_20200603_cn/sp3.py b/campaign/war_archives_20200603_cn/sp3.py new file mode 100644 index 000000000..0b6442b84 --- /dev/null +++ b/campaign/war_archives_20200603_cn/sp3.py @@ -0,0 +1,77 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.logger import logger +from module.map.map_base import CampaignMap +from module.map.map_grids import RoadGrids, SelectedGrids + +from .sp1 import Config as ConfigBase + +MAP = CampaignMap('SP3') +MAP.shape = 'K7' +MAP.camera_data = ['D2', 'D5', 'H2', 'H5'] +MAP.camera_data_spawn_point = ['D2'] +MAP.map_data = """ + ++ ++ ++ MS ++ ME -- ME ++ ++ ++ + SP -- -- -- -- -- MS -- ME Me -- + SP -- -- -- -- Me -- __ -- -- ME + ++ ++ ++ MS -- ++ ++ -- Me ++ -- + MB MB ++ -- Me ++ ++ -- -- ++ ++ + -- -- -- -- -- -- -- -- -- ME -- + ME 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 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, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, K5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, K6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, K7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Z19'] + MOVABLE_ENEMY_TURN = (3,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.168, 1.190) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.130, 1.150) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.097, 1.116) + + +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/config/template.json b/config/template.json index bebcdad9c..4a79b18c8 100644 --- a/config/template.json +++ b/config/template.json @@ -77,11 +77,10 @@ "ScreenshotDedithering": false, "AdbRestart": false }, - "RestartEmulator": { - "Enable": false, - "EmulatorData": "emulator:\nname:\npath:", - "ErrorRestart": false, - "DailyRestart": false + "EmulatorInfo": { + "Emulator": "auto", + "name": null, + "path": null }, "Error": { "HandleError": true, @@ -379,8 +378,9 @@ }, "GemsFarming": { "ChangeFlagship": "ship", - "ChangeVanguard": "ship", "CommonCV": "any", + "ChangeVanguard": "ship", + "CommonDD": "any", "CommissionLimit": true }, "Campaign": { diff --git a/deploy/Windows/adb.py b/deploy/Windows/adb.py new file mode 100644 index 000000000..d995b6b63 --- /dev/null +++ b/deploy/Windows/adb.py @@ -0,0 +1,74 @@ +import logging +import os + +from deploy.Windows.emulator import EmulatorManager +from deploy.Windows.logger import Progress, logger + + +def show_fix_tip(module): + logger.info(f""" + To fix this: + 1. Open console.bat + 2. Execute the following commands: + pip uninstall -y {module} + pip install --no-cache-dir {module} + 3. Re-open Alas.exe + """) + + +class AdbManager(EmulatorManager): + def adb_install(self): + logger.hr('Start ADB service', 0) + + if self.ReplaceAdb: + logger.hr('Replace ADB', 1) + self.adb_replace() + Progress.AdbReplace() + if self.AutoConnect: + logger.hr('ADB Connect', 1) + self.brute_force_connect() + Progress.AdbConnect() + + if False: + logger.hr('Uiautomator2 Init', 1) + try: + import adbutils + from uiautomator2 import init + except ModuleNotFoundError as e: + message = str(e) + for module in ['apkutils2', 'progress']: + # ModuleNotFoundError: No module named 'apkutils2' + # ModuleNotFoundError: No module named 'progress.bar' + if module in message: + show_fix_tip(module) + exit(1) + raise + + # Remove global proxies, or uiautomator2 will go through it + for k in list(os.environ.keys()): + if k.lower().endswith('_proxy'): + del os.environ[k] + + for device in adbutils.adb.iter_device(): + initer = init.Initer(device, loglevel=logging.DEBUG) + # MuMu X has no ro.product.cpu.abi, pick abi from ro.product.cpu.abilist + if initer.abi not in ['x86_64', 'x86', 'arm64-v8a', 'armeabi-v7a', 'armeabi']: + initer.abi = initer.abis[0] + initer.set_atx_agent_addr('127.0.0.1:7912') + + for _ in range(2): + try: + initer.install() + break + except AssertionError: + logger.info(f'AssertionError when installing uiautomator2 on device {device.serial}') + logger.info('If you are using BlueStacks or LD player or WSA, ' + 'please enable ADB in the settings of your emulator') + exit(1) + except ConnectionError: + if _ == 1: + raise + init.GITHUB_BASEURL = 'http://tool.appetizer.io/openatx' + + initer._device.shell(["rm", "/data/local/tmp/minicap"]) + initer._device.shell(["rm", "/data/local/tmp/minicap.so"]) diff --git a/deploy/Windows/alas.py b/deploy/Windows/alas.py new file mode 100644 index 000000000..9f8873938 --- /dev/null +++ b/deploy/Windows/alas.py @@ -0,0 +1,80 @@ +import os +import time +import typing as t + +from deploy.Windows.config import DeployConfig +from deploy.Windows.logger import Progress, logger +from deploy.Windows.utils import DataProcessInfo, cached_property, iter_process + + +class AlasManager(DeployConfig): + @cached_property + def alas_folder(self): + return [ + self.filepath(self.PythonExecutable), + self.root_filepath + ] + + @cached_property + def self_pid(self): + return os.getpid() + + def list_process(self) -> t.List[DataProcessInfo]: + logger.info('List process') + process = list(iter_process()) + logger.info(f'Found {len(process)} processes') + return process + + def iter_process_by_names(self, names, in_alas=False) -> t.Iterable[DataProcessInfo]: + """ + Args: + names (str, list[str]): process name, such as 'alas.exe' + in_alas (bool): If the output process must in Alas + + Yields: + DataProcessInfo: + """ + if not isinstance(names, list): + names = [names] + try: + for proc in self.list_process(): + + if not (proc.name and proc.name in names): + continue + if proc.pid == self.self_pid: + continue + if in_alas: + cmdline = proc.cmdline.replace(r"\\", "/").replace("\\", "/") + for folder in self.alas_folder: + if folder in cmdline: + yield proc + else: + yield proc + except Exception as e: + logger.info(str(e)) + return False + + def kill_process(self, process: DataProcessInfo): + self.execute(f'taskkill /f /t /pid {process.pid}', allow_failure=True, output=False) + + def alas_kill(self): + for _ in range(10): + logger.hr(f'Kill existing Alas', 0) + proc_list = list(self.iter_process_by_names(['python.exe'], in_alas=True)) + if not len(proc_list): + Progress.KillExisting() + return True + for proc in proc_list: + logger.info(proc) + self.kill_process(proc) + + logger.warning('Unable to kill existing Alas, skip') + Progress.KillExisting() + return False + + +if __name__ == '__main__': + self = AlasManager() + start = time.time() + self.alas_kill() + print(time.time() - start) diff --git a/deploy/Windows/app.py b/deploy/Windows/app.py new file mode 100644 index 000000000..750a50d13 --- /dev/null +++ b/deploy/Windows/app.py @@ -0,0 +1,57 @@ +import filecmp +import os +import shutil + +from deploy.Windows.config import DeployConfig +from deploy.Windows.logger import Progress, logger + + +class AppManager(DeployConfig): + @staticmethod + def app_asar_replace(folder, path='./toolkit/WebApp/resources/app.asar'): + """ + Args: + folder (str): Path to AzurLaneAutoScript + path (str): Path from AzurLaneAutoScript to app.asar + + Returns: + bool: If updated. + """ + source = os.path.abspath(os.path.join(folder, path)) + logger.info(f'Old file: {source}') + + try: + import alas_webapp + except ImportError: + logger.info(f'Dependency alas_webapp not exists, skip updating') + return False + + update = alas_webapp.app_file() + logger.info(f'New version: {alas_webapp.__version__}') + logger.info(f'New file: {update}') + + if os.path.exists(source): + if filecmp.cmp(source, update, shallow=True): + logger.info('app.asar is already up to date') + return False + else: + # Keyword "Update app.asar" is used in AlasApp + # to determine whether there is a hot update + logger.info(f'Update app.asar {update} -----> {source}') + os.remove(source) + shutil.copy(update, source) + return True + else: + logger.info(f'{source} not exists, skip updating') + return False + + def app_update(self): + logger.hr(f'Update app', 0) + + if not self.AppAsarUpdate: + logger.info('AppAsarUpdate is disabled, skip') + Progress.UpdateAlasApp() + return False + + # self.app_asar_replace(os.getcwd()) + # Progress.UpdateAlasApp() diff --git a/deploy/Windows/config.py b/deploy/Windows/config.py new file mode 100644 index 000000000..adf91ee67 --- /dev/null +++ b/deploy/Windows/config.py @@ -0,0 +1,226 @@ +import copy +import os +import subprocess +from typing import Optional, Union + +from deploy.Windows.logger import logger +from deploy.Windows.utils import DEPLOY_CONFIG, DEPLOY_TEMPLATE, cached_property, poor_yaml_read, poor_yaml_write + + +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 = 22367 + Language: str = "en-US" + Theme: str = "default" + DpiScaling: bool = True + Password: Optional[str] = None + CDN: Union[str, bool] = False + Run: Optional[str] = None + AppAsarUpdate: bool = True + NoSandbox: bool = True + + # 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.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() + + 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[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) + + def write(self): + poor_yaml_write(self.config, self.file) + + def filepath(self, path): + """ + Args: + path (str): + + Returns: + str: Absolute filepath. + """ + if os.path.isabs(path): + return path + + return ( + os.path.abspath(os.path.join(self.root_filepath, path)) + .replace(r"\\", "/") + .replace("\\", "/") + ) + + @cached_property + def root_filepath(self): + return ( + os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) + .replace(r"\\", "/") + .replace("\\", "/") + ) + + @cached_property + def adb(self) -> str: + exe = self.filepath(self.AdbExecutable) + if os.path.exists(exe): + return exe + + logger.warning(f'AdbExecutable: {exe} does not exists, use `adb` instead') + return 'adb' + + @cached_property + def git(self) -> str: + exe = self.filepath(self.GitExecutable) + if os.path.exists(exe): + return exe + + logger.warning(f'GitExecutable: {exe} does not exists, use `git` instead') + return 'git' + + @cached_property + def python(self) -> str: + return self.filepath(self.PythonExecutable) + + @cached_property + def requirements_file(self) -> str: + if self.RequirementsFile == 'requirements.txt': + return 'requirements.txt' + else: + return self.filepath(self.RequirementsFile) + + 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 subprocess_execute(self, cmd, timeout=10): + """ + Args: + cmd (list[str]): + timeout: + + Returns: + str: + """ + logger.info(' '.join(cmd)) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) + try: + stdout, stderr = process.communicate(timeout=timeout) + process.kill() + except subprocess.TimeoutExpired: + process.kill() + stdout, stderr = process.communicate() + logger.info(f'TimeoutExpired, stdout={stdout}, stderr={stderr}') + return stdout.decode() + + 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/Windows/emulator.py b/deploy/Windows/emulator.py new file mode 100644 index 000000000..3dfc74f15 --- /dev/null +++ b/deploy/Windows/emulator.py @@ -0,0 +1,171 @@ +import asyncio +import filecmp +import os +import shutil +import sys +import typing as t +from dataclasses import dataclass + +from deploy.Windows.alas import AlasManager +from deploy.Windows.logger import logger +from deploy.Windows.utils import cached_property + +if sys.platform.startswith("win"): + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + +@dataclass +class DataAdbDevice: + serial: str + status: str + + +class EmulatorManager(AlasManager): + @cached_property + def emulator_manager(self): + from module.device.platform.emulator_windows import EmulatorManager + return EmulatorManager() + + def adb_kill(self): + # Just kill it, because some adb don't obey. + logger.hr('Kill all known ADB', level=2) + for proc in self.iter_process_by_names([ + # Most emulator use this + 'adb.exe', + # NoxPlayer 夜神模拟器 + 'nox_adb.exe', + # MumuPlayer MuMu模拟器 + 'adb_server.exe', + # Bluestacks 蓝叠模拟器 + 'HD-Adb.exe' + ]): + logger.info(proc) + self.kill_process(proc) + + def adb_devices(self): + """ + Returns: + list[DataAdbDevice]: Connected devices in adb + """ + logger.hr('Adb deivces', level=2) + result = self.subprocess_execute([self.adb, 'devices']) + devices = [] + for line in result.replace('\r\r\n', '\n').replace('\r\n', '\n').split('\n'): + if line.startswith('List') or '\t' not in line: + continue + serial, status = line.split('\t') + device = DataAdbDevice( + serial=serial, + status=status, + ) + devices.append(device) + logger.info(device) + return devices + + def brute_force_connect(self): + """ + Brute-force connect all available emulator instances + """ + devices = self.adb_devices() + + # Disconnect offline devices + for device in devices: + if device.status == 'offline': + self.subprocess_execute([self.adb, 'disconnect', device.serial]) + + # Get serial + list_serial = self.emulator_manager.all_emulator_serials + + logger.hr('Brute force connect', level=2) + + async def _connect(serial): + try: + await asyncio.create_subprocess_exec(self.adb, 'connect', serial) + except Exception as e: + logger.info(e) + + async def connect(): + await asyncio.gather( + *[_connect(serial) for serial in list_serial] + ) + + asyncio.run(connect()) + + return self.adb_devices() + + @staticmethod + def adb_path_to_backup(adb, new_backup=True): + """ + Args: + adb (str): Filepath to an adb binary + new_backup (bool): True to return a new backup path, + False to return an existing backup + + Returns: + str: Filepath to its backup file + """ + for n in range(10): + backup = f'{adb}.bak{n}' if n else f'{adb}.bak' + if os.path.exists(backup): + if new_backup: + continue + else: + return backup + else: + if new_backup: + return backup + else: + continue + + # Too many backups, override the first one + return f'{adb}.bak' + + def iter_adb_to_replace(self) -> t.Iterable[str]: + for adb in self.emulator_manager.all_adb_binaries: + if filecmp.cmp(adb, self.adb, shallow=True): + logger.info(f'{adb} is same as {self.adb}, skip') + continue + else: + yield adb + + def adb_replace(self): + """ + Backup the adb in emulator folder to xxx.bak, replace it with your adb. + `adb kill-server` must be called before replacing. + """ + replace = list(self.iter_adb_to_replace()) + if not replace: + logger.info('No need to replace') + return + + self.adb_kill() + for adb in replace: + logger.info(f'Replacing {adb}') + bak = self.adb_path_to_backup(adb, new_backup=True) + logger.info(f'{adb} -----> {bak}') + shutil.move(adb, bak) + logger.info(f'{self.adb} -----> {adb}') + shutil.copy(self.adb, adb) + + def adb_recover(self): + """ + Revert `adb_replace()` + """ + for adb in self.emulator_manager.all_adb_binaries: + logger.info(f'Recovering {adb}') + bak = self.adb_path_to_backup(adb, new_backup=False) + if os.path.exists(bak): + logger.info(f'Delete {adb}') + if os.path.exists(adb): + os.remove(adb) + logger.info(f'{bak} -----> {adb}') + shutil.move(bak, adb) + else: + logger.info('No backup available, skip') + continue + + +if __name__ == '__main__': + os.chdir(os.path.join(os.path.dirname(__file__), '../../')) + self = EmulatorManager() + self.brute_force_connect() diff --git a/deploy/Windows/git.py b/deploy/Windows/git.py new file mode 100644 index 000000000..c873f6b35 --- /dev/null +++ b/deploy/Windows/git.py @@ -0,0 +1,162 @@ +import configparser +import os + +from deploy.Windows.config import DeployConfig +from deploy.Windows.logger import Progress, logger +from deploy.Windows.utils import cached_property +from deploy.git_over_cdn.client import GitOverCdnClient + + +class GitConfigParser(configparser.ConfigParser): + def check(self, section, option, value): + result = self.get(section, option, fallback=None) + if result == value: + logger.info(f'Git config {section}.{option} = {value}') + return True + else: + return False + + +class GitOverCdnClientWindows(GitOverCdnClient): + def update(self, *args, **kwargs): + Progress.GitInit() + _ = super().update(*args, **kwargs) + Progress.GitShowVersion() + return _ + + @cached_property + def latest_commit(self) -> str: + _ = super().latest_commit + Progress.GitLatestCommit() + return _ + + def download_pack(self): + _ = super().download_pack() + Progress.GitDownloadPack() + return _ + + +class GitManager(DeployConfig): + @staticmethod + def remove(file): + try: + os.remove(file) + logger.info(f'Removed file: {file}') + except FileNotFoundError: + logger.info(f'File not found: {file}') + + @cached_property + def git_config(self): + conf = GitConfigParser() + conf.read('./.git/config') + return conf + + 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.remove('./.git/ORIG_HEAD') + self.execute(f'"{self.git}" init') + Progress.GitInit() + + logger.hr('Set Git Proxy', 1) + if proxy: + if not self.git_config.check('http', 'proxy', value=proxy): + self.execute(f'"{self.git}" config --local http.proxy {proxy}') + if not self.git_config.check('https', 'proxy', value=proxy): + self.execute(f'"{self.git}" config --local https.proxy {proxy}') + else: + if not self.git_config.check('http', 'proxy', value=None): + self.execute(f'"{self.git}" config --local --unset http.proxy', allow_failure=True) + if not self.git_config.check('https', 'proxy', value=None): + self.execute(f'"{self.git}" config --local --unset https.proxy', allow_failure=True) + + if ssl_verify: + if not self.git_config.check('http', 'sslVerify', value='true'): + self.execute(f'"{self.git}" config --local http.sslVerify true', allow_failure=True) + else: + if not self.git_config.check('http', 'sslVerify', value='false'): + self.execute(f'"{self.git}" config --local http.sslVerify false', allow_failure=True) + Progress.GitSetConfig() + + logger.hr('Set Git Repository', 1) + if not self.git_config.check(f'remote "{source}"', 'url', value=repo): + if not self.execute(f'"{self.git}" remote set-url {source} {repo}', allow_failure=True): + self.execute(f'"{self.git}" remote add {source} {repo}') + Progress.GitSetRepo() + + logger.hr('Fetch Repository Branch', 1) + self.execute(f'"{self.git}" fetch {source} {branch}') + Progress.GitFetch() + + 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}') + Progress.GitReset() + # Since `git fetch` is already called, checkout is faster + if not self.execute(f'"{self.git}" checkout {branch}', allow_failure=True): + self.execute(f'"{self.git}" pull --ff-only {source} {branch}') + Progress.GitCheckout() + + logger.hr('Show Version', 1) + self.execute(f'"{self.git}" --no-pager log --no-merges -1') + Progress.GitShowVersion() + + @property + def goc_client(self): + client = GitOverCdnClient( + url='https://vip.123pan.cn/1815343254/pack/LmeSzinc_StarRailCopilot_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') + Progress.GitShowVersion() + 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, + ) diff --git a/deploy/Windows/installer_test.py b/deploy/Windows/installer_test.py new file mode 100644 index 000000000..a3995e528 --- /dev/null +++ b/deploy/Windows/installer_test.py @@ -0,0 +1,117 @@ +import time + +from deploy.Windows.logger import logger + +output = r""" +Process: [ 0% ] +./toolkit/Lib/site-packages/requests/sessions.py trust_env already patched +./toolkit/Lib/site-packages/pip/_vendor/requests/sessions.py trust_env already patched +./toolkit/Lib/site-packages/uiautomator2/init.py minicap_urls no need to patch +./toolkit/Lib/site-packages/uiautomator2/init.py appdir already patched +./toolkit/Lib/site-packages/adbutils/mixin.py apkutils2 no need to patch +Process: [ 5% ] +==================== SHOW DEPLOY CONFIG ==================== +Repository: https://e.coding.net/llop18870/alas/AzurLaneAutoScript.git +Branch: feature +PypiMirror: https://pypi.tuna.tsinghua.edu.cn/simple +Language: zh-CN +Rest of the configs are the same as default +Process: [ 10% ] ++---------------------------------------------------+ +| UPDATE ALAS | ++---------------------------------------------------+ +==================== GIT INIT ==================== +"D:/AlasRelease/AzurLaneAutoScript/toolkit/Git/mingw64/bin/git.exe" init +Reinitialized existing Git repository in D:/AlasRelease/AzurLaneAutoScript/.git/ +[ success ] +Process: [ 15% ] +==================== SET GIT PROXY ==================== +Git config http.proxy = None +Git config https.proxy = None +Git config http.sslVerify = true +Process: [ 18% ] +==================== SET GIT REPOSITORY ==================== +Git config remote "origin".url = https://e.coding.net/llop18870/alas/AzurLaneAutoScript.git +Process: [ 20% ] +==================== FETCH REPOSITORY BRANCH ==================== +"D:/AlasRelease/AzurLaneAutoScript/toolkit/Git/mingw64/bin/git.exe" fetch origin feature +From https://e.coding.net/llop18870/alas/AzurLaneAutoScript + * branch feature -> FETCH_HEAD +[ success ] +Process: [ 40% ] +==================== PULL REPOSITORY BRANCH ==================== +"D:/AlasRelease/AzurLaneAutoScript/toolkit/Git/mingw64/bin/git.exe" reset --hard origin/feature +HEAD is now at 11595208 Fix: No process cache since it's fast already +[ success ] +Process: [ 45% ] +"D:/AlasRelease/AzurLaneAutoScript/toolkit/Git/mingw64/bin/git.exe" checkout feature +Already on 'feature' +Your branch is up to date with 'origin/feature'. +[ success ] +Process: [ 48% ] +==================== SHOW VERSION ==================== +"D:/AlasRelease/AzurLaneAutoScript/toolkit/Git/mingw64/bin/git.exe" --no-pager log --no-merges -1 +commit 11595208afe1ca1b3d48f5722795ce2387bccd87 (HEAD -> feature, origin/feature) +Author: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> +Date: Tue Apr 4 01:17:09 2023 +0800 + + Fix: No process cache since it's fast already +[ success ] +Process: [ 50% ] ++----------------------------------------------------------+ +| KILL EXISTING ALAS | ++----------------------------------------------------------+ +List process +Found 264 processes +Process: [ 60% ] ++-----------------------------------------------------------+ +| UPDATE DEPENDENCIES | ++-----------------------------------------------------------+ +All dependencies installed +Process: [ 70% ] ++--------------------------------------------------+ +| UPDATE APP | ++--------------------------------------------------+ +Old file: D:\AlasRelease\AzurLaneAutoScript\toolkit\WebApp\resources\app.asar +New version: 0.3.7 +New file: D:\AlasRelease\AzurLaneAutoScript\toolkit\lib\site-packages\alas_webapp\app.asar +app.asar is already up to date +Process: [ 75% ] ++---------------------------------------------------------+ +| START ADB SERVICE | ++---------------------------------------------------------+ +==================== REPLACE ADB ==================== +No need to replace +Process: [ 90% ] +==================== ADB CONNECT ==================== +-------------------- ADB DEIVCES -------------------- +D:/AlasRelease/AzurLaneAutoScript/toolkit/Lib/site-packages/adbutils/binaries/adb.exe devices +DataAdbDevice(serial='127.0.0.1:16384', status='device') +DataAdbDevice(serial='127.0.0.1:16480', status='device') +DataAdbDevice(serial='127.0.0.1:7555', status='device') +Process: [ 92% ] +-------------------- BRUTE FORCE CONNECT -------------------- +already connected to 127.0.0.1:7555 +already connected to 127.0.0.1:16384 +already connected to 127.0.0.1:16480 +already connected to 127.0.0.1:7555 +Process: [ 98% ] +-------------------- ADB DEIVCES -------------------- +D:/AlasRelease/AzurLaneAutoScript/toolkit/Lib/site-packages/adbutils/binaries/adb.exe devices +DataAdbDevice(serial='127.0.0.1:16384', status='device') +DataAdbDevice(serial='127.0.0.1:16480', status='device') +DataAdbDevice(serial='127.0.0.1:7555', status='device') +Process: [ 100% ] +中文测试,!@#nfoir +""" + + +def run(): + for row in output.split('\n'): + time.sleep(0.05) + if row: + logger.info(row) + + +if __name__ == '__main__': + run() diff --git a/deploy/Windows/logger.py b/deploy/Windows/logger.py new file mode 100644 index 000000000..a0607dc31 --- /dev/null +++ b/deploy/Windows/logger.py @@ -0,0 +1,75 @@ +import logging +import os +import sys + +os.chdir(os.path.join(os.path.dirname(__file__), '../../')) + +logger = logging.getLogger("deploy") +_logger = logger + +formatter = logging.Formatter(fmt="%(message)s") +hdlr = logging.StreamHandler(stream=sys.stdout) +hdlr.setFormatter(formatter) +logger.addHandler(hdlr) +logger.setLevel(logging.INFO) + + +def hr(title, level=3): + if logger is not _logger: + return logger.hr(title, level) + + title = str(title).upper() + if level == 0: + middle = "|" + " " * 20 + title + " " * 20 + "|" + border = "+" + "-" * (len(middle) - 2) + "+" + logger.info(border) + logger.info(middle) + logger.info(border) + if level == 1: + logger.info("=" * 20 + " " + title + " " + "=" * 20) + if level == 2: + logger.info("-" * 20 + " " + title + " " + "-" * 20) + if level == 3: + logger.info(f"<<< {title} >>>") + + +def attr(name, text): + print(f'[{name}] {text}') + + +logger.hr = hr +logger.attr = attr + + +class Percentage: + def __init__(self, progress): + self.progress = progress + + def __call__(self, *args, **kwargs): + logger.info(f'Process: [ {self.progress}% ]') + + +class Progress: + Start = Percentage(0) + ShowDeployConfig = Percentage(10) + + GitInit = Percentage(12) + GitSetConfig = Percentage(13) + GitSetRepo = Percentage(15) + GitFetch = Percentage(40) + GitReset = Percentage(45) + GitCheckout = Percentage(48) + GitShowVersion = Percentage(50) + + GitLatestCommit = Percentage(25) + GitDownloadPack = Percentage(40) + + KillExisting = Percentage(60) + UpdateDependency = Percentage(70) + UpdateAlasApp = Percentage(75) + + AdbReplace = Percentage(80) + AdbConnect = Percentage(95) + + # Must have a 100% + Finish = Percentage(100) diff --git a/deploy/Windows/patch.py b/deploy/Windows/patch.py new file mode 100644 index 000000000..a1d3be03f --- /dev/null +++ b/deploy/Windows/patch.py @@ -0,0 +1,154 @@ +import os +import re + +from deploy.Windows.logger import logger + + +def patch_trust_env(file): + """ + People use proxies, but they never realize that proxy software leaves a + global proxy pointing to itself even when the software is not running. + In most situations we set `session.trust_env = False` in requests, but this + does not effect the `pip` command. + + To handle untrusted user environment for good. We patch the code file in + requests directly. Of course, the patch only effect the python env inside + Alas. + + Returns: + bool: If patched. + """ + try: + with open(file, 'r', encoding='utf-8') as f: + content = f.read() + except FileNotFoundError: + logger.info(f'{file} trust_env not exist') + return + + if re.search('self.trust_env = True', content): + content = re.sub('self.trust_env = True', 'self.trust_env = False', content) + with open(file, 'w', encoding='utf-8') as f: + f.write(content) + logger.info(f'{file} trust_env patched') + elif re.search('self.trust_env = False', content): + logger.info(f'{file} trust_env already patched') + else: + logger.info(f'{file} trust_env is not in the file') + + +def check_running_directory(): + """ + An fool-proof mechanism. + Show error if user is running Easy Install in compressing software, + since Alas can't install in temp directories. + """ + file = __file__.replace(r"\\", "/").replace("\\", "/") + # C:/Users//AppData/Local/Temp/360zip$temp/360$3/AzurLaneAutoScript + if 'Temp/360zip' in file: + logger.critical('请先解压Alas的压缩包,再安装Alas') + exit(1) + # C:/Users//AppData/Local/Temp/Rar$EXa9248.23428/AzurLaneAutoScript + if 'Temp/Rar' in file or 'Local/Temp' in file: + logger.critical('Please unzip ALAS installer first') + exit(1) + + +def patch_uiautomator2(): + """ + uiautomator2 download assets from https://tool.appetizer.io first then fallback to https://github.com/openatx. + https://tool.appetizer.io is added to bypass the wall in China but https://tool.appetizer.io is slow outside of CN + plus some CN users cannot access it for unknown reason. + + So we patch `uiautomator2/init.py` to a local assets cache `uiautomator2cache/cache`. + appdir = os.path.join(os.path.expanduser('~'), '.uiautomator2') + to: + appdir = os.path.join(__file__, '../../uiautomator2cache') + + And we also remove minicap installations since emulators doesn't need it. + for url in self.minicap_urls: + self.push_url(url) + to: + for url in []: + self.push_url(url) + """ + init_file = './toolkit/Lib/site-packages/uiautomator2/init.py' + cache_dir = './toolkit/Lib/site-packages/uiautomator2cache/cache' + appdir = "os.path.join(__file__, '../../uiautomator2cache')" + + modified = False + try: + with open(init_file, 'r', encoding='utf-8') as f: + content = f.read() + except FileNotFoundError: + logger.info(f'{init_file} not exist') + return + + # Patch minicap_urls + res = re.search(r'self.minicap_urls', content) + if res: + content = re.sub(r'self.minicap_urls', '[]', content) + modified = True + logger.info(f'{init_file} minicap_urls patched') + else: + logger.info(f'{init_file} minicap_urls no need to patch') + + # Patch appdir + if os.path.exists(cache_dir): + res = re.search(r'appdir ?=(.*)\n', content) + if res: + prev = res.group(1).strip() + if prev == appdir: + logger.info(f'{init_file} appdir already patched') + else: + content = re.sub(r'appdir ?=.*\n', f'appdir = {appdir}\n', content) + modified = True + logger.info(f'{init_file} appdir patched') + else: + logger.info(f'{init_file} appdir not found') + else: + logger.info('uiautomator2cache is not installed skip patching') + + # Save file + if modified: + with open(init_file, 'w', encoding='utf-8') as f: + f.write(content) + logger.info(f'{init_file} content saved') + + +def patch_apkutils2(): + """ + `adbutils/mixin.py` `ShellMixin.install` imports `apkutils2`, but `apkutils2` does not provide wheel files, + it may failed to install for unknown reasons. Since we never used that method, we just remove the import. + """ + mixin = './toolkit/Lib/site-packages/adbutils/mixin.py' + + try: + with open(mixin, 'r', encoding='utf-8') as f: + content = f.read() + except FileNotFoundError: + logger.info(f'{mixin} not exist') + return + + res = re.search(r'import apkutils2', content) + if res: + content = re.sub(r'import apkutils2', '', content) + with open(mixin, 'w', encoding='utf-8') as f: + f.write(content) + logger.info(f'{mixin} apkutils2 patched') + else: + logger.info(f'{mixin} apkutils2 no need to patch') + + +def pre_checks(): + check_running_directory() + + # patch_trust_env + patch_trust_env('./toolkit/Lib/site-packages/requests/sessions.py') + patch_trust_env('./toolkit/Lib/site-packages/pip/_vendor/requests/sessions.py') + + patch_uiautomator2() + patch_apkutils2() + + +if __name__ == '__main__': + pre_checks() diff --git a/deploy/Windows/pip.py b/deploy/Windows/pip.py new file mode 100644 index 000000000..cedb0a19a --- /dev/null +++ b/deploy/Windows/pip.py @@ -0,0 +1,132 @@ +import os +import re +import typing as t +from dataclasses import dataclass +from urllib.parse import urlparse + +from deploy.Windows.config import DeployConfig +from deploy.Windows.logger import logger, Progress +from deploy.Windows.utils import cached_property + + +@dataclass +class DataDependency: + name: str + version: str + + def __post_init__(self): + # uvicorn[standard] -> uvicorn + self.name = re.sub(r'\[.*\]', '', self.name) + # opencv_python -> opencv-python + self.name = self.name.replace('_', '-').strip() + # PyYaml -> pyyaml + self.name = self.name.lower() + self.version = self.version.strip() + self.version = re.sub(r'\.0$', '', self.version) + + @cached_property + def pretty_name(self): + return f'{self.name}=={self.version}' + + def __str__(self): + return self.pretty_name + + __repr__ = __str__ + + def __eq__(self, other): + return str(self) == str(other) + + def __hash__(self): + return hash(str(self)) + + +class PipManager(DeployConfig): + @cached_property + def pip(self): + return f'"{self.python}" -m pip' + + @cached_property + def python_site_packages(self): + return os.path.abspath(os.path.join(self.python, '../Lib/site-packages')) \ + .replace(r"\\", "/").replace("\\", "/") + + @cached_property + def set_installed_dependency(self) -> t.Set[DataDependency]: + data = [] + regex = re.compile(r'(.*)-(.*).dist-info') + try: + for name in os.listdir(self.python_site_packages): + res = regex.search(name) + if res: + dep = DataDependency(name=res.group(1), version=res.group(2)) + data.append(dep) + except FileNotFoundError: + logger.info(f'Directory not found: {self.python_site_packages}') + return set(data) + + @cached_property + def set_required_dependency(self) -> t.Set[DataDependency]: + data = [] + regex = re.compile('(.*)==(.*)[ ]*#') + file = self.filepath('./requirements.txt') + try: + with open(file, 'r', encoding='utf-8') as f: + for line in f.readlines(): + res = regex.search(line) + if res: + dep = DataDependency(name=res.group(1), version=res.group(2)) + data.append(dep) + except FileNotFoundError: + logger.info(f'File not found: {file}') + return set(data) + + @cached_property + def set_dependency_to_install(self) -> t.Set[DataDependency]: + """ + A poor dependency comparison, but much much faster than `pip install` and `pip list` + """ + data = [] + for dep in self.set_required_dependency: + if dep not in self.set_installed_dependency: + data.append(dep) + return set(data) + + def pip_install(self): + logger.hr('Update Dependencies', 0) + + if not self.InstallDependencies: + logger.info('InstallDependencies is disabled, skip') + Progress.UpdateDependency() + return + + if not len(self.set_dependency_to_install): + logger.info('All dependencies installed') + Progress.UpdateDependency() + return + else: + logger.info(f'Dependencies to install: {self.set_dependency_to_install}') + + # Install + logger.hr('Check Python', 1) + self.execute(f'"{self.python}" --version') + + arg = [] + if self.PypiMirror: + mirror = self.PypiMirror + arg += ['-i', mirror] + # Trust http mirror or skip ssl verify + if 'http:' in mirror or not self.SSLVerify: + arg += ['--trusted-host', urlparse(mirror).hostname] + elif not self.SSLVerify: + arg += ['--trusted-host', 'pypi.org'] + arg += ['--trusted-host', 'files.pythonhosted.org'] + + # Don't update pip, just leave it. + # logger.hr('Update pip', 1) + # self.execute(f'"{self.pip}" install --upgrade pip{arg}') + arg += ['--disable-pip-version-check'] + + logger.hr('Update Dependencies', 1) + arg = ' ' + ' '.join(arg) if arg else '' + self.execute(f'{self.pip} install -r {self.requirements_file}{arg}') + Progress.UpdateDependency() diff --git a/deploy/Windows/template.yaml b/deploy/Windows/template.yaml new file mode 100644 index 000000000..40e5fd1d6 --- /dev/null +++ b/deploy/Windows/template.yaml @@ -0,0 +1,166 @@ +Deploy: + Git: + # URL of AzurLaneAutoScript repository + # [CN user] Use 'cn' to get update from git-over-cdn service + # [Other] Use 'global' to get update from https://github.com/LmeSzinc/StarRailCopilot + Repository: 'global' + # Branch of Alas + # [Developer] Use 'dev', 'app', etc, to try new features + # [Other] Use 'master', the stable branch + Branch: 'master' + # Filepath of git executable `git.exe` + # [Easy installer] Use './toolkit/Git/mingw64/bin/git.exe' + # [Other] Use you own git + GitExecutable: './toolkit/Git/mingw64/bin/git.exe' + # Set git proxy + # [CN user] Use your local http proxy (http://127.0.0.1:{port}) or socks5 proxy (socks5://127.0.0.1:{port}) + # [Other] Use null + GitProxy: null + # Set SSL Verify + # [In most cases] Use true + # [Other] Use false to when connected to an untrusted network + SSLVerify: true + # Update Alas at startup + # [In most cases] Use true + AutoUpdate: true + # Whether to keep local changes during update + # User settings, logs and screenshots will be kept, no mather this is true or false + # [Developer] Use true, if you modified the code + # [Other] Use false + KeepLocalChanges: false + + Python: + # Filepath of python executable `python.exe` + # [Easy installer] Use './toolkit/python.exe' + # [Other] Use you own python, and its version should be 3.7.6 64bit + PythonExecutable: './toolkit/python.exe' + # URL of pypi mirror + # [CN user] Use 'https://pypi.tuna.tsinghua.edu.cn/simple' for faster and more stable download + # [Other] Use null + PypiMirror: null + # Install dependencies at startup + # [In most cases] Use true + InstallDependencies: true + # Path to requirements.txt + # [In most cases] Use 'requirements.txt' + # [In AidLux] Use './deploy/AidLux/{version}/requirements.txt', version is default to 0.92 + RequirementsFile: 'requirements.txt' + + Adb: + # Filepath of ADB executable `adb.exe` + # [Easy installer] Use './toolkit/Lib/site-packages/adbutils/binaries/adb.exe' + # [Other] Use you own latest ADB, but not the ADB in your emulator + AdbExecutable: './toolkit/Lib/site-packages/adbutils/binaries/adb.exe' + # Whether to replace ADB + # Chinese emulators (NoxPlayer, LDPlayer, MemuPlayer, MuMuPlayer) use their own ADB, instead of the latest. + # Different ADB servers will terminate each other at startup, resulting in disconnection. + # For compatibility, we have to replace them all. + # This will do: + # 1. Terminate current ADB server + # 2. Rename ADB from all emulators to *.bak and replace them by the AdbExecutable set above + # 3. Brute-force connect to all available emulator instances + # [In most cases] Use true + # [In few cases] Use false, if you have other programs using ADB. + ReplaceAdb: true + # Brute-force connect to all available emulator instances + # [In most cases] Use true + AutoConnect: true + # Re-install uiautomator2 + # [In most cases] Use true + InstallUiautomator2: true + + Ocr: + # Run Ocr as a service, can reduce memory usage by not import mxnet everytime you start an alas instance + + # Whether to use ocr server + # [Default] false + UseOcrServer: false + # Whether to start ocr server when start GUI + # [Default] false + StartOcrServer: false + # Port of ocr server runs by GUI + # [Default] 22268 + OcrServerPort: 22268 + # Address of ocr server for alas instance to connect + # [Default] 127.0.0.1:22268 + OcrClientAddress: 127.0.0.1:22268 + + Update: + # Use auto update and builtin updater feature + # This may cause problem https://github.com/LmeSzinc/AzurLaneAutoScript/issues/876 + EnableReload: true + # Check update every X minute + # [Disable] 0 + # [Default] 5 + CheckUpdateInterval: 5 + # Scheduled restart time + # If there are updates, Alas will automatically restart and update at this time every day + # and run all alas instances that running before restarted + # [Disable] null + # [Default] 03:50 + AutoRestartTime: 03:50 + + Misc: + # Enable discord rich presence + DiscordRichPresence: false + + RemoteAccess: + # Enable remote access (using ssh reverse tunnel serve by https://github.com/wang0618/localshare) + # ! You need to set Password below to enable remote access since everyone can access to your alas if they have your url. + # See here (http://app.azurlane.cloud/en.html) for more infomation. + EnableRemoteAccess: false + # Username when login into ssh server + # [Default] null (will generate a random one when startup) + SSHUser: null + # Server to connect + # [Default] null + # [Format] host:port + SSHServer: null + # Filepath of SSH executable `ssh.exe` + # [Default] ssh (find ssh in system PATH) + # If you don't have one, install OpenSSH or download it here (https://github.com/PowerShell/Win32-OpenSSH/releases) + SSHExecutable: ssh + + Webui: + # --host. Host to listen + # [Use IPv6] '::' + # [In most cases] Default to '0.0.0.0' + WebuiHost: 0.0.0.0 + # --port. Port to listen + # You will be able to access webui via `http://{host}:{port}` + # [In most cases] Default to 22367 + WebuiPort: 22367 + # Language to use on web ui + # 'zh-CN' for Chinese simplified + # 'en-US' for English + # 'ja-JP' for Japanese + # 'zh-TW' for Chinese traditional + # 'es-ES' for Spanish + Language: en-US + # Theme of web ui + # 'default' for light theme + # 'dark' for dark theme + Theme: default + # Follow system DPI scaling + # [In most cases] true + # [In few cases] false to make Alas smaller, if you have a low resolution but high DPI scaling. + DpiScaling: true + # --key. Password of web ui + # Useful when expose Alas to the public network + Password: null + # --cdn. Use jsdelivr cdn for pywebio static files (css, js). + # 'true' for jsdelivr cdn + # 'false' for self host cdn (automatically) + # 'https://path.to.your/cdn' to use custom cdn + CDN: false + # --run. Auto-run specified config when startup + # 'null' default no specified config + # '["alas"]' specified "alas" config + # '["alas","alas2"]' specified "alas" "alas2" configs + Run: null + # To update app.asar + # [In most cases] true + AppAsarUpdate: true + # --no-sandbox. https://github.com/electron/electron/issues/30966 + # Some Windows systems cannot call the GPU normally for virtualization, and you need to manually turn off sandbox mode + NoSandbox: false diff --git a/deploy/Windows/utils.py b/deploy/Windows/utils.py new file mode 100644 index 000000000..6b27a3ace --- /dev/null +++ b/deploy/Windows/utils.py @@ -0,0 +1,166 @@ +import os +import re +from dataclasses import dataclass +from typing import Callable, Generic, Iterable, TypeVar + +T = TypeVar("T") + +DEPLOY_CONFIG = './config/deploy.yaml' +DEPLOY_TEMPLATE = './deploy/Windows/template.yaml' + + +class cached_property(Generic[T]): + """ + cached-property from https://github.com/pydanny/cached-property + Add typing support + + A property that is only computed once per instance and then replaces itself + with an ordinary attribute. Deleting the attribute resets the property. + Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76 + """ + + def __init__(self, func: Callable[..., T]): + self.func = func + + def __get__(self, obj, cls) -> T: + if obj is None: + return self + + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +def iter_folder(folder, is_dir=False, ext=None): + """ + Args: + folder (str): + is_dir (bool): True to iter directories only + ext (str): File extension, such as `.yaml` + + Yields: + str: Absolute path of files + """ + for file in os.listdir(folder): + sub = os.path.join(folder, file) + if is_dir: + if os.path.isdir(sub): + yield sub.replace('\\\\', '/').replace('\\', '/') + elif ext is not None: + if not os.path.isdir(sub): + _, extension = os.path.splitext(file) + if extension == ext: + yield os.path.join(folder, file).replace('\\\\', '/').replace('\\', '/') + else: + yield os.path.join(folder, file).replace('\\\\', '/').replace('\\', '/') + + +def poor_yaml_read(file): + """ + Poor implementation to load yaml without pyyaml dependency, but with re + + Args: + file (str): + + Returns: + dict: + """ + if not os.path.exists(file): + return {} + + data = {} + regex = re.compile(r'^(.*?):(.*?)$') + with open(file, 'r', encoding='utf-8') as f: + for line in f.readlines(): + line = line.strip('\n\r\t ').replace('\\', '/') + if line.startswith('#'): + continue + result = re.match(regex, line) + if result: + k, v = result.group(1), result.group(2).strip('\n\r\t\' ') + if v: + if v.lower() == 'null': + v = None + elif v.lower() == 'false': + v = False + elif v.lower() == 'true': + v = True + elif v.isdigit(): + v = int(v) + data[k] = v + + return data + + +def poor_yaml_write(data, file, template_file=DEPLOY_TEMPLATE): + """ + Args: + data (dict): + file (str): + template_file (str): + """ + with open(template_file, 'r', encoding='utf-8') as f: + text = f.read().replace('\\', '/') + + for key, value in data.items(): + if value is None: + value = 'null' + elif value is True: + value = "true" + elif value is False: + value = "false" + text = re.sub(f'{key}:.*?\n', f'{key}: {value}\n', text) + + with open(file, 'w', encoding='utf-8', newline='') as f: + f.write(text) + + +@dataclass +class DataProcessInfo: + proc: object # psutil.Process or psutil._pswindows.Process + pid: int + + @cached_property + def name(self): + name = self.proc.name() + return name + + @cached_property + def cmdline(self): + try: + cmdline = self.proc.cmdline() + except: + # psutil.AccessDenied + cmdline = [] + cmdline = ' '.join(cmdline).replace(r'\\', '/').replace('\\', '/') + return cmdline + + def __str__(self): + # Don't print `proc`, it will take some time to get process properties + return f'DataProcessInfo(name="{self.name}", pid={self.pid}, cmdline="{self.cmdline}")' + + __repr__ = __str__ + + +def iter_process() -> Iterable[DataProcessInfo]: + try: + import psutil + except ModuleNotFoundError: + return + + if psutil.WINDOWS: + # 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) + yield DataProcessInfo( + proc=proc, + pid=proc.pid, + ) + else: + # This will cost about 0.45s, even `attr` is given. + for proc in psutil.process_iter(): + yield DataProcessInfo( + proc=proc, + pid=proc.pid, + ) 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/assets.py b/module/campaign/assets.py index e462b92f2..6d6f2917f 100644 --- a/module/campaign/assets.py +++ b/module/campaign/assets.py @@ -17,7 +17,7 @@ OCR_OIL_LIMIT = Button(area={'cn': (608, 0, 736, 19), 'en': (608, 0, 736, 19), ' OCR_OIL_CHECK = Button(area={'cn': (573, 30, 592, 49), 'en': (573, 30, 592, 49), 'jp': (573, 30, 592, 49), 'tw': (573, 30, 592, 49)}, color={'cn': (82, 82, 82), 'en': (82, 82, 82), 'jp': (82, 82, 82), 'tw': (82, 82, 82)}, button={'cn': (573, 30, 592, 49), 'en': (573, 30, 592, 49), 'jp': (573, 30, 592, 49), 'tw': (573, 30, 592, 49)}, file={'cn': './assets/cn/campaign/OCR_OIL_CHECK.png', 'en': './assets/en/campaign/OCR_OIL_CHECK.png', 'jp': './assets/jp/campaign/OCR_OIL_CHECK.png', 'tw': './assets/tw/campaign/OCR_OIL_CHECK.png'}) SWITCH_1_HARD = Button(area={'cn': (82, 641, 148, 675), 'en': (87, 642, 148, 676), 'jp': (24, 645, 150, 697), 'tw': (82, 641, 148, 675)}, color={'cn': (233, 141, 128), 'en': (234, 139, 124), 'jp': (219, 116, 106), 'tw': (236, 159, 148)}, button={'cn': (82, 641, 148, 675), 'en': (87, 642, 148, 676), 'jp': (24, 645, 150, 697), 'tw': (82, 641, 148, 675)}, file={'cn': './assets/cn/campaign/SWITCH_1_HARD.png', 'en': './assets/en/campaign/SWITCH_1_HARD.png', 'jp': './assets/jp/campaign/SWITCH_1_HARD.png', 'tw': './assets/tw/campaign/SWITCH_1_HARD.png'}) SWITCH_1_NORMAL = Button(area={'cn': (80, 641, 148, 675), 'en': (79, 638, 147, 675), 'jp': (24, 644, 150, 697), 'tw': (79, 641, 148, 675)}, color={'cn': (157, 180, 227), 'en': (157, 180, 227), 'jp': (143, 169, 222), 'tw': (156, 179, 227)}, button={'cn': (80, 641, 148, 675), 'en': (79, 638, 147, 675), 'jp': (24, 644, 150, 697), 'tw': (79, 641, 148, 675)}, file={'cn': './assets/cn/campaign/SWITCH_1_NORMAL.png', 'en': './assets/en/campaign/SWITCH_1_NORMAL.png', 'jp': './assets/jp/campaign/SWITCH_1_NORMAL.png', 'tw': './assets/tw/campaign/SWITCH_1_NORMAL.png'}) -SWITCH_2_EX = Button(area={'cn': (241, 642, 311, 690), 'en': (251, 644, 313, 697), 'jp': (186, 638, 314, 692), 'tw': (241, 640, 312, 692)}, color={'cn': (254, 161, 74), 'en': (254, 163, 80), 'jp': (205, 136, 64), 'tw': (254, 161, 72)}, button={'cn': (241, 642, 311, 690), 'en': (251, 644, 313, 697), 'jp': (186, 638, 314, 692), 'tw': (241, 640, 312, 692)}, file={'cn': './assets/cn/campaign/SWITCH_2_EX.png', 'en': './assets/en/campaign/SWITCH_2_EX.png', 'jp': './assets/jp/campaign/SWITCH_2_EX.png', 'tw': './assets/tw/campaign/SWITCH_2_EX.png'}) +SWITCH_2_EX = Button(area={'cn': (272, 658, 310, 676), 'en': (251, 644, 313, 697), 'jp': (186, 638, 314, 692), 'tw': (241, 640, 312, 692)}, color={'cn': (253, 168, 98), 'en': (254, 163, 80), 'jp': (205, 136, 64), 'tw': (254, 161, 72)}, button={'cn': (272, 658, 310, 676), 'en': (251, 644, 313, 697), 'jp': (186, 638, 314, 692), 'tw': (241, 640, 312, 692)}, file={'cn': './assets/cn/campaign/SWITCH_2_EX.png', 'en': './assets/en/campaign/SWITCH_2_EX.png', 'jp': './assets/jp/campaign/SWITCH_2_EX.png', 'tw': './assets/tw/campaign/SWITCH_2_EX.png'}) SWITCH_2_HARD = Button(area={'cn': (246, 641, 311, 675), 'en': (244, 640, 312, 684), 'jp': (233, 655, 310, 681), 'tw': (245, 641, 311, 674)}, color={'cn': (233, 140, 127), 'en': (228, 121, 106), 'jp': (223, 110, 96), 'tw': (237, 161, 150)}, button={'cn': (246, 641, 311, 675), 'en': (244, 640, 312, 684), 'jp': (233, 655, 310, 681), 'tw': (245, 641, 311, 674)}, file={'cn': './assets/cn/campaign/SWITCH_2_HARD.png', 'en': './assets/en/campaign/SWITCH_2_HARD.png', 'jp': './assets/jp/campaign/SWITCH_2_HARD.png', 'tw': './assets/tw/campaign/SWITCH_2_HARD.png'}) TEMPLATE_EVENT_20230817_STORY_E1 = Template(file={'cn': './assets/cn/campaign/TEMPLATE_EVENT_20230817_STORY_E1.png', 'en': './assets/en/campaign/TEMPLATE_EVENT_20230817_STORY_E1.png', 'jp': './assets/jp/campaign/TEMPLATE_EVENT_20230817_STORY_E1.png', 'tw': './assets/tw/campaign/TEMPLATE_EVENT_20230817_STORY_E1.png'}) TEMPLATE_EVENT_20230817_STORY_E2 = Template(file={'cn': './assets/cn/campaign/TEMPLATE_EVENT_20230817_STORY_E2.png', 'en': './assets/en/campaign/TEMPLATE_EVENT_20230817_STORY_E2.png', 'jp': './assets/jp/campaign/TEMPLATE_EVENT_20230817_STORY_E2.png', 'tw': './assets/tw/campaign/TEMPLATE_EVENT_20230817_STORY_E2.png'}) diff --git a/module/campaign/campaign_ocr.py b/module/campaign/campaign_ocr.py index 637481d7c..763dda45f 100644 --- a/module/campaign/campaign_ocr.py +++ b/module/campaign/campaign_ocr.py @@ -188,6 +188,17 @@ class CampaignOcr(ModuleBase): image, self._stage_image_gray, 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_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 eb537eed1..6c86ae0a6 100644 --- a/module/campaign/gems_farming.py +++ b/module/campaign/gems_farming.py @@ -4,15 +4,21 @@ from module.combat.assets import BATTLE_PREPARATION from module.equipment.assets import * from module.equipment.equipment_change import EquipmentChange from module.equipment.fleet_equipment import OCR_FLEET_INDEX -from module.exception import CampaignEnd +from module.exception import CampaignEnd, ScriptError from module.handler.assets import AUTO_SEARCH_MAP_OPTION_OFF from module.logger import logger from module.map.assets import FLEET_PREPARATION, MAP_PREPARATION -from module.retire.assets import DOCK_CHECK, TEMPLATE_BOGUE, TEMPLATE_HERMES, TEMPLATE_LANGLEY, TEMPLATE_RANGER +from module.retire.assets import ( + DOCK_CHECK, + TEMPLATE_BOGUE, TEMPLATE_HERMES, TEMPLATE_LANGLEY, TEMPLATE_RANGER, + TEMPLATE_CASSIN_1, TEMPLATE_CASSIN_2, TEMPLATE_DOWNES_1, TEMPLATE_DOWNES_2, + TEMPLATE_AULICK, TEMPLATE_FOOTE +) + from module.retire.dock import Dock from module.retire.scanner import ShipScanner -from module.ui.page import page_fleet from module.ui.assets import BACK_ARROW +from module.ui.page import page_fleet SIM_VALUE = 0.95 @@ -150,6 +156,7 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): Returns: bool: True if vanguard changed """ + logger.hr('Change vanguard', level=1) logger.attr('ChangeVanguard', self.config.GemsFarming_ChangeVanguard) if self.change_vanguard_equip: @@ -177,6 +184,7 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): self.dock_select_one(button) self.dock_filter_set() + self.dock_sort_method_dsc_set() self.dock_select_confirm(check_button=page_fleet.check_button) def get_common_rarity_cv(self): @@ -194,7 +202,6 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): scanner.disable('rarity') if self.config.GemsFarming_CommonCV == 'any': - logger.info('') self.dock_sort_method_dsc_set(False) @@ -253,13 +260,61 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): fleet=self.config.Fleet_Fleet1, status='free') scanner.disable('rarity') + self.dock_sort_method_dsc_set() + ships = scanner.scan(self.device.image) if ships: # Don't need to change current return ships scanner.set_limitation(fleet=0) - 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 + + def find_candidates(self, template, scanner): + """ + Find candidates based on template matching using a scanner. + + """ + candidates = [] + for item in template: + candidates = [ship for ship in scanner.scan(self.device.image, output=False) + if item.match(self.image_crop(ship.button), similarity=SIM_VALUE)] + if candidates: + break + return candidates + + @staticmethod + def get_templates(common_dd): + """ + Returns the corresponding template list based on CommonDD + """ + if common_dd == 'any': + return [ + TEMPLATE_CASSIN_1, TEMPLATE_CASSIN_2, + TEMPLATE_DOWNES_1, TEMPLATE_DOWNES_2, + TEMPLATE_AULICK, + TEMPLATE_FOOTE + ] + elif common_dd == 'aulick_or_foote': + return [ + TEMPLATE_AULICK, + TEMPLATE_FOOTE + ] + elif common_dd == 'cassin_or_downes': + return [ + TEMPLATE_CASSIN_1, TEMPLATE_CASSIN_2, + TEMPLATE_DOWNES_1, TEMPLATE_DOWNES_2 + ] + else: + logger.error(f'Invalid CommonDD setting: {common_dd}') + raise ScriptError(f'Invalid CommonDD setting: {common_dd}') def flagship_change_execute(self): """ diff --git a/module/campaign/run.py b/module/campaign/run.py index 797292c73..faa29cd0f 100644 --- a/module/campaign/run.py +++ b/module/campaign/run.py @@ -344,7 +344,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 136d63adc..188a63b8e 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -318,7 +318,8 @@ "aScreenCap_nc", "DroidCast", "DroidCast_raw", - "scrcpy" + "scrcpy", + "nemu_ipc" ] }, "ControlMethod": { @@ -341,23 +342,34 @@ "value": false } }, - "RestartEmulator": { - "Enable": { - "type": "checkbox", - "value": false + "EmulatorInfo": { + "Emulator": { + "type": "select", + "value": "auto", + "option": [ + "auto", + "NoxPlayer", + "NoxPlayer64", + "BlueStacks4", + "BlueStacks5", + "BlueStacks4HyperV", + "BlueStacks5HyperV", + "LDPlayer3", + "LDPlayer4", + "LDPlayer9", + "MuMuPlayer", + "MuMuPlayerX", + "MuMuPlayer12", + "MEmuPlayer" + ] }, - "EmulatorData": { + "name": { "type": "textarea", - "value": "emulator:\nname:\npath:", - "mode": "yaml" + "value": null }, - "ErrorRestart": { - "type": "checkbox", - "value": false - }, - "DailyRestart": { - "type": "checkbox", - "value": false + "path": { + "type": "textarea", + "value": null } }, "Error": { @@ -1783,15 +1795,6 @@ "ship_equip" ] }, - "ChangeVanguard": { - "type": "select", - "value": "ship", - "option": [ - "disabled", - "ship", - "ship_equip" - ] - }, "CommonCV": { "type": "select", "value": "any", @@ -1803,6 +1806,24 @@ "hermes" ] }, + "ChangeVanguard": { + "type": "select", + "value": "ship", + "option": [ + "disabled", + "ship", + "ship_equip" + ] + }, + "CommonDD": { + "type": "select", + "value": "any", + "option": [ + "any", + "aulick_or_foote", + "cassin_or_downes" + ] + }, "CommissionLimit": { "type": "checkbox", "value": true @@ -1883,13 +1904,13 @@ ], "display": "hide", "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -2216,13 +2237,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -2664,13 +2685,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -3071,16 +3092,17 @@ "raid_20221027", "raid_20230118", "raid_20230629", - "raid_20240130" + "raid_20240130", + "raid_20240328" ], "option_bold": [ - "raid_20240130", - "raid_20230118" + "raid_20240328", + "raid_20230629" ], - "cn": "raid_20240130", - "en": "raid_20240130", - "jp": "raid_20240130", - "tw": "raid_20230118" + "cn": "raid_20240328", + "en": "raid_20240328", + "jp": "raid_20240328", + "tw": "raid_20230629" }, "Mode": { "type": "select", @@ -3573,11 +3595,15 @@ "war_archives_20181026_en", "war_archives_20181227_cn", "war_archives_20190221_en", + "war_archives_20190314_en", "war_archives_20190321_en", "war_archives_20190620_en", "war_archives_20190911_cn", "war_archives_20191010_en", "war_archives_20191031_en", + "war_archives_20200312_cn", + "war_archives_20200507_cn", + "war_archives_20200603_cn", "war_archives_20200806_cn", "war_archives_20200820_cn", "war_archives_20200903_cn", @@ -3595,13 +3621,12 @@ "war_archives_20220414_cn" ], "option_bold": [ - "war_archives_20220414_cn", "war_archives_20210225_cn" ], "cn": "war_archives_20210225_cn", "en": "war_archives_20210225_cn", "jp": "war_archives_20210225_cn", - "tw": "war_archives_20220414_cn" + "tw": "war_archives_20210225_cn" }, "Mode": { "type": "select", @@ -4054,13 +4079,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -4519,13 +4544,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -4984,13 +5009,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -5449,13 +5474,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -5904,13 +5929,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20230223_cn", - "event_20240229_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20240229_cn", - "en": "event_20240229_cn", - "jp": "event_20240229_cn", - "tw": "event_20230223_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -6308,16 +6333,17 @@ "raid_20221027", "raid_20230118", "raid_20230629", - "raid_20240130" + "raid_20240130", + "raid_20240328" ], "option_bold": [ - "raid_20240130", - "raid_20230118" + "raid_20240328", + "raid_20230629" ], - "cn": "raid_20240130", - "en": "raid_20240130", - "jp": "raid_20240130", - "tw": "raid_20230118" + "cn": "raid_20240328", + "en": "raid_20240328", + "jp": "raid_20240328", + "tw": "raid_20230629" }, "Mode": { "type": "select", @@ -9268,11 +9294,9 @@ "value": "emulator", "option": [ "emulator", - "emulator_android_12", "plone_cloud_with_adb", "phone_cloud_without_adb", "android_phone", - "android_phone_12", "android_phone_vmos" ] }, diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 0bac5e713..ac37a96c9 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -85,23 +85,54 @@ Emulator: option: [ disabled, ] ScreenshotMethod: value: auto - option: [ auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy ] + option: [ + auto, + ADB, + ADB_nc, + uiautomator2, + aScreenCap, + aScreenCap_nc, + DroidCast, + DroidCast_raw, + scrcpy, + nemu_ipc, + ] ControlMethod: value: minitouch - option: [ ADB, uiautomator2, minitouch, Hermit, MaaTouch ] + option: [ + ADB, + uiautomator2, + minitouch, + Hermit, + MaaTouch, + ] ScreenshotDedithering: false AdbRestart: false -RestartEmulator: - Enable: false - EmulatorData: +EmulatorInfo: + Emulator: + value: auto + option: [ + auto, + NoxPlayer, + NoxPlayer64, + BlueStacks4, + BlueStacks5, + BlueStacks4HyperV, + BlueStacks5HyperV, + LDPlayer3, + LDPlayer4, + LDPlayer9, + MuMuPlayer, + MuMuPlayerX, + MuMuPlayer12, + MEmuPlayer, + ] + name: + value: null + type: textarea + path: + value: null type: textarea - mode: yaml - value: |- - emulator: - name: - path: - ErrorRestart: False - DailyRestart: False Error: HandleError: true SaveError: true @@ -290,12 +321,15 @@ GemsFarming: ChangeFlagship: value: ship option: [ ship, ship_equip ] - ChangeVanguard: - value: ship - option: [ disabled, ship, ship_equip ] CommonCV: value: any option: [ any, langley, bogue, ranger, hermes ] + ChangeVanguard: + value: ship + option: [ disabled, ship, ship_equip ] + CommonDD: + value: any + option: [ any, aulick_or_foote , cassin_or_downes ] CommissionLimit: true # ==================== Event ==================== @@ -727,9 +761,11 @@ Benchmark: DeviceType: value: emulator option: [ - emulator, emulator_android_12, - plone_cloud_with_adb, phone_cloud_without_adb, - android_phone, android_phone_12, android_phone_vmos + emulator, + plone_cloud_with_adb, + phone_cloud_without_adb, + android_phone, + android_phone_vmos, ] TestScene: value: screenshot_click diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index a6cabef1b..74e40c088 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -10,7 +10,7 @@ Alas: tasks: Alas: - Emulator - - RestartEmulator + - EmulatorInfo - Error - Optimization - DropRecord diff --git a/module/config/config.py b/module/config/config.py index 1b7d0dc54..c91660937 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 16f902c0f..6a43e53cd 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -84,16 +84,15 @@ class GeneratedConfig: Emulator_Serial = 'auto' 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 + 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 Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False - # Group `RestartEmulator` - RestartEmulator_Enable = False - RestartEmulator_EmulatorData = 'emulator:\nname:\npath:' - RestartEmulator_ErrorRestart = False - RestartEmulator_DailyRestart = False + # Group `EmulatorInfo` + EmulatorInfo_Emulator = 'auto' # auto, NoxPlayer, NoxPlayer64, BlueStacks4, BlueStacks5, BlueStacks4HyperV, BlueStacks5HyperV, LDPlayer3, LDPlayer4, LDPlayer9, MuMuPlayer, MuMuPlayerX, MuMuPlayer12, MEmuPlayer + EmulatorInfo_name = None + EmulatorInfo_path = None # Group `Error` Error_HandleError = True @@ -213,8 +212,9 @@ class GeneratedConfig: # Group `GemsFarming` GemsFarming_ChangeFlagship = 'ship' # ship, ship_equip - GemsFarming_ChangeVanguard = 'ship' # disabled, ship, ship_equip GemsFarming_CommonCV = 'any' # any, langley, bogue, ranger, hermes + GemsFarming_ChangeVanguard = 'ship' # disabled, ship, ship_equip + GemsFarming_CommonDD = 'any' # any, aulick_or_foote, cassin_or_downes GemsFarming_CommissionLimit = True # Group `EventGeneral` @@ -490,7 +490,7 @@ class GeneratedConfig: OpsiDaemon_SelectEnemy = True # Group `Benchmark` - Benchmark_DeviceType = 'emulator' # emulator, emulator_android_12, plone_cloud_with_adb, phone_cloud_without_adb, android_phone, android_phone_12, android_phone_vmos + Benchmark_DeviceType = 'emulator' # emulator, plone_cloud_with_adb, phone_cloud_without_adb, android_phone, android_phone_vmos Benchmark_TestScene = 'screenshot_click' # screenshot_click, screenshot, click # Group `AzurLaneUncensored` diff --git a/module/config/config_updater.py b/module/config/config_updater.py index f0d68dbf0..1f21cfc1f 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -737,6 +737,11 @@ class ConfigUpdater: key = key.split(".") key[-1] = key[-1].replace("Value", "Record") yield ".".join(key), datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # Oh no, dynamic dropdown update can only be used on pywebio > 1.8.0 + # elif key == 'Alas.Emulator.ScreenshotMethod' and value == 'nemu_ipc': + # yield 'Alas.Emulator.ControlMethod', 'nemu_ipc' + # elif key == 'Alas.Emulator.ControlMethod' and value == 'nemu_ipc': + # yield 'Alas.Emulator.ScreenshotMethod', 'nemu_ipc' def read_file(self, config_name, is_template=False): """ diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 92763fcea..2166150a6 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -637,7 +637,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "Control Method", @@ -657,26 +658,36 @@ "help": "" } }, - "RestartEmulator": { + "EmulatorInfo": { "_info": { - "name": "Restart Emulator", + "name": "Emulator Settings", + "help": "The following values are auto-filled according to \"Serial\", if you don’t understand, please don't modify them" + }, + "Emulator": { + "name": "Emulator Type", + "help": "", + "auto": "Auto-detect", + "NoxPlayer": "Nox Player", + "NoxPlayer64": "Nox Player 64bit", + "BlueStacks4": "BlueStacks 4", + "BlueStacks5": "BlueStacks 5", + "BlueStacks4HyperV": "BlueStacks 4 Hyper-V", + "BlueStacks5HyperV": "BlueStacks 5 Hyper-V", + "LDPlayer3": "LD Player 3", + "LDPlayer4": "LD Player 4", + "LDPlayer9": "LD Player 9", + "MuMuPlayer": "MuMu Player", + "MuMuPlayerX": "MuMu Player X", + "MuMuPlayer12": "MuMu Player 12", + "MEmuPlayer": "MEmu Player" + }, + "name": { + "name": "Emulator Instance Name", "help": "" }, - "Enable": { - "name": "RestartEmulator.Enable.name", - "help": "RestartEmulator.Enable.help" - }, - "EmulatorData": { - "name": "RestartEmulator.EmulatorData.name", - "help": "RestartEmulator.EmulatorData.help" - }, - "ErrorRestart": { - "name": "Restart Emulator on Error", - "help": "Automatically restart the emulator when it cannot be connected" - }, - "DailyRestart": { - "name": "Restart Emulator when Server Refreshes", - "help": "Restart emulator every day to solve the memory leak problem" + "path": { + "name": "Emulator Installation Path", + "help": "" } }, "Error": { @@ -919,9 +930,9 @@ "event_20211125_cn": "World-spanning Arclight Rerun", "event_20211229_cn": "Tower of Transcendence Rerun", "event_20220210_cn": "Northern Overture Rerun", - "event_20220224_cn": "Abyssal Refrain", + "event_20220224_cn": "Abyssal Refrain Rerun", "event_20220310_tw": "復刻斯圖爾特的硝煙", - "event_20220324_cn": "Virtual Tower", + "event_20220324_cn": "Virtual Tower Rerun", "event_20220407_tw": "蒼紅的迴響(復刻)", "event_20220414_cn": "Aurora Noctis Rerun", "event_20220428_cn": "Rondo at Rainbows End", @@ -948,17 +959,22 @@ "raid_20230118": "Winter Pathfinder", "raid_20230629": "Reflections of the Oasis", "raid_20240130": "Spring Festive Fiasco", + "raid_20240328": "From Zero to Hero", "war_archives_20180607_cn": "archives Ink Stained Steel Sakura", "war_archives_20180726_cn": "archives Iris of Light and Dark", "war_archives_20181020_en": "archives Strive Wish and Strategize", "war_archives_20181026_en": "archives Fallen Wings", "war_archives_20181227_cn": "archives Crimson Echoes", "war_archives_20190221_en": "archives Winters Crown", + "war_archives_20190314_en": "archives Prelude under the Moon", "war_archives_20190321_en": "archives Visitors Dyed in Red", "war_archives_20190620_en": "archives Glorious Battle", "war_archives_20190911_cn": "archives Empyreal Tragicomedy", "war_archives_20191010_en": "archives Encircling Graf Spee", "war_archives_20191031_en": "archives Divergent Chessboard", + "war_archives_20200312_cn": "archives The Solomon Ranger", + "war_archives_20200507_cn": "archives The Way Home in the Night", + "war_archives_20200603_cn": "archives Counterattack Within the Fjord", "war_archives_20200806_cn": "archives The Enigma and the Shark", "war_archives_20200820_cn": "archives Scherzo of Iron and Blood", "war_archives_20200903_cn": "archives Stars of the Shimmering Fjord", @@ -1345,13 +1361,6 @@ "ship": "Change Ship", "ship_equip": "Change Ship + Gears" }, - "ChangeVanguard": { - "name": "Change Vanguard", - "help": "Change vanguard if flagship or vanguard are emotion exhausted.If you choose not to change, your fleet will ignore the low emotion warning and continue combat.\nSwitch out to new level 100(70) common vanguard after current flagship has reached level 32+ (Only CN players'0 limit break ship can reach level 100)\n\nThe vanguard's equipment is replaced when being switched out by first recording the current setup. Only populated equipment slots will be replaced.", - "disabled": "Don't Change", - "ship": "Change Ship", - "ship_equip": "Change Ship + Gears" - }, "CommonCV": { "name": "Flagship Common CV/CVL Preference", "help": "", @@ -1361,6 +1370,20 @@ "ranger": "ranger", "hermes": "hermes" }, + "ChangeVanguard": { + "name": "Change Vanguard", + "help": "Change vanguard if flagship or vanguard are emotion exhausted.If you choose not to change, your fleet will ignore the low emotion warning and continue combat.\nSwitch out to new level 100(70) common vanguard after current flagship has reached level 32+ (Only CN players'0 limit break ship can reach level 100)\n\nThe vanguard's equipment is replaced when being switched out by first recording the current setup. Only populated equipment slots will be replaced.", + "disabled": "Don't Change", + "ship": "Change Ship", + "ship_equip": "Change Ship + Gears" + }, + "CommonDD": { + "name": "Flagship Common DD Preference", + "help": "", + "any": "any", + "aulick_or_foote": "aulick or foote", + "cassin_or_downes": "cassin or downes" + }, "CommissionLimit": { "name": "Prevent Too Many Urgent Commissions", "help": "When running 7x24, prevent having a lot of urgent commissions and not being able to complete daily commissions. It is recommended to select only short-terms and high-yields in the commission filter" @@ -1642,7 +1665,7 @@ }, "LastRun": { "name": "Last Check Time", - "help": "The time of the last check is recorded here to prevent the task from running repeatedly. Check interval is 7 days. This value is automatically recorded and generally does not need to be modified." + "help": "The time of the last check is recorded here to prevent the task from running repeatedly. Check interval is 6 days. This value is automatically recorded and generally does not need to be modified." } }, "Meowfficer": { @@ -2635,13 +2658,11 @@ "DeviceType": { "name": "Device Type", "help": "", - "emulator": "Emulator (Android <= 9)", - "emulator_android_12": "Emulator (Android > 9)", - "plone_cloud_with_adb": "Phone cloud with public network ADB", - "phone_cloud_without_adb": "Phone cloud without public network ADB", - "android_phone": "Android Phone (Android <= 9)", - "android_phone_12": "Android Phone (Android > 9)", - "android_phone_vmos": "Android Phone (VMOS emulator)" + "emulator": "Emulators", + "plone_cloud_with_adb": "Phone clouds with public network ADB", + "phone_cloud_without_adb": "Phone clouds without public network ADB", + "android_phone": "Android Phones", + "android_phone_vmos": "Android Phones (VMOS emulator)" }, "TestScene": { "name": "Test Scene", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index e5cb3b206..27ea8fd8e 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -637,7 +637,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "Emulator.ControlMethod.name", @@ -657,26 +658,36 @@ "help": "Emulator.AdbRestart.help" } }, - "RestartEmulator": { + "EmulatorInfo": { "_info": { - "name": "RestartEmulator._info.name", - "help": "RestartEmulator._info.help" + "name": "EmulatorInfo._info.name", + "help": "EmulatorInfo._info.help" }, - "Enable": { - "name": "RestartEmulator.Enable.name", - "help": "RestartEmulator.Enable.help" + "Emulator": { + "name": "EmulatorInfo.Emulator.name", + "help": "EmulatorInfo.Emulator.help", + "auto": "auto", + "NoxPlayer": "NoxPlayer", + "NoxPlayer64": "NoxPlayer64", + "BlueStacks4": "BlueStacks4", + "BlueStacks5": "BlueStacks5", + "BlueStacks4HyperV": "BlueStacks4HyperV", + "BlueStacks5HyperV": "BlueStacks5HyperV", + "LDPlayer3": "LDPlayer3", + "LDPlayer4": "LDPlayer4", + "LDPlayer9": "LDPlayer9", + "MuMuPlayer": "MuMuPlayer", + "MuMuPlayerX": "MuMuPlayerX", + "MuMuPlayer12": "MuMuPlayer12", + "MEmuPlayer": "MEmuPlayer" }, - "EmulatorData": { - "name": "RestartEmulator.EmulatorData.name", - "help": "RestartEmulator.EmulatorData.help" + "name": { + "name": "EmulatorInfo.name.name", + "help": "EmulatorInfo.name.help" }, - "ErrorRestart": { - "name": "RestartEmulator.ErrorRestart.name", - "help": "RestartEmulator.ErrorRestart.help" - }, - "DailyRestart": { - "name": "RestartEmulator.DailyRestart.name", - "help": "RestartEmulator.DailyRestart.help" + "path": { + "name": "EmulatorInfo.path.name", + "help": "EmulatorInfo.path.help" } }, "Error": { @@ -919,9 +930,9 @@ "event_20211125_cn": "弧光は交わる世界にて(復刻)", "event_20211229_cn": "遡望せし虹彩の塔(復刻)", "event_20220210_cn": "凍絶の北海(復刻)", - "event_20220224_cn": "鳴動せし星霜の淵", + "event_20220224_cn": "鳴動せし星霜の淵(復刻)", "event_20220310_tw": "復刻斯圖爾特的硝煙", - "event_20220324_cn": "幻像の塔", + "event_20220324_cn": "幻像の塔(復刻)", "event_20220407_tw": "蒼紅的迴響(復刻)", "event_20220414_cn": "極夜照らす幻光(復刻)", "event_20220428_cn": "吟ずる瑠璃の楽章", @@ -948,17 +959,22 @@ "raid_20230118": "冬の案内人", "raid_20230629": "緑地伽話", "raid_20240130": "新春宴会狂騒曲", + "raid_20240328": "ゼロから頑張る魔王討伐", "war_archives_20180607_cn": "檔案 墨染まりし鋼の桜", "war_archives_20180726_cn": "檔案 光と影のアイリス", "war_archives_20181020_en": "檔案 努力希望と計画", "war_archives_20181026_en": "檔案 闇に堕ちた青き翼", "war_archives_20181227_cn": "檔案 縹映る深緋の残響", "war_archives_20190221_en": "檔案 凛冽なりし冬の王冠", + "war_archives_20190314_en": "檔案 月夜の開幕曲", "war_archives_20190321_en": "檔案 紅染の来訪者", "war_archives_20190620_en": "檔案 栄光なる最終戦", "war_archives_20190911_cn": "檔案 悲歎せし焔海の詩", "war_archives_20191010_en": "檔案 アドミラル・グラーフ・シュペー追撃戦", "war_archives_20191031_en": "檔案 鏡写されし異色", + "war_archives_20200312_cn": "檔案 南洋に靡く硝煙", + "war_archives_20200507_cn": "檔案 帰路は海色の陰りへと", + "war_archives_20200603_cn": "檔案 峡湾間の反撃", "war_archives_20200806_cn": "檔案 鉄血鮫とエニグマ", "war_archives_20200820_cn": "檔案 黒鉄の楽章 誓いの海", "war_archives_20200903_cn": "檔案 輝ける峡湾の星", @@ -1345,13 +1361,6 @@ "ship": "ship", "ship_equip": "ship_equip" }, - "ChangeVanguard": { - "name": "GemsFarming.ChangeVanguard.name", - "help": "GemsFarming.ChangeVanguard.help", - "disabled": "disabled", - "ship": "ship", - "ship_equip": "ship_equip" - }, "CommonCV": { "name": "GemsFarming.CommonCV.name", "help": "GemsFarming.CommonCV.help", @@ -1361,6 +1370,20 @@ "ranger": "ranger", "hermes": "hermes" }, + "ChangeVanguard": { + "name": "GemsFarming.ChangeVanguard.name", + "help": "GemsFarming.ChangeVanguard.help", + "disabled": "disabled", + "ship": "ship", + "ship_equip": "ship_equip" + }, + "CommonDD": { + "name": "GemsFarming.CommonDD.name", + "help": "GemsFarming.CommonDD.help", + "any": "any", + "aulick_or_foote": "aulick or foote", + "cassin_or_downes": "cassin or downes" + }, "CommissionLimit": { "name": "GemsFarming.CommissionLimit.name", "help": "GemsFarming.CommissionLimit.help" @@ -2636,11 +2659,9 @@ "name": "Benchmark.DeviceType.name", "help": "Benchmark.DeviceType.help", "emulator": "emulator", - "emulator_android_12": "emulator_android_12", "plone_cloud_with_adb": "plone_cloud_with_adb", "phone_cloud_without_adb": "phone_cloud_without_adb", "android_phone": "android_phone", - "android_phone_12": "android_phone_12", "android_phone_vmos": "android_phone_vmos" }, "TestScene": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 4f3a3927d..2fcde1e63 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -637,7 +637,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "模拟器控制方案", @@ -657,26 +658,36 @@ "help": "" } }, - "RestartEmulator": { + "EmulatorInfo": { "_info": { - "name": "重启模拟器", + "name": "模拟器设置", + "help": "下列数值是根据Serial自动填充的,如果不懂请不要随意修改" + }, + "Emulator": { + "name": "模拟器类型", + "help": "", + "auto": "自动检测", + "NoxPlayer": "夜神模拟器", + "NoxPlayer64": "夜神模拟器64位", + "BlueStacks4": "蓝叠模拟器4", + "BlueStacks5": "蓝叠模拟器5", + "BlueStacks4HyperV": "蓝叠模拟器4 Hyper-V", + "BlueStacks5HyperV": "蓝叠模拟器5 Hyper-V", + "LDPlayer3": "雷电模拟器3", + "LDPlayer4": "雷电模拟器4", + "LDPlayer9": "雷电模拟器9", + "MuMuPlayer": "MuMu模拟器", + "MuMuPlayerX": "MuMu模拟器X", + "MuMuPlayer12": "MuMu模拟器12", + "MEmuPlayer": "逍遥模拟器" + }, + "name": { + "name": "模拟器实例名称", "help": "" }, - "Enable": { - "name": "RestartEmulator.Enable.name", - "help": "RestartEmulator.Enable.help" - }, - "EmulatorData": { - "name": "RestartEmulator.EmulatorData.name", - "help": "RestartEmulator.EmulatorData.help" - }, - "ErrorRestart": { - "name": "出错时重启模拟器", - "help": "在无法连接到模拟器时自动重启模拟器" - }, - "DailyRestart": { - "name": "服务器刷新时重启模拟器", - "help": "每天主动重启模拟器以解决内存泄漏问题" + "path": { + "name": "模拟器安装路径", + "help": "" } }, "Error": { @@ -919,9 +930,9 @@ "event_20211125_cn": "复刻交汇世界的弧光", "event_20211229_cn": "复刻逆转彩虹之塔", "event_20220210_cn": "复刻北境序曲", - "event_20220224_cn": "深度回音", + "event_20220224_cn": "复刻深度回音", "event_20220310_tw": "復刻斯圖爾特的硝煙", - "event_20220324_cn": "虚像构筑之塔", + "event_20220324_cn": "复刻虚像构筑之塔", "event_20220407_tw": "蒼紅的迴響(復刻)", "event_20220414_cn": "复刻永夜幻光", "event_20220428_cn": "虹彩的终幕曲", @@ -948,17 +959,22 @@ "raid_20230118": "冬日的寻路人", "raid_20230629": "绿洲往事", "raid_20240130": "寰昌宇定家事忙", + "raid_20240328": "从零开始的魔王讨伐之旅", "war_archives_20180607_cn": "档案 墨染的钢铁之花", "war_archives_20180726_cn": "档案 光与影的鸢尾之华", "war_archives_20181020_en": "档案 努力希望和计划", "war_archives_20181026_en": "档案 坠落之翼", "war_archives_20181227_cn": "档案 苍红的回响", "war_archives_20190221_en": "档案 凛冬王冠", + "war_archives_20190314_en": "档案 月光下的序曲", "war_archives_20190321_en": "档案 红染的参访者", "war_archives_20190620_en": "档案 光荣的一战", "war_archives_20190911_cn": "档案 神圣的悲喜剧", "war_archives_20191010_en": "档案 围剿斯佩伯爵", "war_archives_20191031_en": "档案 异色格", + "war_archives_20200312_cn": "档案 斯图尔特的硝烟", + "war_archives_20200507_cn": "档案 夜幕下的归途", + "war_archives_20200603_cn": "档案 峡湾间的反击", "war_archives_20200806_cn": "档案 最重要的宝物", "war_archives_20200820_cn": "档案 铁血音符誓言", "war_archives_20200903_cn": "档案 峡湾间的星辰", @@ -1345,13 +1361,6 @@ "ship": "更换舰船", "ship_equip": "更换舰船 + 装备" }, - "ChangeVanguard": { - "name": "更换前排", - "help": "当前排红脸时更换前排,选择不更换则会强制红脸出击\n换前排通过找一艘心情不低于10、等级100的白鹰白皮驱逐完成,所以尽量保证有足够多的驱逐。国服以外则为等级70的白鹰白船驱逐。\n\n换装备只会更换正在装备中的栏位,即使是白装也会更换。如果指定了旗舰,则会更换全部5个装备,未指定旗舰只会更换设备。", - "disabled": "不更换", - "ship": "更换舰船", - "ship_equip": "更换舰船 + 装备" - }, "CommonCV": { "name": "指定旗舰航母", "help": "", @@ -1361,6 +1370,20 @@ "ranger": "突击者", "hermes": "竞技神" }, + "ChangeVanguard": { + "name": "更换前排", + "help": "当前排红脸时更换前排,选择不更换则会强制红脸出击\n换前排通过找一艘心情不低于10、等级100的白鹰白皮驱逐完成,所以尽量保证有足够多的驱逐。国服以外则为等级70的白鹰白船驱逐。\n\n换装备只会更换正在装备中的栏位,即使是白装也会更换。如果指定了旗舰,则会更换全部5个装备,未指定旗舰只会更换设备。", + "disabled": "不更换", + "ship": "更换舰船", + "ship_equip": "更换舰船 + 装备" + }, + "CommonDD": { + "name": "指定前排", + "help": "", + "any": "任意", + "aulick_or_foote": "奥利克或富特", + "cassin_or_downes": "卡辛或唐斯" + }, "CommissionLimit": { "name": "防止紧急委托数量过多", "help": "在7x24运行时防止紧急委托数量过多做不完每日委托,建议在委托过滤器仅选择短时长高收益委托" @@ -1642,7 +1665,7 @@ }, "LastRun": { "name": "上次检查时间", - "help": "记录上一次检查的时间以防止任务重复运行,检查间隔为7天,这个数值是自动记录的,一般不需要修改" + "help": "记录上一次检查的时间以防止任务重复运行,检查间隔为6天,这个数值是自动记录的,一般不需要修改" } }, "Meowfficer": { @@ -2635,12 +2658,10 @@ "DeviceType": { "name": "设备类型", "help": "", - "emulator": "模拟器 安卓<=9", - "emulator_android_12": "模拟器 安卓>9", + "emulator": "模拟器", "plone_cloud_with_adb": "云手机 有公网ADB", "phone_cloud_without_adb": "云手机 无公网ADB", - "android_phone": "安卓真机 安卓<=9", - "android_phone_12": "安卓真机 安卓>9", + "android_phone": "安卓真机", "android_phone_vmos": "安卓真机VMOS虚拟机" }, "TestScene": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index edc4421ab..789d9cabe 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -637,7 +637,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "模擬器控制方案", @@ -657,26 +658,36 @@ "help": "" } }, - "RestartEmulator": { + "EmulatorInfo": { "_info": { - "name": "重啟模擬器", + "name": "模擬器設定", + "help": "下列數值是根據Serial自動填充的,如果不懂請不要隨意修改" + }, + "Emulator": { + "name": "模擬器類型", + "help": "", + "auto": "自動檢測", + "NoxPlayer": "夜神模擬器", + "NoxPlayer64": "夜神模擬器64位", + "BlueStacks4": "藍疊模擬器4", + "BlueStacks5": "藍疊模擬器5", + "BlueStacks4HyperV": "藍疊模擬器4 Hyper-V", + "BlueStacks5HyperV": "藍疊模擬器5 Hyper-V", + "LDPlayer3": "雷電模擬器3", + "LDPlayer4": "雷電模擬器4", + "LDPlayer9": "雷電模擬器9", + "MuMuPlayer": "MuMu模擬器", + "MuMuPlayerX": "MuMu模擬器X", + "MuMuPlayer12": "MuMu模擬器12", + "MEmuPlayer": "逍遙模擬器" + }, + "name": { + "name": "模擬器實例名稱", "help": "" }, - "Enable": { - "name": "RestartEmulator.Enable.name", - "help": "RestartEmulator.Enable.help" - }, - "EmulatorData": { - "name": "RestartEmulator.EmulatorData.name", - "help": "RestartEmulator.EmulatorData.help" - }, - "ErrorRestart": { - "name": "啟用重啟模擬器", - "help": "在模擬器被關閉的時候自動啟動模擬器" - }, - "DailyRestart": { - "name": "伺服器重繪時重啟模擬器", - "help": "每天主動重啟模擬器以解决記憶體洩漏問題" + "path": { + "name": "模擬器安裝路徑", + "help": "" } }, "Error": { @@ -903,7 +914,7 @@ "event_20210225_tw": "北境序曲", "event_20210325_cn": "復刻箱庭療法", "event_20210415_tw": "復刻圍剿施佩伯爵", - "event_20210422_cn": "復興的讚美詩", + "event_20210422_cn": "復刻復興的讚美詩", "event_20210429_tw": "復刻墨染的鋼鐵之花", "event_20210527_cn": "復刻鏡位螺旋", "event_20210527_tw": "微層混合", @@ -915,7 +926,7 @@ "event_20210916_cn": "碧海光粼", "event_20211028_cn": "復刻穹頂下的聖詠曲", "event_20211028_tw": "復刻光與影的鳶尾之華", - "event_20211111_cn": "杰諾瓦的焰火", + "event_20211111_cn": "復刻杰諾瓦的焰火", "event_20211125_cn": "復刻-交匯世界的弧光", "event_20211229_cn": "逆轉彩虹之塔", "event_20220210_cn": "復刻北境序曲", @@ -946,28 +957,33 @@ "raid_20220630": "來自鳶尾的天使", "raid_20221027": "戰鬥!皇家女僕隊3rd", "raid_20230118": "冬日的尋路人", - "raid_20230629": "Reflections of the Oasis", + "raid_20230629": "綠洲往事", "raid_20240130": "Spring Festive Fiasco", + "raid_20240328": "From Zero to Hero", "war_archives_20180607_cn": "檔案 墨染的鋼鐵之花", "war_archives_20180726_cn": "檔案 光與影的鳶尾之華", "war_archives_20181020_en": "檔案 努力希望和計劃", "war_archives_20181026_en": "檔案 墜落之翼", "war_archives_20181227_cn": "檔案 蒼紅的迴響", "war_archives_20190221_en": "檔案 凜冬王冠", + "war_archives_20190314_en": "檔案 月光下的序曲", "war_archives_20190321_en": "檔案 紅染的參訪者", "war_archives_20190620_en": "檔案 光榮的一戰", - "war_archives_20190911_cn": "archives Empyreal Tragicomedy", + "war_archives_20190911_cn": "檔案 神聖的悲喜劇", "war_archives_20191010_en": "檔案 圍剿斯佩伯爵", "war_archives_20191031_en": "檔案 異色格", - "war_archives_20200806_cn": "archives The Enigma and the Shark", - "war_archives_20200820_cn": "archives Scherzo of Iron and Blood", + "war_archives_20200312_cn": "檔案 斯圖爾特的硝煙", + "war_archives_20200507_cn": "檔案 夜幕下的歸途", + "war_archives_20200603_cn": "檔案 峽灣間的反擊", + "war_archives_20200806_cn": "檔案 最重要的寶物", + "war_archives_20200820_cn": "檔案 鐵血音符誓言", "war_archives_20200903_cn": "檔案 峽灣間的星辰", - "war_archives_20200917_cn": "archives Dreamwakers Butterfly", - "war_archives_20201029_cn": "archives Universe in Unison", - "war_archives_20201229_cn": "archives Inverted Orthant", - "war_archives_20210225_cn": "archives Khorovod of Dawns Rime", - "war_archives_20210325_cn": "archives Ashen Simulacrum", - "war_archives_20210527_cn": "archives Mirror Involution", + "war_archives_20200917_cn": "檔案 蝶海夢花", + "war_archives_20201029_cn": "檔案 激唱的UNIVERSE", + "war_archives_20201229_cn": "檔案 負象限作戰", + "war_archives_20210225_cn": "檔案 破曉冰華", + "war_archives_20210325_cn": "檔案 箱庭療法", + "war_archives_20210527_cn": "檔案 鏡位螺旋", "war_archives_20210624_cn": "檔案 浮櫻影華", "war_archives_20210819_cn": "檔案 微層混合", "war_archives_20211014_cn": "檔案 激奏的Polaris", @@ -1345,13 +1361,6 @@ "ship": "更換艦船", "ship_equip": "更換艦船 + 裝備" }, - "ChangeVanguard": { - "name": "更換前排", - "help": "當前排紅臉時更換前排,選擇不更換則會強制紅臉出擊\n換前排通過找一艘心情不低於10、等級70的白鷹白船驅逐完成,所以盡量保證有足夠多的驅逐。國服則為等級100的白鷹白船驅逐。\n\n換裝備只會更換正在裝備中的欄位,即使是白裝也會更換。如果指定了旗艦,則會更換全部5個裝備,未指定旗艦隻會更換設備。", - "disabled": "不更换", - "ship": "更換艦船", - "ship_equip": "更換艦船 + 裝備" - }, "CommonCV": { "name": "指定旗艦航母", "help": "", @@ -1361,6 +1370,20 @@ "ranger": "突擊者", "hermes": "競技神" }, + "ChangeVanguard": { + "name": "更換前排", + "help": "當前排紅臉時更換前排,選擇不更換則會強制紅臉出擊\n換前排通過找一艘心情不低於10、等級70的白鷹白船驅逐完成,所以盡量保證有足夠多的驅逐。國服則為等級100的白鷹白船驅逐。\n\n換裝備只會更換正在裝備中的欄位,即使是白裝也會更換。如果指定了旗艦,則會更換全部5個裝備,未指定旗艦隻會更換設備。", + "disabled": "不更换", + "ship": "更換艦船", + "ship_equip": "更換艦船 + 裝備" + }, + "CommonDD": { + "name": "指定前排", + "help": "", + "any": "任意", + "aulick_or_foote": "奧利克或富特", + "cassin_or_downes": "卡辛或唐斯" + }, "CommissionLimit": { "name": "防止緊急委託數量過多", "help": "在7x24運行時防止緊急委託數量過多做不完每日委託,建議在委託過濾器僅選擇短時長高收益委託" @@ -1642,7 +1665,7 @@ }, "LastRun": { "name": "上次檢查時間", - "help": "記錄上一次檢查的時間以防止任務重複執行,這個數值是自動記錄的,一般不需要修改" + "help": "記錄上一次檢查的時間以防止任務重複執行,檢查間隔為6天,這個數值是自動記錄的,一般不需要修改" } }, "Meowfficer": { @@ -2635,12 +2658,10 @@ "DeviceType": { "name": "設備類型", "help": "", - "emulator": "模擬器 安卓<=9", - "emulator_android_12": "模擬器 安卓>9", + "emulator": "模擬器", "plone_cloud_with_adb": "雲手機 有公網ADB", "phone_cloud_without_adb": "雲手機 無公網ADB", - "android_phone": "安卓真機 安卓<=9", - "android_phone_12": "安卓真機 安卓>9", + "android_phone": "安卓真機", "android_phone_vmos": "安卓真機VMOS虛擬機" }, "TestScene": { diff --git a/module/daemon/benchmark.py b/module/daemon/benchmark.py index c0e4ec778..cc43a8aee 100644 --- a/module/daemon/benchmark.py +++ b/module/daemon/benchmark.py @@ -69,17 +69,19 @@ class Benchmark(DaemonBase, CampaignUI): if not isinstance(cost, (float, int)): return Text(cost, style="bold bright_red") - if cost < 0.10: + if cost < 0.025: + return Text('Insane Fast', style="bold bright_green") + if cost < 0.100: return Text('Ultra Fast', style="bold bright_green") - if cost < 0.20: + if cost < 0.200: return Text('Very Fast', style="bright_green") - if cost < 0.30: + if cost < 0.300: return Text('Fast', style="green") - if cost < 0.50: + if cost < 0.500: return Text('Medium', style="yellow") - if cost < 0.75: + if cost < 0.750: return Text('Slow', style="red") - if cost < 1.00: + if cost < 1.000: return Text('Very Slow', style="bright_red") return Text('Ultra Slow', style="bold bright_red") @@ -88,11 +90,11 @@ class Benchmark(DaemonBase, CampaignUI): if not isinstance(cost, (float, int)): return Text(cost, style="bold bright_red") - if cost < 0.1: + if cost < 0.100: return Text('Fast', style="bright_green") - if cost < 0.2: + if cost < 0.200: return Text('Medium', style="yellow") - if cost < 0.4: + if cost < 0.400: return Text('Slow', style="red") return Text('Very Slow', style="bright_red") @@ -178,7 +180,9 @@ class Benchmark(DaemonBase, CampaignUI): return [l for l in screenshot if l not in args] # No ascreencap on Android > 9 - if device in ['emulator_android_12', 'android_phone_12']: + sdk = self.device.sdk_ver + logger.info(f'sdk_ver: {sdk}') + if not (21 <= sdk <= 28): screenshot = remove('aScreenCap', 'aScreenCap_nc') # No nc loopback if device in ['plone_cloud_with_adb']: @@ -187,6 +191,8 @@ class Benchmark(DaemonBase, CampaignUI): if device == 'android_phone_vmos': screenshot = ['ADB', 'aScreenCap', 'DroidCast', 'DroidCast_raw'] click = ['ADB', 'Hermit', 'MaaTouch'] + if self.device.nemu_ipc_available(): + screenshot.append('nemu_ipc') scene = self.config.Benchmark_TestScene if 'screenshot' not in scene: @@ -223,6 +229,8 @@ class Benchmark(DaemonBase, CampaignUI): screenshot = remove('aScreenCap', 'aScreenCap_nc') if self.device.is_chinac_phone_cloud: screenshot = remove('ADB_nc', 'aScreenCap_nc') + if self.device.nemu_ipc_available(): + screenshot.append('nemu_ipc') screenshot = tuple(screenshot) self.TEST_TOTAL = 3 diff --git a/module/device/app_control.py b/module/device/app_control.py index a6b45229e..1368a74f1 100644 --- a/module/device/app_control.py +++ b/module/device/app_control.py @@ -9,7 +9,7 @@ from module.logger import logger class AppControl(Adb, WSA, Uiautomator2): hierarchy: etree._Element - _app_u2_family = ['uiautomator2', 'minitouch', 'scrcpy', 'MaaTouch'] + _app_u2_family = ['uiautomator2', 'minitouch', 'scrcpy', 'MaaTouch', 'nemu_ipc'] def app_is_running(self) -> bool: method = self.config.Emulator_ControlMethod diff --git a/module/device/connection.py b/module/device/connection.py index cd5f75de0..0611d523b 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 @@ -11,7 +12,7 @@ import uiautomator2 as u2 from adbutils import AdbClient, AdbDevice, AdbTimeout, ForwardItem, ReverseItem from adbutils.errors import AdbError -from module.base.decorator import Config, cached_property, del_cached_property +from module.base.decorator import Config, cached_property, del_cached_property, run_once from module.base.utils import ensure_time from module.config.server import VALID_CHANNEL_PACKAGE, VALID_PACKAGE, set_server from module.device.connection_attr import ConnectionAttr @@ -84,6 +85,18 @@ 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 16384 <= self.port <= 17408 + class Connection(ConnectionAttr): def __init__(self, config): @@ -261,15 +274,20 @@ class Connection(ConnectionAttr): return True return False + @cached_property + def nemud_app_keep_alive(self) -> str: + res = self.adb_getprop('nemud.app_keep_alive') + logger.attr('nemud.app_keep_alive', res) + return res + @retry def check_mumu_app_keep_alive(self): if not self.is_mumu_family: return False - res = self.adb_getprop('nemud.app_keep_alive') - logger.attr('nemud.app_keep_alive', res) + res = self.nemud_app_keep_alive if res == '': - # Empry property, might not be a mumu emulator or might be an old mumu + # Empty property, probably MuMu6 or MuMu12 version < 3.5.6 return True elif res == 'false': # Disabled @@ -282,6 +300,15 @@ class Connection(ConnectionAttr): logger.warning(f'Invalid nemud.app_keep_alive value: {res}') return False + @cached_property + def is_mumu_over_version_356(self) -> bool: + """ + Returns: + bool: If MuMu12 version >= 3.5.6, + which has nemud.app_keep_alive and always be a vertical device + """ + return self.nemud_app_keep_alive != '' + @cached_property def _nc_server_host_port(self): """ @@ -747,23 +774,45 @@ class Connection(ConnectionAttr): If serial=='auto' and only 1 device detected, use it """ logger.hr('Detect device') - logger.info('Here are the available devices, ' - 'copy to Alas.Emulator.Serial to use it or set Alas.Emulator.Serial="auto"') - devices = self.list_device() + available = SelectedGrids([]) + devices = SelectedGrids([]) - # Show available devices - available = devices.select(status='device') - for device in available: - logger.info(device.serial) - if not len(available): - logger.info('No available devices') + @run_once + def brute_force_connect(): + logger.info('Brute force connect') + from deploy.Windows.emulator import EmulatorManager + manager = EmulatorManager() + manager.brute_force_connect() - # Show unavailable devices if having any - unavailable = devices.delete(available) - if len(unavailable): - logger.info('Here are the devices detected but unavailable') - for device in unavailable: - logger.info(f'{device.serial} ({device.status})') + for _ in range(2): + logger.info('Here are the available devices, ' + 'copy to Alas.Emulator.Serial to use it or set Alas.Emulator.Serial="auto"') + devices = self.list_device() + + # Show available devices + available = devices.select(status='device') + for device in available: + logger.info(device.serial) + if not len(available): + logger.info('No available devices') + + # Show unavailable devices if having any + unavailable = devices.delete(available) + if len(unavailable): + logger.info('Here are the devices detected but unavailable') + for device in unavailable: + logger.info(f'{device.serial} ({device.status})') + + # brute_force_connect + if self.config.Emulator_Serial == 'auto' and available.count == 0: + logger.warning(f'No available device found') + if sys.platform == 'win32': + brute_force_connect() + continue + else: + break + else: + break # Auto device detection if self.config.Emulator_Serial == 'auto': @@ -773,7 +822,16 @@ class Connection(ConnectionAttr): raise RequestHumanTakeover elif available.count == 1: logger.info(f'Auto device detection found only one device, using it') - self.serial = devices[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') \ + and available.select(may_mumu12_family=True): + logger.info(f'Auto device detection found MuMu12 device, using it') + # 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.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, ' @@ -782,6 +840,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 @@ -808,6 +867,57 @@ class Connection(ConnectionAttr): f'Using serial: {emu_serial}') self.serial = emu_serial + # Redirect MuMu12 from 127.0.0.1:7555 to 127.0.0.1:16xxx + if self.serial == '127.0.0.1:7555': + for _ in range(2): + mumu12 = available.select(may_mumu12_family=True) + if mumu12.count == 1: + emu_serial = mumu12.first_or_none().serial + logger.warning(f'Redirect MuMu12 {self.serial} to {emu_serial}') + self.config.Emulator_Serial = self.serial = emu_serial + break + elif mumu12.count >= 2: + logger.warning(f'Multiple MuMu12 serial found, cannot redirect') + break + 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() + # Show available devices + available = devices.select(status='device') + for device in available: + logger.info(device.serial) + if not len(available): + logger.info('No available devices') + continue + else: + # 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 65c483dd7..5fe64733a 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -142,11 +142,27 @@ 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 # 127.0.0.1:16384 + 32*n - return self.serial == '127.0.0.1:7555' or self.serial.startswith('127.0.0.1:16') + return self.serial == '127.0.0.1:7555' or self.is_mumu12_family + + @cached_property + def is_nox_family(self): + return 62001 <= self.port <= 63025 @cached_property def is_emulator(self): @@ -192,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/control.py b/module/device/control.py index 5834d3625..2a793c29d 100644 --- a/module/device/control.py +++ b/module/device/control.py @@ -5,11 +5,12 @@ from module.base.utils import * from module.device.method.hermit import Hermit from module.device.method.maatouch import MaaTouch from module.device.method.minitouch import Minitouch +from module.device.method.nemu_ipc import NemuIpc from module.device.method.scrcpy import Scrcpy from module.logger import logger -class Control(Hermit, Minitouch, Scrcpy, MaaTouch): +class Control(Hermit, Minitouch, Scrcpy, MaaTouch, NemuIpc): def handle_control_check(self, button): # Will be overridden in Device pass @@ -22,6 +23,7 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): 'minitouch': self.click_minitouch, 'Hermit': self.click_hermit, 'MaaTouch': self.click_maatouch, + 'nemu_ipc': self.click_nemu_ipc, } def click(self, button, control_check=True): @@ -78,6 +80,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): self.long_click_scrcpy(x, y, duration) elif method == 'MaaTouch': self.long_click_maatouch(x, y, duration) + elif method == 'nemu_ipc': + self.long_click_nemu_ipc(x, y, duration) else: self.swipe_adb((x, y), (x, y), duration) @@ -86,13 +90,9 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): p1, p2 = ensure_int(p1, p2) duration = ensure_time(duration) method = self.config.Emulator_ControlMethod - if method == 'minitouch': - logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2))) - elif method == 'uiautomator2': + if method == 'uiautomator2': logger.info('Swipe %s -> %s, %s' % (point2str(*p1), point2str(*p2), duration)) - elif method == 'scrcpy': - logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2))) - elif method == 'MaaTouch': + elif method in ['minitouch', 'MaaTouch', 'scrcpy', 'nemu_ipc']: logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2))) else: # ADB needs to be slow, or swipe doesn't work @@ -114,6 +114,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): self.swipe_scrcpy(p1, p2) elif method == 'MaaTouch': self.swipe_maatouch(p1, p2) + elif method == 'nemu_ipc': + self.swipe_nemu_ipc(p1, p2) else: self.swipe_adb(p1, p2, duration=duration) @@ -163,8 +165,10 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): self.drag_scrcpy(p1, p2, point_random=point_random) elif method == 'MaaTouch': self.drag_maatouch(p1, p2, point_random=point_random) + elif method == 'nemu_ipc': + self.drag_nemu_ipc(p1, p2, point_random=point_random) else: logger.warning(f'Control method {method} does not support drag well, ' f'falling back to ADB swipe may cause unexpected behaviour') self.swipe_adb(p1, p2, duration=ensure_time(swipe_duration * 2)) - self.click(Button(area=(), color=(), button=area_offset(point_random, p2), name=name ),False) + self.click(Button(area=(), color=(), button=area_offset(point_random, p2), name=name), False) diff --git a/module/device/device.py b/module/device/device.py index ff116ea18..038b6719b 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -1,23 +1,22 @@ import collections -import sys 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 from module.device.control import Control from module.device.screenshot import Screenshot -from module.exception import (GameNotRunningError, GameStuckError, - GameTooManyClickError, RequestHumanTakeover) +from module.exception import (EmulatorNotRunningError, GameNotRunningError, GameStuckError, GameTooManyClickError, + RequestHumanTakeover) from module.handler.assets import GET_MISSION from module.logger import logger -if sys.platform == 'win32': - from module.device.emulator import EmulatorManager -else: - class EmulatorManager: - pass - def show_function_call(): """ @@ -59,7 +58,7 @@ def show_function_call(): logger.info('Function calls:' + ''.join(func_list)) -class Device(Screenshot, Control, AppControl, EmulatorManager): +class Device(Screenshot, Control, AppControl): _screen_size_checked = False detect_record = set() click_record = collections.deque(maxlen=15) @@ -68,17 +67,39 @@ class Device(Screenshot, Control, AppControl, EmulatorManager): stuck_long_wait_list = ['BATTLE_STATUS_S', 'PAUSE', 'LOGIN_CHECK'] def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.screenshot_interval_set() + for _ in range(2): + try: + super().__init__(*args, **kwargs) + break + except EmulatorNotRunningError: + # Try to start emulator + if self.emulator_instance is not None: + self.emulator_start() + else: + logger.critical( + f'No emulator with serial "{self.config.Emulator_Serial}" found, ' + f'please set a correct serial' + ) + raise + + # Auto-fill emulator info + if self.config.EmulatorInfo_Emulator == 'auto': + _ = self.emulator_instance + + self.screenshot_interval_set() + self.method_check() - # Temp fix for MuMu 12 before DroidCast updated - if self.is_mumu_family: - logger.info('Patching screenshot method for mumu') - self.config.override(Emulator_ScreenshotMethod='ADB_nc') # Auto-select the fastest screenshot method 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. @@ -92,7 +113,23 @@ class Device(Screenshot, Control, AppControl, EmulatorManager): bench = Benchmark(config=self.config, device=self) method = bench.run_simple_screenshot_benchmark() # Set - self.config.Emulator_ScreenshotMethod = method + with self.config.multi_set(): + self.config.Emulator_ScreenshotMethod = method + # if method == 'nemu_ipc': + # self.config.Emulator_ControlMethod = 'nemu_ipc' + + def method_check(self): + """ + Check combinations of screenshot method and control methods + """ + # nemu_ipc should be together + # if self.config.Emulator_ScreenshotMethod == 'nemu_ipc' and self.config.Emulator_ControlMethod != 'nemu_ipc': + # logger.warning('When using nemu_ipc, both screenshot and control should use nemu_ipc') + # self.config.Emulator_ControlMethod = 'nemu_ipc' + # if self.config.Emulator_ScreenshotMethod != 'nemu_ipc' and self.config.Emulator_ControlMethod == 'nemu_ipc': + # logger.warning('When not using nemu_ipc, both screenshot and control should not use nemu_ipc') + # self.config.Emulator_ControlMethod = 'minitouch' + pass def handle_night_commission(self, daily_trigger='21:00', threshold=30): """ @@ -143,6 +180,8 @@ class Device(Screenshot, Control, AppControl, EmulatorManager): # stop it during wait if self.config.Emulator_ScreenshotMethod == 'scrcpy': self._scrcpy_server_stop() + if self.config.Emulator_ScreenshotMethod == 'nemu_ipc': + self.nemu_ipc_release() def stuck_record_add(self, button): self.detect_record.add(str(button)) diff --git a/module/device/emulator.py b/module/device/emulator.py deleted file mode 100644 index 44ef65592..000000000 --- a/module/device/emulator.py +++ /dev/null @@ -1,325 +0,0 @@ -import os -import re -import winreg -import subprocess - -from adbutils.errors import AdbError - -from deploy.emulator import VirtualBoxEmulator -from module.base.decorator import cached_property -from module.device.connection import Connection -from module.device.method.utils import get_serial_pair -from module.exception import RequestHumanTakeover, EmulatorNotRunningError -from module.logger import logger - - -class EmulatorInstance(VirtualBoxEmulator): - - def __init__(self, name, root_path, emu_path, - vbox_path=None, vbox_name=None, kill_para=None, multi_para=None): - """ - Args: - name (str): Emulator name in windows uninstall list. - root_path (str): Relative path from uninstall.exe to emulator installation folder. - emu_path (str): Relative path to executable simulator file. - vbox_path (str): Relative path to virtual box folder. - vbox_name (str): Regular Expression to match the name of .vbox file. - kill_para (str): Parameters required by kill emulator. - multi_para (str): Parameters required by start multi open emulator, - #id will be replaced with the real ID. - """ - super().__init__( - name=name, - root_path=root_path, - adb_path=None, - vbox_path=vbox_path, - vbox_name=vbox_name, - ) - self.emu_path = emu_path - self.kill_para = kill_para - self.multi_para = multi_para - - @cached_property - def id_and_serial(self): - """ - Returns: - list[str, str]: List of multi_id and serial. - """ - vbox = [] - for path, folders, files in os.walk(os.path.join(self.root, self.vbox_path)): - for file in files: - if re.match(self.vbox_name, file): - file = os.path.join(path, file) - vbox.append(file) - - serial = [] - for file in vbox: - with open(file, 'r', encoding='utf-8', errors='ignore') as f: - for line in f.readlines(): - # - res = re.search('<*?hostport="(.*?)".*?guestport="5555"/>', line) - if res: - serial.append([os.path.basename(file).split(".")[0], f'127.0.0.1:{res.group(1)}']) - - return serial - - -class Bluestacks5Instance(EmulatorInstance): - @cached_property - def root(self): - try: - return super().root - except FileNotFoundError: - self.name = 'BlueStacks_nxt_cn' - return super().root - - @cached_property - def id_and_serial(self): - try: - reg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\BlueStacks_nxt") - except FileNotFoundError: - reg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\BlueStacks_nxt_cn") - directory = winreg.QueryValueEx(reg, 'UserDefinedDir')[0] - - with open(os.path.join(directory, 'bluestacks.conf'), encoding='utf-8') as f: - content = f.read() - emulators = re.findall(r'bst.instance.(\w+).status.adb_port="(\d+)"', content) - serial = [] - for emulator in emulators: - serial.append([emulator[0], f'127.0.0.1:{emulator[1]}']) - return serial - - -class EmulatorManager(Connection): - pid = None - SUPPORTED_EMULATORS = { - 'nox_player': EmulatorInstance( - name="Nox", - root_path=".", - emu_path="./Nox.exe", - vbox_path="./BignoxVMS", - vbox_name='.*.vbox$', - kill_para='-quit', - multi_para='-clone:#id', - ), - 'mumu_player': EmulatorInstance( - name="Nemu", - root_path=".", - emu_path="./EmulatorShell/NemuPlayer.exe", - vbox_path="./vms", - vbox_name='.*.nemu$', - ), - 'bluestacks_5': Bluestacks5Instance( - name='BlueStacks_nxt', - root_path='.', - emu_path='./HD-Player.exe', - multi_para='--instance #id', - ), - } - - def detect_emulator(self, serial, emulator=None): - """ - Args: - serial (str): - emulator (EmulatorInstance): - - Returns: - list[EmulatorInstance, str]:Emulator and multi_id - """ - if emulator is None: - logger.info('Detect emulator from all emulators installed') - emulators = [] - for emulator in self.SUPPORTED_EMULATORS.values(): - try: - serials = emulator.id_and_serial - for cur_serial in serials: - if cur_serial[1] == serial: - emulators.append([emulator, cur_serial[0]]) - except FileNotFoundError: - pass - - logger.info('Detected emulators:') - for emulator in emulators: - logger.info(f'Name: {emulator[0].name}, Multi_id: {emulator[1]}') - - if len(emulators) == 1 or \ - (len(emulators) > 0 and emulators[0][0] == self.SUPPORTED_EMULATORS['mumu_player']): - logger.info('Find the only emulator, using it') - return emulators[0][0], emulators[0][1] - elif len(emulators) == 0: - logger.warning('The emulator corresponding to serial is not found, ' - 'please check the setting or use custom command') - else: - logger.warning('Multiple emulators with the same serial have been found, ' - 'please select one manually or use custom command') - raise RequestHumanTakeover - - else: - try: - logger.info(f'Detect emulator from {emulator.name}') - serials = emulator.id_and_serial - for cur_serial in serials: - if cur_serial[1] == serial: - logger.info('Find the only emulator, using it') - return emulator, cur_serial[0] - except FileNotFoundError: - pass - logger.warning('The emulator corresponding to serial is not found, ' - 'please check the setting or use custom command') - raise RequestHumanTakeover - - @staticmethod - def execute(command): - """ - Args: - command (str): - - Returns: - subprocess.Popen: - """ - command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') - logger.info(f'Execute: {command}') - return subprocess.Popen(command, close_fds=True) # only work on Windows - - @staticmethod - def task_kill(pid=None, name=None): - """ - Args: - pid (list, int): - name (list, str): - - Returns: - subprocess.Popen: - """ - command = 'taskkill ' - if pid is not None: - if isinstance(pid, list): - for p in pid: - command += f'/pid {p} ' - else: - command += f'/pid {pid} ' - elif name is not None: - if isinstance(name, list): - for n in name: - command += f'/im {n} ' - else: - command += f'/im {name} ' - else: - raise RequestHumanTakeover - command += '/t /f' - - return EmulatorManager.execute(command) - - def adb_connect(self, serial): - try: - return super(EmulatorManager, self).adb_connect(serial) - except EmulatorNotRunningError: - raise RequestHumanTakeover - - def detect_emulator_status(self, serial): - devices = self.list_device() - for device in devices: - if device.serial == serial: - return device.status - return 'offline' - - def emulator_start(self, serial, emulator=None, multi_id=None, command=None): - """ - Args: - serial (str): Expected serial after simulator starts successfully. - emulator (EmulatorInstance): Emulator to start. - multi_id (str): Emulator ID used by multi open emulator. - command (str): Customized path and parameters of the simulator to start. - - Return: - bool: If start successful. - """ - if command is None: - command = '\"' + os.path.abspath(os.path.join(emulator.root, emulator.emu_path)) + '\"' - if emulator.multi_para is not None and multi_id is not None: - command += " " + emulator.multi_para.replace("#id", multi_id) - - logger.info('Start emulator') - pipe = self.execute(command) - self.pid = pipe.pid - self.sleep(10) - - for _ in range(20): - if pipe.poll() is not None: - break - try: - if super().adb_connect(serial): - # Wait until emulator start completely - self.sleep(10) - return True - except EmulatorNotRunningError: - pass - self.sleep(5) - return False - - def emulator_kill(self, serial, emulator=None, multi_id=None, command=None): - """ - Args: - serial (str): Expected serial after simulator starts successfully. - emulator (EmulatorInstance): Emulator to start. - multi_id (str): Emulator ID used by multi open emulator. - command (str): Customized path and parameters of the simulator to start. - - Return: - bool: If kill successful. - """ - if command is None and emulator.kill_para is not None: - command = '\"' + os.path.abspath(os.path.join(emulator.root, emulator.emu_path)) + '\"' - if emulator.multi_para is not None and multi_id is not None: - command += " " + emulator.multi_para.replace("#id", multi_id) - command += " " + emulator.kill_para - - logger.info('Kill emulator') - if emulator == self.SUPPORTED_EMULATORS['bluestacks_5']: - try: - self.adb_command(['reboot', '-p'], timeout=20) - if self.detect_emulator_status(serial) == 'offline': - self.pid = None - return True - except AdbError: - return False - - if emulator == self.SUPPORTED_EMULATORS['mumu_player']: - self.task_kill(pid=None, name=['NemuHeadless.exe', 'NemuPlayer.exe', 'NemuSvc.exe']) - elif command is not None: - self.execute(command) - else: - self.task_kill(pid=self.pid, name=os.path.basename(emulator.emu_path)) - self.sleep(5) - - for _ in range(10): - if self.detect_emulator_status(serial) == 'offline': - self.pid = None - return True - self.sleep(2) - return False - - def emulator_restart(self): - serial, _ = get_serial_pair(self.serial) - if serial is None: - serial = self.serial - - if os.name != 'nt': - logger.warning('Restart simulator only works under Windows platform') - return False - - logger.hr('Emulator restart') - if self.config.RestartEmulator_EmulatorType == 'auto': - emulator, multi_id = self.detect_emulator(serial) - else: - emulator = self.SUPPORTED_EMULATORS[self.config.RestartEmulator_EmulatorType] - emulator, multi_id = self.detect_emulator(serial, emulator=emulator) - - for _ in range(3): - if not self.emulator_kill(serial, emulator, multi_id): - continue - if self.emulator_start(serial, emulator, multi_id): - return True - - logger.warning('Restart emulator failed for 3 times, please check your settings') - raise RequestHumanTakeover diff --git a/module/device/method/adb.py b/module/device/method/adb.py index 282447ce2..bd3397edf 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -128,7 +128,7 @@ class Adb(Connection): if image is None: raise ImageTruncated('Empty image after cv2.imdecode') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') diff --git a/module/device/method/ascreencap.py b/module/device/method/ascreencap.py index 10ac1110c..c93321cfe 100644 --- a/module/device/method/ascreencap.py +++ b/module/device/method/ascreencap.py @@ -165,11 +165,11 @@ class AScreenCap(Connection): # ValueError: cannot reshape array of size 0 into shape (720,1280,4) raise ImageTruncated(str(e)) - image = cv2.flip(image, 0) + cv2.flip(image, 0, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.flip') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index dc4b5360c..3c7a97762 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -95,6 +95,8 @@ class DroidCast(Uiautomator2): """ _droidcast_port: int = 0 + droidcast_width: int = 0 + droidcast_height: int = 0 @cached_property def droidcast_session(self): @@ -112,15 +114,37 @@ class DroidCast(Uiautomator2): - /preview To get PNG screenshots. """ + def droidcast_url(self, url='/preview'): + if self.is_mumu_over_version_356: + w, h = self.droidcast_width, self.droidcast_height + if self.orientation == 0: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={w}&height={h}' + elif self.orientation == 1: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={h}&height={w}' + else: + # logger.warning('DroidCast receives invalid device orientation') + pass + return f'http://127.0.0.1:{self._droidcast_port}{url}' def droidcast_raw_url(self, url='/screenshot'): + if self.is_mumu_over_version_356: + w, h = self.droidcast_width, self.droidcast_height + if self.orientation == 0: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={w}&height={h}' + elif self.orientation == 1: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={h}&height={w}' + else: + # logger.warning('DroidCast receives invalid device orientation') + pass + return f'http://127.0.0.1:{self._droidcast_port}{url}' def droidcast_init(self): logger.hr('DroidCast init') self.droidcast_stop() + self._droidcast_update_resolution() logger.info('Pushing DroidCast apk') self.adb_push(self.config.DROIDCAST_FILEPATH_LOCAL, self.config.DROIDCAST_FILEPATH_REMOTE) @@ -150,10 +174,25 @@ class DroidCast(Uiautomator2): else: logger.error(f'Unknown DROIDCAST_VERSION: {self.config.DROIDCAST_VERSION}') + def _droidcast_update_resolution(self): + if self.is_mumu_over_version_356: + logger.info('Update droidcast resolution') + w, h = self.resolution_uiautomator2(cal_rotation=False) + self.get_orientation() + # 720, 1280 + # mumu12 > 3.5.6 is always a vertical device + self.droidcast_width, self.droidcast_height = w, h + logger.info(f'Droicast resolution: {(w, h)}') + @retry def screenshot_droidcast(self): self.config.DROIDCAST_VERSION = 'DroidCast' + if self.is_mumu_over_version_356: + if not self.droidcast_width or not self.droidcast_height: + self._droidcast_update_resolution() + resp = self.droidcast_session.get(self.droidcast_url(), timeout=3) + if resp.status_code == 404: raise DroidCastVersionIncompatible('DroidCast server does not have /preview') image = resp.content @@ -169,20 +208,31 @@ class DroidCast(Uiautomator2): if image is None: raise ImageTruncated('Empty image after cv2.imdecode') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') + if self.is_mumu_over_version_356: + if self.orientation == 1: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + return image @retry def screenshot_droidcast_raw(self): self.config.DROIDCAST_VERSION = 'DroidCast_raw' + shape = (720, 1280) + if self.is_mumu_over_version_356: + if not self.droidcast_width or not self.droidcast_height: + self._droidcast_update_resolution() + if self.droidcast_height and self.droidcast_width: + shape = (self.droidcast_height, self.droidcast_width) + image = self.droidcast_session.get(self.droidcast_raw_url(), timeout=3).content # DroidCast_raw returns a RGB565 bitmap try: - arr = np.frombuffer(image, dtype=np.uint16).reshape((720, 1280)) + arr = np.frombuffer(image, dtype=np.uint16).reshape(shape) except ValueError as e: if len(image) < 500: logger.warning(f'Unexpected screenshot: {image}') @@ -211,14 +261,30 @@ class DroidCast(Uiautomator2): # image = cv2.merge([r, g, b]) # The same as the code above but costs about 5ms instead of 10ms. - r = cv2.multiply(arr & 0b1111100000000000, 0.00390625).astype(np.uint8) - g = cv2.multiply(arr & 0b0000011111100000, 0.125).astype(np.uint8) - b = cv2.multiply(arr & 0b0000000000011111, 8).astype(np.uint8) - r = cv2.add(r, cv2.multiply(r, 0.03125)) - g = cv2.add(g, cv2.multiply(g, 0.015625)) - b = cv2.add(b, cv2.multiply(b, 0.03125)) + r = cv2.bitwise_and(arr, 0b1111100000000000) + cv2.multiply(r, 0.00390625, dst=r) + r = np.uint8(r) + m = cv2.multiply(r, 0.03125) + cv2.add(r, m, dst=r) + + g = cv2.bitwise_and(arr, 0b0000011111100000) + cv2.multiply(g, 0.125, dst=g) + g = np.uint8(g) + m = cv2.multiply(g, 0.015625) + cv2.add(g, m, dst=g) + + b = cv2.bitwise_and(arr, 0b0000000000011111) + cv2.multiply(b, 8, dst=b) + b = np.uint8(b) + m = cv2.multiply(b, 0.03125) + cv2.add(b, m, dst=b) + image = cv2.merge([r, g, b]) + if self.is_mumu_over_version_356: + if self.orientation == 1: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + return image def droidcast_wait_startup(self): diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index b1d6e3471..300e8ff9c 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 @@ -90,12 +104,37 @@ class MaaTouch(Connection): max_y: int _maatouch_stream = socket.socket _maatouch_stream_storage = None + _maatouch_init_thread = None @cached_property - def maatouch_builder(self): + 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 maatouch_init(self): logger.hr('MaaTouch init') @@ -166,14 +205,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 +227,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 +235,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 +243,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 +260,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..18a09009f 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 @@ -324,7 +328,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,7 +337,7 @@ 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): @@ -345,7 +349,7 @@ def retry(func): 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) @@ -366,12 +370,38 @@ class Minitouch(Connection): _minitouch_ws: websockets.WebSocketClientProtocol max_x: int max_y: int + _minitouch_init_thread = None @cached_property - def minitouch_builder(self): + 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') @@ -446,14 +476,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 +544,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 +553,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 +569,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 +577,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 +594,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 new file mode 100644 index 000000000..b79e1c322 --- /dev/null +++ b/module/device/method/nemu_ipc.py @@ -0,0 +1,541 @@ +import asyncio +import ctypes +import os +import sys +from functools import partial, wraps + +import cv2 +import numpy as np + +from module.base.decorator import cached_property, del_cached_property, has_cached_property +from module.base.utils import ensure_time +from module.device.method.minitouch import insert_swipe, random_rectangle_point +from module.device.method.utils import RETRY_TRIES, retry_sleep +from module.device.platform import Platform +from module.exception import RequestHumanTakeover +from module.logger import logger + + +class NemuIpcIncompatible(Exception): + pass + + +class NemuIpcError(Exception): + pass + + +class CaptureStd: + """ + Capture stdout and stderr from both python and C library + https://stackoverflow.com/questions/5081657/how-do-i-prevent-a-c-shared-library-to-print-on-stdout-in-python/17954769 + + ``` + with CaptureStd() as capture: + # String wasn't printed + print('whatever') + # But captured in ``capture.stdout`` + print(f'Got stdout: "{capture.stdout}"') + print(f'Got stderr: "{capture.stderr}"') + ``` + """ + + def __init__(self): + self.stdout = b'' + self.stderr = b'' + + def _redirect_stdout(self, to): + sys.stdout.close() + os.dup2(to, self.fdout) + sys.stdout = os.fdopen(self.fdout, 'w') + + def _redirect_stderr(self, to): + sys.stderr.close() + os.dup2(to, self.fderr) + sys.stderr = os.fdopen(self.fderr, 'w') + + def __enter__(self): + self.fdout = sys.stdout.fileno() + self.fderr = sys.stderr.fileno() + self.reader_out, self.writer_out = os.pipe() + self.reader_err, self.writer_err = os.pipe() + self.old_stdout = os.dup(self.fdout) + self.old_stderr = os.dup(self.fderr) + + file_out = os.fdopen(self.writer_out, 'w') + file_err = os.fdopen(self.writer_err, 'w') + self._redirect_stdout(to=file_out.fileno()) + self._redirect_stderr(to=file_err.fileno()) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._redirect_stdout(to=self.old_stdout) + self._redirect_stderr(to=self.old_stderr) + os.close(self.old_stdout) + os.close(self.old_stderr) + + self.stdout = self.recvall(self.reader_out) + self.stderr = self.recvall(self.reader_err) + os.close(self.reader_out) + os.close(self.reader_err) + + @staticmethod + def recvall(reader, length=1024) -> bytes: + fragments = [] + while 1: + chunk = os.read(reader, length) + if chunk: + fragments.append(chunk) + else: + break + output = b''.join(fragments) + return output + + +class CaptureNemuIpc(CaptureStd): + instance = None + + def is_capturing(self): + """ + Only capture at the topmost wrapper to avoid nested capturing + If a capture is ongoing, this instance does nothing + """ + cls = self.__class__ + return isinstance(cls.instance, cls) and cls.instance != self + + def __enter__(self): + if self.is_capturing(): + return self + + super().__enter__() + CaptureNemuIpc.instance = self + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.is_capturing(): + return + + CaptureNemuIpc.instance = None + super().__exit__(exc_type, exc_val, exc_tb) + + self.check_stdout() + self.check_stderr() + + def check_stdout(self): + if not self.stdout: + return + logger.info(f'NemuIpc stdout: {self.stdout}') + + def check_stderr(self): + if not self.stderr: + return + logger.error(f'NemuIpc stderr: {self.stderr}') + + # Calling an old MuMu12 player + # Tested on 3.4.0 + # b'nemu_capture_display rpc error: 1783\r\n' + # Tested on 3.7.3 + # b'nemu_capture_display rpc error: 1745\r\n' + if b'error: 1783' in self.stderr or b'error: 1745' in self.stderr: + raise NemuIpcIncompatible( + f'NemuIpc requires MuMu12 version >= 3.8.13, please check your version') + # contact_id incorrect + # b'nemu_capture_display cannot find rpc connection\r\n' + if b'cannot find rpc connection' in self.stderr: + raise NemuIpcError(self.stderr) + # Emulator died + # b'nemu_capture_display rpc error: 1722\r\n' + # 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): + @wraps(func) + def retry_wrapper(self, *args, **kwargs): + """ + Args: + self (NemuIpcImpl): + """ + init = None + for _ in range(RETRY_TRIES): + try: + if callable(init): + retry_sleep(_) + init() + return func(self, *args, **kwargs) + # Can't handle + except RequestHumanTakeover: + break + # Can't handle + except NemuIpcIncompatible as e: + logger.error(e) + break + # Function call timeout + except asyncio.TimeoutError: + logger.warning(f'Func {func.__name__}() call timeout, retrying: {_}') + + def init(): + self.reconnect() + # NemuIpcError + except NemuIpcError as e: + logger.error(e) + + def init(): + self.reconnect() + # Unknown, probably a trucked image + except Exception as e: + logger.exception(e) + + def init(): + pass + + logger.critical(f'Retry {func.__name__}() failed') + raise RequestHumanTakeover + + return retry_wrapper + + +class NemuIpcImpl: + def __init__(self, nemu_folder: str, instance_id: int, display_id: int = 0): + """ + Args: + nemu_folder: Installation path of MuMu12, e.g. E:/ProgramFiles/MuMuPlayer-12.0 + instance_id: Emulator instance ID, starting from 0 + display_id: Always 0 if keep app alive was disabled + """ + self.nemu_folder: str = nemu_folder + self.instance_id: int = instance_id + self.display_id: int = display_id + + ipc_dll = os.path.abspath(os.path.join(nemu_folder, './shell/sdk/external_renderer_ipc.dll')) + logger.info( + f'NemuIpcImpl init, ' + f'nemu_folder={nemu_folder}, ' + f'ipc_dll={ipc_dll}, ' + f'instance_id={instance_id}, ' + f'display_id={display_id}' + ) + + try: + self.lib = ctypes.CDLL(ipc_dll) + except OSError as e: + logger.error(e) + # OSError: [WinError 126] 找不到指定的模块。 + if not os.path.exists(ipc_dll): + raise NemuIpcIncompatible( + f'ipc_dll={ipc_dll} does not exist, ' + f'NemuIpc requires MuMu12 version >= 3.8.13, please check your version') + else: + raise NemuIpcIncompatible( + f'ipc_dll={ipc_dll} exists, but cannot be loaded') + self.connect_id: int = 0 + self.width = 0 + self.height = 0 + + def connect(self): + if self.connect_id > 0: + return + + 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' + ) + + self.connect_id = connect_id + # logger.info(f'NemuIpc connected: {self.connect_id}') + + def disconnect(self): + if self.connect_id == 0: + return + + self.ev_run_sync( + self.lib.nemu_disconnect, + self.connect_id + ) + + # logger.info(f'NemuIpc disconnected: {self.connect_id}') + self.connect_id = 0 + + def reconnect(self): + self.disconnect() + self.connect() + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.disconnect() + + @cached_property + def _ev(self): + return asyncio.new_event_loop() + + async def ev_run_async(self, func, *args, **kwargs): + """ + Args: + func: Sync function to call + *args: + **kwargs: + + Raises: + asyncio.TimeoutError: If function call timeout + """ + func_wrapped = partial(func, *args, **kwargs) + # 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): + """ + Args: + func: Sync function to call + *args: + **kwargs: + + 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): + """ + Get emulator resolution, `self.width` and `self.height` will be set + """ + if self.connect_id == 0: + self.connect() + + width_ptr = ctypes.pointer(ctypes.c_int(0)) + height_ptr = ctypes.pointer(ctypes.c_int(0)) + nullptr = ctypes.POINTER(ctypes.c_int)() + + 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 + self.height = height_ptr.contents.value + + @retry + def screenshot(self): + """ + Returns: + np.ndarray: Image array in RGBA color space + Note that image is upside down + """ + if self.connect_id == 0: + self.connect() + + 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)()) + + 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()') + + # image = np.ctypeslib.as_array(pixels_pointer, shape=(self.height, self.width, 4)) + image = np.ctypeslib.as_array(pixels_pointer.contents).reshape((self.height, self.width, 4)) + return image + + def convert_xy(self, x, y): + """ + Convert classic ADB coordinates to Nemu's + `self.height` must be updated before calling this method + + Returns: + int, int + """ + x, y = int(x), int(y) + x, y = self.height - y, x + return x, y + + @retry + def down(self, x, y): + """ + Contact down, continuous contact down will be considered as swipe + """ + if self.connect_id == 0: + self.connect() + if self.height == 0: + self.get_resolution() + + x, y = self.convert_xy(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') + + @retry + def up(self): + """ + Contact up + """ + if self.connect_id == 0: + self.connect() + + 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') + + +def serial_to_id(serial: str): + """ + Predict instance ID from serial + E.g. + "127.0.0.1:16384" -> 0 + "127.0.0.1:16416" -> 1 + + Returns: + int: instance_id, or None if failed to predict + """ + try: + port = int(serial.split(':')[1]) + except (IndexError, ValueError): + return None + index, offset = divmod(port - 16384, 32) + if 0 <= index < 32 and offset in [0, 1, 2]: + return index + else: + return None + + +class NemuIpc(Platform): + @cached_property + def nemu_ipc(self) -> NemuIpcImpl: + """ + Initialize a nemu ipc implementation + """ + # Try existing settings first + if self.config.EmulatorInfo_path: + folder = os.path.abspath(os.path.join(self.config.EmulatorInfo_path, '../../')) + index = serial_to_id(self.serial) + if index is not None: + try: + return NemuIpcImpl( + nemu_folder=folder, + instance_id=index, + display_id=0 + ).__enter__() + except (NemuIpcIncompatible, NemuIpcError) as e: + logger.error(e) + logger.error('Emulator info incorrect') + + # Search emulator instance + # with E:\ProgramFiles\MuMuPlayer-12.0\shell\MuMuPlayer.exe + # installation path is E:\ProgramFiles\MuMuPlayer-12.0 + 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('../'), + instance_id=self.emulator_instance.MuMuPlayer12_id, + display_id=0 + ).__enter__() + except (NemuIpcIncompatible, NemuIpcError) as e: + logger.error(e) + logger.error('Unable to initialize NemuIpc') + raise RequestHumanTakeover + + def nemu_ipc_available(self) -> bool: + if not self.is_mumu_family: + return False + if self.nemud_app_keep_alive == '': + return False + try: + _ = self.nemu_ipc + except RequestHumanTakeover: + return False + return True + + def nemu_ipc_release(self): + if has_cached_property(self, 'nemu_ipc'): + self.nemu_ipc.disconnect() + del_cached_property(self, 'nemu_ipc') + logger.info('nemu_ipc released') + + def screenshot_nemu_ipc(self): + image = self.nemu_ipc.screenshot() + + image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR) + cv2.flip(image, 0, dst=image) + return image + + def click_nemu_ipc(self, x, y): + down = ensure_time((0.010, 0.020)) + 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): + 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) + + for point in points: + self.nemu_ipc.down(*point) + self.sleep(0.010) + + 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) + + 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.up() + self.sleep(0.050) diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index dea534a31..765f7dde6 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -122,7 +122,7 @@ class Uiautomator2(Connection): if image is None: raise ImageTruncated('Empty image after cv2.imdecode') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') @@ -243,7 +243,7 @@ class Uiautomator2(Connection): return hierarchy @retry - def resolution_uiautomator2(self) -> t.Tuple[int, int]: + def resolution_uiautomator2(self, cal_rotation=True) -> t.Tuple[int, int]: """ Faster u2.window_size(), cause that calls `dumpsys display` twice. @@ -252,9 +252,10 @@ class Uiautomator2(Connection): """ info = self.u2.http.get('/info').json() w, h = info['display']['width'], info['display']['height'] - rotation = self.get_orientation() - if (w > h) != (rotation % 2 == 1): - w, h = h, w + if cal_rotation: + rotation = self.get_orientation() + if (w > h) != (rotation % 2 == 1): + w, h = h, w return w, h def resolution_check_uiautomator2(self): diff --git a/module/device/method/utils.py b/module/device/method/utils.py index 477188b3d..817cb4ab8 100644 --- a/module/device/method/utils.py +++ b/module/device/method/utils.py @@ -18,9 +18,11 @@ except ImportError: # We expect `screencap | nc 192.168.0.1 20298` instead of `screencap '|' nc 192.168.80.1 20298` import adbutils import subprocess + adbutils._utils.list2cmdline = subprocess.list2cmdline adbutils._device.list2cmdline = subprocess.list2cmdline + # BaseDevice.shell() is missing a check_okay() call before reading output, # resulting in an `OKAY` prefix in output. def shell(self, @@ -40,6 +42,7 @@ except ImportError: output = c.read_until_close() return output.rstrip() if rstrip else output + adbutils._device.BaseDevice.shell = shell from module.base.decorator import cached_property @@ -309,7 +312,7 @@ class HierarchyButton: if res: return res[0] else: - return 'HierarchyButton' + return self.xpath @cached_property def count(self): @@ -319,15 +322,30 @@ class HierarchyButton: def exist(self): return self.count == 1 + @cached_property + def attrib(self): + if self.exist: + return self.nodes[0].attrib + else: + return {} + @cached_property def area(self): if self.exist: - bounds = self.nodes[0].attrib.get("bounds") + bounds = self.attrib.get("bounds") lx, ly, rx, ry = map(int, re.findall(r"\d+", bounds)) return lx, ly, rx, ry else: return None + @cached_property + def size(self): + if self.area is not None: + lx, ly, rx, ry = self.area + return rx - lx, ry - ly + else: + return None + @cached_property def button(self): return self.area @@ -338,9 +356,68 @@ class HierarchyButton: def __str__(self): return self.name + """ + Element props + """ + + def _get_bool_prop(self, prop: str) -> bool: + return self.attrib.get(prop, "").lower() == 'true' + @cached_property - def focused(self): - if self.exist: - return self.nodes[0].attrib.get("focused").lower() == 'true' - else: - return False + def index(self) -> int: + try: + return int(self.attrib.get("index", 0)) + except IndexError: + return 0 + + @cached_property + def text(self) -> str: + return self.attrib.get("text", "").strip() + + @cached_property + def resourceId(self) -> str: + return self.attrib.get("resourceId", "").strip() + + @cached_property + def package(self) -> str: + return self.attrib.get("resourceId", "").strip() + + @cached_property + def description(self) -> str: + return self.attrib.get("resourceId", "").strip() + + @cached_property + def checkable(self) -> bool: + return self._get_bool_prop('checkable') + + @cached_property + def clickable(self) -> bool: + return self._get_bool_prop('clickable') + + @cached_property + def enabled(self) -> bool: + return self._get_bool_prop('enabled') + + @cached_property + def fucusable(self) -> bool: + return self._get_bool_prop('fucusable') + + @cached_property + def focused(self) -> bool: + return self._get_bool_prop('focused') + + @cached_property + def scrollable(self) -> bool: + return self._get_bool_prop('scrollable') + + @cached_property + def longClickable(self) -> bool: + return self._get_bool_prop('longClickable') + + @cached_property + def password(self) -> bool: + return self._get_bool_prop('password') + + @cached_property + def selected(self) -> bool: + return self._get_bool_prop('selected') diff --git a/module/device/pkg_resources/__init__.py b/module/device/pkg_resources/__init__.py new file mode 100644 index 000000000..61014ef79 --- /dev/null +++ b/module/device/pkg_resources/__init__.py @@ -0,0 +1,82 @@ +import os +import re +import sys + +from module.base.decorator import cached_property + +""" +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 +sys.modules['pkg_resources'] = sys.modules['module.device.pkg_resources'] + + +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 + res = re.match(r'^(.+)-(.+)\.dist-info$', file) + if res: + obj = FakeDistributionObject( + dist=res.group(1), + version=res.group(2), + ) + 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', '0.11.0') + if dist == 'uiautomator2': + return PACKAGE_CACHE.dict_installed_packages.get('uiautomator2', '2.16.17') + + +class DistributionNotFound(Exception): + pass diff --git a/module/device/platform/__init__.py b/module/device/platform/__init__.py new file mode 100644 index 000000000..0dbc4b9fb --- /dev/null +++ b/module/device/platform/__init__.py @@ -0,0 +1,6 @@ +import sys + +if sys.platform == 'win32': + from module.device.platform.platform_windows import PlatformWindows as Platform +else: + from module.device.platform.platform_base import PlatformBase as Platform diff --git a/module/device/platform/emulator_base.py b/module/device/platform/emulator_base.py index f5decef4f..94483e9f9 100644 --- a/module/device/platform/emulator_base.py +++ b/module/device/platform/emulator_base.py @@ -1,14 +1,56 @@ import os +import re import typing as t from dataclasses import dataclass -from deploy.utils import cached_property, iter_folder +from module.device.platform.utils import cached_property, iter_folder def abspath(path): return os.path.abspath(path).replace('\\', '/') +def get_serial_pair(serial): + """ + Args: + serial (str): + + Returns: + str, str: `127.0.0.1:5555+{X}` and `emulator-5554+{X}`, 0 <= X <= 32 + """ + if serial.startswith('127.0.0.1:'): + try: + port = int(serial[10:]) + if 5555 <= port <= 5555 + 32: + return f'127.0.0.1:{port}', f'emulator-{port - 1}' + except (ValueError, IndexError): + pass + if serial.startswith('emulator-'): + try: + port = int(serial[9:]) + if 5554 <= port <= 5554 + 32: + return f'127.0.0.1:{port + 1}', f'emulator-{port}' + except (ValueError, IndexError): + pass + + 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 @@ -27,7 +69,7 @@ class EmulatorInstanceBase: Returns: str: Emulator type, such as Emulator.NoxPlayer """ - return EmulatorBase.path_to_type(self.path) + return self.emulator.type @cached_property def emulator(self): @@ -52,22 +94,46 @@ class EmulatorInstanceBase: def __bool__(self): return True + @cached_property + def MuMuPlayer12_id(self): + """ + Convert MuMu 12 instance name to instance id. + Example names: + MuMuPlayer-12.0-3 + YXArkNights-12.0-1 + + Returns: + int: Instance ID, or None if this is not a MuMu 12 instance + """ + res = re.search(r'MuMuPlayer-12.0-(\d+)', self.name) + if res: + return int(res.group(1)) + res = re.search(r'YXArkNights-12.0-(\d+)', self.name) + if res: + return int(res.group(1)) + + return None + class EmulatorBase: + # Values here must match those in argument.yaml EmulatorInfo.Emulator.option NoxPlayer = 'NoxPlayer' NoxPlayer64 = 'NoxPlayer64' NoxPlayerFamily = [NoxPlayer, NoxPlayer64] BlueStacks4 = 'BlueStacks4' BlueStacks5 = 'BlueStacks5' + BlueStacks4HyperV = 'BlueStacks4HyperV' + BlueStacks5HyperV = 'BlueStacks5HyperV' BlueStacksFamily = [BlueStacks4, BlueStacks5] LDPlayer3 = 'LDPlayer3' LDPlayer4 = 'LDPlayer4' LDPlayer9 = 'LDPlayer9' LDPlayerFamily = [LDPlayer3, LDPlayer4, LDPlayer9] - MumuPlayer = 'MumuPlayer' - MumuPlayer9 = 'MumuPlayer9' - MumuPlayerFamily = [MumuPlayer, MumuPlayer9] - MemuPlayer = 'MemuPlayer' + MuMuPlayer = 'MuMuPlayer' + MuMuPlayerX = 'MuMuPlayerX' + MuMuPlayer12 = 'MuMuPlayer12' + MuMuPlayerFamily = [MuMuPlayer, MuMuPlayerX, MuMuPlayer12] + MEmuPlayer = 'MEmuPlayer' @classmethod def path_to_type(cls, path: str) -> str: @@ -81,12 +147,19 @@ class EmulatorBase: """ return '' - def iter_instances(self): + def iter_instances(self) -> t.Iterable[EmulatorInstanceBase]: """ Yields: EmulatorInstance: Emulator instances found in this emulator """ - return + pass + + def iter_adb_binaries(self) -> t.Iterable[str]: + """ + Yields: + str: Filepath to adb binaries found in this emulator + """ + pass def __init__(self, path): # Path to .exe file @@ -143,13 +216,18 @@ class EmulatorBase: list[str]: """ folder = self.abspath(folder) - try: - return list(iter_folder(folder, is_dir=is_dir, ext=ext)) - except FileNotFoundError: - return [] + return list(iter_folder(folder, is_dir=is_dir, ext=ext)) 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]: """ @@ -163,3 +241,30 @@ class EmulatorManagerBase: Get all emulator instances installed on current computer. """ return [] + + @cached_property + def all_emulator_serials(self) -> t.List[str]: + """ + Returns: + list[str]: All possible serials on current computer. + """ + out = [] + for emulator in self.all_emulator_instances: + out.append(emulator.serial) + # Also add serial like `emulator-5554` + port_serial, emu_serial = get_serial_pair(emulator.serial) + if emu_serial: + out.append(emu_serial) + return out + + @cached_property + def all_adb_binaries(self) -> t.List[str]: + """ + Returns: + list[str]: All adb binaries of emulators on current computer. + """ + out = [] + for emulator in self.all_emulators: + for exe in emulator.iter_adb_binaries(): + out.append(exe) + return out diff --git a/module/device/platform/windows_emulator.py b/module/device/platform/emulator_windows.py similarity index 68% rename from module/device/platform/windows_emulator.py rename to module/device/platform/emulator_windows.py index cc08bb41b..f7a5e54bc 100644 --- a/module/device/platform/windows_emulator.py +++ b/module/device/platform/emulator_windows.py @@ -5,11 +5,12 @@ import typing as t import winreg from dataclasses import dataclass -import psutil - -from module.base.decorator import cached_property -from module.config.utils import iter_folder -from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase +# 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, \ + remove_duplicated_path +from module.device.platform.utils import cached_property, iter_folder @dataclass @@ -56,14 +57,6 @@ def abspath(path): class EmulatorInstance(EmulatorInstanceBase): - @cached_property - def type(self) -> str: - """ - Returns: - str: Emulator type, such as Emulator.NoxPlayer - """ - return Emulator.path_to_type(self.path) - @cached_property def emulator(self): """ @@ -78,7 +71,7 @@ class Emulator(EmulatorBase): def path_to_type(cls, path: str) -> str: """ Args: - path: Path to .exe file + path: Path to .exe file, case insensitive Returns: str: Emulator type, such as Emulator.NoxPlayer @@ -86,45 +79,50 @@ class Emulator(EmulatorBase): folder, exe = os.path.split(path) folder, dir1 = os.path.split(folder) folder, dir2 = os.path.split(folder) - if exe == 'Nox.exe': - if dir2 == 'Nox': + exe = exe.lower() + dir1 = dir1.lower() + dir2 = dir2.lower() + if exe == 'nox.exe': + if dir2 == 'nox': return cls.NoxPlayer - elif dir2 == 'Nox64': + elif dir2 == 'nox64': return cls.NoxPlayer64 else: return cls.NoxPlayer - if exe == 'Bluestacks.exe': - if dir1 in ['BlueStacks', 'BlueStacks_cn']: + if exe == 'bluestacks.exe': + if dir1 in ['bluestacks', 'bluestacks_cn']: return cls.BlueStacks4 - elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']: + elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']: return cls.BlueStacks5 else: return cls.BlueStacks4 - if exe == 'HD-Player.exe': - if dir1 in ['BlueStacks', 'BlueStacks_cn']: + if exe == 'hd-player.exe': + if dir1 in ['bluestacks', 'bluestacks_cn']: return cls.BlueStacks4 - elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']: + elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']: return cls.BlueStacks5 else: return cls.BlueStacks5 if exe == 'dnplayer.exe': - if dir1 == 'LDPlayer': + if dir1 == 'ldplayer': return cls.LDPlayer3 - elif dir1 == 'LDPlayer4': + elif dir1 == 'ldplayer4': return cls.LDPlayer4 - elif dir1 == 'LDPlayer9': + elif dir1 == 'ldplayer9': return cls.LDPlayer9 else: return cls.LDPlayer3 - if exe == 'NemuPlayer.exe': + if exe == 'nemuplayer.exe': if dir2 == 'nemu': - return cls.MumuPlayer + return cls.MuMuPlayer elif dir2 == 'nemu9': - return cls.MumuPlayer9 + return cls.MuMuPlayerX else: - return cls.MumuPlayer - if exe == 'MEmu.exe': - return cls.MemuPlayer + return cls.MuMuPlayer + if exe == 'mumuplayer.exe': + return cls.MuMuPlayer12 + if exe == 'memu.exe': + return cls.MEmuPlayer return '' @@ -148,6 +146,10 @@ class Emulator(EmulatorBase): yield exe.replace('dnmultiplayer.exe', 'dnplayer.exe') elif 'NemuMultiPlayer.exe' in exe: yield exe.replace('NemuMultiPlayer.exe', 'NemuPlayer.exe') + elif 'MuMuMultiPlayer.exe' in exe: + yield exe.replace('MuMuMultiPlayer.exe', 'MuMuPlayer.exe') + elif 'MuMuManager.exe' in exe: + yield exe.replace('MuMuManager.exe', 'MuMuPlayer.exe') elif 'MEmuConsole.exe' in exe: yield exe.replace('MEmuConsole.exe', 'MEmu.exe') else: @@ -250,14 +252,14 @@ class Emulator(EmulatorBase): name=folder, path=self.path ) - elif self == Emulator.MumuPlayer: + elif self == Emulator.MuMuPlayer: # MuMu has no multi instances, on 7555 only yield EmulatorInstance( serial='127.0.0.1:7555', name='', path=self.path, ) - elif self == Emulator.MumuPlayer9: + elif self == Emulator.MuMuPlayerX: # vms/nemu-12.0-x64-default for folder in self.list_folder('../vms', is_dir=True): for file in iter_folder(folder, ext='.nemu'): @@ -268,7 +270,18 @@ class Emulator(EmulatorBase): name=os.path.basename(folder), path=self.path, ) - elif self == Emulator.MemuPlayer: + elif self == Emulator.MuMuPlayer12: + # vms/MuMuPlayer-12.0-0 + for folder in self.list_folder('../vms', is_dir=True): + for file in iter_folder(folder, ext='.nemu'): + serial = Emulator.vbox_file_to_serial(file) + if serial: + yield EmulatorInstance( + serial=serial, + name=os.path.basename(folder), + path=self.path, + ) + elif self == Emulator.MEmuPlayer: # ./MemuHyperv VMs/{name}/{name}.memu for folder in self.list_folder('./MemuHyperv VMs', is_dir=True): for file in iter_folder(folder, ext='.memu'): @@ -280,6 +293,27 @@ class Emulator(EmulatorBase): path=self.path, ) + def iter_adb_binaries(self) -> t.Iterable[str]: + """ + Yields: + str: Filepath to adb binaries found in this emulator + """ + if self == Emulator.NoxPlayerFamily: + exe = self.abspath('./nox_adb.exe') + if os.path.exists(exe): + yield exe + if self == Emulator.MuMuPlayerFamily: + # From MuMu9\emulator\nemu9\EmulatorShell + # to MuMu9\emulator\nemu9\vmonitor\bin\adb_server.exe + exe = self.abspath('../vmonitor/bin/adb_server.exe') + if os.path.exists(exe): + yield exe + + # All emulators have adb.exe + exe = self.abspath('./adb.exe') + if os.path.exists(exe): + yield exe + class EmulatorManager(EmulatorManagerBase): @staticmethod @@ -288,23 +322,32 @@ 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' # {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}\xxx.exe regex_hash = re.compile(r'{.*}') - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as reg: - folders = list_key(reg) + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as reg: + folders = list_key(reg) + except FileNotFoundError: + return + for folder in folders: - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, f'{path}\\{folder}\\Count') as reg: - for key in list_reg(reg): - key = codecs.decode(key.name, 'rot-13') - # Skip those with hash - if regex_hash.search(key): - continue - for file in Emulator.multi_to_single(key): - yield file + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, f'{path}\\{folder}\\Count') as reg: + for key in list_reg(reg): + key = codecs.decode(key.name, 'rot-13') + # Skip those with hash + if regex_hash.search(key): + continue + for file in Emulator.multi_to_single(key): + yield file + except FileNotFoundError: + # FileNotFoundError: [WinError 2] 系统找不到指定的文件。 + # Might be a random directory without "Count" subdirectory + continue @staticmethod def iter_mui_cache(): @@ -317,8 +360,11 @@ class EmulatorManager(EmulatorManagerBase): str: Path to emulator executable, may contains duplicate values """ path = r'Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache' - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as reg: - rows = list_reg(reg) + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as reg: + rows = list_reg(reg) + except FileNotFoundError: + return regex = re.compile(r'(^.*\.exe)\.') for row in rows: @@ -380,36 +426,57 @@ class EmulatorManager(EmulatorManagerBase): 'leidian9', 'Nemu', 'Nemu9', + 'MuMuPlayer-12.0' 'MEmu', ] for path in known_uninstall_registry_path: - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) as reg: - for software in list_key(reg): - if software not in known_emulator_registry_name: - continue + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) as reg: + software_list = list_key(reg) + except FileNotFoundError: + continue + for software in software_list: + if software not in known_emulator_registry_name: + continue + try: with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, f'{path}\\{software}') as software_reg: - try: - uninstall = winreg.QueryValueEx(software_reg, 'UninstallString')[0] - except FileNotFoundError: - continue - if not uninstall: - continue - # UninstallString is like: - # C:\Program Files\BlueStacks_nxt\BlueStacksUninstaller.exe -tmp - # "E:\ProgramFiles\Microvirt\MEmu\uninstall\uninstall.exe" -u - # Extract path in "" - res = re.search('"(.*?)"', uninstall) - uninstall = res.group(1) if res else uninstall - yield uninstall + uninstall = winreg.QueryValueEx(software_reg, 'UninstallString')[0] + except FileNotFoundError: + continue + if not uninstall: + continue + # UninstallString is like: + # C:\Program Files\BlueStacks_nxt\BlueStacksUninstaller.exe -tmp + # "E:\ProgramFiles\Microvirt\MEmu\uninstall\uninstall.exe" -u + # Extract path in "" + res = re.search('"(.*?)"', uninstall) + uninstall = res.group(1) if res else uninstall + yield uninstall @staticmethod - def iter_running_emulator() -> t.Iterable[psutil.Process]: + def iter_running_emulator(): """ - This may cost some time. + Yields: + str: Path to emulator executables, may contains duplicate values """ - for proc in psutil.process_iter(): - if Emulator.is_emulator(str(proc.name())): - yield proc + 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]: @@ -438,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): @@ -448,15 +515,18 @@ class EmulatorManager(EmulatorManagerBase): if Emulator.is_emulator(file) and os.path.exists(file): exe.add(file) # MuMu specific directory - folder = abspath(os.path.join(os.path.dirname(uninstall), 'EmulatorShell')) - if os.path.exists(folder): - for file in iter_folder(folder, ext='.exe'): - if Emulator.is_emulator(file) and os.path.exists(file): - exe.add(file) + for file in iter_folder(abspath(os.path.join(os.path.dirname(uninstall), 'EmulatorShell')), ext='.exe'): + 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)) - exe = [Emulator(path) for path in exe] + 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 e38fe2729..d4c2dae04 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -1,23 +1,24 @@ +import sys import typing as t -import yaml -from pydantic import BaseModel, SecretStr +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 -from module.base.decorator import cached_property, del_cached_property -class EmulatorData(BaseModel): +class EmulatorInfo(BaseModel): emulator: str = '' name: str = '' path: str = '' # For APIs of chinac.com, a phone cloud platform. - access_key: SecretStr = '' - secret: SecretStr = '' + # access_key: SecretStr = '' + # secret: SecretStr = '' class PlatformBase(Connection, EmulatorManagerBase): @@ -36,23 +37,37 @@ class PlatformBase(Connection, EmulatorManagerBase): - Retry is required. - Using bored sleep to wait startup is forbidden. """ - pass + logger.info(f'Current platform {sys.platform} does not support emulator_start, skip') def emulator_stop(self): """ Stop a emulator. """ - pass + logger.info(f'Current platform {sys.platform} does not support emulator_stop, skip') @cached_property - def emulator_data(self) -> EmulatorData: - try: - data = yaml.safe_load(self.config.RestartEmulator_EmulatorData) - return EmulatorData(**data) - except Exception as e: - logger.error(e) - logger.error("Failed to load EmulatorData, no emulator_instance") - return EmulatorData() + def emulator_info(self) -> EmulatorInfo: + emulator = self.config.EmulatorInfo_Emulator + if emulator == 'auto': + emulator = '' + + def parse_info(value): + if isinstance(value, str): + value = value.strip().replace('\n', '') + if value in ['None', 'False', 'True']: + value = '' + return value + else: + return '' + + name = parse_info(self.config.EmulatorInfo_name) + path = parse_info(self.config.EmulatorInfo_path) + + return EmulatorInfo( + emulator=emulator, + name=name, + path=path, + ) @cached_property def emulator_instance(self) -> t.Optional[EmulatorInstanceBase]: @@ -60,28 +75,38 @@ class PlatformBase(Connection, EmulatorManagerBase): Returns: EmulatorInstanceBase: Emulator instance or None """ - data = self.emulator_data + data = self.emulator_info old_info = dict( emulator=data.emulator, 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, ) # Write complete emulator data - new_info = dict( - emulator=instance.type, - path=instance.path, - name=instance.name, - ) - if new_info != old_info: - self.config.RestartEmulator_EmulatorData = yaml.safe_dump(new_info).strip() - del_cached_property(self, 'emulator_data') + if instance is not None: + new_info = dict( + emulator=instance.type, + path=instance.path, + name=instance.name, + ) + if new_info != old_info: + with self.config.multi_set(): + self.config.EmulatorInfo_Emulator = instance.type + self.config.EmulatorInfo_name = instance.name + self.config.EmulatorInfo_path = instance.path + del_cached_property(self, 'emulator_info') return instance @@ -102,7 +127,7 @@ class PlatformBase(Connection, EmulatorManagerBase): Returns: EmulatorInstance: Emulator instance or None if no instances not found. """ - logger.hr('Find emulator instance') + logger.hr('Find emulator instance', level=2) instances = SelectedGrids(self.all_emulator_instances) for instance in instances: logger.info(instance) @@ -111,10 +136,11 @@ 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] + logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') return instance @@ -123,10 +149,11 @@ 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}') return instance @@ -135,10 +162,11 @@ 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}') return instance @@ -147,19 +175,33 @@ 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}') return instance # Still too many instances logger.warning(f'Found multiple emulator instances with {search_args}') return None - - -if __name__ == '__main__': - self = PlatformBase('alas') - d = self.emulator_instance - print(d) diff --git a/module/device/platform/windows.py b/module/device/platform/platform_windows.py similarity index 63% rename from module/device/platform/windows.py rename to module/device/platform/platform_windows.py index 9ae83b2c1..de00efeca 100644 --- a/module/device/platform/windows.py +++ b/module/device/platform/platform_windows.py @@ -1,14 +1,15 @@ import ctypes +import re import subprocess -import typing as t import psutil +from deploy.Windows.utils import DataProcessInfo from module.base.decorator import run_once from module.base.timer import Timer from module.device.connection import AdbDeviceWithStatus from module.device.platform.platform_base import PlatformBase -from module.device.platform.windows_emulator import Emulator, EmulatorInstance, EmulatorManager +from module.device.platform.emulator_windows import Emulator, EmulatorInstance, EmulatorManager from module.logger import logger @@ -30,11 +31,11 @@ def minimize_window(hwnd): def get_window_title(hwnd): """Returns the window title as a string.""" - textLenInCharacters = ctypes.windll.user32.GetWindowTextLengthW(hwnd) - stringBuffer = ctypes.create_unicode_buffer( - textLenInCharacters + 1) # +1 for the \0 at the end of the null-terminated string. - ctypes.windll.user32.GetWindowTextW(hwnd, stringBuffer, textLenInCharacters + 1) - return stringBuffer.value + text_len_in_characters = ctypes.windll.user32.GetWindowTextLengthW(hwnd) + string_buffer = ctypes.create_unicode_buffer( + text_len_in_characters + 1) # +1 for the \0 at the end of the null-terminated string. + ctypes.windll.user32.GetWindowTextW(hwnd, string_buffer, text_len_in_characters + 1) + return string_buffer.value def flash_window(hwnd, flash=True): @@ -42,8 +43,8 @@ def flash_window(hwnd, flash=True): class PlatformWindows(PlatformBase, EmulatorManager): - @staticmethod - def execute(command): + @classmethod + def execute(cls, command): """ Args: command (str): @@ -55,67 +56,53 @@ class PlatformWindows(PlatformBase, EmulatorManager): logger.info(f'Execute: {command}') return subprocess.Popen(command, close_fds=True) # only work on Windows - @staticmethod - def taskkill(process): + @classmethod + def kill_process_by_regex(cls, regex: str) -> int: """ - Args: - process (str, list[str]): Process name or a list of them - - Returns: - subprocess.Popen: - """ - if not isinstance(process, list): - process = [process] - return self.execute(f'taskkill /t /f /im ' + ''.join(process)) - - @staticmethod - def find_running_emulator(instance: EmulatorInstance) -> t.Optional[psutil.Process]: - for proc in EmulatorManager.iter_running_emulator(): - cmdline = [arg.replace('\\', '/').replace(r'\\', '/') for arg in proc.cmdline()] - cmdline = ' '.join(cmdline) - if instance.path in cmdline and instance.name in cmdline: - return proc - - logger.warning(f'Cannot find a running emulator process with path={instance.path}, name={instance.name}') - return None - - def emulator_kill_by_process(self, instance: EmulatorInstance) -> bool: - """ - Kill a emulator by finding its process. + Kill processes with cmdline match the given regex. Args: - instance: + regex: Returns: - bool: If success + int: Number of processes killed """ - proc = self.find_running_emulator(instance) - if proc is not None: - proc.kill() - return True - else: - return False + count = 0 + + for proc in psutil.process_iter(): + cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + if re.search(regex, cmdline): + logger.info(f'Kill emulator: {cmdline}') + proc.kill() + count += 1 + + return count def _emulator_start(self, instance: EmulatorInstance): """ Start a emulator without error handling """ exe = instance.emulator.path - if instance == Emulator.MumuPlayer: + if instance == Emulator.MuMuPlayer: # NemuPlayer.exe self.execute(exe) - if instance == Emulator.MumuPlayer9: + elif instance == Emulator.MuMuPlayerX: # NemuPlayer.exe -m nemu-12.0-x64-default - self.execute(f'{exe} -m {instance.name}') + self.execute(f'"{exe}" -m {instance.name}') + elif instance == Emulator.MuMuPlayer12: + # MuMuPlayer.exe -v 0 + if instance.MuMuPlayer12_id is None: + logger.warning(f'Cannot get MuMu instance index from name {instance.name}') + self.execute(f'"{exe}" -v {instance.MuMuPlayer12_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 - self.execute(f'{exe} -clone:{instance.name}') + self.execute(f'"{exe}" -clone:{instance.name}') elif instance == Emulator.BlueStacks5: # HD-Player.exe -instance Pie64 - self.execute(f'{exe} -instance {instance.name}') + self.execute(f'"{exe}" -instance {instance.name}') elif instance == Emulator.BlueStacks4: # BlueStacks\Client\Bluestacks.exe -vmname Android_1 - self.execute(f'{exe} -vmname {instance.name}') + self.execute(f'"{exe}" -vmname {instance.name}') else: raise EmulatorUnknown(f'Cannot start an unknown emulator instance: {instance}') @@ -123,16 +110,53 @@ class PlatformWindows(PlatformBase, EmulatorManager): """ Stop a emulator without error handling """ + logger.hr('Emulator stop', level=2) exe = instance.emulator.path - if instance == Emulator.MumuPlayer: - # taskkill /t /f /im NemuHeadless.exe NemuPlayer.exe NemuSvc.exe - self.taskkill(['NemuHeadless.exe', 'NemuPlayer.exe', 'NemuSvc.exe']) - elif instance == Emulator.MumuPlayer9: - # Kill by process - self.emulator_kill_by_process(instance) + if instance == Emulator.MuMuPlayer: + # MuMu6 does not have multi instance, kill one means kill all + # Has 4 processes + # "C:\Program Files\NemuVbox\Hypervisor\NemuHeadless.exe" --comment nemu-6.0-x64-default --startvm + # "E:\ProgramFiles\MuMu\emulator\nemu\EmulatorShell\NemuPlayer.exe" + # E:\ProgramFiles\MuMu\emulator\nemu\EmulatorShell\NemuService.exe + # "C:\Program Files\NemuVbox\Hypervisor\NemuSVC.exe" -Embedding + self.kill_process_by_regex( + rf'(' + rf'NemuHeadless.exe' + rf'|NemuPlayer.exe\"' + rf'|NemuPlayer.exe$' + rf'|NemuService.exe' + rf'|NemuSVC.exe' + rf')' + ) + elif instance == Emulator.MuMuPlayerX: + # MuMu X has 3 processes + # "E:\ProgramFiles\MuMu9\emulator\nemu9\EmulatorShell\NemuPlayer.exe" -m nemu-12.0-x64-default -s 0 -l + # "C:\Program Files\Muvm6Vbox\Hypervisor\Muvm6Headless.exe" --comment nemu-12.0-x64-default --startvm xxx + # "C:\Program Files\Muvm6Vbox\Hypervisor\Muvm6SVC.exe" --Embedding + self.kill_process_by_regex( + rf'(' + rf'NemuPlayer.exe.*-m {instance.name}' + rf'|Muvm6Headless.exe' + rf'|Muvm6SVC.exe' + rf')' + ) + elif instance == Emulator.MuMuPlayer12: + # MuMu 12 has 2 processes: + # E:\ProgramFiles\Netease\MuMuPlayer-12.0\shell\MuMuPlayer.exe -v 0 + # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMHeadless.exe" --comment MuMuPlayer-12.0-0 --startvm xxx + if instance.MuMuPlayer12_id is None: + logger.warning(f'Cannot get MuMu instance index from name {instance.name}') + self.kill_process_by_regex( + rf'(' + rf'MuMuVMMHeadless.exe.*--comment {instance.name}' + rf'|MuMuPlayer.exe.*-v {instance.MuMuPlayer12_id}' + rf')' + ) + # There is also a shared service, no need to kill it + # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMSVC.exe" --Embedding elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit - self.execute(f'{exe} -clone:{instance.name} -quit') + self.execute(f'"{exe}" -clone:{instance.name} -quit') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') @@ -152,8 +176,10 @@ class PlatformWindows(PlatformBase, EmulatorManager): # OSError: [WinError 740] 请求的操作需要提升。 if 'WinError 740' in msg: logger.error('To start/stop MumuAppPlayer, ALAS needs to be run as administrator') - except Exception as e: + except EmulatorUnknown as e: logger.error(e) + except Exception as e: + logger.exception(e) logger.error(f'Emulator function {func.__name__}() failed') return False @@ -164,6 +190,7 @@ class PlatformWindows(PlatformBase, EmulatorManager): bool: True if startup completed False if timeout """ + logger.hr('Emulator start', level=2) current_window = get_focused_window() serial = self.emulator_instance.serial logger.info(f'Current window: {current_window}') @@ -263,6 +290,7 @@ class PlatformWindows(PlatformBase, EmulatorManager): return True def emulator_start(self): + logger.hr('Emulator start', level=1) for _ in range(3): # Stop if not self._emulator_function_wrapper(self._emulator_stop): @@ -283,9 +311,11 @@ class PlatformWindows(PlatformBase, EmulatorManager): return False def emulator_stop(self): + logger.hr('Emulator stop', level=1) return self._emulator_function_wrapper(self._emulator_stop) if __name__ == '__main__': self = PlatformWindows('alas') - self.emulator_start() + d = self.emulator_instance + print(d) diff --git a/module/device/platform/utils.py b/module/device/platform/utils.py new file mode 100644 index 000000000..4ebf94af1 --- /dev/null +++ b/module/device/platform/utils.py @@ -0,0 +1,54 @@ +import os +from typing import Callable, Generic, TypeVar + +T = TypeVar("T") + + +class cached_property(Generic[T]): + """ + cached-property from https://github.com/pydanny/cached-property + Add typing support + + A property that is only computed once per instance and then replaces itself + with an ordinary attribute. Deleting the attribute resets the property. + Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76 + """ + + def __init__(self, func: Callable[..., T]): + self.func = func + + def __get__(self, obj, cls) -> T: + if obj is None: + return self + + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +def iter_folder(folder, is_dir=False, ext=None): + """ + Args: + folder (str): + is_dir (bool): True to iter directories only + ext (str): File extension, such as `.yaml` + + Yields: + str: Absolute path of files + """ + try: + files = os.listdir(folder) + except FileNotFoundError: + return + + for file in files: + sub = os.path.join(folder, file) + if is_dir: + if os.path.isdir(sub): + yield sub.replace('\\\\', '/').replace('\\', '/') + elif ext is not None: + if not os.path.isdir(sub): + _, extension = os.path.splitext(file) + if extension == ext: + yield os.path.join(folder, file).replace('\\\\', '/').replace('\\', '/') + else: + yield os.path.join(folder, file).replace('\\\\', '/').replace('\\', '/') diff --git a/module/device/screenshot.py b/module/device/screenshot.py index c651be54a..fb02063de 100644 --- a/module/device/screenshot.py +++ b/module/device/screenshot.py @@ -13,13 +13,14 @@ from module.base.utils import get_color, image_size, limit_in, save_image from module.device.method.adb import Adb from module.device.method.ascreencap import AScreenCap from module.device.method.droidcast import DroidCast +from module.device.method.nemu_ipc import NemuIpc from module.device.method.scrcpy import Scrcpy from module.device.method.wsa import WSA from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger -class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): +class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy, NemuIpc): _screen_size_checked = False _screen_black_checked = False _minicap_uninstalled = False @@ -38,6 +39,7 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): 'DroidCast': self.screenshot_droidcast, 'DroidCast_raw': self.screenshot_droidcast_raw, 'scrcpy': self.screenshot_scrcpy, + 'nemu_ipc': self.screenshot_nemu_ipc, } def screenshot(self): @@ -70,6 +72,10 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): 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: @@ -155,6 +161,9 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): if interval != origin: logger.warning(f'Optimization.ScreenshotInterval {origin} is revised to {interval}') self.config.Optimization_ScreenshotInterval = interval + # Allow nemu_ipc to have a lower default + if self.config.Emulator_ScreenshotMethod == 'nemu_ipc': + interval = limit_in(origin, 0.1, 0.2) elif interval == 'combat': origin = self.config.Optimization_CombatScreenshotInterval interval = limit_in(origin, 0.3, 5.0) diff --git a/module/dorm/assets.py b/module/dorm/assets.py index a0b2f5d57..4464ace41 100644 --- a/module/dorm/assets.py +++ b/module/dorm/assets.py @@ -5,7 +5,7 @@ from module.base.template import Template # Don't modify it manually. DORM_FEED_CHECK = Button(area={'cn': (162, 342, 274, 370), 'en': (162, 342, 274, 370), 'jp': (162, 342, 274, 370), 'tw': (162, 342, 274, 370)}, color={'cn': (182, 179, 171), 'en': (182, 179, 171), 'jp': (182, 179, 171), 'tw': (182, 179, 171)}, button={'cn': (162, 342, 274, 370), 'en': (162, 342, 274, 370), 'jp': (162, 342, 274, 370), 'tw': (162, 342, 274, 370)}, file={'cn': './assets/cn/dorm/DORM_FEED_CHECK.png', 'en': './assets/en/dorm/DORM_FEED_CHECK.png', 'jp': './assets/jp/dorm/DORM_FEED_CHECK.png', 'tw': './assets/tw/dorm/DORM_FEED_CHECK.png'}) -DORM_FEED_ENTER = Button(area={'cn': (254, 581, 300, 605), 'en': (254, 581, 300, 605), 'jp': (254, 581, 300, 605), 'tw': (254, 581, 300, 605)}, color={'cn': (204, 192, 177), 'en': (204, 192, 177), 'jp': (204, 192, 177), 'tw': (204, 192, 177)}, button={'cn': (254, 581, 300, 605), 'en': (254, 581, 300, 605), 'jp': (254, 581, 300, 605), 'tw': (254, 581, 300, 605)}, file={'cn': './assets/cn/dorm/DORM_FEED_ENTER.png', 'en': './assets/en/dorm/DORM_FEED_ENTER.png', 'jp': './assets/jp/dorm/DORM_FEED_ENTER.png', 'tw': './assets/tw/dorm/DORM_FEED_ENTER.png'}) +DORM_FEED_ENTER = Button(area={'cn': (254, 581, 300, 605), 'en': (298, 581, 344, 605), 'jp': (254, 581, 300, 605), 'tw': (254, 581, 300, 605)}, color={'cn': (204, 192, 177), 'en': (204, 192, 176), 'jp': (204, 192, 177), 'tw': (204, 192, 177)}, button={'cn': (254, 581, 300, 605), 'en': (298, 581, 344, 605), 'jp': (254, 581, 300, 605), 'tw': (254, 581, 300, 605)}, file={'cn': './assets/cn/dorm/DORM_FEED_ENTER.png', 'en': './assets/en/dorm/DORM_FEED_ENTER.png', 'jp': './assets/jp/dorm/DORM_FEED_ENTER.png', 'tw': './assets/tw/dorm/DORM_FEED_ENTER.png'}) DORM_FURNITURE_BUY_ALL = Button(area={'cn': (818, 621, 1072, 677), 'en': (819, 621, 1072, 677), 'jp': (820, 621, 1072, 677), 'tw': (818, 621, 1072, 677)}, color={'cn': (249, 202, 66), 'en': (248, 201, 66), 'jp': (247, 201, 66), 'tw': (248, 201, 66)}, button={'cn': (818, 621, 1072, 677), 'en': (819, 621, 1072, 677), 'jp': (820, 621, 1072, 677), 'tw': (818, 621, 1072, 677)}, file={'cn': './assets/cn/dorm/DORM_FURNITURE_BUY_ALL.png', 'en': './assets/en/dorm/DORM_FURNITURE_BUY_ALL.png', 'jp': './assets/jp/dorm/DORM_FURNITURE_BUY_ALL.png', 'tw': './assets/tw/dorm/DORM_FURNITURE_BUY_ALL.png'}) DORM_FURNITURE_BUY_CONFIRM = Button(area={'cn': (644, 464, 972, 517), 'en': (645, 464, 971, 518), 'jp': (645, 465, 973, 517), 'tw': (644, 464, 972, 518)}, color={'cn': (251, 204, 66), 'en': (250, 203, 66), 'jp': (250, 203, 66), 'tw': (250, 203, 66)}, button={'cn': (644, 464, 972, 517), 'en': (645, 464, 971, 518), 'jp': (645, 465, 973, 517), 'tw': (644, 464, 972, 518)}, file={'cn': './assets/cn/dorm/DORM_FURNITURE_BUY_CONFIRM.png', 'en': './assets/en/dorm/DORM_FURNITURE_BUY_CONFIRM.png', 'jp': './assets/jp/dorm/DORM_FURNITURE_BUY_CONFIRM.png', 'tw': './assets/tw/dorm/DORM_FURNITURE_BUY_CONFIRM.png'}) DORM_FURNITURE_BUY_SET = Button(area={'cn': (505, 621, 759, 677), 'en': (505, 621, 759, 677), 'jp': (505, 621, 760, 677), 'tw': (505, 621, 759, 677)}, color={'cn': (248, 202, 66), 'en': (247, 201, 66), 'jp': (245, 199, 65), 'tw': (247, 201, 66)}, button={'cn': (505, 621, 759, 677), 'en': (505, 621, 759, 677), 'jp': (505, 621, 760, 677), 'tw': (505, 621, 759, 677)}, file={'cn': './assets/cn/dorm/DORM_FURNITURE_BUY_SET.png', 'en': './assets/en/dorm/DORM_FURNITURE_BUY_SET.png', 'jp': './assets/jp/dorm/DORM_FURNITURE_BUY_SET.png', 'tw': './assets/tw/dorm/DORM_FURNITURE_BUY_SET.png'}) @@ -17,6 +17,7 @@ DORM_FURNITURE_SHOP_ENTER = Button(area={'cn': (1067, 604, 1120, 650), 'en': (10 DORM_FURNITURE_SHOP_FIRST = Button(area={'cn': (241, 565, 271, 572), 'en': (241, 565, 271, 572), 'jp': (241, 565, 271, 572), 'tw': (241, 565, 271, 572)}, color={'cn': (247, 213, 129), 'en': (247, 213, 129), 'jp': (247, 213, 129), 'tw': (247, 213, 129)}, button={'cn': (241, 565, 271, 572), 'en': (241, 565, 271, 572), 'jp': (241, 565, 271, 572), 'tw': (241, 565, 271, 572)}, file={'cn': './assets/cn/dorm/DORM_FURNITURE_SHOP_FIRST.png', 'en': './assets/en/dorm/DORM_FURNITURE_SHOP_FIRST.png', 'jp': './assets/jp/dorm/DORM_FURNITURE_SHOP_FIRST.png', 'tw': './assets/tw/dorm/DORM_FURNITURE_SHOP_FIRST.png'}) DORM_FURNITURE_SHOP_FIRST_SELECTED = Button(area={'cn': (239, 519, 273, 525), 'en': (239, 519, 273, 525), 'jp': (239, 519, 273, 525), 'tw': (239, 519, 273, 525)}, color={'cn': (242, 205, 114), 'en': (242, 205, 114), 'jp': (242, 205, 114), 'tw': (242, 205, 114)}, button={'cn': (239, 519, 273, 525), 'en': (239, 519, 273, 525), 'jp': (239, 519, 273, 525), 'tw': (239, 519, 273, 525)}, file={'cn': './assets/cn/dorm/DORM_FURNITURE_SHOP_FIRST_SELECTED.png', 'en': './assets/en/dorm/DORM_FURNITURE_SHOP_FIRST_SELECTED.png', 'jp': './assets/jp/dorm/DORM_FURNITURE_SHOP_FIRST_SELECTED.png', 'tw': './assets/tw/dorm/DORM_FURNITURE_SHOP_FIRST_SELECTED.png'}) DORM_FURNITURE_SHOP_QUIT = Button(area={'cn': (38, 34, 58, 63), 'en': (38, 34, 58, 63), 'jp': (38, 34, 58, 63), 'tw': (38, 34, 58, 63)}, color={'cn': (255, 241, 195), 'en': (255, 241, 195), 'jp': (255, 241, 195), 'tw': (255, 241, 195)}, button={'cn': (38, 34, 58, 63), 'en': (38, 34, 58, 63), 'jp': (38, 34, 58, 63), 'tw': (38, 34, 58, 63)}, file={'cn': './assets/cn/dorm/DORM_FURNITURE_SHOP_QUIT.png', 'en': './assets/en/dorm/DORM_FURNITURE_SHOP_QUIT.png', 'jp': './assets/jp/dorm/DORM_FURNITURE_SHOP_QUIT.png', 'tw': './assets/tw/dorm/DORM_FURNITURE_SHOP_QUIT.png'}) +DORM_MANAGE = Button(area={'cn': (949, 600, 1005, 659), 'en': (949, 600, 1005, 659), 'jp': (949, 600, 1005, 659), 'tw': (949, 600, 1005, 659)}, color={'cn': (255, 245, 213), 'en': (255, 245, 213), 'jp': (255, 245, 213), 'tw': (255, 245, 213)}, button={'cn': (949, 600, 1005, 659), 'en': (949, 600, 1005, 659), 'jp': (949, 600, 1005, 659), 'tw': (949, 600, 1005, 659)}, file={'cn': './assets/cn/dorm/DORM_MANAGE.png', 'en': './assets/en/dorm/DORM_MANAGE.png', 'jp': './assets/jp/dorm/DORM_MANAGE.png', 'tw': './assets/tw/dorm/DORM_MANAGE.png'}) DORM_MANAGE_CHECK = Button(area={'cn': (1128, 116, 1150, 135), 'en': (1128, 116, 1150, 135), 'jp': (1128, 116, 1150, 135), 'tw': (1128, 116, 1150, 135)}, color={'cn': (173, 147, 77), 'en': (173, 147, 77), 'jp': (173, 147, 77), 'tw': (173, 147, 77)}, button={'cn': (1128, 116, 1150, 135), 'en': (1128, 116, 1150, 135), 'jp': (1128, 116, 1150, 135), 'tw': (1128, 116, 1150, 135)}, file={'cn': './assets/cn/dorm/DORM_MANAGE_CHECK.png', 'en': './assets/en/dorm/DORM_MANAGE_CHECK.png', 'jp': './assets/jp/dorm/DORM_MANAGE_CHECK.png', 'tw': './assets/tw/dorm/DORM_MANAGE_CHECK.png'}) DORM_RED_DOT = Button(area={'cn': (528, 339, 543, 356), 'en': (528, 339, 543, 356), 'jp': (528, 339, 543, 356), 'tw': (734, 215, 767, 248)}, color={'cn': (214, 126, 114), 'en': (214, 126, 114), 'jp': (214, 126, 114), 'tw': (130, 89, 94)}, button={'cn': (528, 339, 543, 356), 'en': (528, 339, 543, 356), 'jp': (528, 339, 543, 356), 'tw': (734, 215, 767, 248)}, file={'cn': './assets/cn/dorm/DORM_RED_DOT.png', 'en': './assets/en/dorm/DORM_RED_DOT.png', 'jp': './assets/jp/dorm/DORM_RED_DOT.png', 'tw': './assets/tw/dorm/DORM_RED_DOT.png'}) OCR_DORM_FILL = Button(area={'cn': (813, 271, 987, 296), 'en': (813, 271, 987, 296), 'jp': (813, 271, 987, 296), 'tw': (813, 271, 987, 296)}, color={'cn': (222, 213, 193), 'en': (222, 213, 193), 'jp': (222, 213, 193), 'tw': (222, 213, 193)}, button={'cn': (813, 271, 987, 296), 'en': (813, 271, 987, 296), 'jp': (813, 271, 987, 296), 'tw': (813, 271, 987, 296)}, file={'cn': './assets/cn/dorm/OCR_DORM_FILL.png', 'en': './assets/en/dorm/OCR_DORM_FILL.png', 'jp': './assets/jp/dorm/OCR_DORM_FILL.png', 'tw': './assets/tw/dorm/OCR_DORM_FILL.png'}) diff --git a/module/dorm/buy_furniture.py b/module/dorm/buy_furniture.py index d0b51f49a..4d2061d38 100644 --- a/module/dorm/buy_furniture.py +++ b/module/dorm/buy_furniture.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta from module.combat.assets import GET_SHIP from module.dorm.assets import * +from module.exercise.assets import EXERCISE_PREPARATION from module.logger import logger from module.ocr.ocr import Digit from module.ui.assets import DORM_CHECK @@ -10,7 +11,7 @@ from module.ui.ui import UI OCR_FURNITURE_COIN = Digit(OCR_DORM_FURNITURE_COIN, letter=(107, 89, 82), threshold=128, alphabet='0123456789', name='OCR_FURNITURE_COIN') OCR_FURNITURE_PRICE = Digit(OCR_DORM_FURNITURE_PRICE, letter=(255, 247, 247), threshold=64, alphabet='0123456789', name='OCR_FURNITURE_PRICE') -CHECK_INTERVAL = 7 # Check every 7 days +CHECK_INTERVAL = 6 # Check every 6 days # Only for click FURNITURE_BUY_BUTTON = { "all": DORM_FURNITURE_BUY_ALL, @@ -33,10 +34,6 @@ class BuyFurniture(UI): else: self.device.screenshot() - if self.ui_additional(): - self.interval_clear(DORM_CHECK) - continue - # Enter furniture shop page from page_dorm, only need to enter once if self.appear(DORM_CHECK, offset=(20, 20), interval=3): self.device.click(DORM_FURNITURE_SHOP_ENTER) @@ -44,6 +41,7 @@ class BuyFurniture(UI): continue if self.appear(DORM_FURNITURE_SHOP_FIRST_SELECTED, offset=(20, 20)): + self.interval_reset(EXERCISE_PREPARATION) # Enter furniture details page from furniture shop page if self.appear(DORM_FURNITURE_DETAILS_ENTER, offset=(20, 20), interval=3): self.device.click(DORM_FURNITURE_DETAILS_ENTER) @@ -57,6 +55,10 @@ class BuyFurniture(UI): if self.appear(DORM_FURNITURE_DETAILS_QUIT, offset=(20, 20)): break + if self.ui_additional(): + self.interval_clear(DORM_CHECK) + continue + def furniture_shop_quit(self, skip_first_screenshot=False): """ Pages: diff --git a/module/dorm/dorm.py b/module/dorm/dorm.py index 873504dc0..5c76eded7 100644 --- a/module/dorm/dorm.py +++ b/module/dorm/dorm.py @@ -9,6 +9,7 @@ from module.base.timer import Timer from module.base.utils import * from module.dorm.assets import * from module.dorm.buy_furniture import BuyFurniture +from module.handler.assets import POPUP_CONFIRM from module.logger import logger from module.ocr.ocr import Digit, DigitCounter from module.template.assets import TEMPLATE_DORM_COIN, TEMPLATE_DORM_LOVE @@ -78,8 +79,8 @@ class RewardDorm(UI): out: page_dorm, with info_bar """ image = MASK_DORM.apply(self.device.image) - loves = TEMPLATE_DORM_LOVE.match_multi(image, name='DORM_LOVE', scaling=1.5) - coins = TEMPLATE_DORM_COIN.match_multi(image, name='DORM_COIN', scaling=1.5) + loves = TEMPLATE_DORM_LOVE.match_multi(image, name='DORM_LOVE') + coins = TEMPLATE_DORM_COIN.match_multi(image, name='DORM_COIN') logger.info(f'Dorm loves: {len(loves)}, Dorm coins: {len(coins)}') # Complicated dorm background if len(loves) > 6: @@ -107,24 +108,49 @@ 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) \ or self.handle_info_bar() \ - or self.handle_popup_cancel('DORM_FEED'): + or self.appear(POPUP_CONFIRM, offset=self._popup_offset): break if timeout.reached(): 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) + builder = self.device.maatouch_builder + builder.down(x, y).commit() + builder.send() + + while 1: + builder.move(x, y).commit().wait(10) + builder.send() + self.device.screenshot() + + if not self._dorm_has_food(button) \ + or self.handle_info_bar() \ + or self.appear(POPUP_CONFIRM, offset=self._popup_offset): + break + if timeout.reached(): + logger.warning('Wait dorm feed timeout') + break + + builder.up().commit() + builder.send() @Config.when(DEVICE_CONTROL_METHOD='uiautomator2') def _dorm_feed_long_tap(self, button, count): @@ -139,7 +165,7 @@ class RewardDorm(UI): if not self._dorm_has_food(button) \ or self.handle_info_bar() \ - or self.handle_popup_cancel('DORM_FEED'): + or self.appear(POPUP_CONFIRM, offset=self._popup_offset): break if timeout.reached(): logger.warning('Wait dorm feed timeout') @@ -147,12 +173,73 @@ class RewardDorm(UI): self.device.u2.touch.up(x, y) + @Config.when(DEVICE_CONTROL_METHOD='nemu_ipc') + def _dorm_feed_long_tap(self, button, count): + timeout = Timer(count // 5 + 5).start() + x, y = random_rectangle_point(button.button) + + while 1: + self.device.nemu_ipc.down(x, y) + time.sleep(.01) + self.device.screenshot() + + if not self._dorm_has_food(button) \ + or self.handle_info_bar() \ + or self.appear(POPUP_CONFIRM, offset=self._popup_offset): + break + if timeout.reached(): + logger.warning('Wait dorm feed timeout') + break + + self.device.nemu_ipc.up() + @Config.when(DEVICE_CONTROL_METHOD=None) def _dorm_feed_long_tap(self, button, count): logger.warning(f'Current control method {self.config.Emulator_ControlMethod} ' f'does not support DOWN/UP events, use multi-click instead') self.device.multi_click(button, count) + def dorm_view_reset(self, skip_first_screenshot=True): + """ + Use Dorm manage and Back to reset dorm view. + + Pages: + in: page_dorm + out: page_dorm + """ + logger.info('Dorm view reset') + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.appear(DORM_MANAGE_CHECK, offset=(20, 20)): + break + + if self.appear_then_click(DORM_MANAGE, offset=(20, 20), interval=3): + continue + # Handle all popups + if self.ui_additional(): + continue + if self.appear_then_click(DORM_FURNITURE_CONFIRM, offset=(30, 30), interval=3): + continue + + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.appear(DORM_MANAGE, offset=(20, 20)): + break + + if self.appear(DORM_MANAGE_CHECK, offset=(20, 20), interval=3): + self.device.click(DORM_FURNITURE_SHOP_QUIT) + continue + def dorm_collect(self): """ Click all coins and loves on current screen. @@ -164,26 +251,8 @@ class RewardDorm(UI): out: page_dorm, without info_bar """ logger.hr('Dorm collect') - # if self.config.Emulator_ControlMethod not in ['uiautomator2', 'minitouch']: - # logger.warning(f'Current control method {self.config.Emulator_ControlMethod} ' - # f'does not support 2 finger zoom out, skip dorm collect') - # return - # Already at a high camera view now, no need to zoom-out. - # for _ in range(2): - # logger.info('Dorm zoom out') - # # Left hand down - # x, y = random_rectangle_point((33, 228, 234, 469)) - # self.device.minitouch_builder.down(x, y, contact_id=1).commit() - # self.device.minitouch_send() - # # Right hand swipe - # # Need to avoid drop-down menu in android, which is 38 px. - # p1, p2 = random_rectangle_vector( - # (-700, 450), box=(247, 45, 1045, 594), random_range=(-50, -50, 50, 50), padding=0) - # self.device.drag_minitouch(p1, p2, point_random=(0, 0, 0, 0)) - # # Left hand up - # self.device.minitouch_builder.up(contact_id=1).commit() - # self.device.minitouch_send() + self.dorm_view_reset() # Collect _dorm_receive_attempt = 0 @@ -241,17 +310,24 @@ class RewardDorm(UI): for _ in range(count): self.device.click(button) self.device.sleep((0.5, 0.8)) + skip_first_screenshot = False else: self._dorm_feed_long_tap(button, count) + skip_first_screenshot = True + self.popup_interval_clear() while 1: - self.device.screenshot() - if self.handle_popup_cancel('DORM_FEED'): - continue + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() # End if self.appear(DORM_FEED_CHECK, offset=(20, 20)): break + # Click + if self.handle_popup_cancel('DORM_FEED'): + continue def dorm_food_get(self): """ @@ -408,6 +484,7 @@ class RewardDorm(UI): return self.ui_ensure(page_dormmenu) + self.handle_info_bar() if not self.appear(DORM_RED_DOT, offset=(30, 30)): logger.info('Nothing to collect. Dorm collecting skipped.') collect = False @@ -519,7 +596,7 @@ class RewardDorm(UI): self.config.Scheduler_Enable = False self.config.task_stop() - self.dorm_run(feed=self.config.Dorm_Feed, + self.dorm_run(feed=self.config.Dorm_Feed, collect=self.config.Dorm_Collect, buy_furniture=self.config.BuyFurniture_Enable) diff --git a/module/gacha/gacha_reward.py b/module/gacha/gacha_reward.py index 4a9ffda1f..a5aaaa5df 100644 --- a/module/gacha/gacha_reward.py +++ b/module/gacha/gacha_reward.py @@ -69,6 +69,10 @@ class RewardGacha(GachaUI, GeneralShop, Retirement): ocr_submit = OCR_BUILD_SUBMIT_WW_COUNT confirm_timer.reset() continue + # Continue gacha even if UR exchange point is full + if self.handle_popup_confirm('GACHA_PREP'): + confirm_timer.reset() + continue # End if self.appear(BUILD_PLUS, offset=index_offset) \ @@ -320,6 +324,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/handler/assets.py b/module/handler/assets.py index e2dc0cb29..fd6591181 100644 --- a/module/handler/assets.py +++ b/module/handler/assets.py @@ -26,9 +26,9 @@ FAST_FORWARD_OFF = Button(area={'cn': (982, 587, 1022, 605), 'en': (982, 587, 10 FAST_FORWARD_ON = Button(area={'cn': (1024, 587, 1064, 605), 'en': (1024, 587, 1064, 605), 'jp': (1024, 587, 1064, 605), 'tw': (1024, 587, 1064, 605)}, color={'cn': (251, 251, 251), 'en': (251, 251, 251), 'jp': (251, 251, 251), 'tw': (251, 251, 251)}, button={'cn': (1024, 587, 1064, 605), 'en': (1024, 587, 1064, 605), 'jp': (1024, 587, 1064, 605), 'tw': (1024, 587, 1064, 605)}, file={'cn': './assets/cn/handler/FAST_FORWARD_ON.png', 'en': './assets/en/handler/FAST_FORWARD_ON.png', 'jp': './assets/jp/handler/FAST_FORWARD_ON.png', 'tw': './assets/tw/handler/FAST_FORWARD_ON.png'}) FLEET_LOCKED = Button(area={'cn': (1185, 501, 1192, 519), 'en': (1184, 502, 1191, 519), 'jp': (1172, 496, 1180, 531), 'tw': (1185, 501, 1192, 519)}, color={'cn': (59, 100, 110), 'en': (61, 102, 111), 'jp': (56, 87, 103), 'tw': (59, 100, 110)}, button={'cn': (1185, 501, 1192, 519), 'en': (1184, 502, 1191, 519), 'jp': (1172, 496, 1180, 531), 'tw': (1185, 501, 1192, 519)}, file={'cn': './assets/cn/handler/FLEET_LOCKED.png', 'en': './assets/en/handler/FLEET_LOCKED.png', 'jp': './assets/jp/handler/FLEET_LOCKED.png', 'tw': './assets/tw/handler/FLEET_LOCKED.png'}) FLEET_UNLOCKED = Button(area={'cn': (1185, 492, 1192, 525), 'en': (1184, 496, 1191, 522), 'jp': (1171, 492, 1181, 531), 'tw': (1185, 492, 1192, 525)}, color={'cn': (84, 74, 98), 'en': (95, 81, 101), 'jp': (80, 75, 103), 'tw': (84, 74, 98)}, button={'cn': (1185, 492, 1192, 525), 'en': (1184, 496, 1191, 522), 'jp': (1171, 492, 1181, 531), 'tw': (1185, 492, 1192, 525)}, file={'cn': './assets/cn/handler/FLEET_UNLOCKED.png', 'en': './assets/en/handler/FLEET_UNLOCKED.png', 'jp': './assets/jp/handler/FLEET_UNLOCKED.png', 'tw': './assets/tw/handler/FLEET_UNLOCKED.png'}) -FORMATION_1 = Button(area={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, color={'cn': (80, 82, 93), 'en': (80, 82, 93), 'jp': (80, 82, 93), 'tw': (80, 82, 93)}, button={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, file={'cn': './assets/cn/handler/FORMATION_1.png', 'en': './assets/en/handler/FORMATION_1.png', 'jp': './assets/jp/handler/FORMATION_1.png', 'tw': './assets/tw/handler/FORMATION_1.png'}) -FORMATION_2 = Button(area={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, color={'cn': (80, 82, 92), 'en': (80, 82, 92), 'jp': (80, 82, 92), 'tw': (80, 82, 92)}, button={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, file={'cn': './assets/cn/handler/FORMATION_2.png', 'en': './assets/en/handler/FORMATION_2.png', 'jp': './assets/jp/handler/FORMATION_2.png', 'tw': './assets/tw/handler/FORMATION_2.png'}) -FORMATION_3 = Button(area={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, color={'cn': (79, 82, 92), 'en': (79, 82, 92), 'jp': (79, 82, 92), 'tw': (79, 82, 92)}, button={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, file={'cn': './assets/cn/handler/FORMATION_3.png', 'en': './assets/en/handler/FORMATION_3.png', 'jp': './assets/jp/handler/FORMATION_3.png', 'tw': './assets/tw/handler/FORMATION_3.png'}) +FORMATION_1 = Button(area={'cn': (1108, 446, 1170, 477), 'en': (1108, 446, 1170, 477), 'jp': (1108, 446, 1170, 477), 'tw': (1108, 446, 1170, 477)}, color={'cn': (79, 82, 92), 'en': (79, 82, 92), 'jp': (79, 82, 92), 'tw': (79, 82, 92)}, button={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, file={'cn': './assets/cn/handler/FORMATION_1.png', 'en': './assets/en/handler/FORMATION_1.png', 'jp': './assets/jp/handler/FORMATION_1.png', 'tw': './assets/tw/handler/FORMATION_1.png'}) +FORMATION_2 = Button(area={'cn': (1108, 446, 1170, 477), 'en': (1108, 446, 1170, 477), 'jp': (1108, 446, 1170, 477), 'tw': (1108, 446, 1170, 477)}, color={'cn': (81, 83, 92), 'en': (81, 83, 92), 'jp': (81, 83, 92), 'tw': (81, 83, 92)}, button={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, file={'cn': './assets/cn/handler/FORMATION_2.png', 'en': './assets/en/handler/FORMATION_2.png', 'jp': './assets/jp/handler/FORMATION_2.png', 'tw': './assets/tw/handler/FORMATION_2.png'}) +FORMATION_3 = Button(area={'cn': (1108, 446, 1170, 477), 'en': (1108, 446, 1170, 477), 'jp': (1108, 446, 1170, 477), 'tw': (1108, 446, 1170, 477)}, color={'cn': (79, 82, 91), 'en': (79, 82, 91), 'jp': (79, 82, 91), 'tw': (79, 82, 91)}, button={'cn': (1108, 415, 1170, 477), 'en': (1108, 415, 1170, 477), 'jp': (1108, 415, 1170, 477), 'tw': (1108, 415, 1170, 477)}, file={'cn': './assets/cn/handler/FORMATION_3.png', 'en': './assets/en/handler/FORMATION_3.png', 'jp': './assets/jp/handler/FORMATION_3.png', 'tw': './assets/tw/handler/FORMATION_3.png'}) FORMATION_LOCATION = Button(area={'cn': (145, 115, 437, 159), 'en': (145, 115, 437, 159), 'jp': (145, 115, 437, 159), 'tw': (145, 115, 437, 159)}, color={'cn': (103, 118, 118), 'en': (103, 118, 118), 'jp': (103, 118, 118), 'tw': (103, 118, 118)}, button={'cn': (145, 115, 437, 159), 'en': (145, 115, 437, 159), 'jp': (145, 115, 437, 159), 'tw': (145, 115, 437, 159)}, file={'cn': './assets/cn/handler/FORMATION_LOCATION.png', 'en': './assets/en/handler/FORMATION_LOCATION.png', 'jp': './assets/jp/handler/FORMATION_LOCATION.png', 'tw': './assets/tw/handler/FORMATION_LOCATION.png'}) GAME_TIPS = Button(area={'cn': (1009, 586, 1028, 614), 'en': (1009, 586, 1028, 614), 'jp': (1009, 586, 1028, 614), 'tw': (1009, 586, 1028, 614)}, color={'cn': (85, 84, 85), 'en': (85, 84, 85), 'jp': (85, 84, 85), 'tw': (85, 84, 85)}, button={'cn': (924, 653, 943, 672), 'en': (924, 653, 943, 672), 'jp': (924, 653, 943, 672), 'tw': (924, 653, 943, 672)}, file={'cn': './assets/cn/handler/GAME_TIPS.png', 'en': './assets/en/handler/GAME_TIPS.png', 'jp': './assets/jp/handler/GAME_TIPS.png', 'tw': './assets/tw/handler/GAME_TIPS.png'}) GAME_TIPS3 = Button(area={'cn': (1008, 586, 1028, 614), 'en': (1008, 586, 1028, 614), 'jp': (1008, 586, 1028, 614), 'tw': (1008, 586, 1028, 614)}, color={'cn': (105, 104, 105), 'en': (105, 104, 105), 'jp': (105, 104, 105), 'tw': (105, 104, 105)}, button={'cn': (1008, 586, 1028, 614), 'en': (1008, 586, 1028, 614), 'jp': (1008, 586, 1028, 614), 'tw': (1008, 586, 1028, 614)}, file={'cn': './assets/cn/handler/GAME_TIPS3.png', 'en': './assets/en/handler/GAME_TIPS3.png', 'jp': './assets/jp/handler/GAME_TIPS3.png', 'tw': './assets/tw/handler/GAME_TIPS3.png'}) @@ -46,6 +46,7 @@ INFO_BAR_DETECT = Button(area={'cn': (194, 299, 1086, 348), 'en': (194, 299, 108 IN_MAP = Button(area={'cn': (749, 654, 921, 707), 'en': (757, 654, 917, 699), 'jp': (748, 653, 919, 705), 'tw': (749, 654, 921, 707)}, color={'cn': (213, 124, 124), 'en': (215, 132, 132), 'jp': (212, 124, 124), 'tw': (213, 124, 124)}, button={'cn': (749, 654, 921, 707), 'en': (757, 654, 917, 699), 'jp': (748, 653, 919, 705), 'tw': (749, 654, 921, 707)}, file={'cn': './assets/cn/handler/IN_MAP.png', 'en': './assets/en/handler/IN_MAP.png', 'jp': './assets/jp/handler/IN_MAP.png', 'tw': './assets/tw/handler/IN_MAP.png'}) IN_STAGE = Button(area={'cn': (122, 16, 172, 39), 'en': (120, 18, 208, 40), 'jp': (121, 15, 174, 40), 'tw': (122, 16, 172, 39)}, color={'cn': (149, 167, 207), 'en': (104, 118, 157), 'jp': (151, 167, 205), 'tw': (149, 167, 207)}, button={'cn': (122, 16, 172, 39), 'en': (120, 18, 208, 40), 'jp': (121, 15, 174, 40), 'tw': (122, 16, 172, 39)}, file={'cn': './assets/cn/handler/IN_STAGE.png', 'en': './assets/en/handler/IN_STAGE.png', 'jp': './assets/jp/handler/IN_STAGE.png', 'tw': './assets/tw/handler/IN_STAGE.png'}) LOGIN_ANNOUNCE = Button(area={'cn': (1160, 45, 1227, 90), 'en': (1159, 44, 1228, 91), 'jp': (1160, 46, 1224, 86), 'tw': (1160, 45, 1227, 90)}, color={'cn': (174, 61, 56), 'en': (193, 79, 73), 'jp': (191, 79, 74), 'tw': (174, 61, 56)}, button={'cn': (1160, 61, 1190, 90), 'en': (1160, 61, 1190, 90), 'jp': (1160, 61, 1190, 90), 'tw': (1160, 61, 1190, 90)}, file={'cn': './assets/cn/handler/LOGIN_ANNOUNCE.png', 'en': './assets/en/handler/LOGIN_ANNOUNCE.png', 'jp': './assets/jp/handler/LOGIN_ANNOUNCE.png', 'tw': './assets/tw/handler/LOGIN_ANNOUNCE.png'}) +LOGIN_ANNOUNCE_2 = Button(area={'cn': (1193, 83, 1215, 105), 'en': (1193, 83, 1215, 105), 'jp': (1193, 83, 1215, 105), 'tw': (1193, 83, 1215, 105)}, color={'cn': (158, 170, 175), 'en': (158, 170, 175), 'jp': (158, 170, 175), 'tw': (158, 170, 175)}, button={'cn': (1171, 83, 1193, 105), 'en': (1171, 83, 1193, 105), 'jp': (1171, 83, 1193, 105), 'tw': (1171, 83, 1193, 105)}, file={'cn': './assets/cn/handler/LOGIN_ANNOUNCE_2.png', 'en': './assets/en/handler/LOGIN_ANNOUNCE_2.png', 'jp': './assets/jp/handler/LOGIN_ANNOUNCE_2.png', 'tw': './assets/tw/handler/LOGIN_ANNOUNCE_2.png'}) LOGIN_CHECK = Button(area={'cn': (1214, 653, 1268, 709), 'en': (1214, 653, 1268, 709), 'jp': (1214, 653, 1268, 709), 'tw': (1214, 653, 1268, 709)}, color={'cn': (203, 215, 230), 'en': (203, 215, 230), 'jp': (203, 215, 230), 'tw': (203, 215, 230)}, button={'cn': (416, 294, 534, 400), 'en': (1078, 591, 1168, 635), 'jp': (416, 294, 534, 400), 'tw': (416, 294, 534, 400)}, file={'cn': './assets/cn/handler/LOGIN_CHECK.png', 'en': './assets/en/handler/LOGIN_CHECK.png', 'jp': './assets/jp/handler/LOGIN_CHECK.png', 'tw': './assets/tw/handler/LOGIN_CHECK.png'}) LOGIN_GAME_UPDATE = Button(area={'cn': (700, 471, 873, 529), 'en': (726, 474, 850, 519), 'jp': (704, 475, 867, 525), 'tw': (706, 477, 866, 528)}, color={'cn': (238, 170, 78), 'en': (241, 169, 73), 'jp': (234, 167, 77), 'tw': (235, 169, 80)}, button={'cn': (700, 471, 873, 529), 'en': (726, 474, 850, 519), 'jp': (704, 475, 867, 525), 'tw': (706, 477, 866, 528)}, file={'cn': './assets/cn/handler/LOGIN_GAME_UPDATE.png', 'en': './assets/en/handler/LOGIN_GAME_UPDATE.png', 'jp': './assets/jp/handler/LOGIN_GAME_UPDATE.png', 'tw': './assets/tw/handler/LOGIN_GAME_UPDATE.png'}) LOGIN_RETURN_INFO = Button(area={'cn': (960, 123, 1158, 333), 'en': (960, 123, 1158, 333), 'jp': (960, 123, 1158, 333), 'tw': (960, 123, 1158, 333)}, color={'cn': (170, 185, 166), 'en': (170, 185, 166), 'jp': (170, 185, 166), 'tw': (170, 185, 166)}, button={'cn': (960, 123, 1158, 333), 'en': (960, 123, 1158, 333), 'jp': (960, 123, 1158, 333), 'tw': (960, 123, 1158, 333)}, file={'cn': './assets/cn/handler/LOGIN_RETURN_INFO.png', 'en': './assets/en/handler/LOGIN_RETURN_INFO.png', 'jp': './assets/jp/handler/LOGIN_RETURN_INFO.png', 'tw': './assets/tw/handler/LOGIN_RETURN_INFO.png'}) @@ -64,8 +65,8 @@ MAP_STAR_2 = Button(area={'cn': (532, 377, 540, 384), 'en': (518, 382, 526, 389) MAP_STAR_3 = Button(area={'cn': (818, 377, 827, 384), 'en': (804, 382, 812, 389), 'jp': (818, 377, 827, 384), 'tw': (818, 377, 827, 384)}, color={'cn': (251, 233, 143), 'en': (252, 234, 144), 'jp': (251, 233, 143), 'tw': (251, 233, 143)}, button={'cn': (818, 377, 827, 384), 'en': (804, 382, 812, 389), 'jp': (818, 377, 827, 384), 'tw': (818, 377, 827, 384)}, file={'cn': './assets/cn/handler/MAP_STAR_3.png', 'en': './assets/en/handler/MAP_STAR_3.png', 'jp': './assets/jp/handler/MAP_STAR_3.png', 'tw': './assets/tw/handler/MAP_STAR_3.png'}) MAP_WALK_OUT_OF_STEP = Button(area={'cn': (654, 312, 704, 335), 'en': (454, 314, 698, 338), 'jp': (736, 312, 783, 336), 'tw': (653, 309, 705, 334)}, color={'cn': (109, 113, 120), 'en': (108, 109, 116), 'jp': (137, 135, 143), 'tw': (118, 124, 132)}, button={'cn': (654, 312, 704, 335), 'en': (454, 314, 698, 338), 'jp': (736, 312, 783, 336), 'tw': (653, 309, 705, 334)}, file={'cn': './assets/cn/handler/MAP_WALK_OUT_OF_STEP.png', 'en': './assets/en/handler/MAP_WALK_OUT_OF_STEP.png', 'jp': './assets/jp/handler/MAP_WALK_OUT_OF_STEP.png', 'tw': './assets/tw/handler/MAP_WALK_OUT_OF_STEP.png'}) 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': (413, 489, 566, 532), 'en': (413, 489, 566, 532), 'jp': (410, 482, 574, 539), 'tw': (413, 489, 566, 532)}, color={'cn': (169, 170, 172), 'en': (169, 170, 172), 'jp': (162, 164, 167), 'tw': (169, 170, 172)}, button={'cn': (413, 489, 566, 532), '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': (716, 488, 869, 533), 'en': (716, 488, 869, 533), 'jp': (711, 482, 874, 539), 'tw': (716, 488, 869, 533)}, color={'cn': (89, 138, 201), 'en': (89, 138, 201), 'jp': (93, 142, 204), 'tw': (89, 138, 201)}, button={'cn': (716, 488, 869, 533), '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'}) +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'}) 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/auto_search.py b/module/handler/auto_search.py index 9f1bed545..f4785fd56 100644 --- a/module/handler/auto_search.py +++ b/module/handler/auto_search.py @@ -133,7 +133,7 @@ class AutoSearchHandler(EnemySearchingHandler): active = [] for index, button in enumerate(AUTO_SEARCH_SETTINGS): - if self.image_color_count(button, color=(156, 255, 82), threshold=221, count=20): + if self.image_color_count(button.button, color=(156, 255, 82), threshold=221, count=20): active.append(index) if not active: diff --git a/module/handler/login.py b/module/handler/login.py index 937ad805f..5b7664324 100644 --- a/module/handler/login.py +++ b/module/handler/login.py @@ -58,6 +58,8 @@ class LoginHandler(UI): login_success = True if self.appear_then_click(LOGIN_ANNOUNCE, offset=(30, 30), interval=5): continue + if self.appear_then_click(LOGIN_ANNOUNCE_2, offset=(30, 30), interval=5): + continue if self.appear(EVENT_LIST_CHECK, offset=(30, 30), interval=5): self.device.click(BACK_ARROW) continue diff --git a/module/map/assets.py b/module/map/assets.py index b6e3bc28a..84842cf99 100644 --- a/module/map/assets.py +++ b/module/map/assets.py @@ -22,7 +22,7 @@ FLEET_PREPARATION = Button(area={'cn': (1013, 558, 1141, 588), 'en': (1048, 569, FLEET_PREPARATION_CHECK = Button(area={'cn': (1146, 107, 1174, 136), 'en': (1129, 111, 1158, 140), 'jp': (1146, 107, 1174, 136), 'tw': (1145, 106, 1175, 136)}, color={'cn': (180, 98, 111), 'en': (189, 105, 109), 'jp': (180, 98, 111), 'tw': (180, 90, 92)}, button={'cn': (1146, 107, 1174, 136), 'en': (1129, 111, 1158, 140), 'jp': (1146, 107, 1174, 136), 'tw': (1145, 106, 1175, 136)}, file={'cn': './assets/cn/map/FLEET_PREPARATION_CHECK.png', 'en': './assets/en/map/FLEET_PREPARATION_CHECK.png', 'jp': './assets/jp/map/FLEET_PREPARATION_CHECK.png', 'tw': './assets/tw/map/FLEET_PREPARATION_CHECK.png'}) MAP_CAT_ATTACK = Button(area={'cn': (1237, 103, 1252, 153), 'en': (1237, 103, 1252, 153), 'jp': (1237, 103, 1252, 153), 'tw': (1237, 103, 1252, 153)}, color={'cn': (43, 45, 52), 'en': (43, 45, 52), 'jp': (43, 45, 52), 'tw': (43, 45, 52)}, button={'cn': (1148, 653, 1262, 705), 'en': (1147, 651, 1263, 701), 'jp': (1149, 653, 1261, 704), 'tw': (1148, 653, 1262, 705)}, file={'cn': './assets/cn/map/MAP_CAT_ATTACK.png', 'en': './assets/en/map/MAP_CAT_ATTACK.png', 'jp': './assets/jp/map/MAP_CAT_ATTACK.png', 'tw': './assets/tw/map/MAP_CAT_ATTACK.png'}) MAP_CAT_ATTACK_MIRROR = Button(area={'cn': (147, 145, 187, 157), 'en': (147, 145, 187, 157), 'jp': (147, 145, 187, 157), 'tw': (147, 145, 187, 157)}, color={'cn': (214, 191, 99), 'en': (214, 191, 99), 'jp': (214, 191, 99), 'tw': (214, 191, 99)}, button={'cn': (147, 145, 187, 157), 'en': (147, 145, 187, 157), 'jp': (147, 145, 187, 157), 'tw': (147, 145, 187, 157)}, file={'cn': './assets/cn/map/MAP_CAT_ATTACK_MIRROR.png', 'en': './assets/en/map/MAP_CAT_ATTACK_MIRROR.png', 'jp': './assets/jp/map/MAP_CAT_ATTACK_MIRROR.png', 'tw': './assets/tw/map/MAP_CAT_ATTACK_MIRROR.png'}) -MAP_OFFENSIVE = Button(area={'cn': (1148, 653, 1262, 705), 'en': (1147, 652, 1263, 701), 'jp': (1147, 652, 1263, 706), 'tw': (1148, 653, 1262, 705)}, color={'cn': (234, 180, 108), 'en': (234, 183, 108), 'jp': (233, 184, 105), 'tw': (234, 180, 108)}, button={'cn': (1148, 653, 1262, 705), 'en': (1147, 652, 1263, 701), 'jp': (1147, 652, 1263, 706), 'tw': (1148, 653, 1262, 705)}, file={'cn': './assets/cn/map/MAP_OFFENSIVE.png', 'en': './assets/en/map/MAP_OFFENSIVE.png', 'jp': './assets/jp/map/MAP_OFFENSIVE.png', 'tw': './assets/tw/map/MAP_OFFENSIVE.png'}) +MAP_OFFENSIVE = Button(area={'cn': (1148, 653, 1262, 705), 'en': (1147, 652, 1263, 701), 'jp': (1147, 652, 1263, 706), 'tw': (1148, 653, 1262, 705)}, color={'cn': (234, 180, 108), 'en': (234, 183, 108), 'jp': (233, 184, 105), 'tw': (243, 199, 104)}, button={'cn': (1148, 653, 1262, 705), 'en': (1147, 652, 1263, 701), 'jp': (1147, 652, 1263, 706), 'tw': (1148, 653, 1262, 705)}, file={'cn': './assets/cn/map/MAP_OFFENSIVE.png', 'en': './assets/en/map/MAP_OFFENSIVE.png', 'jp': './assets/jp/map/MAP_OFFENSIVE.png', 'tw': './assets/tw/map/MAP_OFFENSIVE.png'}) MAP_PREPARATION = Button(area={'cn': (854, 488, 1052, 548), 'en': (852, 489, 1054, 553), 'jp': (850, 485, 1051, 548), 'tw': (854, 488, 1052, 548)}, color={'cn': (236, 186, 115), 'en': (234, 179, 93), 'jp': (232, 181, 101), 'tw': (236, 186, 115)}, button={'cn': (854, 488, 1052, 548), 'en': (852, 489, 1054, 553), 'jp': (850, 485, 1051, 548), 'tw': (854, 488, 1052, 548)}, file={'cn': './assets/cn/map/MAP_PREPARATION.png', 'en': './assets/en/map/MAP_PREPARATION.png', 'jp': './assets/jp/map/MAP_PREPARATION.png', 'tw': './assets/tw/map/MAP_PREPARATION.png'}) MAP_PREPARATION_CANCEL = Button(area={'cn': (234, 12, 278, 47), 'en': (234, 12, 278, 47), 'jp': (234, 12, 278, 47), 'tw': (234, 12, 278, 47)}, color={'cn': (45, 46, 69), 'en': (45, 46, 69), 'jp': (45, 46, 69), 'tw': (45, 46, 69)}, button={'cn': (234, 12, 278, 47), 'en': (234, 12, 278, 47), 'jp': (234, 12, 278, 47), 'tw': (234, 12, 278, 47)}, file={'cn': './assets/cn/map/MAP_PREPARATION_CANCEL.png', 'en': './assets/en/map/MAP_PREPARATION_CANCEL.png', 'jp': './assets/jp/map/MAP_PREPARATION_CANCEL.png', 'tw': './assets/tw/map/MAP_PREPARATION_CANCEL.png'}) SUBMARINE_ADVICE = Button(area={'cn': (1030, 457, 1074, 477), 'en': (1002, 466, 1064, 478), 'jp': (1030, 457, 1074, 478), 'tw': (1035, 462, 1080, 484)}, color={'cn': (243, 222, 179), 'en': (222, 186, 129), 'jp': (243, 222, 179), 'tw': (240, 214, 168)}, button={'cn': (1030, 457, 1074, 477), 'en': (1002, 466, 1064, 478), 'jp': (1030, 457, 1074, 478), 'tw': (1035, 462, 1080, 484)}, file={'cn': './assets/cn/map/SUBMARINE_ADVICE.png', 'en': './assets/en/map/SUBMARINE_ADVICE.png', 'jp': './assets/jp/map/SUBMARINE_ADVICE.png', 'tw': './assets/tw/map/SUBMARINE_ADVICE.png'}) @@ -31,5 +31,5 @@ SUBMARINE_CHOOSE = Button(area={'cn': (1022, 443, 1082, 501), 'en': (1003, 443, SUBMARINE_CLEAR = Button(area={'cn': (1109, 443, 1169, 501), 'en': (1093, 443, 1153, 501), 'jp': (1113, 442, 1169, 501), 'tw': (1109, 440, 1175, 501)}, color={'cn': (156, 158, 159), 'en': (139, 140, 143), 'jp': (151, 153, 154), 'tw': (152, 152, 155)}, button={'cn': (1109, 443, 1169, 501), 'en': (1093, 443, 1153, 501), 'jp': (1113, 442, 1169, 501), 'tw': (1109, 440, 1175, 501)}, file={'cn': './assets/cn/map/SUBMARINE_CLEAR.png', 'en': './assets/en/map/SUBMARINE_CLEAR.png', 'jp': './assets/jp/map/SUBMARINE_CLEAR.png', 'tw': './assets/tw/map/SUBMARINE_CLEAR.png'}) SUBMARINE_HARD_SATIESFIED = Button(area={'cn': (211, 514, 375, 539), 'en': (189, 517, 339, 541), 'jp': (211, 514, 375, 539), 'tw': (217, 511, 309, 536)}, color={'cn': (59, 57, 58), 'en': (68, 69, 66), 'jp': (59, 57, 58), 'tw': (99, 95, 90)}, button={'cn': (211, 514, 375, 539), 'en': (189, 517, 339, 541), 'jp': (211, 514, 375, 539), 'tw': (217, 511, 309, 536)}, file={'cn': './assets/cn/map/SUBMARINE_HARD_SATIESFIED.png', 'en': './assets/en/map/SUBMARINE_HARD_SATIESFIED.png', 'jp': './assets/jp/map/SUBMARINE_HARD_SATIESFIED.png', 'tw': './assets/tw/map/SUBMARINE_HARD_SATIESFIED.png'}) SUBMARINE_IN_USE = Button(area={'cn': (454, 427, 537, 510), 'en': (436, 429, 517, 510), 'jp': (454, 427, 537, 510), 'tw': (454, 427, 538, 510)}, color={'cn': (99, 98, 116), 'en': (183, 170, 163), 'jp': (99, 98, 116), 'tw': (129, 116, 105)}, button={'cn': (454, 427, 537, 510), 'en': (436, 429, 517, 510), 'jp': (454, 427, 537, 510), 'tw': (454, 427, 538, 510)}, file={'cn': './assets/cn/map/SUBMARINE_IN_USE.png', 'en': './assets/en/map/SUBMARINE_IN_USE.png', 'jp': './assets/jp/map/SUBMARINE_IN_USE.png', 'tw': './assets/tw/map/SUBMARINE_IN_USE.png'}) -SWITCH_OVER = Button(area={'cn': (947, 654, 1118, 706), 'en': (945, 647, 1119, 702), 'jp': (946, 653, 1119, 707), 'tw': (947, 654, 1118, 706)}, color={'cn': (144, 158, 198), 'en': (139, 154, 194), 'jp': (142, 156, 195), 'tw': (144, 158, 198)}, button={'cn': (947, 654, 1118, 706), 'en': (945, 647, 1119, 702), 'jp': (946, 653, 1119, 707), 'tw': (947, 654, 1118, 706)}, file={'cn': './assets/cn/map/SWITCH_OVER.png', 'en': './assets/en/map/SWITCH_OVER.png', 'jp': './assets/jp/map/SWITCH_OVER.png', 'tw': './assets/tw/map/SWITCH_OVER.png'}) +SWITCH_OVER = Button(area={'cn': (947, 654, 1118, 706), 'en': (945, 647, 1119, 702), 'jp': (946, 653, 1119, 707), 'tw': (947, 654, 1118, 706)}, color={'cn': (144, 158, 198), 'en': (139, 154, 194), 'jp': (142, 156, 195), 'tw': (145, 159, 197)}, button={'cn': (947, 654, 1118, 706), 'en': (945, 647, 1119, 702), 'jp': (946, 653, 1119, 707), 'tw': (947, 654, 1118, 706)}, file={'cn': './assets/cn/map/SWITCH_OVER.png', 'en': './assets/en/map/SWITCH_OVER.png', 'jp': './assets/jp/map/SWITCH_OVER.png', 'tw': './assets/tw/map/SWITCH_OVER.png'}) WITHDRAW = Button(area={'cn': (749, 654, 921, 707), 'en': (748, 652, 922, 702), 'jp': (748, 653, 921, 707), 'tw': (749, 654, 921, 707)}, color={'cn': (213, 124, 124), 'en': (211, 124, 124), 'jp': (210, 122, 122), 'tw': (213, 124, 124)}, button={'cn': (749, 654, 921, 707), 'en': (748, 652, 922, 702), 'jp': (748, 653, 921, 707), 'tw': (749, 654, 921, 707)}, file={'cn': './assets/cn/map/WITHDRAW.png', 'en': './assets/en/map/WITHDRAW.png', 'jp': './assets/jp/map/WITHDRAW.png', 'tw': './assets/tw/map/WITHDRAW.png'}) diff --git a/module/map/map_fleet_preparation.py b/module/map/map_fleet_preparation.py index 71f76cb9f..fc3b51aee 100644 --- a/module/map/map_fleet_preparation.py +++ b/module/map/map_fleet_preparation.py @@ -5,6 +5,9 @@ from module.base.button import Button from module.base.timer import Timer from module.base.utils import * from module.exception import RequestHumanTakeover +from module.handler.assets import AUTO_SEARCH_SET_MOB, AUTO_SEARCH_SET_BOSS, \ + AUTO_SEARCH_SET_ALL, AUTO_SEARCH_SET_STANDBY, \ + AUTO_SEARCH_SET_SUB_AUTO, AUTO_SEARCH_SET_SUB_STANDBY from module.handler.info_handler import InfoHandler from module.logger import logger from module.map.assets import * @@ -346,4 +349,13 @@ class FleetPreparation(InfoHandler): else: submarine.clear() + if self.appear(FLEET_1_CLEAR, offset=(-20, -80, 20, 5)): + AUTO_SEARCH_SET_MOB.load_offset(FLEET_1_CLEAR) + AUTO_SEARCH_SET_BOSS.load_offset(FLEET_1_CLEAR) + AUTO_SEARCH_SET_ALL.load_offset(FLEET_1_CLEAR) + AUTO_SEARCH_SET_STANDBY.load_offset(FLEET_1_CLEAR) + if self.appear(SUBMARINE_CLEAR, offset=(-20, -80, 20, 5)): + AUTO_SEARCH_SET_SUB_AUTO.load_offset(SUBMARINE_CLEAR) + AUTO_SEARCH_SET_SUB_STANDBY.load_offset(SUBMARINE_CLEAR) + return True diff --git a/module/ocr/al_ocr.py b/module/ocr/al_ocr.py index f010621aa..e913c42f3 100644 --- a/module/ocr/al_ocr.py +++ b/module/ocr/al_ocr.py @@ -2,23 +2,25 @@ import os import cv2 import numpy as np -from cnocr import CnOcr -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 PIL import Image from module.exception import RequestHumanTakeover from module.logger import logger +logger.info('Loading OCR dependencies') +from cnocr import CnOcr +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/ocr/models.py b/module/ocr/models.py index 75f5b7e9f..9547e8abd 100644 --- a/module/ocr/models.py +++ b/module/ocr/models.py @@ -1,5 +1,4 @@ from module.base.decorator import cached_property -from module.ocr.al_ocr import AlOcr class OcrModel: @@ -13,8 +12,9 @@ class OcrModel: # Font: Impact, AgencyFB-Regular, MStiffHeiHK-UltraBold # Charset: 0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ:/- (Letter 'O' and is not included) # _num_classes: 39 + from module.ocr.al_ocr import AlOcr return AlOcr(model_name='densenet-lite-gru', model_epoch=15, root='./bin/cnocr_models/azur_lane', - name='azur_lane') + name='azur_lane') @cached_property def cnocr(self): @@ -26,10 +26,12 @@ class OcrModel: # Font: Various # Charset: Number, English character, Chinese character, symbols, # _num_classes: 6426 + from module.ocr.al_ocr import AlOcr return AlOcr(model_name='densenet-lite-gru', model_epoch=39, root='./bin/cnocr_models/cnocr', name='cnocr') @cached_property def jp(self): + from module.ocr.al_ocr import AlOcr return AlOcr(model_name='densenet-lite-gru', model_epoch=125, root='./bin/cnocr_models/jp', name='jp') @cached_property @@ -42,7 +44,8 @@ class OcrModel: # Font: Various, 6 kinds # Charset: Numbers, Upper english characters, Chinese traditional characters # _num_classes: 5322 + from module.ocr.al_ocr import AlOcr return AlOcr(model_name='densenet-lite-gru', model_epoch=63, root='./bin/cnocr_models/tw', name='tw') -OCR_MODEL = OcrModel() \ No newline at end of file +OCR_MODEL = OcrModel() diff --git a/module/os/globe_camera.py b/module/os/globe_camera.py index c4e72783c..8b4215f7a 100644 --- a/module/os/globe_camera.py +++ b/module/os/globe_camera.py @@ -6,8 +6,8 @@ from module.os.assets import * from module.os.globe_detection import GLOBE_MAP_SHAPE, GlobeDetection from module.os.globe_operation import GlobeOperation from module.os.globe_zone import Zone, ZoneManager -from module.os_ash.assets import ASH_SHOWDOWN, ASH_QUIT -from module.os_handler.assets import AUTO_SEARCH_REWARD +from module.os_ash.assets import ASH_QUIT, ASH_SHOWDOWN +from module.os_handler.assets import ACTION_POINT_CANCEL, ACTION_POINT_USE, AUTO_SEARCH_REWARD class GlobeCamera(GlobeOperation, ZoneManager): @@ -66,6 +66,11 @@ class GlobeCamera(GlobeOperation, ZoneManager): self.device.click(ASH_QUIT) timeout.reset() continue + # Action point popup + if self.appear(ACTION_POINT_USE, offset=(20, 20), interval=3): + self.device.click(ACTION_POINT_CANCEL) + timeout.reset() + continue logger.warning('Trying to do globe_update(), but not in os globe map') continue diff --git a/module/os/map.py b/module/os/map.py index 94f574908..67f70eddc 100644 --- a/module/os/map.py +++ b/module/os/map.py @@ -6,9 +6,7 @@ import inflection from module.base.timer import Timer from module.combat.assets import PAUSE from module.config.utils import get_os_reset_remain -from module.exception import CampaignEnd, RequestHumanTakeover -from module.exception import GameTooManyClickError -from module.exception import MapWalkError, ScriptError +from module.exception import CampaignEnd, GameTooManyClickError, MapWalkError, RequestHumanTakeover, ScriptError from module.exercise.assets import QUIT_CONFIRM, QUIT_RECONFIRM from module.handler.login import LoginHandler, MAINTENANCE_ANNOUNCE from module.logger import logger @@ -17,7 +15,7 @@ from module.os.assets import FLEET_EMP_DEBUFF, MAP_GOTO_GLOBE_FOG from module.os.fleet import OSFleet from module.os.globe_camera import GlobeCamera from module.os.globe_operation import RewardUncollectedError -from module.os_handler.assets import AUTO_SEARCH_OS_MAP_OPTION_OFF, \ +from module.os_handler.assets import AUTO_SEARCH_OS_MAP_OPTION_OFF, AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED, \ AUTO_SEARCH_OS_MAP_OPTION_ON, AUTO_SEARCH_REWARD from module.os_handler.strategic import StrategicSearchHandler from module.ui.assets import GOTO_MAIN @@ -508,6 +506,8 @@ class OSMap(OSFleet, Map, GlobeCamera, StrategicSearchHandler): if not unlock_checked: if self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF, offset=(5, 120)): unlock_checked = True + elif self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED, offset=(5, 120)): + unlock_checked = True elif self.appear(AUTO_SEARCH_OS_MAP_OPTION_ON, offset=(5, 120)): unlock_checked = True diff --git a/module/os_handler/assets.py b/module/os_handler/assets.py index 68694c637..a982033e8 100644 --- a/module/os_handler/assets.py +++ b/module/os_handler/assets.py @@ -11,6 +11,7 @@ ACTION_POINT_REMAIN_OS = Button(area={'cn': (878, 28, 928, 46), 'en': (878, 28, ACTION_POINT_USE = Button(area={'cn': (738, 528, 910, 585), 'en': (742, 531, 909, 584), 'jp': (737, 528, 911, 586), 'tw': (739, 528, 909, 585)}, color={'cn': (93, 142, 203), 'en': (107, 152, 208), 'jp': (92, 141, 203), 'tw': (95, 144, 205)}, button={'cn': (738, 528, 910, 585), 'en': (742, 531, 909, 584), 'jp': (737, 528, 911, 586), 'tw': (739, 528, 909, 585)}, file={'cn': './assets/cn/os_handler/ACTION_POINT_USE.png', 'en': './assets/en/os_handler/ACTION_POINT_USE.png', 'jp': './assets/jp/os_handler/ACTION_POINT_USE.png', 'tw': './assets/tw/os_handler/ACTION_POINT_USE.png'}) ASH_POPUP_CHECK = Button(area={'cn': (665, 318, 759, 340), 'en': (372, 324, 601, 342), 'jp': (438, 311, 534, 346), 'tw': (665, 298, 759, 320)}, color={'cn': (154, 163, 172), 'en': (174, 179, 184), 'jp': (146, 154, 161), 'tw': (157, 166, 176)}, button={'cn': (665, 318, 759, 340), 'en': (372, 324, 601, 342), 'jp': (438, 311, 534, 346), 'tw': (665, 298, 759, 320)}, file={'cn': './assets/cn/os_handler/ASH_POPUP_CHECK.png', 'en': './assets/en/os_handler/ASH_POPUP_CHECK.png', 'jp': './assets/jp/os_handler/ASH_POPUP_CHECK.png', 'tw': './assets/tw/os_handler/ASH_POPUP_CHECK.png'}) AUTO_SEARCH_OS_MAP_OPTION_OFF = Button(area={'cn': (1205, 549, 1275, 566), 'en': (1203, 534, 1274, 544), 'jp': (1204, 572, 1276, 593), 'tw': (1206, 573, 1275, 591)}, color={'cn': (196, 169, 169), 'en': (167, 140, 142), 'jp': (180, 154, 157), 'tw': (167, 143, 147)}, button={'cn': (1205, 549, 1275, 566), 'en': (1203, 534, 1274, 544), 'jp': (1204, 572, 1276, 593), 'tw': (1206, 573, 1275, 591)}, file={'cn': './assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF.png', 'en': './assets/en/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF.png', 'jp': './assets/jp/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF.png', 'tw': './assets/tw/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF.png'}) +AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED = Button(area={'cn': (1205, 531, 1275, 547), 'en': (1205, 531, 1275, 547), 'jp': (1205, 531, 1274, 548), 'tw': (1205, 531, 1275, 547)}, color={'cn': (156, 135, 134), 'en': (156, 135, 134), 'jp': (157, 137, 136), 'tw': (156, 135, 134)}, button={'cn': (1205, 531, 1275, 547), 'en': (1205, 531, 1275, 547), 'jp': (1205, 531, 1274, 548), 'tw': (1205, 531, 1275, 547)}, file={'cn': './assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png', 'en': './assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png', 'jp': './assets/jp/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png', 'tw': './assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.png'}) AUTO_SEARCH_OS_MAP_OPTION_ON = Button(area={'cn': (1205, 549, 1275, 566), 'en': (1203, 534, 1274, 544), 'jp': (1205, 573, 1275, 592), 'tw': (1206, 573, 1275, 591)}, color={'cn': (149, 176, 193), 'en': (110, 131, 152), 'jp': (145, 172, 190), 'tw': (123, 148, 169)}, button={'cn': (1205, 549, 1275, 566), 'en': (1203, 534, 1274, 544), 'jp': (1205, 573, 1275, 592), 'tw': (1206, 573, 1275, 591)}, file={'cn': './assets/cn/os_handler/AUTO_SEARCH_OS_MAP_OPTION_ON.png', 'en': './assets/en/os_handler/AUTO_SEARCH_OS_MAP_OPTION_ON.png', 'jp': './assets/jp/os_handler/AUTO_SEARCH_OS_MAP_OPTION_ON.png', 'tw': './assets/tw/os_handler/AUTO_SEARCH_OS_MAP_OPTION_ON.png'}) AUTO_SEARCH_REWARD = Button(area={'cn': (575, 598, 721, 646), 'en': (574, 597, 722, 648), 'jp': (577, 597, 722, 645), 'tw': (576, 598, 720, 647)}, color={'cn': (169, 168, 170), 'en': (168, 171, 174), 'jp': (165, 170, 175), 'tw': (171, 174, 179)}, button={'cn': (575, 598, 721, 646), 'en': (574, 597, 722, 648), 'jp': (577, 597, 722, 645), 'tw': (576, 598, 720, 647)}, file={'cn': './assets/cn/os_handler/AUTO_SEARCH_REWARD.png', 'en': './assets/en/os_handler/AUTO_SEARCH_REWARD.png', 'jp': './assets/jp/os_handler/AUTO_SEARCH_REWARD.png', 'tw': './assets/tw/os_handler/AUTO_SEARCH_REWARD.png'}) CLICK_SAFE_AREA = Button(area={'cn': (1104, 169, 1214, 284), 'en': (1104, 169, 1214, 284), 'jp': (1104, 169, 1214, 284), 'tw': (1104, 169, 1214, 284)}, color={'cn': (96, 114, 142), 'en': (96, 114, 142), 'jp': (96, 114, 142), 'tw': (96, 114, 142)}, button={'cn': (1104, 169, 1214, 284), 'en': (1104, 169, 1214, 284), 'jp': (1104, 169, 1214, 284), 'tw': (1104, 169, 1214, 284)}, file={'cn': './assets/cn/os_handler/CLICK_SAFE_AREA.png', 'en': './assets/en/os_handler/CLICK_SAFE_AREA.png', 'jp': './assets/jp/os_handler/CLICK_SAFE_AREA.png', 'tw': './assets/tw/os_handler/CLICK_SAFE_AREA.png'}) diff --git a/module/os_handler/map_event.py b/module/os_handler/map_event.py index 3e909d531..3b186ec58 100644 --- a/module/os_handler/map_event.py +++ b/module/os_handler/map_event.py @@ -213,6 +213,7 @@ class MapEventHandler(EnemySearchingHandler): AUTO_SEARCH_REWARD, AUTO_SEARCH_OS_MAP_OPTION_ON, AUTO_SEARCH_OS_MAP_OPTION_OFF, + AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED, ]) confirm_timer.reset() continue @@ -244,11 +245,17 @@ class MapEventHandler(EnemySearchingHandler): bool: If clicked. """ if self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF, offset=(5, 120)) \ - and AUTO_SEARCH_OS_MAP_OPTION_OFF.match_appear_on(self.device.image) \ - and self.info_bar_count() >= 2: - self.device.screenshot_interval_set() - self.os_auto_search_quit(drop=drop) - raise CampaignEnd + and AUTO_SEARCH_OS_MAP_OPTION_OFF.match_appear_on(self.device.image): + if self.info_bar_count() >= 2: + self.device.screenshot_interval_set() + self.os_auto_search_quit(drop=drop) + raise CampaignEnd + if self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED, offset=(5, 120)) \ + and AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.match_appear_on(self.device.image): + if self.info_bar_count() >= 2: + self.device.screenshot_interval_set() + self.os_auto_search_quit(drop=drop) + raise CampaignEnd if self.appear(AUTO_SEARCH_REWARD, offset=(50, 50)): self.device.screenshot_interval_set() if self.os_auto_search_quit(drop=drop): @@ -265,6 +272,11 @@ class MapEventHandler(EnemySearchingHandler): and AUTO_SEARCH_OS_MAP_OPTION_OFF.match_appear_on(self.device.image): self.device.click(AUTO_SEARCH_OS_MAP_OPTION_OFF) return True + # Game client bugged sometimes, AUTO_SEARCH_OS_MAP_OPTION_OFF grayed out but still functional + if self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED, offset=(5, 120), interval=3) \ + and AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED.match_appear_on(self.device.image): + self.device.click(AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED) + return True else: if self.appear(AUTO_SEARCH_OS_MAP_OPTION_ON, offset=(5, 120), interval=3) \ and AUTO_SEARCH_OS_MAP_OPTION_ON.match_appear_on(self.device.image): diff --git a/module/raid/assets.py b/module/raid/assets.py index e37fb9c29..bf94c56c3 100644 --- a/module/raid/assets.py +++ b/module/raid/assets.py @@ -25,13 +25,13 @@ ESSEX_RAID_HARD = Button(area={'cn': (1155, 271, 1215, 313), 'en': (1155, 264, 1 ESSEX_RAID_NORMAL = Button(area={'cn': (1111, 385, 1176, 428), 'en': (1132, 385, 1185, 421), 'jp': (1118, 381, 1188, 404), 'tw': (1120, 381, 1175, 428)}, color={'cn': (123, 173, 242), 'en': (126, 182, 241), 'jp': (163, 190, 224), 'tw': (130, 178, 239)}, button={'cn': (1111, 385, 1176, 428), 'en': (1132, 385, 1185, 421), 'jp': (1118, 381, 1188, 404), 'tw': (1120, 381, 1175, 428)}, file={'cn': './assets/cn/raid/ESSEX_RAID_NORMAL.png', 'en': './assets/en/raid/ESSEX_RAID_NORMAL.png', 'jp': './assets/jp/raid/ESSEX_RAID_NORMAL.png', 'tw': './assets/tw/raid/ESSEX_RAID_NORMAL.png'}) GORIZIA_OCR_PT = Button(area={'cn': (1160, 605, 1280, 637), 'en': (1160, 605, 1280, 637), 'jp': (1160, 605, 1280, 637), 'tw': (1160, 605, 1280, 637)}, color={'cn': (216, 220, 171), 'en': (216, 220, 171), 'jp': (216, 220, 171), 'tw': (216, 220, 171)}, button={'cn': (1160, 605, 1280, 637), 'en': (1160, 605, 1280, 637), 'jp': (1160, 605, 1280, 637), 'tw': (1160, 605, 1280, 637)}, file={'cn': './assets/cn/raid/GORIZIA_OCR_PT.png', 'en': './assets/en/raid/GORIZIA_OCR_PT.png', 'jp': './assets/jp/raid/GORIZIA_OCR_PT.png', 'tw': './assets/tw/raid/GORIZIA_OCR_PT.png'}) GORIZIA_OCR_REMAIN_EASY = Button(area={'cn': (1138, 513, 1194, 539), 'en': (1138, 513, 1194, 539), 'jp': (1138, 513, 1194, 539), 'tw': (1138, 513, 1194, 539)}, color={'cn': (205, 208, 201), 'en': (205, 208, 201), 'jp': (205, 208, 201), 'tw': (205, 208, 201)}, button={'cn': (1138, 513, 1194, 539), 'en': (1138, 513, 1194, 539), 'jp': (1138, 513, 1194, 539), 'tw': (1138, 513, 1194, 539)}, file={'cn': './assets/cn/raid/GORIZIA_OCR_REMAIN_EASY.png', 'en': './assets/en/raid/GORIZIA_OCR_REMAIN_EASY.png', 'jp': './assets/jp/raid/GORIZIA_OCR_REMAIN_EASY.png', 'tw': './assets/tw/raid/GORIZIA_OCR_REMAIN_EASY.png'}) -GORIZIA_OCR_REMAIN_EX = Button(area={'cn': (1082, 16, 1151, 42), 'en': (1082, 16, 1151, 42), 'jp': (1082, 16, 1151, 42), 'tw': (1082, 16, 1151, 42)}, color={'cn': (80, 93, 94), 'en': (80, 93, 94), 'jp': (80, 93, 94), 'tw': (80, 93, 94)}, button={'cn': (1082, 16, 1151, 42), 'en': (1082, 16, 1151, 42), 'jp': (1082, 16, 1151, 42), 'tw': (1082, 16, 1151, 42)}, file={'cn': './assets/cn/raid/GORIZIA_OCR_REMAIN_EX.png', 'en': './assets/en/raid/GORIZIA_OCR_REMAIN_EX.png', 'jp': './assets/cn/raid/GORIZIA_OCR_REMAIN_EX.png', 'tw': './assets/cn/raid/GORIZIA_OCR_REMAIN_EX.png'}) +GORIZIA_OCR_REMAIN_EX = Button(area={'cn': (1082, 16, 1151, 42), 'en': (1082, 16, 1151, 42), 'jp': (1082, 16, 1151, 42), 'tw': (1082, 16, 1151, 42)}, color={'cn': (80, 93, 94), 'en': (80, 93, 94), 'jp': (80, 93, 94), 'tw': (80, 93, 94)}, button={'cn': (1082, 16, 1151, 42), 'en': (1082, 16, 1151, 42), 'jp': (1082, 16, 1151, 42), 'tw': (1082, 16, 1151, 42)}, file={'cn': './assets/cn/raid/GORIZIA_OCR_REMAIN_EX.png', 'en': './assets/en/raid/GORIZIA_OCR_REMAIN_EX.png', 'jp': './assets/cn/raid/GORIZIA_OCR_REMAIN_EX.png', 'tw': './assets/tw/raid/GORIZIA_OCR_REMAIN_EX.png'}) GORIZIA_OCR_REMAIN_HARD = Button(area={'cn': (1138, 346, 1195, 372), 'en': (1138, 346, 1195, 372), 'jp': (1138, 346, 1195, 372), 'tw': (1138, 346, 1195, 372)}, color={'cn': (206, 208, 202), 'en': (206, 208, 202), 'jp': (206, 208, 202), 'tw': (206, 208, 202)}, button={'cn': (1138, 346, 1195, 372), 'en': (1138, 346, 1195, 372), 'jp': (1138, 346, 1195, 372), 'tw': (1138, 346, 1195, 372)}, file={'cn': './assets/cn/raid/GORIZIA_OCR_REMAIN_HARD.png', 'en': './assets/en/raid/GORIZIA_OCR_REMAIN_HARD.png', 'jp': './assets/jp/raid/GORIZIA_OCR_REMAIN_HARD.png', 'tw': './assets/tw/raid/GORIZIA_OCR_REMAIN_HARD.png'}) GORIZIA_OCR_REMAIN_NORMAL = Button(area={'cn': (1174, 428, 1231, 455), 'en': (1174, 428, 1231, 455), 'jp': (1174, 428, 1231, 455), 'tw': (1174, 428, 1231, 455)}, color={'cn': (208, 210, 204), 'en': (208, 210, 204), 'jp': (208, 210, 204), 'tw': (208, 210, 204)}, button={'cn': (1174, 428, 1231, 455), 'en': (1174, 428, 1231, 455), 'jp': (1174, 428, 1231, 455), 'tw': (1174, 428, 1231, 455)}, file={'cn': './assets/cn/raid/GORIZIA_OCR_REMAIN_NORMAL.png', 'en': './assets/en/raid/GORIZIA_OCR_REMAIN_NORMAL.png', 'jp': './assets/jp/raid/GORIZIA_OCR_REMAIN_NORMAL.png', 'tw': './assets/tw/raid/GORIZIA_OCR_REMAIN_NORMAL.png'}) -GORIZIA_RAID_EASY = Button(area={'cn': (1071, 515, 1118, 538), 'en': (1071, 520, 1118, 541), 'jp': (1069, 513, 1119, 538), 'tw': (1071, 515, 1118, 538)}, color={'cn': (183, 185, 177), 'en': (206, 208, 202), 'jp': (194, 197, 189), 'tw': (183, 185, 177)}, button={'cn': (1071, 515, 1118, 538), 'en': (1071, 520, 1118, 541), 'jp': (1069, 513, 1119, 538), 'tw': (1071, 515, 1118, 538)}, file={'cn': './assets/cn/raid/GORIZIA_RAID_EASY.png', 'en': './assets/en/raid/GORIZIA_RAID_EASY.png', 'jp': './assets/jp/raid/GORIZIA_RAID_EASY.png', 'tw': './assets/cn/raid/GORIZIA_RAID_EASY.png'}) +GORIZIA_RAID_EASY = Button(area={'cn': (1071, 515, 1118, 538), 'en': (1071, 520, 1118, 541), 'jp': (1069, 513, 1119, 538), 'tw': (1069, 514, 1120, 539)}, color={'cn': (183, 185, 177), 'en': (206, 208, 202), 'jp': (194, 197, 189), 'tw': (185, 187, 179)}, button={'cn': (1071, 515, 1118, 538), 'en': (1071, 520, 1118, 541), 'jp': (1069, 513, 1119, 538), 'tw': (1069, 514, 1120, 539)}, file={'cn': './assets/cn/raid/GORIZIA_RAID_EASY.png', 'en': './assets/en/raid/GORIZIA_RAID_EASY.png', 'jp': './assets/jp/raid/GORIZIA_RAID_EASY.png', 'tw': './assets/tw/raid/GORIZIA_RAID_EASY.png'}) GORIZIA_RAID_EX = Button(area={'cn': (978, 210, 1050, 242), 'en': (978, 210, 1050, 242), 'jp': (978, 210, 1050, 242), 'tw': (978, 210, 1050, 242)}, color={'cn': (166, 186, 137), 'en': (166, 186, 137), 'jp': (166, 186, 137), 'tw': (166, 186, 137)}, button={'cn': (978, 210, 1050, 242), 'en': (978, 210, 1050, 242), 'jp': (978, 210, 1050, 242), 'tw': (978, 210, 1050, 242)}, file={'cn': './assets/cn/raid/GORIZIA_RAID_EX.png', 'en': './assets/en/raid/GORIZIA_RAID_EX.png', 'jp': './assets/jp/raid/GORIZIA_RAID_EX.png', 'tw': './assets/tw/raid/GORIZIA_RAID_EX.png'}) -GORIZIA_RAID_HARD = Button(area={'cn': (1072, 348, 1118, 370), 'en': (1065, 352, 1116, 368), 'jp': (1056, 345, 1126, 371), 'tw': (1072, 348, 1118, 370)}, color={'cn': (169, 172, 162), 'en': (183, 185, 177), 'jp': (215, 217, 212), 'tw': (169, 172, 162)}, button={'cn': (1072, 348, 1118, 370), 'en': (1065, 352, 1116, 368), 'jp': (1056, 345, 1126, 371), 'tw': (1072, 348, 1118, 370)}, file={'cn': './assets/cn/raid/GORIZIA_RAID_HARD.png', 'en': './assets/en/raid/GORIZIA_RAID_HARD.png', 'jp': './assets/jp/raid/GORIZIA_RAID_HARD.png', 'tw': './assets/cn/raid/GORIZIA_RAID_HARD.png'}) -GORIZIA_RAID_NORMAL = Button(area={'cn': (1108, 430, 1153, 453), 'en': (1087, 435, 1163, 451), 'jp': (1105, 429, 1157, 454), 'tw': (1108, 430, 1153, 453)}, color={'cn': (177, 179, 171), 'en': (189, 191, 183), 'jp': (196, 198, 192), 'tw': (177, 179, 171)}, button={'cn': (1108, 430, 1153, 453), 'en': (1087, 435, 1163, 451), 'jp': (1105, 429, 1157, 454), 'tw': (1108, 430, 1153, 453)}, file={'cn': './assets/cn/raid/GORIZIA_RAID_NORMAL.png', 'en': './assets/en/raid/GORIZIA_RAID_NORMAL.png', 'jp': './assets/jp/raid/GORIZIA_RAID_NORMAL.png', 'tw': './assets/cn/raid/GORIZIA_RAID_NORMAL.png'}) +GORIZIA_RAID_HARD = Button(area={'cn': (1072, 348, 1118, 370), 'en': (1065, 352, 1116, 368), 'jp': (1056, 345, 1126, 371), 'tw': (1070, 346, 1120, 372)}, color={'cn': (169, 172, 162), 'en': (183, 185, 177), 'jp': (215, 217, 212), 'tw': (179, 182, 173)}, button={'cn': (1072, 348, 1118, 370), 'en': (1065, 352, 1116, 368), 'jp': (1056, 345, 1126, 371), 'tw': (1070, 346, 1120, 372)}, file={'cn': './assets/cn/raid/GORIZIA_RAID_HARD.png', 'en': './assets/en/raid/GORIZIA_RAID_HARD.png', 'jp': './assets/jp/raid/GORIZIA_RAID_HARD.png', 'tw': './assets/tw/raid/GORIZIA_RAID_HARD.png'}) +GORIZIA_RAID_NORMAL = Button(area={'cn': (1108, 430, 1153, 453), 'en': (1087, 435, 1163, 451), 'jp': (1105, 429, 1157, 454), 'tw': (1107, 429, 1155, 454)}, color={'cn': (177, 179, 171), 'en': (189, 191, 183), 'jp': (196, 198, 192), 'tw': (187, 189, 181)}, button={'cn': (1108, 430, 1153, 453), 'en': (1087, 435, 1163, 451), 'jp': (1105, 429, 1157, 454), 'tw': (1107, 429, 1155, 454)}, file={'cn': './assets/cn/raid/GORIZIA_RAID_NORMAL.png', 'en': './assets/en/raid/GORIZIA_RAID_NORMAL.png', 'jp': './assets/jp/raid/GORIZIA_RAID_NORMAL.png', 'tw': './assets/tw/raid/GORIZIA_RAID_NORMAL.png'}) HUANCHANG_OCR_PT = Button(area={'cn': (1166, 604, 1279, 635), 'en': (1166, 604, 1279, 635), 'jp': (1166, 604, 1279, 635), 'tw': (1166, 604, 1279, 635)}, color={'cn': (143, 143, 144), 'en': (143, 143, 144), 'jp': (143, 143, 144), 'tw': (143, 143, 144)}, button={'cn': (1166, 604, 1279, 635), 'en': (1166, 604, 1279, 635), 'jp': (1166, 604, 1279, 635), 'tw': (1166, 604, 1279, 635)}, file={'cn': './assets/cn/raid/HUANCHANG_OCR_PT.png', 'en': './assets/en/raid/HUANCHANG_OCR_PT.png', 'jp': './assets/jp/raid/HUANCHANG_OCR_PT.png', 'tw': './assets/cn/raid/HUANCHANG_OCR_PT.png'}) HUANCHANG_OCR_REMAIN_EASY = Button(area={'cn': (961, 548, 983, 567), 'en': (961, 522, 983, 540), 'jp': (961, 548, 983, 567), 'tw': (961, 548, 983, 567)}, color={'cn': (136, 134, 134), 'en': (143, 141, 140), 'jp': (136, 134, 134), 'tw': (136, 134, 134)}, button={'cn': (961, 548, 983, 567), 'en': (961, 522, 983, 540), 'jp': (961, 548, 983, 567), 'tw': (961, 548, 983, 567)}, file={'cn': './assets/cn/raid/HUANCHANG_OCR_REMAIN_EASY.png', 'en': './assets/en/raid/HUANCHANG_OCR_REMAIN_EASY.png', 'jp': './assets/jp/raid/HUANCHANG_OCR_REMAIN_EASY.png', 'tw': './assets/cn/raid/HUANCHANG_OCR_REMAIN_EASY.png'}) HUANCHANG_OCR_REMAIN_EX = Button(area={'cn': (1082, 16, 1151, 42), 'en': (1082, 16, 1151, 42), 'jp': (1082, 16, 1151, 42), 'tw': (1082, 16, 1151, 42)}, color={'cn': (54, 54, 54), 'en': (54, 54, 54), 'jp': (54, 54, 54), 'tw': (54, 54, 54)}, button={'cn': (1082, 16, 1151, 42), 'en': (1082, 16, 1151, 42), 'jp': (1082, 16, 1151, 42), 'tw': (1082, 16, 1151, 42)}, file={'cn': './assets/cn/raid/HUANCHANG_OCR_REMAIN_EX.png', 'en': './assets/en/raid/HUANCHANG_OCR_REMAIN_EX.png', 'jp': './assets/jp/raid/HUANCHANG_OCR_REMAIN_EX.png', 'tw': './assets/cn/raid/HUANCHANG_OCR_REMAIN_EX.png'}) @@ -59,6 +59,15 @@ KUYBYSHEY_RAID_HARD = Button(area={'cn': (1073, 345, 1125, 371), 'en': (1074, 34 KUYBYSHEY_RAID_NORMAL = Button(area={'cn': (1045, 423, 1097, 451), 'en': (1036, 424, 1099, 449), 'jp': (1048, 427, 1091, 448), 'tw': (1044, 423, 1096, 452)}, color={'cn': (86, 95, 109), 'en': (81, 92, 105), 'jp': (131, 143, 154), 'tw': (86, 95, 109)}, button={'cn': (1045, 423, 1097, 451), 'en': (1036, 424, 1099, 449), 'jp': (1048, 427, 1091, 448), 'tw': (1044, 423, 1096, 452)}, file={'cn': './assets/cn/raid/KUYBYSHEY_RAID_NORMAL.png', 'en': './assets/en/raid/KUYBYSHEY_RAID_NORMAL.png', 'jp': './assets/jp/raid/KUYBYSHEY_RAID_NORMAL.png', 'tw': './assets/tw/raid/KUYBYSHEY_RAID_NORMAL.png'}) RAID_FLEET_PREPARATION = Button(area={'cn': (983, 577, 1181, 638), 'en': (1041, 592, 1121, 631), 'jp': (983, 579, 1180, 635), 'tw': (983, 577, 1181, 638)}, color={'cn': (236, 188, 115), 'en': (236, 184, 117), 'jp': (235, 183, 103), 'tw': (236, 188, 115)}, button={'cn': (983, 577, 1181, 638), 'en': (1041, 592, 1121, 631), 'jp': (983, 579, 1180, 635), 'tw': (983, 577, 1181, 638)}, file={'cn': './assets/cn/raid/RAID_FLEET_PREPARATION.png', 'en': './assets/en/raid/RAID_FLEET_PREPARATION.png', 'jp': './assets/jp/raid/RAID_FLEET_PREPARATION.png', 'tw': './assets/tw/raid/RAID_FLEET_PREPARATION.png'}) RAID_REWARDS = Button(area={'cn': (836, 127, 900, 169), 'en': (836, 127, 900, 169), 'jp': (836, 127, 900, 169), 'tw': (836, 127, 900, 169)}, color={'cn': (217, 103, 98), 'en': (217, 103, 98), 'jp': (217, 103, 98), 'tw': (217, 103, 98)}, button={'cn': (836, 127, 900, 169), 'en': (836, 127, 900, 169), 'jp': (836, 127, 900, 169), 'tw': (836, 127, 900, 169)}, file={'cn': './assets/cn/raid/RAID_REWARDS.png', 'en': './assets/en/raid/RAID_REWARDS.png', 'jp': './assets/jp/raid/RAID_REWARDS.png', 'tw': './assets/tw/raid/RAID_REWARDS.png'}) +RPG_GOTO_STAGE = Button(area={'cn': (55, 495, 80, 520), 'en': (55, 495, 80, 520), 'jp': (55, 495, 80, 520), 'tw': (55, 495, 80, 520)}, color={'cn': (174, 168, 160), 'en': (174, 168, 160), 'jp': (174, 168, 160), 'tw': (174, 168, 160)}, button={'cn': (55, 495, 80, 520), 'en': (55, 495, 80, 520), 'jp': (55, 495, 80, 520), 'tw': (55, 495, 80, 520)}, file={'cn': './assets/cn/raid/RPG_GOTO_STAGE.png', 'en': './assets/en/raid/RPG_GOTO_STAGE.png', 'jp': './assets/jp/raid/RPG_GOTO_STAGE.png', 'tw': './assets/tw/raid/RPG_GOTO_STAGE.png'}) +RPG_GOTO_STORY = Button(area={'cn': (59, 491, 84, 516), 'en': (59, 491, 84, 516), 'jp': (59, 491, 84, 516), 'tw': (59, 491, 84, 516)}, color={'cn': (182, 122, 105), 'en': (182, 122, 105), 'jp': (182, 122, 105), 'tw': (182, 122, 105)}, button={'cn': (59, 491, 84, 516), 'en': (59, 491, 84, 516), 'jp': (59, 491, 84, 516), 'tw': (59, 491, 84, 516)}, file={'cn': './assets/cn/raid/RPG_GOTO_STORY.png', 'en': './assets/en/raid/RPG_GOTO_STORY.png', 'jp': './assets/jp/raid/RPG_GOTO_STORY.png', 'tw': './assets/tw/raid/RPG_GOTO_STORY.png'}) +RPG_HOME = Button(area={'cn': (1222, 29, 1240, 51), 'en': (1222, 29, 1240, 51), 'jp': (1222, 29, 1240, 51), 'tw': (1222, 29, 1240, 51)}, color={'cn': (197, 181, 158), 'en': (197, 181, 158), 'jp': (197, 181, 158), 'tw': (197, 181, 158)}, button={'cn': (1222, 29, 1240, 51), 'en': (1222, 29, 1240, 51), 'jp': (1222, 29, 1240, 51), 'tw': (1222, 29, 1240, 51)}, file={'cn': './assets/cn/raid/RPG_HOME.png', 'en': './assets/en/raid/RPG_HOME.png', 'jp': './assets/jp/raid/RPG_HOME.png', 'tw': './assets/tw/raid/RPG_HOME.png'}) +RPG_LEAVE_CITY = Button(area={'cn': (688, 642, 711, 664), 'en': (688, 642, 711, 664), 'jp': (688, 642, 711, 664), 'tw': (688, 642, 711, 664)}, color={'cn': (158, 130, 109), 'en': (158, 130, 109), 'jp': (158, 130, 109), 'tw': (158, 130, 109)}, button={'cn': (688, 642, 711, 664), 'en': (688, 642, 711, 664), 'jp': (688, 642, 711, 664), 'tw': (688, 642, 711, 664)}, file={'cn': './assets/cn/raid/RPG_LEAVE_CITY.png', 'en': './assets/en/raid/RPG_LEAVE_CITY.png', 'jp': './assets/jp/raid/RPG_LEAVE_CITY.png', 'tw': './assets/tw/raid/RPG_LEAVE_CITY.png'}) +RPG_RAID_EASY = Button(area={'cn': (149, 561, 179, 591), 'en': (149, 561, 179, 591), 'jp': (149, 561, 179, 591), 'tw': (149, 561, 179, 591)}, color={'cn': (152, 57, 59), 'en': (152, 57, 59), 'jp': (152, 57, 59), 'tw': (152, 57, 59)}, button={'cn': (149, 561, 179, 591), 'en': (149, 561, 179, 591), 'jp': (149, 561, 179, 591), 'tw': (149, 561, 179, 591)}, file={'cn': './assets/cn/raid/RPG_RAID_EASY.png', 'en': './assets/en/raid/RPG_RAID_EASY.png', 'jp': './assets/jp/raid/RPG_RAID_EASY.png', 'tw': './assets/tw/raid/RPG_RAID_EASY.png'}) +RPG_RAID_EX = Button(area={'cn': (949, 518, 976, 565), 'en': (949, 518, 976, 565), 'jp': (949, 518, 976, 565), 'tw': (949, 518, 976, 565)}, color={'cn': (166, 66, 69), 'en': (166, 66, 69), 'jp': (166, 66, 69), 'tw': (166, 66, 69)}, button={'cn': (949, 518, 976, 565), 'en': (949, 518, 976, 565), 'jp': (949, 518, 976, 565), 'tw': (949, 518, 976, 565)}, file={'cn': './assets/cn/raid/RPG_RAID_EX.png', 'en': './assets/en/raid/RPG_RAID_EX.png', 'jp': './assets/jp/raid/RPG_RAID_EX.png', 'tw': './assets/tw/raid/RPG_RAID_EX.png'}) +RPG_RAID_HARD = Button(area={'cn': (475, 108, 505, 138), 'en': (475, 108, 505, 138), 'jp': (475, 108, 505, 138), 'tw': (475, 108, 505, 138)}, color={'cn': (97, 59, 59), 'en': (97, 59, 59), 'jp': (97, 59, 59), 'tw': (97, 59, 59)}, button={'cn': (475, 108, 505, 138), 'en': (475, 108, 505, 138), 'jp': (475, 108, 505, 138), 'tw': (475, 108, 505, 138)}, file={'cn': './assets/cn/raid/RPG_RAID_HARD.png', 'en': './assets/en/raid/RPG_RAID_HARD.png', 'jp': './assets/jp/raid/RPG_RAID_HARD.png', 'tw': './assets/tw/raid/RPG_RAID_HARD.png'}) +RPG_RAID_NORMAL = Button(area={'cn': (313, 259, 343, 289), 'en': (313, 259, 343, 289), 'jp': (313, 259, 343, 289), 'tw': (313, 259, 343, 289)}, color={'cn': (147, 61, 62), 'en': (147, 61, 62), 'jp': (147, 61, 62), 'tw': (147, 61, 62)}, button={'cn': (313, 259, 343, 289), 'en': (313, 259, 343, 289), 'jp': (313, 259, 343, 289), 'tw': (313, 259, 343, 289)}, file={'cn': './assets/cn/raid/RPG_RAID_NORMAL.png', 'en': './assets/en/raid/RPG_RAID_NORMAL.png', 'jp': './assets/jp/raid/RPG_RAID_NORMAL.png', 'tw': './assets/tw/raid/RPG_RAID_NORMAL.png'}) +RPG_STATUS_POPUP = Button(area={'cn': (1120, 97, 1144, 121), 'en': (1120, 97, 1144, 121), 'jp': (1120, 97, 1144, 121), 'tw': (1120, 97, 1144, 121)}, color={'cn': (158, 165, 176), 'en': (158, 165, 176), 'jp': (158, 165, 176), 'tw': (158, 165, 176)}, button={'cn': (1120, 97, 1144, 121), 'en': (1120, 97, 1144, 121), 'jp': (1120, 97, 1144, 121), 'tw': (1120, 97, 1144, 121)}, file={'cn': './assets/cn/raid/RPG_STATUS_POPUP.png', 'en': './assets/en/raid/RPG_STATUS_POPUP.png', 'jp': './assets/jp/raid/RPG_STATUS_POPUP.png', 'tw': './assets/tw/raid/RPG_STATUS_POPUP.png'}) SURUGA_OCR_REMAIN_EASY = Button(area={'cn': (1093, 549, 1141, 563), 'en': (1093, 549, 1141, 563), 'jp': (1096, 549, 1141, 563), 'tw': (1096, 549, 1141, 563)}, color={'cn': (161, 161, 161), 'en': (161, 161, 161), 'jp': (155, 155, 155), 'tw': (155, 155, 155)}, button={'cn': (1093, 549, 1141, 563), 'en': (1093, 549, 1141, 563), 'jp': (1096, 549, 1141, 563), 'tw': (1096, 549, 1141, 563)}, file={'cn': './assets/cn/raid/SURUGA_OCR_REMAIN_EASY.png', 'en': './assets/en/raid/SURUGA_OCR_REMAIN_EASY.png', 'jp': './assets/jp/raid/SURUGA_OCR_REMAIN_EASY.png', 'tw': './assets/tw/raid/SURUGA_OCR_REMAIN_EASY.png'}) SURUGA_OCR_REMAIN_HARD = Button(area={'cn': (1071, 318, 1118, 332), 'en': (1071, 318, 1118, 332), 'jp': (1073, 318, 1118, 331), 'tw': (1079, 318, 1118, 332)}, color={'cn': (158, 158, 159), 'en': (158, 158, 159), 'jp': (173, 173, 173), 'tw': (168, 168, 168)}, button={'cn': (1071, 318, 1118, 332), 'en': (1071, 318, 1118, 332), 'jp': (1073, 318, 1118, 331), 'tw': (1079, 318, 1118, 332)}, file={'cn': './assets/cn/raid/SURUGA_OCR_REMAIN_HARD.png', 'en': './assets/en/raid/SURUGA_OCR_REMAIN_HARD.png', 'jp': './assets/jp/raid/SURUGA_OCR_REMAIN_HARD.png', 'tw': './assets/tw/raid/SURUGA_OCR_REMAIN_HARD.png'}) SURUGA_OCR_REMAIN_NORMAL = Button(area={'cn': (1137, 426, 1185, 439), 'en': (1137, 426, 1185, 439), 'jp': (1140, 426, 1185, 439), 'tw': (1140, 426, 1185, 439)}, color={'cn': (164, 164, 164), 'en': (164, 164, 164), 'jp': (158, 158, 158), 'tw': (158, 158, 158)}, button={'cn': (1137, 426, 1185, 439), 'en': (1137, 426, 1185, 439), 'jp': (1140, 426, 1185, 439), 'tw': (1140, 426, 1185, 439)}, file={'cn': './assets/cn/raid/SURUGA_OCR_REMAIN_NORMAL.png', 'en': './assets/en/raid/SURUGA_OCR_REMAIN_NORMAL.png', 'jp': './assets/jp/raid/SURUGA_OCR_REMAIN_NORMAL.png', 'tw': './assets/tw/raid/SURUGA_OCR_REMAIN_NORMAL.png'}) diff --git a/module/raid/daily.py b/module/raid/daily.py index 4510d29d8..8dda1bdf6 100644 --- a/module/raid/daily.py +++ b/module/raid/daily.py @@ -25,6 +25,11 @@ class RaidDaily(RaidRun): Args: name (str): Raid name, such as 'raid_20200624' """ + if self.is_raid_rpg(): + logger.info('RPG raid has no dailies') + self.config.Scheduler_Enable = False + self.config.task_stop() + name = name if name else self.config.Campaign_Event stages = [RaidStage(name) for name in STAGES] STAGE_FILTER.load(self.config.RaidDaily_StageFilter) diff --git a/module/raid/raid.py b/module/raid/raid.py index be405dd4a..5b27ea51d 100644 --- a/module/raid/raid.py +++ b/module/raid/raid.py @@ -13,6 +13,7 @@ from module.ocr.ocr import Digit, DigitCounter from module.raid.assets import * from module.raid.combat import RaidCombat from module.ui.assets import RAID_CHECK +from module.ui.page import page_rpg_stage class OilExhausted(Exception): @@ -31,6 +32,7 @@ class HuanChangCounter(Digit): The limit on number of raid event "Spring Festive Fiasco" is vertical, Ocr numbers on the top half. """ + def ocr(self, image, direct_ocr=False): result = super().ocr(image, direct_ocr) return (result, 0, 15) @@ -51,7 +53,8 @@ class HuanChangPtOcr(Digit): # Calculate connected area, greater than 60 is considered a number, # CN, JP background rightmost is connected but EN is not, # EN need judge both [0, -1] and [-1, -1] - num_idx = [i for i in range(1, count + 1) if i != cc[0, -1] and i != cc[-1, -1] and np.count_nonzero(cc == i) > 60] + num_idx = [i for i in range(1, count + 1) if + i != cc[0, -1] and i != cc[-1, -1] and np.count_nonzero(cc == i) > 60] image = ~(np.isin(cc, num_idx) * 255) # Numbers are white, need invert return image.astype(np.uint8) @@ -80,6 +83,8 @@ def raid_name_shorten(name): return "GORIZIA" elif name == "raid_20240130": return "HUANCHANG" + elif name == "raid_20240328": + return "RPG" else: raise ScriptError(f'Unknown raid name: {name}') @@ -287,7 +292,10 @@ class Raid(MapOperation, RaidCombat, CampaignEvent): def raid_expected_end(self): if self.appear_then_click(RAID_REWARDS, offset=(30, 30), interval=3): return False - return self.appear(RAID_CHECK, offset=(30, 30)) + if self.is_raid_rpg(): + return self.appear(page_rpg_stage.check_button, offset=(30, 30)) + else: + return self.appear(RAID_CHECK, offset=(30, 30)) def raid_execute_once(self, mode, raid): """ @@ -355,3 +363,31 @@ class Raid(MapOperation, RaidCombat, CampaignEvent): else: logger.info(f'Raid {self.config.Campaign_Event} does not support PT ocr, skip') return 0 + + def is_raid_rpg(self): + return self.config.Campaign_Event == 'raid_20240328' + + def raid_rpg_swipe(self, skip_first_screenshot=True): + """ + Swipe til the rightmost in RPG raid (raid_20240328) + """ + interval = Timer(1) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.appear(RPG_RAID_EASY, offset=(10, 10)): + logger.info('RPG raid already at rightmost') + break + + if self.handle_story_skip(): + continue + if self.handle_get_items(): + continue + if interval.reached(): + self.device.swipe_vector((-900, 0), box=(0, 130, 1280, 440)) + interval.reset() + continue diff --git a/module/raid/run.py b/module/raid/run.py index 4daa93820..0bed97141 100644 --- a/module/raid/run.py +++ b/module/raid/run.py @@ -4,7 +4,7 @@ from module.exception import ScriptEnd, ScriptError from module.logger import logger from module.raid.assets import RAID_REWARDS from module.raid.raid import OilExhausted, Raid, raid_ocr -from module.ui.page import page_raid +from module.ui.page import page_raid, page_rpg_stage class RaidRun(Raid, CampaignEvent): @@ -100,7 +100,11 @@ class RaidRun(Raid, CampaignEvent): # UI ensure self.device.stuck_record_clear() self.device.click_record_clear() - self.ui_ensure(page_raid) + if not self.is_raid_rpg(): + self.ui_ensure(page_raid) + else: + self.ui_ensure(page_rpg_stage) + self.raid_rpg_swipe() # End for mode EX if mode == 'ex': diff --git a/module/retire/assets.py b/module/retire/assets.py index a10ccadd5..c5820aebb 100644 --- a/module/retire/assets.py +++ b/module/retire/assets.py @@ -42,13 +42,19 @@ SORTING_CLICK = Button(area={'cn': (1004, 14, 1096, 42), 'en': (1002, 12, 1058, SORT_ASC = Button(area={'cn': (1014, 22, 1019, 26), 'en': (1014, 22, 1019, 26), 'jp': (1013, 23, 1019, 25), 'tw': (1014, 22, 1019, 26)}, color={'cn': (189, 207, 231), 'en': (189, 207, 231), 'jp': (189, 207, 231), 'tw': (189, 207, 231)}, button={'cn': (1014, 22, 1019, 26), 'en': (1014, 22, 1019, 26), 'jp': (1013, 23, 1019, 25), 'tw': (1014, 22, 1019, 26)}, file={'cn': './assets/cn/retire/SORT_ASC.png', 'en': './assets/en/retire/SORT_ASC.png', 'jp': './assets/jp/retire/SORT_ASC.png', 'tw': './assets/tw/retire/SORT_ASC.png'}) SORT_DESC = Button(area={'cn': (1014, 29, 1019, 33), 'en': (1014, 29, 1019, 33), 'jp': (1013, 29, 1019, 32), 'tw': (1014, 29, 1019, 33)}, color={'cn': (189, 207, 231), 'en': (189, 207, 231), 'jp': (189, 207, 231), 'tw': (189, 207, 231)}, button={'cn': (1014, 29, 1019, 33), 'en': (1014, 29, 1019, 33), 'jp': (1013, 29, 1019, 32), 'tw': (1014, 29, 1019, 33)}, file={'cn': './assets/cn/retire/SORT_DESC.png', 'en': './assets/en/retire/SORT_DESC.png', 'jp': './assets/jp/retire/SORT_DESC.png', 'tw': './assets/tw/retire/SORT_DESC.png'}) SR_SSR_CONFIRM = Button(area={'cn': (757, 480, 829, 511), 'en': (706, 463, 881, 523), 'jp': (723, 470, 870, 509), 'tw': (757, 473, 829, 504)}, color={'cn': (150, 181, 221), 'en': (106, 152, 208), 'jp': (101, 146, 203), 'tw': (148, 180, 221)}, button={'cn': (757, 480, 829, 511), 'en': (706, 463, 881, 523), 'jp': (723, 470, 870, 509), 'tw': (757, 473, 829, 504)}, file={'cn': './assets/cn/retire/SR_SSR_CONFIRM.png', 'en': './assets/en/retire/SR_SSR_CONFIRM.png', 'jp': './assets/jp/retire/SR_SSR_CONFIRM.png', 'tw': './assets/tw/retire/SR_SSR_CONFIRM.png'}) +TEMPLATE_AULICK = Template(file={'cn': './assets/cn/retire/TEMPLATE_AULICK.png', 'en': './assets/en/retire/TEMPLATE_AULICK.png', 'jp': './assets/jp/retire/TEMPLATE_AULICK.png', 'tw': './assets/tw/retire/TEMPLATE_AULICK.png'}) TEMPLATE_BOGUE = Template(file={'cn': './assets/cn/retire/TEMPLATE_BOGUE.png', 'en': './assets/en/retire/TEMPLATE_BOGUE.png', 'jp': './assets/jp/retire/TEMPLATE_BOGUE.png', 'tw': './assets/tw/retire/TEMPLATE_BOGUE.png'}) +TEMPLATE_CASSIN_1 = Template(file={'cn': './assets/cn/retire/TEMPLATE_CASSIN_1.png', 'en': './assets/en/retire/TEMPLATE_CASSIN_1.png', 'jp': './assets/jp/retire/TEMPLATE_CASSIN_1.png', 'tw': './assets/tw/retire/TEMPLATE_CASSIN_1.png'}) +TEMPLATE_CASSIN_2 = Template(file={'cn': './assets/cn/retire/TEMPLATE_CASSIN_2.png', 'en': './assets/en/retire/TEMPLATE_CASSIN_2.png', 'jp': './assets/jp/retire/TEMPLATE_CASSIN_2.png', 'tw': './assets/tw/retire/TEMPLATE_CASSIN_2.png'}) +TEMPLATE_DOWNES_1 = Template(file={'cn': './assets/cn/retire/TEMPLATE_DOWNES_1.png', 'en': './assets/en/retire/TEMPLATE_DOWNES_1.png', 'jp': './assets/jp/retire/TEMPLATE_DOWNES_1.png', 'tw': './assets/tw/retire/TEMPLATE_DOWNES_1.png'}) +TEMPLATE_DOWNES_2 = Template(file={'cn': './assets/cn/retire/TEMPLATE_DOWNES_2.png', 'en': './assets/en/retire/TEMPLATE_DOWNES_2.png', 'jp': './assets/jp/retire/TEMPLATE_DOWNES_2.png', 'tw': './assets/tw/retire/TEMPLATE_DOWNES_2.png'}) TEMPLATE_FLEET_1 = Template(file={'cn': './assets/cn/retire/TEMPLATE_FLEET_1.png', 'en': './assets/en/retire/TEMPLATE_FLEET_1.png', 'jp': './assets/jp/retire/TEMPLATE_FLEET_1.png', 'tw': './assets/tw/retire/TEMPLATE_FLEET_1.png'}) TEMPLATE_FLEET_2 = Template(file={'cn': './assets/cn/retire/TEMPLATE_FLEET_2.png', 'en': './assets/en/retire/TEMPLATE_FLEET_2.png', 'jp': './assets/jp/retire/TEMPLATE_FLEET_2.png', 'tw': './assets/tw/retire/TEMPLATE_FLEET_2.png'}) TEMPLATE_FLEET_3 = Template(file={'cn': './assets/cn/retire/TEMPLATE_FLEET_3.png', 'en': './assets/en/retire/TEMPLATE_FLEET_3.png', 'jp': './assets/jp/retire/TEMPLATE_FLEET_3.png', 'tw': './assets/tw/retire/TEMPLATE_FLEET_3.png'}) TEMPLATE_FLEET_4 = Template(file={'cn': './assets/cn/retire/TEMPLATE_FLEET_4.png', 'en': './assets/en/retire/TEMPLATE_FLEET_4.png', 'jp': './assets/jp/retire/TEMPLATE_FLEET_4.png', 'tw': './assets/tw/retire/TEMPLATE_FLEET_4.png'}) TEMPLATE_FLEET_5 = Template(file={'cn': './assets/cn/retire/TEMPLATE_FLEET_5.png', 'en': './assets/en/retire/TEMPLATE_FLEET_5.png', 'jp': './assets/jp/retire/TEMPLATE_FLEET_5.png', 'tw': './assets/tw/retire/TEMPLATE_FLEET_5.png'}) TEMPLATE_FLEET_6 = Template(file={'cn': './assets/cn/retire/TEMPLATE_FLEET_6.png', 'en': './assets/en/retire/TEMPLATE_FLEET_6.png', 'jp': './assets/jp/retire/TEMPLATE_FLEET_6.png', 'tw': './assets/tw/retire/TEMPLATE_FLEET_6.png'}) +TEMPLATE_FOOTE = Template(file={'cn': './assets/cn/retire/TEMPLATE_FOOTE.png', 'en': './assets/en/retire/TEMPLATE_FOOTE.png', 'jp': './assets/jp/retire/TEMPLATE_FOOTE.png', 'tw': './assets/tw/retire/TEMPLATE_FOOTE.png'}) TEMPLATE_HERMES = Template(file={'cn': './assets/cn/retire/TEMPLATE_HERMES.png', 'en': './assets/en/retire/TEMPLATE_HERMES.png', 'jp': './assets/jp/retire/TEMPLATE_HERMES.png', 'tw': './assets/tw/retire/TEMPLATE_HERMES.png'}) TEMPLATE_IN_BATTLE = Template(file={'cn': './assets/cn/retire/TEMPLATE_IN_BATTLE.png', 'en': './assets/en/retire/TEMPLATE_IN_BATTLE.png', 'jp': './assets/jp/retire/TEMPLATE_IN_BATTLE.png', 'tw': './assets/tw/retire/TEMPLATE_IN_BATTLE.png'}) TEMPLATE_IN_COMMISSION = Template(file={'cn': './assets/cn/retire/TEMPLATE_IN_COMMISSION.png', 'en': './assets/en/retire/TEMPLATE_IN_COMMISSION.png', 'jp': './assets/jp/retire/TEMPLATE_IN_COMMISSION.png', 'tw': './assets/tw/retire/TEMPLATE_IN_COMMISSION.png'}) 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/tactical/tactical_class.py b/module/tactical/tactical_class.py index fed15206e..01cf62c30 100644 --- a/module/tactical/tactical_class.py +++ b/module/tactical/tactical_class.py @@ -7,7 +7,7 @@ from module.base.timer import Timer from module.base.utils import * from module.combat.level import LevelOcr from module.exception import ScriptError -from module.handler.assets import GET_MISSION, POPUP_CANCEL, POPUP_CONFIRM +from module.handler.assets import GET_MISSION, MISSION_POPUP_ACK, MISSION_POPUP_GO, POPUP_CANCEL, POPUP_CONFIRM from module.logger import logger from module.map.map_grids import SelectedGrids from module.ocr.ocr import DigitCounter, Duration, Ocr @@ -445,6 +445,10 @@ class RewardTacticalClass(Dock): continue if self.ui_page_main_popups(): continue + # Similar to handle_mission_popup_ack, but battle pass item expire popup has a different ACK button + if self.appear(MISSION_POPUP_GO, offset=self._popup_offset, interval=2): + self.device.click(MISSION_POPUP_ACK) + continue if self.appear(TACTICAL_CLASS_CANCEL, offset=(30, 30), interval=2) \ and self.appear(TACTICAL_CLASS_START, offset=(30, 30)): if self._tactical_books_choose(): diff --git a/module/template/assets.py b/module/template/assets.py index 27c3d4721..3604cee76 100644 --- a/module/template/assets.py +++ b/module/template/assets.py @@ -212,6 +212,7 @@ TEMPLATE_SIREN_Zuikaku = Template(file={'cn': './assets/cn/template/TEMPLATE_SIR 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'}) +TEMPLATE_STAGE_CLEAR_SMALL = Template(file={'cn': './assets/cn/template/TEMPLATE_STAGE_CLEAR_SMALL.png', 'en': './assets/cn/template/TEMPLATE_STAGE_CLEAR_SMALL.png', 'jp': './assets/cn/template/TEMPLATE_STAGE_CLEAR_SMALL.png', 'tw': './assets/cn/template/TEMPLATE_STAGE_CLEAR_SMALL.png'}) TEMPLATE_STAGE_GREEN_CLEAR = Template(file={'cn': './assets/cn/template/TEMPLATE_STAGE_GREEN_CLEAR.png', 'en': './assets/en/template/TEMPLATE_STAGE_GREEN_CLEAR.png', 'jp': './assets/jp/template/TEMPLATE_STAGE_GREEN_CLEAR.png', 'tw': './assets/tw/template/TEMPLATE_STAGE_GREEN_CLEAR.png'}) TEMPLATE_STAGE_HALF_PERCENT = Template(file={'cn': './assets/cn/template/TEMPLATE_STAGE_HALF_PERCENT.png', 'en': './assets/en/template/TEMPLATE_STAGE_HALF_PERCENT.png', 'jp': './assets/jp/template/TEMPLATE_STAGE_HALF_PERCENT.png', 'tw': './assets/tw/template/TEMPLATE_STAGE_HALF_PERCENT.png'}) TEMPLATE_STAGE_PERCENT = Template(file={'cn': './assets/cn/template/TEMPLATE_STAGE_PERCENT.png', 'en': './assets/en/template/TEMPLATE_STAGE_PERCENT.png', 'jp': './assets/jp/template/TEMPLATE_STAGE_PERCENT.png', 'tw': './assets/tw/template/TEMPLATE_STAGE_PERCENT.png'}) diff --git a/module/ui/assets.py b/module/ui/assets.py index e3b907738..127144e52 100644 --- a/module/ui/assets.py +++ b/module/ui/assets.py @@ -64,7 +64,7 @@ MISSION_CHECK = Button(area={'cn': (120, 15, 173, 40), 'en': (123, 18, 221, 37), MUNITIONS_CHECK = Button(area={'cn': (32, 621, 86, 647), 'en': (25, 622, 85, 644), 'jp': (23, 625, 80, 645), 'tw': (31, 619, 88, 649)}, color={'cn': (151, 147, 147), 'en': (174, 171, 171), 'jp': (99, 91, 91), 'tw': (127, 121, 121)}, button={'cn': (32, 621, 86, 647), 'en': (25, 622, 85, 644), 'jp': (23, 625, 80, 645), 'tw': (31, 619, 88, 649)}, file={'cn': './assets/cn/ui/MUNITIONS_CHECK.png', 'en': './assets/en/ui/MUNITIONS_CHECK.png', 'jp': './assets/jp/ui/MUNITIONS_CHECK.png', 'tw': './assets/tw/ui/MUNITIONS_CHECK.png'}) OS_CHECK = Button(area={'cn': (613, 17, 627, 34), 'en': (613, 17, 627, 34), 'jp': (613, 17, 627, 34), 'tw': (613, 17, 627, 34)}, color={'cn': (58, 117, 146), 'en': (58, 117, 146), 'jp': (58, 117, 146), 'tw': (58, 117, 146)}, button={'cn': (613, 17, 627, 34), 'en': (613, 17, 627, 34), 'jp': (613, 17, 627, 34), 'tw': (613, 17, 627, 34)}, file={'cn': './assets/cn/ui/OS_CHECK.png', 'en': './assets/en/ui/OS_CHECK.png', 'jp': './assets/jp/ui/OS_CHECK.png', 'tw': './assets/tw/ui/OS_CHECK.png'}) PLAYER_CHECK = Button(area={'cn': (28, 668, 139, 688), 'en': (11, 649, 157, 705), 'jp': (26, 668, 139, 689), 'tw': (28, 668, 139, 688)}, color={'cn': (237, 204, 127), 'en': (197, 156, 97), 'jp': (237, 205, 128), 'tw': (237, 204, 127)}, button={'cn': (28, 668, 139, 688), 'en': (11, 649, 157, 705), 'jp': (26, 668, 139, 689), 'tw': (28, 668, 139, 688)}, file={'cn': './assets/cn/ui/PLAYER_CHECK.png', 'en': './assets/en/ui/PLAYER_CHECK.png', 'jp': './assets/jp/ui/PLAYER_CHECK.png', 'tw': './assets/tw/ui/PLAYER_CHECK.png'}) -RAID_CHECK = Button(area={'cn': (107, 13, 216, 38), 'en': (107, 15, 188, 34), 'jp': (107, 13, 217, 40), 'tw': (114, 11, 229, 42)}, color={'cn': (129, 131, 129), 'en': (85, 87, 85), 'jp': (127, 129, 127), 'tw': (126, 140, 178)}, button={'cn': (107, 13, 216, 38), 'en': (107, 15, 188, 34), 'jp': (107, 13, 217, 40), 'tw': (114, 11, 229, 42)}, file={'cn': './assets/cn/ui/RAID_CHECK.png', 'en': './assets/en/ui/RAID_CHECK.png', 'jp': './assets/jp/ui/RAID_CHECK.png', 'tw': './assets/tw/ui/RAID_CHECK.png'}) +RAID_CHECK = Button(area={'cn': (107, 13, 216, 38), 'en': (107, 15, 188, 34), 'jp': (107, 13, 217, 40), 'tw': (111, 9, 228, 42)}, color={'cn': (129, 131, 129), 'en': (85, 87, 85), 'jp': (127, 129, 127), 'tw': (132, 154, 140)}, button={'cn': (107, 13, 216, 38), 'en': (107, 15, 188, 34), 'jp': (107, 13, 217, 40), 'tw': (111, 9, 228, 42)}, file={'cn': './assets/cn/ui/RAID_CHECK.png', 'en': './assets/en/ui/RAID_CHECK.png', 'jp': './assets/jp/ui/RAID_CHECK.png', 'tw': './assets/tw/ui/RAID_CHECK.png'}) RESEARCH_CHECK = Button(area={'cn': (118, 15, 170, 39), 'en': (119, 14, 259, 36), 'jp': (117, 14, 171, 40), 'tw': (117, 13, 172, 40)}, color={'cn': (165, 179, 215), 'en': (118, 133, 174), 'jp': (135, 154, 195), 'tw': (148, 165, 205)}, button={'cn': (118, 15, 170, 39), 'en': (119, 14, 259, 36), 'jp': (117, 14, 171, 40), 'tw': (117, 13, 172, 40)}, file={'cn': './assets/cn/ui/RESEARCH_CHECK.png', 'en': './assets/en/ui/RESEARCH_CHECK.png', 'jp': './assets/jp/ui/RESEARCH_CHECK.png', 'tw': './assets/tw/ui/RESEARCH_CHECK.png'}) RESHMENU_CHECK = Button(area={'cn': (121, 15, 174, 39), 'en': (118, 14, 279, 35), 'jp': (116, 13, 174, 42), 'tw': (121, 14, 175, 40)}, color={'cn': (156, 171, 209), 'en': (100, 113, 152), 'jp': (136, 149, 186), 'tw': (147, 162, 201)}, button={'cn': (121, 15, 174, 39), 'en': (118, 14, 279, 35), 'jp': (116, 13, 174, 42), 'tw': (121, 14, 175, 40)}, file={'cn': './assets/cn/ui/RESHMENU_CHECK.png', 'en': './assets/en/ui/RESHMENU_CHECK.png', 'jp': './assets/jp/ui/RESHMENU_CHECK.png', 'tw': './assets/tw/ui/RESHMENU_CHECK.png'}) RESHMENU_GOTO_META = Button(area={'cn': (1076, 254, 1155, 334), 'en': (1102, 251, 1193, 332), 'jp': (1076, 254, 1155, 334), 'tw': (1076, 254, 1155, 334)}, color={'cn': (144, 156, 183), 'en': (141, 152, 181), 'jp': (144, 156, 183), 'tw': (144, 156, 183)}, button={'cn': (1076, 254, 1155, 334), 'en': (1102, 251, 1193, 332), 'jp': (1076, 254, 1155, 334), 'tw': (1076, 254, 1155, 334)}, file={'cn': './assets/cn/ui/RESHMENU_GOTO_META.png', 'en': './assets/en/ui/RESHMENU_GOTO_META.png', 'jp': './assets/jp/ui/RESHMENU_GOTO_META.png', 'tw': './assets/cn/ui/RESHMENU_GOTO_META.png'}) diff --git a/module/ui/page.py b/module/ui/page.py index f42090ca8..d07ad0f1e 100644 --- a/module/ui/page.py +++ b/module/ui/page.py @@ -1,6 +1,7 @@ import traceback from module.ui.assets import * +from module.raid.assets import * MAIN_CHECK = MAIN_GOTO_CAMPAIGN @@ -248,3 +249,17 @@ page_supply_pack.link(button=GOTO_MAIN, destination=page_main) page_build = Page(BUILD_CHECK) page_main.link(button=MAIN_GOTO_BUILD, destination=page_build) page_build.link(button=GOTO_MAIN, destination=page_main) + +# RPG event (raid_20240328) +page_rpg_stage = Page(RPG_GOTO_STORY) +page_rpg_story = Page(RPG_GOTO_STAGE) +page_rpg_stage.link(button=RPG_GOTO_STORY, destination=page_rpg_story) +page_rpg_stage.link(button=RPG_HOME, destination=page_main) +page_rpg_story.link(button=RPG_GOTO_STAGE, destination=page_rpg_stage) +page_rpg_story.link(button=RPG_HOME, destination=page_main) + +page_main.link(button=MAIN_GOTO_RAID, destination=page_rpg_stage) + +page_rpg_city = Page(RPG_LEAVE_CITY) +page_rpg_city.link(button=RPG_LEAVE_CITY, destination=page_rpg_stage) +page_rpg_city.link(button=RPG_HOME, destination=page_main) diff --git a/module/ui/ui.py b/module/ui/ui.py index dc66e1918..0c6288494 100644 --- a/module/ui/ui.py +++ b/module/ui/ui.py @@ -3,14 +3,14 @@ from module.base.decorator import run_once from module.base.timer import Timer from module.coalition.assets import FLEET_PREPARATION as COALITION_FLEET_PREPARATION from module.combat.assets import GET_ITEMS_1, GET_ITEMS_2, GET_SHIP +from module.raid.assets import * from module.exception import (GameNotRunningError, GamePageUnknownError, RequestHumanTakeover) from module.exercise.assets import EXERCISE_PREPARATION from module.freebies.assets import PURCHASE_POPUP -from module.handler.assets import (AUTO_SEARCH_MENU_EXIT, BATTLE_PASS_NOTICE, - GAME_TIPS, LOGIN_ANNOUNCE, - LOGIN_CHECK, LOGIN_RETURN_SIGN, - MAINTENANCE_ANNOUNCE, MONTHLY_PASS_NOTICE) +from module.handler.assets import (AUTO_SEARCH_MENU_EXIT, BATTLE_PASS_NOTICE, GAME_TIPS, LOGIN_ANNOUNCE, + LOGIN_ANNOUNCE_2, LOGIN_CHECK, LOGIN_RETURN_SIGN, MAINTENANCE_ANNOUNCE, + MONTHLY_PASS_NOTICE) from module.handler.info_handler import InfoHandler from module.logger import logger from module.map.assets import (FLEET_PREPARATION, MAP_PREPARATION, @@ -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() @@ -166,7 +166,13 @@ class UI(InfoHandler): # Unknown page but able to handle logger.info("Unknown ui page") - if self.appear_then_click(GOTO_MAIN, offset=(30, 30), interval=2) or self.ui_additional(): + if self.appear_then_click(GOTO_MAIN, offset=(30, 30), interval=2): + timeout.reset() + continue + if self.appear_then_click(RPG_HOME, offset=(30, 30), interval=2): + timeout.reset() + continue + if self.ui_additional(): timeout.reset() continue @@ -331,6 +337,8 @@ class UI(InfoHandler): # Daily reset if self.appear_then_click(LOGIN_ANNOUNCE, offset=(30, 30), interval=3): return True + if self.appear_then_click(LOGIN_ANNOUNCE_2, offset=(30, 30), interval=3): + return True if self.appear_then_click(GET_ITEMS_1, offset=True, interval=3): return True if self.appear_then_click(GET_ITEMS_2, offset=True, interval=3): @@ -494,6 +502,10 @@ class UI(InfoHandler): self.device.click(GOTO_MAIN) return True + # RPG event (raid_20240328) + if self.appear_then_click(RPG_STATUS_POPUP, offset=(30, 30), interval=3): + return True + return False def ui_button_interval_reset(self, button): @@ -514,5 +526,5 @@ class UI(InfoHandler): self.interval_reset(RAID_CHECK) if button == SHOP_GOTO_SUPPLY_PACK: self.interval_reset(EXCHANGE_CHECK) - if button == DORMMENU_GOTO_DORM: - self.interval_reset(GET_SHIP) + if button in [RPG_GOTO_STAGE, RPG_GOTO_STORY, RPG_LEAVE_CITY]: + self.interval_timer[GET_SHIP.name] = Timer(5).reset() diff --git a/module/war_archives/assets.py b/module/war_archives/assets.py index 52b5e16c7..e55421fa1 100644 --- a/module/war_archives/assets.py +++ b/module/war_archives/assets.py @@ -7,6 +7,7 @@ from module.base.template import Template OCR_DATA_KEY_CAMPAIGN = Button(area={'cn': (1188, 107, 1272, 131), 'en': (1188, 107, 1272, 131), 'jp': (1188, 107, 1272, 131), 'tw': (1188, 107, 1272, 131)}, color={'cn': (104, 101, 107), 'en': (104, 101, 107), 'jp': (104, 101, 107), 'tw': (104, 101, 107)}, button={'cn': (1188, 107, 1272, 131), 'en': (1188, 107, 1272, 131), 'jp': (1188, 107, 1272, 131), 'tw': (1188, 107, 1272, 131)}, file={'cn': './assets/cn/war_archives/OCR_DATA_KEY_CAMPAIGN.png', 'en': './assets/en/war_archives/OCR_DATA_KEY_CAMPAIGN.png', 'jp': './assets/jp/war_archives/OCR_DATA_KEY_CAMPAIGN.png', 'tw': './assets/tw/war_archives/OCR_DATA_KEY_CAMPAIGN.png'}) TEMPLATE_ASHEN_SIMULACRUM = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_ASHEN_SIMULACRUM.png', 'en': './assets/en/war_archives/TEMPLATE_ASHEN_SIMULACRUM.png', 'jp': './assets/cn/war_archives/TEMPLATE_ASHEN_SIMULACRUM.png', 'tw': './assets/cn/war_archives/TEMPLATE_ASHEN_SIMULACRUM.png'}) TEMPLATE_AURORA_NOCTIS = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_AURORA_NOCTIS.png', 'en': './assets/en/war_archives/TEMPLATE_AURORA_NOCTIS.png', 'jp': './assets/cn/war_archives/TEMPLATE_AURORA_NOCTIS.png', 'tw': './assets/cn/war_archives/TEMPLATE_AURORA_NOCTIS.png'}) +TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD.png', 'en': './assets/cn/war_archives/TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD.png', 'jp': './assets/cn/war_archives/TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD.png', 'tw': './assets/cn/war_archives/TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD.png'}) TEMPLATE_CRESCENDO_OF_POLARIS = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_CRESCENDO_OF_POLARIS.png', 'en': './assets/en/war_archives/TEMPLATE_CRESCENDO_OF_POLARIS.png', 'jp': './assets/jp/war_archives/TEMPLATE_CRESCENDO_OF_POLARIS.png', 'tw': './assets/cn/war_archives/TEMPLATE_CRESCENDO_OF_POLARIS.png'}) TEMPLATE_CRIMSON_ECHOES = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_CRIMSON_ECHOES.png', 'en': './assets/en/war_archives/TEMPLATE_CRIMSON_ECHOES.png', 'jp': './assets/jp/war_archives/TEMPLATE_CRIMSON_ECHOES.png', 'tw': './assets/cn/war_archives/TEMPLATE_CRIMSON_ECHOES.png'}) TEMPLATE_DIVERGENT_CHESSBOARD = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_DIVERGENT_CHESSBOARD.png', 'en': './assets/en/war_archives/TEMPLATE_DIVERGENT_CHESSBOARD.png', 'jp': './assets/jp/war_archives/TEMPLATE_DIVERGENT_CHESSBOARD.png', 'tw': './assets/tw/war_archives/TEMPLATE_DIVERGENT_CHESSBOARD.png'}) @@ -22,12 +23,15 @@ TEMPLATE_KHOROVOD_OF_DAWNS_RIME = Template(file={'cn': './assets/cn/war_archives TEMPLATE_MICROLAYER_MEDLEY = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_MICROLAYER_MEDLEY.png', 'en': './assets/en/war_archives/TEMPLATE_MICROLAYER_MEDLEY.png', 'jp': './assets/cn/war_archives/TEMPLATE_MICROLAYER_MEDLEY.png', 'tw': './assets/tw/war_archives/TEMPLATE_MICROLAYER_MEDLEY.png'}) TEMPLATE_MIRROR_INVOLUTION = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_MIRROR_INVOLUTION.png', 'en': './assets/cn/war_archives/TEMPLATE_MIRROR_INVOLUTION.png', 'jp': './assets/cn/war_archives/TEMPLATE_MIRROR_INVOLUTION.png', 'tw': './assets/cn/war_archives/TEMPLATE_MIRROR_INVOLUTION.png'}) TEMPLATE_NORTHERN_OVERTURE = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_NORTHERN_OVERTURE.png', 'en': './assets/en/war_archives/TEMPLATE_NORTHERN_OVERTURE.png', 'jp': './assets/cn/war_archives/TEMPLATE_NORTHERN_OVERTURE.png', 'tw': './assets/cn/war_archives/TEMPLATE_NORTHERN_OVERTURE.png'}) +TEMPLATE_PRELUDE_UNDER_THE_MOON = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png', 'en': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png', 'jp': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png', 'tw': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png'}) TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png', 'en': './assets/en/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png', 'jp': './assets/cn/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png', 'tw': './assets/cn/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png'}) TEMPLATE_SKYBOUND_ORATORIO = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png', 'en': './assets/en/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png', 'jp': './assets/cn/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png', 'tw': './assets/cn/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png'}) TEMPLATE_STARS_OF_THE_SHIMMERING_FJORD = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_STARS_OF_THE_SHIMMERING_FJORD.png', 'en': './assets/en/war_archives/TEMPLATE_STARS_OF_THE_SHIMMERING_FJORD.png', 'jp': './assets/cn/war_archives/TEMPLATE_STARS_OF_THE_SHIMMERING_FJORD.png', 'tw': './assets/tw/war_archives/TEMPLATE_STARS_OF_THE_SHIMMERING_FJORD.png'}) TEMPLATE_STRIVE_WISH_AND_STRATEGIZE = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_STRIVE_WISH_AND_STRATEGIZE.png', 'en': './assets/en/war_archives/TEMPLATE_STRIVE_WISH_AND_STRATEGIZE.png', 'jp': './assets/jp/war_archives/TEMPLATE_STRIVE_WISH_AND_STRATEGIZE.png', 'tw': './assets/tw/war_archives/TEMPLATE_STRIVE_WISH_AND_STRATEGIZE.png'}) TEMPLATE_SWIRLING_CHERRY_BLOSSOMS = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_SWIRLING_CHERRY_BLOSSOMS.png', 'en': './assets/en/war_archives/TEMPLATE_SWIRLING_CHERRY_BLOSSOMS.png', 'jp': './assets/cn/war_archives/TEMPLATE_SWIRLING_CHERRY_BLOSSOMS.png', 'tw': './assets/cn/war_archives/TEMPLATE_SWIRLING_CHERRY_BLOSSOMS.png'}) TEMPLATE_THE_ENIGMA_AND_THE_SHARK = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_THE_ENIGMA_AND_THE_SHARK.png', 'en': './assets/en/war_archives/TEMPLATE_THE_ENIGMA_AND_THE_SHARK.png', 'jp': './assets/cn/war_archives/TEMPLATE_THE_ENIGMA_AND_THE_SHARK.png', 'tw': './assets/cn/war_archives/TEMPLATE_THE_ENIGMA_AND_THE_SHARK.png'}) +TEMPLATE_THE_SOLOMON_RANGER = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_THE_SOLOMON_RANGER.png', 'en': './assets/en/war_archives/TEMPLATE_THE_SOLOMON_RANGER.png', 'jp': './assets/cn/war_archives/TEMPLATE_THE_SOLOMON_RANGER.png', 'tw': './assets/cn/war_archives/TEMPLATE_THE_SOLOMON_RANGER.png'}) +TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT.png', 'en': './assets/en/war_archives/TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT.png', 'jp': './assets/cn/war_archives/TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT.png', 'tw': './assets/cn/war_archives/TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT.png'}) TEMPLATE_UNIVERSE_IN_UNISON = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_UNIVERSE_IN_UNISON.png', 'en': './assets/en/war_archives/TEMPLATE_UNIVERSE_IN_UNISON.png', 'jp': './assets/jp/war_archives/TEMPLATE_UNIVERSE_IN_UNISON.png', 'tw': './assets/cn/war_archives/TEMPLATE_UNIVERSE_IN_UNISON.png'}) TEMPLATE_VISITORS_DYED_IN_RED = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_VISITORS_DYED_IN_RED.png', 'en': './assets/en/war_archives/TEMPLATE_VISITORS_DYED_IN_RED.png', 'jp': './assets/jp/war_archives/TEMPLATE_VISITORS_DYED_IN_RED.png', 'tw': './assets/tw/war_archives/TEMPLATE_VISITORS_DYED_IN_RED.png'}) TEMPLATE_WINTERS_CROWN = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_WINTERS_CROWN.png', 'en': './assets/en/war_archives/TEMPLATE_WINTERS_CROWN.png', 'jp': './assets/jp/war_archives/TEMPLATE_WINTERS_CROWN.png', 'tw': './assets/tw/war_archives/TEMPLATE_WINTERS_CROWN.png'}) diff --git a/module/war_archives/dictionary.py b/module/war_archives/dictionary.py index 0ecee5f89..b232fa86f 100644 --- a/module/war_archives/dictionary.py +++ b/module/war_archives/dictionary.py @@ -27,4 +27,8 @@ dic_archives_template = { 'war_archives_20200917_cn': TEMPLATE_DREAMWAKERS_BUTTERFLY, 'war_archives_20210527_cn': TEMPLATE_MIRROR_INVOLUTION, 'war_archives_20210225_cn': TEMPLATE_KHOROVOD_OF_DAWNS_RIME, + 'war_archives_20200603_cn': TEMPLATE_COUNTERATTACK_WITHIN_THE_FJORD, + 'war_archives_20190314_en': TEMPLATE_PRELUDE_UNDER_THE_MOON, + 'war_archives_20200312_cn': TEMPLATE_THE_SOLOMON_RANGER, + 'war_archives_20200507_cn': TEMPLATE_THE_WAY_HOME_IN_THE_NIGHT, }