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/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/en/combat/BATTLE_PREPARATION.gif b/assets/en/combat/BATTLE_PREPARATION.gif index 9bb9622ce..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/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/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/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/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/campaign/Readme.md b/campaign/Readme.md index fd3a4751a..318c74f10 100644 --- a/campaign/Readme.md +++ b/campaign/Readme.md @@ -189,3 +189,5 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 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/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/config/template.json b/config/template.json index 8106eae25..01bb92be5 100644 --- a/config/template.json +++ b/config/template.json @@ -315,11 +315,12 @@ }, "GemsFarming": { "ChangeFlagship": "ship", + "CommonCV": "any", "ChangeVanguard": "ship", + "CommonDD": "any", "ALLowLowVanguardLevel": true, "FleetNumberInHardMode": 0, "StopIFAutoNotEnsured": true, - "CommonCV": "any", "CommissionLimit": true }, "FlagshipFilter": { diff --git a/deploy/Windows/app.py b/deploy/Windows/app.py index c558a1a3f..750a50d13 100644 --- a/deploy/Windows/app.py +++ b/deploy/Windows/app.py @@ -53,5 +53,5 @@ class AppManager(DeployConfig): Progress.UpdateAlasApp() return False - self.app_asar_replace(os.getcwd()) - Progress.UpdateAlasApp() + # self.app_asar_replace(os.getcwd()) + # Progress.UpdateAlasApp() diff --git a/deploy/Windows/config.py b/deploy/Windows/config.py index 136afe866..adf91ee67 100644 --- a/deploy/Windows/config.py +++ b/deploy/Windows/config.py @@ -55,7 +55,7 @@ class ConfigModel: # Webui WebuiHost: str = "0.0.0.0" - WebuiPort: int = 22267 + WebuiPort: int = 22367 Language: str = "en-US" Theme: str = "default" DpiScaling: bool = True @@ -80,42 +80,21 @@ class DeployConfig(ConfigModel): self.config_template = {} self.read() - # Redirection - if self.Repository in [ - 'https://gitee.com/LmeSzinc/AzurLaneAutoScript', - 'https://gitee.com/lmeszinc/azur-lane-auto-script-mirror', - 'https://e.coding.net/llop18870/alas/AzurLaneAutoScript.git', - 'https://e.coding.net/saarcenter/alas/AzurLaneAutoScript.git', - 'https://git.saarcenter.com/LmeSzinc/AzurLaneAutoScript.git', - ]: - self.Repository = 'git://git.lyoko.io/AzurLaneAutoScript' - # Bypass webui.config.DeployConfig.__setattr__() # Don't write these into deploy.yaml - super().__setattr__( - 'GitOverCdn', - self.Repository == 'git://git.lyoko.io/AzurLaneAutoScript' and self.Branch == 'master' - ) - if self.Repository in ['global']: - super().__setattr__('Repository', 'https://github.com/LmeSzinc/AzurLaneAutoScript') - if self.Repository in ['cn']: - super().__setattr__('Repository', 'git://git.lyoko.io/AzurLaneAutoScript') + 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() - @cached_property - def flag_feature_test_0_4_0(self): - flag = os.path.exists('./toolkit/flag_feature_test_0_4_0') - logger.info(f'flag_feature_test_0_4_0: {flag}') - return flag - def show_config(self): logger.hr("Show deploy config", 1) for k, v in self.config.items(): if k in ("Password", "SSHUser"): continue - if self.config_template.get(k) == v: + if self.config_template[k] == v: continue logger.info(f"{k}: {v}") diff --git a/deploy/Windows/emulator.py b/deploy/Windows/emulator.py index e01d59cbd..3dfc74f15 100644 --- a/deploy/Windows/emulator.py +++ b/deploy/Windows/emulator.py @@ -2,6 +2,7 @@ import asyncio import filecmp import os import shutil +import sys import typing as t from dataclasses import dataclass @@ -9,7 +10,8 @@ from deploy.Windows.alas import AlasManager from deploy.Windows.logger import logger from deploy.Windows.utils import cached_property -asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) +if sys.platform.startswith("win"): + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) @dataclass diff --git a/deploy/Windows/git.py b/deploy/Windows/git.py index 0f3f9bea9..c873f6b35 100644 --- a/deploy/Windows/git.py +++ b/deploy/Windows/git.py @@ -2,9 +2,10 @@ import configparser import os from deploy.Windows.config import DeployConfig -from deploy.git_over_cdn.client import GitOverCdnClient from deploy.Windows.logger import Progress, logger -from deploy.Windows.utils import * +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): @@ -16,6 +17,25 @@ class GitConfigParser(configparser.ConfigParser): 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): @@ -111,7 +131,7 @@ class GitManager(DeployConfig): @property def goc_client(self): client = GitOverCdnClient( - url='https://vip.123pan.cn/1818706573/pack/LmeSzinc_AzurLaneAutoScript_master', + url='https://vip.123pan.cn/1815343254/pack/LmeSzinc_StarRailCopilot_master', folder=self.root_filepath, source='origin', branch='master', diff --git a/deploy/Windows/logger.py b/deploy/Windows/logger.py index 77f527d3e..a0607dc31 100644 --- a/deploy/Windows/logger.py +++ b/deploy/Windows/logger.py @@ -61,6 +61,9 @@ class Progress: GitCheckout = Percentage(48) GitShowVersion = Percentage(50) + GitLatestCommit = Percentage(25) + GitDownloadPack = Percentage(40) + KillExisting = Percentage(60) UpdateDependency = Percentage(70) UpdateAlasApp = Percentage(75) diff --git a/deploy/Windows/template.yaml b/deploy/Windows/template.yaml index fc8878866..40e5fd1d6 100644 --- a/deploy/Windows/template.yaml +++ b/deploy/Windows/template.yaml @@ -1,9 +1,9 @@ Deploy: Git: # URL of AzurLaneAutoScript repository - # [CN user] Use 'git://git.lyoko.io/AzurLaneAutoScript' for faster and more stable download - # [Other] Use 'https://github.com/LmeSzinc/AzurLaneAutoScript' - Repository: 'https://github.com/LmeSzinc/AzurLaneAutoScript' + # [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 @@ -128,13 +128,14 @@ Deploy: 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 22267 - WebuiPort: 22267 + # [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 diff --git a/module/campaign/gems_farming.py b/module/campaign/gems_farming.py index eadad9174..ac7ebac02 100644 --- a/module/campaign/gems_farming.py +++ b/module/campaign/gems_farming.py @@ -5,21 +5,29 @@ 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, RequestHumanTakeover +from module.exception import CampaignEnd, ScriptError, RequestHumanTakeover from module.handler.assets import AUTO_SEARCH_MAP_OPTION_OFF from module.logger import logger -from module.map.assets import (FLEET_PREPARATION, MAP_PREPARATION, FLEET_ENTER_FLAGSHIP_HARD_1, +from module.map.assets import (FLEET_ENTER_FLAGSHIP_HARD_1, FLEET_ENTER_FLAGSHIP_HARD_2, FLEET_ENTER_HARD_1, FLEET_ENTER_HARD_2, FLEET_ENTER_FLAGSHIP_HARD_1_3, FLEET_ENTER_FLAGSHIP_HARD_2_3, FLEET_ENTER_HARD_1_3, FLEET_ENTER_HARD_2_3) -from module.retire.assets import (DOCK_CHECK, TEMPLATE_BOGUE, TEMPLATE_HERMES, TEMPLATE_LANGLEY, TEMPLATE_RANGER, +from module.retire.assets import ( DOCK_SHIP_DOWN) +from module.map.assets import FLEET_PREPARATION, MAP_PREPARATION +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, page_event from module.ui.assets import BACK_ARROW from module.config.config import deep_get import inflection +from module.ui.page import page_fleet SIM_VALUE = 0.95 @@ -216,6 +224,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: @@ -243,7 +252,8 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): self.dock_select_one(button) self.dock_filter_set() - self.dock_select_confirm(check_button=self.page_fleet_check_button) + self.dock_sort_method_dsc_set() + self.dock_select_confirm(check_button=page_fleet.check_button) def get_common_rarity_cv(self, lv=31, emotion=16): """ @@ -260,7 +270,6 @@ class GemsFarming(CampaignRun, Dock, EquipmentChange): scanner.disable('rarity') if self.config.GemsFarming_CommonCV == 'any': - logger.info('') self.dock_sort_method_dsc_set(False) @@ -326,13 +335,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 solve_hard_flagship_black(self): if self.hard_mode: diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 401c12623..f9aa2e192 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -116,7 +116,8 @@ "aScreenCap_nc", "DroidCast", "DroidCast_raw", - "scrcpy" + "scrcpy", + "nemu_ipc" ] }, "ControlMethod": { @@ -127,7 +128,8 @@ "uiautomator2", "minitouch", "Hermit", - "MaaTouch" + "MaaTouch", + "nemu_ipc" ] }, "ScreenshotDedithering": { @@ -1606,6 +1608,17 @@ "ship_equip" ] }, + "CommonCV": { + "type": "select", + "value": "any", + "option": [ + "any", + "langley", + "bogue", + "ranger", + "hermes" + ] + }, "ChangeVanguard": { "type": "select", "value": "ship", @@ -1615,6 +1628,15 @@ "ship_equip" ] }, + "CommonDD": { + "type": "select", + "value": "any", + "option": [ + "any", + "aulick_or_foote", + "cassin_or_downes" + ] + }, "ALLowLowVanguardLevel": { "type": "checkbox", "value": true @@ -1632,17 +1654,6 @@ "type": "checkbox", "value": true }, - "CommonCV": { - "type": "select", - "value": "any", - "option": [ - "any", - "langley", - "bogue", - "ranger", - "hermes" - ] - }, "CommissionLimit": { "type": "checkbox", "value": true @@ -1891,13 +1902,13 @@ ], "display": "hide", "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -2224,13 +2235,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -2672,13 +2683,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -4514,13 +4525,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -4979,13 +4990,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -5444,13 +5455,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -5909,13 +5920,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -6364,13 +6375,13 @@ "event_20240229_cn" ], "option_bold": [ - "event_20210422_cn", - "event_20220324_cn" + "event_20211111_cn", + "event_20220224_cn" ], - "cn": "event_20220324_cn", - "en": "event_20220324_cn", - "jp": "event_20220324_cn", - "tw": "event_20210422_cn" + "cn": "event_20220224_cn", + "en": "event_20220224_cn", + "jp": "event_20220224_cn", + "tw": "event_20211111_cn" }, "Mode": { "type": "select", @@ -9835,11 +9846,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 293c61d9d..285355ee1 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -32,10 +32,28 @@ 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, + nemu_ipc, + ] ScreenshotDedithering: false AdbRestart: false EmulatorInfo: @@ -251,17 +269,20 @@ GemsFarming: ChangeFlagship: value: ship option: [ 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 ] ALLowLowVanguardLevel: true FleetNumberInHardMode: value: 0 option: [ 0, 1, 2 ] StopIFAutoNotEnsured: true - CommonCV: - value: any - option: [ any, langley, bogue, ranger, hermes ] CommissionLimit: true FlagshipFilter: Sort: @@ -732,9 +753,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/config_generated.py b/module/config/config_generated.py index 3860bd2b8..b0e2e7130 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -21,8 +21,8 @@ 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_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch + Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy, nemu_ipc + Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch, nemu_ipc Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False @@ -154,6 +154,7 @@ class GeneratedConfig: GemsFarming_FleetNumberInHardMode = 0 # 0, 1, 2 GemsFarming_StopIFAutoNotEnsured = True GemsFarming_CommonCV = 'any' # any, langley, bogue, ranger, hermes + GemsFarming_CommonDD = 'any' # any, aulick_or_foote, cassin_or_downes GemsFarming_CommissionLimit = True # Group `FlagshipFilter` @@ -448,7 +449,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 535ae3851..fcd4a2ced 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -755,6 +755,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 21175a268..92a76fead 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -417,7 +417,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "Control Method", @@ -426,7 +427,8 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch" + "MaaTouch": "MaaTouch", + "nemu_ipc": "nemu_ipc" }, "ScreenshotDedithering": { "name": "Image Color De-dithering", @@ -709,7 +711,7 @@ "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 Rerun", "event_20220407_tw": "蒼紅的迴響(復刻)", @@ -1140,6 +1142,15 @@ "ship": "Change Ship", "ship_equip": "Change Ship + Gears" }, + "CommonCV": { + "name": "Flagship Common CV/CVL Preference", + "help": "", + "any": "any", + "langley": "langley", + "bogue": "bogue", + "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.", @@ -1162,14 +1173,12 @@ "name": "GemsFarming.StopIFAutoNotEnsured.name", "help": "GemsFarming.StopIFAutoNotEnsured.help" }, - "CommonCV": { - "name": "Flagship Common CV/CVL Preference", + "CommonDD": { + "name": "Flagship Common DD Preference", "help": "", "any": "any", - "langley": "langley", - "bogue": "bogue", - "ranger": "ranger", - "hermes": "hermes" + "aulick_or_foote": "aulick or foote", + "cassin_or_downes": "cassin or downes" }, "CommissionLimit": { "name": "Prevent Too Many Urgent Commissions", @@ -2629,13 +2638,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 fdafd8469..de5e71311 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -417,7 +417,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "Emulator.ControlMethod.name", @@ -426,7 +427,8 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch" + "MaaTouch": "MaaTouch", + "nemu_ipc": "nemu_ipc" }, "ScreenshotDedithering": { "name": "Emulator.ScreenshotDedithering.name", @@ -709,7 +711,7 @@ "event_20211125_cn": "弧光は交わる世界にて(復刻)", "event_20211229_cn": "遡望せし虹彩の塔(復刻)", "event_20220210_cn": "凍絶の北海(復刻)", - "event_20220224_cn": "鳴動せし星霜の淵", + "event_20220224_cn": "鳴動せし星霜の淵(復刻)", "event_20220310_tw": "復刻斯圖爾特的硝煙", "event_20220324_cn": "幻像の塔(復刻)", "event_20220407_tw": "蒼紅的迴響(復刻)", @@ -1140,13 +1142,6 @@ "ship": "ship", "ship_equip": "ship_equip" }, - "ChangeVanguard": { - "name": "GemsFarming.ChangeVanguard.name", - "help": "GemsFarming.ChangeVanguard.help", - "disabled": "disabled", - "ship": "ship", - "ship_equip": "ship_equip" - }, "ALLowLowVanguardLevel": { "name": "GemsFarming.ALLowLowVanguardLevel.name", "help": "GemsFarming.ALLowLowVanguardLevel.help" @@ -1171,6 +1166,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" @@ -2630,11 +2639,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 884825fe0..4dc9537d7 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -417,7 +417,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "模拟器控制方案", @@ -426,7 +427,8 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch" + "MaaTouch": "MaaTouch", + "nemu_ipc": "nemu_ipc" }, "ScreenshotDedithering": { "name": "去除图片色彩抖动", @@ -709,7 +711,7 @@ "event_20211125_cn": "复刻交汇世界的弧光", "event_20211229_cn": "复刻逆转彩虹之塔", "event_20220210_cn": "复刻北境序曲", - "event_20220224_cn": "深度回音", + "event_20220224_cn": "复刻深度回音", "event_20220310_tw": "復刻斯圖爾特的硝煙", "event_20220324_cn": "复刻虚像构筑之塔", "event_20220407_tw": "蒼紅的迴響(復刻)", @@ -1140,6 +1142,15 @@ "ship": "更换舰船", "ship_equip": "更换舰船 + 装备" }, + "CommonCV": { + "name": "指定旗舰航母", + "help": "", + "any": "任意", + "langley": "兰利", + "bogue": "博格", + "ranger": "突击者", + "hermes": "竞技神" + }, "ChangeVanguard": { "name": "更换前排", "help": "当前排红脸时更换前排,选择不更换则会强制红脸出击\n换前排通过找一艘心情不低于16、等级100的白鹰白皮驱逐完成,所以尽量保证有足够多的驱逐。国服以外则为等级70的白鹰白船驱逐。\n\n换装备只会更换正在装备中的栏位,即使是白装也会更换。如果指定了旗舰,则会更换全部5个装备,未指定旗舰只会更换设备。", @@ -1162,14 +1173,12 @@ "name": "无法设置职能时停止任务", "help": "无法设置舰队职能时,以以下方式停止任务以免跨队队伍出击,石油被大量消耗:\n若设置了错误推送则推送任务停止的消息并停止该任务。\n若没有设置错误推送,则直接停止Alas。" }, - "CommonCV": { - "name": "指定旗舰航母", + "CommonDD": { + "name": "指定前排", "help": "", "any": "任意", - "langley": "兰利", - "bogue": "博格", - "ranger": "突击者", - "hermes": "竞技神" + "aulick_or_foote": "奥利克或富特", + "cassin_or_downes": "卡辛或唐斯" }, "CommissionLimit": { "name": "防止紧急委托数量过多", @@ -2629,12 +2638,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 8636e8f8f..26ae0111c 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -417,7 +417,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "模擬器控制方案", @@ -426,7 +427,8 @@ "uiautomator2": "uiautomator2", "minitouch": "minitouch", "Hermit": "Hermit", - "MaaTouch": "MaaTouch" + "MaaTouch": "MaaTouch", + "nemu_ipc": "nemu_ipc" }, "ScreenshotDedithering": { "name": "去除圖片色彩抖動", @@ -705,7 +707,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": "復刻北境序曲", @@ -1140,6 +1142,15 @@ "ship": "更換艦船", "ship_equip": "更換艦船 + 裝備" }, + "CommonCV": { + "name": "指定旗艦航母", + "help": "", + "any": "任意", + "langley": "蘭利", + "bogue": "博格", + "ranger": "突擊者", + "hermes": "競技神" + }, "ChangeVanguard": { "name": "更換前排", "help": "當前排紅臉時更換前排,選擇不更換則會強制紅臉出擊\n換前排通過找一艘心情不低於16、等級70的白鷹白船驅逐完成,所以盡量保證有足夠多的驅逐。國服則為等級100的白鷹白船驅逐。\n\n換裝備只會更換正在裝備中的欄位,即使是白裝也會更換。如果指定了旗艦,則會更換全部5個裝備,未指定旗艦隻會更換設備。", @@ -1162,14 +1173,12 @@ "name": "GemsFarming.StopIFAutoNotEnsured.name", "help": "GemsFarming.StopIFAutoNotEnsured.help" }, - "CommonCV": { - "name": "指定旗艦航母", + "CommonDD": { + "name": "指定前排", "help": "", "any": "任意", - "langley": "蘭利", - "bogue": "博格", - "ranger": "突擊者", - "hermes": "競技神" + "aulick_or_foote": "奧利克或富特", + "cassin_or_downes": "卡辛或唐斯" }, "CommissionLimit": { "name": "防止緊急委託數量過多", @@ -2629,12 +2638,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 3db639ad9..bbd6ed0b4 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -11,7 +11,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 @@ -266,15 +266,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 @@ -287,6 +292,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): """ @@ -752,23 +766,42 @@ 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') + brute_force_connect() + continue + else: + break # Auto device detection if self.config.Emulator_Serial == 'auto': @@ -822,6 +855,36 @@ 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.serial = emu_serial + self.config.Emulator_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: + 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 + @retry def list_package(self, show_log=True): """ diff --git a/module/device/connection_attr.py b/module/device/connection_attr.py index 65c483dd7..da16756d5 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -146,7 +146,12 @@ class ConnectionAttr: 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_mumu12_family(self): + # 127.0.0.1:16384 + 32*n + return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16') @cached_property def is_emulator(self): 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 6d122ba96..2542d29f1 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -1,5 +1,4 @@ import collections -import sys from datetime import datetime from module.base.timer import Timer @@ -17,11 +16,6 @@ from module.exception import ( from module.handler.assets import GET_MISSION from module.logger import logger -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 - def show_function_call(): """ @@ -63,7 +57,7 @@ def show_function_call(): logger.info('Function calls:' + ''.join(func_list)) -class Device(Screenshot, Control, AppControl, Platform): +class Device(Screenshot, Control, AppControl): _screen_size_checked = False detect_record = set() click_record = collections.deque(maxlen=15) @@ -87,12 +81,13 @@ class Device(Screenshot, Control, AppControl, Platform): ) raise - self.screenshot_interval_set() + # 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() @@ -110,7 +105,22 @@ class Device(Screenshot, Control, AppControl, Platform): 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' def handle_night_commission(self, daily_trigger='21:00', threshold=30): """ @@ -161,6 +171,8 @@ class Device(Screenshot, Control, AppControl, Platform): # 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 index b061a3a65..e69de29bb 100644 --- a/module/device/emulator.py +++ b/module/device/emulator.py @@ -1,325 +0,0 @@ -import os -import re -import winreg -import subprocess - -from adbutils.errors import AdbError - -from deploy.Windows.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 c2a1f01b4..9bf0f216b 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -146,7 +146,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 635df6d7b..d9bf3aabe 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,36 +174,25 @@ class DroidCast(Uiautomator2): else: logger.error(f'Unknown DROIDCAST_VERSION: {self.config.DROIDCAST_VERSION}') - @Config.when(DROIDCAST_VERSION='DroidCast_raw') - def droidcast_init(self): - logger.hr('Droidcast init') - self.droidcast_stop() - - logger.info('Pushing DroidCast apk') - self.adb_push(self.config.DROIDCAST_FILEPATH_LOCAL, self.config.DROIDCAST_FILEPATH_REMOTE) - - logger.info('Starting DroidCast apk') - # DroidCastS-release-1.1.5.apk - # CLASSPATH=/data/local/tmp/DroidCastS-release-1.1.5.apk app_process / com.torther.droidcasts.Main > /dev/null - resp = self.u2_shell_background([ - 'CLASSPATH=/data/local/tmp/DroidCastS.apk', - 'app_process', - '/', - 'com.torther.droidcasts.Main', - '>', - '/dev/null' - ]) - logger.info(resp) - - del_cached_property(self, 'droidcast_session') - _ = self.droidcast_session - logger.attr('DroidCast', self.droidcast_url()) - self.droidcast_wait_startup() + 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 @@ -195,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}') @@ -237,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/nemu_ipc.py b/module/device/method/nemu_ipc.py new file mode 100644 index 000000000..90131962c --- /dev/null +++ b/module/device/method/nemu_ipc.py @@ -0,0 +1,529 @@ +import asyncio +import ctypes +import os +import sys +from functools import wraps, partial + +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 + + +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 + + with CaptureNemuIpc(): + 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 + + with CaptureNemuIpc(): + 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) + result = await asyncio.wait_for(self._ev.run_in_executor(None, func_wrapped), timeout=0.05) + 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 + """ + 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)() + + with CaptureNemuIpc(): + 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() + + with CaptureNemuIpc(): + 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) + + with CaptureNemuIpc(): + 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() + + with CaptureNemuIpc(): + 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 + _ = self.emulator_instance + 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)) + with CaptureNemuIpc(): + self.nemu_ipc.down(x, y) + self.sleep(down) + self.nemu_ipc.up() + self.sleep(0.050 - down) + + def long_click_nemu_ipc(self, x, y, duration=1.0): + with CaptureNemuIpc(): + self.nemu_ipc.down(x, y) + self.sleep(duration) + self.nemu_ipc.up() + self.sleep(0.050) + + def swipe_nemu_ipc(self, p1, p2): + points = insert_swipe(p0=p1, p3=p2) + + with CaptureNemuIpc(): + for point in points: + self.nemu_ipc.down(*point) + self.sleep(0.010) + + self.nemu_ipc.up() + self.sleep(0.050) + + def drag_nemu_ipc(self, p1, p2, point_random=(-10, -10, 10, 10)): + p1 = np.array(p1) - random_rectangle_point(point_random) + p2 = np.array(p2) - random_rectangle_point(point_random) + points = insert_swipe(p0=p1, p3=p2, speed=20) + + with CaptureNemuIpc(): + for point in points: + self.nemu_ipc.down(*point) + self.sleep(0.010) + + 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/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 066f0bf3d..ecd026ba1 100644 --- a/module/device/platform/emulator_base.py +++ b/module/device/platform/emulator_base.py @@ -3,7 +3,7 @@ import re import typing as t from dataclasses import dataclass -from deploy.Windows.utils import cached_property, iter_folder +from module.device.platform.utils import cached_property, iter_folder def abspath(path): @@ -54,7 +54,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): @@ -83,8 +83,9 @@ class EmulatorInstanceBase: def MuMuPlayer12_id(self): """ Convert MuMu 12 instance name to instance id. - Example name: MuMuPlayer-12.0-3 - Example ID : 3 + Example names: + MuMuPlayer-12.0-3 + YXArkNights-12.0-1 Returns: int: Instance ID, or None if this is not a MuMu 12 instance @@ -92,8 +93,11 @@ class EmulatorInstanceBase: res = re.search(r'MuMuPlayer-12.0-(\d+)', self.name) if res: return int(res.group(1)) - else: - return None + res = re.search(r'YXArkNights-12.0-(\d+)', self.name) + if res: + return int(res.group(1)) + + return None class EmulatorBase: @@ -197,10 +201,7 @@ 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: diff --git a/module/device/platform/emulator_windows.py b/module/device/platform/emulator_windows.py index 1f50195a6..bb873e690 100644 --- a/module/device/platform/emulator_windows.py +++ b/module/device/platform/emulator_windows.py @@ -8,8 +8,8 @@ from dataclasses import dataclass # module/device/platform/emulator_base.py # module/device/platform/emulator_windows.py # Will be used in Alas Easy Install, they shouldn't import any Alas modules. -from deploy.Windows.utils import cached_property, iter_folder from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase +from module.device.platform.utils import cached_property, iter_folder @dataclass @@ -56,14 +56,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 +70,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,46 +78,49 @@ 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 elif dir2 == 'nemu9': return cls.MuMuPlayerX else: return cls.MuMuPlayer - if exe == 'MuMuPlayer.exe': + if exe == 'mumuplayer.exe': return cls.MuMuPlayer12 - if exe == 'MEmu.exe': + if exe == 'memu.exe': return cls.MEmuPlayer return '' @@ -151,7 +146,9 @@ class Emulator(EmulatorBase): elif 'NemuMultiPlayer.exe' in exe: yield exe.replace('NemuMultiPlayer.exe', 'NemuPlayer.exe') elif 'MuMuMultiPlayer.exe' in exe: - yield exe.replace('MuMuMultiPlayer.exe', 'MuMuManager.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: @@ -330,17 +327,26 @@ class EmulatorManager(EmulatorManagerBase): 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(): @@ -353,8 +359,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: @@ -420,24 +429,28 @@ class EmulatorManager(EmulatorManagerBase): '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 @cached_property def all_emulators(self) -> t.List[Emulator]: @@ -476,15 +489,16 @@ 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) exe = [Emulator(path).path for path in exe if Emulator.is_emulator(path)] exe = sorted(set(exe)) - exe = [Emulator(path) for path in exe] + dic = {} + for path in exe: + dic.setdefault(path.lower(), path) + exe = [Emulator(path) for path in dic.values()] return exe @cached_property diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index 7fd38b7f5..16313f87d 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -1,8 +1,7 @@ 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 @@ -48,8 +47,20 @@ class PlatformBase(Connection, EmulatorManagerBase): @cached_property def emulator_info(self) -> EmulatorInfo: emulator = self.config.EmulatorInfo_Emulator - name = str(self.config.EmulatorInfo_name).strip().replace('\n', '') - path = str(self.config.EmulatorInfo_path).strip().replace('\n', '') + 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, @@ -168,9 +179,3 @@ class PlatformBase(Connection, EmulatorManagerBase): # 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/platform_windows.py b/module/device/platform/platform_windows.py index fc82db60c..de00efeca 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -88,19 +88,21 @@ class PlatformWindows(PlatformBase, EmulatorManager): self.execute(exe) 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 - self.execute(f'{exe} -v {instance.MuMuPlayer12_id}') + 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}') @@ -142,6 +144,8 @@ class PlatformWindows(PlatformBase, EmulatorManager): # 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}' @@ -152,7 +156,7 @@ class PlatformWindows(PlatformBase, EmulatorManager): # "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}') @@ -313,4 +317,5 @@ class PlatformWindows(PlatformBase, EmulatorManager): 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 5d4fd3dbe..71e2b0cf3 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): @@ -155,6 +157,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/dorm.py b/module/dorm/dorm.py index ef9983a91..1f7df2e6d 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: @@ -117,7 +118,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') @@ -126,6 +127,29 @@ class RewardDorm(UI): self.device.minitouch_builder.up().commit() self.device.minitouch_send() + @Config.when(DEVICE_CONTROL_METHOD='MaaTouch') + def _dorm_feed_long_tap(self, button, count): + timeout = Timer(count // 5 + 5).start() + x, y = random_rectangle_point(button.button) + self.device.maatouch_builder.down(x, y).commit() + self.device.maatouch_send() + + while 1: + self.device.maatouch_builder.move(x, y).commit().wait(10) + self.device.maatouch_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 + + self.device.maatouch_builder.up().commit() + self.device.maatouch_send() + @Config.when(DEVICE_CONTROL_METHOD='uiautomator2') def _dorm_feed_long_tap(self, button, count): timeout = Timer(count // 5 + 5).start() @@ -139,7 +163,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 +171,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 +249,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 +308,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): """ @@ -520,7 +594,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/handler/assets.py b/module/handler/assets.py index 2459d1578..0c0ef5cba 100644 --- a/module/handler/assets.py +++ b/module/handler/assets.py @@ -32,9 +32,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'}) diff --git a/module/handler/auto_search.py b/module/handler/auto_search.py index 5dd35f1bc..c8d587c9c 100644 --- a/module/handler/auto_search.py +++ b/module/handler/auto_search.py @@ -141,8 +141,9 @@ class AutoSearchHandler(EnemySearchingHandler): active = [] self.AUTO_SEARCH_SETTINGS = AUTO_SEARCH_SETTINGS_15 if 'campaign_15' in self.config.campaign_name \ else AUTO_SEARCH_SETTINGS - for index, button in enumerate(self.AUTO_SEARCH_SETTINGS): - if self.image_color_count(button, color=(156, 255, 82), threshold=221, count=20): + + for index, button in enumerate(AUTO_SEARCH_SETTINGS): + 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/map/map_fleet_preparation.py b/module/map/map_fleet_preparation.py index 23375f16e..3c74bd487 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..5f6fee115 100644 --- a/module/ocr/al_ocr.py +++ b/module/ocr/al_ocr.py @@ -2,15 +2,17 @@ import os import cv2 import numpy as np +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 PIL import Image - -from module.exception import RequestHumanTakeover -from module.logger import logger def get_mxnet_context(): 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/retire/assets.py b/module/retire/assets.py index 06999cfe6..68b9d9ea3 100644 --- a/module/retire/assets.py +++ b/module/retire/assets.py @@ -43,13 +43,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/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():