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():