From 27dd3769e4f7995813f2d55bb782deef1e0ee676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CPPPlatelet=E2=80=9D?= Date: Mon, 20 May 2024 13:52:16 +0800 Subject: [PATCH 001/161] Opt: Stop emulator when no task pending. :) --- alas.py | 9 +++++++++ module/config/argument/args.json | 3 ++- module/config/argument/argument.yaml | 2 +- module/config/i18n/en-US.json | 3 ++- module/config/i18n/ja-JP.json | 3 ++- module/config/i18n/zh-CN.json | 3 ++- module/config/i18n/zh-TW.json | 3 ++- module/device/device.py | 7 +++++++ 8 files changed, 27 insertions(+), 6 deletions(-) diff --git a/alas.py b/alas.py index 46a91e4f65..86c6622acb 100644 --- a/alas.py +++ b/alas.py @@ -462,6 +462,15 @@ def get_next_task(self): if not self.wait_until(task.next_run): del_cached_property(self, 'config') continue + elif method == 'stop_emulator': + logger.info('Stop emulator during wait') + self.device.emulator_stop() + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + self.device.emulator_start() + del_cached_property(self, 'config') + continue else: logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') release_resources() diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 13ce975149..d38bdaedd2 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -208,7 +208,8 @@ "option": [ "stay_there", "goto_main", - "close_game" + "close_game", + "stop_emulator" ] } }, diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index a5f6a2ba3e..10202f21bb 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -94,7 +94,7 @@ Optimization: TaskHoardingDuration: 0 WhenTaskQueueEmpty: value: goto_main - option: [ stay_there, goto_main, close_game ] + option: [ stay_there, goto_main, close_game, stop_emulator ] DropRecord: SaveFolder: ./screenshots AzurStatsID: null diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index bcb8c317ec..7001972810 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -506,7 +506,8 @@ "help": "Close AL when there are no pending tasks, can help reduce CPU", "stay_there": "Stay There", "goto_main": "Goto Main Page", - "close_game": "Close Game" + "close_game": "Close Game", + "stop_emulator": "Stop Emulator" } }, "DropRecord": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 97fa431c73..14f5373706 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -506,7 +506,8 @@ "help": "Optimization.WhenTaskQueueEmpty.help", "stay_there": "stay_there", "goto_main": "goto_main", - "close_game": "close_game" + "close_game": "close_game", + "stop_emulator": "stop_emulator" } }, "DropRecord": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index f39e7f661b..836608cec2 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -506,7 +506,8 @@ "help": "无任务时关闭游戏,能在收菜期间降低 CPU 占用", "stay_there": "停在原处", "goto_main": "前往主界面", - "close_game": "关闭游戏" + "close_game": "关闭游戏", + "stop_emulator": "关闭模拟器" } }, "DropRecord": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index a2508f8151..38a69431e3 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -506,7 +506,8 @@ "help": "無任務時關閉遊戲,能在收菜期間降低 CPU 佔用", "stay_there": "停在原處", "goto_main": "前往主界面", - "close_game": "關閉遊戲" + "close_game": "關閉遊戲", + "stop_emulator": "關閉模擬器" } }, "DropRecord": { diff --git a/module/device/device.py b/module/device/device.py index 26b6313230..40e04e9c29 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -306,3 +306,10 @@ def app_stop(self): super().app_stop() self.stuck_record_clear() self.click_record_clear() + + def emulator_stop(self): + #kill emulator + if self.emulator_instance: + super().emulator_stop() + self.stuck_record_clear() + self.click_record_clear() \ No newline at end of file From 62965a1f2449e0a5d0b87c8fe92a915dc6dec351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CPPPlatelet=E2=80=9D?= Date: Mon, 20 May 2024 23:50:02 +0800 Subject: [PATCH 002/161] Opt: reverse device init and get_next_task --- alas.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/alas.py b/alas.py index 86c6622acb..f79aca5496 100644 --- a/alas.py +++ b/alas.py @@ -469,6 +469,8 @@ def get_next_task(self): self.device.release_during_wait() if not self.wait_until(task.next_run): self.device.emulator_start() + time.sleep(10) + self.config.task_call('Restart') del_cached_property(self, 'config') continue else: @@ -504,10 +506,10 @@ def loop(self): del_cached_property(self, 'config') logger.info('Server or network is recovered. Restart game client') self.config.task_call('Restart') - # Get task - task = self.get_next_task() # Init device and change server _ = self.device + # Get task + task = self.get_next_task() # Skip first restart if self.is_first_task and task == 'Restart': logger.info('Skip task `Restart` at scheduler start') From 62b187e39c4ae0f74e26047bdfaac6bbe4c8c628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CPPPlatelet=E2=80=9D?= Date: Tue, 21 May 2024 12:43:40 +0800 Subject: [PATCH 003/161] Opt: add more exception detection. :) --- alas.py | 6 ++++++ module/device/method/droidcast.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/alas.py b/alas.py index f79aca5496..5605fc3791 100644 --- a/alas.py +++ b/alas.py @@ -121,6 +121,11 @@ def run(self, command): content=f"<{self.config_name}> RequestHumanTakeover", ) exit(1) + except EmulatorNotRunningError: + logger.exception(e) + self.device.emulator_start() + time.sleep(10) + self.config.task_call('Restart') except Exception as e: logger.exception(e) self.save_error_log() @@ -131,6 +136,7 @@ def run(self, command): ) exit(1) + def save_error_log(self): """ Save last 60 screenshots in ./log/error/ diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index 73c0f22b9b..516febc6a3 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -12,6 +12,7 @@ from module.device.method.utils import ( ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, retry_sleep) from module.exception import RequestHumanTakeover +from module.exception import EmulatorNotRunningError from module.logger import logger @@ -75,6 +76,9 @@ def init(): def init(): pass + # Emulator not running + except EmulatorNotRunningError as e: + raise(e) # Unknown except Exception as e: logger.exception(e) From 429c6cb9307fb0ef6bf39499bee8c4aafe3cc941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CPPPlatelet=E2=80=9D?= Date: Wed, 22 May 2024 11:54:46 +0800 Subject: [PATCH 004/161] Opt: add one more, and it can run stably. :) --- alas.py | 4 ++-- module/device/method/droidcast.py | 4 ++-- module/device/method/uiautomator_2.py | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/alas.py b/alas.py index 5605fc3791..bbb5799718 100644 --- a/alas.py +++ b/alas.py @@ -121,8 +121,8 @@ def run(self, command): content=f"<{self.config_name}> RequestHumanTakeover", ) exit(1) - except EmulatorNotRunningError: - logger.exception(e) + except EmulatorNotRunningError as e: + logger.warning(e) self.device.emulator_start() time.sleep(10) self.config.task_call('Restart') diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index 516febc6a3..9da6463559 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -77,8 +77,8 @@ def init(): def init(): pass # Emulator not running - except EmulatorNotRunningError as e: - raise(e) + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index c116bce94a..ddb5e2978a 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -13,6 +13,7 @@ from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, ImageTruncated, PackageNotInstalled, possible_reasons) from module.exception import RequestHumanTakeover +from module.exception import EmulatorNotRunningError from module.logger import logger @@ -81,6 +82,9 @@ def init(): def init(): pass + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) From b6353069665e61e3577c9df90ad1aaa7f6e98994 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 23 May 2024 03:40:02 +0800 Subject: [PATCH 005/161] Opt: I dont like writing msg at all. :( Just delete one blank line. --- alas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/alas.py b/alas.py index bbb5799718..1fd18073f3 100644 --- a/alas.py +++ b/alas.py @@ -136,7 +136,6 @@ def run(self, command): ) exit(1) - def save_error_log(self): """ Save last 60 screenshots in ./log/error/ From a83be40ef3c0e4bc310a4a6f2a8ab5712ed514c1 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 27 May 2024 12:41:22 +0800 Subject: [PATCH 006/161] Partical rollback. :) --- alas.py | 11 +++---- config/template.json | 3 +- module/config/argument/args.json | 36 ++++++++++++---------- module/config/argument/argument.yaml | 1 + module/config/config_generated.py | 1 + module/config/i18n/en-US.json | 4 +++ module/config/i18n/ja-JP.json | 4 +++ module/config/i18n/zh-CN.json | 4 +++ module/config/i18n/zh-TW.json | 6 +++- module/device/connection.py | 3 ++ module/device/device.py | 10 ++++-- module/device/method/adb.py | 5 ++- module/device/method/ascreencap.py | 5 ++- module/device/method/hermit.py | 5 ++- module/device/method/maatouch.py | 5 ++- module/device/method/minitouch.py | 5 ++- module/device/method/nemu_ipc.py | 5 ++- module/device/method/scrcpy/scrcpy.py | 5 ++- module/device/method/wsa.py | 5 ++- module/device/platform/platform_windows.py | 31 ++++++++++++++----- 20 files changed, 112 insertions(+), 42 deletions(-) diff --git a/alas.py b/alas.py index 1fd18073f3..1679edca27 100644 --- a/alas.py +++ b/alas.py @@ -73,6 +73,11 @@ def run(self, command): logger.warning(e) self.config.task_call('Restart') return True + except EmulatorNotRunningError as e: + logger.warning(e) + self.device.emulator_start() + self.config.task_call('Restart') + return True except (GameStuckError, GameTooManyClickError) as e: logger.error(e) self.save_error_log() @@ -121,11 +126,6 @@ def run(self, command): content=f"<{self.config_name}> RequestHumanTakeover", ) exit(1) - except EmulatorNotRunningError as e: - logger.warning(e) - self.device.emulator_start() - time.sleep(10) - self.config.task_call('Restart') except Exception as e: logger.exception(e) self.save_error_log() @@ -474,7 +474,6 @@ def get_next_task(self): self.device.release_during_wait() if not self.wait_until(task.next_run): self.device.emulator_start() - time.sleep(10) self.config.task_call('Restart') del_cached_property(self, 'config') continue diff --git a/config/template.json b/config/template.json index 13cd3c054e..8d6087d82e 100644 --- a/config/template.json +++ b/config/template.json @@ -7,7 +7,8 @@ "ScreenshotMethod": "auto", "ControlMethod": "minitouch", "ScreenshotDedithering": false, - "AdbRestart": false + "AdbRestart": false, + "SilentStart": false }, "EmulatorInfo": { "Emulator": "auto", diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 99850080ed..3be9067064 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -138,6 +138,10 @@ "AdbRestart": { "type": "checkbox", "value": false + }, + "SilentStart": { + "type": "checkbox", + "value": false } }, "EmulatorInfo": { @@ -1705,13 +1709,13 @@ ], "display": "hide", "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", @@ -2040,13 +2044,13 @@ "event_20240521_cn" ], "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", @@ -2490,13 +2494,13 @@ "event_20240521_cn" ], "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", @@ -3886,13 +3890,13 @@ "event_20240521_cn" ], "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", @@ -4353,13 +4357,13 @@ "event_20240521_cn" ], "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", @@ -4820,13 +4824,13 @@ "event_20240521_cn" ], "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", @@ -5287,13 +5291,13 @@ "event_20240521_cn" ], "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", @@ -5744,13 +5748,13 @@ "event_20240521_cn" ], "option_bold": [ - "event_20210916_cn", + "event_20230525_cn", "event_20240521_cn" ], "cn": "event_20240521_cn", "en": "event_20240521_cn", "jp": "event_20240521_cn", - "tw": "event_20210916_cn" + "tw": "event_20230525_cn" }, "Mode": { "type": "select", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 10202f21bb..9219ab140f 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -55,6 +55,7 @@ Emulator: ] ScreenshotDedithering: false AdbRestart: false + SilentStart: false EmulatorInfo: Emulator: value: auto diff --git a/module/config/config_generated.py b/module/config/config_generated.py index ee75a2ad4e..0094beda54 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -25,6 +25,7 @@ class GeneratedConfig: Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False + Emulator_SilentStart = False # Group `EmulatorInfo` EmulatorInfo_Emulator = 'auto' # auto, NoxPlayer, NoxPlayer64, BlueStacks4, BlueStacks5, BlueStacks4HyperV, BlueStacks5HyperV, LDPlayer3, LDPlayer4, LDPlayer9, MuMuPlayer, MuMuPlayerX, MuMuPlayer12, MEmuPlayer diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 9fe78a7e8e..ebc5199030 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -428,6 +428,10 @@ "AdbRestart": { "name": "Try to restart adb when no device found", "help": "" + }, + "SilentStart": { + "name": "Start the emulator silently", + "help": "If this option is checked, the emulator launched by Alas will run silently in the background. \nIf you need to monitor the emulator, do not check it. \nAdditionally the emulator will be killed when the scheduler ends" } }, "EmulatorInfo": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index ae71b6295c..56ab755402 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -428,6 +428,10 @@ "AdbRestart": { "name": "Emulator.AdbRestart.name", "help": "Emulator.AdbRestart.help" + }, + "SilentStart": { + "name": "Emulator.SilentStart.name", + "help": "Emulator.SilentStart.help" } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index caf4bf6f42..a052eb6b21 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -428,6 +428,10 @@ "AdbRestart": { "name": "在检测不到设备的时候尝试重启adb", "help": "" + }, + "SilentStart": { + "name": "静默启动模拟器", + "help": "勾选此项后由Alas启动的模拟器将会在后台静默运行,若需要监看模拟器运行状况则不要勾选,且调度器结束后模拟器将被中止" } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index d6b1bb263a..b3a3bb855b 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -428,6 +428,10 @@ "AdbRestart": { "name": "在檢測不到設備的時候嘗試重啟adb", "help": "" + }, + "SilentStart": { + "name": "靜默啓働模擬器", + "help": "勾選此項後由Alas啓働的模擬器將會在後颱靜默運行,若需要監看模擬器運行狀況則不要勾選,且調度器結束後模擬器將被中止" } }, "EmulatorInfo": { @@ -716,7 +720,7 @@ "event_20221124_cn": "鍊金術士與秘密遺跡群島", "event_20221222_cn": "定向折疊", "event_20230223_cn": "湮燼塵墟", - "event_20230525_cn": "Confluence of Nothingness", + "event_20230525_cn": "空相交會點", "event_20230803_cn": "奏響鳶尾之歌", "event_20230817_cn": "愚者的天平", "event_20230914_cn": "Effulgence Before Eclipse", diff --git a/module/device/connection.py b/module/device/connection.py index 71ebbdeef7..03e783c41e 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -59,6 +59,9 @@ def init(): def init(): self.detect_package() + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/device.py b/module/device/device.py index 40e04e9c29..d68e14928f 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -308,8 +308,14 @@ def app_stop(self): self.click_record_clear() def emulator_stop(self): - #kill emulator - if self.emulator_instance: + # kill emulator + if self.emulator_instance is not None: super().emulator_stop() + else: + logger.critical( + f'No emulator with serial "{self.config.Emulator_Serial}" found, ' + f'please set a correct serial' + ) + raise self.stuck_record_clear() self.click_record_clear() \ No newline at end of file diff --git a/module/device/method/adb.py b/module/device/method/adb.py index bd3397edf7..2aedbb568e 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -11,7 +11,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, remove_prefix, handle_adb_error, ImageTruncated, PackageNotInstalled) -from module.exception import RequestHumanTakeover, ScriptError +from module.exception import EmulatorNotRunningError, RequestHumanTakeover, ScriptError from module.logger import logger @@ -57,6 +57,9 @@ def init(): def init(): pass + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) diff --git a/module/device/method/ascreencap.py b/module/device/method/ascreencap.py index c93321cfe2..d6130ee51d 100644 --- a/module/device/method/ascreencap.py +++ b/module/device/method/ascreencap.py @@ -8,7 +8,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, ImageTruncated) -from module.exception import RequestHumanTakeover, ScriptError +from module.exception import EmulatorNotRunningError, RequestHumanTakeover, ScriptError from module.logger import logger @@ -58,6 +58,9 @@ def init(): def init(): pass + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) diff --git a/module/device/method/hermit.py b/module/device/method/hermit.py index ad0c39746e..e474ff8eb8 100644 --- a/module/device/method/hermit.py +++ b/module/device/method/hermit.py @@ -10,7 +10,7 @@ from module.device.method.adb import Adb from module.device.method.utils import (RETRY_TRIES, retry_sleep, HierarchyButton, handle_adb_error) -from module.exception import RequestHumanTakeover +from module.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger @@ -71,6 +71,9 @@ def init(): def init(): self.adb_reconnect() self.hermit_init() + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index 04f1d1aa1d..55ebc42298 100644 --- a/module/device/method/maatouch.py +++ b/module/device/method/maatouch.py @@ -11,7 +11,7 @@ from module.device.connection import Connection from module.device.method.minitouch import Command, CommandBuilder, insert_swipe from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep -from module.exception import RequestHumanTakeover +from module.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger @@ -75,6 +75,9 @@ def init(): def init(): del_cached_property(self, '_maatouch_builder') + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index d48ed4191f..aeb100ad36 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -15,7 +15,7 @@ from module.base.utils import * from module.device.connection import Connection from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep -from module.exception import RequestHumanTakeover, ScriptError +from module.exception import EmulatorNotRunningError, RequestHumanTakeover, ScriptError from module.logger import logger @@ -440,6 +440,9 @@ def init(): def init(): del_cached_property(self, '_minitouch_builder') + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/nemu_ipc.py b/module/device/method/nemu_ipc.py index b79e1c3225..4ca8ed82bf 100644 --- a/module/device/method/nemu_ipc.py +++ b/module/device/method/nemu_ipc.py @@ -12,7 +12,7 @@ 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.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger @@ -184,6 +184,9 @@ def init(): def init(): self.reconnect() + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/scrcpy/scrcpy.py b/module/device/method/scrcpy/scrcpy.py index 0730e9bac7..4b23dd85ed 100644 --- a/module/device/method/scrcpy/scrcpy.py +++ b/module/device/method/scrcpy/scrcpy.py @@ -11,7 +11,7 @@ from module.device.method.scrcpy.core import ScrcpyCore, ScrcpyError from module.device.method.uiautomator_2 import Uiautomator2 from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep -from module.exception import RequestHumanTakeover +from module.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger @@ -64,6 +64,9 @@ def init(): self.adb_reconnect() else: break + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/wsa.py b/module/device/method/wsa.py index 56f0ea23f6..0351fe182c 100644 --- a/module/device/method/wsa.py +++ b/module/device/method/wsa.py @@ -6,7 +6,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, PackageNotInstalled) -from module.exception import RequestHumanTakeover +from module.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger @@ -46,6 +46,9 @@ def init(): def init(): self.detect_package() + # Emulator not running + except EmulatorNotRunningError: + raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index de00efeca3..4125786a3f 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,8 +1,9 @@ import ctypes import re -import subprocess import psutil +import shlex +import win32api from deploy.Windows.utils import DataProcessInfo from module.base.decorator import run_once @@ -43,8 +44,7 @@ def flash_window(hwnd, flash=True): class PlatformWindows(PlatformBase, EmulatorManager): - @classmethod - def execute(cls, command): + def execute(self, command): """ Args: command (str): @@ -52,9 +52,12 @@ def execute(cls, command): 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 + parts = shlex.split(command) + command = parts[0] + params = " ".join(parts[1:]) + silent = 0 if self.config.Emulator_SilentStart else 1 + logger.info(f'Execute: {command} {params}') + return win32api.ShellExecute(hwnd=0, op='open', file=command, params=params, dir='', bShow=silent) @classmethod def kill_process_by_regex(cls, regex: str) -> int: @@ -312,8 +315,20 @@ def emulator_start(self): def emulator_stop(self): logger.hr('Emulator stop', level=1) - return self._emulator_function_wrapper(self._emulator_stop) - + for _ in range(3): + # Start + if not self._emulator_function_wrapper(self._emulator_start): + return False + # Stop + if self._emulator_function_wrapper(self._emulator_stop): + # Success + return True + else: + # Failed to stop, start and stop again + if self._emulator_function_wrapper(self._emulator_start): + continue + else: + return False if __name__ == '__main__': self = PlatformWindows('alas') From 0a1466754db2461fcac909a3acf1eea82999f976 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 27 May 2024 14:56:14 +0800 Subject: [PATCH 007/161] Fix: Add bugs. :) --- module/device/platform/platform_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 4125786a3f..2d16656784 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -57,7 +57,7 @@ def execute(self, command): params = " ".join(parts[1:]) silent = 0 if self.config.Emulator_SilentStart else 1 logger.info(f'Execute: {command} {params}') - return win32api.ShellExecute(hwnd=0, op='open', file=command, params=params, dir='', bShow=silent) + return win32api.ShellExecute(0, 'open', command, params, '', silent) @classmethod def kill_process_by_regex(cls, regex: str) -> int: From 7f4b280046a53929ac3a3dc3490ecc28dc40231d Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 27 May 2024 14:56:14 +0800 Subject: [PATCH 008/161] Fix: Add bugs. :) --- module/device/platform/platform_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 4125786a3f..2d16656784 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -57,7 +57,7 @@ def execute(self, command): params = " ".join(parts[1:]) silent = 0 if self.config.Emulator_SilentStart else 1 logger.info(f'Execute: {command} {params}') - return win32api.ShellExecute(hwnd=0, op='open', file=command, params=params, dir='', bShow=silent) + return win32api.ShellExecute(0, 'open', command, params, '', silent) @classmethod def kill_process_by_regex(cls, regex: str) -> int: From 999a3c80255089f8d1a77dcfbaf42d72ab07a1c7 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 30 May 2024 19:36:22 +0800 Subject: [PATCH 009/161] Opt: Support Linux start. :) --- module/device/platform/platform_windows.py | 59 +++++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 2d16656784..0b08ea6bc9 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,9 +1,12 @@ import ctypes import re +import sys +import os +import subprocess import psutil -import shlex -import win32api +from shlex import split +from win32api import ShellExecute from deploy.Windows.utils import DataProcessInfo from module.base.decorator import run_once @@ -52,12 +55,52 @@ def execute(self, command): Returns: subprocess.Popen: """ - parts = shlex.split(command) - command = parts[0] - params = " ".join(parts[1:]) - silent = 0 if self.config.Emulator_SilentStart else 1 - logger.info(f'Execute: {command} {params}') - return win32api.ShellExecute(0, 'open', command, params, '', silent) + if not self.config.Emulator_SilentStart: + + # Win32 + if sys.platform == 'win32': + return subprocess.Popen( + command, + creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, + close_fds=True + ) + # Linux + return subprocess.Popen( + command, + preexec_fn=os.setpgrp + ) + + # Win32 + if sys.platform == 'win32': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + return subprocess.Popen( + command, + startupinfo=startupinfo, + creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + close_fds=True + ) + + # Linux + return subprocess.Popen( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + preexec_fn=os.setpgrp, + ) + + + # parts = split(command) + # command = parts[0] + # params = " ".join(parts[1:]) + # silent = 0 if self.config.Emulator_SilentStart else 1 + # logger.info(f'Execute: {command} {params}') + # return ShellExecute(0, 'open', command, params, '', silent) # Windows only @classmethod def kill_process_by_regex(cls, regex: str) -> int: From 9ceedf1a8b1ddb814524490f8f909f163e39099d Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 30 May 2024 12:36:49 +0800 Subject: [PATCH 010/161] Dev: [ALAS] Log ScriptError as exception so traceback can be logged --- alas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alas.py b/alas.py index 1679edca27..e0f0ffe376 100644 --- a/alas.py +++ b/alas.py @@ -110,7 +110,7 @@ def run(self, command): self.checker.wait_until_available() return False except ScriptError as e: - logger.critical(e) + logger.exception(e) logger.critical('This is likely to be a mistake of developers, but sometimes just random issues') handle_notify( self.config.Error_OnePushConfig, From 722c0d74d77d102fc99378a745f2760514f7a765 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 30 May 2024 12:37:03 +0800 Subject: [PATCH 011/161] Fix: [ALAS] Game restarted twice if close game during wait --- alas.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/alas.py b/alas.py index e0f0ffe376..88d2da3339 100644 --- a/alas.py +++ b/alas.py @@ -451,7 +451,8 @@ def get_next_task(self): if not self.wait_until(task.next_run): del_cached_property(self, 'config') continue - self.run('start') + if task.command != 'Restart': + self.run('start') elif method == 'goto_main': logger.info('Goto main page during wait') self.run('goto_main') From 17d8471de632ba525843cf4bfe7c91833ea5d186 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 30 May 2024 13:34:24 +0800 Subject: [PATCH 012/161] Chore: [ALAS] Move func methods to the Alas class --- alas.py | 20 ++++++++++++++++++++ module/base/base.py | 5 +++++ module/config/config.py | 25 +++++++++++++++---------- module/submodule/utils.py | 10 ++++++++++ module/webui/process_manager.py | 25 +++++-------------------- 5 files changed, 55 insertions(+), 30 deletions(-) diff --git a/alas.py b/alas.py index 88d2da3339..5ba8e39a6b 100644 --- a/alas.py +++ b/alas.py @@ -399,6 +399,26 @@ def gems_farming(self): GemsFarming(config=self.config, device=self.device).run( name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode) + def daemon(self): + from module.daemon.daemon import AzurLaneDaemon + AzurLaneDaemon(config=self.config, device=self.device, task="Daemon").run() + + def opsi_daemon(self): + from module.daemon.os_daemon import AzurLaneDaemon + AzurLaneDaemon(config=self.config, device=self.device, task="OpsiDaemon").run() + + def azur_lane_uncensored(self): + from module.daemon.uncensored import AzurLaneUncensored + AzurLaneUncensored(config=self.config, device=self.device, task="AzurLaneUncensored").run() + + def benchmark(self): + from module.daemon.benchmark import run_benchmark + run_benchmark(config=self.config) + + def game_manager(self): + from module.daemon.game_manager import GameManager + GameManager(config=self.config, device=self.device, task="GameManager").run() + def wait_until(self, future): """ Wait until a specific time. diff --git a/module/base/base.py b/module/base/base.py index f59db88543..a2de748673 100644 --- a/module/base/base.py +++ b/module/base/base.py @@ -34,6 +34,8 @@ def __init__(self, config, device=None, task=None): """ if isinstance(config, AzurLaneConfig): self.config = config + if task is not None: + self.config.init_task(task) elif isinstance(config, str): self.config = AzurLaneConfig(config, task=task) else: @@ -73,6 +75,9 @@ def early_ocr_import(self): if not self.config.is_actual_task: logger.info('No actual task bound, skip early_ocr_import') return + if self.config.task.command in ['Daemon', 'OpsiDaemon']: + logger.info('No ocr in daemon task, skip early_ocr_import') + return def do_ocr_import(): # Wait first image diff --git a/module/config/config.py b/module/config/config.py index c916609373..42005801f8 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -104,17 +104,22 @@ def __init__(self, config_name, task=None): logger.info("Using template config, which is read only") self.auto_update = False self.task = name_to_function("template") + self.init_task(task) + + def init_task(self, task=None): + if self.is_template_config: + return + + self.load() + if task is None: + # Bind `Alas` by default which includes emulator settings. + task = name_to_function("Alas") else: - self.load() - if task is None: - # Bind `Alas` by default which includes emulator settings. - task = name_to_function("Alas") - else: - # Bind a specific task for debug purpose. - task = name_to_function(task) - self.bind(task) - self.task = task - self.save() + # Bind a specific task for debug purpose. + task = name_to_function(task) + self.bind(task) + self.task = task + self.save() def load(self): self.data = self.read_file(self.config_name) diff --git a/module/submodule/utils.py b/module/submodule/utils.py index 317b36c078..800dabe5da 100644 --- a/module/submodule/utils.py +++ b/module/submodule/utils.py @@ -13,6 +13,16 @@ MOD_CONFIG_DICT = {} +def get_available_func(): + return ( + 'Daemon', + 'OpsiDaemon', + 'AzurLaneUncensored', + 'Benchmark', + 'GameManager', + ) + + def get_available_mod(): return set(MOD_DICT) diff --git a/module/webui/process_manager.py b/module/webui/process_manager.py index 2964f1d1a0..3b260565a8 100644 --- a/module/webui/process_manager.py +++ b/module/webui/process_manager.py @@ -12,7 +12,8 @@ from module.config.utils import filepath_config from module.logger import logger, set_file_logger, set_func_logger from module.submodule.submodule import load_mod -from module.submodule.utils import get_available_mod, get_available_mod_func, get_config_mod, get_func_mod, list_mod_instance +from module.submodule.utils import get_available_func, get_available_mod, get_available_mod_func, get_config_mod, \ + get_func_mod, list_mod_instance from module.webui.setting import State @@ -149,26 +150,10 @@ def run_process( if e is not None: AzurLaneAutoScript.stop_event = e AzurLaneAutoScript(config_name=config_name).loop() - elif func == "Daemon": - from module.daemon.daemon import AzurLaneDaemon - - AzurLaneDaemon(config=config_name, task="Daemon").run() - elif func == "OpsiDaemon": - from module.daemon.os_daemon import AzurLaneDaemon - - AzurLaneDaemon(config=config_name, task="OpsiDaemon").run() - elif func == "AzurLaneUncensored": - from module.daemon.uncensored import AzurLaneUncensored - - AzurLaneUncensored(config=config_name, task="AzurLaneUncensored").run() - elif func == "Benchmark": - from module.daemon.benchmark import run_benchmark - - run_benchmark(config=config_name) - elif func == "GameManager": - from module.daemon.game_manager import GameManager + elif func in get_available_func(): + from alas import AzurLaneAutoScript - GameManager(config=config_name, task="GameManager").run() + AzurLaneAutoScript(config_name=config_name).run(inflection.underscore(func)) elif func in get_available_mod(): mod = load_mod(func) From 725c791ff585fa3197dd71aba8f8b5d05070e77a Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 30 May 2024 21:44:34 +0800 Subject: [PATCH 013/161] Fix: Disable mail collect before assets updated --- module/freebies/freebies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/freebies/freebies.py b/module/freebies/freebies.py index 3e5b4017a8..1f836fc948 100644 --- a/module/freebies/freebies.py +++ b/module/freebies/freebies.py @@ -19,9 +19,9 @@ def run(self): logger.hr('Data key', level=1) DataKey(self.config, self.device).run() - if self.config.Mail_Collect: - logger.hr('Mail', level=1) - Mail(self.config, self.device).run() + # if self.config.Mail_Collect: + # logger.hr('Mail', level=1) + # Mail(self.config, self.device).run() if self.config.SupplyPack_Collect: logger.hr('Supply pack', level=1) From 2b29c0f3dfc3506ba9f890f102395d7397d44e7d Mon Sep 17 00:00:00 2001 From: rilylc Date: Wed, 29 May 2024 18:13:18 +0800 Subject: [PATCH 014/161] Upd:[TW]dorm assets --- assets/tw/dorm/DORM_RED_DOT.png | Bin 4238 -> 9907 bytes module/dorm/assets.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/tw/dorm/DORM_RED_DOT.png b/assets/tw/dorm/DORM_RED_DOT.png index 892c271dea870023312f8187fe2f942d6a54b234..453a0f28f2d983542349c3e1304c4133babecc7d 100644 GIT binary patch literal 9907 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 Date: Fri, 31 May 2024 00:32:21 +0800 Subject: [PATCH 015/161] Sync upstream. :) --- module/device/platform/platform_windows.py | 26 +++++++--------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 0b08ea6bc9..c0beb733d5 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,12 +1,8 @@ import ctypes import re -import sys -import os import subprocess import psutil -from shlex import split -from win32api import ShellExecute from deploy.Windows.utils import DataProcessInfo from module.base.decorator import run_once @@ -55,23 +51,17 @@ def execute(self, command): Returns: subprocess.Popen: """ + from sys import platform if not self.config.Emulator_SilentStart: - # Win32 - if sys.platform == 'win32': - return subprocess.Popen( - command, - creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, - close_fds=True - ) + if platform == 'win32': + return subprocess.Popen(command,close_fds=True) # Linux - return subprocess.Popen( - command, - preexec_fn=os.setpgrp - ) + from os import setsid + return subprocess.Popen(command,preexec_fn=setsid) # Win32 - if sys.platform == 'win32': + if platform == 'win32': startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE @@ -86,15 +76,15 @@ def execute(self, command): ) # Linux + from os import setsid return subprocess.Popen( command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, - preexec_fn=os.setpgrp, + preexec_fn=setsid, ) - # parts = split(command) # command = parts[0] # params = " ".join(parts[1:]) From f0bcca073f54a7929f6e57f8dbe825421bc88a31 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 31 May 2024 00:32:21 +0800 Subject: [PATCH 016/161] Fix: Optimize texts. :() --- module/config/i18n/en-US.json | 2 +- module/config/i18n/zh-CN.json | 2 +- module/config/i18n/zh-TW.json | 2 +- module/device/platform/platform_windows.py | 7 ------- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index ebc5199030..03b3f739b5 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -431,7 +431,7 @@ }, "SilentStart": { "name": "Start the emulator silently", - "help": "If this option is checked, the emulator launched by Alas will run silently in the background. \nIf you need to monitor the emulator, do not check it. \nAdditionally the emulator will be killed when the scheduler ends" + "help": "If this option is checked, the emulator launched by Alas will run silently in the background. \nIf you need to monitor the emulator, do not check it." } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index a052eb6b21..219b91cb3b 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -431,7 +431,7 @@ }, "SilentStart": { "name": "静默启动模拟器", - "help": "勾选此项后由Alas启动的模拟器将会在后台静默运行,若需要监看模拟器运行状况则不要勾选,且调度器结束后模拟器将被中止" + "help": "勾选此项后由Alas启动的模拟器将会在后台静默运行,若需要监看模拟器运行状况则不要勾选。" } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index b3a3bb855b..4a47595a5c 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -431,7 +431,7 @@ }, "SilentStart": { "name": "靜默啓働模擬器", - "help": "勾選此項後由Alas啓働的模擬器將會在後颱靜默運行,若需要監看模擬器運行狀況則不要勾選,且調度器結束後模擬器將被中止" + "help": "勾選此項後由Alas啓働的模擬器將會在後颱靜默運行,若需要監看模擬器運行狀況則不要勾選。" } }, "EmulatorInfo": { diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index c0beb733d5..63d697f4a1 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -85,13 +85,6 @@ def execute(self, command): preexec_fn=setsid, ) - # parts = split(command) - # command = parts[0] - # params = " ".join(parts[1:]) - # silent = 0 if self.config.Emulator_SilentStart else 1 - # logger.info(f'Execute: {command} {params}') - # return ShellExecute(0, 'open', command, params, '', silent) # Windows only - @classmethod def kill_process_by_regex(cls, regex: str) -> int: """ From 19f0a1811c678d83c4490a0a25cd13df54908065 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 31 May 2024 02:43:37 +0800 Subject: [PATCH 017/161] Opt: Add LDPlayer start/stop support. (May not stable) XD --- module/device/platform/emulator_base.py | 7 +++++ module/device/platform/platform_windows.py | 34 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/module/device/platform/emulator_base.py b/module/device/platform/emulator_base.py index 394becc244..3e9717a040 100644 --- a/module/device/platform/emulator_base.py +++ b/module/device/platform/emulator_base.py @@ -114,6 +114,13 @@ def MuMuPlayer12_id(self): return None + @cached_property + def LDPlayer_id(self): + res = re.search(r'leidian(\d+)', self.name) + if res: + return int(res.group(1)) + + return None class EmulatorBase: # Values here must match those in argument.yaml EmulatorInfo.Emulator.option diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 63d697f4a1..fcd4447c2c 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -43,7 +43,7 @@ def flash_window(hwnd, flash=True): class PlatformWindows(PlatformBase, EmulatorManager): - def execute(self, command): + def execute(self, command:str): """ Args: command (str): @@ -51,6 +51,8 @@ def execute(self, command): Returns: subprocess.Popen: """ + command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + logger.info(f'Execute: {command}') from sys import platform if not self.config.Emulator_SilentStart: # Win32 @@ -123,6 +125,15 @@ def _emulator_start(self, instance: EmulatorInstance): 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.LDPlayer3: + # LDPlayer.exe index=0 + self.execute(f'"{exe}" index={instance.LDPlayer_id}') + elif instance == Emulator.LDPlayer4: + # LDPlayer.exe index=0 + self.execute(f'"{exe}" index={instance.LDPlayer_id}') + elif instance == Emulator.LDPlayer9: + # LDPlayer.exe index=0 + self.execute(f'"{exe}" index={instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 self.execute(f'"{exe}" -clone:{instance.name}') @@ -183,6 +194,27 @@ def _emulator_stop(self, instance: EmulatorInstance): ) # There is also a shared service, no need to kill it # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMSVC.exe" --Embedding + # LDPlayer has simply 1 process + # E:\Program Files\leidian\LDPlayer9\dnplayer.exe index=0 + # Maybe "E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0" is better? I don't know. XD + elif instance == Emulator.LDPlayer3: + self.kill_process_by_regex( + rf'(' + rf'dnplayer.exe.*index={instance.LDPlayer_id}' + rf')' + ) + elif instance == Emulator.LDPlayer4: + self.kill_process_by_regex( + rf'(' + rf'dnplayer.exe.*index={instance.LDPlayer_id}' + rf')' + ) + elif instance == Emulator.LDPlayer9: + self.kill_process_by_regex( + rf'(' + rf'dnplayer.exe.*index={instance.LDPlayer_id}' + rf')' + ) elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit self.execute(f'"{exe}" -clone:{instance.name} -quit') From e8068d851f0585ceb3f50a7fdcd62d40194c7888 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 31 May 2024 05:01:13 +0800 Subject: [PATCH 018/161] Fix: Delete him. --- module/device/method/droidcast.py | 3 +- module/device/method/uiautomator_2.py | 3 +- module/device/platform/emulator_base.py | 9 ++++ module/device/platform/platform_windows.py | 54 +++++++++------------- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index 9da6463559..3c43b2be42 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -11,8 +11,7 @@ from module.device.method.uiautomator_2 import ProcessInfo, Uiautomator2 from module.device.method.utils import ( ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, retry_sleep) -from module.exception import RequestHumanTakeover -from module.exception import EmulatorNotRunningError +from module.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index ddb5e2978a..4b44ff6b4f 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -12,8 +12,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, ImageTruncated, PackageNotInstalled, possible_reasons) -from module.exception import RequestHumanTakeover -from module.exception import EmulatorNotRunningError +from module.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger diff --git a/module/device/platform/emulator_base.py b/module/device/platform/emulator_base.py index 3e9717a040..869be5809b 100644 --- a/module/device/platform/emulator_base.py +++ b/module/device/platform/emulator_base.py @@ -116,6 +116,15 @@ def MuMuPlayer12_id(self): @cached_property def LDPlayer_id(self): + """ + Convert LDPlayer instance name to instance id. + Example names: + leidian0 + leidian1 + + Returns: + int: Instance ID, or None if this is not a LDPlayer instance + """ res = re.search(r'leidian(\d+)', self.name) if res: return int(res.group(1)) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index fcd4447c2c..51f10dcd43 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -43,7 +43,7 @@ def flash_window(hwnd, flash=True): class PlatformWindows(PlatformBase, EmulatorManager): - def execute(self, command:str): + def execute(self, command: str): """ Args: command (str): @@ -87,6 +87,19 @@ def execute(self, command:str): preexec_fn=setsid, ) + @classmethod + def kill_process(cls, command: str): + """ + Args: + command (str): + + Returns: + subprocess.Popen: + """ + command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + logger.info(f'Kill: {command}') + return subprocess.Popen(command,close_fds=True,shell=True) + @classmethod def kill_process_by_regex(cls, regex: str) -> int: """ @@ -113,7 +126,7 @@ def _emulator_start(self, instance: EmulatorInstance): """ Start a emulator without error handling """ - exe = instance.emulator.path + exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: # NemuPlayer.exe self.execute(exe) @@ -125,13 +138,7 @@ def _emulator_start(self, instance: EmulatorInstance): 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.LDPlayer3: - # LDPlayer.exe index=0 - self.execute(f'"{exe}" index={instance.LDPlayer_id}') - elif instance == Emulator.LDPlayer4: - # LDPlayer.exe index=0 - self.execute(f'"{exe}" index={instance.LDPlayer_id}') - elif instance == Emulator.LDPlayer9: + elif instance == Emulator.LDPlayerFamily: # LDPlayer.exe index=0 self.execute(f'"{exe}" index={instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: @@ -151,7 +158,7 @@ def _emulator_stop(self, instance: EmulatorInstance): Stop a emulator without error handling """ logger.hr('Emulator stop', level=2) - exe = instance.emulator.path + exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: # MuMu6 does not have multi instance, kill one means kill all # Has 4 processes @@ -194,30 +201,13 @@ def _emulator_stop(self, instance: EmulatorInstance): ) # There is also a shared service, no need to kill it # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMSVC.exe" --Embedding - # LDPlayer has simply 1 process - # E:\Program Files\leidian\LDPlayer9\dnplayer.exe index=0 - # Maybe "E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0" is better? I don't know. XD - elif instance == Emulator.LDPlayer3: - self.kill_process_by_regex( - rf'(' - rf'dnplayer.exe.*index={instance.LDPlayer_id}' - rf')' - ) - elif instance == Emulator.LDPlayer4: - self.kill_process_by_regex( - rf'(' - rf'dnplayer.exe.*index={instance.LDPlayer_id}' - rf')' - ) - elif instance == Emulator.LDPlayer9: - self.kill_process_by_regex( - rf'(' - rf'dnplayer.exe.*index={instance.LDPlayer_id}' - rf')' - ) + elif instance == Emulator.LDPlayerFamily: + # LDPlayer has simply 1 process + # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 + self.kill_process(f'{exe.rsplit('/',1)[0]}/dnconsole.exe quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit - self.execute(f'"{exe}" -clone:{instance.name} -quit') + self.kill_process(f'"{exe}" -clone:{instance.name} -quit') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') From 798181d2d52b656fb160e23ababd174ecdfa66c3 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 1 Jun 2024 22:59:33 +0800 Subject: [PATCH 019/161] Opt: Add stop(LDPlayer, BlueStack4&5) support. :()()()()()()()()()()()()()()() --- module/device/platform/emulator_windows.py | 6 +++--- module/device/platform/platform_windows.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/module/device/platform/emulator_windows.py b/module/device/platform/emulator_windows.py index f7a5e54bc5..cdf74ffeae 100644 --- a/module/device/platform/emulator_windows.py +++ b/module/device/platform/emulator_windows.py @@ -89,8 +89,8 @@ def path_to_type(cls, path: str) -> str: return cls.NoxPlayer64 else: return cls.NoxPlayer - if exe == 'bluestacks.exe': - if dir1 in ['bluestacks', 'bluestacks_cn']: + if exe in ['bluestacks.exe', 'bluestacksgp.exe']: + if dir1 in ['bluestacks', 'bluestacks_cn', 'bluestackscn']: return cls.BlueStacks4 elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']: return cls.BlueStacks5 @@ -224,7 +224,7 @@ def iter_instances(self): elif self == Emulator.BlueStacks4: # ../Engine/Android regex = re.compile(r'^Android') - for folder in self.list_folder('../Engine', is_dir=True): + for folder in self.list_folder('./Engine/ProgramData/Engine', is_dir=True): folder = os.path.basename(folder) res = regex.match(folder) if not res: diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 51f10dcd43..992db07bd4 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -97,7 +97,7 @@ def kill_process(cls, command: str): subprocess.Popen: """ command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') - logger.info(f'Kill: {command}') + logger.info(f'Execute: {command}') return subprocess.Popen(command,close_fds=True,shell=True) @classmethod @@ -204,10 +204,23 @@ def _emulator_stop(self, instance: EmulatorInstance): elif instance == Emulator.LDPlayerFamily: # LDPlayer has simply 1 process # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 - self.kill_process(f'{exe.rsplit('/',1)[0]}/dnconsole.exe quit --index {instance.LDPlayer_id}') + self.kill_process(f'"{exe.rsplit("/",1)[0]}/dnconsole.exe" quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit self.kill_process(f'"{exe}" -clone:{instance.name} -quit') + elif instance == Emulator.BlueStacks5: + # BlueStack has 2 processes + # C:\Program Files\BlueStacks_nxt_cn\HD-Player.exe --instance Pie64 + # C:\Program Files\BlueStacks_nxt_cn\BstkSVC.exe -Embedding + self.kill_process_by_regex( + rf'(' + rf'HD-Player.exe.*"--instance" "{instance.name}"' + rf'BstkSVC.exe.*-Embedding' + rf')' + ) + elif instance == Emulator.BlueStacks4: + # E:\Program Files (x86)\BluestacksCN\bsconsole.exe quit --name Android + self.kill_process(f'"{exe.rsplit("/",1)[0]}/bsconsole.exe" quit --name {instance.name}') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') From 2dbedf0c566b66152df424afa798a716f6aed52b Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 3 Jun 2024 00:32:43 +0800 Subject: [PATCH 020/161] Opt: Add start/stop MEmuPlayer support. :) --- alas.py | 9 ++++- module/device/device.py | 5 ++- module/device/platform/platform_windows.py | 44 ++++++---------------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/alas.py b/alas.py index 5ba8e39a6b..fec00de46d 100644 --- a/alas.py +++ b/alas.py @@ -26,6 +26,7 @@ def __init__(self, config_name='alas'): # Failure count of tasks # Key: str, task name, value: int, failure count self.failure_record = {} + self.emulator_stopped = False @cached_property def config(self): @@ -491,11 +492,10 @@ def get_next_task(self): elif method == 'stop_emulator': logger.info('Stop emulator during wait') self.device.emulator_stop() + self.emulator_stopped = True release_resources() self.device.release_during_wait() if not self.wait_until(task.next_run): - self.device.emulator_start() - self.config.task_call('Restart') del_cached_property(self, 'config') continue else: @@ -535,6 +535,11 @@ def loop(self): _ = self.device # Get task task = self.get_next_task() + # Reboot emulator + if self.emulator_stopped: + self.device.emulator_start() + self.config.task_call('Restart') + self.emulator_stopped = False # Skip first restart if self.is_first_task and task == 'Restart': logger.info('Skip task `Restart` at scheduler start') diff --git a/module/device/device.py b/module/device/device.py index d68e14928f..8e7e27fbb0 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -318,4 +318,7 @@ def emulator_stop(self): ) raise self.stuck_record_clear() - self.click_record_clear() \ No newline at end of file + self.click_record_clear() + + def emulator_start(self): + return super().emulator_start() \ No newline at end of file diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 992db07bd4..ae32b2fe3a 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -51,18 +51,14 @@ def execute(self, command: str): Returns: subprocess.Popen: """ + # CAUTION!!!!!!: Windows only. command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') from sys import platform if not self.config.Emulator_SilentStart: - # Win32 if platform == 'win32': return subprocess.Popen(command,close_fds=True) - # Linux - from os import setsid - return subprocess.Popen(command,preexec_fn=setsid) - # Win32 if platform == 'win32': startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW @@ -77,16 +73,6 @@ def execute(self, command: str): close_fds=True ) - # Linux - from os import setsid - return subprocess.Popen( - command, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - preexec_fn=setsid, - ) - @classmethod def kill_process(cls, command: str): """ @@ -150,6 +136,9 @@ def _emulator_start(self, instance: EmulatorInstance): elif instance == Emulator.BlueStacks4: # BlueStacks\Client\Bluestacks.exe -vmname Android_1 self.execute(f'"{exe}" -vmname {instance.name}') + elif instance == Emulator.MEmuPlayer: + # MEmu.exe MEmu_0 + self.execute(f'"{exe}" {instance.name}') else: raise EmulatorUnknown(f'Cannot start an unknown emulator instance: {instance}') @@ -157,6 +146,7 @@ def _emulator_stop(self, instance: EmulatorInstance): """ Stop a emulator without error handling """ + import os logger.hr('Emulator stop', level=2) exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: @@ -188,23 +178,13 @@ def _emulator_stop(self, instance: EmulatorInstance): rf')' ) elif instance == Emulator.MuMuPlayer12: - # MuMu 12 has 2 processes: - # E:\ProgramFiles\Netease\MuMuPlayer-12.0\shell\MuMuPlayer.exe -v 0 - # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMHeadless.exe" --comment MuMuPlayer-12.0-0 --startvm xxx + # E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.kill_process_by_regex( - rf'(' - rf'MuMuVMMHeadless.exe.*--comment {instance.name}' - rf'|MuMuPlayer.exe.*-v {instance.MuMuPlayer12_id}' - rf')' - ) - # There is also a shared service, no need to kill it - # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMSVC.exe" --Embedding + self.kill_process(f'"{os.path.join(os.path.dirname(exe),"MuMuManager.exe")}" api -v {instance.MuMuPlayer12_id} shutdown_player') elif instance == Emulator.LDPlayerFamily: - # LDPlayer has simply 1 process # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 - self.kill_process(f'"{exe.rsplit("/",1)[0]}/dnconsole.exe" quit --index {instance.LDPlayer_id}') + self.kill_process(f'"{os.path.join(os.path.dirname(exe),"dnconsole.exe")}" quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit self.kill_process(f'"{exe}" -clone:{instance.name} -quit') @@ -220,7 +200,10 @@ def _emulator_stop(self, instance: EmulatorInstance): ) elif instance == Emulator.BlueStacks4: # E:\Program Files (x86)\BluestacksCN\bsconsole.exe quit --name Android - self.kill_process(f'"{exe.rsplit("/",1)[0]}/bsconsole.exe" quit --name {instance.name}') + self.kill_process(f'"{os.path.join(os.path.dirname(exe),"bsconsole.exe")}" quit --name {instance.name}') + elif instance == Emulator.MEmuPlayer: + # F:\Program Files\Microvirt\MEmu\memuc.exe stop -n MEmu_0 + self.kill_process(f'"{os.path.join(os.path.dirname(exe),"memuc.exe")}" stop -n {instance.name}') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') @@ -377,9 +360,6 @@ def emulator_start(self): def emulator_stop(self): logger.hr('Emulator stop', level=1) for _ in range(3): - # Start - if not self._emulator_function_wrapper(self._emulator_start): - return False # Stop if self._emulator_function_wrapper(self._emulator_stop): # Success From bb6533563a0da68b342b5591f5809c1b31355b7f Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 4 Jun 2024 00:48:24 +0800 Subject: [PATCH 021/161] Opt: Add reboot emulator. :-) --- alas.py | 11 ++++---- module/device/platform/platform_windows.py | 29 ++++++++++------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/alas.py b/alas.py index fec00de46d..d237517743 100644 --- a/alas.py +++ b/alas.py @@ -505,6 +505,12 @@ def get_next_task(self): if not self.wait_until(task.next_run): del_cached_property(self, 'config') continue + + # Reboot emulator + if self.emulator_stopped: + self.device.emulator_start() + self.config.task_call('Restart') + self.emulator_stopped = False break AzurLaneConfig.is_hoarding_task = False @@ -535,11 +541,6 @@ def loop(self): _ = self.device # Get task task = self.get_next_task() - # Reboot emulator - if self.emulator_stopped: - self.device.emulator_start() - self.config.task_call('Restart') - self.emulator_stopped = False # Skip first restart if self.is_first_task and task == 'Restart': logger.info('Skip task `Restart` at scheduler start') diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index ae32b2fe3a..b3b0d91209 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -54,24 +54,21 @@ def execute(self, command: str): # CAUTION!!!!!!: Windows only. command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') - from sys import platform if not self.config.Emulator_SilentStart: - if platform == 'win32': - return subprocess.Popen(command,close_fds=True) + return subprocess.Popen(command,close_fds=True) - if platform == 'win32': - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = subprocess.SW_HIDE - return subprocess.Popen( - command, - startupinfo=startupinfo, - creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - close_fds=True - ) + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + return subprocess.Popen( + command, + startupinfo=startupinfo, + creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + close_fds=True + ) @classmethod def kill_process(cls, command: str): From 56c130ef7a5978b72e2a03be1805e7f686cf85e6 Mon Sep 17 00:00:00 2001 From: Air111 <1796389814@qq.com> Date: Tue, 14 May 2024 23:09:11 +0800 Subject: [PATCH 022/161] Fix: increase tile_center matching success rate in light map --- campaign/campaign_main/campaign_12_1.py | 1 + campaign/campaign_main/campaign_13_1.py | 1 + campaign/campaign_main/campaign_15_base.py | 1 + campaign/campaign_main/campaign_1_1.py | 1 + campaign/campaign_main/campaign_2_1.py | 1 + campaign/campaign_main/campaign_3_1.py | 1 + campaign/campaign_main/campaign_5_1.py | 1 + 7 files changed, 7 insertions(+) diff --git a/campaign/campaign_main/campaign_12_1.py b/campaign/campaign_main/campaign_12_1.py index e2f0818381..f8db699a2c 100644 --- a/campaign/campaign_main/campaign_12_1.py +++ b/campaign/campaign_main/campaign_12_1.py @@ -59,6 +59,7 @@ class Config: 'distance': 50, 'wlen': 1000 } + HOMO_CANNY_THRESHOLD = (75, 100) HOMO_EDGE_COLOR_RANGE = (0, 49) HOMO_EDGE_HOUGHLINES_THRESHOLD = 210 MAP_SWIPE_MULTIPLY = (0.977, 0.995) diff --git a/campaign/campaign_main/campaign_13_1.py b/campaign/campaign_main/campaign_13_1.py index 4d4a018f01..d5c6734765 100644 --- a/campaign/campaign_main/campaign_13_1.py +++ b/campaign/campaign_main/campaign_13_1.py @@ -54,6 +54,7 @@ class Config: 'distance': 50, 'wlen': 1000 } + HOMO_CANNY_THRESHOLD = (75, 100) HOMO_EDGE_COLOR_RANGE = (0, 49) MAP_SWIPE_MULTIPLY = (0.994, 1.013) MAP_SWIPE_MULTIPLY_MINITOUCH = (0.961, 0.979) diff --git a/campaign/campaign_main/campaign_15_base.py b/campaign/campaign_main/campaign_15_base.py index ce400f6da8..34e9040653 100644 --- a/campaign/campaign_main/campaign_15_base.py +++ b/campaign/campaign_main/campaign_15_base.py @@ -22,6 +22,7 @@ class Config: 'prominence': 10, 'distance': 35, } + HOMO_CANNY_THRESHOLD = (50, 100) MAP_SWIPE_MULTIPLY = (0.993, 1.011) MAP_SWIPE_MULTIPLY_MINITOUCH = (0.960, 0.978) MAP_SWIPE_MULTIPLY_MAATOUCH = (0.932, 0.949) diff --git a/campaign/campaign_main/campaign_1_1.py b/campaign/campaign_main/campaign_1_1.py index 59c945996e..2d6ccd6b6b 100644 --- a/campaign/campaign_main/campaign_1_1.py +++ b/campaign/campaign_main/campaign_1_1.py @@ -33,6 +33,7 @@ class Config: 'distance': 50, 'wlen': 1000 } + HOMO_CANNY_THRESHOLD = (75, 100) HOMO_EDGE_COLOR_RANGE = (0, 49) INTERNAL_LINES_HOUGHLINES_THRESHOLD = 40 EDGE_LINES_HOUGHLINES_THRESHOLD = 40 diff --git a/campaign/campaign_main/campaign_2_1.py b/campaign/campaign_main/campaign_2_1.py index 682e19f96c..f4faeb8056 100644 --- a/campaign/campaign_main/campaign_2_1.py +++ b/campaign/campaign_main/campaign_2_1.py @@ -50,6 +50,7 @@ class Config: 'distance': 50, 'wlen': 1000 } + HOMO_CANNY_THRESHOLD = (75, 100) HOMO_EDGE_COLOR_RANGE = (0, 49) diff --git a/campaign/campaign_main/campaign_3_1.py b/campaign/campaign_main/campaign_3_1.py index 3c2498aa13..f1c512afea 100644 --- a/campaign/campaign_main/campaign_3_1.py +++ b/campaign/campaign_main/campaign_3_1.py @@ -50,6 +50,7 @@ class Config: 'distance': 50, 'wlen': 1000 } + HOMO_CANNY_THRESHOLD = (75, 100) HOMO_EDGE_COLOR_RANGE = (0, 49) diff --git a/campaign/campaign_main/campaign_5_1.py b/campaign/campaign_main/campaign_5_1.py index c0bf41cf4e..68513ff983 100644 --- a/campaign/campaign_main/campaign_5_1.py +++ b/campaign/campaign_main/campaign_5_1.py @@ -55,6 +55,7 @@ class Config: 'distance': 50, 'wlen': 1000 } + HOMO_CANNY_THRESHOLD = (75, 100) HOMO_EDGE_COLOR_RANGE = (0, 49) From c5917afa8caeeb8accdc141b20607dee7f458927 Mon Sep 17 00:00:00 2001 From: Air111 <1796389814@qq.com> Date: Mon, 20 May 2024 23:30:47 +0800 Subject: [PATCH 023/161] Fix: mob move optimization --- assets/cn/handler/MOB_MOVE_1.png | Bin 12252 -> 0 bytes assets/cn/handler/MOB_MOVE_2.png | Bin 12527 -> 0 bytes assets/cn/handler/MOB_MOVE_ENTER.png | Bin 0 -> 8889 bytes assets/en/handler/MOB_MOVE_1.png | Bin 12252 -> 0 bytes assets/en/handler/MOB_MOVE_2.png | Bin 12527 -> 0 bytes assets/en/handler/MOB_MOVE_ENTER.png | Bin 0 -> 8889 bytes assets/jp/handler/MOB_MOVE_1.png | Bin 12252 -> 0 bytes assets/jp/handler/MOB_MOVE_2.png | Bin 12527 -> 0 bytes assets/jp/handler/MOB_MOVE_ENTER.png | Bin 0 -> 8889 bytes assets/tw/handler/MOB_MOVE_1.png | Bin 12252 -> 0 bytes assets/tw/handler/MOB_MOVE_2.png | Bin 12527 -> 0 bytes assets/tw/handler/MOB_MOVE_ENTER.png | Bin 0 -> 8889 bytes campaign/campaign_main/campaign_15_2.py | 2 +- campaign/campaign_main/campaign_15_base.py | 7 +++---- module/handler/assets.py | 3 +-- module/handler/strategy.py | 22 +++++++++------------ 16 files changed, 14 insertions(+), 20 deletions(-) delete mode 100644 assets/cn/handler/MOB_MOVE_1.png delete mode 100644 assets/cn/handler/MOB_MOVE_2.png create mode 100644 assets/cn/handler/MOB_MOVE_ENTER.png delete mode 100644 assets/en/handler/MOB_MOVE_1.png delete mode 100644 assets/en/handler/MOB_MOVE_2.png create mode 100644 assets/en/handler/MOB_MOVE_ENTER.png delete mode 100644 assets/jp/handler/MOB_MOVE_1.png delete mode 100644 assets/jp/handler/MOB_MOVE_2.png create mode 100644 assets/jp/handler/MOB_MOVE_ENTER.png delete mode 100644 assets/tw/handler/MOB_MOVE_1.png delete mode 100644 assets/tw/handler/MOB_MOVE_2.png create mode 100644 assets/tw/handler/MOB_MOVE_ENTER.png diff --git a/assets/cn/handler/MOB_MOVE_1.png b/assets/cn/handler/MOB_MOVE_1.png deleted file mode 100644 index de5971cf4c01f94f85f665964b66b9f111d4f394..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12252 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 0) - logger.attr("Map has mob move", self.map_has_mob_move) + logger.attr("Map has mob move", self.strategy_has_mob_move()) def _map_swipe(self, vector, box=(239, 159, 1175, 628)): # Left border to 239, avoid swiping on support fleet @@ -184,6 +183,7 @@ def _mob_move_info_change(self, location, target): self.map[target].is_boss = self.map[location].is_boss self.map[location].is_boss = False self.map[target].is_enemy = True + self.map[target].may_enemy = True self.map[location].is_enemy = False def mob_move(self, location, target): @@ -205,8 +205,7 @@ def mob_move(self, location, target): return False self.strategy_open() - remain = self.strategy_get_mob_move_remain() - if remain == 0: + if not self.strategy_has_mob_move(): logger.warning(f'No remain mob move trials, will abandon moving') self.strategy_close() return False diff --git a/module/handler/assets.py b/module/handler/assets.py index 9d4e65d071..0dbcb85f9d 100644 --- a/module/handler/assets.py +++ b/module/handler/assets.py @@ -67,9 +67,8 @@ MAP_WALK_SPEEDUP = Button(area={'cn': (1025, 406, 1055, 436), 'en': (1025, 406, 1055, 436), 'jp': (1025, 406, 1055, 436), 'tw': (1025, 406, 1055, 436)}, color={'cn': (62, 97, 72), 'en': (62, 97, 72), 'jp': (62, 97, 72), 'tw': (62, 97, 72)}, button={'cn': (1025, 406, 1055, 436), 'en': (1025, 406, 1055, 436), 'jp': (1025, 406, 1055, 436), 'tw': (1025, 406, 1055, 436)}, file={'cn': './assets/cn/handler/MAP_WALK_SPEEDUP.png', 'en': './assets/en/handler/MAP_WALK_SPEEDUP.png', 'jp': './assets/jp/handler/MAP_WALK_SPEEDUP.png', 'tw': './assets/tw/handler/MAP_WALK_SPEEDUP.png'}) MISSION_POPUP_ACK = Button(area={'cn': (432, 493, 543, 533), 'en': (413, 489, 566, 532), 'jp': (410, 482, 574, 539), 'tw': (413, 489, 566, 532)}, color={'cn': (181, 182, 184), 'en': (169, 170, 172), 'jp': (162, 164, 167), 'tw': (169, 170, 172)}, button={'cn': (432, 493, 543, 533), 'en': (413, 489, 566, 532), 'jp': (410, 482, 574, 539), 'tw': (413, 489, 566, 532)}, file={'cn': './assets/cn/handler/MISSION_POPUP_ACK.png', 'en': './assets/en/handler/MISSION_POPUP_ACK.png', 'jp': './assets/jp/handler/MISSION_POPUP_ACK.png', 'tw': './assets/tw/handler/MISSION_POPUP_ACK.png'}) MISSION_POPUP_GO = Button(area={'cn': (719, 493, 861, 534), 'en': (716, 488, 869, 533), 'jp': (711, 482, 874, 539), 'tw': (716, 488, 869, 533)}, color={'cn': (125, 164, 214), 'en': (89, 138, 201), 'jp': (93, 142, 204), 'tw': (89, 138, 201)}, button={'cn': (719, 493, 861, 534), 'en': (716, 488, 869, 533), 'jp': (711, 482, 874, 539), 'tw': (716, 488, 869, 533)}, file={'cn': './assets/cn/handler/MISSION_POPUP_GO.png', 'en': './assets/en/handler/MISSION_POPUP_GO.png', 'jp': './assets/jp/handler/MISSION_POPUP_GO.png', 'tw': './assets/tw/handler/MISSION_POPUP_GO.png'}) -MOB_MOVE_1 = Button(area={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, color={'cn': (118, 120, 127), 'en': (118, 120, 127), 'jp': (118, 120, 127), 'tw': (118, 120, 127)}, button={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, file={'cn': './assets/cn/handler/MOB_MOVE_1.png', 'en': './assets/en/handler/MOB_MOVE_1.png', 'jp': './assets/jp/handler/MOB_MOVE_1.png', 'tw': './assets/tw/handler/MOB_MOVE_1.png'}) -MOB_MOVE_2 = Button(area={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, color={'cn': (119, 121, 128), 'en': (119, 121, 128), 'jp': (119, 121, 128), 'tw': (119, 121, 128)}, button={'cn': (1102, 504, 1176, 578), 'en': (1102, 504, 1176, 578), 'jp': (1102, 504, 1176, 578), 'tw': (1102, 504, 1176, 578)}, file={'cn': './assets/cn/handler/MOB_MOVE_2.png', 'en': './assets/en/handler/MOB_MOVE_2.png', 'jp': './assets/jp/handler/MOB_MOVE_2.png', 'tw': './assets/tw/handler/MOB_MOVE_2.png'}) MOB_MOVE_CANCEL = Button(area={'cn': (1162, 646, 1220, 674), 'en': (1162, 646, 1220, 674), 'jp': (1162, 644, 1222, 675), 'tw': (1162, 646, 1220, 674)}, color={'cn': (224, 176, 173), 'en': (224, 176, 173), 'jp': (207, 140, 136), 'tw': (224, 176, 173)}, button={'cn': (1162, 646, 1220, 674), 'en': (1162, 646, 1220, 674), 'jp': (1162, 644, 1222, 675), 'tw': (1162, 646, 1220, 674)}, file={'cn': './assets/cn/handler/MOB_MOVE_CANCEL.png', 'en': './assets/cn/handler/MOB_MOVE_CANCEL.png', 'jp': './assets/jp/handler/MOB_MOVE_CANCEL.png', 'tw': './assets/cn/handler/MOB_MOVE_CANCEL.png'}) +MOB_MOVE_ENTER = Button(area={'cn': (1102, 504, 1157, 578), 'en': (1102, 504, 1157, 578), 'jp': (1102, 504, 1157, 578), 'tw': (1102, 504, 1157, 578)}, color={'cn': (122, 124, 131), 'en': (122, 124, 131), 'jp': (122, 124, 131), 'tw': (122, 124, 131)}, button={'cn': (1102, 504, 1157, 578), 'en': (1102, 504, 1157, 578), 'jp': (1102, 504, 1157, 578), 'tw': (1102, 504, 1157, 578)}, file={'cn': './assets/cn/handler/MOB_MOVE_ENTER.png', 'en': './assets/en/handler/MOB_MOVE_ENTER.png', 'jp': './assets/jp/handler/MOB_MOVE_ENTER.png', 'tw': './assets/tw/handler/MOB_MOVE_ENTER.png'}) MONTHLY_PASS_NOTICE = Button(area={'cn': (554, 505, 726, 561), 'en': (716, 488, 869, 533), 'jp': (554, 505, 726, 561), 'tw': (554, 505, 726, 561)}, color={'cn': (109, 153, 208), 'en': (89, 138, 201), 'jp': (109, 153, 208), 'tw': (109, 153, 208)}, button={'cn': (872, 152, 939, 196), 'en': (863, 173, 929, 217), 'jp': (872, 152, 939, 196), 'tw': (872, 152, 939, 196)}, file={'cn': './assets/cn/handler/MONTHLY_PASS_NOTICE.png', 'en': './assets/en/handler/MONTHLY_PASS_NOTICE.png', 'jp': './assets/cn/handler/MONTHLY_PASS_NOTICE.png', 'tw': './assets/cn/handler/MONTHLY_PASS_NOTICE.png'}) MYSTERY_ITEM = Button(area={'cn': (589, 294, 691, 427), 'en': (589, 294, 691, 427), 'jp': (589, 294, 691, 427), 'tw': (589, 294, 691, 427)}, color={'cn': (144, 127, 83), 'en': (144, 127, 83), 'jp': (144, 127, 83), 'tw': (144, 127, 83)}, button={'cn': (588, 478, 698, 496), 'en': (588, 478, 698, 496), 'jp': (588, 478, 698, 496), 'tw': (588, 478, 698, 496)}, file={'cn': './assets/cn/handler/MYSTERY_ITEM.png', 'en': './assets/en/handler/MYSTERY_ITEM.png', 'jp': './assets/jp/handler/MYSTERY_ITEM.png', 'tw': './assets/tw/handler/MYSTERY_ITEM.png'}) POPUP_CANCEL = Button(area={'cn': (453, 506, 525, 536), 'en': (407, 485, 574, 535), 'jp': (455, 515, 521, 546), 'tw': (454, 495, 525, 526)}, color={'cn': (196, 198, 199), 'en': (168, 169, 171), 'jp': (181, 183, 184), 'tw': (195, 196, 197)}, button={'cn': (453, 506, 525, 536), 'en': (407, 485, 574, 535), 'jp': (455, 515, 521, 546), 'tw': (454, 495, 525, 526)}, file={'cn': './assets/cn/handler/POPUP_CANCEL.png', 'en': './assets/en/handler/POPUP_CANCEL.gif', 'jp': './assets/jp/handler/POPUP_CANCEL.png', 'tw': './assets/tw/handler/POPUP_CANCEL.png'}) diff --git a/module/handler/strategy.py b/module/handler/strategy.py index 7462a72eb8..126e4bc445 100644 --- a/module/handler/strategy.py +++ b/module/handler/strategy.py @@ -211,23 +211,22 @@ def is_in_strategy_mob_move(self): """ return self.appear(MOB_MOVE_CANCEL, offset=(20, 20)) - def strategy_get_mob_move_remain(self): + def strategy_has_mob_move(self): """ Pages: in: STRATEGY_OPENED out: STRATEGY_OPENED """ - if self.appear(MOB_MOVE_2, offset=MOB_MOVE_OFFSET): - return 2 - elif self.appear(MOB_MOVE_1, offset=MOB_MOVE_OFFSET): - return 1 + if (self.appear(MOB_MOVE_ENTER, offset=MOB_MOVE_OFFSET) + and MOB_MOVE_ENTER.match_appear_on(self.device.image)): + return True else: - return 0 + return False def strategy_mob_move_enter(self, skip_first_screenshot=True): """ Pages: - in: STRATEGY_OPENED, MOB_MOVE_1 or MOB_MOVE_2 + in: STRATEGY_OPENED, MOB_MOVE_ENTER out: MOB_MOVE_CANCEL """ logger.info('Mob move enter') @@ -240,16 +239,14 @@ def strategy_mob_move_enter(self, skip_first_screenshot=True): if self.appear(MOB_MOVE_CANCEL, offset=(20, 20)): break - if self.appear_then_click(MOB_MOVE_1, offset=MOB_MOVE_OFFSET, interval=5): - continue - if self.appear_then_click(MOB_MOVE_2, offset=MOB_MOVE_OFFSET, interval=5): + if self.appear_then_click(MOB_MOVE_ENTER, offset=MOB_MOVE_OFFSET, interval=5): continue def strategy_mob_move_cancel(self, skip_first_screenshot=True): """ Pages: in: MOB_MOVE_CANCEL - out: STRATEGY_OPENED, MOB_MOVE_1 or MOB_MOVE_2 + out: STRATEGY_OPENED, MOB_MOVE_ENTER """ logger.info('Mob move cancel') while 1: @@ -258,8 +255,7 @@ def strategy_mob_move_cancel(self, skip_first_screenshot=True): else: self.device.screenshot() - if self.appear(MOB_MOVE_1, offset=MOB_MOVE_OFFSET) \ - or self.appear(MOB_MOVE_2, offset=MOB_MOVE_OFFSET): + if self.appear(MOB_MOVE_ENTER, offset=MOB_MOVE_OFFSET): break if self.appear_then_click(MOB_MOVE_CANCEL, offset=(20, 20), interval=5): From 3313e07f247edb86168b8537a29496ae76a6b5ea Mon Sep 17 00:00:00 2001 From: Air111 <54128005+Air111@users.noreply.github.com> Date: Mon, 3 Jun 2024 00:05:23 +0800 Subject: [PATCH 024/161] Opt: wait until navbar active is not None (#3832) --- module/gacha/ui.py | 16 +++++++--------- module/ui/navbar.py | 3 ++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/module/gacha/ui.py b/module/gacha/ui.py index 277313da92..29bafbd286 100644 --- a/module/gacha/ui.py +++ b/module/gacha/ui.py @@ -103,17 +103,15 @@ def gacha_side_navbar_ensure(self, upper=None, bottom=None): def _construct_bottom_navbar(self): """ limited 4 options - build. - limited_build. - orders. - shop. - retire. + event. + light. + heavy. + special. regular 3 options - build. - orders. - shop. - retire. + light. + heavy. + special. """ construct_bottom_navbar = ButtonGrid( origin=(262, 615), delta=(209, 0), diff --git a/module/ui/navbar.py b/module/ui/navbar.py index 3a64d581d0..2a24389cba 100644 --- a/module/ui/navbar.py +++ b/module/ui/navbar.py @@ -188,7 +188,8 @@ def set(self, main, left=None, right=None, upper=None, bottom=None, skip_first_s active, minimum, maximum = self.get_info(main=main) logger.info(f'Nav item active: {active} from range ({minimum}, {maximum})') # Get None when receiving a pure black screenshot. - if minimum is None or maximum is None: + # Active is None could be because of slow animation + if active is None or minimum is None or maximum is None: continue index = minimum + left - 1 if left is not None else maximum - right + 1 From d327f2e35a5acf6c79d745bbb03c6a4df75515e6 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 3 Jun 2024 00:52:20 +0800 Subject: [PATCH 025/161] Upd: [TW] Shop indexes, all server synced now --- module/shop/shop_reward.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/module/shop/shop_reward.py b/module/shop/shop_reward.py index 3cbaa126db..53a98df6d1 100644 --- a/module/shop/shop_reward.py +++ b/module/shop/shop_reward.py @@ -1,4 +1,3 @@ -from module.base.decorator import Config from module.shop.shop_core import CoreShop from module.shop.shop_general import GeneralShop from module.shop.shop_guild import GuildShop @@ -18,7 +17,6 @@ def run_frequent(self): self.config.task_delay(server_update=True) - @Config.when(SERVER='tw') def run_once(self): # Munitions shops self.ui_goto_shop() @@ -41,27 +39,3 @@ def run_once(self): MedalShop2(self.config, self.device).run() self.config.task_delay(server_update=True) - - @Config.when(SERVER=None) - def run_once(self): - # Munitions shops - self.ui_goto_shop() - - self.shop_tab.set(main=self, left=2) - self.shop_nav.set(main=self, upper=2) - MeritShop(self.config, self.device).run() - - self.shop_tab.set(main=self, left=2) - self.shop_nav.set(main=self, upper=3) - GuildShop(self.config, self.device).run() - - # core limited, core monthly, medal, prototype - self.shop_tab.set(main=self, left=1) - self.shop_nav.set(main=self, upper=2) - CoreShop(self.config, self.device).run() - - self.shop_tab.set(main=self, left=1) - self.shop_nav.set(main=self, upper=3) - MedalShop2(self.config, self.device).run() - - self.config.task_delay(server_update=True) From 075e54a5919d3c0031d18cea4ca3f89b2691321b Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 3 Jun 2024 01:55:44 +0800 Subject: [PATCH 026/161] Upd: [TW] Shop indexes, all server synced now --- module/shop/shop_reward.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/shop/shop_reward.py b/module/shop/shop_reward.py index 53a98df6d1..ed2a407ad5 100644 --- a/module/shop/shop_reward.py +++ b/module/shop/shop_reward.py @@ -29,13 +29,13 @@ def run_once(self): self.shop_nav.set(main=self, upper=3) GuildShop(self.config, self.device).run() - # core monthly, medal, prototype + # core limited, core monthly, medal, prototype self.shop_tab.set(main=self, left=1) - self.shop_nav.set(main=self, upper=1) + self.shop_nav.set(main=self, upper=2) CoreShop(self.config, self.device).run() self.shop_tab.set(main=self, left=1) - self.shop_nav.set(main=self, upper=2) + self.shop_nav.set(main=self, upper=3) MedalShop2(self.config, self.device).run() self.config.task_delay(server_update=True) From 96a5ead68b993d635346759e03f7369aff48fc4f Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 7 Jun 2024 21:45:57 +0800 Subject: [PATCH 027/161] Opt: Stage upload. --- alas.py | 11 ++- module/config/config_generated.py | 1 - module/device/device.py | 16 ++- module/device/platform/platform_windows.py | 109 +++++++++++++++++++++ 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/alas.py b/alas.py index d237517743..3b69b9b808 100644 --- a/alas.py +++ b/alas.py @@ -452,6 +452,12 @@ def get_next_task(self): str: Name of the next task. """ while 1: + # Reboot emulator + if self.emulator_stopped and task.next_run <= datetime.now(): + self.device.emulator_start() + self.config.task_call('Restart') + self.emulator_stopped = False + task = self.config.get_next() self.config.task = task self.config.bind(task) @@ -506,11 +512,6 @@ def get_next_task(self): del_cached_property(self, 'config') continue - # Reboot emulator - if self.emulator_stopped: - self.device.emulator_start() - self.config.task_call('Restart') - self.emulator_stopped = False break AzurLaneConfig.is_hoarding_task = False diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 0094beda54..ee75a2ad4e 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -25,7 +25,6 @@ class GeneratedConfig: Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False - Emulator_SilentStart = False # Group `EmulatorInfo` EmulatorInfo_Emulator = 'auto' # auto, NoxPlayer, NoxPlayer64, BlueStacks4, BlueStacks5, BlueStacks4HyperV, BlueStacks5HyperV, LDPlayer3, LDPlayer4, LDPlayer9, MuMuPlayer, MuMuPlayerX, MuMuPlayer12, MEmuPlayer diff --git a/module/device/device.py b/module/device/device.py index 8e7e27fbb0..20a3c51640 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -100,6 +100,10 @@ def __init__(self, *args, **kwargs): if self.config.Emulator_ControlMethod == 'minitouch': self.early_minitouch_init() + if not self.config.Emulator_SilentStart: + self.reshow_window(self.emulator_instance) + pass + def run_simple_screenshot_benchmark(self): """ Perform a screenshot method benchmark, test 3 times on each method. @@ -321,4 +325,14 @@ def emulator_stop(self): self.click_record_clear() def emulator_start(self): - return super().emulator_start() \ No newline at end of file + # start emulator + if self.emulator_instance is not None: + super().emulator_start() + else: + logger.critical( + f'No emulator with serial "{self.config.Emulator_Serial}" found, ' + f'please set a correct serial' + ) + raise + self.stuck_record_clear() + self.click_record_clear() \ No newline at end of file diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index b3b0d91209..086f0f5bd8 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -105,6 +105,55 @@ def kill_process_by_regex(cls, regex: str) -> int: return count + @classmethod + def reshow_window(cls, instance: EmulatorInstance): + import win32con + import win32gui + import win32process + def gethwnds(pid:int) -> list: + def callback(hwnd:int, hwnds:list): + _, fpid = win32process.GetWindowThreadProcessId(hwnd) + if fpid == pid: + hwnds.append(hwnd) + return True + hwnds = [] + win32gui.EnumWindows(callback, hwnds) + return hwnds + def switch(hwnds:list, arg): + for hwnd in hwnds: + win32gui.ShowWindow(hwnd,arg) + + process:psutil.Process = None + for proc in psutil.process_iter(['pid', 'name', 'cmdline']): + if proc.info['cmdline'] is None: + continue + cmdline = ' '.join(proc.info['cmdline']).replace(r"\\", "/").replace("\\", "/").replace('"', '"') + match = re.search(r'\d+$',cmdline) + matchstr = re.search(fr'\b{instance.name}$',cmdline) + if not instance.path in cmdline: + continue + if instance == Emulator.MuMuPlayer12: + if not match: + continue + if int(match.group()) == instance.MuMuPlayer12_id: + process = proc + break + elif instance == Emulator.LDPlayerFamily: + if not match: + continue + if int(match.group()) == instance.LDPlayer_id: + process = proc + break + else: + if not matchstr: + continue + if match.group() == instance.name: + process = proc + break + if process: + hwnds:int = gethwnds(process.pid) + logger.info(hwnds) + def _emulator_start(self, instance: EmulatorInstance): """ Start a emulator without error handling @@ -204,6 +253,66 @@ def _emulator_stop(self, instance: EmulatorInstance): else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') + def _emulator_stop_hard(self, instance: EmulatorInstance): + """ + stop emulator by kill process + """ + logger.hr('Emulator stop', level=2) + if instance == Emulator.MuMuPlayer: + # MuMu6 does not have multi instance, kill one means kill all + # Has 4 processes + # "C:\Program Files\NemuVbox\Hypervisor\NemuHeadless.exe" --comment nemu-6.0-x64-default --startvm + # "E:\ProgramFiles\MuMu\emulator\nemu\EmulatorShell\NemuPlayer.exe" + # E:\ProgramFiles\MuMu\emulator\nemu\EmulatorShell\NemuService.exe + # "C:\Program Files\NemuVbox\Hypervisor\NemuSVC.exe" -Embedding + self.kill_process_by_regex( + rf'(' + rf'NemuHeadless.exe' + rf'|NemuPlayer.exe\"' + rf'|NemuPlayer.exe$' + rf'|NemuService.exe' + rf'|NemuSVC.exe' + rf')' + ) + elif instance == Emulator.MuMuPlayerX: + # MuMu X has 3 processes + # "E:\ProgramFiles\MuMu9\emulator\nemu9\EmulatorShell\NemuPlayer.exe" -m nemu-12.0-x64-default -s 0 -l + # "C:\Program Files\Muvm6Vbox\Hypervisor\Muvm6Headless.exe" --comment nemu-12.0-x64-default --startvm xxx + # "C:\Program Files\Muvm6Vbox\Hypervisor\Muvm6SVC.exe" --Embedding + self.kill_process_by_regex( + rf'(' + rf'NemuPlayer.exe.*-m {instance.name}' + rf'|Muvm6Headless.exe' + rf'|Muvm6SVC.exe' + rf')' + ) + elif instance == Emulator.MuMuPlayer12: + # MuMu 12 has 2 processes: + # E:\ProgramFiles\Netease\MuMuPlayer-12.0\shell\MuMuPlayer.exe -v 0 + # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMHeadless.exe" --comment MuMuPlayer-12.0-0 --startvm xxx + if instance.MuMuPlayer12_id is None: + logger.warning(f'Cannot get MuMu instance index from name {instance.name}') + self.kill_process_by_regex( + rf'(' + rf'MuMuVMMHeadless.exe.*--comment {instance.name}' + rf'|MuMuPlayer.exe.*-v {instance.MuMuPlayer12_id}' + rf')' + ) + # There is also a shared service, no need to kill it + # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMSVC.exe" --Embedding + elif instance == Emulator.BlueStacks5: + # BlueStack has 2 processes + # C:\Program Files\BlueStacks_nxt_cn\HD-Player.exe --instance Pie64 + # C:\Program Files\BlueStacks_nxt_cn\BstkSVC.exe -Embedding + self.kill_process_by_regex( + rf'(' + rf'HD-Player.exe.*"--instance" "{instance.name}"' + rf'BstkSVC.exe.*-Embedding' + rf')' + ) + else: + raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') + def _emulator_function_wrapper(self, func): """ Args: From a96b710afaee8d58e8c1db550f19b7706d4f7177 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 8 Jun 2024 01:41:57 +0800 Subject: [PATCH 028/161] Opt: Add emulator MINIMIZE support. :) --- module/device/device.py | 5 +- module/device/platform/platform_windows.py | 128 +++++++++++---------- 2 files changed, 71 insertions(+), 62 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index 20a3c51640..deb758ce98 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -100,9 +100,8 @@ def __init__(self, *args, **kwargs): if self.config.Emulator_ControlMethod == 'minitouch': self.early_minitouch_init() - if not self.config.Emulator_SilentStart: - self.reshow_window(self.emulator_instance) - pass + self.get_process(self.emulator_instance) + self.reshow_window(self.emulator_instance) def run_simple_screenshot_benchmark(self): """ diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 086f0f5bd8..6780fbdbd0 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,6 +1,8 @@ import ctypes import re -import subprocess +import win32process +import win32con +import win32gui import psutil @@ -29,6 +31,21 @@ def minimize_window(hwnd): ctypes.windll.user32.ShowWindow(hwnd, 6) +def switch_window(hwnd:int, arg:int): + win32gui.ShowWindow(hwnd,arg) + + +def gethwnds(pid: int) -> list: + def callback(hwnd:int, hwnds:list): + _, fpid = win32process.GetWindowThreadProcessId(hwnd) + if fpid == pid: + hwnds.append(hwnd) + return True + hwnds = [] + win32gui.EnumWindows(callback, hwnds) + return hwnds + + def get_window_title(hwnd): """Returns the window title as a string.""" text_len_in_characters = ctypes.windll.user32.GetWindowTextLengthW(hwnd) @@ -43,32 +60,33 @@ def flash_window(hwnd, flash=True): class PlatformWindows(PlatformBase, EmulatorManager): + def __init__(self, config): + super().__init__(config) + self.process: psutil.Process = None + self.hwnds: list[int] = None + def execute(self, command: str): """ Args: command (str): Returns: - subprocess.Popen: + win32process.CreateProcess -> tuple(int, int, Incomplete, Incomplete): """ # CAUTION!!!!!!: Windows only. command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') - if not self.config.Emulator_SilentStart: - return subprocess.Popen(command,close_fds=True) - - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = subprocess.SW_HIDE - return subprocess.Popen( - command, - startupinfo=startupinfo, - creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - close_fds=True - ) + startupinfo = win32process.STARTUPINFO() + startupinfo.dwFlags = win32con.STARTF_USESHOWWINDOW + if self.config.Emulator_SilentStart: + startupinfo.wShowWindow = win32con.SW_HIDE + else: + startupinfo.wShowWindow = win32con.SW_MINIMIZE + return win32process.CreateProcess( + None,command,None,None,False, + win32con.NORMAL_PRIORITY_CLASS, + None,None,startupinfo + ) @classmethod def kill_process(cls, command: str): @@ -77,11 +95,17 @@ def kill_process(cls, command: str): command (str): Returns: - subprocess.Popen: + win32process.CreateProcess -> tuple(int, int, Incomplete, Incomplete): """ command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') - return subprocess.Popen(command,close_fds=True,shell=True) + startupinfo = win32process.STARTUPINFO() + startupinfo.dwFlags = win32con.STARTF_USESHOWWINDOW + return win32process.CreateProcess( + None,command,None,None,False, + win32con.NORMAL_PRIORITY_CLASS, + None,None,startupinfo + ) @classmethod def kill_process_by_regex(cls, regex: str) -> int: @@ -104,55 +128,41 @@ def kill_process_by_regex(cls, regex: str) -> int: count += 1 return count - - @classmethod - def reshow_window(cls, instance: EmulatorInstance): - import win32con - import win32gui - import win32process - def gethwnds(pid:int) -> list: - def callback(hwnd:int, hwnds:list): - _, fpid = win32process.GetWindowThreadProcessId(hwnd) - if fpid == pid: - hwnds.append(hwnd) - return True - hwnds = [] - win32gui.EnumWindows(callback, hwnds) - return hwnds - def switch(hwnds:list, arg): - for hwnd in hwnds: - win32gui.ShowWindow(hwnd,arg) - - process:psutil.Process = None + + def get_process(self, instance: EmulatorInstance): for proc in psutil.process_iter(['pid', 'name', 'cmdline']): if proc.info['cmdline'] is None: continue cmdline = ' '.join(proc.info['cmdline']).replace(r"\\", "/").replace("\\", "/").replace('"', '"') - match = re.search(r'\d+$',cmdline) - matchstr = re.search(fr'\b{instance.name}$',cmdline) if not instance.path in cmdline: continue if instance == Emulator.MuMuPlayer12: - if not match: - continue - if int(match.group()) == instance.MuMuPlayer12_id: - process = proc + match = re.search(r'\d+$',cmdline) + if match and int(match.group()) == instance.MuMuPlayer12_id: + self.process = proc break elif instance == Emulator.LDPlayerFamily: - if not match: - continue - if int(match.group()) == instance.LDPlayer_id: - process = proc + match = re.search(r'\d+$',cmdline) + if match and int(match.group()) == instance.LDPlayer_id: + self.process = proc break else: - if not matchstr: - continue - if match.group() == instance.name: - process = proc + matchstr = re.search(fr'\b{instance.name}$',cmdline) + if matchstr and matchstr.group() == instance.name: + self.process = proc break - if process: - hwnds:int = gethwnds(process.pid) - logger.info(hwnds) + self.hwnds = gethwnds(self.process.pid) + + def reshow_window(self, arg: int): + if self.process is None: + return + for hwnd in self.hwnds: + if win32gui.GetParent(hwnd): + continue + rect = win32gui.GetWindowRect(hwnd) + if set(rect) == {0}: + continue + switch_window(hwnd,win32con.SW_MINIMIZE)# May arg will be sent in. def _emulator_start(self, instance: EmulatorInstance): """ @@ -180,7 +190,7 @@ def _emulator_start(self, instance: EmulatorInstance): # HD-Player.exe -instance Pie64 self.execute(f'"{exe}" -instance {instance.name}') elif instance == Emulator.BlueStacks4: - # BlueStacks\Client\Bluestacks.exe -vmname Android_1 + # Bluestacks.exe -vmname Android_1 self.execute(f'"{exe}" -vmname {instance.name}') elif instance == Emulator.MEmuPlayer: # MEmu.exe MEmu_0 @@ -241,7 +251,7 @@ def _emulator_stop(self, instance: EmulatorInstance): self.kill_process_by_regex( rf'(' rf'HD-Player.exe.*"--instance" "{instance.name}"' - rf'BstkSVC.exe.*-Embedding' + rf'|BstkSVC.exe.*-Embedding' rf')' ) elif instance == Emulator.BlueStacks4: @@ -307,7 +317,7 @@ def _emulator_stop_hard(self, instance: EmulatorInstance): self.kill_process_by_regex( rf'(' rf'HD-Player.exe.*"--instance" "{instance.name}"' - rf'BstkSVC.exe.*-Embedding' + rf'|BstkSVC.exe.*-Embedding' rf')' ) else: From 899de34ead2e7c15abd5797e4542f45302f12eb0 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 8 Jun 2024 22:01:12 +0800 Subject: [PATCH 029/161] Opt: del old function and opt logics. :) --- module/device/device.py | 18 +- module/device/platform/platform_windows.py | 194 +++++---------------- 2 files changed, 55 insertions(+), 157 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index deb758ce98..64b484a5e3 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -67,6 +67,7 @@ class Device(Screenshot, Control, AppControl): stuck_long_wait_list = ['BATTLE_STATUS_S', 'PAUSE', 'LOGIN_CHECK'] def __init__(self, *args, **kwargs): + self.initialized = False for _ in range(2): try: super().__init__(*args, **kwargs) @@ -100,8 +101,9 @@ def __init__(self, *args, **kwargs): if self.config.Emulator_ControlMethod == 'minitouch': self.early_minitouch_init() - self.get_process(self.emulator_instance) - self.reshow_window(self.emulator_instance) + if not self.initialized: + self.switch_window() + self.initialized = True def run_simple_screenshot_benchmark(self): """ @@ -333,5 +335,15 @@ def emulator_start(self): f'please set a correct serial' ) raise + if not self.initialized: + self.initialized = True + self.switch_window() self.stuck_record_clear() - self.click_record_clear() \ No newline at end of file + self.click_record_clear() + + def switch_window(self): + import win32con + if self.config.Emulator_SilentStart: + return super().switch_window(win32con.SW_MINIMIZE) + else: + return super().switch_window(win32con.SW_SHOW) \ No newline at end of file diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 6780fbdbd0..16675ec369 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,4 +1,3 @@ -import ctypes import re import win32process import win32con @@ -19,44 +18,8 @@ class EmulatorUnknown(Exception): pass -def get_focused_window(): - return ctypes.windll.user32.GetForegroundWindow() - - -def set_focus_window(hwnd): - ctypes.windll.user32.SetForegroundWindow(hwnd) - - -def minimize_window(hwnd): - ctypes.windll.user32.ShowWindow(hwnd, 6) - - -def switch_window(hwnd:int, arg:int): - win32gui.ShowWindow(hwnd,arg) - - -def gethwnds(pid: int) -> list: - def callback(hwnd:int, hwnds:list): - _, fpid = win32process.GetWindowThreadProcessId(hwnd) - if fpid == pid: - hwnds.append(hwnd) - return True - hwnds = [] - win32gui.EnumWindows(callback, hwnds) - return hwnds - - -def get_window_title(hwnd): - """Returns the window title as a string.""" - text_len_in_characters = ctypes.windll.user32.GetWindowTextLengthW(hwnd) - string_buffer = ctypes.create_unicode_buffer( - text_len_in_characters + 1) # +1 for the \0 at the end of the null-terminated string. - ctypes.windll.user32.GetWindowTextW(hwnd, string_buffer, text_len_in_characters + 1) - return string_buffer.value - - -def flash_window(hwnd, flash=True): - ctypes.windll.user32.FlashWindow(hwnd, flash) +class HwndNotFoundError(Exception): + pass class PlatformWindows(PlatformBase, EmulatorManager): @@ -73,7 +36,6 @@ def execute(self, command: str): Returns: win32process.CreateProcess -> tuple(int, int, Incomplete, Incomplete): """ - # CAUTION!!!!!!: Windows only. command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') startupinfo = win32process.STARTUPINFO() @@ -82,26 +44,7 @@ def execute(self, command: str): startupinfo.wShowWindow = win32con.SW_HIDE else: startupinfo.wShowWindow = win32con.SW_MINIMIZE - return win32process.CreateProcess( - None,command,None,None,False, - win32con.NORMAL_PRIORITY_CLASS, - None,None,startupinfo - ) - - @classmethod - def kill_process(cls, command: str): - """ - Args: - command (str): - - Returns: - win32process.CreateProcess -> tuple(int, int, Incomplete, Incomplete): - """ - command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') - logger.info(f'Execute: {command}') - startupinfo = win32process.STARTUPINFO() - startupinfo.dwFlags = win32con.STARTF_USESHOWWINDOW - return win32process.CreateProcess( + return win32process.CreateProcess( #Only work for Windows. None,command,None,None,False, win32con.NORMAL_PRIORITY_CLASS, None,None,startupinfo @@ -129,7 +72,24 @@ def kill_process_by_regex(cls, regex: str) -> int: return count - def get_process(self, instance: EmulatorInstance): + def gethwnds(self, pid: int): + def callback(hwnd:int, hwnds:list): + _, fpid = win32process.GetWindowThreadProcessId(hwnd) + if fpid == pid: + hwnds.append(hwnd) + return True + hwnds = [] + win32gui.EnumWindows(callback, hwnds) + if not hwnds: + logger.critical( + "Hwnd not found! " + "1.Maybe emulator was killed " + "2.Environment has something wrong. Please check the running environment. " + ) + raise HwndNotFoundError("Hwnd not found") + self.hwnds = hwnds + + def getprocess(self, instance: EmulatorInstance): for proc in psutil.process_iter(['pid', 'name', 'cmdline']): if proc.info['cmdline'] is None: continue @@ -151,18 +111,20 @@ def get_process(self, instance: EmulatorInstance): if matchstr and matchstr.group() == instance.name: self.process = proc break - self.hwnds = gethwnds(self.process.pid) + raise ProcessLookupError("Process not found") - def reshow_window(self, arg: int): + def _switch_window(self, hwnd:int, arg:int): + win32gui.ShowWindow(hwnd,arg) + + def switch_window(self, arg: int): if self.process is None: return for hwnd in self.hwnds: if win32gui.GetParent(hwnd): continue - rect = win32gui.GetWindowRect(hwnd) - if set(rect) == {0}: + if set(win32gui.GetWindowRect(hwnd)) == {0}: continue - switch_window(hwnd,win32con.SW_MINIMIZE)# May arg will be sent in. + self._switch_window(hwnd,arg)# May arg will be sent in. def _emulator_start(self, instance: EmulatorInstance): """ @@ -237,13 +199,13 @@ def _emulator_stop(self, instance: EmulatorInstance): # E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.kill_process(f'"{os.path.join(os.path.dirname(exe),"MuMuManager.exe")}" api -v {instance.MuMuPlayer12_id} shutdown_player') + self.execute(f'"{os.path.join(os.path.dirname(exe),"MuMuManager.exe")}" api -v {instance.MuMuPlayer12_id} shutdown_player') elif instance == Emulator.LDPlayerFamily: # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 - self.kill_process(f'"{os.path.join(os.path.dirname(exe),"dnconsole.exe")}" quit --index {instance.LDPlayer_id}') + self.execute(f'"{os.path.join(os.path.dirname(exe),"dnconsole.exe")}" quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit - self.kill_process(f'"{exe}" -clone:{instance.name} -quit') + self.execute(f'"{exe}" -clone:{instance.name} -quit') elif instance == Emulator.BlueStacks5: # BlueStack has 2 processes # C:\Program Files\BlueStacks_nxt_cn\HD-Player.exe --instance Pie64 @@ -256,70 +218,10 @@ def _emulator_stop(self, instance: EmulatorInstance): ) elif instance == Emulator.BlueStacks4: # E:\Program Files (x86)\BluestacksCN\bsconsole.exe quit --name Android - self.kill_process(f'"{os.path.join(os.path.dirname(exe),"bsconsole.exe")}" quit --name {instance.name}') + self.execute(f'"{os.path.join(os.path.dirname(exe),"bsconsole.exe")}" quit --name {instance.name}') elif instance == Emulator.MEmuPlayer: # F:\Program Files\Microvirt\MEmu\memuc.exe stop -n MEmu_0 - self.kill_process(f'"{os.path.join(os.path.dirname(exe),"memuc.exe")}" stop -n {instance.name}') - else: - raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') - - def _emulator_stop_hard(self, instance: EmulatorInstance): - """ - stop emulator by kill process - """ - logger.hr('Emulator stop', level=2) - if instance == Emulator.MuMuPlayer: - # MuMu6 does not have multi instance, kill one means kill all - # Has 4 processes - # "C:\Program Files\NemuVbox\Hypervisor\NemuHeadless.exe" --comment nemu-6.0-x64-default --startvm - # "E:\ProgramFiles\MuMu\emulator\nemu\EmulatorShell\NemuPlayer.exe" - # E:\ProgramFiles\MuMu\emulator\nemu\EmulatorShell\NemuService.exe - # "C:\Program Files\NemuVbox\Hypervisor\NemuSVC.exe" -Embedding - self.kill_process_by_regex( - rf'(' - rf'NemuHeadless.exe' - rf'|NemuPlayer.exe\"' - rf'|NemuPlayer.exe$' - rf'|NemuService.exe' - rf'|NemuSVC.exe' - rf')' - ) - elif instance == Emulator.MuMuPlayerX: - # MuMu X has 3 processes - # "E:\ProgramFiles\MuMu9\emulator\nemu9\EmulatorShell\NemuPlayer.exe" -m nemu-12.0-x64-default -s 0 -l - # "C:\Program Files\Muvm6Vbox\Hypervisor\Muvm6Headless.exe" --comment nemu-12.0-x64-default --startvm xxx - # "C:\Program Files\Muvm6Vbox\Hypervisor\Muvm6SVC.exe" --Embedding - self.kill_process_by_regex( - rf'(' - rf'NemuPlayer.exe.*-m {instance.name}' - rf'|Muvm6Headless.exe' - rf'|Muvm6SVC.exe' - rf')' - ) - elif instance == Emulator.MuMuPlayer12: - # MuMu 12 has 2 processes: - # E:\ProgramFiles\Netease\MuMuPlayer-12.0\shell\MuMuPlayer.exe -v 0 - # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMHeadless.exe" --comment MuMuPlayer-12.0-0 --startvm xxx - if instance.MuMuPlayer12_id is None: - logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.kill_process_by_regex( - rf'(' - rf'MuMuVMMHeadless.exe.*--comment {instance.name}' - rf'|MuMuPlayer.exe.*-v {instance.MuMuPlayer12_id}' - rf')' - ) - # There is also a shared service, no need to kill it - # "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMSVC.exe" --Embedding - elif instance == Emulator.BlueStacks5: - # BlueStack has 2 processes - # C:\Program Files\BlueStacks_nxt_cn\HD-Player.exe --instance Pie64 - # C:\Program Files\BlueStacks_nxt_cn\BstkSVC.exe -Embedding - self.kill_process_by_regex( - rf'(' - rf'HD-Player.exe.*"--instance" "{instance.name}"' - rf'|BstkSVC.exe.*-Embedding' - rf')' - ) + self.execute(f'"{os.path.join(os.path.dirname(exe),"memuc.exe")}" stop -n {instance.name}') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') @@ -354,9 +256,7 @@ def emulator_start_watch(self): False if timeout """ logger.hr('Emulator start', level=2) - current_window = get_focused_window() serial = self.emulator_instance.serial - logger.info(f'Current window: {current_window}') def adb_connect(): m = self.adb_client.connect(self.serial) @@ -383,9 +283,12 @@ def show_ping(m): def show_package(m): logger.info(f'Found azurlane packages: {m}') + # Check emulator process and hwnds + self.getprocess(self.emulator_instance) + self.gethwnds(self.process.pid) + interval = Timer(0.5).start() timeout = Timer(300).start() - new_window = 0 while 1: interval.wait() interval.reset() @@ -393,16 +296,6 @@ def show_package(m): logger.warning(f'Emulator start timeout') return False - # Check emulator window showing up - # logger.info([get_focused_window(), get_window_title(get_focused_window())]) - if current_window != 0 and new_window == 0: - new_window = get_focused_window() - if current_window != new_window: - logger.info(f'New window showing up: {new_window}, focus back') - set_focus_window(current_window) - else: - new_window = 0 - # Check device connection devices = self.list_device().select(serial=serial) # logger.info(devices) @@ -440,16 +333,9 @@ def show_package(m): # All check passed break - if new_window != 0 and new_window != current_window: - logger.info(f'Minimize new window: {new_window}') - minimize_window(new_window) - if current_window: - logger.info(f'De-flash current window: {current_window}') - flash_window(current_window, flash=False) - if new_window: - logger.info(f'Flash new window: {new_window}') - flash_window(new_window, flash=True) - logger.info('Emulator start completed') + logger.info(f'Emulator start completed') + logger.info(f'Emulator Process: {self.process}') + logger.info(f'Emulator hwnds: {self.hwnds}') return True def emulator_start(self): From c441356dde7b91f7950a806400fff3bff4bb08f4 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 8 Jun 2024 22:07:06 +0800 Subject: [PATCH 030/161] Fix: fix texts. --- module/device/platform/platform_windows.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 16675ec369..99ce8a67ee 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -83,7 +83,7 @@ def callback(hwnd:int, hwnds:list): if not hwnds: logger.critical( "Hwnd not found! " - "1.Maybe emulator was killed " + "1.Perhaps emulator was killed. " "2.Environment has something wrong. Please check the running environment. " ) raise HwndNotFoundError("Hwnd not found") @@ -286,7 +286,7 @@ def show_package(m): # Check emulator process and hwnds self.getprocess(self.emulator_instance) self.gethwnds(self.process.pid) - + interval = Timer(0.5).start() timeout = Timer(300).start() while 1: @@ -373,6 +373,9 @@ def emulator_stop(self): else: return False + logger.error('Failed to stop emulator 3 times, stopped') + return False + if __name__ == '__main__': self = PlatformWindows('alas') d = self.emulator_instance From 9027f3c082501efb2ddf0e14b9c7a2475b9ac2a0 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 8 Jun 2024 22:38:30 +0800 Subject: [PATCH 031/161] :( --- alas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alas.py b/alas.py index 3b69b9b808..f968a7e489 100644 --- a/alas.py +++ b/alas.py @@ -77,7 +77,7 @@ def run(self, command): except EmulatorNotRunningError as e: logger.warning(e) self.device.emulator_start() - self.config.task_call('Restart') + self.run('start') return True except (GameStuckError, GameTooManyClickError) as e: logger.error(e) From 33d9c64f1f1fd0809107ef7b9b253879d59220c3 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 9 Jun 2024 01:41:09 +0800 Subject: [PATCH 032/161] Upd: Fix bugs. --- alas.py | 13 +++++++------ module/device/platform/platform_windows.py | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/alas.py b/alas.py index f968a7e489..f83a17afa4 100644 --- a/alas.py +++ b/alas.py @@ -452,12 +452,6 @@ def get_next_task(self): str: Name of the next task. """ while 1: - # Reboot emulator - if self.emulator_stopped and task.next_run <= datetime.now(): - self.device.emulator_start() - self.config.task_call('Restart') - self.emulator_stopped = False - task = self.config.get_next() self.config.task = task self.config.bind(task) @@ -512,6 +506,13 @@ def get_next_task(self): del_cached_property(self, 'config') continue + # Reboot emulator + if self.emulator_stopped: + self.device.emulator_start() + if not task == 'Restart': + self.run('start') + self.emulator_stopped = False + del_cached_property(self, 'config') break AzurLaneConfig.is_hoarding_task = False diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 99ce8a67ee..108a4eb34f 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -120,6 +120,8 @@ def switch_window(self, arg: int): if self.process is None: return for hwnd in self.hwnds: + if not win32gui.IsWindow(hwnd): + continue if win32gui.GetParent(hwnd): continue if set(win32gui.GetWindowRect(hwnd)) == {0}: From 6bfbcec18f04b6ac3479703c961f7fff3a009f96 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 9 Jun 2024 02:05:58 +0800 Subject: [PATCH 033/161] Upd: bug fix. --- module/device/platform/platform_windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 108a4eb34f..06f3ab44b9 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -111,6 +111,7 @@ def getprocess(self, instance: EmulatorInstance): if matchstr and matchstr.group() == instance.name: self.process = proc break + if self.process is None: raise ProcessLookupError("Process not found") def _switch_window(self, hwnd:int, arg:int): From d90738095f41cfda1543cbf24a95a90a9d7128f5 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 9 Jun 2024 23:01:55 +0800 Subject: [PATCH 034/161] Upd: Fix bugs. --- module/device/connection.py | 3 -- module/device/method/adb.py | 5 +- module/device/method/ascreencap.py | 5 +- module/device/method/droidcast.py | 5 +- module/device/method/hermit.py | 5 +- module/device/method/maatouch.py | 5 +- module/device/method/minitouch.py | 5 +- module/device/method/nemu_ipc.py | 5 +- module/device/method/scrcpy/scrcpy.py | 5 +- module/device/method/uiautomator_2.py | 5 +- module/device/method/wsa.py | 5 +- module/device/platform/platform_windows.py | 58 ++++++---------------- 12 files changed, 25 insertions(+), 86 deletions(-) diff --git a/module/device/connection.py b/module/device/connection.py index 03e783c41e..71ebbdeef7 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -59,9 +59,6 @@ def init(): def init(): self.detect_package() - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/adb.py b/module/device/method/adb.py index 2aedbb568e..bd3397edf7 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -11,7 +11,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, remove_prefix, handle_adb_error, ImageTruncated, PackageNotInstalled) -from module.exception import EmulatorNotRunningError, RequestHumanTakeover, ScriptError +from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -57,9 +57,6 @@ def init(): def init(): pass - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) diff --git a/module/device/method/ascreencap.py b/module/device/method/ascreencap.py index d6130ee51d..c93321cfe2 100644 --- a/module/device/method/ascreencap.py +++ b/module/device/method/ascreencap.py @@ -8,7 +8,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, ImageTruncated) -from module.exception import EmulatorNotRunningError, RequestHumanTakeover, ScriptError +from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -58,9 +58,6 @@ def init(): def init(): pass - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index 3c43b2be42..73c0f22b9b 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -11,7 +11,7 @@ from module.device.method.uiautomator_2 import ProcessInfo, Uiautomator2 from module.device.method.utils import ( ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, retry_sleep) -from module.exception import EmulatorNotRunningError, RequestHumanTakeover +from module.exception import RequestHumanTakeover from module.logger import logger @@ -75,9 +75,6 @@ def init(): def init(): pass - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) diff --git a/module/device/method/hermit.py b/module/device/method/hermit.py index e474ff8eb8..ad0c39746e 100644 --- a/module/device/method/hermit.py +++ b/module/device/method/hermit.py @@ -10,7 +10,7 @@ from module.device.method.adb import Adb from module.device.method.utils import (RETRY_TRIES, retry_sleep, HierarchyButton, handle_adb_error) -from module.exception import EmulatorNotRunningError, RequestHumanTakeover +from module.exception import RequestHumanTakeover from module.logger import logger @@ -71,9 +71,6 @@ def init(): def init(): self.adb_reconnect() self.hermit_init() - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index 55ebc42298..04f1d1aa1d 100644 --- a/module/device/method/maatouch.py +++ b/module/device/method/maatouch.py @@ -11,7 +11,7 @@ from module.device.connection import Connection from module.device.method.minitouch import Command, CommandBuilder, insert_swipe from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep -from module.exception import EmulatorNotRunningError, RequestHumanTakeover +from module.exception import RequestHumanTakeover from module.logger import logger @@ -75,9 +75,6 @@ def init(): def init(): del_cached_property(self, '_maatouch_builder') - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index aeb100ad36..d48ed4191f 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -15,7 +15,7 @@ from module.base.utils import * from module.device.connection import Connection from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep -from module.exception import EmulatorNotRunningError, RequestHumanTakeover, ScriptError +from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -440,9 +440,6 @@ def init(): def init(): del_cached_property(self, '_minitouch_builder') - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/nemu_ipc.py b/module/device/method/nemu_ipc.py index 4ca8ed82bf..b79e1c3225 100644 --- a/module/device/method/nemu_ipc.py +++ b/module/device/method/nemu_ipc.py @@ -12,7 +12,7 @@ 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 EmulatorNotRunningError, RequestHumanTakeover +from module.exception import RequestHumanTakeover from module.logger import logger @@ -184,9 +184,6 @@ def init(): def init(): self.reconnect() - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/scrcpy/scrcpy.py b/module/device/method/scrcpy/scrcpy.py index 4b23dd85ed..0730e9bac7 100644 --- a/module/device/method/scrcpy/scrcpy.py +++ b/module/device/method/scrcpy/scrcpy.py @@ -11,7 +11,7 @@ from module.device.method.scrcpy.core import ScrcpyCore, ScrcpyError from module.device.method.uiautomator_2 import Uiautomator2 from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep -from module.exception import EmulatorNotRunningError, RequestHumanTakeover +from module.exception import RequestHumanTakeover from module.logger import logger @@ -64,9 +64,6 @@ def init(): self.adb_reconnect() else: break - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index 4b44ff6b4f..c116bce94a 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -12,7 +12,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, ImageTruncated, PackageNotInstalled, possible_reasons) -from module.exception import EmulatorNotRunningError, RequestHumanTakeover +from module.exception import RequestHumanTakeover from module.logger import logger @@ -81,9 +81,6 @@ def init(): def init(): pass - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown except Exception as e: logger.exception(e) diff --git a/module/device/method/wsa.py b/module/device/method/wsa.py index 0351fe182c..56f0ea23f6 100644 --- a/module/device/method/wsa.py +++ b/module/device/method/wsa.py @@ -6,7 +6,7 @@ from module.device.connection import Connection from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, PackageNotInstalled) -from module.exception import EmulatorNotRunningError, RequestHumanTakeover +from module.exception import RequestHumanTakeover from module.logger import logger @@ -46,9 +46,6 @@ def init(): def init(): self.detect_package() - # Emulator not running - except EmulatorNotRunningError: - raise EmulatorNotRunningError("Emulator not running") # Unknown, probably a trucked image except Exception as e: logger.exception(e) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 06f3ab44b9..5ffb33292c 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -25,7 +25,7 @@ class HwndNotFoundError(Exception): class PlatformWindows(PlatformBase, EmulatorManager): def __init__(self, config): super().__init__(config) - self.process: psutil.Process = None + self.process: tuple = None self.hwnds: list[int] = None def execute(self, command: str): @@ -34,7 +34,7 @@ def execute(self, command: str): command (str): Returns: - win32process.CreateProcess -> tuple(int, int, Incomplete, Incomplete): + win32process.CreateProcess -> tuple(Incomplete, Incomplete, int, int): """ command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') @@ -44,11 +44,12 @@ def execute(self, command: str): startupinfo.wShowWindow = win32con.SW_HIDE else: startupinfo.wShowWindow = win32con.SW_MINIMIZE - return win32process.CreateProcess( #Only work for Windows. + self.process = win32process.CreateProcess( #Only work for Windows. None,command,None,None,False, - win32con.NORMAL_PRIORITY_CLASS, + win32con.DETACHED_PROCESS, None,None,startupinfo ) + return True @classmethod def kill_process_by_regex(cls, regex: str) -> int: @@ -73,7 +74,7 @@ def kill_process_by_regex(cls, regex: str) -> int: return count def gethwnds(self, pid: int): - def callback(hwnd:int, hwnds:list): + def callback(hwnd: int, hwnds: list): _, fpid = win32process.GetWindowThreadProcessId(hwnd) if fpid == pid: hwnds.append(hwnd) @@ -82,37 +83,12 @@ def callback(hwnd:int, hwnds:list): win32gui.EnumWindows(callback, hwnds) if not hwnds: logger.critical( - "Hwnd not found! " - "1.Perhaps emulator was killed. " + "Hwnd not found! \n" + "1.Perhaps emulator was killed. \n" "2.Environment has something wrong. Please check the running environment. " ) raise HwndNotFoundError("Hwnd not found") - self.hwnds = hwnds - - def getprocess(self, instance: EmulatorInstance): - for proc in psutil.process_iter(['pid', 'name', 'cmdline']): - if proc.info['cmdline'] is None: - continue - cmdline = ' '.join(proc.info['cmdline']).replace(r"\\", "/").replace("\\", "/").replace('"', '"') - if not instance.path in cmdline: - continue - if instance == Emulator.MuMuPlayer12: - match = re.search(r'\d+$',cmdline) - if match and int(match.group()) == instance.MuMuPlayer12_id: - self.process = proc - break - elif instance == Emulator.LDPlayerFamily: - match = re.search(r'\d+$',cmdline) - if match and int(match.group()) == instance.LDPlayer_id: - self.process = proc - break - else: - matchstr = re.search(fr'\b{instance.name}$',cmdline) - if matchstr and matchstr.group() == instance.name: - self.process = proc - break - if self.process is None: - raise ProcessLookupError("Process not found") + return hwnds def _switch_window(self, hwnd:int, arg:int): win32gui.ShowWindow(hwnd,arg) @@ -127,7 +103,7 @@ def switch_window(self, arg: int): continue if set(win32gui.GetWindowRect(hwnd)) == {0}: continue - self._switch_window(hwnd,arg)# May arg will be sent in. + self._switch_window(hwnd, arg)# May arg will be sent in. def _emulator_start(self, instance: EmulatorInstance): """ @@ -228,7 +204,7 @@ def _emulator_stop(self, instance: EmulatorInstance): else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') - def _emulator_function_wrapper(self, func): + def _emulator_function_wrapper(self, func: callable): """ Args: func (callable): _emulator_start or _emulator_stop @@ -258,7 +234,7 @@ def emulator_start_watch(self): bool: True if startup completed False if timeout """ - logger.hr('Emulator start', level=2) + logger.info("Emulator starting...") serial = self.emulator_instance.serial def adb_connect(): @@ -286,10 +262,6 @@ def show_ping(m): def show_package(m): logger.info(f'Found azurlane packages: {m}') - # Check emulator process and hwnds - self.getprocess(self.emulator_instance) - self.gethwnds(self.process.pid) - interval = Timer(0.5).start() timeout = Timer(300).start() while 1: @@ -336,6 +308,9 @@ def show_package(m): # All check passed break + # Check emulator process and hwnds + self.hwnds = self.gethwnds(self.process[2]) + logger.info(f'Emulator start completed') logger.info(f'Emulator Process: {self.process}') logger.info(f'Emulator hwnds: {self.hwnds}') @@ -344,9 +319,6 @@ def show_package(m): def emulator_start(self): logger.hr('Emulator start', level=1) for _ in range(3): - # Stop - if not self._emulator_function_wrapper(self._emulator_stop): - return False # Start if self._emulator_function_wrapper(self._emulator_start): # Success From 9880b20b252cfd9c9c356f6c8610567775f2053d Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 10 Jun 2024 21:06:13 +0800 Subject: [PATCH 035/161] Upd: fix bugs. --- alas.py | 22 +++++++++++++++------- module/device/platform/platform_windows.py | 3 +-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/alas.py b/alas.py index f83a17afa4..73b69a1b74 100644 --- a/alas.py +++ b/alas.py @@ -460,6 +460,14 @@ def get_next_task(self): if self.config.task.command != 'Alas': release_resources(next_task=task.command) + # Reboot emulator + if self.emulator_stopped: + self.device.emulator_start() + if not task == 'Restart': + self.run('start') + self.emulator_stopped = False + del_cached_property(self, 'config') + if task.next_run > datetime.now(): logger.info(f'Wait until {task.next_run} for task `{task.command}`') self.is_first_task = False @@ -506,13 +514,6 @@ def get_next_task(self): del_cached_property(self, 'config') continue - # Reboot emulator - if self.emulator_stopped: - self.device.emulator_start() - if not task == 'Restart': - self.run('start') - self.emulator_stopped = False - del_cached_property(self, 'config') break AzurLaneConfig.is_hoarding_task = False @@ -543,6 +544,13 @@ def loop(self): _ = self.device # Get task task = self.get_next_task() + # Reboot emulator + if self.emulator_stopped: + self.device.emulator_start() + if not task == 'Restart': + self.run('start') + self.emulator_stopped = False + del_cached_property(self, 'config') # Skip first restart if self.is_first_task and task == 'Restart': logger.info('Skip task `Restart` at scheduler start') diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 5ffb33292c..cbae2b7487 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -103,7 +103,7 @@ def switch_window(self, arg: int): continue if set(win32gui.GetWindowRect(hwnd)) == {0}: continue - self._switch_window(hwnd, arg)# May arg will be sent in. + self._switch_window(hwnd, arg) def _emulator_start(self, instance: EmulatorInstance): """ @@ -144,7 +144,6 @@ def _emulator_stop(self, instance: EmulatorInstance): Stop a emulator without error handling """ import os - logger.hr('Emulator stop', level=2) exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: # MuMu6 does not have multi instance, kill one means kill all From 7d0ca3abb5d2e3fef4210a69eece52578c8d07d3 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 10 Jun 2024 21:20:00 +0800 Subject: [PATCH 036/161] Upd: opt logics --- alas.py | 92 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/alas.py b/alas.py index 73b69a1b74..fef0296ed2 100644 --- a/alas.py +++ b/alas.py @@ -468,51 +468,53 @@ def get_next_task(self): self.emulator_stopped = False del_cached_property(self, 'config') - if task.next_run > datetime.now(): - logger.info(f'Wait until {task.next_run} for task `{task.command}`') - self.is_first_task = False - method = self.config.Optimization_WhenTaskQueueEmpty - if method == 'close_game': - logger.info('Close game during wait') - self.device.app_stop() - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue - if task.command != 'Restart': - self.run('start') - elif method == 'goto_main': - logger.info('Goto main page during wait') - self.run('goto_main') - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue - elif method == 'stay_there': - logger.info('Stay there during wait') - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue - elif method == 'stop_emulator': - logger.info('Stop emulator during wait') - self.device.emulator_stop() - self.emulator_stopped = True - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue - else: - logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue + if task.next_run <= datetime.now(): + break + + logger.info(f'Wait until {task.next_run} for task `{task.command}`') + self.is_first_task = False + method = self.config.Optimization_WhenTaskQueueEmpty + if method == 'close_game': + logger.info('Close game during wait') + self.device.app_stop() + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + if task.command != 'Restart': + self.run('start') + elif method == 'goto_main': + logger.info('Goto main page during wait') + self.run('goto_main') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + elif method == 'stay_there': + logger.info('Stay there during wait') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + elif method == 'stop_emulator': + logger.info('Stop emulator during wait') + self.device.emulator_stop() + self.emulator_stopped = True + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + else: + logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue break From b774dacb70537b2863fa1114f387dee5015e88ee Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 15 Jun 2024 01:47:50 +0800 Subject: [PATCH 037/161] Opt: Add winapi. --- alas.py | 16 +- module/device/device.py | 13 +- module/device/platform/platform_windows.py | 96 ++++--- module/device/platform/winapi.py | 308 +++++++++++++++++++++ 4 files changed, 376 insertions(+), 57 deletions(-) create mode 100644 module/device/platform/winapi.py diff --git a/alas.py b/alas.py index fef0296ed2..e82aff69f4 100644 --- a/alas.py +++ b/alas.py @@ -26,7 +26,6 @@ def __init__(self, config_name='alas'): # Failure count of tasks # Key: str, task name, value: int, failure count self.failure_record = {} - self.emulator_stopped = False @cached_property def config(self): @@ -74,11 +73,6 @@ def run(self, command): logger.warning(e) self.config.task_call('Restart') return True - except EmulatorNotRunningError as e: - logger.warning(e) - self.device.emulator_start() - self.run('start') - return True except (GameStuckError, GameTooManyClickError) as e: logger.error(e) self.save_error_log() @@ -461,12 +455,13 @@ def get_next_task(self): release_resources(next_task=task.command) # Reboot emulator - if self.emulator_stopped: + if not self.device.emulator_check() and task.next_run <= datetime.now(): + logger.warning('Emulator is not running') self.device.emulator_start() if not task == 'Restart': self.run('start') - self.emulator_stopped = False del_cached_property(self, 'config') + break if task.next_run <= datetime.now(): break @@ -502,7 +497,6 @@ def get_next_task(self): elif method == 'stop_emulator': logger.info('Stop emulator during wait') self.device.emulator_stop() - self.emulator_stopped = True release_resources() self.device.release_during_wait() if not self.wait_until(task.next_run): @@ -547,11 +541,11 @@ def loop(self): # Get task task = self.get_next_task() # Reboot emulator - if self.emulator_stopped: + if not self.device.emulator_check(): + logger.warning('Emulator is not running') self.device.emulator_start() if not task == 'Restart': self.run('start') - self.emulator_stopped = False del_cached_property(self, 'config') # Skip first restart if self.is_first_task and task == 'Restart': diff --git a/module/device/device.py b/module/device/device.py index 64b484a5e3..b5bded183b 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -101,9 +101,11 @@ def __init__(self, *args, **kwargs): if self.config.Emulator_ControlMethod == 'minitouch': self.early_minitouch_init() + import sys if not self.initialized: - self.switch_window() self.initialized = True + if sys.platform == 'win32': + self.switch_window() def run_simple_screenshot_benchmark(self): """ @@ -136,6 +138,9 @@ def method_check(self): # self.config.Emulator_ControlMethod = 'minitouch' pass + def emulator_check(self): + return super().emulator_check() + def handle_night_commission(self, daily_trigger='21:00', threshold=30): """ Args: @@ -342,8 +347,8 @@ def emulator_start(self): self.click_record_clear() def switch_window(self): - import win32con + from module.device.platform import winapi if self.config.Emulator_SilentStart: - return super().switch_window(win32con.SW_MINIMIZE) + return super().switch_window(winapi.SW_MINIMIZE) else: - return super().switch_window(win32con.SW_SHOW) \ No newline at end of file + return super().switch_window(winapi.SW_SHOW) \ No newline at end of file diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index cbae2b7487..3f76f3aec2 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,7 +1,5 @@ +import ctypes import re -import win32process -import win32con -import win32gui import psutil @@ -11,6 +9,7 @@ from module.device.connection import AdbDeviceWithStatus from module.device.platform.platform_base import PlatformBase from module.device.platform.emulator_windows import Emulator, EmulatorInstance, EmulatorManager +from module.device.platform import winapi from module.logger import logger @@ -18,15 +17,13 @@ class EmulatorUnknown(Exception): pass -class HwndNotFoundError(Exception): - pass - - class PlatformWindows(PlatformBase, EmulatorManager): def __init__(self, config): super().__init__(config) self.process: tuple = None + self.proc: psutil.Process = None self.hwnds: list[int] = None + self.focusedwindow: int = None def execute(self, command: str): """ @@ -34,21 +31,11 @@ def execute(self, command: str): command (str): Returns: - win32process.CreateProcess -> tuple(Incomplete, Incomplete, int, int): + bool: """ command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') - startupinfo = win32process.STARTUPINFO() - startupinfo.dwFlags = win32con.STARTF_USESHOWWINDOW - if self.config.Emulator_SilentStart: - startupinfo.wShowWindow = win32con.SW_HIDE - else: - startupinfo.wShowWindow = win32con.SW_MINIMIZE - self.process = win32process.CreateProcess( #Only work for Windows. - None,command,None,None,False, - win32con.DETACHED_PROCESS, - None,None,startupinfo - ) + self.process, self.focusedwindow = winapi.execute(command, self.config.Emulator_SilentStart) return True @classmethod @@ -73,35 +60,30 @@ def kill_process_by_regex(cls, regex: str) -> int: return count - def gethwnds(self, pid: int): - def callback(hwnd: int, hwnds: list): - _, fpid = win32process.GetWindowThreadProcessId(hwnd) - if fpid == pid: - hwnds.append(hwnd) - return True - hwnds = [] - win32gui.EnumWindows(callback, hwnds) - if not hwnds: - logger.critical( - "Hwnd not found! \n" - "1.Perhaps emulator was killed. \n" - "2.Environment has something wrong. Please check the running environment. " - ) - raise HwndNotFoundError("Hwnd not found") - return hwnds + @staticmethod + def gethwnds(pid: int) -> list: + return winapi.gethwnds(pid) + + @staticmethod + def getprocess(instance: EmulatorInstance): + return winapi.getprocess(instance) - def _switch_window(self, hwnd:int, arg:int): - win32gui.ShowWindow(hwnd,arg) + @staticmethod + def _switch_window(hwnd: int, arg: int): + winapi.ShowWindow(hwnd, arg) + return True def switch_window(self, arg: int): if self.process is None: return for hwnd in self.hwnds: - if not win32gui.IsWindow(hwnd): + if not winapi.IsWindow(hwnd): continue - if win32gui.GetParent(hwnd): + if winapi.GetParent(hwnd): continue - if set(win32gui.GetWindowRect(hwnd)) == {0}: + rect = winapi.RECT() + winapi.GetWindowRect(hwnd, ctypes.byref(rect)) + if {rect.left, rect.top, rect.right, rect.bottom} == {0}: continue self._switch_window(hwnd, arg) @@ -177,7 +159,10 @@ def _emulator_stop(self, instance: EmulatorInstance): # E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.execute(f'"{os.path.join(os.path.dirname(exe),"MuMuManager.exe")}" api -v {instance.MuMuPlayer12_id} shutdown_player') + self.execute( + f'"{os.path.join(os.path.dirname(exe),"MuMuManager.exe")}"' + f' api -v {instance.MuMuPlayer12_id} shutdown_player' + ) elif instance == Emulator.LDPlayerFamily: # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 self.execute(f'"{os.path.join(os.path.dirname(exe),"dnconsole.exe")}" quit --index {instance.LDPlayer_id}') @@ -191,7 +176,6 @@ def _emulator_stop(self, instance: EmulatorInstance): self.kill_process_by_regex( rf'(' rf'HD-Player.exe.*"--instance" "{instance.name}"' - rf'|BstkSVC.exe.*-Embedding' rf')' ) elif instance == Emulator.BlueStacks4: @@ -306,9 +290,14 @@ def show_package(m): # All check passed break + + # Flash window + if self.focusedwindow != winapi.getfocusedwindow(): + winapi.setfocustowindow(self.focusedwindow) # Check emulator process and hwnds self.hwnds = self.gethwnds(self.process[2]) + self.proc = psutil.Process(self.process[2]) logger.info(f'Emulator start completed') logger.info(f'Emulator Process: {self.process}') @@ -350,6 +339,29 @@ def emulator_stop(self): logger.error('Failed to stop emulator 3 times, stopped') return False + def emulator_check(self): + try: + if self.process is None: + self.process = self.getprocess(self.emulator_instance) + return True + if self.proc is None: + pid = self.process[2] + self.proc = psutil.Process(pid) + cmdline = DataProcessInfo(proc=self.proc, pid=self.proc.pid).cmdline + if self.emulator_instance.path in cmdline: + return True + else: + return False + except ProcessLookupError as e: + return False + except psutil.NoSuchProcess as e: + return False + except psutil.AccessDenied as e: + return False + except Exception as e: + logger.error(e) + return False + if __name__ == '__main__': self = PlatformWindows('alas') d = self.emulator_instance diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py new file mode 100644 index 0000000000..7fc3c94c4c --- /dev/null +++ b/module/device/platform/winapi.py @@ -0,0 +1,308 @@ +import ctypes +import ctypes.wintypes +import re + +import psutil + +from deploy.Windows.utils import DataProcessInfo +from module.device.platform.emulator_windows import Emulator, EmulatorInstance +from module.logger import logger + + +PyHANDLE = ctypes.wintypes.HANDLE +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +psapi = ctypes.windll.psapi +DWORD = ctypes.wintypes.DWORD +WORD = ctypes.wintypes.WORD +BYTE = ctypes.wintypes.BYTE +BOOL = ctypes.wintypes.BOOL +LONG = ctypes.wintypes.LONG +CHAR = ctypes.wintypes.CHAR +WCHAR = ctypes.wintypes.WCHAR +LPWSTR = ctypes.wintypes.LPWSTR +LPCWSTR = ctypes.wintypes.LPCWSTR +LPARAM = ctypes.wintypes.LPARAM +LPVOID = ctypes.wintypes.LPVOID +HWND = ctypes.wintypes.HWND +RECT = ctypes.wintypes.RECT +ULONG_PTR = ctypes.wintypes.PULONG +MAX_PATH = ctypes.wintypes.MAX_PATH + + +class EmulatorLaunchFailedError(Exception): + pass + +class HwndNotFoundError(Exception): + pass + + +PROCESS_ALL_ACCESS = 0x1F0FFF +THREAD_ALL_ACCESS = 0x1F03FF +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_VM_READ = 0x0010 +ERROR_NO_MORE_FILES = 0x12 +TH32CS_SNAPPROCESS = DWORD(0x00000002) + +STARTF_USESHOWWINDOW = 1 +STARTF_USESIZE = 2 +STARTF_USEPOSITION = 4 +STARTF_USECOUNTCHARS = 8 +STARTF_USEFILLATTRIBUTE = 16 +STARTF_FORCEONFEEDBACK = 64 +STARTF_FORCEOFFFEEDBACK = 128 +STARTF_USESTDHANDLES = 256 +STARTF_USEHOTKEY = 512 + +SW_HIDE = 0 +SW_SHOWNORMAL = 1 +SW_NORMAL = 1 +SW_SHOWMINIMIZED = 2 +SW_SHOWMAXIMIZED = 3 +SW_MAXIMIZE = 3 +SW_SHOWNOACTIVATE = 4 +SW_SHOW = 5 +SW_MINIMIZE = 6 +SW_SHOWMINNOACTIVE = 7 +SW_SHOWNA = 8 +SW_RESTORE = 9 +SW_SHOWDEFAULT = 10 +SW_FORCEMINIMIZE = 11 +SW_MAX = 11 + +DEBUG_PROCESS = 1 +DEBUG_ONLY_THIS_PROCESS = 2 +CREATE_SUSPENDED = 4 +DETACHED_PROCESS = 8 +CREATE_NEW_CONSOLE = 16 +NORMAL_PRIORITY_CLASS = 32 +IDLE_PRIORITY_CLASS = 64 +HIGH_PRIORITY_CLASS = 128 +REALTIME_PRIORITY_CLASS = 256 +CREATE_NEW_PROCESS_GROUP = 512 +CREATE_UNICODE_ENVIRONMENT = 1024 +CREATE_SEPARATE_WOW_VDM = 2048 +CREATE_SHARED_WOW_VDM = 4096 +CREATE_DEFAULT_ERROR_MODE = 67108864 +CREATE_NO_WINDOW = 134217728 +PROFILE_USER = 268435456 +PROFILE_KERNEL = 536870912 +PROFILE_SERVER = 1073741824 + + +class STARTUPINFO(ctypes.Structure): + _fields_ = [ + ('cb', DWORD), + ('lpReserved', LPWSTR), + ('lpDesktop', LPWSTR), + ('lpTitle', LPWSTR), + ('dwX', DWORD), + ('dwY', DWORD), + ('dwXSize', DWORD), + ('dwYSize', DWORD), + ('dwXCountChars', DWORD), + ('dwYCountChars', DWORD), + ('dwFillAttribute', DWORD), + ('dwFlags', DWORD), + ('wShowWindow', WORD), + ('cbReserved2', WORD), + ('lpReserved2', ctypes.POINTER(BYTE)), + ('hStdInput', PyHANDLE), + ('hStdOutput', PyHANDLE), + ('hStdError', PyHANDLE), + ] + +class PROCESS_INFORMATION(ctypes.Structure): + _fields_ = [ + ('hProcess', PyHANDLE), + ('hThread', PyHANDLE), + ('dwProcessId', DWORD), + ('dwThreadId', DWORD), + ] + +class SECURITY_ATTRIBUTES(ctypes.Structure): + _fields_ = [ + ("nLength", DWORD), + ("lpSecurityDescriptor", ctypes.c_void_p), + ("bInheritHandle", BOOL) + ] + +class PROCESSENTRY32(ctypes.Structure): + _fields_ = [ + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ProcessID", DWORD), + ("th32DefaultHeapID", ULONG_PTR), + ("th32ModuleID", DWORD), + ("cntThreads", DWORD), + ("th32ParentProcessID", DWORD), + ("pcPriClassBase", LONG), + ("dwFlags", DWORD), + ("szExeFile", CHAR * MAX_PATH), + ] + + +CreateProcessW = kernel32.CreateProcessW +CreateProcessW.argtypes = [ + LPCWSTR, + LPWSTR, + ctypes.POINTER(SECURITY_ATTRIBUTES), + ctypes.POINTER(SECURITY_ATTRIBUTES), + BOOL, + DWORD, + LPVOID, + LPCWSTR, + ctypes.POINTER(STARTUPINFO), + ctypes.POINTER(PROCESS_INFORMATION) +] +CreateProcessW.restype = BOOL + +GetForegroundWindow = user32.GetForegroundWindow +SetForegroundWindow = user32.SetForegroundWindow +SetForegroundWindow.argtypes = [HWND] +SetForegroundWindow.restype = BOOL + +ShowWindow = user32.ShowWindow +IsWindow = user32.IsWindow +GetParent = user32.GetParent +GetWindowRect = user32.GetWindowRect + +EnumWindows = user32.EnumWindows +EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) +GetWindowThreadProcessId = user32.GetWindowThreadProcessId +GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(DWORD)] +GetWindowThreadProcessId.restype = DWORD + +OpenProcess = kernel32.OpenProcess +OpenProcess.argtypes = [DWORD, BOOL, DWORD] +OpenProcess.restype = PyHANDLE +OpenThread = kernel32.OpenThread + +CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot +CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] +CreateToolhelp32Snapshot.restype = PyHANDLE + +CloseHandle = kernel32.CloseHandle +CloseHandle.argtypes = [PyHANDLE] +CloseHandle.restype = BOOL + +Process32First = kernel32.Process32First +Process32First.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32First.restype = BOOL + +Process32Next = kernel32.Process32Next +Process32Next.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32Next.restype = BOOL + +GetLastError = kernel32.GetLastError +GetLastError.restype = BOOL + +def getfocusedwindow() -> int: + return GetForegroundWindow() + +def setfocustowindow(hwnd: int) -> bool: + return SetForegroundWindow(hwnd) + +def execute(command: str, arg: bool): + startupinfo = STARTUPINFO() + startupinfo.cb = ctypes.sizeof(STARTUPINFO) + startupinfo.dwFlags = STARTF_USESHOWWINDOW + startupinfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE + + focusedwindow = getfocusedwindow() + + processinformation = PROCESS_INFORMATION() + + success = CreateProcessW( + None, + command, + None, + None, + False, + DETACHED_PROCESS, + None, + None, + ctypes.byref(startupinfo), + ctypes.byref(processinformation) + ) + + if not success: + errorcode = GetLastError() + raise EmulatorLaunchFailedError(f"Failed to start emulator. Error code: {errorcode}") + + process = ( + processinformation.hProcess, + processinformation.hThread, + processinformation.dwProcessId, + processinformation.dwThreadId + ) + return process, focusedwindow + +def gethwnds(pid: int) -> list: + hwnds = [] + + @EnumWindowsProc + def callback(hwnd: int, lparam): + processid = DWORD() + GetWindowThreadProcessId(hwnd, ctypes.byref(processid)) + if processid.value == pid: + hwnds.append(hwnd) + return True + + EnumWindows(callback, 0) + if not hwnds: + logger.critical( + "Hwnd not found! \n" + "1.Perhaps emulator was killed. \n" + "2.Environment has something wrong. Please check the running environment. " + ) + raise HwndNotFoundError("Hwnd not found") + return hwnds + +def _getprocess(proc: psutil.Process): + try: + processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) + if not processhandle: + raise ctypes.WinError(ctypes.get_last_error()) + + mainthreadid = proc.threads()[0].id + threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) + if not threadhandle: + raise ctypes.WinError(ctypes.get_last_error()) + + return (PyHANDLE(processhandle), PyHANDLE(threadhandle), proc.pid, mainthreadid) + except Exception as e: + logger.warning(f"Failed to get process and thread handles: {e}") + return (None, None, proc.pid, proc.threads()[0].id) + +def getprocess(instance: EmulatorInstance): + lppe = PROCESSENTRY32() + lppe.dwSize = ctypes.sizeof(PROCESSENTRY32) + hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) + Process32First(hSnapshot, ctypes.pointer(lppe)) + + while Process32Next(hSnapshot, ctypes.pointer(lppe)): + proc = psutil.Process(lppe.th32ProcessID) + cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + if not instance.path in cmdline: + continue + if instance == Emulator.MuMuPlayer12: + match = re.search(r'\d+$', cmdline) + if match and int(match.group()) == instance.MuMuPlayer12_id: + CloseHandle(hSnapshot) + return _getprocess(proc) + elif instance == Emulator.LDPlayerFamily: + match = re.search(r'\d+$', cmdline) + if match and int(match.group()) == instance.LDPlayer_id: + CloseHandle(hSnapshot) + return _getprocess(proc) + else: + matchstr = re.search(fr'\b{instance.name}$', cmdline) + if matchstr and matchstr.group() == instance.name: + CloseHandle(hSnapshot) + return _getprocess(proc) + else: + errorcode = GetLastError() + if errorcode != ERROR_NO_MORE_FILES: + logger.error(f'Error: {GetLastError()}') + raise ProcessLookupError("Process not found") \ No newline at end of file From 4b394d0879bbfc1fed425113bec0c035fd08d0cd Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 15 Jun 2024 03:35:39 +0800 Subject: [PATCH 038/161] Upd: Fix texts. --- module/config/i18n/en-US.json | 2 +- module/config/i18n/zh-CN.json | 2 +- module/config/i18n/zh-TW.json | 2 +- module/device/platform/winapi.py | 9 ++++++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 03b3f739b5..e2fc315877 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -431,7 +431,7 @@ }, "SilentStart": { "name": "Start the emulator silently", - "help": "If this option is checked, the emulator launched by Alas will run silently in the background. \nIf you need to monitor the emulator, do not check it." + "help": "When checked, the emulator launched by Alas will start silently and be minimized then." } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 219b91cb3b..2ffd46b853 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -431,7 +431,7 @@ }, "SilentStart": { "name": "静默启动模拟器", - "help": "勾选此项后由Alas启动的模拟器将会在后台静默运行,若需要监看模拟器运行状况则不要勾选。" + "help": "勾选此项后由Alas启动的模拟器将会静默启动并转为最小化" } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 60191fab9f..650e813f32 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -431,7 +431,7 @@ }, "SilentStart": { "name": "靜默啓働模擬器", - "help": "勾選此項後由Alas啓働的模擬器將會在後颱靜默運行,若需要監看模擬器運行狀況則不要勾選。" + "help": "勾選此項後由Alas啓働的模擬器將會靜默啓働竝轉爲最小化" } }, "EmulatorInfo": { diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 7fc3c94c4c..31012e688e 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -9,10 +9,10 @@ from module.logger import logger -PyHANDLE = ctypes.wintypes.HANDLE user32 = ctypes.windll.user32 kernel32 = ctypes.windll.kernel32 psapi = ctypes.windll.psapi +PyHANDLE = ctypes.wintypes.HANDLE DWORD = ctypes.wintypes.DWORD WORD = ctypes.wintypes.WORD BYTE = ctypes.wintypes.BYTE @@ -22,12 +22,12 @@ WCHAR = ctypes.wintypes.WCHAR LPWSTR = ctypes.wintypes.LPWSTR LPCWSTR = ctypes.wintypes.LPCWSTR -LPARAM = ctypes.wintypes.LPARAM LPVOID = ctypes.wintypes.LPVOID HWND = ctypes.wintypes.HWND +MAX_PATH = ctypes.wintypes.MAX_PATH +LPARAM = ctypes.wintypes.LPARAM RECT = ctypes.wintypes.RECT ULONG_PTR = ctypes.wintypes.PULONG -MAX_PATH = ctypes.wintypes.MAX_PATH class EmulatorLaunchFailedError(Exception): @@ -44,6 +44,7 @@ class HwndNotFoundError(Exception): ERROR_NO_MORE_FILES = 0x12 TH32CS_SNAPPROCESS = DWORD(0x00000002) +# winbase.h STARTF_USESHOWWINDOW = 1 STARTF_USESIZE = 2 STARTF_USEPOSITION = 4 @@ -54,6 +55,7 @@ class HwndNotFoundError(Exception): STARTF_USESTDHANDLES = 256 STARTF_USEHOTKEY = 512 +# winuser.h SW_HIDE = 0 SW_SHOWNORMAL = 1 SW_NORMAL = 1 @@ -70,6 +72,7 @@ class HwndNotFoundError(Exception): SW_FORCEMINIMIZE = 11 SW_MAX = 11 +# winuser.h DEBUG_PROCESS = 1 DEBUG_ONLY_THIS_PROCESS = 2 CREATE_SUSPENDED = 4 From a1a0b7c5a150fc040bd2f0a755c4f5b86b1c071a Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 15 Jun 2024 12:37:37 +0800 Subject: [PATCH 039/161] Upd: fix logics. --- module/device/device.py | 13 +++-- module/device/platform/platform_windows.py | 22 ++------- module/device/platform/winapi.py | 55 +++++++++++++++------- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index b5bded183b..2189cf9f30 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -105,7 +105,7 @@ def __init__(self, *args, **kwargs): if not self.initialized: self.initialized = True if sys.platform == 'win32': - self.switch_window() + self.switchwindow() def run_simple_screenshot_benchmark(self): """ @@ -139,6 +139,9 @@ def method_check(self): pass def emulator_check(self): + import sys + if sys.platform != 'win32': + return True return super().emulator_check() def handle_night_commission(self, daily_trigger='21:00', threshold=30): @@ -342,13 +345,13 @@ def emulator_start(self): raise if not self.initialized: self.initialized = True - self.switch_window() + self.switchwindow() self.stuck_record_clear() self.click_record_clear() - def switch_window(self): + def switchwindow(self): from module.device.platform import winapi if self.config.Emulator_SilentStart: - return super().switch_window(winapi.SW_MINIMIZE) + return super().switchwindow(winapi.SW_MINIMIZE) else: - return super().switch_window(winapi.SW_SHOW) \ No newline at end of file + return super().switchwindow(winapi.SW_SHOW) \ No newline at end of file diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 3f76f3aec2..c347dfb8db 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -66,26 +66,12 @@ def gethwnds(pid: int) -> list: @staticmethod def getprocess(instance: EmulatorInstance): - return winapi.getprocess(instance) + return winapi.findemulatorprocess(instance) - @staticmethod - def _switch_window(hwnd: int, arg: int): - winapi.ShowWindow(hwnd, arg) - return True - - def switch_window(self, arg: int): + def switchwindow(self, arg: int): if self.process is None: return - for hwnd in self.hwnds: - if not winapi.IsWindow(hwnd): - continue - if winapi.GetParent(hwnd): - continue - rect = winapi.RECT() - winapi.GetWindowRect(hwnd, ctypes.byref(rect)) - if {rect.left, rect.top, rect.right, rect.bottom} == {0}: - continue - self._switch_window(hwnd, arg) + return winapi.switchwindow(self.hwnds, arg) def _emulator_start(self, instance: EmulatorInstance): """ @@ -352,7 +338,7 @@ def emulator_check(self): return True else: return False - except ProcessLookupError as e: + except winapi.ProcessNotFoundError as e: return False except psutil.NoSuchProcess as e: return False diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 31012e688e..0e58dee2f6 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -8,7 +8,6 @@ from module.device.platform.emulator_windows import Emulator, EmulatorInstance from module.logger import logger - user32 = ctypes.windll.user32 kernel32 = ctypes.windll.kernel32 psapi = ctypes.windll.psapi @@ -29,13 +28,11 @@ RECT = ctypes.wintypes.RECT ULONG_PTR = ctypes.wintypes.PULONG - -class EmulatorLaunchFailedError(Exception): - pass - -class HwndNotFoundError(Exception): - pass - +class EmulatorLaunchFailedError(Exception): ... +class HwndNotFoundError(Exception): ... +class ProcessNotFoundError(Exception): ... +class WinApiError(Exception): ... +class EmulatorNotFoundError(Exception): ... PROCESS_ALL_ACCESS = 0x1F0FFF THREAD_ALL_ACCESS = 0x1F03FF @@ -262,7 +259,7 @@ def callback(hwnd: int, lparam): raise HwndNotFoundError("Hwnd not found") return hwnds -def _getprocess(proc: psutil.Process): +def _findemulatorprocess(proc: psutil.Process): try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) if not processhandle: @@ -278,12 +275,13 @@ def _getprocess(proc: psutil.Process): logger.warning(f"Failed to get process and thread handles: {e}") return (None, None, proc.pid, proc.threads()[0].id) -def getprocess(instance: EmulatorInstance): +def findemulatorprocess(instance: EmulatorInstance): lppe = PROCESSENTRY32() lppe.dwSize = ctypes.sizeof(PROCESSENTRY32) hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) Process32First(hSnapshot, ctypes.pointer(lppe)) + process = None while Process32Next(hSnapshot, ctypes.pointer(lppe)): proc = psutil.Process(lppe.th32ProcessID) cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline @@ -292,20 +290,41 @@ def getprocess(instance: EmulatorInstance): if instance == Emulator.MuMuPlayer12: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.MuMuPlayer12_id: - CloseHandle(hSnapshot) - return _getprocess(proc) + process = proc + break elif instance == Emulator.LDPlayerFamily: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.LDPlayer_id: - CloseHandle(hSnapshot) - return _getprocess(proc) + process = proc + break else: matchstr = re.search(fr'\b{instance.name}$', cmdline) if matchstr and matchstr.group() == instance.name: - CloseHandle(hSnapshot) - return _getprocess(proc) + process = proc + break else: + CloseHandle(hSnapshot) errorcode = GetLastError() if errorcode != ERROR_NO_MORE_FILES: - logger.error(f'Error: {GetLastError()}') - raise ProcessLookupError("Process not found") \ No newline at end of file + logger.error(f'Error: {errorcode}') + raise WinApiError("Process not found") + + CloseHandle(hSnapshot) + return _findemulatorprocess(process) + +def _switchwindow(hwnd: int, arg: int): + ShowWindow(hwnd, arg) + return True + +def switchwindow(hwnds: list, arg: int): + for hwnd in hwnds: + if not IsWindow(hwnd): + continue + if GetParent(hwnd): + continue + rect = RECT() + GetWindowRect(hwnd, ctypes.byref(rect)) + if {rect.left, rect.top, rect.right, rect.bottom} == {0}: + continue + _switchwindow(hwnd, arg) + return True \ No newline at end of file From e9bb9d7fa0cb4514c72f0f7c55b6dc37be3d2893 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 15 Jun 2024 20:31:34 +0800 Subject: [PATCH 040/161] Upd: fix logics. --- alas.py | 7 +- module/device/platform/platform_windows.py | 5 +- module/device/platform/winapi.py | 424 +++++++++++++-------- 3 files changed, 271 insertions(+), 165 deletions(-) diff --git a/alas.py b/alas.py index e82aff69f4..cd4af2cde6 100644 --- a/alas.py +++ b/alas.py @@ -455,13 +455,16 @@ def get_next_task(self): release_resources(next_task=task.command) # Reboot emulator - if not self.device.emulator_check() and task.next_run <= datetime.now(): + if (not self.device.emulator_check() and + task.next_run > datetime.now() and + self.config.Optimization_WhenTaskQueueEmpty != 'stop_emulator' + ): logger.warning('Emulator is not running') self.device.emulator_start() if not task == 'Restart': self.run('start') del_cached_property(self, 'config') - break + continue if task.next_run <= datetime.now(): break diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index c347dfb8db..893d293153 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -66,7 +66,7 @@ def gethwnds(pid: int) -> list: @staticmethod def getprocess(instance: EmulatorInstance): - return winapi.findemulatorprocess(instance) + return winapi.getprocess(instance) def switchwindow(self, arg: int): if self.process is None: @@ -282,6 +282,7 @@ def show_package(m): winapi.setfocustowindow(self.focusedwindow) # Check emulator process and hwnds + self.process = self.getprocess(self.emulator_instance) self.hwnds = self.gethwnds(self.process[2]) self.proc = psutil.Process(self.process[2]) @@ -340,6 +341,8 @@ def emulator_check(self): return False except winapi.ProcessNotFoundError as e: return False + except winapi.WinApiError as e: + return False except psutil.NoSuchProcess as e: return False except psutil.AccessDenied as e: diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 0e58dee2f6..3c53f073de 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -8,137 +8,226 @@ from module.device.platform.emulator_windows import Emulator, EmulatorInstance from module.logger import logger -user32 = ctypes.windll.user32 -kernel32 = ctypes.windll.kernel32 -psapi = ctypes.windll.psapi -PyHANDLE = ctypes.wintypes.HANDLE -DWORD = ctypes.wintypes.DWORD -WORD = ctypes.wintypes.WORD -BYTE = ctypes.wintypes.BYTE -BOOL = ctypes.wintypes.BOOL -LONG = ctypes.wintypes.LONG -CHAR = ctypes.wintypes.CHAR -WCHAR = ctypes.wintypes.WCHAR -LPWSTR = ctypes.wintypes.LPWSTR -LPCWSTR = ctypes.wintypes.LPCWSTR -LPVOID = ctypes.wintypes.LPVOID -HWND = ctypes.wintypes.HWND -MAX_PATH = ctypes.wintypes.MAX_PATH -LPARAM = ctypes.wintypes.LPARAM -RECT = ctypes.wintypes.RECT -ULONG_PTR = ctypes.wintypes.PULONG +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +psapi = ctypes.windll.psapi +PyHANDLE = ctypes.wintypes.HANDLE +DWORD = ctypes.wintypes.DWORD +WORD = ctypes.wintypes.WORD +BYTE = ctypes.wintypes.BYTE +BOOL = ctypes.wintypes.BOOL +LONG = ctypes.wintypes.LONG +CHAR = ctypes.wintypes.CHAR +WCHAR = ctypes.wintypes.WCHAR +LPWSTR = ctypes.wintypes.LPWSTR +LPCWSTR = ctypes.wintypes.LPCWSTR +LPVOID = ctypes.wintypes.LPVOID +HWND = ctypes.wintypes.HWND +MAX_PATH = ctypes.wintypes.MAX_PATH +LPARAM = ctypes.wintypes.LPARAM +RECT = ctypes.wintypes.RECT +ULONG_PTR = ctypes.wintypes.PULONG + class EmulatorLaunchFailedError(Exception): ... + + class HwndNotFoundError(Exception): ... + + class ProcessNotFoundError(Exception): ... + + class WinApiError(Exception): ... -class EmulatorNotFoundError(Exception): ... - -PROCESS_ALL_ACCESS = 0x1F0FFF -THREAD_ALL_ACCESS = 0x1F03FF -PROCESS_QUERY_INFORMATION = 0x0400 -PROCESS_VM_READ = 0x0010 -ERROR_NO_MORE_FILES = 0x12 -TH32CS_SNAPPROCESS = DWORD(0x00000002) - -# winbase.h -STARTF_USESHOWWINDOW = 1 -STARTF_USESIZE = 2 -STARTF_USEPOSITION = 4 -STARTF_USECOUNTCHARS = 8 -STARTF_USEFILLATTRIBUTE = 16 -STARTF_FORCEONFEEDBACK = 64 -STARTF_FORCEOFFFEEDBACK = 128 -STARTF_USESTDHANDLES = 256 -STARTF_USEHOTKEY = 512 - -# winuser.h -SW_HIDE = 0 -SW_SHOWNORMAL = 1 -SW_NORMAL = 1 -SW_SHOWMINIMIZED = 2 -SW_SHOWMAXIMIZED = 3 -SW_MAXIMIZE = 3 -SW_SHOWNOACTIVATE = 4 -SW_SHOW = 5 -SW_MINIMIZE = 6 -SW_SHOWMINNOACTIVE = 7 -SW_SHOWNA = 8 -SW_RESTORE = 9 -SW_SHOWDEFAULT = 10 -SW_FORCEMINIMIZE = 11 -SW_MAX = 11 - -# winuser.h -DEBUG_PROCESS = 1 -DEBUG_ONLY_THIS_PROCESS = 2 -CREATE_SUSPENDED = 4 -DETACHED_PROCESS = 8 -CREATE_NEW_CONSOLE = 16 -NORMAL_PRIORITY_CLASS = 32 -IDLE_PRIORITY_CLASS = 64 -HIGH_PRIORITY_CLASS = 128 -REALTIME_PRIORITY_CLASS = 256 -CREATE_NEW_PROCESS_GROUP = 512 -CREATE_UNICODE_ENVIRONMENT = 1024 -CREATE_SEPARATE_WOW_VDM = 2048 -CREATE_SHARED_WOW_VDM = 4096 -CREATE_DEFAULT_ERROR_MODE = 67108864 -CREATE_NO_WINDOW = 134217728 -PROFILE_USER = 268435456 -PROFILE_KERNEL = 536870912 -PROFILE_SERVER = 1073741824 + + +# winnt.h line 2809 +SYNCHRONIZE = 0x00100000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 + +# winnt.h line 3961 +PROCESS_TERMINATE = 0x0001 +PROCESS_CREATE_THREAD = 0x0002 +PROCESS_SET_SESSIONID = 0x0004 +PROCESS_VM_OPERATION = 0x0008 +PROCESS_VM_READ = 0x0010 +PROCESS_VM_WRITE = 0x0020 +PROCESS_DUP_HANDLE = 0x0040 +PROCESS_CREATE_PROCESS = 0x0080 +PROCESS_SET_QUOTA = 0x0100 +PROCESS_SET_INFORMATION = 0x0200 +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_SUSPEND_RESUME = 0x0800 +PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + +THREAD_TERMINATE = 0x0001 +THREAD_SUSPEND_RESUME = 0x0002 +THREAD_GET_CONTEXT = 0x0008 +THREAD_SET_CONTEXT = 0x0010 +THREAD_SET_INFORMATION = 0x0020 +THREAD_QUERY_INFORMATION = 0x0040 +THREAD_SET_THREAD_TOKEN = 0x0080 +THREAD_IMPERSONATE = 0x0100 +THREAD_DIRECT_IMPERSONATION = 0x0200 +THREAD_SET_LIMITED_INFORMATION = 0x0400 +THREAD_QUERY_LIMITED_INFORMATION = 0x0800 + +NTDDI_WIN10 = 0x0A000000 # Windows 10 +NTDDI_WINBLUE = 0x06030000 # Windows 8.1 +NTDDI_WIN8 = 0x06020000 # Windows 8 +NTDDI_WIN7 = 0x06010000 # Windows 7 + +# if NTDDI_VERSION >= 0x06000000: #above win7 +PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff +THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff +# else: #below win7 +# PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xfff +# THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3ff + +# if _WIN64: #64bit +MAXIMUM_PROC_PER_GROUP = 64 +MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP +# else: #32bit +# MAXIMUM_PROC_PER_GROUP = 32 +# MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP + +# error.h line 23 +ERROR_NO_MORE_FILES = 0x12 + +# tlhelp32.h line 17 +TH32CS_SNAPHEAPLIST = 0x00000001 +TH32CS_SNAPPROCESS = 0x00000002 +TH32CS_SNAPTHREAD = 0x00000004 +TH32CS_SNAPMODULE = 0x00000008 +TH32CS_SNAPMODULE32 = 0x00000010 +TH32CS_SNAPALL = TH32CS_SNAPHEAPLIST | TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE +TH32CS_INHERIT = 0x80000000 + +# winbase.h line 1463 +STARTF_USESHOWWINDOW = 0x00000001 +STARTF_USESIZE = 0x00000002 +STARTF_USEPOSITION = 0x00000004 +STARTF_USECOUNTCHARS = 0x00000008 +STARTF_USEFILLATTRIBUTE = 0x00000010 +STARTF_RUNFULLSCREEN = 0x00000020 +STARTF_FORCEONFEEDBACK = 0x00000040 +STARTF_FORCEOFFFEEDBACK = 0x00000080 +STARTF_USESTDHANDLES = 0x00000100 + +STARTF_USEHOTKEY = 0x00000200 +STARTF_TITLEISLINKNAME = 0x00000800 +STARTF_TITLEISAPPID = 0x00001000 +STARTF_PREVENTPINNING = 0x00002000 + +# winuser.h line 200 +SW_HIDE = 0 +SW_SHOWNORMAL = 1 +SW_NORMAL = 1 +SW_SHOWMINIMIZED = 2 +SW_SHOWMAXIMIZED = 3 +SW_MAXIMIZE = 3 +SW_SHOWNOACTIVATE = 4 +SW_SHOW = 5 +SW_MINIMIZE = 6 +SW_SHOWMINNOACTIVE = 7 +SW_SHOWNA = 8 +SW_RESTORE = 9 +SW_SHOWDEFAULT = 10 +SW_FORCEMINIMIZE = 11 +SW_MAX = 11 + +# winbase.h line 377 +DEBUG_PROCESS = 0x00000001 +DEBUG_ONLY_THIS_PROCESS = 0x00000002 +CREATE_SUSPENDED = 0x00000004 +DETACHED_PROCESS = 0x00000008 + +CREATE_NEW_CONSOLE = 0x00000010 +NORMAL_PRIORITY_CLASS = 0x00000020 +IDLE_PRIORITY_CLASS = 0x00000040 +HIGH_PRIORITY_CLASS = 0x00000080 + +REALTIME_PRIORITY_CLASS = 0x00000100 +CREATE_NEW_PROCESS_GROUP = 0x00000200 +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +CREATE_SEPARATE_WOW_VDM = 0x00000800 + +CREATE_SHARED_WOW_VDM = 0x00001000 +CREATE_FORCEDOS = 0x00002000 +BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 +ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 + +INHERIT_PARENT_AFFINITY = 0x00010000 +INHERIT_CALLER_PRIORITY = 0x00020000 +CREATE_PROTECTED_PROCESS = 0x00040000 +EXTENDED_STARTUPINFO_PRESENT = 0x00080000 + +PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000 +PROCESS_MODE_BACKGROUND_END = 0x00200000 + +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 +CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000 +CREATE_DEFAULT_ERROR_MODE = 0x04000000 +CREATE_NO_WINDOW = 0x08000000 + +PROFILE_USER = 0x10000000 +PROFILE_KERNEL = 0x20000000 +PROFILE_SERVER = 0x40000000 +CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000 class STARTUPINFO(ctypes.Structure): _fields_ = [ - ('cb', DWORD), - ('lpReserved', LPWSTR), - ('lpDesktop', LPWSTR), - ('lpTitle', LPWSTR), - ('dwX', DWORD), - ('dwY', DWORD), - ('dwXSize', DWORD), - ('dwYSize', DWORD), - ('dwXCountChars', DWORD), - ('dwYCountChars', DWORD), + ('cb', DWORD), + ('lpReserved', LPWSTR), + ('lpDesktop', LPWSTR), + ('lpTitle', LPWSTR), + ('dwX', DWORD), + ('dwY', DWORD), + ('dwXSize', DWORD), + ('dwYSize', DWORD), + ('dwXCountChars', DWORD), + ('dwYCountChars', DWORD), ('dwFillAttribute', DWORD), - ('dwFlags', DWORD), - ('wShowWindow', WORD), - ('cbReserved2', WORD), - ('lpReserved2', ctypes.POINTER(BYTE)), - ('hStdInput', PyHANDLE), - ('hStdOutput', PyHANDLE), - ('hStdError', PyHANDLE), + ('dwFlags', DWORD), + ('wShowWindow', WORD), + ('cbReserved2', WORD), + ('lpReserved2', ctypes.POINTER(BYTE)), + ('hStdInput', PyHANDLE), + ('hStdOutput', PyHANDLE), + ('hStdError', PyHANDLE), ] + class PROCESS_INFORMATION(ctypes.Structure): _fields_ = [ - ('hProcess', PyHANDLE), - ('hThread', PyHANDLE), + ('hProcess', PyHANDLE), + ('hThread', PyHANDLE), ('dwProcessId', DWORD), - ('dwThreadId', DWORD), + ('dwThreadId', DWORD), ] + class SECURITY_ATTRIBUTES(ctypes.Structure): _fields_ = [ - ("nLength", DWORD), - ("lpSecurityDescriptor", ctypes.c_void_p), - ("bInheritHandle", BOOL) + ("nLength", DWORD), + ("lpSecurityDescriptor", ctypes.c_void_p), + ("bInheritHandle", BOOL) ] + class PROCESSENTRY32(ctypes.Structure): _fields_ = [ - ("dwSize", DWORD), - ("cntUsage", DWORD), - ("th32ProcessID", DWORD), - ("th32DefaultHeapID", ULONG_PTR), - ("th32ModuleID", DWORD), - ("cntThreads", DWORD), + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ProcessID", DWORD), + ("th32DefaultHeapID", ULONG_PTR), + ("th32ModuleID", DWORD), + ("cntThreads", DWORD), ("th32ParentProcessID", DWORD), - ("pcPriClassBase", LONG), - ("dwFlags", DWORD), - ("szExeFile", CHAR * MAX_PATH), + ("pcPriClassBase", LONG), + ("dwFlags", DWORD), + ("szExeFile", CHAR * MAX_PATH), ] @@ -157,58 +246,61 @@ class PROCESSENTRY32(ctypes.Structure): ] CreateProcessW.restype = BOOL -GetForegroundWindow = user32.GetForegroundWindow -SetForegroundWindow = user32.SetForegroundWindow -SetForegroundWindow.argtypes = [HWND] -SetForegroundWindow.restype = BOOL +GetForegroundWindow = user32.GetForegroundWindow +SetForegroundWindow = user32.SetForegroundWindow +SetForegroundWindow.argtypes = [HWND] +SetForegroundWindow.restype = BOOL -ShowWindow = user32.ShowWindow -IsWindow = user32.IsWindow -GetParent = user32.GetParent -GetWindowRect = user32.GetWindowRect +ShowWindow = user32.ShowWindow +IsWindow = user32.IsWindow +GetParent = user32.GetParent +GetWindowRect = user32.GetWindowRect -EnumWindows = user32.EnumWindows -EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) -GetWindowThreadProcessId = user32.GetWindowThreadProcessId -GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(DWORD)] -GetWindowThreadProcessId.restype = DWORD +EnumWindows = user32.EnumWindows +EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) +GetWindowThreadProcessId = user32.GetWindowThreadProcessId +GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(DWORD)] +GetWindowThreadProcessId.restype = DWORD -OpenProcess = kernel32.OpenProcess -OpenProcess.argtypes = [DWORD, BOOL, DWORD] -OpenProcess.restype = PyHANDLE -OpenThread = kernel32.OpenThread +OpenProcess = kernel32.OpenProcess +OpenProcess.argtypes = [DWORD, BOOL, DWORD] +OpenProcess.restype = PyHANDLE +OpenThread = kernel32.OpenThread -CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot -CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] -CreateToolhelp32Snapshot.restype = PyHANDLE +CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot +CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] +CreateToolhelp32Snapshot.restype = PyHANDLE -CloseHandle = kernel32.CloseHandle -CloseHandle.argtypes = [PyHANDLE] -CloseHandle.restype = BOOL +CloseHandle = kernel32.CloseHandle +CloseHandle.argtypes = [PyHANDLE] +CloseHandle.restype = BOOL -Process32First = kernel32.Process32First -Process32First.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] -Process32First.restype = BOOL +Process32First = kernel32.Process32First +Process32First.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32First.restype = BOOL -Process32Next = kernel32.Process32Next -Process32Next.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] -Process32Next.restype = BOOL +Process32Next = kernel32.Process32Next +Process32Next.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32Next.restype = BOOL + +GetLastError = kernel32.GetLastError +GetLastError.restype = BOOL -GetLastError = kernel32.GetLastError -GetLastError.restype = BOOL def getfocusedwindow() -> int: return GetForegroundWindow() + def setfocustowindow(hwnd: int) -> bool: return SetForegroundWindow(hwnd) + def execute(command: str, arg: bool): - startupinfo = STARTUPINFO() - startupinfo.cb = ctypes.sizeof(STARTUPINFO) - startupinfo.dwFlags = STARTF_USESHOWWINDOW + startupinfo = STARTUPINFO() + startupinfo.cb = ctypes.sizeof(STARTUPINFO) + startupinfo.dwFlags = STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE - + focusedwindow = getfocusedwindow() processinformation = PROCESS_INFORMATION() @@ -229,7 +321,7 @@ def execute(command: str, arg: bool): if not success: errorcode = GetLastError() raise EmulatorLaunchFailedError(f"Failed to start emulator. Error code: {errorcode}") - + process = ( processinformation.hProcess, processinformation.hThread, @@ -238,6 +330,7 @@ def execute(command: str, arg: bool): ) return process, focusedwindow + def gethwnds(pid: int) -> list: hwnds = [] @@ -248,7 +341,7 @@ def callback(hwnd: int, lparam): if processid.value == pid: hwnds.append(hwnd) return True - + EnumWindows(callback, 0) if not hwnds: logger.critical( @@ -259,67 +352,74 @@ def callback(hwnd: int, lparam): raise HwndNotFoundError("Hwnd not found") return hwnds -def _findemulatorprocess(proc: psutil.Process): + +def _getprocess(proc: psutil.Process): try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) if not processhandle: - raise ctypes.WinError(ctypes.get_last_error()) + raise ctypes.WinError(GetLastError()) mainthreadid = proc.threads()[0].id threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) if not threadhandle: - raise ctypes.WinError(ctypes.get_last_error()) + raise ctypes.WinError(GetLastError()) return (PyHANDLE(processhandle), PyHANDLE(threadhandle), proc.pid, mainthreadid) except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") return (None, None, proc.pid, proc.threads()[0].id) -def findemulatorprocess(instance: EmulatorInstance): + +def getprocess(instance: EmulatorInstance): lppe = PROCESSENTRY32() lppe.dwSize = ctypes.sizeof(PROCESSENTRY32) hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) Process32First(hSnapshot, ctypes.pointer(lppe)) - process = None while Process32Next(hSnapshot, ctypes.pointer(lppe)): - proc = psutil.Process(lppe.th32ProcessID) - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + try: + proc = psutil.Process(lppe.th32ProcessID) + cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + except psutil.NoSuchProcess: + continue if not instance.path in cmdline: continue if instance == Emulator.MuMuPlayer12: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.MuMuPlayer12_id: - process = proc break elif instance == Emulator.LDPlayerFamily: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.LDPlayer_id: - process = proc break else: matchstr = re.search(fr'\b{instance.name}$', cmdline) if matchstr and matchstr.group() == instance.name: - process = proc break else: - CloseHandle(hSnapshot) + # finished querying errorcode = GetLastError() + CloseHandle(hSnapshot) + if errorcode != ERROR_NO_MORE_FILES: - logger.error(f'Error: {errorcode}') - raise WinApiError("Process not found") - + # error code != ERROR_NO_MORE_FILES, means that win api failed + raise WinApiError(f"Win api failed with error code: {errorcode}") + # process not found + raise ProcessNotFoundError("Process not found") + CloseHandle(hSnapshot) - return _findemulatorprocess(process) - + return _getprocess(proc) + + def _switchwindow(hwnd: int, arg: int): ShowWindow(hwnd, arg) return True + def switchwindow(hwnds: list, arg: int): for hwnd in hwnds: if not IsWindow(hwnd): - continue + continue if GetParent(hwnd): continue rect = RECT() @@ -327,4 +427,4 @@ def switchwindow(hwnds: list, arg: int): if {rect.left, rect.top, rect.right, rect.bottom} == {0}: continue _switchwindow(hwnd, arg) - return True \ No newline at end of file + return True From cb2ca47e6a9d430430f1453155ac5fc856592b03 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 15 Jun 2024 20:38:25 +0800 Subject: [PATCH 041/161] Upd: fix texts --- module/device/platform/winapi.py | 392 +++++++++++++++---------------- 1 file changed, 189 insertions(+), 203 deletions(-) diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 3c53f073de..67bac0d6ea 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -8,85 +8,77 @@ from module.device.platform.emulator_windows import Emulator, EmulatorInstance from module.logger import logger -user32 = ctypes.windll.user32 -kernel32 = ctypes.windll.kernel32 -psapi = ctypes.windll.psapi -PyHANDLE = ctypes.wintypes.HANDLE -DWORD = ctypes.wintypes.DWORD -WORD = ctypes.wintypes.WORD -BYTE = ctypes.wintypes.BYTE -BOOL = ctypes.wintypes.BOOL -LONG = ctypes.wintypes.LONG -CHAR = ctypes.wintypes.CHAR -WCHAR = ctypes.wintypes.WCHAR -LPWSTR = ctypes.wintypes.LPWSTR -LPCWSTR = ctypes.wintypes.LPCWSTR -LPVOID = ctypes.wintypes.LPVOID -HWND = ctypes.wintypes.HWND -MAX_PATH = ctypes.wintypes.MAX_PATH -LPARAM = ctypes.wintypes.LPARAM -RECT = ctypes.wintypes.RECT -ULONG_PTR = ctypes.wintypes.PULONG - +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +psapi = ctypes.windll.psapi +PyHANDLE = ctypes.wintypes.HANDLE +DWORD = ctypes.wintypes.DWORD +WORD = ctypes.wintypes.WORD +BYTE = ctypes.wintypes.BYTE +BOOL = ctypes.wintypes.BOOL +LONG = ctypes.wintypes.LONG +CHAR = ctypes.wintypes.CHAR +WCHAR = ctypes.wintypes.WCHAR +LPWSTR = ctypes.wintypes.LPWSTR +LPCWSTR = ctypes.wintypes.LPCWSTR +LPVOID = ctypes.wintypes.LPVOID +HWND = ctypes.wintypes.HWND +MAX_PATH = ctypes.wintypes.MAX_PATH +LPARAM = ctypes.wintypes.LPARAM +RECT = ctypes.wintypes.RECT +ULONG_PTR = ctypes.wintypes.PULONG class EmulatorLaunchFailedError(Exception): ... - - class HwndNotFoundError(Exception): ... - - class ProcessNotFoundError(Exception): ... - - class WinApiError(Exception): ... - # winnt.h line 2809 -SYNCHRONIZE = 0x00100000 -STANDARD_RIGHTS_REQUIRED = 0x000F0000 +SYNCHRONIZE = 0x00100000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 # winnt.h line 3961 -PROCESS_TERMINATE = 0x0001 -PROCESS_CREATE_THREAD = 0x0002 -PROCESS_SET_SESSIONID = 0x0004 -PROCESS_VM_OPERATION = 0x0008 -PROCESS_VM_READ = 0x0010 -PROCESS_VM_WRITE = 0x0020 -PROCESS_DUP_HANDLE = 0x0040 -PROCESS_CREATE_PROCESS = 0x0080 -PROCESS_SET_QUOTA = 0x0100 -PROCESS_SET_INFORMATION = 0x0200 -PROCESS_QUERY_INFORMATION = 0x0400 -PROCESS_SUSPEND_RESUME = 0x0800 -PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 - -THREAD_TERMINATE = 0x0001 -THREAD_SUSPEND_RESUME = 0x0002 -THREAD_GET_CONTEXT = 0x0008 -THREAD_SET_CONTEXT = 0x0010 -THREAD_SET_INFORMATION = 0x0020 -THREAD_QUERY_INFORMATION = 0x0040 -THREAD_SET_THREAD_TOKEN = 0x0080 -THREAD_IMPERSONATE = 0x0100 -THREAD_DIRECT_IMPERSONATION = 0x0200 -THREAD_SET_LIMITED_INFORMATION = 0x0400 -THREAD_QUERY_LIMITED_INFORMATION = 0x0800 - -NTDDI_WIN10 = 0x0A000000 # Windows 10 -NTDDI_WINBLUE = 0x06030000 # Windows 8.1 -NTDDI_WIN8 = 0x06020000 # Windows 8 -NTDDI_WIN7 = 0x06010000 # Windows 7 +PROCESS_TERMINATE = 0x0001 +PROCESS_CREATE_THREAD = 0x0002 +PROCESS_SET_SESSIONID = 0x0004 +PROCESS_VM_OPERATION = 0x0008 +PROCESS_VM_READ = 0x0010 +PROCESS_VM_WRITE = 0x0020 +PROCESS_DUP_HANDLE = 0x0040 +PROCESS_CREATE_PROCESS = 0x0080 +PROCESS_SET_QUOTA = 0x0100 +PROCESS_SET_INFORMATION = 0x0200 +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_SUSPEND_RESUME = 0x0800 +PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + +THREAD_TERMINATE = 0x0001 +THREAD_SUSPEND_RESUME = 0x0002 +THREAD_GET_CONTEXT = 0x0008 +THREAD_SET_CONTEXT = 0x0010 +THREAD_SET_INFORMATION = 0x0020 +THREAD_QUERY_INFORMATION = 0x0040 +THREAD_SET_THREAD_TOKEN = 0x0080 +THREAD_IMPERSONATE = 0x0100 +THREAD_DIRECT_IMPERSONATION = 0x0200 +THREAD_SET_LIMITED_INFORMATION = 0x0400 +THREAD_QUERY_LIMITED_INFORMATION = 0x0800 + +NTDDI_WIN10 = 0x0A000000 # Windows 10 +NTDDI_WINBLUE = 0x06030000 # Windows 8.1 +NTDDI_WIN8 = 0x06020000 # Windows 8 +NTDDI_WIN7 = 0x06010000 # Windows 7 # if NTDDI_VERSION >= 0x06000000: #above win7 -PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff -THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff +PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff +THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff # else: #below win7 # PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xfff # THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3ff # if _WIN64: #64bit -MAXIMUM_PROC_PER_GROUP = 64 -MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP +MAXIMUM_PROC_PER_GROUP = 64 +MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP # else: #32bit # MAXIMUM_PROC_PER_GROUP = 32 # MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP @@ -96,138 +88,135 @@ class WinApiError(Exception): ... # tlhelp32.h line 17 TH32CS_SNAPHEAPLIST = 0x00000001 -TH32CS_SNAPPROCESS = 0x00000002 -TH32CS_SNAPTHREAD = 0x00000004 -TH32CS_SNAPMODULE = 0x00000008 +TH32CS_SNAPPROCESS = 0x00000002 +TH32CS_SNAPTHREAD = 0x00000004 +TH32CS_SNAPMODULE = 0x00000008 TH32CS_SNAPMODULE32 = 0x00000010 -TH32CS_SNAPALL = TH32CS_SNAPHEAPLIST | TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE -TH32CS_INHERIT = 0x80000000 +TH32CS_SNAPALL = TH32CS_SNAPHEAPLIST | TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE +TH32CS_INHERIT = 0x80000000 # winbase.h line 1463 -STARTF_USESHOWWINDOW = 0x00000001 -STARTF_USESIZE = 0x00000002 -STARTF_USEPOSITION = 0x00000004 -STARTF_USECOUNTCHARS = 0x00000008 +STARTF_USESHOWWINDOW = 0x00000001 +STARTF_USESIZE = 0x00000002 +STARTF_USEPOSITION = 0x00000004 +STARTF_USECOUNTCHARS = 0x00000008 STARTF_USEFILLATTRIBUTE = 0x00000010 -STARTF_RUNFULLSCREEN = 0x00000020 -STARTF_FORCEONFEEDBACK = 0x00000040 +STARTF_RUNFULLSCREEN = 0x00000020 +STARTF_FORCEONFEEDBACK = 0x00000040 STARTF_FORCEOFFFEEDBACK = 0x00000080 -STARTF_USESTDHANDLES = 0x00000100 +STARTF_USESTDHANDLES = 0x00000100 -STARTF_USEHOTKEY = 0x00000200 -STARTF_TITLEISLINKNAME = 0x00000800 -STARTF_TITLEISAPPID = 0x00001000 -STARTF_PREVENTPINNING = 0x00002000 +STARTF_USEHOTKEY = 0x00000200 +STARTF_TITLEISLINKNAME = 0x00000800 +STARTF_TITLEISAPPID = 0x00001000 +STARTF_PREVENTPINNING = 0x00002000 # winuser.h line 200 -SW_HIDE = 0 -SW_SHOWNORMAL = 1 -SW_NORMAL = 1 -SW_SHOWMINIMIZED = 2 -SW_SHOWMAXIMIZED = 3 -SW_MAXIMIZE = 3 -SW_SHOWNOACTIVATE = 4 -SW_SHOW = 5 -SW_MINIMIZE = 6 -SW_SHOWMINNOACTIVE = 7 -SW_SHOWNA = 8 -SW_RESTORE = 9 -SW_SHOWDEFAULT = 10 -SW_FORCEMINIMIZE = 11 -SW_MAX = 11 +SW_HIDE = 0 +SW_SHOWNORMAL = 1 +SW_NORMAL = 1 +SW_SHOWMINIMIZED = 2 +SW_SHOWMAXIMIZED = 3 +SW_MAXIMIZE = 3 +SW_SHOWNOACTIVATE = 4 +SW_SHOW = 5 +SW_MINIMIZE = 6 +SW_SHOWMINNOACTIVE = 7 +SW_SHOWNA = 8 +SW_RESTORE = 9 +SW_SHOWDEFAULT = 10 +SW_FORCEMINIMIZE = 11 +SW_MAX = 11 # winbase.h line 377 -DEBUG_PROCESS = 0x00000001 -DEBUG_ONLY_THIS_PROCESS = 0x00000002 -CREATE_SUSPENDED = 0x00000004 -DETACHED_PROCESS = 0x00000008 +DEBUG_PROCESS = 0x00000001 +DEBUG_ONLY_THIS_PROCESS = 0x00000002 +CREATE_SUSPENDED = 0x00000004 +DETACHED_PROCESS = 0x00000008 -CREATE_NEW_CONSOLE = 0x00000010 -NORMAL_PRIORITY_CLASS = 0x00000020 -IDLE_PRIORITY_CLASS = 0x00000040 -HIGH_PRIORITY_CLASS = 0x00000080 +CREATE_NEW_CONSOLE = 0x00000010 +NORMAL_PRIORITY_CLASS = 0x00000020 +IDLE_PRIORITY_CLASS = 0x00000040 +HIGH_PRIORITY_CLASS = 0x00000080 -REALTIME_PRIORITY_CLASS = 0x00000100 -CREATE_NEW_PROCESS_GROUP = 0x00000200 -CREATE_UNICODE_ENVIRONMENT = 0x00000400 -CREATE_SEPARATE_WOW_VDM = 0x00000800 +REALTIME_PRIORITY_CLASS = 0x00000100 +CREATE_NEW_PROCESS_GROUP = 0x00000200 +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +CREATE_SEPARATE_WOW_VDM = 0x00000800 -CREATE_SHARED_WOW_VDM = 0x00001000 -CREATE_FORCEDOS = 0x00002000 -BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 -ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 +CREATE_SHARED_WOW_VDM = 0x00001000 +CREATE_FORCEDOS = 0x00002000 +BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 +ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 -INHERIT_PARENT_AFFINITY = 0x00010000 -INHERIT_CALLER_PRIORITY = 0x00020000 -CREATE_PROTECTED_PROCESS = 0x00040000 -EXTENDED_STARTUPINFO_PRESENT = 0x00080000 +INHERIT_PARENT_AFFINITY = 0x00010000 +INHERIT_CALLER_PRIORITY = 0x00020000 +CREATE_PROTECTED_PROCESS = 0x00040000 +EXTENDED_STARTUPINFO_PRESENT = 0x00080000 -PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000 -PROCESS_MODE_BACKGROUND_END = 0x00200000 +PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000 +PROCESS_MODE_BACKGROUND_END = 0x00200000 -CREATE_BREAKAWAY_FROM_JOB = 0x01000000 -CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000 -CREATE_DEFAULT_ERROR_MODE = 0x04000000 -CREATE_NO_WINDOW = 0x08000000 +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 +CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000 +CREATE_DEFAULT_ERROR_MODE = 0x04000000 +CREATE_NO_WINDOW = 0x08000000 -PROFILE_USER = 0x10000000 -PROFILE_KERNEL = 0x20000000 -PROFILE_SERVER = 0x40000000 -CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000 +PROFILE_USER = 0x10000000 +PROFILE_KERNEL = 0x20000000 +PROFILE_SERVER = 0x40000000 +CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000 class STARTUPINFO(ctypes.Structure): _fields_ = [ - ('cb', DWORD), - ('lpReserved', LPWSTR), - ('lpDesktop', LPWSTR), - ('lpTitle', LPWSTR), - ('dwX', DWORD), - ('dwY', DWORD), - ('dwXSize', DWORD), - ('dwYSize', DWORD), - ('dwXCountChars', DWORD), - ('dwYCountChars', DWORD), + ('cb', DWORD), + ('lpReserved', LPWSTR), + ('lpDesktop', LPWSTR), + ('lpTitle', LPWSTR), + ('dwX', DWORD), + ('dwY', DWORD), + ('dwXSize', DWORD), + ('dwYSize', DWORD), + ('dwXCountChars', DWORD), + ('dwYCountChars', DWORD), ('dwFillAttribute', DWORD), - ('dwFlags', DWORD), - ('wShowWindow', WORD), - ('cbReserved2', WORD), - ('lpReserved2', ctypes.POINTER(BYTE)), - ('hStdInput', PyHANDLE), - ('hStdOutput', PyHANDLE), - ('hStdError', PyHANDLE), + ('dwFlags', DWORD), + ('wShowWindow', WORD), + ('cbReserved2', WORD), + ('lpReserved2', ctypes.POINTER(BYTE)), + ('hStdInput', PyHANDLE), + ('hStdOutput', PyHANDLE), + ('hStdError', PyHANDLE), ] - class PROCESS_INFORMATION(ctypes.Structure): _fields_ = [ - ('hProcess', PyHANDLE), - ('hThread', PyHANDLE), + ('hProcess', PyHANDLE), + ('hThread', PyHANDLE), ('dwProcessId', DWORD), - ('dwThreadId', DWORD), + ('dwThreadId', DWORD), ] - class SECURITY_ATTRIBUTES(ctypes.Structure): _fields_ = [ - ("nLength", DWORD), - ("lpSecurityDescriptor", ctypes.c_void_p), - ("bInheritHandle", BOOL) + ("nLength", DWORD), + ("lpSecurityDescriptor", ctypes.c_void_p), + ("bInheritHandle", BOOL) ] - class PROCESSENTRY32(ctypes.Structure): _fields_ = [ - ("dwSize", DWORD), - ("cntUsage", DWORD), - ("th32ProcessID", DWORD), - ("th32DefaultHeapID", ULONG_PTR), - ("th32ModuleID", DWORD), - ("cntThreads", DWORD), + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ProcessID", DWORD), + ("th32DefaultHeapID", ULONG_PTR), + ("th32ModuleID", DWORD), + ("cntThreads", DWORD), ("th32ParentProcessID", DWORD), - ("pcPriClassBase", LONG), - ("dwFlags", DWORD), - ("szExeFile", CHAR * MAX_PATH), + ("pcPriClassBase", LONG), + ("dwFlags", DWORD), + ("szExeFile", CHAR * MAX_PATH), ] @@ -246,61 +235,60 @@ class PROCESSENTRY32(ctypes.Structure): ] CreateProcessW.restype = BOOL -GetForegroundWindow = user32.GetForegroundWindow -SetForegroundWindow = user32.SetForegroundWindow -SetForegroundWindow.argtypes = [HWND] -SetForegroundWindow.restype = BOOL +GetForegroundWindow = user32.GetForegroundWindow +SetForegroundWindow = user32.SetForegroundWindow +SetForegroundWindow.argtypes = [HWND] +SetForegroundWindow.restype = BOOL -ShowWindow = user32.ShowWindow -IsWindow = user32.IsWindow -GetParent = user32.GetParent -GetWindowRect = user32.GetWindowRect +ShowWindow = user32.ShowWindow +IsWindow = user32.IsWindow +GetParent = user32.GetParent +GetWindowRect = user32.GetWindowRect -EnumWindows = user32.EnumWindows -EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) -GetWindowThreadProcessId = user32.GetWindowThreadProcessId -GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(DWORD)] -GetWindowThreadProcessId.restype = DWORD +EnumWindows = user32.EnumWindows +EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) +GetWindowThreadProcessId = user32.GetWindowThreadProcessId +GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(DWORD)] +GetWindowThreadProcessId.restype = DWORD -OpenProcess = kernel32.OpenProcess -OpenProcess.argtypes = [DWORD, BOOL, DWORD] -OpenProcess.restype = PyHANDLE -OpenThread = kernel32.OpenThread +OpenProcess = kernel32.OpenProcess +OpenProcess.argtypes = [DWORD, BOOL, DWORD] +OpenProcess.restype = PyHANDLE +OpenThread = kernel32.OpenThread -CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot -CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] -CreateToolhelp32Snapshot.restype = PyHANDLE +CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot +CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] +CreateToolhelp32Snapshot.restype = PyHANDLE -CloseHandle = kernel32.CloseHandle -CloseHandle.argtypes = [PyHANDLE] -CloseHandle.restype = BOOL +CloseHandle = kernel32.CloseHandle +CloseHandle.argtypes = [PyHANDLE] +CloseHandle.restype = BOOL -Process32First = kernel32.Process32First -Process32First.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] -Process32First.restype = BOOL +Process32First = kernel32.Process32First +Process32First.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32First.restype = BOOL -Process32Next = kernel32.Process32Next -Process32Next.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] -Process32Next.restype = BOOL +Process32Next = kernel32.Process32Next +Process32Next.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32Next.restype = BOOL -GetLastError = kernel32.GetLastError -GetLastError.restype = BOOL +GetLastError = kernel32.GetLastError +GetLastError.restype = BOOL def getfocusedwindow() -> int: return GetForegroundWindow() - def setfocustowindow(hwnd: int) -> bool: return SetForegroundWindow(hwnd) def execute(command: str, arg: bool): - startupinfo = STARTUPINFO() - startupinfo.cb = ctypes.sizeof(STARTUPINFO) - startupinfo.dwFlags = STARTF_USESHOWWINDOW + startupinfo = STARTUPINFO() + startupinfo.cb = ctypes.sizeof(STARTUPINFO) + startupinfo.dwFlags = STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE - + focusedwindow = getfocusedwindow() processinformation = PROCESS_INFORMATION() @@ -321,7 +309,7 @@ def execute(command: str, arg: bool): if not success: errorcode = GetLastError() raise EmulatorLaunchFailedError(f"Failed to start emulator. Error code: {errorcode}") - + process = ( processinformation.hProcess, processinformation.hThread, @@ -341,7 +329,7 @@ def callback(hwnd: int, lparam): if processid.value == pid: hwnds.append(hwnd) return True - + EnumWindows(callback, 0) if not hwnds: logger.critical( @@ -369,7 +357,6 @@ def _getprocess(proc: psutil.Process): logger.warning(f"Failed to get process and thread handles: {e}") return (None, None, proc.pid, proc.threads()[0].id) - def getprocess(instance: EmulatorInstance): lppe = PROCESSENTRY32() lppe.dwSize = ctypes.sizeof(PROCESSENTRY32) @@ -406,20 +393,19 @@ def getprocess(instance: EmulatorInstance): raise WinApiError(f"Win api failed with error code: {errorcode}") # process not found raise ProcessNotFoundError("Process not found") - + CloseHandle(hSnapshot) return _getprocess(proc) - + def _switchwindow(hwnd: int, arg: int): ShowWindow(hwnd, arg) return True - def switchwindow(hwnds: list, arg: int): for hwnd in hwnds: if not IsWindow(hwnd): - continue + continue if GetParent(hwnd): continue rect = RECT() @@ -427,4 +413,4 @@ def switchwindow(hwnds: list, arg: int): if {rect.left, rect.top, rect.right, rect.bottom} == {0}: continue _switchwindow(hwnd, arg) - return True + return True \ No newline at end of file From 18422c4afb8f26038d2e897b79aac7252673b70c Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 15 Jun 2024 22:11:40 +0800 Subject: [PATCH 042/161] Upd: Add ProcessBufferTime --- alas.py | 11 +++++++++-- config/template.json | 3 ++- module/config/argument/args.json | 4 ++++ module/config/argument/argument.yaml | 1 + module/config/i18n/en-US.json | 4 ++++ module/config/i18n/ja-JP.json | 4 ++++ module/config/i18n/zh-CN.json | 4 ++++ module/config/i18n/zh-TW.json | 4 ++++ module/device/platform/platform_windows.py | 1 - 9 files changed, 32 insertions(+), 4 deletions(-) diff --git a/alas.py b/alas.py index cd4af2cde6..b6c26b1890 100644 --- a/alas.py +++ b/alas.py @@ -455,7 +455,8 @@ def get_next_task(self): release_resources(next_task=task.command) # Reboot emulator - if (not self.device.emulator_check() and + if ( + not self.device.emulator_check() and task.next_run > datetime.now() and self.config.Optimization_WhenTaskQueueEmpty != 'stop_emulator' ): @@ -468,10 +469,16 @@ def get_next_task(self): if task.next_run <= datetime.now(): break - + logger.info(f'Wait until {task.next_run} for task `{task.command}`') self.is_first_task = False method = self.config.Optimization_WhenTaskQueueEmpty + if ( + method == 'stop_emulator' and + self.device.emulator_check() and + ((task.next_run - datetime.now()).total_seconds() / 60 <= self.config.Optimization_ProcessBufferTime) + ): + method = 'stay_there' if method == 'close_game': logger.info('Close game during wait') self.device.app_stop() diff --git a/config/template.json b/config/template.json index 8d6087d82e..a6e251ec54 100644 --- a/config/template.json +++ b/config/template.json @@ -25,7 +25,8 @@ "ScreenshotInterval": 0.3, "CombatScreenshotInterval": 1.0, "TaskHoardingDuration": 0, - "WhenTaskQueueEmpty": "goto_main" + "WhenTaskQueueEmpty": "goto_main", + "ProcessBufferTime": 10 }, "DropRecord": { "SaveFolder": "./screenshots", diff --git a/module/config/argument/args.json b/module/config/argument/args.json index d9e208d545..4f781597ea 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -215,6 +215,10 @@ "close_game", "stop_emulator" ] + }, + "ProcessBufferTime": { + "type": "input", + "value": 10 } }, "DropRecord": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 9219ab140f..2ab6f806bd 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -96,6 +96,7 @@ Optimization: WhenTaskQueueEmpty: value: goto_main option: [ stay_there, goto_main, close_game, stop_emulator ] + ProcessBufferTime: 10 DropRecord: SaveFolder: ./screenshots AzurStatsID: null diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index e2fc315877..9ff90a90e0 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -512,6 +512,10 @@ "goto_main": "Goto Main Page", "close_game": "Close Game", "stop_emulator": "Stop Emulator" + }, + "ProcessBufferTime": { + "name": "Emulator turns off buffering for X minutes", + "help": "When the current time is less than X minutes from the next task, ALAS will switch from 'stop_emulator' to 'stay_there',\npreventing frequent start-stop cycles of the emulator.\nThis setting takes effect when the option is 'stop_emulator'." } }, "DropRecord": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 56ab755402..d919de7acb 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -512,6 +512,10 @@ "goto_main": "goto_main", "close_game": "close_game", "stop_emulator": "stop_emulator" + }, + "ProcessBufferTime": { + "name": "Optimization.ProcessBufferTime.name", + "help": "Optimization.ProcessBufferTime.help" } }, "DropRecord": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 2ffd46b853..a49188ed69 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -512,6 +512,10 @@ "goto_main": "前往主界面", "close_game": "关闭游戏", "stop_emulator": "关闭模拟器" + }, + "ProcessBufferTime": { + "name": "模拟器关闭缓冲 X 分钟", + "help": "当前时间距离下个任务小于 X 分钟时,alas将由关闭模拟器转为停在原处,能避免频繁启停模拟器\n当任务队列清空后关闭模拟器,此设置生效" } }, "DropRecord": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 650e813f32..7737b6f7a9 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -512,6 +512,10 @@ "goto_main": "前往主界面", "close_game": "關閉遊戲", "stop_emulator": "關閉模擬器" + }, + "ProcessBufferTime": { + "name": "模擬器關閉緩衝 X 分鐘", + "help": "噹前時間距離下箇任務小於 X 分鐘時,alas將由關閉模擬器轉爲停在原處,能避免頻緐啓停模擬器\n噹任務隊列清空後關閉模擬器,此設置生傚" } }, "DropRecord": { diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 893d293153..4cd6458a3c 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,4 +1,3 @@ -import ctypes import re import psutil From 642b8e5520feccb540e2116a3bb014f02c12b2a8 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 16 Jun 2024 05:10:31 +0800 Subject: [PATCH 043/161] Upd: fix logics. --- module/config/i18n/en-US.json | 2 +- module/device/device.py | 3 +++ module/device/platform/platform_windows.py | 13 +----------- module/device/platform/winapi.py | 23 ++++++++++++++++++---- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 9ff90a90e0..49f216f9a9 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -515,7 +515,7 @@ }, "ProcessBufferTime": { "name": "Emulator turns off buffering for X minutes", - "help": "When the current time is less than X minutes from the next task, ALAS will switch from 'stop_emulator' to 'stay_there',\npreventing frequent start-stop cycles of the emulator.\nThis setting takes effect when the option is 'stop_emulator'." + "help": "When the current time is less than X minutes from the next task, ALAS will switch from 'stop_emulator' to 'stay_there', preventing frequent start-stop cycles of the emulator.\nThis setting takes effect when the option is 'stop_emulator'." } }, "DropRecord": { diff --git a/module/device/device.py b/module/device/device.py index 2189cf9f30..a35caac0bc 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -351,6 +351,9 @@ def emulator_start(self): def switchwindow(self): from module.device.platform import winapi + import sys + if sys.platform != 'win32': + return True if self.config.Emulator_SilentStart: return super().switchwindow(winapi.SW_MINIMIZE) else: diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 4cd6458a3c..30661337f1 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,5 +1,3 @@ -import re - import psutil from deploy.Windows.utils import DataProcessInfo @@ -48,16 +46,7 @@ def kill_process_by_regex(cls, regex: str) -> int: Returns: int: Number of processes killed """ - count = 0 - - for proc in psutil.process_iter(): - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - if re.search(regex, cmdline): - logger.info(f'Kill emulator: {cmdline}') - proc.kill() - count += 1 - - return count + return winapi.kill_process_by_regex(regex) @staticmethod def gethwnds(pid: int) -> list: diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 67bac0d6ea..3d90534760 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -236,9 +236,9 @@ class PROCESSENTRY32(ctypes.Structure): CreateProcessW.restype = BOOL GetForegroundWindow = user32.GetForegroundWindow -SetForegroundWindow = user32.SetForegroundWindow -SetForegroundWindow.argtypes = [HWND] -SetForegroundWindow.restype = BOOL +SwitchToThisWindow = user32.SwitchToThisWindow +SwitchToThisWindow.argtypes = [HWND] +SwitchToThisWindow.restype = BOOL ShowWindow = user32.ShowWindow IsWindow = user32.IsWindow @@ -280,7 +280,8 @@ def getfocusedwindow() -> int: return GetForegroundWindow() def setfocustowindow(hwnd: int) -> bool: - return SetForegroundWindow(hwnd) + SwitchToThisWindow(hwnd, True) + return True def execute(command: str, arg: bool): @@ -319,6 +320,20 @@ def execute(command: str, arg: bool): return process, focusedwindow +def kill_process_by_regex(regex: str) -> int: + count = 0 + + for proc in psutil.process_iter(): + cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + if not re.search(regex, cmdline): + continue + logger.info(f'Kill emulator: {cmdline}') + proc.kill() + count += 1 + + return count + + def gethwnds(pid: int) -> list: hwnds = [] From 69086bd3b8e6172b4365fb473faa325f8be7acfe Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 16 Jun 2024 18:28:14 +0800 Subject: [PATCH 044/161] Upd: fix texts --- module/config/i18n/en-US.json | 2 +- module/device/device.py | 10 +------- module/device/platform/platform_base.py | 14 +++++++++++ module/device/platform/winapi.py | 32 +++++++++++-------------- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 49f216f9a9..c41b0442cf 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -515,7 +515,7 @@ }, "ProcessBufferTime": { "name": "Emulator turns off buffering for X minutes", - "help": "When the current time is less than X minutes from the next task, ALAS will switch from 'stop_emulator' to 'stay_there', preventing frequent start-stop cycles of the emulator.\nThis setting takes effect when the option is 'stop_emulator'." + "help": "When the current time is less than X minutes from the next task, ALAS will switch from 'stop_emulator' to 'stay_there', preventing frequent start-stop cycles of the emulator.\nThis setting takes effect when the option is 'stop emulator'." } }, "DropRecord": { diff --git a/module/device/device.py b/module/device/device.py index a35caac0bc..e68f080636 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -101,11 +101,9 @@ def __init__(self, *args, **kwargs): if self.config.Emulator_ControlMethod == 'minitouch': self.early_minitouch_init() - import sys if not self.initialized: self.initialized = True - if sys.platform == 'win32': - self.switchwindow() + self.switchwindow() def run_simple_screenshot_benchmark(self): """ @@ -139,9 +137,6 @@ def method_check(self): pass def emulator_check(self): - import sys - if sys.platform != 'win32': - return True return super().emulator_check() def handle_night_commission(self, daily_trigger='21:00', threshold=30): @@ -351,9 +346,6 @@ def emulator_start(self): def switchwindow(self): from module.device.platform import winapi - import sys - if sys.platform != 'win32': - return True if self.config.Emulator_SilentStart: return super().switchwindow(winapi.SW_MINIMIZE) else: diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index d4c2dae044..f1d4c2139e 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -31,6 +31,13 @@ class PlatformBase(Connection, EmulatorManagerBase): - emulator_stop() """ + def switchwindow(self, arg: int): + """ + Switch emulator's window. + """ + logger.info(f'Current platform {sys.platform} does not support switchwindow, skip') + return + def emulator_start(self): """ Start a emulator, until startup completed. @@ -45,6 +52,13 @@ def emulator_stop(self): """ logger.info(f'Current platform {sys.platform} does not support emulator_stop, skip') + def emulator_check(self): + """ + Check if emulator is running. + """ + logger.info(f'Current platform {sys.platform} does not support emulator_check, skip') + return True + @cached_property def emulator_info(self) -> EmulatorInfo: emulator = self.config.EmulatorInfo_Emulator diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 3d90534760..1529a55eff 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -1,5 +1,7 @@ import ctypes import ctypes.wintypes +from sys import getwindowsversion + import re import psutil @@ -33,10 +35,6 @@ class HwndNotFoundError(Exception): ... class ProcessNotFoundError(Exception): ... class WinApiError(Exception): ... -# winnt.h line 2809 -SYNCHRONIZE = 0x00100000 -STANDARD_RIGHTS_REQUIRED = 0x000F0000 - # winnt.h line 3961 PROCESS_TERMINATE = 0x0001 PROCESS_CREATE_THREAD = 0x0002 @@ -64,24 +62,22 @@ class WinApiError(Exception): ... THREAD_SET_LIMITED_INFORMATION = 0x0400 THREAD_QUERY_LIMITED_INFORMATION = 0x0800 -NTDDI_WIN10 = 0x0A000000 # Windows 10 -NTDDI_WINBLUE = 0x06030000 # Windows 8.1 -NTDDI_WIN8 = 0x06020000 # Windows 8 -NTDDI_WIN7 = 0x06010000 # Windows 7 +# winnt.h line 2809 +SYNCHRONIZE = 0x00100000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 + +VERSIONINFO = getwindowsversion() +MAJOR, MINOR, BUILD = VERSIONINFO.major, VERSIONINFO.minor, VERSIONINFO.build -# if NTDDI_VERSION >= 0x06000000: #above win7 -PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff -THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff -# else: #below win7 -# PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xfff -# THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3ff +if (MAJOR > 6) or (MAJOR == 6 and MINOR >= 1): + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff + THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff +else: + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xfff + THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3ff -# if _WIN64: #64bit MAXIMUM_PROC_PER_GROUP = 64 MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP -# else: #32bit -# MAXIMUM_PROC_PER_GROUP = 32 -# MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP # error.h line 23 ERROR_NO_MORE_FILES = 0x12 From 345b5ee8ba0f59b554d50d3bf943067d9e1b9a80 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 17 Jun 2024 02:51:15 +0800 Subject: [PATCH 045/161] Upd: add select SilentStart. --- config/template.json | 2 +- module/config/argument/args.json | 9 +++++++-- module/config/argument/argument.yaml | 8 +++++++- module/config/i18n/en-US.json | 5 ++++- module/config/i18n/ja-JP.json | 5 ++++- module/config/i18n/zh-CN.json | 5 ++++- module/config/i18n/zh-TW.json | 5 ++++- module/device/device.py | 8 +++++--- module/device/platform/platform_windows.py | 6 +++++- module/device/platform/winapi.py | 2 +- 10 files changed, 42 insertions(+), 13 deletions(-) diff --git a/config/template.json b/config/template.json index a6e251ec54..990ecd5910 100644 --- a/config/template.json +++ b/config/template.json @@ -8,7 +8,7 @@ "ControlMethod": "minitouch", "ScreenshotDedithering": false, "AdbRestart": false, - "SilentStart": false + "SilentStart": "normal" }, "EmulatorInfo": { "Emulator": "auto", diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 4f781597ea..62f65017bd 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -140,8 +140,13 @@ "value": false }, "SilentStart": { - "type": "checkbox", - "value": false + "type": "select", + "value": "normal", + "option": [ + "normal", + "minimize", + "silent" + ] } }, "EmulatorInfo": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 2ab6f806bd..6b3cd3c672 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -55,7 +55,13 @@ Emulator: ] ScreenshotDedithering: false AdbRestart: false - SilentStart: false + SilentStart: + value: normal + option: [ + normal, + minimize, + silent + ] EmulatorInfo: Emulator: value: auto diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index c41b0442cf..9b48f6841c 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -431,7 +431,10 @@ }, "SilentStart": { "name": "Start the emulator silently", - "help": "When checked, the emulator launched by Alas will start silently and be minimized then." + "help": "When the simulator is started by Alas, selecting 'Minimize mode' will cause the simulator to run minimally and selecting 'Silent mode' will cause the simulator to run silently.", + "normal": "Normal mode", + "minimize": "Minimize mode", + "silent": "Silent mode" } }, "EmulatorInfo": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index d919de7acb..abd5dc8181 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -431,7 +431,10 @@ }, "SilentStart": { "name": "Emulator.SilentStart.name", - "help": "Emulator.SilentStart.help" + "help": "Emulator.SilentStart.help", + "normal": "Normal mode", + "minimize": "Minimize mode", + "silent": "Silent mode" } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index a49188ed69..a6469e78f8 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -431,7 +431,10 @@ }, "SilentStart": { "name": "静默启动模拟器", - "help": "勾选此项后由Alas启动的模拟器将会静默启动并转为最小化" + "help": "由Alas启动模拟器时,选择最小化将使模拟器最小化运行,选择静默将使模拟器静默运行", + "normal": "常规模式", + "minimize": "最小化模式", + "silent": "静默模式" } }, "EmulatorInfo": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 7737b6f7a9..a3f071edac 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -431,7 +431,10 @@ }, "SilentStart": { "name": "靜默啓働模擬器", - "help": "勾選此項後由Alas啓働的模擬器將會靜默啓働竝轉爲最小化" + "help": "由Alas啓動模擬器時,選擇最小化將使模擬器最小化運行,選擇靜默將使模擬器靜默運行", + "normal": "常規模式", + "minimize": "最小化模式", + "silent": "靜默模式" } }, "EmulatorInfo": { diff --git a/module/device/device.py b/module/device/device.py index e68f080636..2af094dba0 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -346,7 +346,9 @@ def emulator_start(self): def switchwindow(self): from module.device.platform import winapi - if self.config.Emulator_SilentStart: + if self.config.Emulator_SilentStart == 'normal': + return super().switchwindow(winapi.SW_SHOW) + elif self.config.Emulator_SilentStart == 'minimize': return super().switchwindow(winapi.SW_MINIMIZE) - else: - return super().switchwindow(winapi.SW_SHOW) \ No newline at end of file + elif self.config.Emulator_SilentStart == 'silent': + return True \ No newline at end of file diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 30661337f1..ea2e2185fe 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -32,7 +32,11 @@ def execute(self, command: str): """ command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') - self.process, self.focusedwindow = winapi.execute(command, self.config.Emulator_SilentStart) + if self.config.Emulator_SilentStart == 'normal': + arg = False + else: + arg = True + self.process, self.focusedwindow = winapi.execute(command, arg) return True @classmethod diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 1529a55eff..180bd80e5d 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -378,7 +378,7 @@ def getprocess(instance: EmulatorInstance): try: proc = psutil.Process(lppe.th32ProcessID) cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - except psutil.NoSuchProcess: + except: continue if not instance.path in cmdline: continue From 580a056b923f9c16372059256fc46fafc92661ec Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 17 Jun 2024 03:23:46 +0800 Subject: [PATCH 046/161] Upd: fix logics. --- module/device/device.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index 2af094dba0..a954178c09 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -346,9 +346,13 @@ def emulator_start(self): def switchwindow(self): from module.device.platform import winapi - if self.config.Emulator_SilentStart == 'normal': + method = self.config.Emulator_SilentStart + if method == 'normal': return super().switchwindow(winapi.SW_SHOW) - elif self.config.Emulator_SilentStart == 'minimize': + elif method == 'minimize': return super().switchwindow(winapi.SW_MINIMIZE) - elif self.config.Emulator_SilentStart == 'silent': - return True \ No newline at end of file + elif method == 'silent': + return True + else: + from module.exception import ScriptError + raise ScriptError("Wrong setting") \ No newline at end of file From 93af96145c4ff10ef7ff8a81bbd412eeceda358e Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 18 Jun 2024 13:29:52 +0800 Subject: [PATCH 047/161] Upd: fix texts. --- alas.py | 10 ++++++++-- module/device/platform/winapi.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/alas.py b/alas.py index b6c26b1890..af4b826ff5 100644 --- a/alas.py +++ b/alas.py @@ -472,13 +472,19 @@ def get_next_task(self): logger.info(f'Wait until {task.next_run} for task `{task.command}`') self.is_first_task = False - method = self.config.Optimization_WhenTaskQueueEmpty + + method: str = self.config.Optimization_WhenTaskQueueEmpty + remainingtime: float = (task.next_run - datetime.now()).total_seconds() / 60 + buffertime: int = self.config.Optimization_ProcessBufferTime if ( method == 'stop_emulator' and self.device.emulator_check() and - ((task.next_run - datetime.now()).total_seconds() / 60 <= self.config.Optimization_ProcessBufferTime) + remainingtime <= buffertime ): + logger.info(f"The time to next task is {remainingtime}, + less than {buffertime}, fallback to stay_there") method = 'stay_there' + if method == 'close_game': logger.info('Close game during wait') self.device.app_stop() diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 180bd80e5d..8bf2bbc187 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -233,7 +233,7 @@ class PROCESSENTRY32(ctypes.Structure): GetForegroundWindow = user32.GetForegroundWindow SwitchToThisWindow = user32.SwitchToThisWindow -SwitchToThisWindow.argtypes = [HWND] +SwitchToThisWindow.argtypes = [HWND, BOOL] SwitchToThisWindow.restype = BOOL ShowWindow = user32.ShowWindow From a540c91a5239ee3e23cff5fd9d3c97896bf43a27 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 18 Jun 2024 13:36:57 +0800 Subject: [PATCH 048/161] Fix bug. --- alas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alas.py b/alas.py index af4b826ff5..ab2ae992c7 100644 --- a/alas.py +++ b/alas.py @@ -481,8 +481,8 @@ def get_next_task(self): self.device.emulator_check() and remainingtime <= buffertime ): - logger.info(f"The time to next task is {remainingtime}, - less than {buffertime}, fallback to stay_there") + logger.info(f"The time to next task is {remainingtime}, " + f"less than {buffertime}, fallback to stay_there") method = 'stay_there' if method == 'close_game': From e8394dba0a5fdeec42212d6c5c4da7ec9a71fcad Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Wed, 19 Jun 2024 15:16:06 +0800 Subject: [PATCH 049/161] Upd: fix texts --- alas.py | 4 ++-- module/device/platform/winapi.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/alas.py b/alas.py index ab2ae992c7..132e0c41d5 100644 --- a/alas.py +++ b/alas.py @@ -481,8 +481,8 @@ def get_next_task(self): self.device.emulator_check() and remainingtime <= buffertime ): - logger.info(f"The time to next task is {remainingtime}, " - f"less than {buffertime}, fallback to stay_there") + logger.info(f"The time to next task is {remainingtime:.2f} minutes, " + f"less than {buffertime} minutes, fallback to stay_there") method = 'stay_there' if method == 'close_game': diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 8bf2bbc187..cdb8bc2ad1 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -28,7 +28,7 @@ MAX_PATH = ctypes.wintypes.MAX_PATH LPARAM = ctypes.wintypes.LPARAM RECT = ctypes.wintypes.RECT -ULONG_PTR = ctypes.wintypes.PULONG +PULONG = ctypes.wintypes.PULONG class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... @@ -206,7 +206,7 @@ class PROCESSENTRY32(ctypes.Structure): ("dwSize", DWORD), ("cntUsage", DWORD), ("th32ProcessID", DWORD), - ("th32DefaultHeapID", ULONG_PTR), + ("th32DefaultHeapID", PULONG), ("th32ModuleID", DWORD), ("cntThreads", DWORD), ("th32ParentProcessID", DWORD), @@ -363,10 +363,12 @@ def _getprocess(proc: psutil.Process): if not threadhandle: raise ctypes.WinError(GetLastError()) - return (PyHANDLE(processhandle), PyHANDLE(threadhandle), proc.pid, mainthreadid) + ret = (PyHANDLE(processhandle), PyHANDLE(threadhandle), proc.pid, mainthreadid) + return ret except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") - return (None, None, proc.pid, proc.threads()[0].id) + ret = (None, None, proc.pid, proc.threads()[0].id) + return ret def getprocess(instance: EmulatorInstance): lppe = PROCESSENTRY32() @@ -416,7 +418,7 @@ def _switchwindow(hwnd: int, arg: int): def switchwindow(hwnds: list, arg: int): for hwnd in hwnds: if not IsWindow(hwnd): - continue + continue if GetParent(hwnd): continue rect = RECT() @@ -424,4 +426,4 @@ def switchwindow(hwnds: list, arg: int): if {rect.left, rect.top, rect.right, rect.bottom} == {0}: continue _switchwindow(hwnd, arg) - return True \ No newline at end of file + return True From 6a58667414fed575043e5a66b80bf22fdae78d0c Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 20 Jun 2024 04:16:00 +0800 Subject: [PATCH 050/161] Upd: fix texts --- alas.py | 2 +- module/device/platform/platform_windows.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/alas.py b/alas.py index 132e0c41d5..02e7b5165f 100644 --- a/alas.py +++ b/alas.py @@ -481,7 +481,7 @@ def get_next_task(self): self.device.emulator_check() and remainingtime <= buffertime ): - logger.info(f"The time to next task is {remainingtime:.2f} minutes, " + logger.info(f"The time to next task `{task.command}` is {remainingtime:.2f} minutes, " f"less than {buffertime} minutes, fallback to stay_there") method = 'stay_there' diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index ea2e2185fe..3549af8ee2 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -198,6 +198,10 @@ def emulator_start_watch(self): logger.info("Emulator starting...") serial = self.emulator_instance.serial + # Flash window + if self.focusedwindow != winapi.getfocusedwindow(): + winapi.setfocustowindow(self.focusedwindow) + def adb_connect(): m = self.adb_client.connect(self.serial) if 'connected' in m: @@ -268,10 +272,6 @@ def show_package(m): # All check passed break - - # Flash window - if self.focusedwindow != winapi.getfocusedwindow(): - winapi.setfocustowindow(self.focusedwindow) # Check emulator process and hwnds self.process = self.getprocess(self.emulator_instance) From d1ec9217526ff6441abd25f91f8755f7308944c7 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 20 Jun 2024 19:56:04 +0800 Subject: [PATCH 051/161] Upd: fix texts --- module/device/platform/platform_windows.py | 4 +++- module/device/platform/winapi.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 3549af8ee2..79ba5f400c 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -274,7 +274,6 @@ def show_package(m): break # Check emulator process and hwnds - self.process = self.getprocess(self.emulator_instance) self.hwnds = self.gethwnds(self.process[2]) self.proc = psutil.Process(self.process[2]) @@ -286,6 +285,9 @@ def show_package(m): def emulator_start(self): logger.hr('Emulator start', level=1) for _ in range(3): + # Stop + if not self._emulator_function_wrapper(self._emulator_stop): + return False # Start if self._emulator_function_wrapper(self._emulator_start): # Success diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index cdb8bc2ad1..1413eb5259 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -363,12 +363,12 @@ def _getprocess(proc: psutil.Process): if not threadhandle: raise ctypes.WinError(GetLastError()) - ret = (PyHANDLE(processhandle), PyHANDLE(threadhandle), proc.pid, mainthreadid) - return ret + process = (PyHANDLE(processhandle), PyHANDLE(threadhandle), proc.pid, mainthreadid) + return process except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") - ret = (None, None, proc.pid, proc.threads()[0].id) - return ret + process = (None, None, proc.pid, proc.threads()[0].id) + return process def getprocess(instance: EmulatorInstance): lppe = PROCESSENTRY32() From 802ca0d4e5c6f88b86cb485c4b3019525e72ca51 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 21 Jun 2024 02:19:31 +0800 Subject: [PATCH 052/161] Upd: Add single_to_console method. --- module/device/platform/emulator_windows.py | 24 +++++++++++++++++++++- module/device/platform/platform_windows.py | 11 ++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/module/device/platform/emulator_windows.py b/module/device/platform/emulator_windows.py index 30d42ca3d5..36f0d9927c 100644 --- a/module/device/platform/emulator_windows.py +++ b/module/device/platform/emulator_windows.py @@ -127,7 +127,7 @@ def path_to_type(cls, path: str) -> str: return '' @staticmethod - def multi_to_single(exe): + def multi_to_single(exe: str): """ Convert a string that might be a multi-instance manager to its single instance executable. @@ -155,6 +155,28 @@ def multi_to_single(exe): else: yield exe + @staticmethod + def single_to_console(exe: str): + """ + Convert a string that might be a single instance executable to its console. + + Args: + exe (str): Path to emulator executable + + Returns: + str: Path to emulator console + """ + if 'MuMuPlayer.exe' in exe: + return exe.replace('MuMuPlayer.exe', 'MuMuManager.exe') + elif 'LDPlayer.exe' in exe: + return exe.replace('LDPlayer.exe', 'dnconsole.exe') + elif 'Bluestacks.exe' in exe: + return exe.replace('Bluestacks.exe', 'bsconsole.exe') + elif 'MEmu.exe' in exe: + return exe.replace('MEmu.exe', 'memuc.exe') + else: + return exe + @staticmethod def vbox_file_to_serial(file: str) -> str: """ diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 79ba5f400c..77b8bf56b1 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -137,13 +137,10 @@ def _emulator_stop(self, instance: EmulatorInstance): # E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.execute( - f'"{os.path.join(os.path.dirname(exe),"MuMuManager.exe")}"' - f' api -v {instance.MuMuPlayer12_id} shutdown_player' - ) + self.execute(f'"{Emulator.single_to_console(exe)}" api -v {instance.MuMuPlayer12_id} shutdown_player') elif instance == Emulator.LDPlayerFamily: # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 - self.execute(f'"{os.path.join(os.path.dirname(exe),"dnconsole.exe")}" quit --index {instance.LDPlayer_id}') + self.execute(f'"{Emulator.single_to_console(exe)}" quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit self.execute(f'"{exe}" -clone:{instance.name} -quit') @@ -158,10 +155,10 @@ def _emulator_stop(self, instance: EmulatorInstance): ) elif instance == Emulator.BlueStacks4: # E:\Program Files (x86)\BluestacksCN\bsconsole.exe quit --name Android - self.execute(f'"{os.path.join(os.path.dirname(exe),"bsconsole.exe")}" quit --name {instance.name}') + self.execute(f'"{Emulator.single_to_console(exe)}" quit --name {instance.name}') elif instance == Emulator.MEmuPlayer: # F:\Program Files\Microvirt\MEmu\memuc.exe stop -n MEmu_0 - self.execute(f'"{os.path.join(os.path.dirname(exe),"memuc.exe")}" stop -n {instance.name}') + self.execute(f'"{Emulator.single_to_console(exe)}" stop -n {instance.name}') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') From 65fec42e34dd4ef2932964840b3c98274c2d6e0c Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 21 Jun 2024 02:22:57 +0800 Subject: [PATCH 053/161] Upd: fix texts --- module/device/platform/platform_windows.py | 1 - 1 file changed, 1 deletion(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 77b8bf56b1..81c89ace82 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -103,7 +103,6 @@ def _emulator_stop(self, instance: EmulatorInstance): """ Stop a emulator without error handling """ - import os exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: # MuMu6 does not have multi instance, kill one means kill all From 779bbb53ac0e1702e8b2b0fd213ee25f89b60b24 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 21 Jun 2024 02:53:18 +0800 Subject: [PATCH 054/161] Upd: fix logics. --- module/device/platform/platform_windows.py | 2 +- module/device/platform/winapi.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 81c89ace82..12722d3019 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -325,7 +325,7 @@ def emulator_check(self): pid = self.process[2] self.proc = psutil.Process(pid) cmdline = DataProcessInfo(proc=self.proc, pid=self.proc.pid).cmdline - if self.emulator_instance.path in cmdline: + if self.emulator_instance.path in cmdline and self.proc.is_running(): return True else: return False diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 1413eb5259..bd75c6e8ec 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -353,22 +353,22 @@ def callback(hwnd: int, lparam): def _getprocess(proc: psutil.Process): + mainthreadid = proc.threads()[0].id + process: list = [None, None, proc.pid, mainthreadid] try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) if not processhandle: raise ctypes.WinError(GetLastError()) - mainthreadid = proc.threads()[0].id threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) if not threadhandle: raise ctypes.WinError(GetLastError()) - process = (PyHANDLE(processhandle), PyHANDLE(threadhandle), proc.pid, mainthreadid) - return process + process[0], process[1] = PyHANDLE(processhandle), PyHANDLE(threadhandle) + return tuple(process) except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") - process = (None, None, proc.pid, proc.threads()[0].id) - return process + return tuple(process) def getprocess(instance: EmulatorInstance): lppe = PROCESSENTRY32() From c6c2525f02b8ce26602e5db39b5112b4dd5637e4 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 22 Jun 2024 08:15:41 +0800 Subject: [PATCH 055/161] Upd: fix init bug. --- alas.py | 2 + module/device/platform/platform_windows.py | 59 ++--- module/device/platform/winapi.py | 243 +++++++++++---------- 3 files changed, 154 insertions(+), 150 deletions(-) diff --git a/alas.py b/alas.py index 6904de91c9..75312137f9 100644 --- a/alas.py +++ b/alas.py @@ -462,6 +462,7 @@ def get_next_task(self): self.config.Optimization_WhenTaskQueueEmpty != 'stop_emulator' ): logger.warning('Emulator is not running') + self.device.emulator_stop() self.device.emulator_start() if not task == 'Restart': self.run('start') @@ -560,6 +561,7 @@ def loop(self): # Reboot emulator if not self.device.emulator_check(): logger.warning('Emulator is not running') + self.device.emulator_stop() self.device.emulator_start() if not task == 'Restart': self.run('start') diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 12722d3019..310efb32e8 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -15,12 +15,16 @@ class EmulatorUnknown(Exception): class PlatformWindows(PlatformBase, EmulatorManager): + initialized = False + def __init__(self, config): super().__init__(config) - self.process: tuple = None - self.proc: psutil.Process = None - self.hwnds: list[int] = None - self.focusedwindow: int = None + if not PlatformWindows.initialized: + self.process: tuple = () + self.psproc: psutil.Process = psutil.Process() + self.hwnds: list = [] + self.focusedwindow: int = 0 + self.__class__.initialized = True def execute(self, command: str): """ @@ -67,7 +71,7 @@ def switchwindow(self, arg: int): def _emulator_start(self, instance: EmulatorInstance): """ - Start a emulator without error handling + Start an emulator without error handling """ exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: @@ -101,7 +105,7 @@ def _emulator_start(self, instance: EmulatorInstance): def _emulator_stop(self, instance: EmulatorInstance): """ - Stop a emulator without error handling + Stop an emulator without error handling """ exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: @@ -196,7 +200,7 @@ def emulator_start_watch(self): # Flash window if self.focusedwindow != winapi.getfocusedwindow(): - winapi.setfocustowindow(self.focusedwindow) + winapi.switchtothiswindow(self.focusedwindow) def adb_connect(): m = self.adb_client.connect(self.serial) @@ -271,7 +275,7 @@ def show_package(m): # Check emulator process and hwnds self.hwnds = self.gethwnds(self.process[2]) - self.proc = psutil.Process(self.process[2]) + self.psproc = psutil.Process(self.process[2]) logger.info(f'Emulator start completed') logger.info(f'Emulator Process: {self.process}') @@ -281,9 +285,6 @@ def show_package(m): def emulator_start(self): logger.hr('Emulator start', level=1) for _ in range(3): - # Stop - if not self._emulator_function_wrapper(self._emulator_stop): - return False # Start if self._emulator_function_wrapper(self._emulator_start): # Success @@ -301,42 +302,30 @@ def emulator_start(self): def emulator_stop(self): logger.hr('Emulator stop', level=1) - for _ in range(3): - # Stop - if self._emulator_function_wrapper(self._emulator_stop): - # Success - return True - else: - # Failed to stop, start and stop again - if self._emulator_function_wrapper(self._emulator_start): - continue - else: - return False - - logger.error('Failed to stop emulator 3 times, stopped') - return False + return self._emulator_function_wrapper(self._emulator_stop) def emulator_check(self): try: if self.process is None: self.process = self.getprocess(self.emulator_instance) return True - if self.proc is None: - pid = self.process[2] - self.proc = psutil.Process(pid) - cmdline = DataProcessInfo(proc=self.proc, pid=self.proc.pid).cmdline - if self.emulator_instance.path in cmdline and self.proc.is_running(): + if self.psproc.pid != self.process[2]: + self.psproc = psutil.Process(self.process[2]) + cmdline = DataProcessInfo(proc=self.psproc, pid=self.psproc.pid).cmdline + if self.emulator_instance.path in cmdline and self.psproc.is_running(): return True else: return False - except winapi.ProcessNotFoundError as e: - return False - except winapi.WinApiError as e: + except ProcessLookupError as e: + logger.warning(e) return False - except psutil.NoSuchProcess as e: + except psutil.NoSuchProcess: return False - except psutil.AccessDenied as e: + except psutil.AccessDenied: return False + except RuntimeError as e: + logger.error(e) + raise except Exception as e: logger.error(e) return False diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index bd75c6e8ec..44e30855be 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -1,39 +1,26 @@ -import ctypes -import ctypes.wintypes -from sys import getwindowsversion - -import re +from ctypes import ( + byref, sizeof, WinError, POINTER, WINFUNCTYPE, + WinDLL, Structure +) +from ctypes.wintypes import ( + HANDLE, DWORD, WORD, BYTE, BOOL, LONG, CHAR, LPWSTR, + LPCWSTR, LPVOID, HWND, MAX_PATH, + LPARAM, RECT, PULONG +) +from sys import getwindowsversion import psutil +import re from deploy.Windows.utils import DataProcessInfo from module.device.platform.emulator_windows import Emulator, EmulatorInstance from module.logger import logger -user32 = ctypes.windll.user32 -kernel32 = ctypes.windll.kernel32 -psapi = ctypes.windll.psapi -PyHANDLE = ctypes.wintypes.HANDLE -DWORD = ctypes.wintypes.DWORD -WORD = ctypes.wintypes.WORD -BYTE = ctypes.wintypes.BYTE -BOOL = ctypes.wintypes.BOOL -LONG = ctypes.wintypes.LONG -CHAR = ctypes.wintypes.CHAR -WCHAR = ctypes.wintypes.WCHAR -LPWSTR = ctypes.wintypes.LPWSTR -LPCWSTR = ctypes.wintypes.LPCWSTR -LPVOID = ctypes.wintypes.LPVOID -HWND = ctypes.wintypes.HWND -MAX_PATH = ctypes.wintypes.MAX_PATH -LPARAM = ctypes.wintypes.LPARAM -RECT = ctypes.wintypes.RECT -PULONG = ctypes.wintypes.PULONG +user32 = WinDLL('user32', use_last_error=True) +kernel32 = WinDLL('kernel32', use_last_error=True) class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... -class ProcessNotFoundError(Exception): ... -class WinApiError(Exception): ... # winnt.h line 3961 PROCESS_TERMINATE = 0x0001 @@ -88,7 +75,12 @@ class WinApiError(Exception): ... TH32CS_SNAPTHREAD = 0x00000004 TH32CS_SNAPMODULE = 0x00000008 TH32CS_SNAPMODULE32 = 0x00000010 -TH32CS_SNAPALL = TH32CS_SNAPHEAPLIST | TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE +TH32CS_SNAPALL = ( + TH32CS_SNAPHEAPLIST | + TH32CS_SNAPPROCESS | + TH32CS_SNAPTHREAD | + TH32CS_SNAPMODULE +) TH32CS_INHERIT = 0x80000000 # winbase.h line 1463 @@ -164,7 +156,7 @@ class WinApiError(Exception): ... CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000 -class STARTUPINFO(ctypes.Structure): +class STARTUPINFO(Structure): _fields_ = [ ('cb', DWORD), ('lpReserved', LPWSTR), @@ -180,28 +172,28 @@ class STARTUPINFO(ctypes.Structure): ('dwFlags', DWORD), ('wShowWindow', WORD), ('cbReserved2', WORD), - ('lpReserved2', ctypes.POINTER(BYTE)), - ('hStdInput', PyHANDLE), - ('hStdOutput', PyHANDLE), - ('hStdError', PyHANDLE), + ('lpReserved2', POINTER(BYTE)), + ('hStdInput', HANDLE), + ('hStdOutput', HANDLE), + ('hStdError', HANDLE), ] -class PROCESS_INFORMATION(ctypes.Structure): +class PROCESSINFORMATION(Structure): _fields_ = [ - ('hProcess', PyHANDLE), - ('hThread', PyHANDLE), + ('hProcess', HANDLE), + ('hThread', HANDLE), ('dwProcessId', DWORD), ('dwThreadId', DWORD), ] -class SECURITY_ATTRIBUTES(ctypes.Structure): +class SECURITYATTRIBUTES(Structure): _fields_ = [ ("nLength", DWORD), - ("lpSecurityDescriptor", ctypes.c_void_p), - ("bInheritHandle", BOOL) + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL), ] -class PROCESSENTRY32(ctypes.Structure): +class PROCESSENTRY32(Structure): _fields_ = [ ("dwSize", DWORD), ("cntUsage", DWORD), @@ -216,20 +208,20 @@ class PROCESSENTRY32(ctypes.Structure): ] -CreateProcessW = kernel32.CreateProcessW -CreateProcessW.argtypes = [ - LPCWSTR, - LPWSTR, - ctypes.POINTER(SECURITY_ATTRIBUTES), - ctypes.POINTER(SECURITY_ATTRIBUTES), - BOOL, - DWORD, - LPVOID, - LPCWSTR, - ctypes.POINTER(STARTUPINFO), - ctypes.POINTER(PROCESS_INFORMATION) +CreateProcessW = kernel32.CreateProcessW +CreateProcessW.argtypes = [ + LPCWSTR, #lpApplicationName + LPWSTR, #lpCommandLine + POINTER(SECURITYATTRIBUTES), #lpProcessAttributes + POINTER(SECURITYATTRIBUTES), #lpThreadAttributes + BOOL, #bInheritHandles + DWORD, #dwCreationFlags + LPVOID, #lpEnvironment + LPCWSTR, #lpCurrentDirectory + POINTER(STARTUPINFO), #lpStartupInfo + POINTER(PROCESSINFORMATION) #lpProcessInformation ] -CreateProcessW.restype = BOOL +CreateProcessW.restype = BOOL GetForegroundWindow = user32.GetForegroundWindow SwitchToThisWindow = user32.SwitchToThisWindow @@ -242,30 +234,30 @@ class PROCESSENTRY32(ctypes.Structure): GetWindowRect = user32.GetWindowRect EnumWindows = user32.EnumWindows -EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) +EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM) GetWindowThreadProcessId = user32.GetWindowThreadProcessId -GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(DWORD)] +GetWindowThreadProcessId.argtypes = [HWND, POINTER(DWORD)] GetWindowThreadProcessId.restype = DWORD OpenProcess = kernel32.OpenProcess OpenProcess.argtypes = [DWORD, BOOL, DWORD] -OpenProcess.restype = PyHANDLE +OpenProcess.restype = HANDLE OpenThread = kernel32.OpenThread CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] -CreateToolhelp32Snapshot.restype = PyHANDLE +CreateToolhelp32Snapshot.restype = HANDLE CloseHandle = kernel32.CloseHandle -CloseHandle.argtypes = [PyHANDLE] +CloseHandle.argtypes = [HANDLE] CloseHandle.restype = BOOL Process32First = kernel32.Process32First -Process32First.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32First.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] Process32First.restype = BOOL Process32Next = kernel32.Process32Next -Process32Next.argtypes = [PyHANDLE, ctypes.POINTER(PROCESSENTRY32)] +Process32Next.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] Process32Next.restype = BOOL GetLastError = kernel32.GetLastError @@ -275,32 +267,47 @@ class PROCESSENTRY32(ctypes.Structure): def getfocusedwindow() -> int: return GetForegroundWindow() -def setfocustowindow(hwnd: int) -> bool: +def switchtothiswindow(hwnd: int) -> bool: SwitchToThisWindow(hwnd, True) return True def execute(command: str, arg: bool): - startupinfo = STARTUPINFO() - startupinfo.cb = ctypes.sizeof(STARTUPINFO) - startupinfo.dwFlags = STARTF_USESHOWWINDOW - startupinfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE - - focusedwindow = getfocusedwindow() + from shlex import split + from os.path import dirname + lpApplicationName = split(command)[0] + lpCommandLine = command + lpProcessAttributes = None + lpThreadAttributes = None + bInheritHandles = False + dwCreationFlags = ( + CREATE_NEW_CONSOLE | + NORMAL_PRIORITY_CLASS | + CREATE_NEW_PROCESS_GROUP | + CREATE_DEFAULT_ERROR_MODE | + CREATE_UNICODE_ENVIRONMENT + ) + lpEnvironment = None + lpCurrentDirectory = dirname(lpApplicationName) + lpStartupInfo = STARTUPINFO() + lpStartupInfo.cb = sizeof(STARTUPINFO) + lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW + lpStartupInfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE + lpProcessInformation = PROCESSINFORMATION() - processinformation = PROCESS_INFORMATION() + focusedwindow = getfocusedwindow() success = CreateProcessW( - None, - command, - None, - None, - False, - DETACHED_PROCESS, - None, - None, - ctypes.byref(startupinfo), - ctypes.byref(processinformation) + lpApplicationName, + lpCommandLine, + lpProcessAttributes, + lpThreadAttributes, + bInheritHandles, + dwCreationFlags, + lpEnvironment, + lpCurrentDirectory, + byref(lpStartupInfo), + byref(lpProcessInformation) ) if not success: @@ -308,10 +315,10 @@ def execute(command: str, arg: bool): raise EmulatorLaunchFailedError(f"Failed to start emulator. Error code: {errorcode}") process = ( - processinformation.hProcess, - processinformation.hThread, - processinformation.dwProcessId, - processinformation.dwThreadId + HANDLE(lpProcessInformation.hProcess), + HANDLE(lpProcessInformation.hThread), + lpProcessInformation.dwProcessId, + lpProcessInformation.dwThreadId ) return process, focusedwindow @@ -336,7 +343,7 @@ def gethwnds(pid: int) -> list: @EnumWindowsProc def callback(hwnd: int, lparam): processid = DWORD() - GetWindowThreadProcessId(hwnd, ctypes.byref(processid)) + GetWindowThreadProcessId(hwnd, byref(processid)) if processid.value == pid: hwnds.append(hwnd) return True @@ -352,68 +359,74 @@ def callback(hwnd: int, lparam): return hwnds +def listprocesses(): + lppe32 = PROCESSENTRY32() + lppe32.dwSize = sizeof(PROCESSENTRY32) + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) + if snapshot == -1: + raise RuntimeError("Failed to create process snapshot") + + if not Process32First(snapshot, byref(lppe32)): + kernel32.CloseHandle(snapshot) + raise RuntimeError("Failed to get first process") + + while 1: + yield lppe32, snapshot + if not kernel32.Process32Next(snapshot, byref(lppe32)): + # finished querying + errorcode = GetLastError() + if errorcode != ERROR_NO_MORE_FILES: + # error code != ERROR_NO_MORE_FILES, means that win api failed + raise RuntimeError("Failed to get next process") + # process not found + raise ProcessLookupError("Process not found") + def _getprocess(proc: psutil.Process): mainthreadid = proc.threads()[0].id process: list = [None, None, proc.pid, mainthreadid] try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) if not processhandle: - raise ctypes.WinError(GetLastError()) + raise WinError(GetLastError()) threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) if not threadhandle: - raise ctypes.WinError(GetLastError()) + CloseHandle(processhandle) + raise WinError(GetLastError()) - process[0], process[1] = PyHANDLE(processhandle), PyHANDLE(threadhandle) + process[0], process[1] = HANDLE(processhandle), HANDLE(threadhandle) return tuple(process) except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") return tuple(process) def getprocess(instance: EmulatorInstance): - lppe = PROCESSENTRY32() - lppe.dwSize = ctypes.sizeof(PROCESSENTRY32) - hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) - Process32First(hSnapshot, ctypes.pointer(lppe)) - - while Process32Next(hSnapshot, ctypes.pointer(lppe)): + for lppe32, snapshot in listprocesses(): try: - proc = psutil.Process(lppe.th32ProcessID) + proc = psutil.Process(lppe32.th32ProcessID) cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - except: + except (psutil.NoSuchProcess, psutil.AccessDenied): continue + if not instance.path in cmdline: continue if instance == Emulator.MuMuPlayer12: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.MuMuPlayer12_id: - break + CloseHandle(snapshot) + return _getprocess(proc) elif instance == Emulator.LDPlayerFamily: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.LDPlayer_id: - break + CloseHandle(snapshot) + return _getprocess(proc) else: matchstr = re.search(fr'\b{instance.name}$', cmdline) if matchstr and matchstr.group() == instance.name: - break - else: - # finished querying - errorcode = GetLastError() - CloseHandle(hSnapshot) + CloseHandle(snapshot) + return _getprocess(proc) - if errorcode != ERROR_NO_MORE_FILES: - # error code != ERROR_NO_MORE_FILES, means that win api failed - raise WinApiError(f"Win api failed with error code: {errorcode}") - # process not found - raise ProcessNotFoundError("Process not found") - - CloseHandle(hSnapshot) - return _getprocess(proc) - - -def _switchwindow(hwnd: int, arg: int): - ShowWindow(hwnd, arg) - return True + raise ProcessLookupError("Process not found") def switchwindow(hwnds: list, arg: int): for hwnd in hwnds: @@ -422,8 +435,8 @@ def switchwindow(hwnds: list, arg: int): if GetParent(hwnd): continue rect = RECT() - GetWindowRect(hwnd, ctypes.byref(rect)) + GetWindowRect(hwnd, byref(rect)) if {rect.left, rect.top, rect.right, rect.bottom} == {0}: continue - _switchwindow(hwnd, arg) + ShowWindow(hwnd, arg) return True From 0408c7644b027ad51d7f6d4d3772499a0518f7d1 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 23 Jun 2024 04:38:21 +0800 Subject: [PATCH 056/161] Upd: fix bugs --- module/device/platform/platform_windows.py | 31 +++++++--------- module/device/platform/winapi.py | 43 +++++++++++++--------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 310efb32e8..9289a8307a 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -14,18 +14,14 @@ class EmulatorUnknown(Exception): pass -class PlatformWindows(PlatformBase, EmulatorManager): - initialized = False - - def __init__(self, config): - super().__init__(config) - if not PlatformWindows.initialized: - self.process: tuple = () - self.psproc: psutil.Process = psutil.Process() - self.hwnds: list = [] - self.focusedwindow: int = 0 - self.__class__.initialized = True +class EmulatorStatus: + process: tuple = None + psproc: psutil.Process = None + hwnds: list = None + focusedwindow: int = None + +class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): def execute(self, command: str): """ Args: @@ -55,7 +51,7 @@ def kill_process_by_regex(cls, regex: str) -> int: int: Number of processes killed """ return winapi.kill_process_by_regex(regex) - + @staticmethod def gethwnds(pid: int) -> list: return winapi.gethwnds(pid) @@ -198,10 +194,6 @@ def emulator_start_watch(self): logger.info("Emulator starting...") serial = self.emulator_instance.serial - # Flash window - if self.focusedwindow != winapi.getfocusedwindow(): - winapi.switchtothiswindow(self.focusedwindow) - def adb_connect(): m = self.adb_client.connect(self.serial) if 'connected' in m: @@ -273,6 +265,10 @@ def show_package(m): # All check passed break + # Flash window + if self.focusedwindow != winapi.getfocusedwindow(): + winapi.switchtothiswindow(self.focusedwindow) + # Check emulator process and hwnds self.hwnds = self.gethwnds(self.process[2]) self.psproc = psutil.Process(self.process[2]) @@ -303,7 +299,7 @@ def emulator_start(self): def emulator_stop(self): logger.hr('Emulator stop', level=1) return self._emulator_function_wrapper(self._emulator_stop) - + def emulator_check(self): try: if self.process is None: @@ -330,6 +326,7 @@ def emulator_check(self): logger.error(e) return False + if __name__ == '__main__': self = PlatformWindows('alas') d = self.emulator_instance diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 44e30855be..267651ad7e 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -175,7 +175,7 @@ class STARTUPINFO(Structure): ('lpReserved2', POINTER(BYTE)), ('hStdInput', HANDLE), ('hStdOutput', HANDLE), - ('hStdError', HANDLE), + ('hStdError', HANDLE) ] class PROCESSINFORMATION(Structure): @@ -183,14 +183,14 @@ class PROCESSINFORMATION(Structure): ('hProcess', HANDLE), ('hThread', HANDLE), ('dwProcessId', DWORD), - ('dwThreadId', DWORD), + ('dwThreadId', DWORD) ] class SECURITYATTRIBUTES(Structure): _fields_ = [ ("nLength", DWORD), ("lpSecurityDescriptor", LPVOID), - ("bInheritHandle", BOOL), + ("bInheritHandle", BOOL) ] class PROCESSENTRY32(Structure): @@ -204,7 +204,7 @@ class PROCESSENTRY32(Structure): ("th32ParentProcessID", DWORD), ("pcPriClassBase", LONG), ("dwFlags", DWORD), - ("szExeFile", CHAR * MAX_PATH), + ("szExeFile", CHAR * MAX_PATH) ] @@ -269,6 +269,7 @@ def getfocusedwindow() -> int: def switchtothiswindow(hwnd: int) -> bool: SwitchToThisWindow(hwnd, True) + ShowWindow(hwnd, SW_RESTORE) return True @@ -359,27 +360,34 @@ def callback(hwnd: int, lparam): return hwnds -def listprocesses(): +def enumprocesses(): lppe32 = PROCESSENTRY32() lppe32.dwSize = sizeof(PROCESSENTRY32) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) if snapshot == -1: - raise RuntimeError("Failed to create process snapshot") + raise RuntimeError(f"Failed to create process snapshot. Errorcode: {GetLastError()}") if not Process32First(snapshot, byref(lppe32)): - kernel32.CloseHandle(snapshot) - raise RuntimeError("Failed to get first process") + CloseHandle(snapshot) + raise RuntimeError(f"Failed to get first process. Errorcode: {GetLastError()}") - while 1: - yield lppe32, snapshot - if not kernel32.Process32Next(snapshot, byref(lppe32)): + try: + while 1: + yield lppe32 + if Process32Next(snapshot, byref(lppe32)): + continue # finished querying errorcode = GetLastError() + CloseHandle(snapshot) if errorcode != ERROR_NO_MORE_FILES: # error code != ERROR_NO_MORE_FILES, means that win api failed - raise RuntimeError("Failed to get next process") + raise RuntimeError(f"Failed to get next process. Errorcode: {errorcode}") # process not found - raise ProcessLookupError("Process not found") + raise ProcessLookupError(f"Process not found. Errorcode: {errorcode}") + except GeneratorExit: + CloseHandle(snapshot) + finally: + del lppe32, snapshot, errorcode def _getprocess(proc: psutil.Process): mainthreadid = proc.threads()[0].id @@ -401,7 +409,7 @@ def _getprocess(proc: psutil.Process): return tuple(process) def getprocess(instance: EmulatorInstance): - for lppe32, snapshot in listprocesses(): + for lppe32 in enumprocesses(): try: proc = psutil.Process(lppe32.th32ProcessID) cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline @@ -413,20 +421,19 @@ def getprocess(instance: EmulatorInstance): if instance == Emulator.MuMuPlayer12: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.MuMuPlayer12_id: - CloseHandle(snapshot) + enumprocesses().close() return _getprocess(proc) elif instance == Emulator.LDPlayerFamily: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.LDPlayer_id: - CloseHandle(snapshot) + enumprocesses().close() return _getprocess(proc) else: matchstr = re.search(fr'\b{instance.name}$', cmdline) if matchstr and matchstr.group() == instance.name: - CloseHandle(snapshot) + enumprocesses().close() return _getprocess(proc) - raise ProcessLookupError("Process not found") def switchwindow(hwnds: list, arg: int): for hwnd in hwnds: From d634b75b52c5d1e1cbfcb582c949d049fe7fbef7 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 23 Jun 2024 04:55:53 +0800 Subject: [PATCH 057/161] Upd: fix bug. --- module/device/platform/platform_windows.py | 2 +- module/device/platform/winapi.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 9289a8307a..bfca1e246f 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -16,7 +16,7 @@ class EmulatorUnknown(Exception): class EmulatorStatus: process: tuple = None - psproc: psutil.Process = None + psproc: psutil.Process = psutil.Process() hwnds: list = None focusedwindow: int = None diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 267651ad7e..38918ccd2f 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -387,7 +387,7 @@ def enumprocesses(): except GeneratorExit: CloseHandle(snapshot) finally: - del lppe32, snapshot, errorcode + del lppe32, snapshot def _getprocess(proc: psutil.Process): mainthreadid = proc.threads()[0].id From 4f866337dfb3364aade6ab17e249677c8ff599d7 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 24 Jun 2024 18:29:48 +0800 Subject: [PATCH 058/161] Upd: Add BufferMethod. --- alas.py | 43 ++++++----- module/config/argument/args.json | 9 +++ module/config/argument/argument.yaml | 3 + module/config/config_generated.py | 5 +- module/config/i18n/en-US.json | 6 ++ module/config/i18n/ja-JP.json | 6 ++ module/config/i18n/zh-CN.json | 6 ++ module/config/i18n/zh-TW.json | 6 ++ module/device/platform/platform_windows.py | 17 +++-- module/device/platform/winapi.py | 84 ++++++++++++++-------- 10 files changed, 127 insertions(+), 58 deletions(-) diff --git a/alas.py b/alas.py index 75312137f9..9493f22789 100644 --- a/alas.py +++ b/alas.py @@ -441,6 +441,14 @@ def wait_until(self, future): if self.config.should_reload(): return False + def emurestart(self, task): + logger.warning('Emulator is not running') + self.device.emulator_stop() + self.device.emulator_start() + if not task == 'Restart': + self.run('start') + del_cached_property(self, 'config') + def get_next_task(self): """ Returns: @@ -454,20 +462,6 @@ def get_next_task(self): from module.base.resource import release_resources if self.config.task.command != 'Alas': release_resources(next_task=task.command) - - # Reboot emulator - if ( - not self.device.emulator_check() and - task.next_run > datetime.now() and - self.config.Optimization_WhenTaskQueueEmpty != 'stop_emulator' - ): - logger.warning('Emulator is not running') - self.device.emulator_stop() - self.device.emulator_start() - if not task == 'Restart': - self.run('start') - del_cached_property(self, 'config') - continue if task.next_run <= datetime.now(): break @@ -483,9 +477,11 @@ def get_next_task(self): self.device.emulator_check() and remainingtime <= buffertime ): - logger.info(f"The time to next task `{task.command}` is {remainingtime:.2f} minutes, " - f"less than {buffertime} minutes, fallback to stay_there") - method = 'stay_there' + method = self.config.Optimization_BufferMethod + logger.info( + f"The time to next task `{task.command}` is {remainingtime:.2f} minutes, " + f"less than {buffertime} minutes, fallback to {method}" + ) if method == 'close_game': logger.info('Close game during wait') @@ -518,6 +514,12 @@ def get_next_task(self): release_resources() self.device.release_during_wait() if not self.wait_until(task.next_run): + method: str = self.config.Optimization_WhenTaskQueueEmpty + if ( + not self.device.emulator_check() and + method != 'stop_emulator' + ): + self.emurestart(task.command) del_cached_property(self, 'config') continue else: @@ -560,12 +562,7 @@ def loop(self): task = self.get_next_task() # Reboot emulator if not self.device.emulator_check(): - logger.warning('Emulator is not running') - self.device.emulator_stop() - self.device.emulator_start() - if not task == 'Restart': - self.run('start') - del_cached_property(self, 'config') + self.emurestart(task) # Skip first restart if self.is_first_task and task == 'Restart': logger.info('Skip task `Restart` at scheduler start') diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 846ab20ea3..37670f403a 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -224,6 +224,15 @@ "ProcessBufferTime": { "type": "input", "value": 10 + }, + "BufferMethod": { + "type": "select", + "value": "stay_there", + "option": [ + "stay_there", + "goto_main", + "close_game" + ] } }, "DropRecord": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 1b95632fd7..0e73a2903d 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -103,6 +103,9 @@ Optimization: value: goto_main option: [ stay_there, goto_main, close_game, stop_emulator ] ProcessBufferTime: 10 + BufferMethod: + value: stay_there + option: [ stay_there, goto_main, close_game ] DropRecord: SaveFolder: ./screenshots AzurStatsID: null diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 5bb338343b..55b68bf525 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -25,6 +25,7 @@ class GeneratedConfig: Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False + Emulator_SilentStart = 'normal' # normal, minimize, silent # Group `EmulatorInfo` EmulatorInfo_Emulator = 'auto' # auto, NoxPlayer, NoxPlayer64, BlueStacks4, BlueStacks5, BlueStacks4HyperV, BlueStacks5HyperV, LDPlayer3, LDPlayer4, LDPlayer9, MuMuPlayer, MuMuPlayerX, MuMuPlayer12, MEmuPlayer @@ -41,7 +42,9 @@ class GeneratedConfig: Optimization_ScreenshotInterval = 0.3 Optimization_CombatScreenshotInterval = 1.0 Optimization_TaskHoardingDuration = 0 - Optimization_WhenTaskQueueEmpty = 'goto_main' # stay_there, goto_main, close_game + Optimization_WhenTaskQueueEmpty = 'goto_main' # stay_there, goto_main, close_game, stop_emulator + Optimization_ProcessBufferTime = 7 + Optimization_BufferMethod = 'stay_there' # Group `DropRecord` DropRecord_SaveFolder = './screenshots' diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 5972350929..8f614d2a48 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -519,6 +519,12 @@ "ProcessBufferTime": { "name": "Emulator turns off buffering for X minutes", "help": "When the current time is less than X minutes from the next task, ALAS will switch from 'stop_emulator' to 'stay_there', preventing frequent start-stop cycles of the emulator.\nThis setting takes effect when the option is 'stop emulator'." + }, + "BufferMethod": { + "name": "Behavior during buffer time", + "stay_there": "stay_there", + "goto_main": "goto_main", + "close_game": "close_game" } }, "DropRecord": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 31af71369d..a58930c3ae 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -519,6 +519,12 @@ "ProcessBufferTime": { "name": "Optimization.ProcessBufferTime.name", "help": "Optimization.ProcessBufferTime.help" + }, + "BufferMethod": { + "name": "Optimization.WhenTaskQueueEmpty.name", + "stay_there": "stay_there", + "goto_main": "goto_main", + "close_game": "close_game" } }, "DropRecord": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index bbf4a5bb85..bad11253dc 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -519,6 +519,12 @@ "ProcessBufferTime": { "name": "模拟器关闭缓冲 X 分钟", "help": "当前时间距离下个任务小于 X 分钟时,alas将由关闭模拟器转为停在原处,能避免频繁启停模拟器\n当任务队列清空后关闭模拟器,此设置生效" + }, + "BufferMethod": { + "name": "缓冲时间内", + "stay_there": "停在原处", + "goto_main": "前往主界面", + "close_game": "关闭游戏" } }, "DropRecord": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 4c712d1163..fecdf23db0 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -519,6 +519,12 @@ "ProcessBufferTime": { "name": "模擬器關閉緩衝 X 分鐘", "help": "噹前時間距離下箇任務小於 X 分鐘時,alas將由關閉模擬器轉爲停在原處,能避免頻緐啓停模擬器\n噹任務隊列清空後關閉模擬器,此設置生傚" + }, + "BufferMethod": { + "name": "緩衝時間内", + "stay_there": "停在原處", + "goto_main": "前往主界面", + "close_game": "關閉遊戲" } }, "DropRecord": { diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index bfca1e246f..3767c00f23 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -18,7 +18,7 @@ class EmulatorStatus: process: tuple = None psproc: psutil.Process = psutil.Process() hwnds: list = None - focusedwindow: int = None + focusedwindow: tuple = None class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): @@ -37,6 +37,7 @@ def execute(self, command: str): else: arg = True self.process, self.focusedwindow = winapi.execute(command, arg) + logger.info(f"Current window: {self.focusedwindow[0]}") return True @classmethod @@ -266,8 +267,10 @@ def show_package(m): break # Flash window - if self.focusedwindow != winapi.getfocusedwindow(): - winapi.switchtothiswindow(self.focusedwindow) + currentwindow = winapi.getfocusedwindow() + if self.focusedwindow is not None and currentwindow is not None and self.focusedwindow != currentwindow: + logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") + winapi.setforegroundwindow(self.focusedwindow) # Check emulator process and hwnds self.hwnds = self.gethwnds(self.process[2]) @@ -311,7 +314,9 @@ def emulator_check(self): if self.emulator_instance.path in cmdline and self.psproc.is_running(): return True else: - return False + self.process = self.getprocess(self.emulator_instance) + self.psproc = psutil.Process(self.process[2]) + return True except ProcessLookupError as e: logger.warning(e) return False @@ -319,12 +324,14 @@ def emulator_check(self): return False except psutil.AccessDenied: return False + except IndexError: + return False except RuntimeError as e: logger.error(e) raise except Exception as e: logger.error(e) - return False + raise if __name__ == '__main__': diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index 38918ccd2f..ab63ed3e77 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -1,23 +1,23 @@ +import re +from sys import getwindowsversion + +import psutil from ctypes import ( byref, sizeof, WinError, POINTER, WINFUNCTYPE, WinDLL, Structure ) from ctypes.wintypes import ( - HANDLE, DWORD, WORD, BYTE, BOOL, LONG, CHAR, LPWSTR, - LPCWSTR, LPVOID, HWND, MAX_PATH, - LPARAM, RECT, PULONG + HANDLE, DWORD, WORD, BYTE, BOOL, INT, UINT, LONG, + CHAR, LPWSTR, LPCWSTR, LPVOID, HWND, MAX_PATH, + LPARAM, RECT, PULONG, POINT ) -from sys import getwindowsversion -import psutil -import re - from deploy.Windows.utils import DataProcessInfo from module.device.platform.emulator_windows import Emulator, EmulatorInstance from module.logger import logger -user32 = WinDLL('user32', use_last_error=True) -kernel32 = WinDLL('kernel32', use_last_error=True) +user32 = WinDLL(name='user32', use_last_error=True) +kernel32 = WinDLL(name='kernel32', use_last_error=True) class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... @@ -207,6 +207,16 @@ class PROCESSENTRY32(Structure): ("szExeFile", CHAR * MAX_PATH) ] +class WINDOWPLACEMENT(Structure): + _fields_ = [ + ("length", UINT), + ("flags", UINT), + ("showCmd", UINT), + ("ptMinPosition", POINT), + ("ptMaxPosition", POINT), + ("rcNormalPosition", RECT) + ] + CreateProcessW = kernel32.CreateProcessW CreateProcessW.argtypes = [ @@ -224,11 +234,19 @@ class PROCESSENTRY32(Structure): CreateProcessW.restype = BOOL GetForegroundWindow = user32.GetForegroundWindow -SwitchToThisWindow = user32.SwitchToThisWindow -SwitchToThisWindow.argtypes = [HWND, BOOL] -SwitchToThisWindow.restype = BOOL +GetForegroundWindow.restype = HWND +SetForegroundWindow = user32.SetForegroundWindow +SetForegroundWindow.argtypes = [HWND] +SetForegroundWindow.restype = BOOL + +GetWindowPlacement = user32.GetWindowPlacement +GetWindowPlacement.argtypes = [HWND, POINTER(WINDOWPLACEMENT)] +GetWindowPlacement.restype = BOOL ShowWindow = user32.ShowWindow +ShowWindow.argtypes = [HWND, INT] +ShowWindow.restype = BOOL + IsWindow = user32.IsWindow GetParent = user32.GetParent GetWindowRect = user32.GetWindowRect @@ -264,16 +282,26 @@ class PROCESSENTRY32(Structure): GetLastError.restype = BOOL -def getfocusedwindow() -> int: - return GetForegroundWindow() - -def switchtothiswindow(hwnd: int) -> bool: - SwitchToThisWindow(hwnd, True) - ShowWindow(hwnd, SW_RESTORE) +def getfocusedwindow(): + hwnd = GetForegroundWindow() + if not hwnd: + return None + wp = WINDOWPLACEMENT() + wp.length = sizeof(WINDOWPLACEMENT) + if GetWindowPlacement(hwnd, byref(wp)): + return hwnd, wp.showCmd + else: + return hwnd, SW_SHOWNORMAL + +def setforegroundwindow(focusedwindow: tuple = ()) -> bool: + if not focusedwindow: + return False + SetForegroundWindow(focusedwindow[0]) + ShowWindow(focusedwindow[0], focusedwindow[1]) return True -def execute(command: str, arg: bool): +def execute(command: str, arg: bool = False): from shlex import split from os.path import dirname lpApplicationName = split(command)[0] @@ -351,11 +379,9 @@ def callback(hwnd: int, lparam): EnumWindows(callback, 0) if not hwnds: - logger.critical( - "Hwnd not found! \n" - "1.Perhaps emulator was killed. \n" - "2.Environment has something wrong. Please check the running environment. " - ) + logger.critical("Hwnd not found!") + logger.critical("1.Perhaps emulator was killed.") + logger.critical("2.Environment has something wrong. Please check the running environment.") raise HwndNotFoundError("Hwnd not found") return hwnds @@ -388,10 +414,11 @@ def enumprocesses(): CloseHandle(snapshot) finally: del lppe32, snapshot + if 'errorcode' in locals(): + del errorcode def _getprocess(proc: psutil.Process): mainthreadid = proc.threads()[0].id - process: list = [None, None, proc.pid, mainthreadid] try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) if not processhandle: @@ -402,11 +429,10 @@ def _getprocess(proc: psutil.Process): CloseHandle(processhandle) raise WinError(GetLastError()) - process[0], process[1] = HANDLE(processhandle), HANDLE(threadhandle) - return tuple(process) + return HANDLE(processhandle), HANDLE(threadhandle), proc.pid, mainthreadid except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") - return tuple(process) + return None, None, proc.pid, mainthreadid def getprocess(instance: EmulatorInstance): for lppe32 in enumprocesses(): @@ -435,7 +461,7 @@ def getprocess(instance: EmulatorInstance): return _getprocess(proc) -def switchwindow(hwnds: list, arg: int): +def switchwindow(hwnds: list, arg: int = 1): for hwnd in hwnds: if not IsWindow(hwnd): continue From 7f11518aa509642a9ebd117341f644cc304fc2b5 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 25 Jun 2024 04:30:25 +0800 Subject: [PATCH 059/161] Upd: fix bug. --- module/config/i18n/en-US.json | 1 + module/config/i18n/ja-JP.json | 1 + module/config/i18n/zh-CN.json | 1 + module/config/i18n/zh-TW.json | 1 + module/device/platform/winapi.py | 49 ++++++++++++++++---------------- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 76ec12a8c4..5d061d44b0 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -522,6 +522,7 @@ }, "BufferMethod": { "name": "Behavior during buffer time", + "help": "", "stay_there": "stay_there", "goto_main": "goto_main", "close_game": "close_game" diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 55ebe0c1b0..8f94b827fe 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -522,6 +522,7 @@ }, "BufferMethod": { "name": "Optimization.WhenTaskQueueEmpty.name", + "help": "", "stay_there": "stay_there", "goto_main": "goto_main", "close_game": "close_game" diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 2a520aefc4..62ce20af53 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -522,6 +522,7 @@ }, "BufferMethod": { "name": "缓冲时间内", + "help": "", "stay_there": "停在原处", "goto_main": "前往主界面", "close_game": "关闭游戏" diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index fbafee823e..966799472e 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -522,6 +522,7 @@ }, "BufferMethod": { "name": "緩衝時間内", + "help": "", "stay_there": "停在原處", "goto_main": "前往主界面", "close_game": "關閉遊戲" diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py index ab63ed3e77..24bca42b7a 100644 --- a/module/device/platform/winapi.py +++ b/module/device/platform/winapi.py @@ -326,7 +326,7 @@ def execute(command: str, arg: bool = False): focusedwindow = getfocusedwindow() - success = CreateProcessW( + success = CreateProcessW( lpApplicationName, lpCommandLine, lpProcessAttributes, @@ -352,20 +352,6 @@ def execute(command: str, arg: bool = False): return process, focusedwindow -def kill_process_by_regex(regex: str) -> int: - count = 0 - - for proc in psutil.process_iter(): - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - if not re.search(regex, cmdline): - continue - logger.info(f'Kill emulator: {cmdline}') - proc.kill() - count += 1 - - return count - - def gethwnds(pid: int) -> list: hwnds = [] @@ -417,6 +403,22 @@ def enumprocesses(): if 'errorcode' in locals(): del errorcode +def kill_process_by_regex(regex: str) -> int: + count = 0 + + try: + for lppe32 in enumprocesses(): + proc = psutil.Process(lppe32.th32ProcessID) + cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + if not re.search(regex, cmdline): + continue + logger.info(f'Kill emulator: {cmdline}') + proc.kill() + count += 1 + except ProcessLookupError: + enumprocesses().throw(GeneratorExit) + return count + def _getprocess(proc: psutil.Process): mainthreadid = proc.threads()[0].id try: @@ -435,29 +437,26 @@ def _getprocess(proc: psutil.Process): return None, None, proc.pid, mainthreadid def getprocess(instance: EmulatorInstance): - for lppe32 in enumprocesses(): - try: - proc = psutil.Process(lppe32.th32ProcessID) - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - + processes = enumprocesses() + for lppe32 in processes: + proc = psutil.Process(lppe32.th32ProcessID) + cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline if not instance.path in cmdline: continue if instance == Emulator.MuMuPlayer12: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.MuMuPlayer12_id: - enumprocesses().close() + processes.close() return _getprocess(proc) elif instance == Emulator.LDPlayerFamily: match = re.search(r'\d+$', cmdline) if match and int(match.group()) == instance.LDPlayer_id: - enumprocesses().close() + processes.close() return _getprocess(proc) else: matchstr = re.search(fr'\b{instance.name}$', cmdline) if matchstr and matchstr.group() == instance.name: - enumprocesses().close() + processes.close() return _getprocess(proc) From 4f86e6fe180396e77105211a1406cf78ac7ae1ed Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 27 Jun 2024 06:04:10 +0800 Subject: [PATCH 060/161] Upd: devide winapi into apart. --- module/device/device.py | 6 +- module/device/platform/api_windows.py | 260 ++++++++++ .../device/platform/api_windows/__init__.py | 0 .../platform/api_windows/const_windows.py | 151 ++++++ .../platform/api_windows/functions_windows.py | 85 ++++ .../api_windows/structures_windows.py | 168 +++++++ module/device/platform/platform_windows.py | 40 +- module/device/platform/winapi.py | 474 ------------------ 8 files changed, 692 insertions(+), 492 deletions(-) create mode 100644 module/device/platform/api_windows.py create mode 100644 module/device/platform/api_windows/__init__.py create mode 100644 module/device/platform/api_windows/const_windows.py create mode 100644 module/device/platform/api_windows/functions_windows.py create mode 100644 module/device/platform/api_windows/structures_windows.py delete mode 100644 module/device/platform/winapi.py diff --git a/module/device/device.py b/module/device/device.py index a954178c09..9608189c40 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -345,12 +345,12 @@ def emulator_start(self): self.click_record_clear() def switchwindow(self): - from module.device.platform import winapi + from module.device.platform import api_windows method = self.config.Emulator_SilentStart if method == 'normal': - return super().switchwindow(winapi.SW_SHOW) + return super().switchwindow(api_windows.SW_SHOW) elif method == 'minimize': - return super().switchwindow(winapi.SW_MINIMIZE) + return super().switchwindow(api_windows.SW_MINIMIZE) elif method == 'silent': return True else: diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py new file mode 100644 index 0000000000..6e86da146a --- /dev/null +++ b/module/device/platform/api_windows.py @@ -0,0 +1,260 @@ +import re + +import psutil +from ctypes import byref, sizeof, WinError, cast, create_unicode_buffer, wstring_at, addressof +from ctypes.wintypes import SIZE + +from deploy.Windows.utils import DataProcessInfo +from module.device.platform.emulator_windows import Emulator, EmulatorInstance +from module.device.platform.api_windows.const_windows import * +from module.device.platform.api_windows.functions_windows import * +from module.device.platform.api_windows.structures_windows import * +from module.logger import logger + +def get_focused_window(): + hwnd = GetForegroundWindow() + if not hwnd: + return None + wp = WINDOWPLACEMENT() + wp.length = sizeof(WINDOWPLACEMENT) + if GetWindowPlacement(hwnd, byref(wp)): + return hwnd, wp + else: + errorcode = GetLastError() + logger.warning(f"GetWindowPlacement failed. GetLastError = {errorcode}") + return hwnd, None + +def set_foreground_window(focusedwindow: tuple = ()) -> bool: + if not focusedwindow: + return False + SetForegroundWindow(focusedwindow[0]) + if focusedwindow[2] is None: + ShowWindow(focusedwindow[0], SW_SHOWNORMAL) + else: + SetWindowPlacement(focusedwindow[0], focusedwindow[1]) + return True + + +def execute(command: str, arg: bool = False): + from shlex import split + from os.path import dirname + lpApplicationName = split(command)[0] + lpCommandLine = command + lpProcessAttributes = None + lpThreadAttributes = None + bInheritHandles = False + dwCreationFlags = ( + CREATE_NEW_CONSOLE | + NORMAL_PRIORITY_CLASS | + CREATE_NEW_PROCESS_GROUP | + CREATE_DEFAULT_ERROR_MODE | + CREATE_UNICODE_ENVIRONMENT + ) + lpEnvironment = None + lpCurrentDirectory = dirname(lpApplicationName) + lpStartupInfo = STARTUPINFO() + lpStartupInfo.cb = sizeof(STARTUPINFO) + lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW + lpStartupInfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE + lpProcessInformation = PROCESS_INFORMATION() + + focusedwindow = get_focused_window() + + success = CreateProcessW( + lpApplicationName, + lpCommandLine, + lpProcessAttributes, + lpThreadAttributes, + bInheritHandles, + dwCreationFlags, + lpEnvironment, + lpCurrentDirectory, + byref(lpStartupInfo), + byref(lpProcessInformation) + ) + + if not success: + errorcode = GetLastError() + raise EmulatorLaunchFailedError(f"Failed to start emulator. Error code: {errorcode}") + + process = ( + lpProcessInformation.hProcess, + lpProcessInformation.hThread, + lpProcessInformation.dwProcessId, + lpProcessInformation.dwThreadId + ) + return process, focusedwindow + + +def get_hwnds(pid: int) -> list: + hwnds = [] + + @EnumWindowsProc + def callback(hwnd: int, lparam): + processid = DWORD() + GetWindowThreadProcessId(hwnd, byref(processid)) + if processid.value == pid: + hwnds.append(hwnd) + return True + + EnumWindows(callback, 0) + if not hwnds: + logger.critical("Hwnd not found!") + logger.critical("1.Perhaps emulator was killed.") + logger.critical("2.Environment has something wrong. Please check the running environment.") + raise HwndNotFoundError("Hwnd not found") + return hwnds + + +def get_cmdline(pid: int) -> str: + hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) + if not hProcess: + raise WinError(GetLastError()) + + # Query process infomation + pbi = PROCESS_BASIC_INFORMATION() + returnlength = SIZE() + status = NtQueryInformationProcess( + hProcess, + 0, + byref(pbi), + sizeof(pbi), + byref(returnlength) + ) + if status != STATUS_SUCCESS: + logger.warning(f"NtQueryInformationProcess failed. Status = 0x{status}") + CloseHandle(hProcess) + raise WinError(GetLastError()) + + # Read PEB + peb = PEB() + if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): + errorcode = GetLastError() + logger.warning(f"ReadProcessMemory failed. GetLastError = {errorcode}") + CloseHandle(hProcess) + raise WinError(errorcode) + + # Read process parameters + upp = RTL_USER_PROCESS_PARAMETERS() + uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) + if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + errorcode = GetLastError() + logger.warning(f"ReadProcessMemory failed. GetLastError = {errorcode}") + CloseHandle(hProcess) + raise WinError(errorcode) + + # Read command line + commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) + if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): + errorcode = GetLastError() + logger.warning(f"ReadProcessMemory failed. GetLastError = {errorcode}") + CloseHandle(hProcess) + raise WinError(errorcode) + + cmdline = wstring_at(addressof(commandLine), len(commandLine)) + + CloseHandle(hProcess) + + return cmdline + + +def enum_processes(): + lppe32 = PROCESSENTRY32() + lppe32.dwSize = sizeof(PROCESSENTRY32) + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) + if snapshot == -1: + raise RuntimeError(f"Failed to create process snapshot. Errorcode: {GetLastError()}") + + if not Process32First(snapshot, byref(lppe32)): + CloseHandle(snapshot) + raise RuntimeError(f"Failed to get first process. Errorcode: {GetLastError()}") + + try: + while 1: + yield lppe32 + if Process32Next(snapshot, byref(lppe32)): + continue + # finished querying + errorcode = GetLastError() + CloseHandle(snapshot) + if errorcode != ERROR_NO_MORE_FILES: + # error code != ERROR_NO_MORE_FILES, means that win api failed + raise RuntimeError(f"Failed to get next process. Errorcode: {errorcode}") + # process not found + raise ProcessLookupError(f"Process not found. Errorcode: {errorcode}") + except GeneratorExit: + pass + finally: + CloseHandle(snapshot) + del lppe32, snapshot + +def kill_process_by_regex(regex: str) -> int: + count = 0 + + try: + for lppe32 in enum_processes(): + proc = psutil.Process(lppe32.th32ProcessID) + cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + if not re.search(regex, cmdline): + continue + logger.info(f'Kill emulator: {cmdline}') + proc.kill() + count += 1 + except ProcessLookupError: + enum_processes().throw(GeneratorExit) + return count + +def _get_process(pid: int): + proc = psutil.Process(pid) + mainthreadid = proc.threads()[0].id + try: + processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) + if not processhandle: + raise WinError(GetLastError()) + + threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) + if not threadhandle: + CloseHandle(processhandle) + raise WinError(GetLastError()) + + return processhandle, threadhandle, proc.pid, mainthreadid + except Exception as e: + logger.warning(f"Failed to get process and thread handles: {e}") + return None, None, proc.pid, mainthreadid + +def get_process(instance: EmulatorInstance): + processes = enum_processes() + for lppe32 in processes: + pid = lppe32.th32ProcessID + cmdline = getcmdline(pid) + if not instance.path in cmdline: + continue + if instance == Emulator.MuMuPlayer12: + match = re.search(r'\d+$', cmdline) + if match and int(match.group()) == instance.MuMuPlayer12_id: + processes.close() + return _get_process(pid) + elif instance == Emulator.LDPlayerFamily: + match = re.search(r'\d+$', cmdline) + if match and int(match.group()) == instance.LDPlayer_id: + processes.close() + return _get_process(pid) + else: + matchstr = re.search(fr'\b{instance.name}$', cmdline) + if matchstr and matchstr.group() == instance.name: + processes.close() + return _get_process(pid) + + +def switch_window(hwnds: list, arg: int = 1): + for hwnd in hwnds: + if not IsWindow(hwnd): + continue + if GetParent(hwnd): + continue + rect = RECT() + GetWindowRect(hwnd, byref(rect)) + if {rect.left, rect.top, rect.right, rect.bottom} == {0}: + continue + ShowWindow(hwnd, arg) + return True diff --git a/module/device/platform/api_windows/__init__.py b/module/device/platform/api_windows/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/module/device/platform/api_windows/const_windows.py b/module/device/platform/api_windows/const_windows.py new file mode 100644 index 0000000000..540f2bc609 --- /dev/null +++ b/module/device/platform/api_windows/const_windows.py @@ -0,0 +1,151 @@ +from sys import getwindowsversion + +# winnt.h line 3961 +PROCESS_TERMINATE = 0x0001 +PROCESS_CREATE_THREAD = 0x0002 +PROCESS_SET_SESSIONID = 0x0004 +PROCESS_VM_OPERATION = 0x0008 +PROCESS_VM_READ = 0x0010 +PROCESS_VM_WRITE = 0x0020 +PROCESS_DUP_HANDLE = 0x0040 +PROCESS_CREATE_PROCESS = 0x0080 +PROCESS_SET_QUOTA = 0x0100 +PROCESS_SET_INFORMATION = 0x0200 +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_SUSPEND_RESUME = 0x0800 +PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + +THREAD_TERMINATE = 0x0001 +THREAD_SUSPEND_RESUME = 0x0002 +THREAD_GET_CONTEXT = 0x0008 +THREAD_SET_CONTEXT = 0x0010 +THREAD_SET_INFORMATION = 0x0020 +THREAD_QUERY_INFORMATION = 0x0040 +THREAD_SET_THREAD_TOKEN = 0x0080 +THREAD_IMPERSONATE = 0x0100 +THREAD_DIRECT_IMPERSONATION = 0x0200 +THREAD_SET_LIMITED_INFORMATION = 0x0400 +THREAD_QUERY_LIMITED_INFORMATION = 0x0800 + +# winnt.h line 2809 +SYNCHRONIZE = 0x00100000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 + +VERSIONINFO = getwindowsversion() +MAJOR, MINOR, BUILD = VERSIONINFO.major, VERSIONINFO.minor, VERSIONINFO.build + +if (MAJOR > 6) or (MAJOR == 6 and MINOR >= 1): + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff + THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff +else: + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xfff + THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3ff + +MAXIMUM_PROC_PER_GROUP = 64 +MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP + +# error.h line 23 +ERROR_NO_MORE_FILES = 0x12 + +# tlhelp32.h line 17 +TH32CS_SNAPHEAPLIST = 0x00000001 +TH32CS_SNAPPROCESS = 0x00000002 +TH32CS_SNAPTHREAD = 0x00000004 +TH32CS_SNAPMODULE = 0x00000008 +TH32CS_SNAPMODULE32 = 0x00000010 +TH32CS_SNAPALL = ( + TH32CS_SNAPHEAPLIST | + TH32CS_SNAPPROCESS | + TH32CS_SNAPTHREAD | + TH32CS_SNAPMODULE +) +TH32CS_INHERIT = 0x80000000 + +# winbase.h line 1463 +STARTF_USESHOWWINDOW = 0x00000001 +STARTF_USESIZE = 0x00000002 +STARTF_USEPOSITION = 0x00000004 +STARTF_USECOUNTCHARS = 0x00000008 +STARTF_USEFILLATTRIBUTE = 0x00000010 +STARTF_RUNFULLSCREEN = 0x00000020 +STARTF_FORCEONFEEDBACK = 0x00000040 +STARTF_FORCEOFFFEEDBACK = 0x00000080 +STARTF_USESTDHANDLES = 0x00000100 + +STARTF_USEHOTKEY = 0x00000200 +STARTF_TITLEISLINKNAME = 0x00000800 +STARTF_TITLEISAPPID = 0x00001000 +STARTF_PREVENTPINNING = 0x00002000 + +# winuser.h line 200 +SW_HIDE = 0 +SW_SHOWNORMAL = 1 +SW_NORMAL = 1 +SW_SHOWMINIMIZED = 2 +SW_SHOWMAXIMIZED = 3 +SW_MAXIMIZE = 3 +SW_SHOWNOACTIVATE = 4 +SW_SHOW = 5 +SW_MINIMIZE = 6 +SW_SHOWMINNOACTIVE = 7 +SW_SHOWNA = 8 +SW_RESTORE = 9 +SW_SHOWDEFAULT = 10 +SW_FORCEMINIMIZE = 11 +SW_MAX = 11 + +# winbase.h line 377 +DEBUG_PROCESS = 0x00000001 +DEBUG_ONLY_THIS_PROCESS = 0x00000002 +CREATE_SUSPENDED = 0x00000004 +DETACHED_PROCESS = 0x00000008 + +CREATE_NEW_CONSOLE = 0x00000010 +NORMAL_PRIORITY_CLASS = 0x00000020 +IDLE_PRIORITY_CLASS = 0x00000040 +HIGH_PRIORITY_CLASS = 0x00000080 + +REALTIME_PRIORITY_CLASS = 0x00000100 +CREATE_NEW_PROCESS_GROUP = 0x00000200 +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +CREATE_SEPARATE_WOW_VDM = 0x00000800 + +CREATE_SHARED_WOW_VDM = 0x00001000 +CREATE_FORCEDOS = 0x00002000 +BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 +ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 + +INHERIT_PARENT_AFFINITY = 0x00010000 +INHERIT_CALLER_PRIORITY = 0x00020000 +CREATE_PROTECTED_PROCESS = 0x00040000 +EXTENDED_STARTUPINFO_PRESENT = 0x00080000 + +PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000 +PROCESS_MODE_BACKGROUND_END = 0x00200000 + +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 +CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000 +CREATE_DEFAULT_ERROR_MODE = 0x04000000 +CREATE_NO_WINDOW = 0x08000000 + +PROFILE_USER = 0x10000000 +PROFILE_KERNEL = 0x20000000 +PROFILE_SERVER = 0x40000000 +CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000 + +# subauth.h line 250 +STATUS_SUCCESS = 0x00000000 +STATUS_INVALID_INFO_CLASS = 0xC0000003 +STATUS_NO_SUCH_USER = 0xC0000064 +STATUS_WRONG_PASSWORD = 0xC000006A +STATUS_PASSWORD_RESTRICTION = 0xC000006C +STATUS_LOGON_FAILURE = 0xC000006D +STATUS_ACCOUNT_RESTRICTION = 0xC000006E +STATUS_INVALID_LOGON_HOURS = 0xC000006F +STATUS_INVALID_WORKSTATION = 0xC0000070 +STATUS_PASSWORD_EXPIRED = 0xC0000071 +STATUS_ACCOUNT_DISABLED = 0xC0000072 +STATUS_INSUFFICIENT_RESOURCES = 0xC000009A +STATUS_ACCOUNT_EXPIRED = 0xC0000193 +STATUS_PASSWORD_MUST_CHANGE = 0xC0000224 +STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234 diff --git a/module/device/platform/api_windows/functions_windows.py b/module/device/platform/api_windows/functions_windows.py new file mode 100644 index 0000000000..3d949e2786 --- /dev/null +++ b/module/device/platform/api_windows/functions_windows.py @@ -0,0 +1,85 @@ +from ctypes import POINTER, WINFUNCTYPE, WinDLL +from ctypes.wintypes import ( + HANDLE, DWORD, BOOL, INT, + LPWSTR, LPCWSTR, LPVOID, HWND, + LPARAM +) + +from module.device.platform.api_windows.structures_windows import ( + SECURITY_ATTRIBUTES, STARTUPINFO, WINDOWPLACEMENT, PROCESS_INFORMATION, PROCESSENTRY32 +) + +user32 = WinDLL(name='user32', use_last_error=True) +kernel32 = WinDLL(name='kernel32', use_last_error=True) +ntdll = WinDLL(name='ntdll', use_last_error=True) + +CreateProcessW = kernel32.CreateProcessW +CreateProcessW.argtypes = [ + LPCWSTR, #lpApplicationName + LPWSTR, #lpCommandLine + POINTER(SECURITY_ATTRIBUTES), #lpProcessAttributes + POINTER(SECURITY_ATTRIBUTES), #lpThreadAttributes + BOOL, #bInheritHandles + DWORD, #dwCreationFlags + LPVOID, #lpEnvironment + LPCWSTR, #lpCurrentDirectory + POINTER(STARTUPINFO), #lpStartupInfo + POINTER(PROCESS_INFORMATION) #lpProcessInformation +] +CreateProcessW.restype = BOOL + +GetForegroundWindow = user32.GetForegroundWindow +GetForegroundWindow.restype = HWND +SetForegroundWindow = user32.SetForegroundWindow +SetForegroundWindow.argtypes = [HWND] +SetForegroundWindow.restype = BOOL + +GetWindowPlacement = user32.GetWindowPlacement +GetWindowPlacement.argtypes = [HWND, POINTER(WINDOWPLACEMENT)] +GetWindowPlacement.restype = BOOL +SetWindowPlacement = user32.SetWindowPlacement +SetWindowPlacement.argtypes = [HWND, POINTER(WINDOWPLACEMENT)] +SetWindowPlacement.restype = BOOL + +ShowWindow = user32.ShowWindow +ShowWindow.argtypes = [HWND, INT] +ShowWindow.restype = BOOL + +IsWindow = user32.IsWindow +GetParent = user32.GetParent +GetWindowRect = user32.GetWindowRect + +EnumWindows = user32.EnumWindows +EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM) +GetWindowThreadProcessId = user32.GetWindowThreadProcessId +GetWindowThreadProcessId.argtypes = [HWND, POINTER(DWORD)] +GetWindowThreadProcessId.restype = DWORD + +OpenProcess = kernel32.OpenProcess +OpenProcess.argtypes = [DWORD, BOOL, DWORD] +OpenProcess.restype = HANDLE +OpenThread = kernel32.OpenThread +OpenThread.argtypes = [DWORD, BOOL, DWORD] +OpenThread.restype = HANDLE + +CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot +CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] +CreateToolhelp32Snapshot.restype = HANDLE + +CloseHandle = kernel32.CloseHandle +CloseHandle.argtypes = [HANDLE] +CloseHandle.restype = BOOL + +Process32First = kernel32.Process32First +Process32First.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] +Process32First.restype = BOOL + +Process32Next = kernel32.Process32Next +Process32Next.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] +Process32Next.restype = BOOL + +GetLastError = kernel32.GetLastError +GetLastError.restype = BOOL + +ReadProcessMemory = kernel32.ReadProcessMemory +NtQueryInformationProcess = ntdll.NtQueryInformationProcess diff --git a/module/device/platform/api_windows/structures_windows.py b/module/device/platform/api_windows/structures_windows.py new file mode 100644 index 0000000000..c9df91a5f7 --- /dev/null +++ b/module/device/platform/api_windows/structures_windows.py @@ -0,0 +1,168 @@ +from ctypes import POINTER, Structure +from ctypes.wintypes import ( + HANDLE, DWORD, WORD, LARGE_INTEGER, BYTE, BOOL, BOOLEAN, + USHORT, UINT, LONG, ULONG, CHAR, LPWSTR, LPVOID, MAX_PATH, + RECT, PULONG, POINT, PWCHAR +) + +class EmulatorLaunchFailedError(Exception): ... +class HwndNotFoundError(Exception): ... + +class STARTUPINFO(Structure): + _fields_ = [ + ('cb', DWORD), + ('lpReserved', LPWSTR), + ('lpDesktop', LPWSTR), + ('lpTitle', LPWSTR), + ('dwX', DWORD), + ('dwY', DWORD), + ('dwXSize', DWORD), + ('dwYSize', DWORD), + ('dwXCountChars', DWORD), + ('dwYCountChars', DWORD), + ('dwFillAttribute', DWORD), + ('dwFlags', DWORD), + ('wShowWindow', WORD), + ('cbReserved2', WORD), + ('lpReserved2', POINTER(BYTE)), + ('hStdInput', HANDLE), + ('hStdOutput', HANDLE), + ('hStdError', HANDLE) + ] + +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ('hProcess', HANDLE), + ('hThread', HANDLE), + ('dwProcessId', DWORD), + ('dwThreadId', DWORD) + ] + +class SECURITY_ATTRIBUTES(Structure): + _fields_ = [ + ("nLength", DWORD), + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL) + ] + +class PROCESSENTRY32(Structure): + _fields_ = [ + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ProcessID", DWORD), + ("th32DefaultHeapID", PULONG), + ("th32ModuleID", DWORD), + ("cntThreads", DWORD), + ("th32ParentProcessID", DWORD), + ("pcPriClassBase", LONG), + ("dwFlags", DWORD), + ("szExeFile", CHAR * MAX_PATH) + ] + +class WINDOWPLACEMENT(Structure): + _fields_ = [ + ("length", UINT), + ("flags", UINT), + ("showCmd", UINT), + ("ptMinPosition", POINT), + ("ptMaxPosition", POINT), + ("rcNormalPosition", RECT) + ] + +class LIST_ENTRY(Structure): + _fields_ = [ + ("Flink", POINTER(LPVOID)), + ("Blink", POINTER(LPVOID)) + ] + +class UNICODE_STRING(Structure): + _fields_ = [ + ("Length", USHORT), + ("MaximumLength", USHORT), + ("Buffer", PWCHAR) + ] + +class PEB_LDR_DATA(Structure): + _fields_ = [ + ("Length", ULONG), + ("Initialized", BOOLEAN), + ("SsHandle", HANDLE), + ("InLoadOrderModuleList", LIST_ENTRY), + ("InMemoryOrderModuleList", LIST_ENTRY), + ("InInitializationOrderModuleList", LIST_ENTRY) + ] + +class PEB(Structure): + _fields_ = [ + ("InheritedAddressSpace", BOOLEAN), + ("ReadImageFileExecOptions", BOOLEAN), + ("BeingDebugged", BOOLEAN), + ("Spare", BOOLEAN), + ("Mutant", HANDLE), + ("ImageBaseAddress", LPVOID), + ("Ldr", POINTER(PEB_LDR_DATA)), + ("ProcessParameters", LPVOID), + ("SubSystemData", LPVOID), + ("ProcessHeap", LPVOID), + ("FastPebLock", LPVOID), + ("FastPebLockRoutine", LPVOID), + ("FastPebUnlockRoutine", LPVOID), + ("EnvironmentUpdateCount", ULONG), + ("KernelCallbackTable", LPVOID), + ("EventLogSection", LPVOID), + ("EventLog", LPVOID), + ("FreeList", LPVOID), + ("TlsExpansionCounter", ULONG), + ("TlsBitmap", LPVOID), + ("TlsBitmapBits", ULONG * 2), + ("ReadOnlySharedMemoryBase", LPVOID), + ("ReadOnlySharedMemoryHeap", LPVOID), + ("ReadOnlyStaticServerData", LPVOID), + ("AnsiCodePageData", LPVOID), + ("OemCodePageData", LPVOID), + ("UnicodeCaseTableData", LPVOID), + ("NumberOfProcessors", ULONG), + ("NtGlobalFlag", ULONG), + ("Spare2", BYTE * 4), + ("CriticalSectionTimeout", LARGE_INTEGER), + ("HeapSegmentReserve", ULONG), + ("HeapSegmentCommit", ULONG), + ("HeapDeCommitTotalFreeThreshold", ULONG), + ("HeapDeCommitFreeBlockThreshold", ULONG), + ("NumberOfHeaps", ULONG), + ("MaximumNumberOfHeaps", ULONG), + ("ProcessHeaps", POINTER(LPVOID)), + ("GdiSharedHandleTable", LPVOID), + ("ProcessStarterHelper", LPVOID), + ("GdiDCAttributeList", LPVOID), + ("LoaderLock", LPVOID), + ("OSMajorVersion", ULONG), + ("OSMinorVersion", ULONG), + ("OSBuildNumber", ULONG), + ("OSPlatformId", ULONG), + ("ImageSubSystem", ULONG), + ("ImageSubSystemMajorVersion", ULONG), + ("ImageSubSystemMinorVersion", ULONG), + ("GdiHandleBuffer", ULONG * 34), + ("PostProcessInitRoutine", ULONG), + ("TlsExpansionBitmap", ULONG), + ("TlsExpansionBitmapBits", BYTE * 32), + ("SessionId", ULONG) + ] + +class RTL_USER_PROCESS_PARAMETERS(Structure): + _fields_ = [ + ("Reserved1", BYTE * 16), + ("Reserved2", LPVOID * 10), + ("ImagePathName", UNICODE_STRING), + ("CommandLine", UNICODE_STRING) + ] + +class PROCESS_BASIC_INFORMATION(Structure): + _fields_ = [ + ("Reserved1", LPVOID), + ("PebBaseAddress", POINTER(PEB)), + ("Reserved2", LPVOID * 2), + ("UniqueProcessId", ULONG), + ("Reserved3", LPVOID) + ] \ No newline at end of file diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 3767c00f23..3c168342f7 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -6,7 +6,7 @@ from module.device.connection import AdbDeviceWithStatus from module.device.platform.platform_base import PlatformBase from module.device.platform.emulator_windows import Emulator, EmulatorInstance, EmulatorManager -from module.device.platform import winapi +from module.device.platform import api_windows from module.logger import logger @@ -36,7 +36,7 @@ def execute(self, command: str): arg = False else: arg = True - self.process, self.focusedwindow = winapi.execute(command, arg) + self.process, self.focusedwindow = api_windows.execute(command, arg) logger.info(f"Current window: {self.focusedwindow[0]}") return True @@ -51,20 +51,20 @@ def kill_process_by_regex(cls, regex: str) -> int: Returns: int: Number of processes killed """ - return winapi.kill_process_by_regex(regex) + return api_windows.kill_process_by_regex(regex) @staticmethod - def gethwnds(pid: int) -> list: - return winapi.gethwnds(pid) + def get_hwnds(pid: int) -> list: + return api_windows.get_hwnds(pid) @staticmethod - def getprocess(instance: EmulatorInstance): - return winapi.getprocess(instance) + def get_process(instance: EmulatorInstance): + return api_windows.get_process(instance) - def switchwindow(self, arg: int): + def switch_window(self, arg: int): if self.process is None: return - return winapi.switchwindow(self.hwnds, arg) + return api_windows.switch_window(self.hwnds, arg) def _emulator_start(self, instance: EmulatorInstance): """ @@ -267,13 +267,13 @@ def show_package(m): break # Flash window - currentwindow = winapi.getfocusedwindow() + currentwindow = api_windows.get_focused_window() if self.focusedwindow is not None and currentwindow is not None and self.focusedwindow != currentwindow: logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") - winapi.setforegroundwindow(self.focusedwindow) + api_windows.set_foreground_window(self.focusedwindow) # Check emulator process and hwnds - self.hwnds = self.gethwnds(self.process[2]) + self.hwnds = self.get_hwnds(self.process[2]) self.psproc = psutil.Process(self.process[2]) logger.info(f'Emulator start completed') @@ -301,12 +301,22 @@ def emulator_start(self): def emulator_stop(self): logger.hr('Emulator stop', level=1) - return self._emulator_function_wrapper(self._emulator_stop) + for _ in range(3): + # Stop + if self._emulator_function_wrapper(self._emulator_stop): + # Success + return True + else: + # Failed to stop, start and stop again + if self._emulator_function_wrapper(self._emulator_start): + continue + else: + return False def emulator_check(self): try: if self.process is None: - self.process = self.getprocess(self.emulator_instance) + self.process = self.get_process(self.emulator_instance) return True if self.psproc.pid != self.process[2]: self.psproc = psutil.Process(self.process[2]) @@ -314,7 +324,7 @@ def emulator_check(self): if self.emulator_instance.path in cmdline and self.psproc.is_running(): return True else: - self.process = self.getprocess(self.emulator_instance) + self.process = self.get_process(self.emulator_instance) self.psproc = psutil.Process(self.process[2]) return True except ProcessLookupError as e: diff --git a/module/device/platform/winapi.py b/module/device/platform/winapi.py deleted file mode 100644 index 24bca42b7a..0000000000 --- a/module/device/platform/winapi.py +++ /dev/null @@ -1,474 +0,0 @@ -import re -from sys import getwindowsversion - -import psutil -from ctypes import ( - byref, sizeof, WinError, POINTER, WINFUNCTYPE, - WinDLL, Structure -) -from ctypes.wintypes import ( - HANDLE, DWORD, WORD, BYTE, BOOL, INT, UINT, LONG, - CHAR, LPWSTR, LPCWSTR, LPVOID, HWND, MAX_PATH, - LPARAM, RECT, PULONG, POINT -) - -from deploy.Windows.utils import DataProcessInfo -from module.device.platform.emulator_windows import Emulator, EmulatorInstance -from module.logger import logger - -user32 = WinDLL(name='user32', use_last_error=True) -kernel32 = WinDLL(name='kernel32', use_last_error=True) - -class EmulatorLaunchFailedError(Exception): ... -class HwndNotFoundError(Exception): ... - -# winnt.h line 3961 -PROCESS_TERMINATE = 0x0001 -PROCESS_CREATE_THREAD = 0x0002 -PROCESS_SET_SESSIONID = 0x0004 -PROCESS_VM_OPERATION = 0x0008 -PROCESS_VM_READ = 0x0010 -PROCESS_VM_WRITE = 0x0020 -PROCESS_DUP_HANDLE = 0x0040 -PROCESS_CREATE_PROCESS = 0x0080 -PROCESS_SET_QUOTA = 0x0100 -PROCESS_SET_INFORMATION = 0x0200 -PROCESS_QUERY_INFORMATION = 0x0400 -PROCESS_SUSPEND_RESUME = 0x0800 -PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 - -THREAD_TERMINATE = 0x0001 -THREAD_SUSPEND_RESUME = 0x0002 -THREAD_GET_CONTEXT = 0x0008 -THREAD_SET_CONTEXT = 0x0010 -THREAD_SET_INFORMATION = 0x0020 -THREAD_QUERY_INFORMATION = 0x0040 -THREAD_SET_THREAD_TOKEN = 0x0080 -THREAD_IMPERSONATE = 0x0100 -THREAD_DIRECT_IMPERSONATION = 0x0200 -THREAD_SET_LIMITED_INFORMATION = 0x0400 -THREAD_QUERY_LIMITED_INFORMATION = 0x0800 - -# winnt.h line 2809 -SYNCHRONIZE = 0x00100000 -STANDARD_RIGHTS_REQUIRED = 0x000F0000 - -VERSIONINFO = getwindowsversion() -MAJOR, MINOR, BUILD = VERSIONINFO.major, VERSIONINFO.minor, VERSIONINFO.build - -if (MAJOR > 6) or (MAJOR == 6 and MINOR >= 1): - PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff - THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff -else: - PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xfff - THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3ff - -MAXIMUM_PROC_PER_GROUP = 64 -MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP - -# error.h line 23 -ERROR_NO_MORE_FILES = 0x12 - -# tlhelp32.h line 17 -TH32CS_SNAPHEAPLIST = 0x00000001 -TH32CS_SNAPPROCESS = 0x00000002 -TH32CS_SNAPTHREAD = 0x00000004 -TH32CS_SNAPMODULE = 0x00000008 -TH32CS_SNAPMODULE32 = 0x00000010 -TH32CS_SNAPALL = ( - TH32CS_SNAPHEAPLIST | - TH32CS_SNAPPROCESS | - TH32CS_SNAPTHREAD | - TH32CS_SNAPMODULE -) -TH32CS_INHERIT = 0x80000000 - -# winbase.h line 1463 -STARTF_USESHOWWINDOW = 0x00000001 -STARTF_USESIZE = 0x00000002 -STARTF_USEPOSITION = 0x00000004 -STARTF_USECOUNTCHARS = 0x00000008 -STARTF_USEFILLATTRIBUTE = 0x00000010 -STARTF_RUNFULLSCREEN = 0x00000020 -STARTF_FORCEONFEEDBACK = 0x00000040 -STARTF_FORCEOFFFEEDBACK = 0x00000080 -STARTF_USESTDHANDLES = 0x00000100 - -STARTF_USEHOTKEY = 0x00000200 -STARTF_TITLEISLINKNAME = 0x00000800 -STARTF_TITLEISAPPID = 0x00001000 -STARTF_PREVENTPINNING = 0x00002000 - -# winuser.h line 200 -SW_HIDE = 0 -SW_SHOWNORMAL = 1 -SW_NORMAL = 1 -SW_SHOWMINIMIZED = 2 -SW_SHOWMAXIMIZED = 3 -SW_MAXIMIZE = 3 -SW_SHOWNOACTIVATE = 4 -SW_SHOW = 5 -SW_MINIMIZE = 6 -SW_SHOWMINNOACTIVE = 7 -SW_SHOWNA = 8 -SW_RESTORE = 9 -SW_SHOWDEFAULT = 10 -SW_FORCEMINIMIZE = 11 -SW_MAX = 11 - -# winbase.h line 377 -DEBUG_PROCESS = 0x00000001 -DEBUG_ONLY_THIS_PROCESS = 0x00000002 -CREATE_SUSPENDED = 0x00000004 -DETACHED_PROCESS = 0x00000008 - -CREATE_NEW_CONSOLE = 0x00000010 -NORMAL_PRIORITY_CLASS = 0x00000020 -IDLE_PRIORITY_CLASS = 0x00000040 -HIGH_PRIORITY_CLASS = 0x00000080 - -REALTIME_PRIORITY_CLASS = 0x00000100 -CREATE_NEW_PROCESS_GROUP = 0x00000200 -CREATE_UNICODE_ENVIRONMENT = 0x00000400 -CREATE_SEPARATE_WOW_VDM = 0x00000800 - -CREATE_SHARED_WOW_VDM = 0x00001000 -CREATE_FORCEDOS = 0x00002000 -BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 -ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 - -INHERIT_PARENT_AFFINITY = 0x00010000 -INHERIT_CALLER_PRIORITY = 0x00020000 -CREATE_PROTECTED_PROCESS = 0x00040000 -EXTENDED_STARTUPINFO_PRESENT = 0x00080000 - -PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000 -PROCESS_MODE_BACKGROUND_END = 0x00200000 - -CREATE_BREAKAWAY_FROM_JOB = 0x01000000 -CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000 -CREATE_DEFAULT_ERROR_MODE = 0x04000000 -CREATE_NO_WINDOW = 0x08000000 - -PROFILE_USER = 0x10000000 -PROFILE_KERNEL = 0x20000000 -PROFILE_SERVER = 0x40000000 -CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000 - - -class STARTUPINFO(Structure): - _fields_ = [ - ('cb', DWORD), - ('lpReserved', LPWSTR), - ('lpDesktop', LPWSTR), - ('lpTitle', LPWSTR), - ('dwX', DWORD), - ('dwY', DWORD), - ('dwXSize', DWORD), - ('dwYSize', DWORD), - ('dwXCountChars', DWORD), - ('dwYCountChars', DWORD), - ('dwFillAttribute', DWORD), - ('dwFlags', DWORD), - ('wShowWindow', WORD), - ('cbReserved2', WORD), - ('lpReserved2', POINTER(BYTE)), - ('hStdInput', HANDLE), - ('hStdOutput', HANDLE), - ('hStdError', HANDLE) - ] - -class PROCESSINFORMATION(Structure): - _fields_ = [ - ('hProcess', HANDLE), - ('hThread', HANDLE), - ('dwProcessId', DWORD), - ('dwThreadId', DWORD) - ] - -class SECURITYATTRIBUTES(Structure): - _fields_ = [ - ("nLength", DWORD), - ("lpSecurityDescriptor", LPVOID), - ("bInheritHandle", BOOL) - ] - -class PROCESSENTRY32(Structure): - _fields_ = [ - ("dwSize", DWORD), - ("cntUsage", DWORD), - ("th32ProcessID", DWORD), - ("th32DefaultHeapID", PULONG), - ("th32ModuleID", DWORD), - ("cntThreads", DWORD), - ("th32ParentProcessID", DWORD), - ("pcPriClassBase", LONG), - ("dwFlags", DWORD), - ("szExeFile", CHAR * MAX_PATH) - ] - -class WINDOWPLACEMENT(Structure): - _fields_ = [ - ("length", UINT), - ("flags", UINT), - ("showCmd", UINT), - ("ptMinPosition", POINT), - ("ptMaxPosition", POINT), - ("rcNormalPosition", RECT) - ] - - -CreateProcessW = kernel32.CreateProcessW -CreateProcessW.argtypes = [ - LPCWSTR, #lpApplicationName - LPWSTR, #lpCommandLine - POINTER(SECURITYATTRIBUTES), #lpProcessAttributes - POINTER(SECURITYATTRIBUTES), #lpThreadAttributes - BOOL, #bInheritHandles - DWORD, #dwCreationFlags - LPVOID, #lpEnvironment - LPCWSTR, #lpCurrentDirectory - POINTER(STARTUPINFO), #lpStartupInfo - POINTER(PROCESSINFORMATION) #lpProcessInformation -] -CreateProcessW.restype = BOOL - -GetForegroundWindow = user32.GetForegroundWindow -GetForegroundWindow.restype = HWND -SetForegroundWindow = user32.SetForegroundWindow -SetForegroundWindow.argtypes = [HWND] -SetForegroundWindow.restype = BOOL - -GetWindowPlacement = user32.GetWindowPlacement -GetWindowPlacement.argtypes = [HWND, POINTER(WINDOWPLACEMENT)] -GetWindowPlacement.restype = BOOL - -ShowWindow = user32.ShowWindow -ShowWindow.argtypes = [HWND, INT] -ShowWindow.restype = BOOL - -IsWindow = user32.IsWindow -GetParent = user32.GetParent -GetWindowRect = user32.GetWindowRect - -EnumWindows = user32.EnumWindows -EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM) -GetWindowThreadProcessId = user32.GetWindowThreadProcessId -GetWindowThreadProcessId.argtypes = [HWND, POINTER(DWORD)] -GetWindowThreadProcessId.restype = DWORD - -OpenProcess = kernel32.OpenProcess -OpenProcess.argtypes = [DWORD, BOOL, DWORD] -OpenProcess.restype = HANDLE -OpenThread = kernel32.OpenThread - -CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot -CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] -CreateToolhelp32Snapshot.restype = HANDLE - -CloseHandle = kernel32.CloseHandle -CloseHandle.argtypes = [HANDLE] -CloseHandle.restype = BOOL - -Process32First = kernel32.Process32First -Process32First.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] -Process32First.restype = BOOL - -Process32Next = kernel32.Process32Next -Process32Next.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] -Process32Next.restype = BOOL - -GetLastError = kernel32.GetLastError -GetLastError.restype = BOOL - - -def getfocusedwindow(): - hwnd = GetForegroundWindow() - if not hwnd: - return None - wp = WINDOWPLACEMENT() - wp.length = sizeof(WINDOWPLACEMENT) - if GetWindowPlacement(hwnd, byref(wp)): - return hwnd, wp.showCmd - else: - return hwnd, SW_SHOWNORMAL - -def setforegroundwindow(focusedwindow: tuple = ()) -> bool: - if not focusedwindow: - return False - SetForegroundWindow(focusedwindow[0]) - ShowWindow(focusedwindow[0], focusedwindow[1]) - return True - - -def execute(command: str, arg: bool = False): - from shlex import split - from os.path import dirname - lpApplicationName = split(command)[0] - lpCommandLine = command - lpProcessAttributes = None - lpThreadAttributes = None - bInheritHandles = False - dwCreationFlags = ( - CREATE_NEW_CONSOLE | - NORMAL_PRIORITY_CLASS | - CREATE_NEW_PROCESS_GROUP | - CREATE_DEFAULT_ERROR_MODE | - CREATE_UNICODE_ENVIRONMENT - ) - lpEnvironment = None - lpCurrentDirectory = dirname(lpApplicationName) - lpStartupInfo = STARTUPINFO() - lpStartupInfo.cb = sizeof(STARTUPINFO) - lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW - lpStartupInfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE - lpProcessInformation = PROCESSINFORMATION() - - focusedwindow = getfocusedwindow() - - success = CreateProcessW( - lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - bInheritHandles, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - byref(lpStartupInfo), - byref(lpProcessInformation) - ) - - if not success: - errorcode = GetLastError() - raise EmulatorLaunchFailedError(f"Failed to start emulator. Error code: {errorcode}") - - process = ( - HANDLE(lpProcessInformation.hProcess), - HANDLE(lpProcessInformation.hThread), - lpProcessInformation.dwProcessId, - lpProcessInformation.dwThreadId - ) - return process, focusedwindow - - -def gethwnds(pid: int) -> list: - hwnds = [] - - @EnumWindowsProc - def callback(hwnd: int, lparam): - processid = DWORD() - GetWindowThreadProcessId(hwnd, byref(processid)) - if processid.value == pid: - hwnds.append(hwnd) - return True - - EnumWindows(callback, 0) - if not hwnds: - logger.critical("Hwnd not found!") - logger.critical("1.Perhaps emulator was killed.") - logger.critical("2.Environment has something wrong. Please check the running environment.") - raise HwndNotFoundError("Hwnd not found") - return hwnds - - -def enumprocesses(): - lppe32 = PROCESSENTRY32() - lppe32.dwSize = sizeof(PROCESSENTRY32) - snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) - if snapshot == -1: - raise RuntimeError(f"Failed to create process snapshot. Errorcode: {GetLastError()}") - - if not Process32First(snapshot, byref(lppe32)): - CloseHandle(snapshot) - raise RuntimeError(f"Failed to get first process. Errorcode: {GetLastError()}") - - try: - while 1: - yield lppe32 - if Process32Next(snapshot, byref(lppe32)): - continue - # finished querying - errorcode = GetLastError() - CloseHandle(snapshot) - if errorcode != ERROR_NO_MORE_FILES: - # error code != ERROR_NO_MORE_FILES, means that win api failed - raise RuntimeError(f"Failed to get next process. Errorcode: {errorcode}") - # process not found - raise ProcessLookupError(f"Process not found. Errorcode: {errorcode}") - except GeneratorExit: - CloseHandle(snapshot) - finally: - del lppe32, snapshot - if 'errorcode' in locals(): - del errorcode - -def kill_process_by_regex(regex: str) -> int: - count = 0 - - try: - for lppe32 in enumprocesses(): - proc = psutil.Process(lppe32.th32ProcessID) - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - if not re.search(regex, cmdline): - continue - logger.info(f'Kill emulator: {cmdline}') - proc.kill() - count += 1 - except ProcessLookupError: - enumprocesses().throw(GeneratorExit) - return count - -def _getprocess(proc: psutil.Process): - mainthreadid = proc.threads()[0].id - try: - processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) - if not processhandle: - raise WinError(GetLastError()) - - threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) - if not threadhandle: - CloseHandle(processhandle) - raise WinError(GetLastError()) - - return HANDLE(processhandle), HANDLE(threadhandle), proc.pid, mainthreadid - except Exception as e: - logger.warning(f"Failed to get process and thread handles: {e}") - return None, None, proc.pid, mainthreadid - -def getprocess(instance: EmulatorInstance): - processes = enumprocesses() - for lppe32 in processes: - proc = psutil.Process(lppe32.th32ProcessID) - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - if not instance.path in cmdline: - continue - if instance == Emulator.MuMuPlayer12: - match = re.search(r'\d+$', cmdline) - if match and int(match.group()) == instance.MuMuPlayer12_id: - processes.close() - return _getprocess(proc) - elif instance == Emulator.LDPlayerFamily: - match = re.search(r'\d+$', cmdline) - if match and int(match.group()) == instance.LDPlayer_id: - processes.close() - return _getprocess(proc) - else: - matchstr = re.search(fr'\b{instance.name}$', cmdline) - if matchstr and matchstr.group() == instance.name: - processes.close() - return _getprocess(proc) - - -def switchwindow(hwnds: list, arg: int = 1): - for hwnd in hwnds: - if not IsWindow(hwnd): - continue - if GetParent(hwnd): - continue - rect = RECT() - GetWindowRect(hwnd, byref(rect)) - if {rect.left, rect.top, rect.right, rect.bottom} == {0}: - continue - ShowWindow(hwnd, arg) - return True From 32a4ea1fd9b8a3a728374000615935ca2dc06eaa Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 27 Jun 2024 06:05:56 +0800 Subject: [PATCH 061/161] Fix. --- module/device/platform/api_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 6e86da146a..7fbbcefe6f 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -226,7 +226,7 @@ def get_process(instance: EmulatorInstance): processes = enum_processes() for lppe32 in processes: pid = lppe32.th32ProcessID - cmdline = getcmdline(pid) + cmdline = get_cmdline(pid) if not instance.path in cmdline: continue if instance == Emulator.MuMuPlayer12: From 0d28cd782915c75366b7516120a5b9fb15a62d3f Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 27 Jun 2024 20:36:42 +0800 Subject: [PATCH 062/161] Upd: Add terminate process. --- module/device/platform/api_windows.py | 226 +++++++++++++----- module/device/platform/platform_windows.py | 59 ++--- .../{api_windows => winapi}/__init__.py | 0 .../{api_windows => winapi}/const_windows.py | 0 .../functions_windows.py | 15 +- .../structures_windows.py | 10 +- 6 files changed, 204 insertions(+), 106 deletions(-) rename module/device/platform/{api_windows => winapi}/__init__.py (100%) rename module/device/platform/{api_windows => winapi}/const_windows.py (100%) rename module/device/platform/{api_windows => winapi}/functions_windows.py (88%) rename module/device/platform/{api_windows => winapi}/structures_windows.py (96%) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 7fbbcefe6f..518a8bdadd 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,34 +1,67 @@ import re import psutil -from ctypes import byref, sizeof, WinError, cast, create_unicode_buffer, wstring_at, addressof +from ctypes import byref, sizeof, cast, create_unicode_buffer, wstring_at, addressof from ctypes.wintypes import SIZE -from deploy.Windows.utils import DataProcessInfo from module.device.platform.emulator_windows import Emulator, EmulatorInstance -from module.device.platform.api_windows.const_windows import * -from module.device.platform.api_windows.functions_windows import * -from module.device.platform.api_windows.structures_windows import * +from module.device.platform.winapi.const_windows import * +from module.device.platform.winapi.functions_windows import * +from module.device.platform.winapi.structures_windows import * from module.logger import logger -def get_focused_window(): + +def _error(errstr: str = '', handle: int = 0, exception: type = OSError, raiseexcept: bool = True): + """ + Raise exception. + + Args: + errstr (str): Error message + handle (int): Handle to close + exception (Type[Exception]): Exception class to raise + raiseexcept (bool): Flag indicating whether to raise the exception + """ + errorcode = GetLastError() + logger.warning(f"{errstr}Errorcode: {errorcode}") + if not handle: + CloseHandle(handle) + if raiseexcept: + raise exception(errorcode) + + +def getfocusedwindow(): + """ + Get focused window. + + Returns: + hwnd (int): Focused window hwnd + WINDOWPLACEMENT: + """ hwnd = GetForegroundWindow() if not hwnd: - return None + return 0, None wp = WINDOWPLACEMENT() wp.length = sizeof(WINDOWPLACEMENT) if GetWindowPlacement(hwnd, byref(wp)): return hwnd, wp else: - errorcode = GetLastError() - logger.warning(f"GetWindowPlacement failed. GetLastError = {errorcode}") + _error(errstr="Failed to get windowplacement. ", raiseexcept=False) return hwnd, None -def set_foreground_window(focusedwindow: tuple = ()) -> bool: +def setforegroundwindow(focusedwindow: tuple = ()) -> bool: + """ + Refocus foreground window. + + Args: + focusedwindow: tuple(hwnd, WINDOWPLACEMENT) | tuple(hwnd, None) + + Returns: + bool: + """ if not focusedwindow: return False SetForegroundWindow(focusedwindow[0]) - if focusedwindow[2] is None: + if focusedwindow[1] is None: ShowWindow(focusedwindow[0], SW_SHOWNORMAL) else: SetWindowPlacement(focusedwindow[0], focusedwindow[1]) @@ -36,6 +69,19 @@ def set_foreground_window(focusedwindow: tuple = ()) -> bool: def execute(command: str, arg: bool = False): + """ + Create a new process. + + Args: + command (str): process's commandline + arg (bool): process's windowplacement + Example: + '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' + + Returns: + process: tuple(processhandle, threadhandle, processid, mainthreadid), + focusedwindow: tuple(hwnd, WINDOWPLACEMENT) + """ from shlex import split from os.path import dirname lpApplicationName = split(command)[0] @@ -58,7 +104,7 @@ def execute(command: str, arg: bool = False): lpStartupInfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE lpProcessInformation = PROCESS_INFORMATION() - focusedwindow = get_focused_window() + focusedwindow = getfocusedwindow() success = CreateProcessW( lpApplicationName, @@ -74,8 +120,7 @@ def execute(command: str, arg: bool = False): ) if not success: - errorcode = GetLastError() - raise EmulatorLaunchFailedError(f"Failed to start emulator. Error code: {errorcode}") + _error(errstr="Failed to start emulator. ", exception=EmulatorLaunchFailedError) process = ( lpProcessInformation.hProcess, @@ -86,7 +131,30 @@ def execute(command: str, arg: bool = False): return process, focusedwindow +def terminate_process(pid: int): + """ + Terminate emulator process. + + Args: + pid (int): Emulator's pid + """ + hProcess = OpenProcess(PROCESS_TERMINATE, False, pid) + if TerminateProcess(hProcess, 0) == 0: + _error("Failed to kill process. ", hProcess) + CloseHandle(hProcess) + return True + + def get_hwnds(pid: int) -> list: + """ + Get process's window hwnds from this processid. + + Args: + pid (int): Emulator's pid + + Returns: + hwnds (list): Emulator's possible window hwnds + """ hwnds = [] @EnumWindowsProc @@ -99,75 +167,74 @@ def callback(hwnd: int, lparam): EnumWindows(callback, 0) if not hwnds: - logger.critical("Hwnd not found!") - logger.critical("1.Perhaps emulator was killed.") - logger.critical("2.Environment has something wrong. Please check the running environment.") - raise HwndNotFoundError("Hwnd not found") + logger.error("Hwnd not found!") + logger.error("1.Perhaps emulator was killed.") + logger.error("2.Environment has something wrong. Please check the running environment.") + _error(errstr="Hwnd not found. ", exception=HwndNotFoundError) return hwnds def get_cmdline(pid: int) -> str: + """ + Get a process's command line from this processid. + + Args: + pid (int): Emulator's pid + + Returns: + command line (str): process's command line + Example: + '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' + """ hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not hProcess: - raise WinError(GetLastError()) + _error("OpenProcess failed. ") # Query process infomation pbi = PROCESS_BASIC_INFORMATION() returnlength = SIZE() - status = NtQueryInformationProcess( - hProcess, - 0, - byref(pbi), - sizeof(pbi), - byref(returnlength) - ) + status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: - logger.warning(f"NtQueryInformationProcess failed. Status = 0x{status}") - CloseHandle(hProcess) - raise WinError(GetLastError()) + _error(f"NtQueryInformationProcess failed. Status: 0x{status}. ", hProcess) # Read PEB peb = PEB() if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - errorcode = GetLastError() - logger.warning(f"ReadProcessMemory failed. GetLastError = {errorcode}") - CloseHandle(hProcess) - raise WinError(errorcode) + _error("ReadProcessMemory failed. ", hProcess) # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - errorcode = GetLastError() - logger.warning(f"ReadProcessMemory failed. GetLastError = {errorcode}") - CloseHandle(hProcess) - raise WinError(errorcode) + _error("ReadProcessMemory failed. ", hProcess) # Read command line commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - errorcode = GetLastError() - logger.warning(f"ReadProcessMemory failed. GetLastError = {errorcode}") - CloseHandle(hProcess) - raise WinError(errorcode) - - cmdline = wstring_at(addressof(commandLine), len(commandLine)) + _error("ReadProcessMemory failed. ", hProcess) CloseHandle(hProcess) + cmdline = wstring_at(addressof(commandLine), len(commandLine)) return cmdline -def enum_processes(): +def _enum_processes(): + """ + Enumerates all the processes currently running on the system. + + Yields: + lppe32 (PROCESSENTRY32) | + None (if enum failed) + """ lppe32 = PROCESSENTRY32() lppe32.dwSize = sizeof(PROCESSENTRY32) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) if snapshot == -1: - raise RuntimeError(f"Failed to create process snapshot. Errorcode: {GetLastError()}") + _error() if not Process32First(snapshot, byref(lppe32)): - CloseHandle(snapshot) - raise RuntimeError(f"Failed to get first process. Errorcode: {GetLastError()}") + _error("Process32First failed. ", snapshot) try: while 1: @@ -179,9 +246,8 @@ def enum_processes(): CloseHandle(snapshot) if errorcode != ERROR_NO_MORE_FILES: # error code != ERROR_NO_MORE_FILES, means that win api failed - raise RuntimeError(f"Failed to get next process. Errorcode: {errorcode}") - # process not found - raise ProcessLookupError(f"Process not found. Errorcode: {errorcode}") + raise OSError(errorcode) + raise ProcessLookupError(f"Finished querying. Errorcode: {errorcode}") except GeneratorExit: pass finally: @@ -189,33 +255,52 @@ def enum_processes(): del lppe32, snapshot def kill_process_by_regex(regex: str) -> int: + """ + Kill processes with cmdline match the given regex. + + Args: + regex: + + Returns: + int: Number of processes killed + """ count = 0 + processes = _enum_processes() try: - for lppe32 in enum_processes(): - proc = psutil.Process(lppe32.th32ProcessID) - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline + for lppe32 in processes: + pid = lppe32.th32ProcessID + cmdline = get_cmdline(lppe32.th32ProcessID) if not re.search(regex, cmdline): continue logger.info(f'Kill emulator: {cmdline}') - proc.kill() + terminate_process(pid) count += 1 except ProcessLookupError: - enum_processes().throw(GeneratorExit) + processes.close() return count def _get_process(pid: int): + """ + Get emulator's handle. + + Args: + pid (int): Emulator's pid + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + """ proc = psutil.Process(pid) mainthreadid = proc.threads()[0].id try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) if not processhandle: - raise WinError(GetLastError()) + _error("OpenProcess failed. ", processhandle) threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) if not threadhandle: - CloseHandle(processhandle) - raise WinError(GetLastError()) + _error("OpenThread failed. ", threadhandle) return processhandle, threadhandle, proc.pid, mainthreadid except Exception as e: @@ -223,7 +308,18 @@ def _get_process(pid: int): return None, None, proc.pid, mainthreadid def get_process(instance: EmulatorInstance): - processes = enum_processes() + """ + Get emulator's process. + + Args: + instance (EmulatorInstance): + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + None (if enum_process() failed) + """ + processes = _enum_processes() for lppe32 in processes: pid = lppe32.th32ProcessID cmdline = get_cmdline(pid) @@ -246,7 +342,17 @@ def get_process(instance: EmulatorInstance): return _get_process(pid) -def switch_window(hwnds: list, arg: int = 1): +def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): + """ + Switch emulator's windowplacement to the given arg + + Args: + hwnds (list): Possible emulator's window hwnds + arg (int): Emulator's windowplacement + + Returns: + bool: + """ for hwnd in hwnds: if not IsWindow(hwnd): continue diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 3c168342f7..d68e6850d3 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,6 +1,5 @@ import psutil -from deploy.Windows.utils import DataProcessInfo from module.base.decorator import run_once from module.base.timer import Timer from module.device.connection import AdbDeviceWithStatus @@ -23,13 +22,6 @@ class EmulatorStatus: class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): def execute(self, command: str): - """ - Args: - command (str): - - Returns: - bool: - """ command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') if self.config.Emulator_SilentStart == 'normal': @@ -40,19 +32,18 @@ def execute(self, command: str): logger.info(f"Current window: {self.focusedwindow[0]}") return True - @classmethod - def kill_process_by_regex(cls, regex: str) -> int: - """ - Kill processes with cmdline match the given regex. - - Args: - regex: - - Returns: - int: Number of processes killed - """ + @staticmethod + def kill_process_by_regex(regex: str) -> int: return api_windows.kill_process_by_regex(regex) + @staticmethod + def getfocusedwindow(): + return api_windows.getfocusedwindow() + + @staticmethod + def setforegroundwindow(focusedwindow: tuple): + return api_windows.setforegroundwindow(focusedwindow) + @staticmethod def get_hwnds(pid: int) -> list: return api_windows.get_hwnds(pid) @@ -60,6 +51,10 @@ def get_hwnds(pid: int) -> list: @staticmethod def get_process(instance: EmulatorInstance): return api_windows.get_process(instance) + + @staticmethod + def get_cmdline(pid: int): + return api_windows.get_cmdline(pid) def switch_window(self, arg: int): if self.process is None: @@ -267,14 +262,17 @@ def show_package(m): break # Flash window - currentwindow = api_windows.get_focused_window() - if self.focusedwindow is not None and currentwindow is not None and self.focusedwindow != currentwindow: + currentwindow = self.getfocusedwindow() + if ( + self.focusedwindow is not None and + currentwindow is not None and + self.focusedwindow != currentwindow + ): logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") - api_windows.set_foreground_window(self.focusedwindow) + self.setforegroundwindow(self.focusedwindow) # Check emulator process and hwnds self.hwnds = self.get_hwnds(self.process[2]) - self.psproc = psutil.Process(self.process[2]) logger.info(f'Emulator start completed') logger.info(f'Emulator Process: {self.process}') @@ -318,26 +316,17 @@ def emulator_check(self): if self.process is None: self.process = self.get_process(self.emulator_instance) return True - if self.psproc.pid != self.process[2]: - self.psproc = psutil.Process(self.process[2]) - cmdline = DataProcessInfo(proc=self.psproc, pid=self.psproc.pid).cmdline - if self.emulator_instance.path in cmdline and self.psproc.is_running(): + cmdline = self.get_cmdline(self.process[2]) + if self.emulator_instance.path in cmdline: return True else: self.process = self.get_process(self.emulator_instance) - self.psproc = psutil.Process(self.process[2]) return True except ProcessLookupError as e: - logger.warning(e) - return False - except psutil.NoSuchProcess: - return False - except psutil.AccessDenied: return False except IndexError: return False - except RuntimeError as e: - logger.error(e) + except OSError as e: raise except Exception as e: logger.error(e) diff --git a/module/device/platform/api_windows/__init__.py b/module/device/platform/winapi/__init__.py similarity index 100% rename from module/device/platform/api_windows/__init__.py rename to module/device/platform/winapi/__init__.py diff --git a/module/device/platform/api_windows/const_windows.py b/module/device/platform/winapi/const_windows.py similarity index 100% rename from module/device/platform/api_windows/const_windows.py rename to module/device/platform/winapi/const_windows.py diff --git a/module/device/platform/api_windows/functions_windows.py b/module/device/platform/winapi/functions_windows.py similarity index 88% rename from module/device/platform/api_windows/functions_windows.py rename to module/device/platform/winapi/functions_windows.py index 3d949e2786..994b195d33 100644 --- a/module/device/platform/api_windows/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,11 +1,11 @@ from ctypes import POINTER, WINFUNCTYPE, WinDLL from ctypes.wintypes import ( - HANDLE, DWORD, BOOL, INT, + HANDLE, DWORD, BOOL, INT, UINT, LPWSTR, LPCWSTR, LPVOID, HWND, LPARAM ) -from module.device.platform.api_windows.structures_windows import ( +from module.device.platform.winapi.structures_windows import ( SECURITY_ATTRIBUTES, STARTUPINFO, WINDOWPLACEMENT, PROCESS_INFORMATION, PROCESSENTRY32 ) @@ -17,17 +17,21 @@ CreateProcessW.argtypes = [ LPCWSTR, #lpApplicationName LPWSTR, #lpCommandLine - POINTER(SECURITY_ATTRIBUTES), #lpProcessAttributes - POINTER(SECURITY_ATTRIBUTES), #lpThreadAttributes + POINTER(SECURITY_ATTRIBUTES), #lpProcessAttributes + POINTER(SECURITY_ATTRIBUTES), #lpThreadAttributes BOOL, #bInheritHandles DWORD, #dwCreationFlags LPVOID, #lpEnvironment LPCWSTR, #lpCurrentDirectory POINTER(STARTUPINFO), #lpStartupInfo - POINTER(PROCESS_INFORMATION) #lpProcessInformation + POINTER(PROCESS_INFORMATION) #lpProcessInformation ] CreateProcessW.restype = BOOL +TerminateProcess = kernel32.TerminateProcess +TerminateProcess.argtypes = [HANDLE, UINT] +TerminateProcess.restype = BOOL + GetForegroundWindow = user32.GetForegroundWindow GetForegroundWindow.restype = HWND SetForegroundWindow = user32.SetForegroundWindow @@ -79,7 +83,6 @@ Process32Next.restype = BOOL GetLastError = kernel32.GetLastError -GetLastError.restype = BOOL ReadProcessMemory = kernel32.ReadProcessMemory NtQueryInformationProcess = ntdll.NtQueryInformationProcess diff --git a/module/device/platform/api_windows/structures_windows.py b/module/device/platform/winapi/structures_windows.py similarity index 96% rename from module/device/platform/api_windows/structures_windows.py rename to module/device/platform/winapi/structures_windows.py index c9df91a5f7..cf21b7b3f6 100644 --- a/module/device/platform/api_windows/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -87,8 +87,8 @@ class PEB_LDR_DATA(Structure): ("Length", ULONG), ("Initialized", BOOLEAN), ("SsHandle", HANDLE), - ("InLoadOrderModuleList", LIST_ENTRY), - ("InMemoryOrderModuleList", LIST_ENTRY), + ("InLoadOrderModuleList", LIST_ENTRY), + ("InMemoryOrderModuleList", LIST_ENTRY), ("InInitializationOrderModuleList", LIST_ENTRY) ] @@ -154,8 +154,8 @@ class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ ("Reserved1", BYTE * 16), ("Reserved2", LPVOID * 10), - ("ImagePathName", UNICODE_STRING), - ("CommandLine", UNICODE_STRING) + ("ImagePathName", UNICODE_STRING), + ("CommandLine", UNICODE_STRING) ] class PROCESS_BASIC_INFORMATION(Structure): @@ -165,4 +165,4 @@ class PROCESS_BASIC_INFORMATION(Structure): ("Reserved2", LPVOID * 2), ("UniqueProcessId", ULONG), ("Reserved3", LPVOID) - ] \ No newline at end of file + ] From 96c03b3f8c73a9cafc5c34fddbf4dc6da11199b7 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 27 Jun 2024 23:39:09 +0800 Subject: [PATCH 063/161] Upd: Add full winapi support. --- module/device/device.py | 12 +- module/device/platform/api_windows.py | 170 ++++++++++++++---- module/device/platform/platform_base.py | 2 +- module/device/platform/platform_windows.py | 5 +- .../device/platform/winapi/const_windows.py | 6 +- .../platform/winapi/functions_windows.py | 22 ++- .../platform/winapi/structures_windows.py | 21 +++ 7 files changed, 191 insertions(+), 47 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index 9608189c40..27251fd831 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -103,7 +103,7 @@ def __init__(self, *args, **kwargs): if not self.initialized: self.initialized = True - self.switchwindow() + self.switch_window() def run_simple_screenshot_benchmark(self): """ @@ -340,19 +340,19 @@ def emulator_start(self): raise if not self.initialized: self.initialized = True - self.switchwindow() + self.switch_window() self.stuck_record_clear() self.click_record_clear() - def switchwindow(self): + def switch_window(self): from module.device.platform import api_windows method = self.config.Emulator_SilentStart if method == 'normal': - return super().switchwindow(api_windows.SW_SHOW) + return super().switch_window(api_windows.SW_SHOW) elif method == 'minimize': - return super().switchwindow(api_windows.SW_MINIMIZE) + return super().switch_window(api_windows.SW_MINIMIZE) elif method == 'silent': return True else: from module.exception import ScriptError - raise ScriptError("Wrong setting") \ No newline at end of file + raise ScriptError("Wrong setting") diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 518a8bdadd..55ca351256 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,6 +1,5 @@ import re -import psutil from ctypes import byref, sizeof, cast, create_unicode_buffer, wstring_at, addressof from ctypes.wintypes import SIZE @@ -10,23 +9,41 @@ from module.device.platform.winapi.structures_windows import * from module.logger import logger - -def _error(errstr: str = '', handle: int = 0, exception: type = OSError, raiseexcept: bool = True): +method = { + 0: logger.debug, + 1: logger.info, + 2: logger.warning, + 3: logger.error, + 4: logger.critical +} + +def _except( + msg: str = '', + statuscode: int = -1, + loglevel: int = 2, + handle: int = 0, + raiseexcept: bool = True, + exception: type = OSError, +): """ Raise exception. Args: - errstr (str): Error message + msg (str): Error message + statuscode (int): Status code of the error + loglevel (int): Logging level handle (int): Handle to close + raiseexcept (bool): Flag indicating whether to raise exception (Type[Exception]): Exception class to raise - raiseexcept (bool): Flag indicating whether to raise the exception """ - errorcode = GetLastError() - logger.warning(f"{errstr}Errorcode: {errorcode}") - if not handle: + if statuscode == -1: + statuscode = GetLastError() + logmethod = method.get(loglevel, logger.warning) + logmethod(f"{msg}Status code: {statuscode}") + if handle: CloseHandle(handle) if raiseexcept: - raise exception(errorcode) + raise exception(statuscode) def getfocusedwindow(): @@ -45,7 +62,7 @@ def getfocusedwindow(): if GetWindowPlacement(hwnd, byref(wp)): return hwnd, wp else: - _error(errstr="Failed to get windowplacement. ", raiseexcept=False) + _except(msg="Failed to get windowplacement. ", raiseexcept=False) return hwnd, None def setforegroundwindow(focusedwindow: tuple = ()) -> bool: @@ -120,7 +137,7 @@ def execute(command: str, arg: bool = False): ) if not success: - _error(errstr="Failed to start emulator. ", exception=EmulatorLaunchFailedError) + _except(msg="Failed to start emulator. ", loglevel=3, exception=EmulatorLaunchFailedError) process = ( lpProcessInformation.hProcess, @@ -140,7 +157,7 @@ def terminate_process(pid: int): """ hProcess = OpenProcess(PROCESS_TERMINATE, False, pid) if TerminateProcess(hProcess, 0) == 0: - _error("Failed to kill process. ", hProcess) + _except(msg="Failed to kill process. ", loglevel=3, handle=hProcess) CloseHandle(hProcess) return True @@ -170,7 +187,7 @@ def callback(hwnd: int, lparam): logger.error("Hwnd not found!") logger.error("1.Perhaps emulator was killed.") logger.error("2.Environment has something wrong. Please check the running environment.") - _error(errstr="Hwnd not found. ", exception=HwndNotFoundError) + _except(msg="Hwnd not found. ", loglevel=3, exception=HwndNotFoundError) return hwnds @@ -188,30 +205,30 @@ def get_cmdline(pid: int) -> str: """ hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not hProcess: - _error("OpenProcess failed. ") + _except(msg="OpenProcess failed. ", loglevel=3) # Query process infomation pbi = PROCESS_BASIC_INFORMATION() returnlength = SIZE() status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: - _error(f"NtQueryInformationProcess failed. Status: 0x{status}. ", hProcess) + _except(msg=f"NtQueryInformationProcess failed. Status: 0x{status}. ", loglevel=3, handle=hProcess) # Read PEB peb = PEB() if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - _error("ReadProcessMemory failed. ", hProcess) + _except(msg="ReadProcessMemory failed. ", loglevel=3, handle=hProcess) # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - _error("ReadProcessMemory failed. ", hProcess) + _except(msg="ReadProcessMemory failed. ", loglevel=3, handle=hProcess) # Read command line commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - _error("ReadProcessMemory failed. ", hProcess) + _except(msg="ReadProcessMemory failed. ", loglevel=3, handle=hProcess) CloseHandle(hProcess) cmdline = wstring_at(addressof(commandLine), len(commandLine)) @@ -230,11 +247,11 @@ def _enum_processes(): lppe32 = PROCESSENTRY32() lppe32.dwSize = sizeof(PROCESSENTRY32) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) - if snapshot == -1: - _error() + if snapshot == INVALID_HANDLE_VALUE: + _except(msg="CreateToolhelp32Snapshot failed. ", loglevel=3) if not Process32First(snapshot, byref(lppe32)): - _error("Process32First failed. ", snapshot) + _except(msg="Process32First failed. ", loglevel=3, handle=snapshot) try: while 1: @@ -243,17 +260,52 @@ def _enum_processes(): continue # finished querying errorcode = GetLastError() - CloseHandle(snapshot) if errorcode != ERROR_NO_MORE_FILES: # error code != ERROR_NO_MORE_FILES, means that win api failed - raise OSError(errorcode) - raise ProcessLookupError(f"Finished querying. Errorcode: {errorcode}") + _except(msg="Process32Next failed. ", statuscode=errorcode, loglevel=3) + _except(msg="Finished querying. ", statuscode=errorcode, loglevel=1, exception=IterationFinished) except GeneratorExit: pass finally: CloseHandle(snapshot) del lppe32, snapshot - + + +def _enum_threads(): + """ + Enumerates all the threads currintly running on the system. + + Yields: + lpte32 (THREADENTRY32) | + None (if enum failed) + """ + lpte32 = THREADENTRY32() + lpte32.dwSize = sizeof(THREADENTRY32) + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, DWORD(0)) + if snapshot == INVALID_HANDLE_VALUE: + _except(msg="CreateToolhelp32Snapshot failed. ", loglevel=3) + + if not Thread32First(snapshot, byref(lpte32)): + _except(msg="Thread32First failed. ", loglevel=3, handle=snapshot) + + try: + while 1: + yield lpte32 + if Thread32Next(snapshot, byref(lpte32)): + continue + # finished querying + errorcode = GetLastError() + if errorcode != ERROR_NO_MORE_FILES: + # error code != ERROR_NO_MORE_FILES, means that win api failed + _except(msg="Process32Next failed. ", statuscode=errorcode, loglevel=3) + _except(msg="Finished querying. ", statuscode=errorcode, loglevel=1, exception=IterationFinished) + except GeneratorExit: + pass + finally: + CloseHandle(snapshot) + del lpte32, snapshot + + def kill_process_by_regex(regex: str) -> int: """ Kill processes with cmdline match the given regex. @@ -276,10 +328,61 @@ def kill_process_by_regex(regex: str) -> int: logger.info(f'Kill emulator: {cmdline}') terminate_process(pid) count += 1 - except ProcessLookupError: + except IterationFinished: processes.close() return count + +def get_thread(pid: int): + """ + Get process's main thread id. + + Args: + pid (int): Emulator's pid + + Returns + mainthreadid (int): Emulator's main thread id + """ + mainthreadid = 0 + minstarttime = MAXULONGLONG + threads = _enum_threads() + creationtime = FILETIME() + exittime = FILETIME() + kerneltime = FILETIME() + usertime = FILETIME() + try: + for lpte32 in threads: + if lpte32.th32OwnerProcessID != pid: + continue + + hThread = OpenThread(THREAD_QUERY_INFORMATION, False, lpte32.th32ThreadID) + if not hThread: + CloseHandle(hThread) + continue + + if not GetThreadTimes( + hThread, + byref(creationtime), + byref(exittime), + byref(kerneltime), + byref(usertime) + ): + CloseHandle(hThread) + continue + + threadstarttime = creationtime.to_int() + if threadstarttime >= minstarttime: + CloseHandle(hThread) + continue + + minstarttime = threadstarttime + mainthreadid = lpte32.th32ThreadID + CloseHandle(hThread) + except IterationFinished: + threads.close() + return mainthreadid + + def _get_process(pid: int): """ Get emulator's handle. @@ -291,21 +394,20 @@ def _get_process(pid: int): tuple(processhandle, threadhandle, processid, mainthreadid) | tuple(None, None, processid, mainthreadid) | (if enum_process() failed) """ - proc = psutil.Process(pid) - mainthreadid = proc.threads()[0].id + mtid = get_thread(pid) try: - processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, proc.pid) + processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid) if not processhandle: - _error("OpenProcess failed. ", processhandle) + _except(msg="OpenProcess failed. ", handle=processhandle) - threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mainthreadid) + threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mtid) if not threadhandle: - _error("OpenThread failed. ", threadhandle) + _except(msg="OpenThread failed. ", handle=threadhandle) - return processhandle, threadhandle, proc.pid, mainthreadid + return processhandle, threadhandle, pid, mtid except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") - return None, None, proc.pid, mainthreadid + return None, None, pid, mtid def get_process(instance: EmulatorInstance): """ diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index f1d4c2139e..e931bc5d2e 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -31,7 +31,7 @@ class PlatformBase(Connection, EmulatorManagerBase): - emulator_stop() """ - def switchwindow(self, arg: int): + def switch_window(self, arg: int): """ Switch emulator's window. """ diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index d68e6850d3..126efd77f8 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,5 +1,3 @@ -import psutil - from module.base.decorator import run_once from module.base.timer import Timer from module.device.connection import AdbDeviceWithStatus @@ -15,7 +13,6 @@ class EmulatorUnknown(Exception): class EmulatorStatus: process: tuple = None - psproc: psutil.Process = psutil.Process() hwnds: list = None focusedwindow: tuple = None @@ -322,7 +319,7 @@ def emulator_check(self): else: self.process = self.get_process(self.emulator_instance) return True - except ProcessLookupError as e: + except api_windows.IterationFinished as e: return False except IndexError: return False diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 540f2bc609..2ecb84227b 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -1,4 +1,5 @@ -from sys import getwindowsversion +from sys import getwindowsversion, maxsize +from ctypes.wintypes import LPVOID # winnt.h line 3961 PROCESS_TERMINATE = 0x0001 @@ -149,3 +150,6 @@ STATUS_ACCOUNT_EXPIRED = 0xC0000193 STATUS_PASSWORD_MUST_CHANGE = 0xC0000224 STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234 + +MAXULONGLONG = maxsize * 2 + 1 +INVALID_HANDLE_VALUE = LPVOID(-1).value diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 994b195d33..18167fcbba 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -6,7 +6,9 @@ ) from module.device.platform.winapi.structures_windows import ( - SECURITY_ATTRIBUTES, STARTUPINFO, WINDOWPLACEMENT, PROCESS_INFORMATION, PROCESSENTRY32 + SECURITY_ATTRIBUTES, STARTUPINFO, WINDOWPLACEMENT, + PROCESS_INFORMATION, PROCESSENTRY32, THREADENTRY32, + FILETIME ) user32 = WinDLL(name='user32', use_last_error=True) @@ -82,6 +84,24 @@ Process32Next.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] Process32Next.restype = BOOL +Thread32First = kernel32.Thread32First +Thread32First.argtypes = [HANDLE, POINTER(THREADENTRY32)] +Thread32First.restype = BOOL + +Thread32Next = kernel32.Thread32Next +Thread32Next.argtypes = [HANDLE, POINTER(THREADENTRY32)] +Thread32Next.restype = BOOL + +GetThreadTimes = kernel32.GetThreadTimes +GetThreadTimes.argtypes = [ + HANDLE, + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME) +] +GetThreadTimes.restype = BOOL + GetLastError = kernel32.GetLastError ReadProcessMemory = kernel32.ReadProcessMemory diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index cf21b7b3f6..b6e74f6780 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -7,6 +7,7 @@ class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... +class IterationFinished(Exception): ... class STARTUPINFO(Structure): _fields_ = [ @@ -59,6 +60,17 @@ class PROCESSENTRY32(Structure): ("szExeFile", CHAR * MAX_PATH) ] +class THREADENTRY32(Structure): + _fields_ = [ + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ThreadID", DWORD), + ("th32OwnerProcessID", DWORD), + ("tpBasePri", LONG), + ("tpDeltaPri", LONG), + ("dwFlags", DWORD) + ] + class WINDOWPLACEMENT(Structure): _fields_ = [ ("length", UINT), @@ -166,3 +178,12 @@ class PROCESS_BASIC_INFORMATION(Structure): ("UniqueProcessId", ULONG), ("Reserved3", LPVOID) ] + +class FILETIME(Structure): + _fields_ = [ + ("dwLowDateTime", DWORD), + ("dwHighDateTime", DWORD) + ] + + def to_int(self): + return (self.dwHighDateTime << 32) + self.dwLowDateTime From fcf2159dbc494a863ace8ecc8b2b32f5ee211f61 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 27 Jun 2024 23:52:36 +0800 Subject: [PATCH 064/161] Upd: fix log. --- module/device/platform/api_windows.py | 53 +++++++++++---------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 55ca351256..7cb63f9bbf 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -9,18 +9,10 @@ from module.device.platform.winapi.structures_windows import * from module.logger import logger -method = { - 0: logger.debug, - 1: logger.info, - 2: logger.warning, - 3: logger.error, - 4: logger.critical -} - def _except( msg: str = '', statuscode: int = -1, - loglevel: int = 2, + level: int = 40, handle: int = 0, raiseexcept: bool = True, exception: type = OSError, @@ -31,15 +23,14 @@ def _except( Args: msg (str): Error message statuscode (int): Status code of the error - loglevel (int): Logging level + level (int): Logging level handle (int): Handle to close raiseexcept (bool): Flag indicating whether to raise exception (Type[Exception]): Exception class to raise """ if statuscode == -1: statuscode = GetLastError() - logmethod = method.get(loglevel, logger.warning) - logmethod(f"{msg}Status code: {statuscode}") + logger.log(level, f"{msg}Status code: {statuscode}") if handle: CloseHandle(handle) if raiseexcept: @@ -62,7 +53,7 @@ def getfocusedwindow(): if GetWindowPlacement(hwnd, byref(wp)): return hwnd, wp else: - _except(msg="Failed to get windowplacement. ", raiseexcept=False) + _except(msg="Failed to get windowplacement. ", level=30, raiseexcept=False) return hwnd, None def setforegroundwindow(focusedwindow: tuple = ()) -> bool: @@ -137,7 +128,7 @@ def execute(command: str, arg: bool = False): ) if not success: - _except(msg="Failed to start emulator. ", loglevel=3, exception=EmulatorLaunchFailedError) + _except(msg="Failed to start emulator. ", exception=EmulatorLaunchFailedError) process = ( lpProcessInformation.hProcess, @@ -157,7 +148,7 @@ def terminate_process(pid: int): """ hProcess = OpenProcess(PROCESS_TERMINATE, False, pid) if TerminateProcess(hProcess, 0) == 0: - _except(msg="Failed to kill process. ", loglevel=3, handle=hProcess) + _except(msg="Failed to kill process. ", handle=hProcess) CloseHandle(hProcess) return True @@ -187,7 +178,7 @@ def callback(hwnd: int, lparam): logger.error("Hwnd not found!") logger.error("1.Perhaps emulator was killed.") logger.error("2.Environment has something wrong. Please check the running environment.") - _except(msg="Hwnd not found. ", loglevel=3, exception=HwndNotFoundError) + _except(msg="Hwnd not found. ", exception=HwndNotFoundError) return hwnds @@ -205,30 +196,30 @@ def get_cmdline(pid: int) -> str: """ hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not hProcess: - _except(msg="OpenProcess failed. ", loglevel=3) + _except(msg="OpenProcess failed. ") # Query process infomation pbi = PROCESS_BASIC_INFORMATION() returnlength = SIZE() status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: - _except(msg=f"NtQueryInformationProcess failed. Status: 0x{status}. ", loglevel=3, handle=hProcess) + _except(msg=f"NtQueryInformationProcess failed. Status: 0x{status}. ", handle=hProcess) # Read PEB peb = PEB() if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - _except(msg="ReadProcessMemory failed. ", loglevel=3, handle=hProcess) + _except(msg="ReadProcessMemory failed. ", handle=hProcess) # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - _except(msg="ReadProcessMemory failed. ", loglevel=3, handle=hProcess) + _except(msg="ReadProcessMemory failed. ", handle=hProcess) # Read command line commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - _except(msg="ReadProcessMemory failed. ", loglevel=3, handle=hProcess) + _except(msg="ReadProcessMemory failed. ", handle=hProcess) CloseHandle(hProcess) cmdline = wstring_at(addressof(commandLine), len(commandLine)) @@ -248,10 +239,10 @@ def _enum_processes(): lppe32.dwSize = sizeof(PROCESSENTRY32) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) if snapshot == INVALID_HANDLE_VALUE: - _except(msg="CreateToolhelp32Snapshot failed. ", loglevel=3) + _except(msg="CreateToolhelp32Snapshot failed. ") if not Process32First(snapshot, byref(lppe32)): - _except(msg="Process32First failed. ", loglevel=3, handle=snapshot) + _except(msg="Process32First failed. ", handle=snapshot) try: while 1: @@ -262,8 +253,8 @@ def _enum_processes(): errorcode = GetLastError() if errorcode != ERROR_NO_MORE_FILES: # error code != ERROR_NO_MORE_FILES, means that win api failed - _except(msg="Process32Next failed. ", statuscode=errorcode, loglevel=3) - _except(msg="Finished querying. ", statuscode=errorcode, loglevel=1, exception=IterationFinished) + _except(msg="Process32Next failed. ", statuscode=errorcode) + _except(msg="Finished querying. ", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: pass finally: @@ -283,10 +274,10 @@ def _enum_threads(): lpte32.dwSize = sizeof(THREADENTRY32) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, DWORD(0)) if snapshot == INVALID_HANDLE_VALUE: - _except(msg="CreateToolhelp32Snapshot failed. ", loglevel=3) + _except(msg="CreateToolhelp32Snapshot failed. ") if not Thread32First(snapshot, byref(lpte32)): - _except(msg="Thread32First failed. ", loglevel=3, handle=snapshot) + _except(msg="Thread32First failed. ", handle=snapshot) try: while 1: @@ -297,8 +288,8 @@ def _enum_threads(): errorcode = GetLastError() if errorcode != ERROR_NO_MORE_FILES: # error code != ERROR_NO_MORE_FILES, means that win api failed - _except(msg="Process32Next failed. ", statuscode=errorcode, loglevel=3) - _except(msg="Finished querying. ", statuscode=errorcode, loglevel=1, exception=IterationFinished) + _except(msg="Process32Next failed. ", statuscode=errorcode) + _except(msg="Finished querying. ", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: pass finally: @@ -398,11 +389,11 @@ def _get_process(pid: int): try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid) if not processhandle: - _except(msg="OpenProcess failed. ", handle=processhandle) + _except(msg="OpenProcess failed. ", level=30, handle=processhandle) threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mtid) if not threadhandle: - _except(msg="OpenThread failed. ", handle=threadhandle) + _except(msg="OpenThread failed. ", level=30, handle=threadhandle) return processhandle, threadhandle, pid, mtid except Exception as e: From ccbfc577b5d05bdd358a9aad5adde2b069d1ef56 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 00:01:29 +0800 Subject: [PATCH 065/161] Upd: fix texts. --- module/device/platform/api_windows.py | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 7cb63f9bbf..39b158467b 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -21,8 +21,8 @@ def _except( Raise exception. Args: - msg (str): Error message - statuscode (int): Status code of the error + msg (str): + statuscode (int): level (int): Logging level handle (int): Handle to close raiseexcept (bool): Flag indicating whether to raise @@ -30,7 +30,7 @@ def _except( """ if statuscode == -1: statuscode = GetLastError() - logger.log(level, f"{msg}Status code: {statuscode}") + logger.log(level, f"{msg} Status code: {statuscode}") if handle: CloseHandle(handle) if raiseexcept: @@ -53,7 +53,7 @@ def getfocusedwindow(): if GetWindowPlacement(hwnd, byref(wp)): return hwnd, wp else: - _except(msg="Failed to get windowplacement. ", level=30, raiseexcept=False) + _except("Failed to get windowplacement.", level=30, raiseexcept=False) return hwnd, None def setforegroundwindow(focusedwindow: tuple = ()) -> bool: @@ -128,7 +128,7 @@ def execute(command: str, arg: bool = False): ) if not success: - _except(msg="Failed to start emulator. ", exception=EmulatorLaunchFailedError) + _except("Failed to start emulator.", exception=EmulatorLaunchFailedError) process = ( lpProcessInformation.hProcess, @@ -148,7 +148,7 @@ def terminate_process(pid: int): """ hProcess = OpenProcess(PROCESS_TERMINATE, False, pid) if TerminateProcess(hProcess, 0) == 0: - _except(msg="Failed to kill process. ", handle=hProcess) + _except("Failed to kill process.", handle=hProcess) CloseHandle(hProcess) return True @@ -178,7 +178,7 @@ def callback(hwnd: int, lparam): logger.error("Hwnd not found!") logger.error("1.Perhaps emulator was killed.") logger.error("2.Environment has something wrong. Please check the running environment.") - _except(msg="Hwnd not found. ", exception=HwndNotFoundError) + _except("Hwnd not found.", exception=HwndNotFoundError) return hwnds @@ -196,30 +196,30 @@ def get_cmdline(pid: int) -> str: """ hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not hProcess: - _except(msg="OpenProcess failed. ") + _except("OpenProcess failed.") # Query process infomation pbi = PROCESS_BASIC_INFORMATION() returnlength = SIZE() status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: - _except(msg=f"NtQueryInformationProcess failed. Status: 0x{status}. ", handle=hProcess) + _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) # Read PEB peb = PEB() if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - _except(msg="ReadProcessMemory failed. ", handle=hProcess) + _except("ReadProcessMemory failed.", handle=hProcess) # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - _except(msg="ReadProcessMemory failed. ", handle=hProcess) + _except("ReadProcessMemory failed.", handle=hProcess) # Read command line commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - _except(msg="ReadProcessMemory failed. ", handle=hProcess) + _except("ReadProcessMemory failed.", handle=hProcess) CloseHandle(hProcess) cmdline = wstring_at(addressof(commandLine), len(commandLine)) @@ -239,10 +239,10 @@ def _enum_processes(): lppe32.dwSize = sizeof(PROCESSENTRY32) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) if snapshot == INVALID_HANDLE_VALUE: - _except(msg="CreateToolhelp32Snapshot failed. ") + _except("CreateToolhelp32Snapshot failed.") if not Process32First(snapshot, byref(lppe32)): - _except(msg="Process32First failed. ", handle=snapshot) + _except("Process32First failed.", handle=snapshot) try: while 1: @@ -253,8 +253,8 @@ def _enum_processes(): errorcode = GetLastError() if errorcode != ERROR_NO_MORE_FILES: # error code != ERROR_NO_MORE_FILES, means that win api failed - _except(msg="Process32Next failed. ", statuscode=errorcode) - _except(msg="Finished querying. ", statuscode=errorcode, level=20, exception=IterationFinished) + _except("Process32Next failed.", statuscode=errorcode) + _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: pass finally: @@ -274,10 +274,10 @@ def _enum_threads(): lpte32.dwSize = sizeof(THREADENTRY32) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, DWORD(0)) if snapshot == INVALID_HANDLE_VALUE: - _except(msg="CreateToolhelp32Snapshot failed. ") + _except("CreateToolhelp32Snapshot failed.") if not Thread32First(snapshot, byref(lpte32)): - _except(msg="Thread32First failed. ", handle=snapshot) + _except("Thread32First failed.", handle=snapshot) try: while 1: @@ -288,8 +288,8 @@ def _enum_threads(): errorcode = GetLastError() if errorcode != ERROR_NO_MORE_FILES: # error code != ERROR_NO_MORE_FILES, means that win api failed - _except(msg="Process32Next failed. ", statuscode=errorcode) - _except(msg="Finished querying. ", statuscode=errorcode, level=20, exception=IterationFinished) + _except("Process32Next failed.", statuscode=errorcode) + _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: pass finally: @@ -312,8 +312,8 @@ def kill_process_by_regex(regex: str) -> int: processes = _enum_processes() try: for lppe32 in processes: - pid = lppe32.th32ProcessID - cmdline = get_cmdline(lppe32.th32ProcessID) + pid = lppe32.th32ProcessID + cmdline = get_cmdline(lppe32.th32ProcessID) if not re.search(regex, cmdline): continue logger.info(f'Kill emulator: {cmdline}') @@ -352,11 +352,11 @@ def get_thread(pid: int): continue if not GetThreadTimes( - hThread, - byref(creationtime), - byref(exittime), - byref(kerneltime), - byref(usertime) + hThread, + byref(creationtime), + byref(exittime), + byref(kerneltime), + byref(usertime) ): CloseHandle(hThread) continue @@ -389,11 +389,11 @@ def _get_process(pid: int): try: processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid) if not processhandle: - _except(msg="OpenProcess failed. ", level=30, handle=processhandle) + _except("OpenProcess failed.", level=30, handle=processhandle) threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mtid) if not threadhandle: - _except(msg="OpenThread failed. ", level=30, handle=threadhandle) + _except("OpenThread failed.", level=30, handle=threadhandle) return processhandle, threadhandle, pid, mtid except Exception as e: From ddf39c1e87a0e373827607cbec7a612359ad43fe Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 00:03:23 +0800 Subject: [PATCH 066/161] Opt: Add full winapi support. --- module/device/api_windows.py | 459 +++++++++++++++++++++ module/device/winapi/__init__.py | 0 module/device/winapi/const_windows.py | 155 +++++++ module/device/winapi/functions_windows.py | 108 +++++ module/device/winapi/structures_windows.py | 189 +++++++++ 5 files changed, 911 insertions(+) create mode 100644 module/device/api_windows.py create mode 100644 module/device/winapi/__init__.py create mode 100644 module/device/winapi/const_windows.py create mode 100644 module/device/winapi/functions_windows.py create mode 100644 module/device/winapi/structures_windows.py diff --git a/module/device/api_windows.py b/module/device/api_windows.py new file mode 100644 index 0000000000..39b158467b --- /dev/null +++ b/module/device/api_windows.py @@ -0,0 +1,459 @@ +import re + +from ctypes import byref, sizeof, cast, create_unicode_buffer, wstring_at, addressof +from ctypes.wintypes import SIZE + +from module.device.platform.emulator_windows import Emulator, EmulatorInstance +from module.device.platform.winapi.const_windows import * +from module.device.platform.winapi.functions_windows import * +from module.device.platform.winapi.structures_windows import * +from module.logger import logger + +def _except( + msg: str = '', + statuscode: int = -1, + level: int = 40, + handle: int = 0, + raiseexcept: bool = True, + exception: type = OSError, +): + """ + Raise exception. + + Args: + msg (str): + statuscode (int): + level (int): Logging level + handle (int): Handle to close + raiseexcept (bool): Flag indicating whether to raise + exception (Type[Exception]): Exception class to raise + """ + if statuscode == -1: + statuscode = GetLastError() + logger.log(level, f"{msg} Status code: {statuscode}") + if handle: + CloseHandle(handle) + if raiseexcept: + raise exception(statuscode) + + +def getfocusedwindow(): + """ + Get focused window. + + Returns: + hwnd (int): Focused window hwnd + WINDOWPLACEMENT: + """ + hwnd = GetForegroundWindow() + if not hwnd: + return 0, None + wp = WINDOWPLACEMENT() + wp.length = sizeof(WINDOWPLACEMENT) + if GetWindowPlacement(hwnd, byref(wp)): + return hwnd, wp + else: + _except("Failed to get windowplacement.", level=30, raiseexcept=False) + return hwnd, None + +def setforegroundwindow(focusedwindow: tuple = ()) -> bool: + """ + Refocus foreground window. + + Args: + focusedwindow: tuple(hwnd, WINDOWPLACEMENT) | tuple(hwnd, None) + + Returns: + bool: + """ + if not focusedwindow: + return False + SetForegroundWindow(focusedwindow[0]) + if focusedwindow[1] is None: + ShowWindow(focusedwindow[0], SW_SHOWNORMAL) + else: + SetWindowPlacement(focusedwindow[0], focusedwindow[1]) + return True + + +def execute(command: str, arg: bool = False): + """ + Create a new process. + + Args: + command (str): process's commandline + arg (bool): process's windowplacement + Example: + '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' + + Returns: + process: tuple(processhandle, threadhandle, processid, mainthreadid), + focusedwindow: tuple(hwnd, WINDOWPLACEMENT) + """ + from shlex import split + from os.path import dirname + lpApplicationName = split(command)[0] + lpCommandLine = command + lpProcessAttributes = None + lpThreadAttributes = None + bInheritHandles = False + dwCreationFlags = ( + CREATE_NEW_CONSOLE | + NORMAL_PRIORITY_CLASS | + CREATE_NEW_PROCESS_GROUP | + CREATE_DEFAULT_ERROR_MODE | + CREATE_UNICODE_ENVIRONMENT + ) + lpEnvironment = None + lpCurrentDirectory = dirname(lpApplicationName) + lpStartupInfo = STARTUPINFO() + lpStartupInfo.cb = sizeof(STARTUPINFO) + lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW + lpStartupInfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE + lpProcessInformation = PROCESS_INFORMATION() + + focusedwindow = getfocusedwindow() + + success = CreateProcessW( + lpApplicationName, + lpCommandLine, + lpProcessAttributes, + lpThreadAttributes, + bInheritHandles, + dwCreationFlags, + lpEnvironment, + lpCurrentDirectory, + byref(lpStartupInfo), + byref(lpProcessInformation) + ) + + if not success: + _except("Failed to start emulator.", exception=EmulatorLaunchFailedError) + + process = ( + lpProcessInformation.hProcess, + lpProcessInformation.hThread, + lpProcessInformation.dwProcessId, + lpProcessInformation.dwThreadId + ) + return process, focusedwindow + + +def terminate_process(pid: int): + """ + Terminate emulator process. + + Args: + pid (int): Emulator's pid + """ + hProcess = OpenProcess(PROCESS_TERMINATE, False, pid) + if TerminateProcess(hProcess, 0) == 0: + _except("Failed to kill process.", handle=hProcess) + CloseHandle(hProcess) + return True + + +def get_hwnds(pid: int) -> list: + """ + Get process's window hwnds from this processid. + + Args: + pid (int): Emulator's pid + + Returns: + hwnds (list): Emulator's possible window hwnds + """ + hwnds = [] + + @EnumWindowsProc + def callback(hwnd: int, lparam): + processid = DWORD() + GetWindowThreadProcessId(hwnd, byref(processid)) + if processid.value == pid: + hwnds.append(hwnd) + return True + + EnumWindows(callback, 0) + if not hwnds: + logger.error("Hwnd not found!") + logger.error("1.Perhaps emulator was killed.") + logger.error("2.Environment has something wrong. Please check the running environment.") + _except("Hwnd not found.", exception=HwndNotFoundError) + return hwnds + + +def get_cmdline(pid: int) -> str: + """ + Get a process's command line from this processid. + + Args: + pid (int): Emulator's pid + + Returns: + command line (str): process's command line + Example: + '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' + """ + hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) + if not hProcess: + _except("OpenProcess failed.") + + # Query process infomation + pbi = PROCESS_BASIC_INFORMATION() + returnlength = SIZE() + status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) + if status != STATUS_SUCCESS: + _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) + + # Read PEB + peb = PEB() + if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): + _except("ReadProcessMemory failed.", handle=hProcess) + + # Read process parameters + upp = RTL_USER_PROCESS_PARAMETERS() + uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) + if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + _except("ReadProcessMemory failed.", handle=hProcess) + + # Read command line + commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) + if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): + _except("ReadProcessMemory failed.", handle=hProcess) + + CloseHandle(hProcess) + cmdline = wstring_at(addressof(commandLine), len(commandLine)) + + return cmdline + + +def _enum_processes(): + """ + Enumerates all the processes currently running on the system. + + Yields: + lppe32 (PROCESSENTRY32) | + None (if enum failed) + """ + lppe32 = PROCESSENTRY32() + lppe32.dwSize = sizeof(PROCESSENTRY32) + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) + if snapshot == INVALID_HANDLE_VALUE: + _except("CreateToolhelp32Snapshot failed.") + + if not Process32First(snapshot, byref(lppe32)): + _except("Process32First failed.", handle=snapshot) + + try: + while 1: + yield lppe32 + if Process32Next(snapshot, byref(lppe32)): + continue + # finished querying + errorcode = GetLastError() + if errorcode != ERROR_NO_MORE_FILES: + # error code != ERROR_NO_MORE_FILES, means that win api failed + _except("Process32Next failed.", statuscode=errorcode) + _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) + except GeneratorExit: + pass + finally: + CloseHandle(snapshot) + del lppe32, snapshot + + +def _enum_threads(): + """ + Enumerates all the threads currintly running on the system. + + Yields: + lpte32 (THREADENTRY32) | + None (if enum failed) + """ + lpte32 = THREADENTRY32() + lpte32.dwSize = sizeof(THREADENTRY32) + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, DWORD(0)) + if snapshot == INVALID_HANDLE_VALUE: + _except("CreateToolhelp32Snapshot failed.") + + if not Thread32First(snapshot, byref(lpte32)): + _except("Thread32First failed.", handle=snapshot) + + try: + while 1: + yield lpte32 + if Thread32Next(snapshot, byref(lpte32)): + continue + # finished querying + errorcode = GetLastError() + if errorcode != ERROR_NO_MORE_FILES: + # error code != ERROR_NO_MORE_FILES, means that win api failed + _except("Process32Next failed.", statuscode=errorcode) + _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) + except GeneratorExit: + pass + finally: + CloseHandle(snapshot) + del lpte32, snapshot + + +def kill_process_by_regex(regex: str) -> int: + """ + Kill processes with cmdline match the given regex. + + Args: + regex: + + Returns: + int: Number of processes killed + """ + count = 0 + + processes = _enum_processes() + try: + for lppe32 in processes: + pid = lppe32.th32ProcessID + cmdline = get_cmdline(lppe32.th32ProcessID) + if not re.search(regex, cmdline): + continue + logger.info(f'Kill emulator: {cmdline}') + terminate_process(pid) + count += 1 + except IterationFinished: + processes.close() + return count + + +def get_thread(pid: int): + """ + Get process's main thread id. + + Args: + pid (int): Emulator's pid + + Returns + mainthreadid (int): Emulator's main thread id + """ + mainthreadid = 0 + minstarttime = MAXULONGLONG + threads = _enum_threads() + creationtime = FILETIME() + exittime = FILETIME() + kerneltime = FILETIME() + usertime = FILETIME() + try: + for lpte32 in threads: + if lpte32.th32OwnerProcessID != pid: + continue + + hThread = OpenThread(THREAD_QUERY_INFORMATION, False, lpte32.th32ThreadID) + if not hThread: + CloseHandle(hThread) + continue + + if not GetThreadTimes( + hThread, + byref(creationtime), + byref(exittime), + byref(kerneltime), + byref(usertime) + ): + CloseHandle(hThread) + continue + + threadstarttime = creationtime.to_int() + if threadstarttime >= minstarttime: + CloseHandle(hThread) + continue + + minstarttime = threadstarttime + mainthreadid = lpte32.th32ThreadID + CloseHandle(hThread) + except IterationFinished: + threads.close() + return mainthreadid + + +def _get_process(pid: int): + """ + Get emulator's handle. + + Args: + pid (int): Emulator's pid + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + """ + mtid = get_thread(pid) + try: + processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid) + if not processhandle: + _except("OpenProcess failed.", level=30, handle=processhandle) + + threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mtid) + if not threadhandle: + _except("OpenThread failed.", level=30, handle=threadhandle) + + return processhandle, threadhandle, pid, mtid + except Exception as e: + logger.warning(f"Failed to get process and thread handles: {e}") + return None, None, pid, mtid + +def get_process(instance: EmulatorInstance): + """ + Get emulator's process. + + Args: + instance (EmulatorInstance): + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + None (if enum_process() failed) + """ + processes = _enum_processes() + for lppe32 in processes: + pid = lppe32.th32ProcessID + cmdline = get_cmdline(pid) + if not instance.path in cmdline: + continue + if instance == Emulator.MuMuPlayer12: + match = re.search(r'\d+$', cmdline) + if match and int(match.group()) == instance.MuMuPlayer12_id: + processes.close() + return _get_process(pid) + elif instance == Emulator.LDPlayerFamily: + match = re.search(r'\d+$', cmdline) + if match and int(match.group()) == instance.LDPlayer_id: + processes.close() + return _get_process(pid) + else: + matchstr = re.search(fr'\b{instance.name}$', cmdline) + if matchstr and matchstr.group() == instance.name: + processes.close() + return _get_process(pid) + + +def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): + """ + Switch emulator's windowplacement to the given arg + + Args: + hwnds (list): Possible emulator's window hwnds + arg (int): Emulator's windowplacement + + Returns: + bool: + """ + for hwnd in hwnds: + if not IsWindow(hwnd): + continue + if GetParent(hwnd): + continue + rect = RECT() + GetWindowRect(hwnd, byref(rect)) + if {rect.left, rect.top, rect.right, rect.bottom} == {0}: + continue + ShowWindow(hwnd, arg) + return True diff --git a/module/device/winapi/__init__.py b/module/device/winapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/module/device/winapi/const_windows.py b/module/device/winapi/const_windows.py new file mode 100644 index 0000000000..2ecb84227b --- /dev/null +++ b/module/device/winapi/const_windows.py @@ -0,0 +1,155 @@ +from sys import getwindowsversion, maxsize +from ctypes.wintypes import LPVOID + +# winnt.h line 3961 +PROCESS_TERMINATE = 0x0001 +PROCESS_CREATE_THREAD = 0x0002 +PROCESS_SET_SESSIONID = 0x0004 +PROCESS_VM_OPERATION = 0x0008 +PROCESS_VM_READ = 0x0010 +PROCESS_VM_WRITE = 0x0020 +PROCESS_DUP_HANDLE = 0x0040 +PROCESS_CREATE_PROCESS = 0x0080 +PROCESS_SET_QUOTA = 0x0100 +PROCESS_SET_INFORMATION = 0x0200 +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_SUSPEND_RESUME = 0x0800 +PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + +THREAD_TERMINATE = 0x0001 +THREAD_SUSPEND_RESUME = 0x0002 +THREAD_GET_CONTEXT = 0x0008 +THREAD_SET_CONTEXT = 0x0010 +THREAD_SET_INFORMATION = 0x0020 +THREAD_QUERY_INFORMATION = 0x0040 +THREAD_SET_THREAD_TOKEN = 0x0080 +THREAD_IMPERSONATE = 0x0100 +THREAD_DIRECT_IMPERSONATION = 0x0200 +THREAD_SET_LIMITED_INFORMATION = 0x0400 +THREAD_QUERY_LIMITED_INFORMATION = 0x0800 + +# winnt.h line 2809 +SYNCHRONIZE = 0x00100000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 + +VERSIONINFO = getwindowsversion() +MAJOR, MINOR, BUILD = VERSIONINFO.major, VERSIONINFO.minor, VERSIONINFO.build + +if (MAJOR > 6) or (MAJOR == 6 and MINOR >= 1): + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff + THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xffff +else: + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xfff + THREAD_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3ff + +MAXIMUM_PROC_PER_GROUP = 64 +MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP + +# error.h line 23 +ERROR_NO_MORE_FILES = 0x12 + +# tlhelp32.h line 17 +TH32CS_SNAPHEAPLIST = 0x00000001 +TH32CS_SNAPPROCESS = 0x00000002 +TH32CS_SNAPTHREAD = 0x00000004 +TH32CS_SNAPMODULE = 0x00000008 +TH32CS_SNAPMODULE32 = 0x00000010 +TH32CS_SNAPALL = ( + TH32CS_SNAPHEAPLIST | + TH32CS_SNAPPROCESS | + TH32CS_SNAPTHREAD | + TH32CS_SNAPMODULE +) +TH32CS_INHERIT = 0x80000000 + +# winbase.h line 1463 +STARTF_USESHOWWINDOW = 0x00000001 +STARTF_USESIZE = 0x00000002 +STARTF_USEPOSITION = 0x00000004 +STARTF_USECOUNTCHARS = 0x00000008 +STARTF_USEFILLATTRIBUTE = 0x00000010 +STARTF_RUNFULLSCREEN = 0x00000020 +STARTF_FORCEONFEEDBACK = 0x00000040 +STARTF_FORCEOFFFEEDBACK = 0x00000080 +STARTF_USESTDHANDLES = 0x00000100 + +STARTF_USEHOTKEY = 0x00000200 +STARTF_TITLEISLINKNAME = 0x00000800 +STARTF_TITLEISAPPID = 0x00001000 +STARTF_PREVENTPINNING = 0x00002000 + +# winuser.h line 200 +SW_HIDE = 0 +SW_SHOWNORMAL = 1 +SW_NORMAL = 1 +SW_SHOWMINIMIZED = 2 +SW_SHOWMAXIMIZED = 3 +SW_MAXIMIZE = 3 +SW_SHOWNOACTIVATE = 4 +SW_SHOW = 5 +SW_MINIMIZE = 6 +SW_SHOWMINNOACTIVE = 7 +SW_SHOWNA = 8 +SW_RESTORE = 9 +SW_SHOWDEFAULT = 10 +SW_FORCEMINIMIZE = 11 +SW_MAX = 11 + +# winbase.h line 377 +DEBUG_PROCESS = 0x00000001 +DEBUG_ONLY_THIS_PROCESS = 0x00000002 +CREATE_SUSPENDED = 0x00000004 +DETACHED_PROCESS = 0x00000008 + +CREATE_NEW_CONSOLE = 0x00000010 +NORMAL_PRIORITY_CLASS = 0x00000020 +IDLE_PRIORITY_CLASS = 0x00000040 +HIGH_PRIORITY_CLASS = 0x00000080 + +REALTIME_PRIORITY_CLASS = 0x00000100 +CREATE_NEW_PROCESS_GROUP = 0x00000200 +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +CREATE_SEPARATE_WOW_VDM = 0x00000800 + +CREATE_SHARED_WOW_VDM = 0x00001000 +CREATE_FORCEDOS = 0x00002000 +BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 +ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 + +INHERIT_PARENT_AFFINITY = 0x00010000 +INHERIT_CALLER_PRIORITY = 0x00020000 +CREATE_PROTECTED_PROCESS = 0x00040000 +EXTENDED_STARTUPINFO_PRESENT = 0x00080000 + +PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000 +PROCESS_MODE_BACKGROUND_END = 0x00200000 + +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 +CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000 +CREATE_DEFAULT_ERROR_MODE = 0x04000000 +CREATE_NO_WINDOW = 0x08000000 + +PROFILE_USER = 0x10000000 +PROFILE_KERNEL = 0x20000000 +PROFILE_SERVER = 0x40000000 +CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000 + +# subauth.h line 250 +STATUS_SUCCESS = 0x00000000 +STATUS_INVALID_INFO_CLASS = 0xC0000003 +STATUS_NO_SUCH_USER = 0xC0000064 +STATUS_WRONG_PASSWORD = 0xC000006A +STATUS_PASSWORD_RESTRICTION = 0xC000006C +STATUS_LOGON_FAILURE = 0xC000006D +STATUS_ACCOUNT_RESTRICTION = 0xC000006E +STATUS_INVALID_LOGON_HOURS = 0xC000006F +STATUS_INVALID_WORKSTATION = 0xC0000070 +STATUS_PASSWORD_EXPIRED = 0xC0000071 +STATUS_ACCOUNT_DISABLED = 0xC0000072 +STATUS_INSUFFICIENT_RESOURCES = 0xC000009A +STATUS_ACCOUNT_EXPIRED = 0xC0000193 +STATUS_PASSWORD_MUST_CHANGE = 0xC0000224 +STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234 + +MAXULONGLONG = maxsize * 2 + 1 +INVALID_HANDLE_VALUE = LPVOID(-1).value diff --git a/module/device/winapi/functions_windows.py b/module/device/winapi/functions_windows.py new file mode 100644 index 0000000000..18167fcbba --- /dev/null +++ b/module/device/winapi/functions_windows.py @@ -0,0 +1,108 @@ +from ctypes import POINTER, WINFUNCTYPE, WinDLL +from ctypes.wintypes import ( + HANDLE, DWORD, BOOL, INT, UINT, + LPWSTR, LPCWSTR, LPVOID, HWND, + LPARAM +) + +from module.device.platform.winapi.structures_windows import ( + SECURITY_ATTRIBUTES, STARTUPINFO, WINDOWPLACEMENT, + PROCESS_INFORMATION, PROCESSENTRY32, THREADENTRY32, + FILETIME +) + +user32 = WinDLL(name='user32', use_last_error=True) +kernel32 = WinDLL(name='kernel32', use_last_error=True) +ntdll = WinDLL(name='ntdll', use_last_error=True) + +CreateProcessW = kernel32.CreateProcessW +CreateProcessW.argtypes = [ + LPCWSTR, #lpApplicationName + LPWSTR, #lpCommandLine + POINTER(SECURITY_ATTRIBUTES), #lpProcessAttributes + POINTER(SECURITY_ATTRIBUTES), #lpThreadAttributes + BOOL, #bInheritHandles + DWORD, #dwCreationFlags + LPVOID, #lpEnvironment + LPCWSTR, #lpCurrentDirectory + POINTER(STARTUPINFO), #lpStartupInfo + POINTER(PROCESS_INFORMATION) #lpProcessInformation +] +CreateProcessW.restype = BOOL + +TerminateProcess = kernel32.TerminateProcess +TerminateProcess.argtypes = [HANDLE, UINT] +TerminateProcess.restype = BOOL + +GetForegroundWindow = user32.GetForegroundWindow +GetForegroundWindow.restype = HWND +SetForegroundWindow = user32.SetForegroundWindow +SetForegroundWindow.argtypes = [HWND] +SetForegroundWindow.restype = BOOL + +GetWindowPlacement = user32.GetWindowPlacement +GetWindowPlacement.argtypes = [HWND, POINTER(WINDOWPLACEMENT)] +GetWindowPlacement.restype = BOOL +SetWindowPlacement = user32.SetWindowPlacement +SetWindowPlacement.argtypes = [HWND, POINTER(WINDOWPLACEMENT)] +SetWindowPlacement.restype = BOOL + +ShowWindow = user32.ShowWindow +ShowWindow.argtypes = [HWND, INT] +ShowWindow.restype = BOOL + +IsWindow = user32.IsWindow +GetParent = user32.GetParent +GetWindowRect = user32.GetWindowRect + +EnumWindows = user32.EnumWindows +EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM) +GetWindowThreadProcessId = user32.GetWindowThreadProcessId +GetWindowThreadProcessId.argtypes = [HWND, POINTER(DWORD)] +GetWindowThreadProcessId.restype = DWORD + +OpenProcess = kernel32.OpenProcess +OpenProcess.argtypes = [DWORD, BOOL, DWORD] +OpenProcess.restype = HANDLE +OpenThread = kernel32.OpenThread +OpenThread.argtypes = [DWORD, BOOL, DWORD] +OpenThread.restype = HANDLE + +CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot +CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] +CreateToolhelp32Snapshot.restype = HANDLE + +CloseHandle = kernel32.CloseHandle +CloseHandle.argtypes = [HANDLE] +CloseHandle.restype = BOOL + +Process32First = kernel32.Process32First +Process32First.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] +Process32First.restype = BOOL + +Process32Next = kernel32.Process32Next +Process32Next.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] +Process32Next.restype = BOOL + +Thread32First = kernel32.Thread32First +Thread32First.argtypes = [HANDLE, POINTER(THREADENTRY32)] +Thread32First.restype = BOOL + +Thread32Next = kernel32.Thread32Next +Thread32Next.argtypes = [HANDLE, POINTER(THREADENTRY32)] +Thread32Next.restype = BOOL + +GetThreadTimes = kernel32.GetThreadTimes +GetThreadTimes.argtypes = [ + HANDLE, + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME) +] +GetThreadTimes.restype = BOOL + +GetLastError = kernel32.GetLastError + +ReadProcessMemory = kernel32.ReadProcessMemory +NtQueryInformationProcess = ntdll.NtQueryInformationProcess diff --git a/module/device/winapi/structures_windows.py b/module/device/winapi/structures_windows.py new file mode 100644 index 0000000000..b6e74f6780 --- /dev/null +++ b/module/device/winapi/structures_windows.py @@ -0,0 +1,189 @@ +from ctypes import POINTER, Structure +from ctypes.wintypes import ( + HANDLE, DWORD, WORD, LARGE_INTEGER, BYTE, BOOL, BOOLEAN, + USHORT, UINT, LONG, ULONG, CHAR, LPWSTR, LPVOID, MAX_PATH, + RECT, PULONG, POINT, PWCHAR +) + +class EmulatorLaunchFailedError(Exception): ... +class HwndNotFoundError(Exception): ... +class IterationFinished(Exception): ... + +class STARTUPINFO(Structure): + _fields_ = [ + ('cb', DWORD), + ('lpReserved', LPWSTR), + ('lpDesktop', LPWSTR), + ('lpTitle', LPWSTR), + ('dwX', DWORD), + ('dwY', DWORD), + ('dwXSize', DWORD), + ('dwYSize', DWORD), + ('dwXCountChars', DWORD), + ('dwYCountChars', DWORD), + ('dwFillAttribute', DWORD), + ('dwFlags', DWORD), + ('wShowWindow', WORD), + ('cbReserved2', WORD), + ('lpReserved2', POINTER(BYTE)), + ('hStdInput', HANDLE), + ('hStdOutput', HANDLE), + ('hStdError', HANDLE) + ] + +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ('hProcess', HANDLE), + ('hThread', HANDLE), + ('dwProcessId', DWORD), + ('dwThreadId', DWORD) + ] + +class SECURITY_ATTRIBUTES(Structure): + _fields_ = [ + ("nLength", DWORD), + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL) + ] + +class PROCESSENTRY32(Structure): + _fields_ = [ + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ProcessID", DWORD), + ("th32DefaultHeapID", PULONG), + ("th32ModuleID", DWORD), + ("cntThreads", DWORD), + ("th32ParentProcessID", DWORD), + ("pcPriClassBase", LONG), + ("dwFlags", DWORD), + ("szExeFile", CHAR * MAX_PATH) + ] + +class THREADENTRY32(Structure): + _fields_ = [ + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ThreadID", DWORD), + ("th32OwnerProcessID", DWORD), + ("tpBasePri", LONG), + ("tpDeltaPri", LONG), + ("dwFlags", DWORD) + ] + +class WINDOWPLACEMENT(Structure): + _fields_ = [ + ("length", UINT), + ("flags", UINT), + ("showCmd", UINT), + ("ptMinPosition", POINT), + ("ptMaxPosition", POINT), + ("rcNormalPosition", RECT) + ] + +class LIST_ENTRY(Structure): + _fields_ = [ + ("Flink", POINTER(LPVOID)), + ("Blink", POINTER(LPVOID)) + ] + +class UNICODE_STRING(Structure): + _fields_ = [ + ("Length", USHORT), + ("MaximumLength", USHORT), + ("Buffer", PWCHAR) + ] + +class PEB_LDR_DATA(Structure): + _fields_ = [ + ("Length", ULONG), + ("Initialized", BOOLEAN), + ("SsHandle", HANDLE), + ("InLoadOrderModuleList", LIST_ENTRY), + ("InMemoryOrderModuleList", LIST_ENTRY), + ("InInitializationOrderModuleList", LIST_ENTRY) + ] + +class PEB(Structure): + _fields_ = [ + ("InheritedAddressSpace", BOOLEAN), + ("ReadImageFileExecOptions", BOOLEAN), + ("BeingDebugged", BOOLEAN), + ("Spare", BOOLEAN), + ("Mutant", HANDLE), + ("ImageBaseAddress", LPVOID), + ("Ldr", POINTER(PEB_LDR_DATA)), + ("ProcessParameters", LPVOID), + ("SubSystemData", LPVOID), + ("ProcessHeap", LPVOID), + ("FastPebLock", LPVOID), + ("FastPebLockRoutine", LPVOID), + ("FastPebUnlockRoutine", LPVOID), + ("EnvironmentUpdateCount", ULONG), + ("KernelCallbackTable", LPVOID), + ("EventLogSection", LPVOID), + ("EventLog", LPVOID), + ("FreeList", LPVOID), + ("TlsExpansionCounter", ULONG), + ("TlsBitmap", LPVOID), + ("TlsBitmapBits", ULONG * 2), + ("ReadOnlySharedMemoryBase", LPVOID), + ("ReadOnlySharedMemoryHeap", LPVOID), + ("ReadOnlyStaticServerData", LPVOID), + ("AnsiCodePageData", LPVOID), + ("OemCodePageData", LPVOID), + ("UnicodeCaseTableData", LPVOID), + ("NumberOfProcessors", ULONG), + ("NtGlobalFlag", ULONG), + ("Spare2", BYTE * 4), + ("CriticalSectionTimeout", LARGE_INTEGER), + ("HeapSegmentReserve", ULONG), + ("HeapSegmentCommit", ULONG), + ("HeapDeCommitTotalFreeThreshold", ULONG), + ("HeapDeCommitFreeBlockThreshold", ULONG), + ("NumberOfHeaps", ULONG), + ("MaximumNumberOfHeaps", ULONG), + ("ProcessHeaps", POINTER(LPVOID)), + ("GdiSharedHandleTable", LPVOID), + ("ProcessStarterHelper", LPVOID), + ("GdiDCAttributeList", LPVOID), + ("LoaderLock", LPVOID), + ("OSMajorVersion", ULONG), + ("OSMinorVersion", ULONG), + ("OSBuildNumber", ULONG), + ("OSPlatformId", ULONG), + ("ImageSubSystem", ULONG), + ("ImageSubSystemMajorVersion", ULONG), + ("ImageSubSystemMinorVersion", ULONG), + ("GdiHandleBuffer", ULONG * 34), + ("PostProcessInitRoutine", ULONG), + ("TlsExpansionBitmap", ULONG), + ("TlsExpansionBitmapBits", BYTE * 32), + ("SessionId", ULONG) + ] + +class RTL_USER_PROCESS_PARAMETERS(Structure): + _fields_ = [ + ("Reserved1", BYTE * 16), + ("Reserved2", LPVOID * 10), + ("ImagePathName", UNICODE_STRING), + ("CommandLine", UNICODE_STRING) + ] + +class PROCESS_BASIC_INFORMATION(Structure): + _fields_ = [ + ("Reserved1", LPVOID), + ("PebBaseAddress", POINTER(PEB)), + ("Reserved2", LPVOID * 2), + ("UniqueProcessId", ULONG), + ("Reserved3", LPVOID) + ] + +class FILETIME(Structure): + _fields_ = [ + ("dwLowDateTime", DWORD), + ("dwHighDateTime", DWORD) + ] + + def to_int(self): + return (self.dwHighDateTime << 32) + self.dwLowDateTime From eded001cbf9c7351e4fc97db32b4f79d6f4ff20c Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 00:28:59 +0800 Subject: [PATCH 067/161] Upd: fix logics --- module/device/device.py | 12 +----------- module/device/platform/platform_base.py | 6 +++--- module/device/platform/platform_windows.py | 15 ++++++++++++--- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index 27251fd831..d20933d4dc 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -345,14 +345,4 @@ def emulator_start(self): self.click_record_clear() def switch_window(self): - from module.device.platform import api_windows - method = self.config.Emulator_SilentStart - if method == 'normal': - return super().switch_window(api_windows.SW_SHOW) - elif method == 'minimize': - return super().switch_window(api_windows.SW_MINIMIZE) - elif method == 'silent': - return True - else: - from module.exception import ScriptError - raise ScriptError("Wrong setting") + return super().switch_window() diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index e931bc5d2e..1c88592fcc 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -31,7 +31,7 @@ class PlatformBase(Connection, EmulatorManagerBase): - emulator_stop() """ - def switch_window(self, arg: int): + def switch_window(self): """ Switch emulator's window. """ @@ -40,7 +40,7 @@ def switch_window(self, arg: int): def emulator_start(self): """ - Start a emulator, until startup completed. + Start an emulator, until startup completed. - Retry is required. - Using bored sleep to wait startup is forbidden. """ @@ -48,7 +48,7 @@ def emulator_start(self): def emulator_stop(self): """ - Stop a emulator. + Stop an emulator. """ logger.info(f'Current platform {sys.platform} does not support emulator_stop, skip') diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 735c956914..90f4fa63e8 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -53,10 +53,19 @@ def get_process(instance: EmulatorInstance): def get_cmdline(pid: int): return api_windows.get_cmdline(pid) - def switch_window(self, arg: int): + def switch_window(self): if self.process is None: return - return api_windows.switch_window(self.hwnds, arg) + method = self.config.Emulator_SilentStart + if method == 'normal': + return api_windows.switch_window(self.hwnds, api_windows.SW_SHOW) + elif method == 'minimize': + return api_windows.switch_window(self.hwnds, api_windows.SW_MINIMIZE) + elif method == 'silent': + return True + else: + from module.exception import ScriptError + raise ScriptError("Wrong setting") def _emulator_start(self, instance: EmulatorInstance): """ @@ -336,4 +345,4 @@ def emulator_check(self): if __name__ == '__main__': self = PlatformWindows('alas') d = self.emulator_instance - print(d) \ No newline at end of file + print(d) From 3388c242a20f34b1b9509f26039be2c93517c183 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 01:38:07 +0800 Subject: [PATCH 068/161] Upd: fix bug. --- module/device/platform/api_windows.py | 68 +++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 39b158467b..e1d5415eae 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -194,36 +194,38 @@ def get_cmdline(pid: int) -> str: Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' """ - hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) - if not hProcess: - _except("OpenProcess failed.") - - # Query process infomation - pbi = PROCESS_BASIC_INFORMATION() - returnlength = SIZE() - status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) - if status != STATUS_SUCCESS: - _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) - - # Read PEB - peb = PEB() - if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read process parameters - upp = RTL_USER_PROCESS_PARAMETERS() - uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) - if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read command line - commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) - if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - _except("ReadProcessMemory failed.", handle=hProcess) - - CloseHandle(hProcess) - cmdline = wstring_at(addressof(commandLine), len(commandLine)) - + try: + hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) + if not hProcess: + _except("OpenProcess failed.") + + # Query process infomation + pbi = PROCESS_BASIC_INFORMATION() + returnlength = SIZE() + status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) + if status != STATUS_SUCCESS: + _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) + + # Read PEB + peb = PEB() + if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): + _except("ReadProcessMemory failed.", handle=hProcess) + + # Read process parameters + upp = RTL_USER_PROCESS_PARAMETERS() + uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) + if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + _except("ReadProcessMemory failed.", handle=hProcess) + + # Read command line + commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) + if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): + _except("ReadProcessMemory failed.", handle=hProcess) + + CloseHandle(hProcess) + cmdline = wstring_at(addressof(commandLine), len(commandLine)) + except OSError: + return '' return cmdline @@ -256,9 +258,8 @@ def _enum_processes(): _except("Process32Next failed.", statuscode=errorcode) _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: - pass - finally: CloseHandle(snapshot) + finally: del lppe32, snapshot @@ -291,9 +292,8 @@ def _enum_threads(): _except("Process32Next failed.", statuscode=errorcode) _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: - pass - finally: CloseHandle(snapshot) + finally: del lpte32, snapshot From 6207c622a56f60dc7461bc4b33bf7d1f20390667 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 01:40:03 +0800 Subject: [PATCH 069/161] Upd: fix bugs. --- module/device/{ => platform}/api_windows.py | 68 +++++++++---------- .../device/{ => platform}/winapi/__init__.py | 0 .../{ => platform}/winapi/const_windows.py | 0 .../winapi/functions_windows.py | 0 .../winapi/structures_windows.py | 0 5 files changed, 34 insertions(+), 34 deletions(-) rename module/device/{ => platform}/api_windows.py (89%) rename module/device/{ => platform}/winapi/__init__.py (100%) rename module/device/{ => platform}/winapi/const_windows.py (100%) rename module/device/{ => platform}/winapi/functions_windows.py (100%) rename module/device/{ => platform}/winapi/structures_windows.py (100%) diff --git a/module/device/api_windows.py b/module/device/platform/api_windows.py similarity index 89% rename from module/device/api_windows.py rename to module/device/platform/api_windows.py index 39b158467b..e1d5415eae 100644 --- a/module/device/api_windows.py +++ b/module/device/platform/api_windows.py @@ -194,36 +194,38 @@ def get_cmdline(pid: int) -> str: Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' """ - hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) - if not hProcess: - _except("OpenProcess failed.") - - # Query process infomation - pbi = PROCESS_BASIC_INFORMATION() - returnlength = SIZE() - status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) - if status != STATUS_SUCCESS: - _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) - - # Read PEB - peb = PEB() - if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read process parameters - upp = RTL_USER_PROCESS_PARAMETERS() - uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) - if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read command line - commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) - if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - _except("ReadProcessMemory failed.", handle=hProcess) - - CloseHandle(hProcess) - cmdline = wstring_at(addressof(commandLine), len(commandLine)) - + try: + hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) + if not hProcess: + _except("OpenProcess failed.") + + # Query process infomation + pbi = PROCESS_BASIC_INFORMATION() + returnlength = SIZE() + status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) + if status != STATUS_SUCCESS: + _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) + + # Read PEB + peb = PEB() + if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): + _except("ReadProcessMemory failed.", handle=hProcess) + + # Read process parameters + upp = RTL_USER_PROCESS_PARAMETERS() + uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) + if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + _except("ReadProcessMemory failed.", handle=hProcess) + + # Read command line + commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) + if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): + _except("ReadProcessMemory failed.", handle=hProcess) + + CloseHandle(hProcess) + cmdline = wstring_at(addressof(commandLine), len(commandLine)) + except OSError: + return '' return cmdline @@ -256,9 +258,8 @@ def _enum_processes(): _except("Process32Next failed.", statuscode=errorcode) _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: - pass - finally: CloseHandle(snapshot) + finally: del lppe32, snapshot @@ -291,9 +292,8 @@ def _enum_threads(): _except("Process32Next failed.", statuscode=errorcode) _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) except GeneratorExit: - pass - finally: CloseHandle(snapshot) + finally: del lpte32, snapshot diff --git a/module/device/winapi/__init__.py b/module/device/platform/winapi/__init__.py similarity index 100% rename from module/device/winapi/__init__.py rename to module/device/platform/winapi/__init__.py diff --git a/module/device/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py similarity index 100% rename from module/device/winapi/const_windows.py rename to module/device/platform/winapi/const_windows.py diff --git a/module/device/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py similarity index 100% rename from module/device/winapi/functions_windows.py rename to module/device/platform/winapi/functions_windows.py diff --git a/module/device/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py similarity index 100% rename from module/device/winapi/structures_windows.py rename to module/device/platform/winapi/structures_windows.py From b63fd01d1ee74575d076b3bc10cd48df06707962 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 05:24:56 +0800 Subject: [PATCH 070/161] Upd: Add context manager and fix logics. --- module/device/platform/api_windows.py | 291 +++++++----------- .../platform/winapi/functions_windows.py | 74 +++++ 2 files changed, 189 insertions(+), 176 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index e1d5415eae..02dc96929d 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -9,32 +9,76 @@ from module.device.platform.winapi.structures_windows import * from module.logger import logger -def _except( - msg: str = '', - statuscode: int = -1, - level: int = 40, - handle: int = 0, - raiseexcept: bool = True, - exception: type = OSError, -): + +def __yieldloop(entry32, snapshot, func: callable): + while 1: + yield entry32 + if func(snapshot, byref(entry32)): + continue + # Finished querying + errorcode = GetLastError() + if errorcode != ERROR_NO_MORE_FILES: + report(f"{func.__name__} failed.", statuscode=errorcode) + report("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) + + +def _enum_processes(): """ - Raise exception. + Enumerates all the processes currently running on the system. + + Yields: + lppe32 (PROCESSENTRY32) | + None (if enum failed) + """ + lppe32 = PROCESSENTRY32() + lppe32.dwSize = sizeof(PROCESSENTRY32) + with create_snapshot(TH32CS_SNAPPROCESS) as snapshot: + if not Process32First(snapshot, byref(lppe32)): + report("Process32First failed.") + yield from __yieldloop(lppe32, snapshot, Process32Next) + + +def _enum_threads(): + """ + Enumerates all the threads currintly running on the system. + + Yields: + lpte32 (THREADENTRY32) | + None (if enum failed) + """ + lpte32 = THREADENTRY32() + lpte32.dwSize = sizeof(THREADENTRY32) + with create_snapshot(TH32CS_SNAPTHREAD) as snapshot: + if not Thread32First(snapshot, byref(lpte32)): + report("Thread32First failed.") + yield from __yieldloop(lpte32, snapshot, Thread32Next) + + +def _get_process(pid: int): + """ + Get emulator's handle. Args: - msg (str): - statuscode (int): - level (int): Logging level - handle (int): Handle to close - raiseexcept (bool): Flag indicating whether to raise - exception (Type[Exception]): Exception class to raise + pid (int): Emulator's pid + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) """ - if statuscode == -1: - statuscode = GetLastError() - logger.log(level, f"{msg} Status code: {statuscode}") - if handle: - CloseHandle(handle) - if raiseexcept: - raise exception(statuscode) + tid = get_thread(pid) + try: + hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) + if not hProcess: + report("OpenProcess failed.", level=30, handle=hProcess) + + hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) + if not hThread: + report("OpenThread failed.", level=30, handle=hThread) + + return hProcess, hThread, pid, tid + except Exception as e: + logger.warning(f"Failed to get process and thread handles: {e}") + return None, None, pid, tid def getfocusedwindow(): @@ -53,7 +97,7 @@ def getfocusedwindow(): if GetWindowPlacement(hwnd, byref(wp)): return hwnd, wp else: - _except("Failed to get windowplacement.", level=30, raiseexcept=False) + report("Failed to get windowplacement.", level=30, raiseexcept=False) return hwnd, None def setforegroundwindow(focusedwindow: tuple = ()) -> bool: @@ -128,7 +172,7 @@ def execute(command: str, arg: bool = False): ) if not success: - _except("Failed to start emulator.", exception=EmulatorLaunchFailedError) + report("Failed to start emulator.", exception=EmulatorLaunchFailedError) process = ( lpProcessInformation.hProcess, @@ -146,10 +190,9 @@ def terminate_process(pid: int): Args: pid (int): Emulator's pid """ - hProcess = OpenProcess(PROCESS_TERMINATE, False, pid) - if TerminateProcess(hProcess, 0) == 0: - _except("Failed to kill process.", handle=hProcess) - CloseHandle(hProcess) + with open_process(PROCESS_TERMINATE, pid) as hProcess: + if TerminateProcess(hProcess, 0) == 0: + report("Failed to kill process.") return True @@ -178,7 +221,7 @@ def callback(hwnd: int, lparam): logger.error("Hwnd not found!") logger.error("1.Perhaps emulator was killed.") logger.error("2.Environment has something wrong. Please check the running environment.") - _except("Hwnd not found.", exception=HwndNotFoundError) + report("Hwnd not found.", exception=HwndNotFoundError) return hwnds @@ -195,108 +238,36 @@ def get_cmdline(pid: int) -> str: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' """ try: - hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) - if not hProcess: - _except("OpenProcess failed.") - - # Query process infomation - pbi = PROCESS_BASIC_INFORMATION() - returnlength = SIZE() - status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) - if status != STATUS_SUCCESS: - _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) - - # Read PEB - peb = PEB() - if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read process parameters - upp = RTL_USER_PROCESS_PARAMETERS() - uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) - if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read command line - commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) - if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - _except("ReadProcessMemory failed.", handle=hProcess) - - CloseHandle(hProcess) - cmdline = wstring_at(addressof(commandLine), len(commandLine)) + with open_process(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, pid) as hProcess: + # Query process infomation + pbi = PROCESS_BASIC_INFORMATION() + returnlength = SIZE() + status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) + if status != STATUS_SUCCESS: + report(f"NtQueryInformationProcess failed. Status: 0x{status}.") + + # Read PEB + peb = PEB() + if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): + report("Failed to read PEB.") + + # Read process parameters + upp = RTL_USER_PROCESS_PARAMETERS() + uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) + if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + report("Failed to read process parameters.") + + # Read command line + commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) + if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): + report("Failed to read command line.") + + cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: return '' return cmdline -def _enum_processes(): - """ - Enumerates all the processes currently running on the system. - - Yields: - lppe32 (PROCESSENTRY32) | - None (if enum failed) - """ - lppe32 = PROCESSENTRY32() - lppe32.dwSize = sizeof(PROCESSENTRY32) - snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) - if snapshot == INVALID_HANDLE_VALUE: - _except("CreateToolhelp32Snapshot failed.") - - if not Process32First(snapshot, byref(lppe32)): - _except("Process32First failed.", handle=snapshot) - - try: - while 1: - yield lppe32 - if Process32Next(snapshot, byref(lppe32)): - continue - # finished querying - errorcode = GetLastError() - if errorcode != ERROR_NO_MORE_FILES: - # error code != ERROR_NO_MORE_FILES, means that win api failed - _except("Process32Next failed.", statuscode=errorcode) - _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) - except GeneratorExit: - CloseHandle(snapshot) - finally: - del lppe32, snapshot - - -def _enum_threads(): - """ - Enumerates all the threads currintly running on the system. - - Yields: - lpte32 (THREADENTRY32) | - None (if enum failed) - """ - lpte32 = THREADENTRY32() - lpte32.dwSize = sizeof(THREADENTRY32) - snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, DWORD(0)) - if snapshot == INVALID_HANDLE_VALUE: - _except("CreateToolhelp32Snapshot failed.") - - if not Thread32First(snapshot, byref(lpte32)): - _except("Thread32First failed.", handle=snapshot) - - try: - while 1: - yield lpte32 - if Thread32Next(snapshot, byref(lpte32)): - continue - # finished querying - errorcode = GetLastError() - if errorcode != ERROR_NO_MORE_FILES: - # error code != ERROR_NO_MORE_FILES, means that win api failed - _except("Process32Next failed.", statuscode=errorcode) - _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) - except GeneratorExit: - CloseHandle(snapshot) - finally: - del lpte32, snapshot - - def kill_process_by_regex(regex: str) -> int: """ Kill processes with cmdline match the given regex. @@ -324,6 +295,22 @@ def kill_process_by_regex(regex: str) -> int: return count +def _get_thread_creation_time(tid): + with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: + creationtime = FILETIME() + exittime = FILETIME() + kerneltime = FILETIME() + usertime = FILETIME() + if not GetThreadTimes( + hThread, + byref(creationtime), + byref(exittime), + byref(kerneltime), + byref(usertime) + ): + return None + return creationtime.to_int() + def get_thread(pid: int): """ Get process's main thread id. @@ -337,69 +324,21 @@ def get_thread(pid: int): mainthreadid = 0 minstarttime = MAXULONGLONG threads = _enum_threads() - creationtime = FILETIME() - exittime = FILETIME() - kerneltime = FILETIME() - usertime = FILETIME() try: for lpte32 in threads: if lpte32.th32OwnerProcessID != pid: continue - hThread = OpenThread(THREAD_QUERY_INFORMATION, False, lpte32.th32ThreadID) - if not hThread: - CloseHandle(hThread) - continue - - if not GetThreadTimes( - hThread, - byref(creationtime), - byref(exittime), - byref(kerneltime), - byref(usertime) - ): - CloseHandle(hThread) - continue - - threadstarttime = creationtime.to_int() - if threadstarttime >= minstarttime: - CloseHandle(hThread) + threadstarttime = _get_thread_creation_time(lpte32.th32ThreadID) + if threadstarttime is None or threadstarttime >= minstarttime: continue minstarttime = threadstarttime mainthreadid = lpte32.th32ThreadID - CloseHandle(hThread) except IterationFinished: threads.close() return mainthreadid - -def _get_process(pid: int): - """ - Get emulator's handle. - - Args: - pid (int): Emulator's pid - - Returns: - tuple(processhandle, threadhandle, processid, mainthreadid) | - tuple(None, None, processid, mainthreadid) | (if enum_process() failed) - """ - mtid = get_thread(pid) - try: - processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid) - if not processhandle: - _except("OpenProcess failed.", level=30, handle=processhandle) - - threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mtid) - if not threadhandle: - _except("OpenThread failed.", level=30, handle=threadhandle) - - return processhandle, threadhandle, pid, mtid - except Exception as e: - logger.warning(f"Failed to get process and thread handles: {e}") - return None, None, pid, mtid - def get_process(instance: EmulatorInstance): """ Get emulator's process. diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 18167fcbba..ff44bb0be0 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -106,3 +106,77 @@ ReadProcessMemory = kernel32.ReadProcessMemory NtQueryInformationProcess = ntdll.NtQueryInformationProcess + +class Handle: + def __init__(self): + self.handle = None + + def __enter__(self): + return self.handle + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.handle: + CloseHandle(self.handle) + self.handle = None + +class ProcessHandle(Handle): + def __init__(self, access, pid): + super().__init__() + self.handle = OpenProcess(access, False, pid) + if not self.handle: + report("OpenProcess failed.") + +class ThreadHandle(Handle): + def __init__(self, access, tid): + super().__init__() + self.handle = OpenThread(access, False, tid) + if not self.handle: + report("OpenThread failed.") + +class CreateSnapshot(Handle): + def __init__(self, arg): + super().__init__() + self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) + from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE + if self.handle == INVALID_HANDLE_VALUE: + report("CreateToolhelp32Snapshot failed.") + +def report( + msg='', + statuscode=-1, + uselog=True, + level=40, + handle=0, + raiseexcept=True, + exception=OSError, +): + """ + Raise exception. + + Args: + msg (str): + statuscode (int): + uselog (bool): + level (int): Logging level + handle (int): Handle to close + raiseexcept (bool): Flag indicating whether to raise + exception (Type[Exception]): Exception class to raise + """ + from module.logger import logger + if statuscode == -1: + statuscode = GetLastError() + if uselog: + logger.log(level, f"{msg} Status code: {statuscode}") + if handle: + CloseHandle(handle) + if raiseexcept: + raise exception(statuscode) + +def open_process(access, pid): + return ProcessHandle(access, pid) + +def open_thread(access, tid): + return ThreadHandle(access, tid) + +def create_snapshot(arg): + return CreateSnapshot(arg) From c7e77611cd4f078b0cd529de22a8f3e01b781455 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 05:27:33 +0800 Subject: [PATCH 071/161] Upd: Add context manager and fix logics. --- module/device/platform/api_windows.py | 291 +++++++----------- .../platform/winapi/functions_windows.py | 74 +++++ 2 files changed, 189 insertions(+), 176 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index e1d5415eae..02dc96929d 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -9,32 +9,76 @@ from module.device.platform.winapi.structures_windows import * from module.logger import logger -def _except( - msg: str = '', - statuscode: int = -1, - level: int = 40, - handle: int = 0, - raiseexcept: bool = True, - exception: type = OSError, -): + +def __yieldloop(entry32, snapshot, func: callable): + while 1: + yield entry32 + if func(snapshot, byref(entry32)): + continue + # Finished querying + errorcode = GetLastError() + if errorcode != ERROR_NO_MORE_FILES: + report(f"{func.__name__} failed.", statuscode=errorcode) + report("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) + + +def _enum_processes(): """ - Raise exception. + Enumerates all the processes currently running on the system. + + Yields: + lppe32 (PROCESSENTRY32) | + None (if enum failed) + """ + lppe32 = PROCESSENTRY32() + lppe32.dwSize = sizeof(PROCESSENTRY32) + with create_snapshot(TH32CS_SNAPPROCESS) as snapshot: + if not Process32First(snapshot, byref(lppe32)): + report("Process32First failed.") + yield from __yieldloop(lppe32, snapshot, Process32Next) + + +def _enum_threads(): + """ + Enumerates all the threads currintly running on the system. + + Yields: + lpte32 (THREADENTRY32) | + None (if enum failed) + """ + lpte32 = THREADENTRY32() + lpte32.dwSize = sizeof(THREADENTRY32) + with create_snapshot(TH32CS_SNAPTHREAD) as snapshot: + if not Thread32First(snapshot, byref(lpte32)): + report("Thread32First failed.") + yield from __yieldloop(lpte32, snapshot, Thread32Next) + + +def _get_process(pid: int): + """ + Get emulator's handle. Args: - msg (str): - statuscode (int): - level (int): Logging level - handle (int): Handle to close - raiseexcept (bool): Flag indicating whether to raise - exception (Type[Exception]): Exception class to raise + pid (int): Emulator's pid + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) """ - if statuscode == -1: - statuscode = GetLastError() - logger.log(level, f"{msg} Status code: {statuscode}") - if handle: - CloseHandle(handle) - if raiseexcept: - raise exception(statuscode) + tid = get_thread(pid) + try: + hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) + if not hProcess: + report("OpenProcess failed.", level=30, handle=hProcess) + + hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) + if not hThread: + report("OpenThread failed.", level=30, handle=hThread) + + return hProcess, hThread, pid, tid + except Exception as e: + logger.warning(f"Failed to get process and thread handles: {e}") + return None, None, pid, tid def getfocusedwindow(): @@ -53,7 +97,7 @@ def getfocusedwindow(): if GetWindowPlacement(hwnd, byref(wp)): return hwnd, wp else: - _except("Failed to get windowplacement.", level=30, raiseexcept=False) + report("Failed to get windowplacement.", level=30, raiseexcept=False) return hwnd, None def setforegroundwindow(focusedwindow: tuple = ()) -> bool: @@ -128,7 +172,7 @@ def execute(command: str, arg: bool = False): ) if not success: - _except("Failed to start emulator.", exception=EmulatorLaunchFailedError) + report("Failed to start emulator.", exception=EmulatorLaunchFailedError) process = ( lpProcessInformation.hProcess, @@ -146,10 +190,9 @@ def terminate_process(pid: int): Args: pid (int): Emulator's pid """ - hProcess = OpenProcess(PROCESS_TERMINATE, False, pid) - if TerminateProcess(hProcess, 0) == 0: - _except("Failed to kill process.", handle=hProcess) - CloseHandle(hProcess) + with open_process(PROCESS_TERMINATE, pid) as hProcess: + if TerminateProcess(hProcess, 0) == 0: + report("Failed to kill process.") return True @@ -178,7 +221,7 @@ def callback(hwnd: int, lparam): logger.error("Hwnd not found!") logger.error("1.Perhaps emulator was killed.") logger.error("2.Environment has something wrong. Please check the running environment.") - _except("Hwnd not found.", exception=HwndNotFoundError) + report("Hwnd not found.", exception=HwndNotFoundError) return hwnds @@ -195,108 +238,36 @@ def get_cmdline(pid: int) -> str: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' """ try: - hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) - if not hProcess: - _except("OpenProcess failed.") - - # Query process infomation - pbi = PROCESS_BASIC_INFORMATION() - returnlength = SIZE() - status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) - if status != STATUS_SUCCESS: - _except(f"NtQueryInformationProcess failed. Status: 0x{status}.", handle=hProcess) - - # Read PEB - peb = PEB() - if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read process parameters - upp = RTL_USER_PROCESS_PARAMETERS() - uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) - if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - _except("ReadProcessMemory failed.", handle=hProcess) - - # Read command line - commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) - if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - _except("ReadProcessMemory failed.", handle=hProcess) - - CloseHandle(hProcess) - cmdline = wstring_at(addressof(commandLine), len(commandLine)) + with open_process(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, pid) as hProcess: + # Query process infomation + pbi = PROCESS_BASIC_INFORMATION() + returnlength = SIZE() + status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) + if status != STATUS_SUCCESS: + report(f"NtQueryInformationProcess failed. Status: 0x{status}.") + + # Read PEB + peb = PEB() + if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): + report("Failed to read PEB.") + + # Read process parameters + upp = RTL_USER_PROCESS_PARAMETERS() + uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) + if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + report("Failed to read process parameters.") + + # Read command line + commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) + if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): + report("Failed to read command line.") + + cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: return '' return cmdline -def _enum_processes(): - """ - Enumerates all the processes currently running on the system. - - Yields: - lppe32 (PROCESSENTRY32) | - None (if enum failed) - """ - lppe32 = PROCESSENTRY32() - lppe32.dwSize = sizeof(PROCESSENTRY32) - snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, DWORD(0)) - if snapshot == INVALID_HANDLE_VALUE: - _except("CreateToolhelp32Snapshot failed.") - - if not Process32First(snapshot, byref(lppe32)): - _except("Process32First failed.", handle=snapshot) - - try: - while 1: - yield lppe32 - if Process32Next(snapshot, byref(lppe32)): - continue - # finished querying - errorcode = GetLastError() - if errorcode != ERROR_NO_MORE_FILES: - # error code != ERROR_NO_MORE_FILES, means that win api failed - _except("Process32Next failed.", statuscode=errorcode) - _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) - except GeneratorExit: - CloseHandle(snapshot) - finally: - del lppe32, snapshot - - -def _enum_threads(): - """ - Enumerates all the threads currintly running on the system. - - Yields: - lpte32 (THREADENTRY32) | - None (if enum failed) - """ - lpte32 = THREADENTRY32() - lpte32.dwSize = sizeof(THREADENTRY32) - snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, DWORD(0)) - if snapshot == INVALID_HANDLE_VALUE: - _except("CreateToolhelp32Snapshot failed.") - - if not Thread32First(snapshot, byref(lpte32)): - _except("Thread32First failed.", handle=snapshot) - - try: - while 1: - yield lpte32 - if Thread32Next(snapshot, byref(lpte32)): - continue - # finished querying - errorcode = GetLastError() - if errorcode != ERROR_NO_MORE_FILES: - # error code != ERROR_NO_MORE_FILES, means that win api failed - _except("Process32Next failed.", statuscode=errorcode) - _except("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) - except GeneratorExit: - CloseHandle(snapshot) - finally: - del lpte32, snapshot - - def kill_process_by_regex(regex: str) -> int: """ Kill processes with cmdline match the given regex. @@ -324,6 +295,22 @@ def kill_process_by_regex(regex: str) -> int: return count +def _get_thread_creation_time(tid): + with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: + creationtime = FILETIME() + exittime = FILETIME() + kerneltime = FILETIME() + usertime = FILETIME() + if not GetThreadTimes( + hThread, + byref(creationtime), + byref(exittime), + byref(kerneltime), + byref(usertime) + ): + return None + return creationtime.to_int() + def get_thread(pid: int): """ Get process's main thread id. @@ -337,69 +324,21 @@ def get_thread(pid: int): mainthreadid = 0 minstarttime = MAXULONGLONG threads = _enum_threads() - creationtime = FILETIME() - exittime = FILETIME() - kerneltime = FILETIME() - usertime = FILETIME() try: for lpte32 in threads: if lpte32.th32OwnerProcessID != pid: continue - hThread = OpenThread(THREAD_QUERY_INFORMATION, False, lpte32.th32ThreadID) - if not hThread: - CloseHandle(hThread) - continue - - if not GetThreadTimes( - hThread, - byref(creationtime), - byref(exittime), - byref(kerneltime), - byref(usertime) - ): - CloseHandle(hThread) - continue - - threadstarttime = creationtime.to_int() - if threadstarttime >= minstarttime: - CloseHandle(hThread) + threadstarttime = _get_thread_creation_time(lpte32.th32ThreadID) + if threadstarttime is None or threadstarttime >= minstarttime: continue minstarttime = threadstarttime mainthreadid = lpte32.th32ThreadID - CloseHandle(hThread) except IterationFinished: threads.close() return mainthreadid - -def _get_process(pid: int): - """ - Get emulator's handle. - - Args: - pid (int): Emulator's pid - - Returns: - tuple(processhandle, threadhandle, processid, mainthreadid) | - tuple(None, None, processid, mainthreadid) | (if enum_process() failed) - """ - mtid = get_thread(pid) - try: - processhandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid) - if not processhandle: - _except("OpenProcess failed.", level=30, handle=processhandle) - - threadhandle = OpenThread(THREAD_ALL_ACCESS, False, mtid) - if not threadhandle: - _except("OpenThread failed.", level=30, handle=threadhandle) - - return processhandle, threadhandle, pid, mtid - except Exception as e: - logger.warning(f"Failed to get process and thread handles: {e}") - return None, None, pid, mtid - def get_process(instance: EmulatorInstance): """ Get emulator's process. diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 18167fcbba..ff44bb0be0 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -106,3 +106,77 @@ ReadProcessMemory = kernel32.ReadProcessMemory NtQueryInformationProcess = ntdll.NtQueryInformationProcess + +class Handle: + def __init__(self): + self.handle = None + + def __enter__(self): + return self.handle + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.handle: + CloseHandle(self.handle) + self.handle = None + +class ProcessHandle(Handle): + def __init__(self, access, pid): + super().__init__() + self.handle = OpenProcess(access, False, pid) + if not self.handle: + report("OpenProcess failed.") + +class ThreadHandle(Handle): + def __init__(self, access, tid): + super().__init__() + self.handle = OpenThread(access, False, tid) + if not self.handle: + report("OpenThread failed.") + +class CreateSnapshot(Handle): + def __init__(self, arg): + super().__init__() + self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) + from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE + if self.handle == INVALID_HANDLE_VALUE: + report("CreateToolhelp32Snapshot failed.") + +def report( + msg='', + statuscode=-1, + uselog=True, + level=40, + handle=0, + raiseexcept=True, + exception=OSError, +): + """ + Raise exception. + + Args: + msg (str): + statuscode (int): + uselog (bool): + level (int): Logging level + handle (int): Handle to close + raiseexcept (bool): Flag indicating whether to raise + exception (Type[Exception]): Exception class to raise + """ + from module.logger import logger + if statuscode == -1: + statuscode = GetLastError() + if uselog: + logger.log(level, f"{msg} Status code: {statuscode}") + if handle: + CloseHandle(handle) + if raiseexcept: + raise exception(statuscode) + +def open_process(access, pid): + return ProcessHandle(access, pid) + +def open_thread(access, tid): + return ThreadHandle(access, tid) + +def create_snapshot(arg): + return CreateSnapshot(arg) From bb1642933eae9cb880adfe7384fabd52e379dec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9C=9E=E9=A3=9B?= Date: Fri, 28 Jun 2024 13:13:28 +0800 Subject: [PATCH 072/161] fix: add mxnet-alas for linux (#3938) --- deploy/docker/requirements.txt | 3 ++- deploy/docker/requirements_generator.py | 5 +++++ deploy/headless/requirements.txt | 3 ++- deploy/headless/requirements_generator.py | 5 +++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/deploy/docker/requirements.txt b/deploy/docker/requirements.txt index 27ad150990..d6c8af5824 100644 --- a/deploy/docker/requirements.txt +++ b/deploy/docker/requirements.txt @@ -40,4 +40,5 @@ pywebio==1.6.2 starlette==0.14.2 uvicorn[standard]==0.17.6 zerorpc==0.6.3 -pyzmq==22.3.0 \ No newline at end of file +pyzmq==22.3.0 +mxnet-alas==0.0.5 \ No newline at end of file diff --git a/deploy/docker/requirements_generator.py b/deploy/docker/requirements_generator.py index 0e13db8d91..10ee2d092a 100644 --- a/deploy/docker/requirements_generator.py +++ b/deploy/docker/requirements_generator.py @@ -41,6 +41,9 @@ def docker_requirements_generate(requirements_in='requirements-in.txt'): logger.info(f'Generate requirements for Docker image') lock = {} + expand = { + 'mxnet-alas': '0.0.5' + } new = {} logger.info(requirements) for name, version in requirements.items(): @@ -54,6 +57,8 @@ def docker_requirements_generate(requirements_in='requirements-in.txt'): version = lock[name] if not isinstance(lock[name], dict) else lock[name]['version'] name = name if not isinstance(lock[name], dict) else lock[name]['name'] new[name] = version + for name, version in expand.items(): + new[name] = version write_file(os.path.join(BASE_FOLDER, f'./requirements.txt'), data=new) diff --git a/deploy/headless/requirements.txt b/deploy/headless/requirements.txt index b10cef4d5a..6711493aa0 100644 --- a/deploy/headless/requirements.txt +++ b/deploy/headless/requirements.txt @@ -40,4 +40,5 @@ pywebio==1.6.2 starlette==0.14.2 uvicorn[standard]==0.17.6 zerorpc==0.6.3 -pyzmq==22.3.0 \ No newline at end of file +pyzmq==22.3.0 +mxnet-alas==0.0.5 \ No newline at end of file diff --git a/deploy/headless/requirements_generator.py b/deploy/headless/requirements_generator.py index 9c3c1ddbde..4f58e001fd 100644 --- a/deploy/headless/requirements_generator.py +++ b/deploy/headless/requirements_generator.py @@ -57,6 +57,9 @@ def headless_requirements_generate(requirements_in='requirements-in.txt'): 'tqdm': '4.65.0', 'wrapt': '1.15.0' } + expand = { + 'mxnet-alas': '0.0.5' + } new = {} logger.info(requirements) for name, version in requirements.items(): @@ -66,6 +69,8 @@ def headless_requirements_generate(requirements_in='requirements-in.txt'): version = lock[name] if not isinstance(lock[name], dict) else lock[name]['version'] name = name if not isinstance(lock[name], dict) else lock[name]['name'] new[name] = version + for name, version in expand.items(): + new[name] = version write_file(os.path.join(BASE_FOLDER, f'./requirements.txt'), data=new) From 5d3c1e4e5e6176fdc0d60eed6d62f82624fd6852 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 13:48:32 +0800 Subject: [PATCH 073/161] Upd: fix log. --- alas.py | 2 +- config/template.json | 3 +- module/device/platform/api_windows.py | 79 +++++++++++-------- module/device/platform/platform_windows.py | 10 ++- .../platform/winapi/functions_windows.py | 6 +- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/alas.py b/alas.py index 9493f22789..b2e75c0225 100644 --- a/alas.py +++ b/alas.py @@ -445,7 +445,7 @@ def emurestart(self, task): logger.warning('Emulator is not running') self.device.emulator_stop() self.device.emulator_start() - if not task == 'Restart': + if task != 'Restart': self.run('start') del_cached_property(self, 'config') diff --git a/config/template.json b/config/template.json index 0ba528e8cf..898c1bd295 100644 --- a/config/template.json +++ b/config/template.json @@ -26,7 +26,8 @@ "CombatScreenshotInterval": 1.0, "TaskHoardingDuration": 0, "WhenTaskQueueEmpty": "goto_main", - "ProcessBufferTime": 10 + "ProcessBufferTime": 10, + "BufferMethod": "stay_there" }, "DropRecord": { "SaveFolder": "./screenshots", diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 02dc96929d..337fb439c7 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -19,7 +19,7 @@ def __yieldloop(entry32, snapshot, func: callable): errorcode = GetLastError() if errorcode != ERROR_NO_MORE_FILES: report(f"{func.__name__} failed.", statuscode=errorcode) - report("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) + report("Finished querying.", statuscode=errorcode, uselog=False, exception=IterationFinished) def _enum_processes(): @@ -54,33 +54,6 @@ def _enum_threads(): yield from __yieldloop(lpte32, snapshot, Thread32Next) -def _get_process(pid: int): - """ - Get emulator's handle. - - Args: - pid (int): Emulator's pid - - Returns: - tuple(processhandle, threadhandle, processid, mainthreadid) | - tuple(None, None, processid, mainthreadid) | (if enum_process() failed) - """ - tid = get_thread(pid) - try: - hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) - if not hProcess: - report("OpenProcess failed.", level=30, handle=hProcess) - - hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) - if not hThread: - report("OpenThread failed.", level=30, handle=hThread) - - return hProcess, hThread, pid, tid - except Exception as e: - logger.warning(f"Failed to get process and thread handles: {e}") - return None, None, pid, tid - - def getfocusedwindow(): """ Get focused window. @@ -120,13 +93,13 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def execute(command: str, arg: bool = False): +def execute(command: str, sstart: bool = False): """ Create a new process. Args: command (str): process's commandline - arg (bool): process's windowplacement + sstart (bool): process's windowplacement Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' @@ -153,7 +126,7 @@ def execute(command: str, arg: bool = False): lpStartupInfo = STARTUPINFO() lpStartupInfo.cb = sizeof(STARTUPINFO) lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW - lpStartupInfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE + lpStartupInfo.wShowWindow = SW_HIDE if sstart else SW_MINIMIZE lpProcessInformation = PROCESS_INFORMATION() focusedwindow = getfocusedwindow() @@ -244,23 +217,23 @@ def get_cmdline(pid: int) -> str: returnlength = SIZE() status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: - report(f"NtQueryInformationProcess failed. Status: 0x{status}.") + report(f"NtQueryInformationProcess failed. Status: 0x{status}.", level=30) # Read PEB peb = PEB() if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - report("Failed to read PEB.") + report("Failed to read PEB.", level=30) # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - report("Failed to read process parameters.") + report("Failed to read process parameters.", level=30) # Read command line commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - report("Failed to read command line.") + report("Failed to read command line.", level=30) cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: @@ -296,6 +269,15 @@ def kill_process_by_regex(regex: str) -> int: def _get_thread_creation_time(tid): + """ + Get thread's creation time. + + Args: + tid (int): Thread id + + Returns: + threadstarttime (int): Thread's start time + """ with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: creationtime = FILETIME() exittime = FILETIME() @@ -339,6 +321,33 @@ def get_thread(pid: int): threads.close() return mainthreadid + +def _get_process(pid: int): + """ + Get emulator's handle. + + Args: + pid (int): Emulator's pid + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + """ + tid = get_thread(pid) + try: + hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) + if not hProcess: + report("OpenProcess failed.", level=30, handle=hProcess) + + hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) + if not hThread: + report("OpenThread failed.", level=30, handle=hThread) + + return hProcess, hThread, pid, tid + except Exception as e: + logger.warning(f"Failed to get process and thread handles: {e}") + return None, None, pid, tid + def get_process(instance: EmulatorInstance): """ Get emulator's process. diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 90f4fa63e8..06d29fb1e3 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -22,10 +22,14 @@ def execute(self, command: str): command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') if self.config.Emulator_SilentStart == 'normal': - arg = False + sstart = False else: - arg = True - self.process, self.focusedwindow = api_windows.execute(command, arg) + sstart = True + if self.process is not None: + if self.process[0] is not None and self.process[1] is not None: + api_windows.CloseHandle(self.process[0]) + api_windows.CloseHandle(self.process[1]) + self.process, self.focusedwindow = api_windows.execute(command, sstart) logger.info(f"Current window: {self.focusedwindow[0]}") return True diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index ff44bb0be0..a59376fe24 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -124,14 +124,14 @@ def __init__(self, access, pid): super().__init__() self.handle = OpenProcess(access, False, pid) if not self.handle: - report("OpenProcess failed.") + report("OpenProcess failed.", uselog=False) class ThreadHandle(Handle): def __init__(self, access, tid): super().__init__() self.handle = OpenThread(access, False, tid) if not self.handle: - report("OpenThread failed.") + report("OpenThread failed.", uselog=False) class CreateSnapshot(Handle): def __init__(self, arg): @@ -139,7 +139,7 @@ def __init__(self, arg): self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE if self.handle == INVALID_HANDLE_VALUE: - report("CreateToolhelp32Snapshot failed.") + report("CreateToolhelp32Snapshot failed.", uselog=False) def report( msg='', From 835098f564eb9374590e48341ef7f70c0bc84364 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 13:59:38 +0800 Subject: [PATCH 074/161] Upd: add slient start support. --- config/template.json | 7 +- module/config/argument/args.json | 23 +++ module/config/argument/argument.yaml | 11 ++ module/config/config_generated.py | 5 +- module/config/i18n/en-US.json | 21 +- module/config/i18n/ja-JP.json | 19 ++ module/config/i18n/zh-CN.json | 19 ++ module/config/i18n/zh-TW.json | 21 +- module/device/device.py | 41 ++++ module/device/platform/api_windows.py | 79 ++++---- module/device/platform/platform_base.py | 18 +- module/device/platform/platform_windows.py | 187 +++++++++--------- .../platform/winapi/functions_windows.py | 6 +- 13 files changed, 322 insertions(+), 135 deletions(-) diff --git a/config/template.json b/config/template.json index 3d09e1b2ef..898c1bd295 100644 --- a/config/template.json +++ b/config/template.json @@ -7,7 +7,8 @@ "ScreenshotMethod": "auto", "ControlMethod": "minitouch", "ScreenshotDedithering": false, - "AdbRestart": false + "AdbRestart": false, + "SilentStart": "normal" }, "EmulatorInfo": { "Emulator": "auto", @@ -24,7 +25,9 @@ "ScreenshotInterval": 0.3, "CombatScreenshotInterval": 1.0, "TaskHoardingDuration": 0, - "WhenTaskQueueEmpty": "goto_main" + "WhenTaskQueueEmpty": "goto_main", + "ProcessBufferTime": 10, + "BufferMethod": "stay_there" }, "DropRecord": { "SaveFolder": "./screenshots", diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 30d6cd93b4..5e5bc9e5ac 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -138,6 +138,15 @@ "AdbRestart": { "type": "checkbox", "value": false + }, + "SilentStart": { + "type": "select", + "value": "normal", + "option": [ + "normal", + "minimize", + "silent" + ] } }, "EmulatorInfo": { @@ -205,6 +214,20 @@ "WhenTaskQueueEmpty": { "type": "select", "value": "goto_main", + "option": [ + "stay_there", + "goto_main", + "close_game", + "stop_emulator" + ] + }, + "ProcessBufferTime": { + "type": "input", + "value": 10 + }, + "BufferMethod": { + "type": "select", + "value": "stay_there", "option": [ "stay_there", "goto_main", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 9319a845f5..5e32e02d0e 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -55,6 +55,13 @@ Emulator: ] ScreenshotDedithering: false AdbRestart: false + SilentStart: + value: normal + option: [ + normal, + minimize, + silent + ] EmulatorInfo: Emulator: value: auto @@ -94,6 +101,10 @@ Optimization: TaskHoardingDuration: 0 WhenTaskQueueEmpty: value: goto_main + option: [ stay_there, goto_main, close_game, stop_emulator ] + ProcessBufferTime: 10 + BufferMethod: + value: stay_there option: [ stay_there, goto_main, close_game ] DropRecord: SaveFolder: ./screenshots diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 7c770d04d1..f90c991598 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -25,6 +25,7 @@ class GeneratedConfig: Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False + Emulator_SilentStart = 'normal' # normal, minimize, silent # Group `EmulatorInfo` EmulatorInfo_Emulator = 'auto' # auto, NoxPlayer, NoxPlayer64, BlueStacks4, BlueStacks5, BlueStacks4HyperV, BlueStacks5HyperV, LDPlayer3, LDPlayer4, LDPlayer9, MuMuPlayer, MuMuPlayerX, MuMuPlayer12, MEmuPlayer @@ -41,7 +42,9 @@ class GeneratedConfig: Optimization_ScreenshotInterval = 0.3 Optimization_CombatScreenshotInterval = 1.0 Optimization_TaskHoardingDuration = 0 - Optimization_WhenTaskQueueEmpty = 'goto_main' # stay_there, goto_main, close_game + Optimization_WhenTaskQueueEmpty = 'goto_main' # stay_there, goto_main, close_game, stop_emulator + Optimization_ProcessBufferTime = 10 + Optimization_BufferMethod = 'stay_there' # Group `DropRecord` DropRecord_SaveFolder = './screenshots' diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index d7d04f2d7a..5d061d44b0 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -428,6 +428,13 @@ "AdbRestart": { "name": "Try to restart adb when no device found", "help": "" + }, + "SilentStart": { + "name": "Start the emulator silently", + "help": "When the simulator is started by Alas, selecting 'Minimize mode' will cause the simulator to run minimally and selecting 'Silent mode' will cause the simulator to run silently.", + "normal": "Normal mode", + "minimize": "Minimize mode", + "silent": "Silent mode" } }, "EmulatorInfo": { @@ -506,7 +513,19 @@ "help": "Close AL when there are no pending tasks, can help reduce CPU", "stay_there": "Stay There", "goto_main": "Goto Main Page", - "close_game": "Close Game" + "close_game": "Close Game", + "stop_emulator": "Stop Emulator" + }, + "ProcessBufferTime": { + "name": "Emulator turns off buffering for X minutes", + "help": "When the current time is less than X minutes from the next task, ALAS will switch from 'stop_emulator' to 'stay_there', preventing frequent start-stop cycles of the emulator.\nThis setting takes effect when the option is 'stop emulator'." + }, + "BufferMethod": { + "name": "Behavior during buffer time", + "help": "", + "stay_there": "stay_there", + "goto_main": "goto_main", + "close_game": "close_game" } }, "DropRecord": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index b8d8cf7184..8f94b827fe 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -428,6 +428,13 @@ "AdbRestart": { "name": "Emulator.AdbRestart.name", "help": "Emulator.AdbRestart.help" + }, + "SilentStart": { + "name": "Emulator.SilentStart.name", + "help": "Emulator.SilentStart.help", + "normal": "Normal mode", + "minimize": "Minimize mode", + "silent": "Silent mode" } }, "EmulatorInfo": { @@ -506,6 +513,18 @@ "help": "Optimization.WhenTaskQueueEmpty.help", "stay_there": "stay_there", "goto_main": "goto_main", + "close_game": "close_game", + "stop_emulator": "stop_emulator" + }, + "ProcessBufferTime": { + "name": "Optimization.ProcessBufferTime.name", + "help": "Optimization.ProcessBufferTime.help" + }, + "BufferMethod": { + "name": "Optimization.WhenTaskQueueEmpty.name", + "help": "", + "stay_there": "stay_there", + "goto_main": "goto_main", "close_game": "close_game" } }, diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 8362c18269..62ce20af53 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -428,6 +428,13 @@ "AdbRestart": { "name": "在检测不到设备的时候尝试重启adb", "help": "" + }, + "SilentStart": { + "name": "静默启动模拟器", + "help": "由Alas启动模拟器时,选择最小化将使模拟器最小化运行,选择静默将使模拟器静默运行", + "normal": "常规模式", + "minimize": "最小化模式", + "silent": "静默模式" } }, "EmulatorInfo": { @@ -506,6 +513,18 @@ "help": "无任务时关闭游戏,能在收菜期间降低 CPU 占用", "stay_there": "停在原处", "goto_main": "前往主界面", + "close_game": "关闭游戏", + "stop_emulator": "关闭模拟器" + }, + "ProcessBufferTime": { + "name": "模拟器关闭缓冲 X 分钟", + "help": "当前时间距离下个任务小于 X 分钟时,alas将由关闭模拟器转为停在原处,能避免频繁启停模拟器\n当任务队列清空后关闭模拟器,此设置生效" + }, + "BufferMethod": { + "name": "缓冲时间内", + "help": "", + "stay_there": "停在原处", + "goto_main": "前往主界面", "close_game": "关闭游戏" } }, diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 0dd4a4da18..966799472e 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -428,6 +428,13 @@ "AdbRestart": { "name": "在檢測不到設備的時候嘗試重啟adb", "help": "" + }, + "SilentStart": { + "name": "靜默啓働模擬器", + "help": "由Alas啓動模擬器時,選擇最小化將使模擬器最小化運行,選擇靜默將使模擬器靜默運行", + "normal": "常規模式", + "minimize": "最小化模式", + "silent": "靜默模式" } }, "EmulatorInfo": { @@ -506,6 +513,18 @@ "help": "無任務時關閉遊戲,能在收菜期間降低 CPU 佔用", "stay_there": "停在原處", "goto_main": "前往主界面", + "close_game": "關閉遊戲", + "stop_emulator": "關閉模擬器" + }, + "ProcessBufferTime": { + "name": "模擬器關閉緩衝 X 分鐘", + "help": "噹前時間距離下箇任務小於 X 分鐘時,alas將由關閉模擬器轉爲停在原處,能避免頻緐啓停模擬器\n噹任務隊列清空後關閉模擬器,此設置生傚" + }, + "BufferMethod": { + "name": "緩衝時間内", + "help": "", + "stay_there": "停在原處", + "goto_main": "前往主界面", "close_game": "關閉遊戲" } }, @@ -719,7 +738,7 @@ "event_20230803_cn": "奏響鳶尾之歌", "event_20230817_cn": "愚者的天平", "event_20230914_cn": "Effulgence Before Eclipse", - "event_20231026_cn": "飓風與青春之泉", + "event_20231026_cn": "Tempesta and the Fountain of Youth", "event_20231123_cn": "蒼閃忍法帖", "event_20231221_cn": "Light-Chasing Sea of Stars", "event_20240229_cn": "Snowrealm Peregrination", diff --git a/module/device/device.py b/module/device/device.py index c9526837c2..1e66353279 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -67,6 +67,7 @@ class Device(Screenshot, Control, AppControl): stuck_long_wait_list = ['BATTLE_STATUS_S', 'PAUSE', 'LOGIN_CHECK'] def __init__(self, *args, **kwargs): + self.initialized = False for _ in range(2): try: super().__init__(*args, **kwargs) @@ -100,6 +101,10 @@ def __init__(self, *args, **kwargs): if self.config.Emulator_ControlMethod == 'minitouch': self.early_minitouch_init() + if not self.initialized: + self.initialized = True + self.switch_window() + def run_simple_screenshot_benchmark(self): """ Perform a screenshot method benchmark, test 3 times on each method. @@ -131,6 +136,9 @@ def method_check(self): # self.config.Emulator_ControlMethod = 'minitouch' pass + def emulator_check(self): + return super().emulator_check() + def handle_night_commission(self, daily_trigger='21:00', threshold=30): """ Args: @@ -306,6 +314,39 @@ def app_stop(self): super().app_stop() self.stuck_record_clear() self.click_record_clear() + + def emulator_stop(self): + # kill emulator + if self.emulator_instance is not None: + super().emulator_stop() + else: + logger.critical( + f'No emulator with serial "{self.config.Emulator_Serial}" found, ' + f'please set a correct serial' + ) + raise + self.stuck_record_clear() + self.click_record_clear() + + def emulator_start(self): + # start emulator + if self.emulator_instance is not None: + super().emulator_start() + else: + logger.critical( + f'No emulator with serial "{self.config.Emulator_Serial}" found, ' + f'please set a correct serial' + ) + raise + if not self.initialized: + self.initialized = True + self.switch_window() + self.stuck_record_clear() + self.click_record_clear() + + def switch_window(self): + return super().switch_window() + if __name__ == '__main__': self = Device('alas') # self.maatouch_uninstall() diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 02dc96929d..337fb439c7 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -19,7 +19,7 @@ def __yieldloop(entry32, snapshot, func: callable): errorcode = GetLastError() if errorcode != ERROR_NO_MORE_FILES: report(f"{func.__name__} failed.", statuscode=errorcode) - report("Finished querying.", statuscode=errorcode, level=20, exception=IterationFinished) + report("Finished querying.", statuscode=errorcode, uselog=False, exception=IterationFinished) def _enum_processes(): @@ -54,33 +54,6 @@ def _enum_threads(): yield from __yieldloop(lpte32, snapshot, Thread32Next) -def _get_process(pid: int): - """ - Get emulator's handle. - - Args: - pid (int): Emulator's pid - - Returns: - tuple(processhandle, threadhandle, processid, mainthreadid) | - tuple(None, None, processid, mainthreadid) | (if enum_process() failed) - """ - tid = get_thread(pid) - try: - hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) - if not hProcess: - report("OpenProcess failed.", level=30, handle=hProcess) - - hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) - if not hThread: - report("OpenThread failed.", level=30, handle=hThread) - - return hProcess, hThread, pid, tid - except Exception as e: - logger.warning(f"Failed to get process and thread handles: {e}") - return None, None, pid, tid - - def getfocusedwindow(): """ Get focused window. @@ -120,13 +93,13 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def execute(command: str, arg: bool = False): +def execute(command: str, sstart: bool = False): """ Create a new process. Args: command (str): process's commandline - arg (bool): process's windowplacement + sstart (bool): process's windowplacement Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' @@ -153,7 +126,7 @@ def execute(command: str, arg: bool = False): lpStartupInfo = STARTUPINFO() lpStartupInfo.cb = sizeof(STARTUPINFO) lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW - lpStartupInfo.wShowWindow = SW_HIDE if arg else SW_MINIMIZE + lpStartupInfo.wShowWindow = SW_HIDE if sstart else SW_MINIMIZE lpProcessInformation = PROCESS_INFORMATION() focusedwindow = getfocusedwindow() @@ -244,23 +217,23 @@ def get_cmdline(pid: int) -> str: returnlength = SIZE() status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: - report(f"NtQueryInformationProcess failed. Status: 0x{status}.") + report(f"NtQueryInformationProcess failed. Status: 0x{status}.", level=30) # Read PEB peb = PEB() if not ReadProcessMemory(hProcess, pbi.PebBaseAddress, byref(peb), sizeof(peb), None): - report("Failed to read PEB.") + report("Failed to read PEB.", level=30) # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): - report("Failed to read process parameters.") + report("Failed to read process parameters.", level=30) # Read command line commandLine = create_unicode_buffer(upp.CommandLine.Length // 2) if not ReadProcessMemory(hProcess, upp.CommandLine.Buffer, commandLine, upp.CommandLine.Length, None): - report("Failed to read command line.") + report("Failed to read command line.", level=30) cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: @@ -296,6 +269,15 @@ def kill_process_by_regex(regex: str) -> int: def _get_thread_creation_time(tid): + """ + Get thread's creation time. + + Args: + tid (int): Thread id + + Returns: + threadstarttime (int): Thread's start time + """ with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: creationtime = FILETIME() exittime = FILETIME() @@ -339,6 +321,33 @@ def get_thread(pid: int): threads.close() return mainthreadid + +def _get_process(pid: int): + """ + Get emulator's handle. + + Args: + pid (int): Emulator's pid + + Returns: + tuple(processhandle, threadhandle, processid, mainthreadid) | + tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + """ + tid = get_thread(pid) + try: + hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) + if not hProcess: + report("OpenProcess failed.", level=30, handle=hProcess) + + hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) + if not hThread: + report("OpenThread failed.", level=30, handle=hThread) + + return hProcess, hThread, pid, tid + except Exception as e: + logger.warning(f"Failed to get process and thread handles: {e}") + return None, None, pid, tid + def get_process(instance: EmulatorInstance): """ Get emulator's process. diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index d4c2dae044..1c88592fcc 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -31,9 +31,16 @@ class PlatformBase(Connection, EmulatorManagerBase): - emulator_stop() """ + def switch_window(self): + """ + Switch emulator's window. + """ + logger.info(f'Current platform {sys.platform} does not support switchwindow, skip') + return + def emulator_start(self): """ - Start a emulator, until startup completed. + Start an emulator, until startup completed. - Retry is required. - Using bored sleep to wait startup is forbidden. """ @@ -41,10 +48,17 @@ def emulator_start(self): def emulator_stop(self): """ - Stop a emulator. + Stop an emulator. """ logger.info(f'Current platform {sys.platform} does not support emulator_stop, skip') + def emulator_check(self): + """ + Check if emulator is running. + """ + logger.info(f'Current platform {sys.platform} does not support emulator_check, skip') + return True + @cached_property def emulator_info(self) -> EmulatorInfo: emulator = self.config.EmulatorInfo_Emulator diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index bdf407db27..82ad8ea45b 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -1,15 +1,9 @@ -import ctypes -import re -import subprocess - -import psutil - -from deploy.Windows.utils import DataProcessInfo from module.base.decorator import run_once from module.base.timer import Timer from module.device.connection import AdbDeviceWithStatus from module.device.platform.platform_base import PlatformBase from module.device.platform.emulator_windows import Emulator, EmulatorInstance, EmulatorManager +from module.device.platform import api_windows from module.logger import logger @@ -17,70 +11,71 @@ class EmulatorUnknown(Exception): pass -def get_focused_window(): - return ctypes.windll.user32.GetForegroundWindow() - - -def set_focus_window(hwnd): - ctypes.windll.user32.SetForegroundWindow(hwnd) - - -def minimize_window(hwnd): - ctypes.windll.user32.ShowWindow(hwnd, 6) +class EmulatorStatus: + process: tuple = None + hwnds: list = None + focusedwindow: tuple = None -def get_window_title(hwnd): - """Returns the window title as a string.""" - text_len_in_characters = ctypes.windll.user32.GetWindowTextLengthW(hwnd) - string_buffer = ctypes.create_unicode_buffer( - text_len_in_characters + 1) # +1 for the \0 at the end of the null-terminated string. - ctypes.windll.user32.GetWindowTextW(hwnd, string_buffer, text_len_in_characters + 1) - return string_buffer.value - - -def flash_window(hwnd, flash=True): - ctypes.windll.user32.FlashWindow(hwnd, flash) - - -class PlatformWindows(PlatformBase, EmulatorManager): - @classmethod - def execute(cls, command): - """ - Args: - command (str): - - Returns: - subprocess.Popen: - """ +class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): + def execute(self, command: str): command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') - return subprocess.Popen(command, close_fds=True) # only work on Windows - - @classmethod - def kill_process_by_regex(cls, regex: str) -> int: - """ - Kill processes with cmdline match the given regex. + if self.config.Emulator_SilentStart == 'normal': + sstart = False + else: + sstart = True + if self.process is not None: + if self.process[0] is not None and self.process[1] is not None: + api_windows.CloseHandle(self.process[0]) + api_windows.CloseHandle(self.process[1]) + self.process, self.focusedwindow = api_windows.execute(command, sstart) + logger.info(f"Current window: {self.focusedwindow[0]}") + return True - Args: - regex: + @staticmethod + def kill_process_by_regex(regex: str) -> int: + return api_windows.kill_process_by_regex(regex) - Returns: - int: Number of processes killed - """ - count = 0 + @staticmethod + def getfocusedwindow(): + return api_windows.getfocusedwindow() + + @staticmethod + def setforegroundwindow(focusedwindow: tuple): + return api_windows.setforegroundwindow(focusedwindow) - for proc in psutil.process_iter(): - cmdline = DataProcessInfo(proc=proc, pid=proc.pid).cmdline - if re.search(regex, cmdline): - logger.info(f'Kill emulator: {cmdline}') - proc.kill() - count += 1 + @staticmethod + def get_hwnds(pid: int) -> list: + return api_windows.get_hwnds(pid) - return count + @staticmethod + def get_process(instance: EmulatorInstance): + return api_windows.get_process(instance) + + @staticmethod + def get_cmdline(pid: int): + return api_windows.get_cmdline(pid) + + def switch_window(self): + if self.process is None: + self.process = self.get_process(self.emulator_instance) + if self.hwnds is None: + self.hwnds = self.get_hwnds(self.process[2]) + method = self.config.Emulator_SilentStart + if method == 'normal': + return api_windows.switch_window(self.hwnds, api_windows.SW_SHOW) + elif method == 'minimize': + return api_windows.switch_window(self.hwnds, api_windows.SW_MINIMIZE) + elif method == 'silent': + return True + else: + from module.exception import ScriptError + raise ScriptError("Wrong setting") def _emulator_start(self, instance: EmulatorInstance): """ - Start a emulator without error handling + Start an emulator without error handling """ exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: @@ -114,7 +109,7 @@ def _emulator_start(self, instance: EmulatorInstance): def _emulator_stop(self, instance: EmulatorInstance): """ - Stop a emulator without error handling + Stop an emulator without error handling """ exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: @@ -204,10 +199,8 @@ def emulator_start_watch(self): bool: True if startup completed False if timeout """ - logger.hr('Emulator start', level=2) - current_window = get_focused_window() + logger.info("Emulator starting...") serial = self.emulator_instance.serial - logger.info(f'Current window: {current_window}') def adb_connect(): m = self.adb_client.connect(self.serial) @@ -236,7 +229,6 @@ def show_package(m): interval = Timer(0.5).start() timeout = Timer(300).start() - new_window = 0 while 1: interval.wait() interval.reset() @@ -244,16 +236,6 @@ def show_package(m): logger.warning(f'Emulator start timeout') return False - # Check emulator window showing up - # logger.info([get_focused_window(), get_window_title(get_focused_window())]) - if current_window != 0 and new_window == 0: - new_window = get_focused_window() - if current_window != new_window: - logger.info(f'New window showing up: {new_window}, focus back') - set_focus_window(current_window) - else: - new_window = 0 - # Check device connection devices = self.list_device().select(serial=serial) # logger.info(devices) @@ -291,24 +273,27 @@ def show_package(m): # All check passed break - if new_window != 0 and new_window != current_window: - logger.info(f'Minimize new window: {new_window}') - minimize_window(new_window) - if current_window: - logger.info(f'De-flash current window: {current_window}') - flash_window(current_window, flash=False) - if new_window: - logger.info(f'Flash new window: {new_window}') - flash_window(new_window, flash=True) - logger.info('Emulator start completed') + # Flash window + currentwindow = self.getfocusedwindow() + if ( + self.focusedwindow is not None and + currentwindow is not None and + self.focusedwindow != currentwindow + ): + logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") + self.setforegroundwindow(self.focusedwindow) + + # Check emulator process and hwnds + self.hwnds = self.get_hwnds(self.process[2]) + + logger.info(f'Emulator start completed') + logger.info(f'Emulator Process: {self.process}') + logger.info(f'Emulator hwnds: {self.hwnds}') return True def emulator_start(self): logger.hr('Emulator start', level=1) for _ in range(3): - # Stop - if not self._emulator_function_wrapper(self._emulator_stop): - return False # Start if self._emulator_function_wrapper(self._emulator_start): # Success @@ -340,8 +325,30 @@ def emulator_stop(self): logger.error('Failed to stop emulator 3 times, stopped') return False - + + def emulator_check(self): + try: + if self.process is None: + self.process = self.get_process(self.emulator_instance) + return True + cmdline = self.get_cmdline(self.process[2]) + if self.emulator_instance.path in cmdline: + return True + else: + self.process = self.get_process(self.emulator_instance) + return True + except api_windows.IterationFinished as e: + return False + except IndexError: + return False + except OSError as e: + raise + except Exception as e: + logger.error(e) + raise + + if __name__ == '__main__': self = PlatformWindows('alas') d = self.emulator_instance - print(d) \ No newline at end of file + print(d) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index ff44bb0be0..a59376fe24 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -124,14 +124,14 @@ def __init__(self, access, pid): super().__init__() self.handle = OpenProcess(access, False, pid) if not self.handle: - report("OpenProcess failed.") + report("OpenProcess failed.", uselog=False) class ThreadHandle(Handle): def __init__(self, access, tid): super().__init__() self.handle = OpenThread(access, False, tid) if not self.handle: - report("OpenThread failed.") + report("OpenThread failed.", uselog=False) class CreateSnapshot(Handle): def __init__(self, arg): @@ -139,7 +139,7 @@ def __init__(self, arg): self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE if self.handle == INVALID_HANDLE_VALUE: - report("CreateToolhelp32Snapshot failed.") + report("CreateToolhelp32Snapshot failed.", uselog=False) def report( msg='', From 9e2079a1900fc1aad614e6b0c0432ffc41bd9102 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:05:18 +0800 Subject: [PATCH 075/161] Upd: fix log format --- module/device/platform/platform_windows.py | 4 +++- module/device/platform/winapi/functions_windows.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 06d29fb1e3..d5e6597794 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -59,7 +59,9 @@ def get_cmdline(pid: int): def switch_window(self): if self.process is None: - return + self.process = self.get_process(self.emulator_instance) + if self.hwnds is None: + self.hwnds = self.get_hwnds(self.process[2]) method = self.config.Emulator_SilentStart if method == 'normal': return api_windows.switch_window(self.hwnds, api_windows.SW_SHOW) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index a59376fe24..f2e82ce3f0 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -166,7 +166,7 @@ def report( if statuscode == -1: statuscode = GetLastError() if uselog: - logger.log(level, f"{msg} Status code: {statuscode}") + logger.log(level, f"{msg} Status code: 0x{statuscode:08x}") if handle: CloseHandle(handle) if raiseexcept: From d5dc97bfbf90d585cf6b35580c068413210301b1 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:05:47 +0800 Subject: [PATCH 076/161] Upd: fix log format --- module/device/platform/winapi/functions_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index a59376fe24..f2e82ce3f0 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -166,7 +166,7 @@ def report( if statuscode == -1: statuscode = GetLastError() if uselog: - logger.log(level, f"{msg} Status code: {statuscode}") + logger.log(level, f"{msg} Status code: 0x{statuscode:08x}") if handle: CloseHandle(handle) if raiseexcept: From 61289aea79577126736f38ac4784684bed6d39d2 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:14:08 +0800 Subject: [PATCH 077/161] Upd: fix bugs. --- module/device/platform/platform_windows.py | 2 +- module/device/platform/winapi/functions_windows.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index d5e6597794..145e47af1e 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -278,7 +278,7 @@ def show_package(m): if ( self.focusedwindow is not None and currentwindow is not None and - self.focusedwindow != currentwindow + self.focusedwindow[0] != currentwindow[0] ): logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") self.setforegroundwindow(self.focusedwindow) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index f2e82ce3f0..1488abfaf6 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -142,13 +142,13 @@ def __init__(self, arg): report("CreateToolhelp32Snapshot failed.", uselog=False) def report( - msg='', - statuscode=-1, - uselog=True, - level=40, - handle=0, - raiseexcept=True, - exception=OSError, + msg: str = '', + statuscode: int = -1, + uselog: bool = True, + level: int = 40, + handle: int = 0, + raiseexcept: bool = True, + exception: type = OSError, ): """ Raise exception. From dfd1e09b1e5230af76f54f403910a1b64594db9f Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:15:08 +0800 Subject: [PATCH 078/161] Upd: fix bugs. --- module/device/platform/platform_windows.py | 2 +- module/device/platform/winapi/functions_windows.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 82ad8ea45b..3a74a39ff9 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -278,7 +278,7 @@ def show_package(m): if ( self.focusedwindow is not None and currentwindow is not None and - self.focusedwindow != currentwindow + self.focusedwindow[0] != currentwindow[0] ): logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") self.setforegroundwindow(self.focusedwindow) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index f2e82ce3f0..1488abfaf6 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -142,13 +142,13 @@ def __init__(self, arg): report("CreateToolhelp32Snapshot failed.", uselog=False) def report( - msg='', - statuscode=-1, - uselog=True, - level=40, - handle=0, - raiseexcept=True, - exception=OSError, + msg: str = '', + statuscode: int = -1, + uselog: bool = True, + level: int = 40, + handle: int = 0, + raiseexcept: bool = True, + exception: type = OSError, ): """ Raise exception. From 5eae798649d5e23e6163e25dc60361994a45ae4b Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:18:51 +0800 Subject: [PATCH 079/161] fix. --- module/config/i18n/zh-TW.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 966799472e..3bdcfd5e17 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -738,7 +738,7 @@ "event_20230803_cn": "奏響鳶尾之歌", "event_20230817_cn": "愚者的天平", "event_20230914_cn": "Effulgence Before Eclipse", - "event_20231026_cn": "Tempesta and the Fountain of Youth", + "event_20231026_cn": "飓風與青春之泉", "event_20231123_cn": "蒼閃忍法帖", "event_20231221_cn": "Light-Chasing Sea of Stars", "event_20240229_cn": "Snowrealm Peregrination", From 15c06e51ba61f05075f3e855b66f82ed1df98942 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:26:50 +0800 Subject: [PATCH 080/161] fix. --- module/device/device.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index 1e66353279..d20933d4dc 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -346,13 +346,3 @@ def emulator_start(self): def switch_window(self): return super().switch_window() - -if __name__ == '__main__': - self = Device('alas') - # self.maatouch_uninstall() - # self.maatouch_install() - # self.click_maatouch(300, 300) - # self.click_maatouch(300, 300) - # self.drag_maatouch((800, 300), (300, 300)) - self.swipe_minitouch((300, 300), (800, 300)) - self.swipe_minitouch((800, 300), (300, 300)) From 470caae2d9cee49d29d8def2a89b3ffe48d094c7 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:36:26 +0800 Subject: [PATCH 081/161] Upd: fix logics. --- module/device/platform/platform_windows.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 145e47af1e..e56b0331c2 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -236,6 +236,16 @@ def show_package(m): logger.warning(f'Emulator start timeout') return False + # Flash window + currentwindow = self.getfocusedwindow() + if ( + self.focusedwindow is not None and + currentwindow is not None and + self.focusedwindow[0] != currentwindow[0] + ): + logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") + self.setforegroundwindow(self.focusedwindow) + # Check device connection devices = self.list_device().select(serial=serial) # logger.info(devices) @@ -273,16 +283,6 @@ def show_package(m): # All check passed break - # Flash window - currentwindow = self.getfocusedwindow() - if ( - self.focusedwindow is not None and - currentwindow is not None and - self.focusedwindow[0] != currentwindow[0] - ): - logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") - self.setforegroundwindow(self.focusedwindow) - # Check emulator process and hwnds self.hwnds = self.get_hwnds(self.process[2]) From f491e6a9c5724291641ae0cf909bfa3fb9bd18a1 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 14:38:34 +0800 Subject: [PATCH 082/161] Upd: fix logics. --- module/device/platform/platform_windows.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 3a74a39ff9..ec05f2935c 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -236,6 +236,16 @@ def show_package(m): logger.warning(f'Emulator start timeout') return False + # Flash window + currentwindow = self.getfocusedwindow() + if ( + self.focusedwindow is not None and + currentwindow is not None and + self.focusedwindow[0] != currentwindow[0] + ): + logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") + self.setforegroundwindow(self.focusedwindow) + # Check device connection devices = self.list_device().select(serial=serial) # logger.info(devices) @@ -273,16 +283,6 @@ def show_package(m): # All check passed break - # Flash window - currentwindow = self.getfocusedwindow() - if ( - self.focusedwindow is not None and - currentwindow is not None and - self.focusedwindow[0] != currentwindow[0] - ): - logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") - self.setforegroundwindow(self.focusedwindow) - # Check emulator process and hwnds self.hwnds = self.get_hwnds(self.process[2]) From fb36f19f4d7e2ec0ddc516dea4d68c7c9b32da25 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 15:22:41 +0800 Subject: [PATCH 083/161] Upd: opt import and logics. --- module/device/platform/api_windows.py | 11 +++++------ module/device/platform/winapi/__init__.py | 5 +++++ module/device/platform/winapi/const_windows.py | 6 +++--- .../platform/winapi/functions_windows.py | 18 +++++++++--------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 337fb439c7..c7c5e2f5f8 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -4,9 +4,7 @@ from ctypes.wintypes import SIZE from module.device.platform.emulator_windows import Emulator, EmulatorInstance -from module.device.platform.winapi.const_windows import * -from module.device.platform.winapi.functions_windows import * -from module.device.platform.winapi.structures_windows import * +from module.device.platform.winapi import * from module.logger import logger @@ -337,11 +335,12 @@ def _get_process(pid: int): try: hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) if not hProcess: - report("OpenProcess failed.", level=30, handle=hProcess) + report("OpenProcess failed.", level=30) - hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) + hThread = OpenThread(THREAD_ALL_ACCESS, False, tid) if not hThread: - report("OpenThread failed.", level=30, handle=hThread) + CloseHandle(hProcess) + report("OpenThread failed.", level=30) return hProcess, hThread, pid, tid except Exception as e: diff --git a/module/device/platform/winapi/__init__.py b/module/device/platform/winapi/__init__.py index e69de29bb2..291c25a2bb 100644 --- a/module/device/platform/winapi/__init__.py +++ b/module/device/platform/winapi/__init__.py @@ -0,0 +1,5 @@ +from module.device.platform.winapi.const_windows import * +from module.device.platform.winapi.functions_windows import * +from module.device.platform.winapi.structures_windows import * + +__all__ = [name for name in dir() if not name.startswith('_')] diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 2ecb84227b..4fd7c0c1e8 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -1,4 +1,4 @@ -from sys import getwindowsversion, maxsize +from sys import getwindowsversion from ctypes.wintypes import LPVOID # winnt.h line 3961 @@ -151,5 +151,5 @@ STATUS_PASSWORD_MUST_CHANGE = 0xC0000224 STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234 -MAXULONGLONG = maxsize * 2 + 1 -INVALID_HANDLE_VALUE = LPVOID(-1).value +MAXULONGLONG = LPVOID(-1).value +INVALID_HANDLE_VALUE = -1 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 1488abfaf6..967e76ecbc 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -120,18 +120,18 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.handle = None class ProcessHandle(Handle): - def __init__(self, access, pid): + def __init__(self, access, pid, uselog): super().__init__() self.handle = OpenProcess(access, False, pid) if not self.handle: - report("OpenProcess failed.", uselog=False) + report("OpenProcess failed.", uselog=uselog) class ThreadHandle(Handle): - def __init__(self, access, tid): + def __init__(self, access, tid, uselog): super().__init__() self.handle = OpenThread(access, False, tid) if not self.handle: - report("OpenThread failed.", uselog=False) + report("OpenThread failed.", uselog=uselog) class CreateSnapshot(Handle): def __init__(self, arg): @@ -139,7 +139,7 @@ def __init__(self, arg): self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE if self.handle == INVALID_HANDLE_VALUE: - report("CreateToolhelp32Snapshot failed.", uselog=False) + report("CreateToolhelp32Snapshot failed.") def report( msg: str = '', @@ -172,11 +172,11 @@ def report( if raiseexcept: raise exception(statuscode) -def open_process(access, pid): - return ProcessHandle(access, pid) +def open_process(access, pid, uselog=False): + return ProcessHandle(access, pid, uselog) -def open_thread(access, tid): - return ThreadHandle(access, tid) +def open_thread(access, tid, uselog=False): + return ThreadHandle(access, tid, uselog) def create_snapshot(arg): return CreateSnapshot(arg) From b7bdc1da1f95f527e10b4c72b292334e555911a7 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 15:23:28 +0800 Subject: [PATCH 084/161] Upd: opt import and logics. --- module/device/platform/api_windows.py | 11 +++++------ module/device/platform/winapi/__init__.py | 5 +++++ module/device/platform/winapi/const_windows.py | 6 +++--- .../platform/winapi/functions_windows.py | 18 +++++++++--------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 337fb439c7..c7c5e2f5f8 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -4,9 +4,7 @@ from ctypes.wintypes import SIZE from module.device.platform.emulator_windows import Emulator, EmulatorInstance -from module.device.platform.winapi.const_windows import * -from module.device.platform.winapi.functions_windows import * -from module.device.platform.winapi.structures_windows import * +from module.device.platform.winapi import * from module.logger import logger @@ -337,11 +335,12 @@ def _get_process(pid: int): try: hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) if not hProcess: - report("OpenProcess failed.", level=30, handle=hProcess) + report("OpenProcess failed.", level=30) - hThread = OpenThread(PROCESS_ALL_ACCESS, False, tid) + hThread = OpenThread(THREAD_ALL_ACCESS, False, tid) if not hThread: - report("OpenThread failed.", level=30, handle=hThread) + CloseHandle(hProcess) + report("OpenThread failed.", level=30) return hProcess, hThread, pid, tid except Exception as e: diff --git a/module/device/platform/winapi/__init__.py b/module/device/platform/winapi/__init__.py index e69de29bb2..291c25a2bb 100644 --- a/module/device/platform/winapi/__init__.py +++ b/module/device/platform/winapi/__init__.py @@ -0,0 +1,5 @@ +from module.device.platform.winapi.const_windows import * +from module.device.platform.winapi.functions_windows import * +from module.device.platform.winapi.structures_windows import * + +__all__ = [name for name in dir() if not name.startswith('_')] diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 2ecb84227b..4fd7c0c1e8 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -1,4 +1,4 @@ -from sys import getwindowsversion, maxsize +from sys import getwindowsversion from ctypes.wintypes import LPVOID # winnt.h line 3961 @@ -151,5 +151,5 @@ STATUS_PASSWORD_MUST_CHANGE = 0xC0000224 STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234 -MAXULONGLONG = maxsize * 2 + 1 -INVALID_HANDLE_VALUE = LPVOID(-1).value +MAXULONGLONG = LPVOID(-1).value +INVALID_HANDLE_VALUE = -1 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 1488abfaf6..967e76ecbc 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -120,18 +120,18 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.handle = None class ProcessHandle(Handle): - def __init__(self, access, pid): + def __init__(self, access, pid, uselog): super().__init__() self.handle = OpenProcess(access, False, pid) if not self.handle: - report("OpenProcess failed.", uselog=False) + report("OpenProcess failed.", uselog=uselog) class ThreadHandle(Handle): - def __init__(self, access, tid): + def __init__(self, access, tid, uselog): super().__init__() self.handle = OpenThread(access, False, tid) if not self.handle: - report("OpenThread failed.", uselog=False) + report("OpenThread failed.", uselog=uselog) class CreateSnapshot(Handle): def __init__(self, arg): @@ -139,7 +139,7 @@ def __init__(self, arg): self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE if self.handle == INVALID_HANDLE_VALUE: - report("CreateToolhelp32Snapshot failed.", uselog=False) + report("CreateToolhelp32Snapshot failed.") def report( msg: str = '', @@ -172,11 +172,11 @@ def report( if raiseexcept: raise exception(statuscode) -def open_process(access, pid): - return ProcessHandle(access, pid) +def open_process(access, pid, uselog=False): + return ProcessHandle(access, pid, uselog) -def open_thread(access, tid): - return ThreadHandle(access, tid) +def open_thread(access, tid, uselog=False): + return ThreadHandle(access, tid, uselog) def create_snapshot(arg): return CreateSnapshot(arg) From 740a3080a08b63d64bee41099775ff80219046e1 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 15:31:57 +0800 Subject: [PATCH 085/161] Upd: opt main loop --- alas.py | 118 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/alas.py b/alas.py index 650f41f22d..b2e75c0225 100644 --- a/alas.py +++ b/alas.py @@ -441,6 +441,14 @@ def wait_until(self, future): if self.config.should_reload(): return False + def emurestart(self, task): + logger.warning('Emulator is not running') + self.device.emulator_stop() + self.device.emulator_start() + if task != 'Restart': + self.run('start') + del_cached_property(self, 'config') + def get_next_task(self): """ Returns: @@ -454,43 +462,74 @@ def get_next_task(self): from module.base.resource import release_resources if self.config.task.command != 'Alas': release_resources(next_task=task.command) + + if task.next_run <= datetime.now(): + break + + logger.info(f'Wait until {task.next_run} for task `{task.command}`') + self.is_first_task = False + + method: str = self.config.Optimization_WhenTaskQueueEmpty + remainingtime: float = (task.next_run - datetime.now()).total_seconds() / 60 + buffertime: int = self.config.Optimization_ProcessBufferTime + if ( + method == 'stop_emulator' and + self.device.emulator_check() and + remainingtime <= buffertime + ): + method = self.config.Optimization_BufferMethod + logger.info( + f"The time to next task `{task.command}` is {remainingtime:.2f} minutes, " + f"less than {buffertime} minutes, fallback to {method}" + ) + + if method == 'close_game': + logger.info('Close game during wait') + self.device.app_stop() + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + if task.command != 'Restart': + self.run('start') + elif method == 'goto_main': + logger.info('Goto main page during wait') + self.run('goto_main') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + elif method == 'stay_there': + logger.info('Stay there during wait') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + elif method == 'stop_emulator': + logger.info('Stop emulator during wait') + self.device.emulator_stop() + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + method: str = self.config.Optimization_WhenTaskQueueEmpty + if ( + not self.device.emulator_check() and + method != 'stop_emulator' + ): + self.emurestart(task.command) + del_cached_property(self, 'config') + continue + else: + logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue - if task.next_run > datetime.now(): - logger.info(f'Wait until {task.next_run} for task `{task.command}`') - self.is_first_task = False - method = self.config.Optimization_WhenTaskQueueEmpty - if method == 'close_game': - logger.info('Close game during wait') - self.device.app_stop() - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue - if task.command != 'Restart': - self.run('start') - elif method == 'goto_main': - logger.info('Goto main page during wait') - self.run('goto_main') - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue - elif method == 'stay_there': - logger.info('Stay there during wait') - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue - else: - logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') - release_resources() - self.device.release_during_wait() - if not self.wait_until(task.next_run): - del_cached_property(self, 'config') - continue break AzurLaneConfig.is_hoarding_task = False @@ -517,10 +556,13 @@ def loop(self): del_cached_property(self, 'config') logger.info('Server or network is recovered. Restart game client') self.config.task_call('Restart') - # Get task - task = self.get_next_task() # Init device and change server _ = self.device + # Get task + task = self.get_next_task() + # Reboot emulator + if not self.device.emulator_check(): + self.emurestart(task) # Skip first restart if self.is_first_task and task == 'Restart': logger.info('Skip task `Restart` at scheduler start') From 451ded3805c82c3321c2fa581340526783b90fdf Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 15:35:50 +0800 Subject: [PATCH 086/161] Upd: fix texts. --- alas.py | 6 +++--- module/device/platform/platform_windows.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/alas.py b/alas.py index b2e75c0225..50984e7203 100644 --- a/alas.py +++ b/alas.py @@ -469,9 +469,9 @@ def get_next_task(self): logger.info(f'Wait until {task.next_run} for task `{task.command}`') self.is_first_task = False - method: str = self.config.Optimization_WhenTaskQueueEmpty - remainingtime: float = (task.next_run - datetime.now()).total_seconds() / 60 - buffertime: int = self.config.Optimization_ProcessBufferTime + method: str = self.config.Optimization_WhenTaskQueueEmpty + remainingtime: float = (task.next_run - datetime.now()).total_seconds() / 60 + buffertime: int = self.config.Optimization_ProcessBufferTime if ( method == 'stop_emulator' and self.device.emulator_check() and diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index e56b0331c2..b6f85bada9 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -239,9 +239,9 @@ def show_package(m): # Flash window currentwindow = self.getfocusedwindow() if ( - self.focusedwindow is not None and - currentwindow is not None and - self.focusedwindow[0] != currentwindow[0] + self.focusedwindow is not None and + currentwindow is not None and + self.focusedwindow[0] != currentwindow[0] ): logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") self.setforegroundwindow(self.focusedwindow) From 22bc1513a95e6b77f6529abd52bc34db7ad78c95 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 15:36:46 +0800 Subject: [PATCH 087/161] Upd: fix texts. --- alas.py | 6 +++--- module/device/platform/platform_windows.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/alas.py b/alas.py index b2e75c0225..50984e7203 100644 --- a/alas.py +++ b/alas.py @@ -469,9 +469,9 @@ def get_next_task(self): logger.info(f'Wait until {task.next_run} for task `{task.command}`') self.is_first_task = False - method: str = self.config.Optimization_WhenTaskQueueEmpty - remainingtime: float = (task.next_run - datetime.now()).total_seconds() / 60 - buffertime: int = self.config.Optimization_ProcessBufferTime + method: str = self.config.Optimization_WhenTaskQueueEmpty + remainingtime: float = (task.next_run - datetime.now()).total_seconds() / 60 + buffertime: int = self.config.Optimization_ProcessBufferTime if ( method == 'stop_emulator' and self.device.emulator_check() and diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index ec05f2935c..4ef67d712d 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -239,9 +239,9 @@ def show_package(m): # Flash window currentwindow = self.getfocusedwindow() if ( - self.focusedwindow is not None and - currentwindow is not None and - self.focusedwindow[0] != currentwindow[0] + self.focusedwindow is not None and + currentwindow is not None and + self.focusedwindow[0] != currentwindow[0] ): logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") self.setforegroundwindow(self.focusedwindow) From 38c272f6add44a31e73204d53e1a4c8c084be36d Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 18:31:56 +0800 Subject: [PATCH 088/161] Upd: fix logics. --- module/device/platform/api_windows.py | 4 +- module/device/platform/platform_windows.py | 19 ++++- .../platform/winapi/functions_windows.py | 10 +-- .../platform/winapi/structures_windows.py | 70 +++++++++++++++++-- 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index c7c5e2f5f8..d55e5ffb0e 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -121,8 +121,8 @@ def execute(command: str, sstart: bool = False): ) lpEnvironment = None lpCurrentDirectory = dirname(lpApplicationName) - lpStartupInfo = STARTUPINFO() - lpStartupInfo.cb = sizeof(STARTUPINFO) + lpStartupInfo = STARTUPINFOW() + lpStartupInfo.cb = sizeof(STARTUPINFOW) lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW lpStartupInfo.wShowWindow = SW_HIDE if sstart else SW_MINIMIZE lpProcessInformation = PROCESS_INFORMATION() diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index b6f85bada9..2ca7d3520d 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -27,12 +27,27 @@ def execute(self, command: str): sstart = True if self.process is not None: if self.process[0] is not None and self.process[1] is not None: - api_windows.CloseHandle(self.process[0]) - api_windows.CloseHandle(self.process[1]) + self.CloseHandle(self.process[:2]) self.process, self.focusedwindow = api_windows.execute(command, sstart) logger.info(f"Current window: {self.focusedwindow[0]}") return True + @staticmethod + def CloseHandle(*args, **kwargs): + for handle in args: + if isinstance(handle, tuple): + for h in handle: + api_windows.CloseHandle(h) + else: + api_windows.CloseHandle(handle) + for _, handle in kwargs.items(): + if isinstance(handle, tuple): + for h in handle: + api_windows.CloseHandle(h) + else: + api_windows.CloseHandle(handle) + return True + @staticmethod def kill_process_by_regex(regex: str) -> int: return api_windows.kill_process_by_regex(regex) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 967e76ecbc..ecf306b3cd 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -6,14 +6,14 @@ ) from module.device.platform.winapi.structures_windows import ( - SECURITY_ATTRIBUTES, STARTUPINFO, WINDOWPLACEMENT, + SECURITY_ATTRIBUTES, STARTUPINFOW, WINDOWPLACEMENT, PROCESS_INFORMATION, PROCESSENTRY32, THREADENTRY32, FILETIME ) -user32 = WinDLL(name='user32', use_last_error=True) -kernel32 = WinDLL(name='kernel32', use_last_error=True) -ntdll = WinDLL(name='ntdll', use_last_error=True) +user32 = WinDLL(name='user32', use_last_error=True) +kernel32 = WinDLL(name='kernel32', use_last_error=True) +ntdll = WinDLL(name='ntdll', use_last_error=True) CreateProcessW = kernel32.CreateProcessW CreateProcessW.argtypes = [ @@ -25,7 +25,7 @@ DWORD, #dwCreationFlags LPVOID, #lpEnvironment LPCWSTR, #lpCurrentDirectory - POINTER(STARTUPINFO), #lpStartupInfo + POINTER(STARTUPINFOW), #lpStartupInfo POINTER(PROCESS_INFORMATION) #lpProcessInformation ] CreateProcessW.restype = BOOL diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index b6e74f6780..b936b192f2 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -9,7 +9,7 @@ class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... class IterationFinished(Exception): ... -class STARTUPINFO(Structure): +class STARTUPINFOW(Structure): _fields_ = [ ('cb', DWORD), ('lpReserved', LPWSTR), @@ -87,6 +87,7 @@ class LIST_ENTRY(Structure): ("Blink", POINTER(LPVOID)) ] +# ntbasic.h line 111 class UNICODE_STRING(Structure): _fields_ = [ ("Length", USHORT), @@ -94,6 +95,7 @@ class UNICODE_STRING(Structure): ("Buffer", PWCHAR) ] +# ntpsapi.h line 63 class PEB_LDR_DATA(Structure): _fields_ = [ ("Length", ULONG), @@ -101,9 +103,13 @@ class PEB_LDR_DATA(Structure): ("SsHandle", HANDLE), ("InLoadOrderModuleList", LIST_ENTRY), ("InMemoryOrderModuleList", LIST_ENTRY), - ("InInitializationOrderModuleList", LIST_ENTRY) + ("InInitializationOrderModuleList", LIST_ENTRY), + ("EntryInProgress", LPVOID), + ("ShutdownInProgress", BOOLEAN), + ("ShutdownThreadId", HANDLE) ] +# ntpebteb.h line 8 class PEB(Structure): _fields_ = [ ("InheritedAddressSpace", BOOLEAN), @@ -162,12 +168,64 @@ class PEB(Structure): ("SessionId", ULONG) ] +# ntrtl.h line 2320 +class CURDIR(Structure): + _fields_ = [ + ("DosPath", UNICODE_STRING), + ("Handle", HANDLE) + ] + +# ntrtl.h line 2329 +class RTL_DRIVE_LETTER_CURDIR(Structure): + _fields_ = [ + ("Flags", USHORT), + ("Length", USHORT), + ("TimeStamp", ULONG), + ("DosPath", UNICODE_STRING) + ] + +# ntrtl.h line 2340 class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ - ("Reserved1", BYTE * 16), - ("Reserved2", LPVOID * 10), - ("ImagePathName", UNICODE_STRING), - ("CommandLine", UNICODE_STRING) + ("MaximumLength", ULONG), + ("Length", ULONG), + + ("Flags", ULONG), + ("DebugFlags", ULONG), + + ("ConsoleHandle", LPVOID), + ("ConsoleFlags", ULONG), + ("StandardInput", LPVOID), + ("StandardOutput", LPVOID), + ("StandardError", LPVOID), + + ("CurrentDirectory", CURDIR), + ("DllPath", UNICODE_STRING), + ("ImagePathName", UNICODE_STRING), + ("CommandLine", UNICODE_STRING), + ("Environment", LPVOID), + + ("StartingX", ULONG), + ("StartingY", ULONG), + ("CountX", ULONG), + ("CountY", ULONG), + ("CountCharsX", ULONG), + ("CountCharsY", ULONG), + ("FillAttribute", ULONG), + + ("WindowFlags", ULONG), + ("ShowWindowFlags", ULONG), + ("WindowTitle", UNICODE_STRING), + ("DesktopInfo", UNICODE_STRING), + ("ShellInfo", UNICODE_STRING), + ("RuntimeData", UNICODE_STRING), + ("CurrentDirectories", RTL_DRIVE_LETTER_CURDIR * 32), + + ("EnvironmentSize", ULONG), + ("EnvironmentVersion", ULONG), + ("PackageDependencyData", LPVOID), + ("ProcessGroupId", ULONG), + ("LoaderThreads", ULONG) ] class PROCESS_BASIC_INFORMATION(Structure): From 31afe19295d1eec1cce61ae00cd2375b4ec8d599 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 18:33:45 +0800 Subject: [PATCH 089/161] Upd: fix logics. --- module/device/platform/api_windows.py | 4 +- module/device/platform/platform_windows.py | 19 ++++- .../platform/winapi/functions_windows.py | 10 +-- .../platform/winapi/structures_windows.py | 70 +++++++++++++++++-- 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index c7c5e2f5f8..d55e5ffb0e 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -121,8 +121,8 @@ def execute(command: str, sstart: bool = False): ) lpEnvironment = None lpCurrentDirectory = dirname(lpApplicationName) - lpStartupInfo = STARTUPINFO() - lpStartupInfo.cb = sizeof(STARTUPINFO) + lpStartupInfo = STARTUPINFOW() + lpStartupInfo.cb = sizeof(STARTUPINFOW) lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW lpStartupInfo.wShowWindow = SW_HIDE if sstart else SW_MINIMIZE lpProcessInformation = PROCESS_INFORMATION() diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 4ef67d712d..6e06fa8534 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -27,12 +27,27 @@ def execute(self, command: str): sstart = True if self.process is not None: if self.process[0] is not None and self.process[1] is not None: - api_windows.CloseHandle(self.process[0]) - api_windows.CloseHandle(self.process[1]) + self.CloseHandle(self.process[:2]) self.process, self.focusedwindow = api_windows.execute(command, sstart) logger.info(f"Current window: {self.focusedwindow[0]}") return True + @staticmethod + def CloseHandle(*args, **kwargs): + for handle in args: + if isinstance(handle, tuple): + for h in handle: + api_windows.CloseHandle(h) + else: + api_windows.CloseHandle(handle) + for _, handle in kwargs.items(): + if isinstance(handle, tuple): + for h in handle: + api_windows.CloseHandle(h) + else: + api_windows.CloseHandle(handle) + return True + @staticmethod def kill_process_by_regex(regex: str) -> int: return api_windows.kill_process_by_regex(regex) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 967e76ecbc..ecf306b3cd 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -6,14 +6,14 @@ ) from module.device.platform.winapi.structures_windows import ( - SECURITY_ATTRIBUTES, STARTUPINFO, WINDOWPLACEMENT, + SECURITY_ATTRIBUTES, STARTUPINFOW, WINDOWPLACEMENT, PROCESS_INFORMATION, PROCESSENTRY32, THREADENTRY32, FILETIME ) -user32 = WinDLL(name='user32', use_last_error=True) -kernel32 = WinDLL(name='kernel32', use_last_error=True) -ntdll = WinDLL(name='ntdll', use_last_error=True) +user32 = WinDLL(name='user32', use_last_error=True) +kernel32 = WinDLL(name='kernel32', use_last_error=True) +ntdll = WinDLL(name='ntdll', use_last_error=True) CreateProcessW = kernel32.CreateProcessW CreateProcessW.argtypes = [ @@ -25,7 +25,7 @@ DWORD, #dwCreationFlags LPVOID, #lpEnvironment LPCWSTR, #lpCurrentDirectory - POINTER(STARTUPINFO), #lpStartupInfo + POINTER(STARTUPINFOW), #lpStartupInfo POINTER(PROCESS_INFORMATION) #lpProcessInformation ] CreateProcessW.restype = BOOL diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index b6e74f6780..b936b192f2 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -9,7 +9,7 @@ class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... class IterationFinished(Exception): ... -class STARTUPINFO(Structure): +class STARTUPINFOW(Structure): _fields_ = [ ('cb', DWORD), ('lpReserved', LPWSTR), @@ -87,6 +87,7 @@ class LIST_ENTRY(Structure): ("Blink", POINTER(LPVOID)) ] +# ntbasic.h line 111 class UNICODE_STRING(Structure): _fields_ = [ ("Length", USHORT), @@ -94,6 +95,7 @@ class UNICODE_STRING(Structure): ("Buffer", PWCHAR) ] +# ntpsapi.h line 63 class PEB_LDR_DATA(Structure): _fields_ = [ ("Length", ULONG), @@ -101,9 +103,13 @@ class PEB_LDR_DATA(Structure): ("SsHandle", HANDLE), ("InLoadOrderModuleList", LIST_ENTRY), ("InMemoryOrderModuleList", LIST_ENTRY), - ("InInitializationOrderModuleList", LIST_ENTRY) + ("InInitializationOrderModuleList", LIST_ENTRY), + ("EntryInProgress", LPVOID), + ("ShutdownInProgress", BOOLEAN), + ("ShutdownThreadId", HANDLE) ] +# ntpebteb.h line 8 class PEB(Structure): _fields_ = [ ("InheritedAddressSpace", BOOLEAN), @@ -162,12 +168,64 @@ class PEB(Structure): ("SessionId", ULONG) ] +# ntrtl.h line 2320 +class CURDIR(Structure): + _fields_ = [ + ("DosPath", UNICODE_STRING), + ("Handle", HANDLE) + ] + +# ntrtl.h line 2329 +class RTL_DRIVE_LETTER_CURDIR(Structure): + _fields_ = [ + ("Flags", USHORT), + ("Length", USHORT), + ("TimeStamp", ULONG), + ("DosPath", UNICODE_STRING) + ] + +# ntrtl.h line 2340 class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ - ("Reserved1", BYTE * 16), - ("Reserved2", LPVOID * 10), - ("ImagePathName", UNICODE_STRING), - ("CommandLine", UNICODE_STRING) + ("MaximumLength", ULONG), + ("Length", ULONG), + + ("Flags", ULONG), + ("DebugFlags", ULONG), + + ("ConsoleHandle", LPVOID), + ("ConsoleFlags", ULONG), + ("StandardInput", LPVOID), + ("StandardOutput", LPVOID), + ("StandardError", LPVOID), + + ("CurrentDirectory", CURDIR), + ("DllPath", UNICODE_STRING), + ("ImagePathName", UNICODE_STRING), + ("CommandLine", UNICODE_STRING), + ("Environment", LPVOID), + + ("StartingX", ULONG), + ("StartingY", ULONG), + ("CountX", ULONG), + ("CountY", ULONG), + ("CountCharsX", ULONG), + ("CountCharsY", ULONG), + ("FillAttribute", ULONG), + + ("WindowFlags", ULONG), + ("ShowWindowFlags", ULONG), + ("WindowTitle", UNICODE_STRING), + ("DesktopInfo", UNICODE_STRING), + ("ShellInfo", UNICODE_STRING), + ("RuntimeData", UNICODE_STRING), + ("CurrentDirectories", RTL_DRIVE_LETTER_CURDIR * 32), + + ("EnvironmentSize", ULONG), + ("EnvironmentVersion", ULONG), + ("PackageDependencyData", LPVOID), + ("ProcessGroupId", ULONG), + ("LoaderThreads", ULONG) ] class PROCESS_BASIC_INFORMATION(Structure): From f4e45a070f9a46bfd35142a1671a496d3cf9bf76 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 22:20:52 +0800 Subject: [PATCH 090/161] Upd: fix bug. --- alas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alas.py b/alas.py index 50984e7203..8f6a8209f1 100644 --- a/alas.py +++ b/alas.py @@ -514,13 +514,13 @@ def get_next_task(self): release_resources() self.device.release_during_wait() if not self.wait_until(task.next_run): + del_cached_property(self, 'config') method: str = self.config.Optimization_WhenTaskQueueEmpty if ( not self.device.emulator_check() and method != 'stop_emulator' ): self.emurestart(task.command) - del_cached_property(self, 'config') continue else: logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') From 89c53c7f3f705988f689b4ca437497f4e42295fc Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 22:21:43 +0800 Subject: [PATCH 091/161] Upd: fix bug. --- alas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alas.py b/alas.py index 50984e7203..8f6a8209f1 100644 --- a/alas.py +++ b/alas.py @@ -514,13 +514,13 @@ def get_next_task(self): release_resources() self.device.release_during_wait() if not self.wait_until(task.next_run): + del_cached_property(self, 'config') method: str = self.config.Optimization_WhenTaskQueueEmpty if ( not self.device.emulator_check() and method != 'stop_emulator' ): self.emurestart(task.command) - del_cached_property(self, 'config') continue else: logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') From 9edd693e6120727f23e137582122ebdd9b0a522d Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 28 Jun 2024 22:25:05 +0800 Subject: [PATCH 092/161] fix --- module/config/config_generated.py | 2 +- module/device/platform/platform_windows.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 9fa9a13d41..d88b1ad9e3 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -43,7 +43,7 @@ class GeneratedConfig: Optimization_CombatScreenshotInterval = 1.0 Optimization_TaskHoardingDuration = 0 Optimization_WhenTaskQueueEmpty = 'goto_main' # stay_there, goto_main, close_game, stop_emulator - Optimization_ProcessBufferTime = 7 + Optimization_ProcessBufferTime = 10 Optimization_BufferMethod = 'stay_there' # Group `DropRecord` diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 2ca7d3520d..6e06fa8534 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -337,7 +337,7 @@ def emulator_stop(self): continue else: return False - + logger.error('Failed to stop emulator 3 times, stopped') return False From ceffdcd67fe12a8451657f02b8f4e5ce4e1c7c32 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 29 Jun 2024 17:49:58 +0800 Subject: [PATCH 093/161] Upd: add argtypes/restype, and fix logics. --- module/device/platform/api_windows.py | 16 ++++---- .../platform/winapi/functions_windows.py | 41 ++++++++++++------- .../platform/winapi/structures_windows.py | 12 ++---- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index d55e5ffb0e..7e81349fa2 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,7 +1,6 @@ import re -from ctypes import byref, sizeof, cast, create_unicode_buffer, wstring_at, addressof -from ctypes.wintypes import SIZE +from ctypes import byref, sizeof, create_unicode_buffer, wstring_at, addressof from module.device.platform.emulator_windows import Emulator, EmulatorInstance from module.device.platform.winapi import * @@ -113,7 +112,7 @@ def execute(command: str, sstart: bool = False): lpThreadAttributes = None bInheritHandles = False dwCreationFlags = ( - CREATE_NEW_CONSOLE | + CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS | CREATE_NEW_PROCESS_GROUP | CREATE_DEFAULT_ERROR_MODE | @@ -212,7 +211,7 @@ def get_cmdline(pid: int) -> str: with open_process(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, pid) as hProcess: # Query process infomation pbi = PROCESS_BASIC_INFORMATION() - returnlength = SIZE() + returnlength = ULONG() status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: report(f"NtQueryInformationProcess failed. Status: 0x{status}.", level=30) @@ -224,8 +223,7 @@ def get_cmdline(pid: int) -> str: # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() - uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) - if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + if not ReadProcessMemory(hProcess, peb.ProcessParameters, byref(upp), sizeof(upp), None): report("Failed to read process parameters.", level=30) # Read command line @@ -289,7 +287,7 @@ def _get_thread_creation_time(tid): byref(usertime) ): return None - return creationtime.to_int() + return to_int(creationtime) def get_thread(pid: int): """ @@ -334,11 +332,11 @@ def _get_process(pid: int): tid = get_thread(pid) try: hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) - if not hProcess: + if hProcess is None: report("OpenProcess failed.", level=30) hThread = OpenThread(THREAD_ALL_ACCESS, False, tid) - if not hThread: + if hThread is None: CloseHandle(hProcess) report("OpenThread failed.", level=30) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index ecf306b3cd..e820b47311 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,8 +1,8 @@ -from ctypes import POINTER, WINFUNCTYPE, WinDLL +from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( - HANDLE, DWORD, BOOL, INT, UINT, - LPWSTR, LPCWSTR, LPVOID, HWND, - LPARAM + HANDLE, DWORD, HWND, BOOL, INT, UINT, + LONG, ULONG, LPWSTR, LPCWSTR, LPRECT, + LPVOID, LPCVOID, LPARAM, PULONG ) from module.device.platform.winapi.structures_windows import ( @@ -35,6 +35,7 @@ TerminateProcess.restype = BOOL GetForegroundWindow = user32.GetForegroundWindow +GetForegroundWindow.argtypes = [] GetForegroundWindow.restype = HWND SetForegroundWindow = user32.SetForegroundWindow SetForegroundWindow.argtypes = [HWND] @@ -52,11 +53,19 @@ ShowWindow.restype = BOOL IsWindow = user32.IsWindow +IsWindow.argtypes = [HWND] +IsWindow.restype = BOOL GetParent = user32.GetParent +GetParent.argtypes = [HWND] +GetParent.restype = HWND GetWindowRect = user32.GetWindowRect +GetWindowRect.argtypes = [HWND, LPRECT] +GetWindowRect.restype = BOOL -EnumWindows = user32.EnumWindows EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM) +EnumWindows = user32.EnumWindows +EnumWindows.argtypes = [EnumWindowsProc, LPARAM] +EnumWindows.restype = BOOL GetWindowThreadProcessId = user32.GetWindowThreadProcessId GetWindowThreadProcessId.argtypes = [HWND, POINTER(DWORD)] GetWindowThreadProcessId.restype = DWORD @@ -86,7 +95,7 @@ Thread32First = kernel32.Thread32First Thread32First.argtypes = [HANDLE, POINTER(THREADENTRY32)] -Thread32First.restype = BOOL +Thread32First.restype = BOOL Thread32Next = kernel32.Thread32Next Thread32Next.argtypes = [HANDLE, POINTER(THREADENTRY32)] @@ -103,39 +112,43 @@ GetThreadTimes.restype = BOOL GetLastError = kernel32.GetLastError +GetLastError.argtypes = [] +GetLastError.restype = DWORD +SIZE_T = c_size_t +NTSTATUS = LONG ReadProcessMemory = kernel32.ReadProcessMemory +ReadProcessMemory.argtypes = [HANDLE, LPCVOID, LPVOID, SIZE_T, POINTER(SIZE_T)] +ReadProcessMemory.restype = BOOL NtQueryInformationProcess = ntdll.NtQueryInformationProcess +NtQueryInformationProcess.argtypes = [HANDLE, INT, LPVOID, ULONG, PULONG] +NtQueryInformationProcess.restype = NTSTATUS class Handle: - def __init__(self): - self.handle = None + handle = None def __enter__(self): return self.handle def __exit__(self, exc_type, exc_val, exc_tb): - if self.handle: + if self.handle is not None: CloseHandle(self.handle) self.handle = None class ProcessHandle(Handle): def __init__(self, access, pid, uselog): - super().__init__() self.handle = OpenProcess(access, False, pid) - if not self.handle: + if self.handle is None: report("OpenProcess failed.", uselog=uselog) class ThreadHandle(Handle): def __init__(self, access, tid, uselog): - super().__init__() self.handle = OpenThread(access, False, tid) - if not self.handle: + if self.handle is None: report("OpenThread failed.", uselog=uselog) class CreateSnapshot(Handle): def __init__(self, arg): - super().__init__() self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE if self.handle == INVALID_HANDLE_VALUE: diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index b936b192f2..d26eb342fe 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -2,7 +2,7 @@ from ctypes.wintypes import ( HANDLE, DWORD, WORD, LARGE_INTEGER, BYTE, BOOL, BOOLEAN, USHORT, UINT, LONG, ULONG, CHAR, LPWSTR, LPVOID, MAX_PATH, - RECT, PULONG, POINT, PWCHAR + RECT, PULONG, POINT, PWCHAR, FILETIME ) class EmulatorLaunchFailedError(Exception): ... @@ -237,11 +237,5 @@ class PROCESS_BASIC_INFORMATION(Structure): ("Reserved3", LPVOID) ] -class FILETIME(Structure): - _fields_ = [ - ("dwLowDateTime", DWORD), - ("dwHighDateTime", DWORD) - ] - - def to_int(self): - return (self.dwHighDateTime << 32) + self.dwLowDateTime +def to_int(time: FILETIME): + return (time.dwHighDateTime << 32) + time.dwLowDateTime From 2c2e1681f43becb4f9ea828092e5e03df137348a Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 29 Jun 2024 17:52:52 +0800 Subject: [PATCH 094/161] Upd: add argtypes/restype, and fix logics. --- module/device/platform/api_windows.py | 16 ++++---- .../platform/winapi/functions_windows.py | 41 ++++++++++++------- .../platform/winapi/structures_windows.py | 12 ++---- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index d55e5ffb0e..7e81349fa2 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,7 +1,6 @@ import re -from ctypes import byref, sizeof, cast, create_unicode_buffer, wstring_at, addressof -from ctypes.wintypes import SIZE +from ctypes import byref, sizeof, create_unicode_buffer, wstring_at, addressof from module.device.platform.emulator_windows import Emulator, EmulatorInstance from module.device.platform.winapi import * @@ -113,7 +112,7 @@ def execute(command: str, sstart: bool = False): lpThreadAttributes = None bInheritHandles = False dwCreationFlags = ( - CREATE_NEW_CONSOLE | + CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS | CREATE_NEW_PROCESS_GROUP | CREATE_DEFAULT_ERROR_MODE | @@ -212,7 +211,7 @@ def get_cmdline(pid: int) -> str: with open_process(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, pid) as hProcess: # Query process infomation pbi = PROCESS_BASIC_INFORMATION() - returnlength = SIZE() + returnlength = ULONG() status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: report(f"NtQueryInformationProcess failed. Status: 0x{status}.", level=30) @@ -224,8 +223,7 @@ def get_cmdline(pid: int) -> str: # Read process parameters upp = RTL_USER_PROCESS_PARAMETERS() - uppAddress = cast(peb.ProcessParameters, POINTER(RTL_USER_PROCESS_PARAMETERS)) - if not ReadProcessMemory(hProcess, uppAddress, byref(upp), sizeof(upp), None): + if not ReadProcessMemory(hProcess, peb.ProcessParameters, byref(upp), sizeof(upp), None): report("Failed to read process parameters.", level=30) # Read command line @@ -289,7 +287,7 @@ def _get_thread_creation_time(tid): byref(usertime) ): return None - return creationtime.to_int() + return to_int(creationtime) def get_thread(pid: int): """ @@ -334,11 +332,11 @@ def _get_process(pid: int): tid = get_thread(pid) try: hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid) - if not hProcess: + if hProcess is None: report("OpenProcess failed.", level=30) hThread = OpenThread(THREAD_ALL_ACCESS, False, tid) - if not hThread: + if hThread is None: CloseHandle(hProcess) report("OpenThread failed.", level=30) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index ecf306b3cd..e820b47311 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,8 +1,8 @@ -from ctypes import POINTER, WINFUNCTYPE, WinDLL +from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( - HANDLE, DWORD, BOOL, INT, UINT, - LPWSTR, LPCWSTR, LPVOID, HWND, - LPARAM + HANDLE, DWORD, HWND, BOOL, INT, UINT, + LONG, ULONG, LPWSTR, LPCWSTR, LPRECT, + LPVOID, LPCVOID, LPARAM, PULONG ) from module.device.platform.winapi.structures_windows import ( @@ -35,6 +35,7 @@ TerminateProcess.restype = BOOL GetForegroundWindow = user32.GetForegroundWindow +GetForegroundWindow.argtypes = [] GetForegroundWindow.restype = HWND SetForegroundWindow = user32.SetForegroundWindow SetForegroundWindow.argtypes = [HWND] @@ -52,11 +53,19 @@ ShowWindow.restype = BOOL IsWindow = user32.IsWindow +IsWindow.argtypes = [HWND] +IsWindow.restype = BOOL GetParent = user32.GetParent +GetParent.argtypes = [HWND] +GetParent.restype = HWND GetWindowRect = user32.GetWindowRect +GetWindowRect.argtypes = [HWND, LPRECT] +GetWindowRect.restype = BOOL -EnumWindows = user32.EnumWindows EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM) +EnumWindows = user32.EnumWindows +EnumWindows.argtypes = [EnumWindowsProc, LPARAM] +EnumWindows.restype = BOOL GetWindowThreadProcessId = user32.GetWindowThreadProcessId GetWindowThreadProcessId.argtypes = [HWND, POINTER(DWORD)] GetWindowThreadProcessId.restype = DWORD @@ -86,7 +95,7 @@ Thread32First = kernel32.Thread32First Thread32First.argtypes = [HANDLE, POINTER(THREADENTRY32)] -Thread32First.restype = BOOL +Thread32First.restype = BOOL Thread32Next = kernel32.Thread32Next Thread32Next.argtypes = [HANDLE, POINTER(THREADENTRY32)] @@ -103,39 +112,43 @@ GetThreadTimes.restype = BOOL GetLastError = kernel32.GetLastError +GetLastError.argtypes = [] +GetLastError.restype = DWORD +SIZE_T = c_size_t +NTSTATUS = LONG ReadProcessMemory = kernel32.ReadProcessMemory +ReadProcessMemory.argtypes = [HANDLE, LPCVOID, LPVOID, SIZE_T, POINTER(SIZE_T)] +ReadProcessMemory.restype = BOOL NtQueryInformationProcess = ntdll.NtQueryInformationProcess +NtQueryInformationProcess.argtypes = [HANDLE, INT, LPVOID, ULONG, PULONG] +NtQueryInformationProcess.restype = NTSTATUS class Handle: - def __init__(self): - self.handle = None + handle = None def __enter__(self): return self.handle def __exit__(self, exc_type, exc_val, exc_tb): - if self.handle: + if self.handle is not None: CloseHandle(self.handle) self.handle = None class ProcessHandle(Handle): def __init__(self, access, pid, uselog): - super().__init__() self.handle = OpenProcess(access, False, pid) - if not self.handle: + if self.handle is None: report("OpenProcess failed.", uselog=uselog) class ThreadHandle(Handle): def __init__(self, access, tid, uselog): - super().__init__() self.handle = OpenThread(access, False, tid) - if not self.handle: + if self.handle is None: report("OpenThread failed.", uselog=uselog) class CreateSnapshot(Handle): def __init__(self, arg): - super().__init__() self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE if self.handle == INVALID_HANDLE_VALUE: diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index b936b192f2..d26eb342fe 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -2,7 +2,7 @@ from ctypes.wintypes import ( HANDLE, DWORD, WORD, LARGE_INTEGER, BYTE, BOOL, BOOLEAN, USHORT, UINT, LONG, ULONG, CHAR, LPWSTR, LPVOID, MAX_PATH, - RECT, PULONG, POINT, PWCHAR + RECT, PULONG, POINT, PWCHAR, FILETIME ) class EmulatorLaunchFailedError(Exception): ... @@ -237,11 +237,5 @@ class PROCESS_BASIC_INFORMATION(Structure): ("Reserved3", LPVOID) ] -class FILETIME(Structure): - _fields_ = [ - ("dwLowDateTime", DWORD), - ("dwHighDateTime", DWORD) - ] - - def to_int(self): - return (self.dwHighDateTime << 32) + self.dwLowDateTime +def to_int(time: FILETIME): + return (time.dwHighDateTime << 32) + time.dwLowDateTime From cb1e4392be5b6764b8175cc65fed2d4c25110cab Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 29 Jun 2024 23:13:29 +0800 Subject: [PATCH 095/161] Upd: fix bugs. --- module/device/platform/api_windows.py | 2 +- module/device/platform/platform_windows.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 7e81349fa2..4f88dab147 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -234,7 +234,7 @@ def get_cmdline(pid: int) -> str: cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: return '' - return cmdline + return cmdline.replace(r"\\", "/").replace("\\", "/").replace('"', '"') def kill_process_by_regex(regex: str) -> int: diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 6e06fa8534..b03203a348 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -83,7 +83,7 @@ def switch_window(self): elif method == 'minimize': return api_windows.switch_window(self.hwnds, api_windows.SW_MINIMIZE) elif method == 'silent': - return True + return api_windows.switch_window(self.hwnds, api_windows.SW_HIDE) else: from module.exception import ScriptError raise ScriptError("Wrong setting") From 52ca1c2c9e96bc0f016a509296ab814b9c495f57 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sat, 29 Jun 2024 23:14:24 +0800 Subject: [PATCH 096/161] Upd: fix bugs. --- module/device/platform/api_windows.py | 2 +- module/device/platform/platform_windows.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 7e81349fa2..4f88dab147 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -234,7 +234,7 @@ def get_cmdline(pid: int) -> str: cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: return '' - return cmdline + return cmdline.replace(r"\\", "/").replace("\\", "/").replace('"', '"') def kill_process_by_regex(regex: str) -> int: diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 6e06fa8534..b03203a348 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -83,7 +83,7 @@ def switch_window(self): elif method == 'minimize': return api_windows.switch_window(self.hwnds, api_windows.SW_MINIMIZE) elif method == 'silent': - return True + return api_windows.switch_window(self.hwnds, api_windows.SW_HIDE) else: from module.exception import ScriptError raise ScriptError("Wrong setting") From c18fd6192146d7f6d65f7b479e083ae496dca2f8 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Mon, 8 Jul 2024 22:36:18 +0800 Subject: [PATCH 097/161] Upd: Add flash_window method. --- module/device/platform/api_windows.py | 238 +++++++++++++++--- module/device/platform/platform_windows.py | 81 +++--- .../device/platform/winapi/const_windows.py | 49 +++- .../platform/winapi/functions_windows.py | 149 +++++++++-- .../platform/winapi/structures_windows.py | 183 +++----------- 5 files changed, 456 insertions(+), 244 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 4f88dab147..11a4ab8b4d 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,4 +1,5 @@ import re +import xml.etree.ElementTree as Et from ctypes import byref, sizeof, create_unicode_buffer, wstring_at, addressof @@ -7,7 +8,29 @@ from module.logger import logger +def is_admin(): + try: + return IsUserAnAdmin() + except: + return False + + def __yieldloop(entry32, snapshot, func: callable): + """ + Generates a loop that yields entries from a snapshot until the function fails or finishes. + + Args: + entry32 (PROCESSENTRY32 or THREADENTRY32): Entry structure to be yielded, either for processes or threads. + snapshot (int): Handle to the snapshot. + func (callable): Next entry (e.g., Process32Next or Thread32Next). + + Yields: + PROCESSENTRY32 or THREADENTRY32: The current entry in the snapshot. + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. + """ while 1: yield entry32 if func(snapshot, byref(entry32)): @@ -24,8 +47,11 @@ def _enum_processes(): Enumerates all the processes currently running on the system. Yields: - lppe32 (PROCESSENTRY32) | - None (if enum failed) + PROCESSENTRY32 or None: The current process entry or None if enumeration failed. + + Raises: + OSError if CreateToolhelp32Snapshot or any winapi failed. + IterationFinished if enumeration completed. """ lppe32 = PROCESSENTRY32() lppe32.dwSize = sizeof(PROCESSENTRY32) @@ -40,8 +66,11 @@ def _enum_threads(): Enumerates all the threads currintly running on the system. Yields: - lpte32 (THREADENTRY32) | - None (if enum failed) + THREADENTRY32 or None: The current thread entry or None if enumeration failed. + + Raises: + OSError if CreateToolhelp32Snapshot or any winapi failed. + IterationFinished if enumeration completed. """ lpte32 = THREADENTRY32() lpte32.dwSize = sizeof(THREADENTRY32) @@ -51,13 +80,63 @@ def _enum_threads(): yield from __yieldloop(lpte32, snapshot, Thread32Next) +def _enum_events(hevent): + event = EVT_HANDLE() + returned = DWORD(0) + while EvtNext(hevent, 1, byref(event), INFINITE, 0, byref(returned)): + if event == INVALID_HANDLE_VALUE: + report(f"Invalid handle: 0x{event}", raiseexcept=False) + continue + + buffer_size = DWORD(0) + buffer_used = DWORD(0) + property_count = DWORD(0) + rendered_content = None + + EvtRender( + None, + event, + EVT_RENDER_EVENT_XML, + buffer_size, + rendered_content, + byref(buffer_used), + byref(property_count) + ) + if GetLastError() == ERROR_SUCCESS: + yield rendered_content + continue + + buffer_size = buffer_used.value + rendered_content = create_unicode_buffer(buffer_size) + if not rendered_content: + report("malloc failed.", raiseexcept=False) + continue + + if not EvtRender( + None, + event, + EVT_RENDER_EVENT_XML, + buffer_size, + rendered_content, + byref(buffer_used), + byref(property_count) + ): + report(f"EvtRender failed with {GetLastError()}", raiseexcept=False) + continue + + if GetLastError() == ERROR_SUCCESS: + yield rendered_content.value + + EvtClose(event) + + def getfocusedwindow(): """ Get focused window. Returns: hwnd (int): Focused window hwnd - WINDOWPLACEMENT: + WINDOWPLACEMENT: The window placement or None if it couldn't be retrieved. """ hwnd = GetForegroundWindow() if not hwnd: @@ -90,22 +169,55 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def execute(command: str, sstart: bool = False): +def flash_window(focusedwindow: tuple, max_attempts: int = 5, interval: int = 1): + from time import sleep + attempts = 0 + failed = 0 + + while attempts < max_attempts: + currentwindow = getfocusedwindow() + if not (focusedwindow[0] and currentwindow[0]): + failed += 1 + if failed >= max_attempts: + report("Flash window failed.") + sleep(interval) + continue + if focusedwindow[0] != currentwindow[0]: + logger.info(f"Current window is {currentwindow[0]}, flash back to {focusedwindow[0]}") + setforegroundwindow(focusedwindow) + attempts += 1 + sleep(interval) + else: + attempts += 1 + sleep(interval) + + +def execute(command: str, silentstart: bool, start: bool): """ Create a new process. Args: command (str): process's commandline - sstart (bool): process's windowplacement + silentstart (bool): process's windowplacement + start (bool): True if start emulator, False if not Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' Returns: process: tuple(processhandle, threadhandle, processid, mainthreadid), focusedwindow: tuple(hwnd, WINDOWPLACEMENT) + + Raises: + EmulatorLaunchFailedError if CreateProcessW failed. """ from shlex import split from os.path import dirname + import threading + focusedwindow = getfocusedwindow() + if start: + focus_thread = threading.Thread(target=flash_window, args=(focusedwindow, )) + focus_thread.start() + lpApplicationName = split(command)[0] lpCommandLine = command lpProcessAttributes = None @@ -113,7 +225,7 @@ def execute(command: str, sstart: bool = False): bInheritHandles = False dwCreationFlags = ( CREATE_NO_WINDOW | - NORMAL_PRIORITY_CLASS | + IDLE_PRIORITY_CLASS | CREATE_NEW_PROCESS_GROUP | CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT @@ -123,11 +235,12 @@ def execute(command: str, sstart: bool = False): lpStartupInfo = STARTUPINFOW() lpStartupInfo.cb = sizeof(STARTUPINFOW) lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW - lpStartupInfo.wShowWindow = SW_HIDE if sstart else SW_MINIMIZE + if start: + lpStartupInfo.wShowWindow = SW_HIDE if silentstart else SW_MINIMIZE + else: + lpStartupInfo.wShowWindow = SW_HIDE lpProcessInformation = PROCESS_INFORMATION() - focusedwindow = getfocusedwindow() - success = CreateProcessW( lpApplicationName, lpCommandLine, @@ -143,7 +256,7 @@ def execute(command: str, sstart: bool = False): if not success: report("Failed to start emulator.", exception=EmulatorLaunchFailedError) - + process = ( lpProcessInformation.hProcess, lpProcessInformation.hThread, @@ -159,6 +272,9 @@ def terminate_process(pid: int): Args: pid (int): Emulator's pid + + Raises: + OSError if OpenProcess failed. """ with open_process(PROCESS_TERMINATE, pid) as hProcess: if TerminateProcess(hProcess, 0) == 0: @@ -166,6 +282,37 @@ def terminate_process(pid: int): return True +def parse_event(event: str): + ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'} + tree = Et.ElementTree(Et.fromstring(event)) + time_created = tree.find('.//ns:TimeCreated', ns).attrib['SystemTime'] + new_process_id = tree.find('.//ns:Data[@Name="NewProcessId"]', ns).text + new_process_name = tree.find('.//ns:Data[@Name="NewProcessName"]', ns).text + process_id = tree.find('.//ns:Data[@Name="ProcessId"]', ns).text + parent_process_name = tree.find('.//ns:Data[@Name="ParentProcessName"]', ns).text + return { + 'TimeCreated': time_created, + 'NewProcessId': new_process_id, + 'NewProcessName': new_process_name, + 'ProcessId': process_id, + 'ParentProcessName': parent_process_name, + } + + +def pids_manager(pid: int): + try: + if IsUserAnAdmin(): + pass + else: + return + except: + return + with evt_query() as hevent: + events = _enum_events(hevent) + for content in events: + logger.info(parse_event(content)) + + def get_hwnds(pid: int) -> list: """ Get process's window hwnds from this processid. @@ -175,6 +322,9 @@ def get_hwnds(pid: int) -> list: Returns: hwnds (list): Emulator's possible window hwnds + + Raises: + HwndNotFoundError if EnumWindows failed. """ hwnds = [] @@ -203,7 +353,7 @@ def get_cmdline(pid: int) -> str: pid (int): Emulator's pid Returns: - command line (str): process's command line + cmdline (str): process's command line Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' """ @@ -239,13 +389,17 @@ def get_cmdline(pid: int) -> str: def kill_process_by_regex(regex: str) -> int: """ - Kill processes with cmdline match the given regex. + Kill processes with cmdline match the given regex. - Args: - regex: + Args: + regex: - Returns: - int: Number of processes killed + Returns: + int: Number of processes killed + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. """ count = 0 @@ -264,7 +418,7 @@ def kill_process_by_regex(regex: str) -> int: return count -def _get_thread_creation_time(tid): +def _get_thread_creation_time(tid: int): """ Get thread's creation time. @@ -273,6 +427,9 @@ def _get_thread_creation_time(tid): Returns: threadstarttime (int): Thread's start time + + Raises: + OSError if OpenThread failed. """ with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: creationtime = FILETIME() @@ -296,8 +453,12 @@ def get_thread(pid: int): Args: pid (int): Emulator's pid - Returns + Returns: mainthreadid (int): Emulator's main thread id + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. """ mainthreadid = 0 minstarttime = MAXULONGLONG @@ -327,7 +488,7 @@ def _get_process(pid: int): Returns: tuple(processhandle, threadhandle, processid, mainthreadid) | - tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + tuple(None, None, processid, mainthreadid) """ tid = get_thread(pid) try: @@ -356,6 +517,10 @@ def get_process(instance: EmulatorInstance): tuple(processhandle, threadhandle, processid, mainthreadid) | tuple(None, None, processid, mainthreadid) | (if enum_process() failed) None (if enum_process() failed) + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. """ processes = _enum_processes() for lppe32 in processes: @@ -365,19 +530,28 @@ def get_process(instance: EmulatorInstance): continue if instance == Emulator.MuMuPlayer12: match = re.search(r'\d+$', cmdline) - if match and int(match.group()) == instance.MuMuPlayer12_id: - processes.close() - return _get_process(pid) + if not match: + continue + if int(match.group()) != instance.MuMuPlayer12_id: + continue + processes.close() + return _get_process(pid) elif instance == Emulator.LDPlayerFamily: match = re.search(r'\d+$', cmdline) - if match and int(match.group()) == instance.LDPlayer_id: - processes.close() - return _get_process(pid) + if not match: + continue + if int(match.group()) != instance.LDPlayer_id: + continue + processes.close() + return _get_process(pid) else: matchstr = re.search(fr'\b{instance.name}$', cmdline) - if matchstr and matchstr.group() == instance.name: - processes.close() - return _get_process(pid) + if not matchstr: + continue + if matchstr.group() != instance.name: + continue + processes.close() + return _get_process(pid) def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): @@ -402,3 +576,7 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): continue ShowWindow(hwnd, arg) return True + +if __name__ == '__main__': + p = 1234 + pids_manager(1234) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index b03203a348..763c2dbbfb 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -12,26 +12,35 @@ class EmulatorUnknown(Exception): class EmulatorStatus: - process: tuple = None - hwnds: list = None - focusedwindow: tuple = None + process: tuple = () + hwnds: list = [] + focusedwindow: tuple = () class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): - def execute(self, command: str): + def __execute(self, command: str, start: bool): command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') logger.info(f'Execute: {command}') + if self.config.Emulator_SilentStart == 'normal': - sstart = False + silentstart = False else: - sstart = True - if self.process is not None: + silentstart = True + + if self.process: if self.process[0] is not None and self.process[1] is not None: self.CloseHandle(self.process[:2]) - self.process, self.focusedwindow = api_windows.execute(command, sstart) - logger.info(f"Current window: {self.focusedwindow[0]}") + self.process = () + + self.process, self.focusedwindow = api_windows.execute(command, silentstart, start) return True + def _start(self, command: str): + self.__execute(command, start=True) + + def _stop(self, command: str): + self.__execute(command, start=False) + @staticmethod def CloseHandle(*args, **kwargs): for handle in args: @@ -73,9 +82,9 @@ def get_cmdline(pid: int): return api_windows.get_cmdline(pid) def switch_window(self): - if self.process is None: + if not self.process: self.process = self.get_process(self.emulator_instance) - if self.hwnds is None: + if not self.hwnds: self.hwnds = self.get_hwnds(self.process[2]) method = self.config.Emulator_SilentStart if method == 'normal': @@ -95,30 +104,30 @@ def _emulator_start(self, instance: EmulatorInstance): exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: # NemuPlayer.exe - self.execute(exe) + self._start(exe) elif instance == Emulator.MuMuPlayerX: # NemuPlayer.exe -m nemu-12.0-x64-default - self.execute(f'"{exe}" -m {instance.name}') + self._start(f'"{exe}" -m {instance.name}') elif instance == Emulator.MuMuPlayer12: # MuMuPlayer.exe -v 0 if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.execute(f'"{exe}" -v {instance.MuMuPlayer12_id}') + self._start(f'"{exe}" -v {instance.MuMuPlayer12_id}') elif instance == Emulator.LDPlayerFamily: # LDPlayer.exe index=0 - self.execute(f'"{exe}" index={instance.LDPlayer_id}') + self._start(f'"{exe}" index={instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 - self.execute(f'"{exe}" -clone:{instance.name}') + self._start(f'"{exe}" -clone:{instance.name}') elif instance == Emulator.BlueStacks5: # HD-Player.exe -instance Pie64 - self.execute(f'"{exe}" -instance {instance.name}') + self._start(f'"{exe}" -instance {instance.name}') elif instance == Emulator.BlueStacks4: # Bluestacks.exe -vmname Android_1 - self.execute(f'"{exe}" -vmname {instance.name}') + self._start(f'"{exe}" -vmname {instance.name}') elif instance == Emulator.MEmuPlayer: # MEmu.exe MEmu_0 - self.execute(f'"{exe}" {instance.name}') + self._start(f'"{exe}" {instance.name}') else: raise EmulatorUnknown(f'Cannot start an unknown emulator instance: {instance}') @@ -159,13 +168,13 @@ def _emulator_stop(self, instance: EmulatorInstance): # E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.execute(f'"{Emulator.single_to_console(exe)}" api -v {instance.MuMuPlayer12_id} shutdown_player') + self._stop(f'"{Emulator.single_to_console(exe)}" api -v {instance.MuMuPlayer12_id} shutdown_player') elif instance == Emulator.LDPlayerFamily: # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 - self.execute(f'"{Emulator.single_to_console(exe)}" quit --index {instance.LDPlayer_id}') + self._stop(f'"{Emulator.single_to_console(exe)}" quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit - self.execute(f'"{exe}" -clone:{instance.name} -quit') + self._stop(f'"{exe}" -clone:{instance.name} -quit') elif instance == Emulator.BlueStacks5: # BlueStack has 2 processes # C:\Program Files\BlueStacks_nxt_cn\HD-Player.exe --instance Pie64 @@ -177,10 +186,10 @@ def _emulator_stop(self, instance: EmulatorInstance): ) elif instance == Emulator.BlueStacks4: # E:\Program Files (x86)\BluestacksCN\bsconsole.exe quit --name Android - self.execute(f'"{Emulator.single_to_console(exe)}" quit --name {instance.name}') + self._stop(f'"{Emulator.single_to_console(exe)}" quit --name {instance.name}') elif instance == Emulator.MEmuPlayer: # F:\Program Files\Microvirt\MEmu\memuc.exe stop -n MEmu_0 - self.execute(f'"{Emulator.single_to_console(exe)}" stop -n {instance.name}') + self._stop(f'"{Emulator.single_to_console(exe)}" stop -n {instance.name}') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') @@ -215,6 +224,7 @@ def emulator_start_watch(self): False if timeout """ logger.info("Emulator starting...") + logger.info(f"Current window: {self.focusedwindow[0]}") serial = self.emulator_instance.serial def adb_connect(): @@ -251,16 +261,6 @@ def show_package(m): logger.warning(f'Emulator start timeout') return False - # Flash window - currentwindow = self.getfocusedwindow() - if ( - self.focusedwindow is not None and - currentwindow is not None and - self.focusedwindow[0] != currentwindow[0] - ): - logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") - self.setforegroundwindow(self.focusedwindow) - # Check device connection devices = self.list_device().select(serial=serial) # logger.info(devices) @@ -343,20 +343,25 @@ def emulator_stop(self): def emulator_check(self): try: - if self.process is None: + if not self.process: self.process = self.get_process(self.emulator_instance) return True cmdline = self.get_cmdline(self.process[2]) if self.emulator_instance.path in cmdline: return True else: - self.process = self.get_process(self.emulator_instance) - return True - except api_windows.IterationFinished as e: + if self.process[0] is not None and self.process[1] is not None: + self.CloseHandle(self.process[:2]) + self.process = () + raise ProcessLookupError + except api_windows.IterationFinished: return False except IndexError: return False + except ProcessLookupError: + return self.emulator_check() except OSError as e: + logger.error(e) raise except Exception as e: logger.error(e) diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 4fd7c0c1e8..2139d1ffb8 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -45,9 +45,6 @@ MAXIMUM_PROC_PER_GROUP = 64 MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP -# error.h line 23 -ERROR_NO_MORE_FILES = 0x12 - # tlhelp32.h line 17 TH32CS_SNAPHEAPLIST = 0x00000001 TH32CS_SNAPPROCESS = 0x00000002 @@ -151,5 +148,51 @@ STATUS_PASSWORD_MUST_CHANGE = 0xC0000224 STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234 +# error.h line 23 +ERROR_NO_MORE_FILES = 0x12 + +# winerror.h +ERROR_SUCCESS = 0 # line 227 +ERROR_INSUFFICIENT_BUFFER = 122 # line 1041 + +# winbase.h line 822 +INFINITE = 0xFFFFFFFF + +# winevt.h line 156 +EVT_QUERY_CHANNEL_PATH = 0x1 +EVT_QUERY_FILE_PATH = 0x2 +EVT_QUERY_FORWARD_DIRECTION = 0x100 +EVT_QUERY_REVERSE_DIRECTION = 0x200 +EVT_QUERY_TOLERATE_QUERY_ERRORS = 0x1000 +# line 176 +EVT_RENDER_EVENT_VALUES = 0 +EVT_RENDER_EVENT_XML = 1 +EVT_RENDER_BOOK_MARK = 2 +# line 242 +EVT_VAR_TYPE_NULL = 0 +EVT_VAR_TYPE_STRING = 1 +EVT_VAR_TYPE_ANSISTRING = 2 +EVT_VAR_TYPE_SBYTE = 3 +EVT_VAR_TYPE_BYTE = 4 +EVT_VAR_TYPE_INT16 = 5 +EVT_VAR_TYPE_UINT16 = 6 +EVT_VAR_TYPE_INT32 = 7 +EVT_VAR_TYPE_UINT32 = 8 +EVT_VAR_TYPE_INT64 = 9 +EVT_VAR_TYPE_UINT64 = 10 +EVT_VAR_TYPE_SINGLE = 11 +EVT_VAR_TYPE_DOUBLE = 12 +EVT_VAR_TYPE_BOOLEAN = 13 +EVT_VAR_TYPE_BINARY = 14 +EVT_VAR_TYPE_GUID = 15 +EVT_VAR_TYPE_SIZET = 16 +EVT_VAR_TYPE_FILETIME = 17 +EVT_VAR_TYPE_SYSTIME = 18 +EVT_VAR_TYPE_SID = 19 +EVT_VAR_TYPE_HEXINT32 = 20 +EVT_VAR_TYPE_HEXINT64 = 21 +EVT_VAR_TYPE_EVTHANDLE = 32 +EVT_VAR_TYPE_EVTXML = 35 + MAXULONGLONG = LPVOID(-1).value INVALID_HANDLE_VALUE = -1 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index e820b47311..89a90518f2 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,3 +1,5 @@ +from abc import ABCMeta, abstractmethod + from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( HANDLE, DWORD, HWND, BOOL, INT, UINT, @@ -14,6 +16,12 @@ user32 = WinDLL(name='user32', use_last_error=True) kernel32 = WinDLL(name='kernel32', use_last_error=True) ntdll = WinDLL(name='ntdll', use_last_error=True) +wevtapi = WinDLL(name='wevtapi', use_last_error=True) +shell32 = WinDLL(name='shell32', use_last_error=True) + +IsUserAnAdmin = shell32.IsUserAnAdmin +IsUserAnAdmin.argtypes = [] +IsUserAnAdmin.restype = BOOL CreateProcessW = kernel32.CreateProcessW CreateProcessW.argtypes = [ @@ -124,35 +132,121 @@ NtQueryInformationProcess.argtypes = [HANDLE, INT, LPVOID, ULONG, PULONG] NtQueryInformationProcess.restype = NTSTATUS -class Handle: - handle = None +EVT_HANDLE = HANDLE +EvtQuery = wevtapi.EvtQuery +EvtQuery.argtypes = [EVT_HANDLE, LPCWSTR, LPCWSTR, DWORD] +EvtQuery.restype = HANDLE + +EvtNext = wevtapi.EvtNext +EvtNext.argtypes = [EVT_HANDLE, DWORD, POINTER(EVT_HANDLE), DWORD, DWORD, POINTER(DWORD)] +EvtNext.restype = BOOL + +EvtRender = wevtapi.EvtRender +EvtRender.argtypes = [EVT_HANDLE, EVT_HANDLE, DWORD, DWORD, LPVOID, POINTER(DWORD), POINTER(DWORD)] +EvtRender.restype = BOOL + +EvtClose = wevtapi.EvtClose +EvtClose.argtypes = [EVT_HANDLE] +EvtClose.restype = BOOL + +class Handle(metaclass=ABCMeta): + """ + Abstract base Handle class. + Please override these functions if needed. + """ + _handle = None + _func = None + _exitfunc = None + + def __init__(self, *args, **kwargs): + self._handle = self._func(*self.__getinitargs__(*args, **kwargs)) + if not self: + report(f"{self._func.__name__} failed.", uselog=kwargs.get('uselog', True)) def __enter__(self): - return self.handle + return self._handle def __exit__(self, exc_type, exc_val, exc_tb): - if self.handle is not None: - CloseHandle(self.handle) - self.handle = None + if self: + self._exitfunc(self._handle) + self._handle = None + + def __bool__(self): + return not self._is_invalid_handle() + + @abstractmethod + def __getinitargs__(self, *args, **kwargs): ... + @abstractmethod + def _is_invalid_handle(self): ... class ProcessHandle(Handle): - def __init__(self, access, pid, uselog): - self.handle = OpenProcess(access, False, pid) - if self.handle is None: - report("OpenProcess failed.", uselog=uselog) + _func = OpenProcess + _exitfunc = CloseHandle + + def __getinitargs__(self, access, pid, uselog): + return access, False, pid + + def _is_invalid_handle(self): + return self._handle is None class ThreadHandle(Handle): - def __init__(self, access, tid, uselog): - self.handle = OpenThread(access, False, tid) - if self.handle is None: - report("OpenThread failed.", uselog=uselog) + _func = OpenThread + _exitfunc = CloseHandle + + def __getinitargs__(self, access, pid, uselog): + return access, False, pid + + def _is_invalid_handle(self): + return self._handle is None class CreateSnapshot(Handle): - def __init__(self, arg): - self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) + _func = CreateToolhelp32Snapshot + _exitfunc = CloseHandle + + def __getinitargs__(self, arg): + return arg, DWORD(0) + + def _is_invalid_handle(self): from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE - if self.handle == INVALID_HANDLE_VALUE: - report("CreateToolhelp32Snapshot failed.") + return self._handle == INVALID_HANDLE_VALUE + +class QueryEvt(Handle): + _func = EvtQuery + _exitfunc = EvtClose + + def __getinitargs__(self): + query = "Event/System[EventID=4688]" + from module.device.platform.winapi.const_windows import EVT_QUERY_REVERSE_DIRECTION, EVT_QUERY_CHANNEL_PATH + return None, "Security", query, EVT_QUERY_REVERSE_DIRECTION | EVT_QUERY_CHANNEL_PATH + + def _is_invalid_handle(self): + return self._handle is None + +class EventTree: + class __Node: + def __init__(self, data=None, parent=None, children: list = None): + self.data = data + self.parent = parent + self.son = [] if children is None else children + + def __init__(self): + self.root = None + + def add_parent(self, data): + if self.root is None: + self.root = self.__Node(data) + return + node = self.__Node(data, children=[self.root, ]) + self.root.parent = node + self.root = node + + def add_son(self, data): + if self.root is None: + self.root = self.__Node(data) + return + node = self.__Node(data, parent=self.root) + self.root.son.append(node) + def report( msg: str = '', @@ -164,8 +258,8 @@ def report( exception: type = OSError, ): """ - Raise exception. - + Report any exception. + Args: msg (str): statuscode (int): @@ -174,22 +268,29 @@ def report( handle (int): Handle to close raiseexcept (bool): Flag indicating whether to raise exception (Type[Exception]): Exception class to raise + + Raises: + Optional[OSError]: """ from module.logger import logger if statuscode == -1: statuscode = GetLastError() + message = f"{msg} Status code: 0x{statuscode:08x}" if uselog: - logger.log(level, f"{msg} Status code: 0x{statuscode:08x}") + logger.log(level, message) if handle: CloseHandle(handle) if raiseexcept: - raise exception(statuscode) + raise exception(message) def open_process(access, pid, uselog=False): - return ProcessHandle(access, pid, uselog) + return ProcessHandle(access, pid, uselog=uselog) def open_thread(access, tid, uselog=False): - return ThreadHandle(access, tid, uselog) + return ThreadHandle(access, tid, uselog=uselog) def create_snapshot(arg): return CreateSnapshot(arg) + +def evt_query(): + return QueryEvt() diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index d26eb342fe..35346abf24 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -1,7 +1,7 @@ from ctypes import POINTER, Structure from ctypes.wintypes import ( - HANDLE, DWORD, WORD, LARGE_INTEGER, BYTE, BOOL, BOOLEAN, - USHORT, UINT, LONG, ULONG, CHAR, LPWSTR, LPVOID, MAX_PATH, + HANDLE, DWORD, WORD, BYTE, BOOL, USHORT, + UINT, LONG, CHAR, LPWSTR, LPVOID, MAX_PATH, RECT, PULONG, POINT, PWCHAR, FILETIME ) @@ -9,6 +9,15 @@ class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... class IterationFinished(Exception): ... +# processthreadsapi.h line 28 +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ('hProcess', HANDLE), + ('hThread', HANDLE), + ('dwProcessId', DWORD), + ('dwThreadId', DWORD) + ] + class STARTUPINFOW(Structure): _fields_ = [ ('cb', DWORD), @@ -31,14 +40,7 @@ class STARTUPINFOW(Structure): ('hStdError', HANDLE) ] -class PROCESS_INFORMATION(Structure): - _fields_ = [ - ('hProcess', HANDLE), - ('hThread', HANDLE), - ('dwProcessId', DWORD), - ('dwThreadId', DWORD) - ] - +# minwinbase.h line 13 class SECURITY_ATTRIBUTES(Structure): _fields_ = [ ("nLength", DWORD), @@ -46,6 +48,7 @@ class SECURITY_ATTRIBUTES(Structure): ("bInheritHandle", BOOL) ] +# tlhelp32.h line 62 class PROCESSENTRY32(Structure): _fields_ = [ ("dwSize", DWORD), @@ -71,6 +74,7 @@ class THREADENTRY32(Structure): ("dwFlags", DWORD) ] +# winuser.h line 1801 class WINDOWPLACEMENT(Structure): _fields_ = [ ("length", UINT), @@ -81,13 +85,8 @@ class WINDOWPLACEMENT(Structure): ("rcNormalPosition", RECT) ] -class LIST_ENTRY(Structure): - _fields_ = [ - ("Flink", POINTER(LPVOID)), - ("Blink", POINTER(LPVOID)) - ] -# ntbasic.h line 111 +# winternl.h line 25 class UNICODE_STRING(Structure): _fields_ = [ ("Length", USHORT), @@ -95,147 +94,33 @@ class UNICODE_STRING(Structure): ("Buffer", PWCHAR) ] -# ntpsapi.h line 63 -class PEB_LDR_DATA(Structure): - _fields_ = [ - ("Length", ULONG), - ("Initialized", BOOLEAN), - ("SsHandle", HANDLE), - ("InLoadOrderModuleList", LIST_ENTRY), - ("InMemoryOrderModuleList", LIST_ENTRY), - ("InInitializationOrderModuleList", LIST_ENTRY), - ("EntryInProgress", LPVOID), - ("ShutdownInProgress", BOOLEAN), - ("ShutdownThreadId", HANDLE) - ] - -# ntpebteb.h line 8 -class PEB(Structure): - _fields_ = [ - ("InheritedAddressSpace", BOOLEAN), - ("ReadImageFileExecOptions", BOOLEAN), - ("BeingDebugged", BOOLEAN), - ("Spare", BOOLEAN), - ("Mutant", HANDLE), - ("ImageBaseAddress", LPVOID), - ("Ldr", POINTER(PEB_LDR_DATA)), - ("ProcessParameters", LPVOID), - ("SubSystemData", LPVOID), - ("ProcessHeap", LPVOID), - ("FastPebLock", LPVOID), - ("FastPebLockRoutine", LPVOID), - ("FastPebUnlockRoutine", LPVOID), - ("EnvironmentUpdateCount", ULONG), - ("KernelCallbackTable", LPVOID), - ("EventLogSection", LPVOID), - ("EventLog", LPVOID), - ("FreeList", LPVOID), - ("TlsExpansionCounter", ULONG), - ("TlsBitmap", LPVOID), - ("TlsBitmapBits", ULONG * 2), - ("ReadOnlySharedMemoryBase", LPVOID), - ("ReadOnlySharedMemoryHeap", LPVOID), - ("ReadOnlyStaticServerData", LPVOID), - ("AnsiCodePageData", LPVOID), - ("OemCodePageData", LPVOID), - ("UnicodeCaseTableData", LPVOID), - ("NumberOfProcessors", ULONG), - ("NtGlobalFlag", ULONG), - ("Spare2", BYTE * 4), - ("CriticalSectionTimeout", LARGE_INTEGER), - ("HeapSegmentReserve", ULONG), - ("HeapSegmentCommit", ULONG), - ("HeapDeCommitTotalFreeThreshold", ULONG), - ("HeapDeCommitFreeBlockThreshold", ULONG), - ("NumberOfHeaps", ULONG), - ("MaximumNumberOfHeaps", ULONG), - ("ProcessHeaps", POINTER(LPVOID)), - ("GdiSharedHandleTable", LPVOID), - ("ProcessStarterHelper", LPVOID), - ("GdiDCAttributeList", LPVOID), - ("LoaderLock", LPVOID), - ("OSMajorVersion", ULONG), - ("OSMinorVersion", ULONG), - ("OSBuildNumber", ULONG), - ("OSPlatformId", ULONG), - ("ImageSubSystem", ULONG), - ("ImageSubSystemMajorVersion", ULONG), - ("ImageSubSystemMinorVersion", ULONG), - ("GdiHandleBuffer", ULONG * 34), - ("PostProcessInitRoutine", ULONG), - ("TlsExpansionBitmap", ULONG), - ("TlsExpansionBitmapBits", BYTE * 32), - ("SessionId", ULONG) - ] - -# ntrtl.h line 2320 -class CURDIR(Structure): - _fields_ = [ - ("DosPath", UNICODE_STRING), - ("Handle", HANDLE) - ] -# ntrtl.h line 2329 -class RTL_DRIVE_LETTER_CURDIR(Structure): +# winternl.h line 54 +class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ - ("Flags", USHORT), - ("Length", USHORT), - ("TimeStamp", ULONG), - ("DosPath", UNICODE_STRING) + ("Reserved", LPVOID * 12), + ("ImagePathName", UNICODE_STRING), + ("CommandLine", UNICODE_STRING) ] -# ntrtl.h line 2340 -class RTL_USER_PROCESS_PARAMETERS(Structure): +class PEB(Structure): _fields_ = [ - ("MaximumLength", ULONG), - ("Length", ULONG), - - ("Flags", ULONG), - ("DebugFlags", ULONG), - - ("ConsoleHandle", LPVOID), - ("ConsoleFlags", ULONG), - ("StandardInput", LPVOID), - ("StandardOutput", LPVOID), - ("StandardError", LPVOID), - - ("CurrentDirectory", CURDIR), - ("DllPath", UNICODE_STRING), - ("ImagePathName", UNICODE_STRING), - ("CommandLine", UNICODE_STRING), - ("Environment", LPVOID), - - ("StartingX", ULONG), - ("StartingY", ULONG), - ("CountX", ULONG), - ("CountY", ULONG), - ("CountCharsX", ULONG), - ("CountCharsY", ULONG), - ("FillAttribute", ULONG), - - ("WindowFlags", ULONG), - ("ShowWindowFlags", ULONG), - ("WindowTitle", UNICODE_STRING), - ("DesktopInfo", UNICODE_STRING), - ("ShellInfo", UNICODE_STRING), - ("RuntimeData", UNICODE_STRING), - ("CurrentDirectories", RTL_DRIVE_LETTER_CURDIR * 32), - - ("EnvironmentSize", ULONG), - ("EnvironmentVersion", ULONG), - ("PackageDependencyData", LPVOID), - ("ProcessGroupId", ULONG), - ("LoaderThreads", ULONG) + ("Reserved", BYTE * 28), + ("ProcessParameters", POINTER(RTL_USER_PROCESS_PARAMETERS)), ] class PROCESS_BASIC_INFORMATION(Structure): + NTSTATUS = LONG + KPRIORITY = LONG + KAFFINITY = PULONG _fields_ = [ - ("Reserved1", LPVOID), - ("PebBaseAddress", POINTER(PEB)), - ("Reserved2", LPVOID * 2), - ("UniqueProcessId", ULONG), - ("Reserved3", LPVOID) + ("ExitStatus", NTSTATUS), + ("PebBaseAddress", POINTER(PEB)), + ("AffinityMask", KAFFINITY), + ("BasePriority", KPRIORITY), + ("UniqueProcessId", PULONG), + ("InheritedFromUniqueProcessId", PULONG), ] -def to_int(time: FILETIME): - return (time.dwHighDateTime << 32) + time.dwLowDateTime +def to_int(filetime: FILETIME): + return (filetime.dwHighDateTime << 32) + filetime.dwLowDateTime From 9a81d175ca30e53e815e9579ac6047477645c93f Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Wed, 10 Jul 2024 18:10:06 +0800 Subject: [PATCH 098/161] upd: add ProcessManager. --- module/device/platform/api_windows.py | 108 ++++++++++-------- module/device/platform/platform_windows.py | 2 +- .../platform/winapi/functions_windows.py | 61 +++++++--- 3 files changed, 105 insertions(+), 66 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 11a4ab8b4d..1ab0f52884 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,5 +1,5 @@ -import re -import xml.etree.ElementTree as Et +import threading +import asyncio from ctypes import byref, sizeof, create_unicode_buffer, wstring_at, addressof @@ -8,13 +8,6 @@ from module.logger import logger -def is_admin(): - try: - return IsUserAnAdmin() - except: - return False - - def __yieldloop(entry32, snapshot, func: callable): """ Generates a loop that yields entries from a snapshot until the function fails or finishes. @@ -212,7 +205,6 @@ def execute(command: str, silentstart: bool, start: bool): """ from shlex import split from os.path import dirname - import threading focusedwindow = getfocusedwindow() if start: focus_thread = threading.Thread(target=flash_window, args=(focusedwindow, )) @@ -282,37 +274,6 @@ def terminate_process(pid: int): return True -def parse_event(event: str): - ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'} - tree = Et.ElementTree(Et.fromstring(event)) - time_created = tree.find('.//ns:TimeCreated', ns).attrib['SystemTime'] - new_process_id = tree.find('.//ns:Data[@Name="NewProcessId"]', ns).text - new_process_name = tree.find('.//ns:Data[@Name="NewProcessName"]', ns).text - process_id = tree.find('.//ns:Data[@Name="ProcessId"]', ns).text - parent_process_name = tree.find('.//ns:Data[@Name="ParentProcessName"]', ns).text - return { - 'TimeCreated': time_created, - 'NewProcessId': new_process_id, - 'NewProcessName': new_process_name, - 'ProcessId': process_id, - 'ParentProcessName': parent_process_name, - } - - -def pids_manager(pid: int): - try: - if IsUserAnAdmin(): - pass - else: - return - except: - return - with evt_query() as hevent: - events = _enum_events(hevent) - for content in events: - logger.info(parse_event(content)) - - def get_hwnds(pid: int) -> list: """ Get process's window hwnds from this processid. @@ -384,7 +345,7 @@ def get_cmdline(pid: int) -> str: cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: return '' - return cmdline.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + return fstr(cmdline) def kill_process_by_regex(regex: str) -> int: @@ -529,26 +490,26 @@ def get_process(instance: EmulatorInstance): if not instance.path in cmdline: continue if instance == Emulator.MuMuPlayer12: - match = re.search(r'\d+$', cmdline) + match = re.search(r'-v\s*(\d+)', cmdline) if not match: continue - if int(match.group()) != instance.MuMuPlayer12_id: + if int(match.group(1)) != instance.MuMuPlayer12_id: continue processes.close() return _get_process(pid) elif instance == Emulator.LDPlayerFamily: - match = re.search(r'\d+$', cmdline) + match = re.search(r'index=\s*(\d+)', cmdline) if not match: continue - if int(match.group()) != instance.LDPlayer_id: + if int(match.group(1)) != instance.LDPlayer_id: continue processes.close() return _get_process(pid) else: - matchstr = re.search(fr'\b{instance.name}$', cmdline) - if not matchstr: + matchname = re.search(fr'{instance.name}(\s+|$)', cmdline) + if not matchname: continue - if matchstr.group() != instance.name: + if matchname.group(0).strip() != instance.name: continue processes.close() return _get_process(pid) @@ -577,6 +538,53 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): ShowWindow(hwnd, arg) return True +class ProcessManager: + def __init__(self, pid: int): + self.mainpid = pid + self.datas = [] + self.lock = threading.Lock() + self.loop = asyncio.get_event_loop() + self.loop.create_task(self.listener()) + + async def listener(self): + # TODO 监听grab/kill事件 + while True: + event = await self.get_event() + if event == "grab": + await self.grab_pids() + elif event == "kill": + await self.kill_pids() + await asyncio.sleep(1) + + async def get_event(self): + # TODO 获取事件 + await asyncio.sleep(1) + return "grab" + + async def grab_pids(self, pid: int): + # TODO 获取data并建立启动链条 + if not IsUserAnAdmin(): + return + with evt_query() as hevent: + evttree = EventTree() + events = _enum_events(hevent) + for content in events: + data = evttree.parse_event(content) + with self.lock: + self.datas.append(data) + if data.process_id == pid: + break + self.datas = self.datas[::-1] + + async def kill_pids(self): + # TODO 依据启动关系依序遍历杀死进程 + with self.lock: + while self.pids: + pass + + def start(self): + threading.Thread(target=self.loop.run_forever).start() + if __name__ == '__main__': p = 1234 - pids_manager(1234) + PM = ProcessManager(1234) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 763c2dbbfb..07ff0545c5 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -19,7 +19,7 @@ class EmulatorStatus: class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): def __execute(self, command: str, start: bool): - command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + command = api_windows.fstr(command) logger.info(f'Execute: {command}') if self.config.Emulator_SilentStart == 'normal': diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 89a90518f2..839f0367a6 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,4 +1,7 @@ from abc import ABCMeta, abstractmethod +from datetime import datetime +import xml.etree.ElementTree as Et +import re from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( @@ -222,30 +225,52 @@ def __getinitargs__(self): def _is_invalid_handle(self): return self._handle is None +class Data: + def __init__(self, data: dict, time: datetime): + self.system_time: datetime = time + self.new_process_id: int = data.get("NewProcessId", 0) + self.new_process_name: str = data.get("NewProcessName", '') + self.process_id: int = data.get("ProcessId", 0) + self.process_name: str = data.get("ParentProcessName", '') + +class Node: + def __init__(self, data: Data = None, parent: 'Node' = None, children: list = None): + self.data = data + self.parent = parent + self.children = [] if children is None else children + class EventTree: - class __Node: - def __init__(self, data=None, parent=None, children: list = None): - self.data = data - self.parent = parent - self.son = [] if children is None else children + # TODO 建立启动链条 + root: Node = None + + @staticmethod + def parse_event(event: str): + ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'} + root = Et.fromstring(event) + system_time_str = root.find('.//ns:TimeCreated', ns).attrib['SystemTime'] + match = re.match(r'(.*\.\d{6})\d?(Z)', system_time_str) + modifiedtime = match.group(1) + match.group(2) if match else system_time_str + system_time = datetime.strptime(modifiedtime, '%Y-%m-%dT%H:%M:%S.%f%z').astimezone() - def __init__(self): - self.root = None + fields = ["NewProcessId", "NewProcessName", "ProcessId", "ParentProcessName"] + data = {field: fstr(root.find(f'.//ns:Data[@Name="{field}"]', ns).text) for field in fields} + + return Data(data, system_time) def add_parent(self, data): if self.root is None: - self.root = self.__Node(data) - return - node = self.__Node(data, children=[self.root, ]) + self.root = Node(data) + return True + node = Node(data, children=[self.root, ]) self.root.parent = node self.root = node - def add_son(self, data): + def add_children(self, data): if self.root is None: - self.root = self.__Node(data) - return - node = self.__Node(data, parent=self.root) - self.root.son.append(node) + self.root = Node(data) + return True + node = Node(data, parent=self.root) + self.root.children.append(node) def report( @@ -283,6 +308,12 @@ def report( if raiseexcept: raise exception(message) +def fstr(formatstr: str): + try: + return int(formatstr, 16) + except ValueError: + return formatstr.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + def open_process(access, pid, uselog=False): return ProcessHandle(access, pid, uselog=uselog) From fd8251a1bbe39d0b4e85559d2275a848c59c3530 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 11 Jul 2024 19:34:13 +0800 Subject: [PATCH 099/161] Upd: fix bugs. --- module/device/platform/api_windows.py | 41 ++++++++++---- .../platform/winapi/functions_windows.py | 53 ++++++++++++------- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 1ab0f52884..bf8159dab8 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -162,27 +162,29 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def flash_window(focusedwindow: tuple, max_attempts: int = 5, interval: int = 1): +def flash_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5): from time import sleep attempts = 0 failed = 0 while attempts < max_attempts: currentwindow = getfocusedwindow() + if not (focusedwindow[0] and currentwindow[0]): failed += 1 if failed >= max_attempts: report("Flash window failed.") sleep(interval) continue + if focusedwindow[0] != currentwindow[0]: logger.info(f"Current window is {currentwindow[0]}, flash back to {focusedwindow[0]}") setforegroundwindow(focusedwindow) attempts += 1 - sleep(interval) - else: - attempts += 1 - sleep(interval) + continue + + attempts += 1 + sleep(interval) def execute(command: str, silentstart: bool, start: bool): @@ -539,15 +541,18 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): return True class ProcessManager: + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, pid: int): self.mainpid = pid self.datas = [] + self.pids = [] + self.evttree = EventTree() self.lock = threading.Lock() self.loop = asyncio.get_event_loop() self.loop.create_task(self.listener()) async def listener(self): - # TODO 监听grab/kill事件 + # TODO listening grab/kill event while True: event = await self.get_event() if event == "grab": @@ -557,28 +562,42 @@ async def listener(self): await asyncio.sleep(1) async def get_event(self): - # TODO 获取事件 + # TODO get event await asyncio.sleep(1) return "grab" async def grab_pids(self, pid: int): - # TODO 获取data并建立启动链条 if not IsUserAnAdmin(): return with evt_query() as hevent: - evttree = EventTree() events = _enum_events(hevent) for content in events: - data = evttree.parse_event(content) + data = self.evttree.parse_event(content) with self.lock: self.datas.append(data) if data.process_id == pid: break self.datas = self.datas[::-1] + await self.filltree() + + async def filltree(self): + self.evttree.root = Node(self.datas[0]) + for data in self.datas[1::]: + evtiter = self.evttree.pre_traversal(self.evttree.root) + for node in evtiter: + if data != node.data: + continue + cmdline = get_cmdline(data.process_id) + if data.process_name not in cmdline: + continue + node.add_children(data) async def kill_pids(self): - # TODO 依据启动关系依序遍历杀死进程 + # TODO kill process by enumerating tree with self.lock: + evtiter = self.evttree.post_traversal(self.evttree.root) + for node in evtiter: + terminate_process(node.data.process_id) while self.pids: pass diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 839f0367a6..a186f6563c 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -226,6 +226,7 @@ def _is_invalid_handle(self): return self._handle is None class Data: + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, data: dict, time: datetime): self.system_time: datetime = time self.new_process_id: int = data.get("NewProcessId", 0) @@ -233,14 +234,28 @@ def __init__(self, data: dict, time: datetime): self.process_id: int = data.get("ProcessId", 0) self.process_name: str = data.get("ParentProcessName", '') + def __eq__(self, other: 'Data'): + if isinstance(other, Data): + return self.process_id == other.new_process_id + return NotImplemented + class Node: - def __init__(self, data: Data = None, parent: 'Node' = None, children: list = None): + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + def __init__(self, data: Data = None): self.data = data - self.parent = parent - self.children = [] if children is None else children + self.children = [] + + def __del__(self): + if self.data is not None: + del self.data + if self.children: + del self.children + + def add_children(self, data): + self.children.append(Node(data)) class EventTree: - # TODO 建立启动链条 + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! root: Node = None @staticmethod @@ -257,21 +272,21 @@ def parse_event(event: str): return Data(data, system_time) - def add_parent(self, data): - if self.root is None: - self.root = Node(data) - return True - node = Node(data, children=[self.root, ]) - self.root.parent = node - self.root = node - - def add_children(self, data): - if self.root is None: - self.root = Node(data) - return True - node = Node(data, parent=self.root) - self.root.children.append(node) - + def pre_traversal(self, node: Node = None): + if node is not None: + yield node + for child in node.children: + yield from self.pre_traversal(child) + + def post_traversal(self, node: Node = None): + if node is not None: + for child in node.children: + yield from self.post_traversal(child) + yield node + + def delete_tree(self): + del self.root + self.root = None def report( msg: str = '', From 7ccaaeb76f1e680373e86418927718efe3ffdb85 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 11 Jul 2024 19:35:08 +0800 Subject: [PATCH 100/161] Upd: add flash_window method. --- module/device/platform/platform_windows.py | 83 ++++---- .../device/platform/winapi/const_windows.py | 49 ++++- .../platform/winapi/functions_windows.py | 195 +++++++++++++++--- .../platform/winapi/structures_windows.py | 183 +++------------- 4 files changed, 295 insertions(+), 215 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index b03203a348..07ff0545c5 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -12,26 +12,35 @@ class EmulatorUnknown(Exception): class EmulatorStatus: - process: tuple = None - hwnds: list = None - focusedwindow: tuple = None + process: tuple = () + hwnds: list = [] + focusedwindow: tuple = () class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): - def execute(self, command: str): - command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + def __execute(self, command: str, start: bool): + command = api_windows.fstr(command) logger.info(f'Execute: {command}') + if self.config.Emulator_SilentStart == 'normal': - sstart = False + silentstart = False else: - sstart = True - if self.process is not None: + silentstart = True + + if self.process: if self.process[0] is not None and self.process[1] is not None: self.CloseHandle(self.process[:2]) - self.process, self.focusedwindow = api_windows.execute(command, sstart) - logger.info(f"Current window: {self.focusedwindow[0]}") + self.process = () + + self.process, self.focusedwindow = api_windows.execute(command, silentstart, start) return True + def _start(self, command: str): + self.__execute(command, start=True) + + def _stop(self, command: str): + self.__execute(command, start=False) + @staticmethod def CloseHandle(*args, **kwargs): for handle in args: @@ -73,9 +82,9 @@ def get_cmdline(pid: int): return api_windows.get_cmdline(pid) def switch_window(self): - if self.process is None: + if not self.process: self.process = self.get_process(self.emulator_instance) - if self.hwnds is None: + if not self.hwnds: self.hwnds = self.get_hwnds(self.process[2]) method = self.config.Emulator_SilentStart if method == 'normal': @@ -95,30 +104,30 @@ def _emulator_start(self, instance: EmulatorInstance): exe: str = instance.emulator.path if instance == Emulator.MuMuPlayer: # NemuPlayer.exe - self.execute(exe) + self._start(exe) elif instance == Emulator.MuMuPlayerX: # NemuPlayer.exe -m nemu-12.0-x64-default - self.execute(f'"{exe}" -m {instance.name}') + self._start(f'"{exe}" -m {instance.name}') elif instance == Emulator.MuMuPlayer12: # MuMuPlayer.exe -v 0 if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.execute(f'"{exe}" -v {instance.MuMuPlayer12_id}') + self._start(f'"{exe}" -v {instance.MuMuPlayer12_id}') elif instance == Emulator.LDPlayerFamily: # LDPlayer.exe index=0 - self.execute(f'"{exe}" index={instance.LDPlayer_id}') + self._start(f'"{exe}" index={instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 - self.execute(f'"{exe}" -clone:{instance.name}') + self._start(f'"{exe}" -clone:{instance.name}') elif instance == Emulator.BlueStacks5: # HD-Player.exe -instance Pie64 - self.execute(f'"{exe}" -instance {instance.name}') + self._start(f'"{exe}" -instance {instance.name}') elif instance == Emulator.BlueStacks4: # Bluestacks.exe -vmname Android_1 - self.execute(f'"{exe}" -vmname {instance.name}') + self._start(f'"{exe}" -vmname {instance.name}') elif instance == Emulator.MEmuPlayer: # MEmu.exe MEmu_0 - self.execute(f'"{exe}" {instance.name}') + self._start(f'"{exe}" {instance.name}') else: raise EmulatorUnknown(f'Cannot start an unknown emulator instance: {instance}') @@ -159,13 +168,13 @@ def _emulator_stop(self, instance: EmulatorInstance): # E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') - self.execute(f'"{Emulator.single_to_console(exe)}" api -v {instance.MuMuPlayer12_id} shutdown_player') + self._stop(f'"{Emulator.single_to_console(exe)}" api -v {instance.MuMuPlayer12_id} shutdown_player') elif instance == Emulator.LDPlayerFamily: # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 - self.execute(f'"{Emulator.single_to_console(exe)}" quit --index {instance.LDPlayer_id}') + self._stop(f'"{Emulator.single_to_console(exe)}" quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit - self.execute(f'"{exe}" -clone:{instance.name} -quit') + self._stop(f'"{exe}" -clone:{instance.name} -quit') elif instance == Emulator.BlueStacks5: # BlueStack has 2 processes # C:\Program Files\BlueStacks_nxt_cn\HD-Player.exe --instance Pie64 @@ -177,10 +186,10 @@ def _emulator_stop(self, instance: EmulatorInstance): ) elif instance == Emulator.BlueStacks4: # E:\Program Files (x86)\BluestacksCN\bsconsole.exe quit --name Android - self.execute(f'"{Emulator.single_to_console(exe)}" quit --name {instance.name}') + self._stop(f'"{Emulator.single_to_console(exe)}" quit --name {instance.name}') elif instance == Emulator.MEmuPlayer: # F:\Program Files\Microvirt\MEmu\memuc.exe stop -n MEmu_0 - self.execute(f'"{Emulator.single_to_console(exe)}" stop -n {instance.name}') + self._stop(f'"{Emulator.single_to_console(exe)}" stop -n {instance.name}') else: raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}') @@ -215,6 +224,7 @@ def emulator_start_watch(self): False if timeout """ logger.info("Emulator starting...") + logger.info(f"Current window: {self.focusedwindow[0]}") serial = self.emulator_instance.serial def adb_connect(): @@ -251,16 +261,6 @@ def show_package(m): logger.warning(f'Emulator start timeout') return False - # Flash window - currentwindow = self.getfocusedwindow() - if ( - self.focusedwindow is not None and - currentwindow is not None and - self.focusedwindow[0] != currentwindow[0] - ): - logger.info(f"Current window is {currentwindow[0]}, flash back to {self.focusedwindow[0]}") - self.setforegroundwindow(self.focusedwindow) - # Check device connection devices = self.list_device().select(serial=serial) # logger.info(devices) @@ -343,20 +343,25 @@ def emulator_stop(self): def emulator_check(self): try: - if self.process is None: + if not self.process: self.process = self.get_process(self.emulator_instance) return True cmdline = self.get_cmdline(self.process[2]) if self.emulator_instance.path in cmdline: return True else: - self.process = self.get_process(self.emulator_instance) - return True - except api_windows.IterationFinished as e: + if self.process[0] is not None and self.process[1] is not None: + self.CloseHandle(self.process[:2]) + self.process = () + raise ProcessLookupError + except api_windows.IterationFinished: return False except IndexError: return False + except ProcessLookupError: + return self.emulator_check() except OSError as e: + logger.error(e) raise except Exception as e: logger.error(e) diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 4fd7c0c1e8..2139d1ffb8 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -45,9 +45,6 @@ MAXIMUM_PROC_PER_GROUP = 64 MAXIMUM_PROCESSORS = MAXIMUM_PROC_PER_GROUP -# error.h line 23 -ERROR_NO_MORE_FILES = 0x12 - # tlhelp32.h line 17 TH32CS_SNAPHEAPLIST = 0x00000001 TH32CS_SNAPPROCESS = 0x00000002 @@ -151,5 +148,51 @@ STATUS_PASSWORD_MUST_CHANGE = 0xC0000224 STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234 +# error.h line 23 +ERROR_NO_MORE_FILES = 0x12 + +# winerror.h +ERROR_SUCCESS = 0 # line 227 +ERROR_INSUFFICIENT_BUFFER = 122 # line 1041 + +# winbase.h line 822 +INFINITE = 0xFFFFFFFF + +# winevt.h line 156 +EVT_QUERY_CHANNEL_PATH = 0x1 +EVT_QUERY_FILE_PATH = 0x2 +EVT_QUERY_FORWARD_DIRECTION = 0x100 +EVT_QUERY_REVERSE_DIRECTION = 0x200 +EVT_QUERY_TOLERATE_QUERY_ERRORS = 0x1000 +# line 176 +EVT_RENDER_EVENT_VALUES = 0 +EVT_RENDER_EVENT_XML = 1 +EVT_RENDER_BOOK_MARK = 2 +# line 242 +EVT_VAR_TYPE_NULL = 0 +EVT_VAR_TYPE_STRING = 1 +EVT_VAR_TYPE_ANSISTRING = 2 +EVT_VAR_TYPE_SBYTE = 3 +EVT_VAR_TYPE_BYTE = 4 +EVT_VAR_TYPE_INT16 = 5 +EVT_VAR_TYPE_UINT16 = 6 +EVT_VAR_TYPE_INT32 = 7 +EVT_VAR_TYPE_UINT32 = 8 +EVT_VAR_TYPE_INT64 = 9 +EVT_VAR_TYPE_UINT64 = 10 +EVT_VAR_TYPE_SINGLE = 11 +EVT_VAR_TYPE_DOUBLE = 12 +EVT_VAR_TYPE_BOOLEAN = 13 +EVT_VAR_TYPE_BINARY = 14 +EVT_VAR_TYPE_GUID = 15 +EVT_VAR_TYPE_SIZET = 16 +EVT_VAR_TYPE_FILETIME = 17 +EVT_VAR_TYPE_SYSTIME = 18 +EVT_VAR_TYPE_SID = 19 +EVT_VAR_TYPE_HEXINT32 = 20 +EVT_VAR_TYPE_HEXINT64 = 21 +EVT_VAR_TYPE_EVTHANDLE = 32 +EVT_VAR_TYPE_EVTXML = 35 + MAXULONGLONG = LPVOID(-1).value INVALID_HANDLE_VALUE = -1 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index e820b47311..a186f6563c 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,3 +1,8 @@ +from abc import ABCMeta, abstractmethod +from datetime import datetime +import xml.etree.ElementTree as Et +import re + from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( HANDLE, DWORD, HWND, BOOL, INT, UINT, @@ -14,6 +19,12 @@ user32 = WinDLL(name='user32', use_last_error=True) kernel32 = WinDLL(name='kernel32', use_last_error=True) ntdll = WinDLL(name='ntdll', use_last_error=True) +wevtapi = WinDLL(name='wevtapi', use_last_error=True) +shell32 = WinDLL(name='shell32', use_last_error=True) + +IsUserAnAdmin = shell32.IsUserAnAdmin +IsUserAnAdmin.argtypes = [] +IsUserAnAdmin.restype = BOOL CreateProcessW = kernel32.CreateProcessW CreateProcessW.argtypes = [ @@ -124,35 +135,158 @@ NtQueryInformationProcess.argtypes = [HANDLE, INT, LPVOID, ULONG, PULONG] NtQueryInformationProcess.restype = NTSTATUS -class Handle: - handle = None +EVT_HANDLE = HANDLE +EvtQuery = wevtapi.EvtQuery +EvtQuery.argtypes = [EVT_HANDLE, LPCWSTR, LPCWSTR, DWORD] +EvtQuery.restype = HANDLE + +EvtNext = wevtapi.EvtNext +EvtNext.argtypes = [EVT_HANDLE, DWORD, POINTER(EVT_HANDLE), DWORD, DWORD, POINTER(DWORD)] +EvtNext.restype = BOOL + +EvtRender = wevtapi.EvtRender +EvtRender.argtypes = [EVT_HANDLE, EVT_HANDLE, DWORD, DWORD, LPVOID, POINTER(DWORD), POINTER(DWORD)] +EvtRender.restype = BOOL + +EvtClose = wevtapi.EvtClose +EvtClose.argtypes = [EVT_HANDLE] +EvtClose.restype = BOOL + +class Handle(metaclass=ABCMeta): + """ + Abstract base Handle class. + Please override these functions if needed. + """ + _handle = None + _func = None + _exitfunc = None + + def __init__(self, *args, **kwargs): + self._handle = self._func(*self.__getinitargs__(*args, **kwargs)) + if not self: + report(f"{self._func.__name__} failed.", uselog=kwargs.get('uselog', True)) def __enter__(self): - return self.handle + return self._handle def __exit__(self, exc_type, exc_val, exc_tb): - if self.handle is not None: - CloseHandle(self.handle) - self.handle = None + if self: + self._exitfunc(self._handle) + self._handle = None + + def __bool__(self): + return not self._is_invalid_handle() + + @abstractmethod + def __getinitargs__(self, *args, **kwargs): ... + @abstractmethod + def _is_invalid_handle(self): ... class ProcessHandle(Handle): - def __init__(self, access, pid, uselog): - self.handle = OpenProcess(access, False, pid) - if self.handle is None: - report("OpenProcess failed.", uselog=uselog) + _func = OpenProcess + _exitfunc = CloseHandle + + def __getinitargs__(self, access, pid, uselog): + return access, False, pid + + def _is_invalid_handle(self): + return self._handle is None class ThreadHandle(Handle): - def __init__(self, access, tid, uselog): - self.handle = OpenThread(access, False, tid) - if self.handle is None: - report("OpenThread failed.", uselog=uselog) + _func = OpenThread + _exitfunc = CloseHandle + + def __getinitargs__(self, access, pid, uselog): + return access, False, pid + + def _is_invalid_handle(self): + return self._handle is None class CreateSnapshot(Handle): - def __init__(self, arg): - self.handle = CreateToolhelp32Snapshot(arg, DWORD(0)) + _func = CreateToolhelp32Snapshot + _exitfunc = CloseHandle + + def __getinitargs__(self, arg): + return arg, DWORD(0) + + def _is_invalid_handle(self): from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE - if self.handle == INVALID_HANDLE_VALUE: - report("CreateToolhelp32Snapshot failed.") + return self._handle == INVALID_HANDLE_VALUE + +class QueryEvt(Handle): + _func = EvtQuery + _exitfunc = EvtClose + + def __getinitargs__(self): + query = "Event/System[EventID=4688]" + from module.device.platform.winapi.const_windows import EVT_QUERY_REVERSE_DIRECTION, EVT_QUERY_CHANNEL_PATH + return None, "Security", query, EVT_QUERY_REVERSE_DIRECTION | EVT_QUERY_CHANNEL_PATH + + def _is_invalid_handle(self): + return self._handle is None + +class Data: + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + def __init__(self, data: dict, time: datetime): + self.system_time: datetime = time + self.new_process_id: int = data.get("NewProcessId", 0) + self.new_process_name: str = data.get("NewProcessName", '') + self.process_id: int = data.get("ProcessId", 0) + self.process_name: str = data.get("ParentProcessName", '') + + def __eq__(self, other: 'Data'): + if isinstance(other, Data): + return self.process_id == other.new_process_id + return NotImplemented + +class Node: + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + def __init__(self, data: Data = None): + self.data = data + self.children = [] + + def __del__(self): + if self.data is not None: + del self.data + if self.children: + del self.children + + def add_children(self, data): + self.children.append(Node(data)) + +class EventTree: + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + root: Node = None + + @staticmethod + def parse_event(event: str): + ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'} + root = Et.fromstring(event) + system_time_str = root.find('.//ns:TimeCreated', ns).attrib['SystemTime'] + match = re.match(r'(.*\.\d{6})\d?(Z)', system_time_str) + modifiedtime = match.group(1) + match.group(2) if match else system_time_str + system_time = datetime.strptime(modifiedtime, '%Y-%m-%dT%H:%M:%S.%f%z').astimezone() + + fields = ["NewProcessId", "NewProcessName", "ProcessId", "ParentProcessName"] + data = {field: fstr(root.find(f'.//ns:Data[@Name="{field}"]', ns).text) for field in fields} + + return Data(data, system_time) + + def pre_traversal(self, node: Node = None): + if node is not None: + yield node + for child in node.children: + yield from self.pre_traversal(child) + + def post_traversal(self, node: Node = None): + if node is not None: + for child in node.children: + yield from self.post_traversal(child) + yield node + + def delete_tree(self): + del self.root + self.root = None def report( msg: str = '', @@ -164,8 +298,8 @@ def report( exception: type = OSError, ): """ - Raise exception. - + Report any exception. + Args: msg (str): statuscode (int): @@ -174,22 +308,35 @@ def report( handle (int): Handle to close raiseexcept (bool): Flag indicating whether to raise exception (Type[Exception]): Exception class to raise + + Raises: + Optional[OSError]: """ from module.logger import logger if statuscode == -1: statuscode = GetLastError() + message = f"{msg} Status code: 0x{statuscode:08x}" if uselog: - logger.log(level, f"{msg} Status code: 0x{statuscode:08x}") + logger.log(level, message) if handle: CloseHandle(handle) if raiseexcept: - raise exception(statuscode) + raise exception(message) + +def fstr(formatstr: str): + try: + return int(formatstr, 16) + except ValueError: + return formatstr.replace(r"\\", "/").replace("\\", "/").replace('"', '"') def open_process(access, pid, uselog=False): - return ProcessHandle(access, pid, uselog) + return ProcessHandle(access, pid, uselog=uselog) def open_thread(access, tid, uselog=False): - return ThreadHandle(access, tid, uselog) + return ThreadHandle(access, tid, uselog=uselog) def create_snapshot(arg): return CreateSnapshot(arg) + +def evt_query(): + return QueryEvt() diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index d26eb342fe..35346abf24 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -1,7 +1,7 @@ from ctypes import POINTER, Structure from ctypes.wintypes import ( - HANDLE, DWORD, WORD, LARGE_INTEGER, BYTE, BOOL, BOOLEAN, - USHORT, UINT, LONG, ULONG, CHAR, LPWSTR, LPVOID, MAX_PATH, + HANDLE, DWORD, WORD, BYTE, BOOL, USHORT, + UINT, LONG, CHAR, LPWSTR, LPVOID, MAX_PATH, RECT, PULONG, POINT, PWCHAR, FILETIME ) @@ -9,6 +9,15 @@ class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... class IterationFinished(Exception): ... +# processthreadsapi.h line 28 +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ('hProcess', HANDLE), + ('hThread', HANDLE), + ('dwProcessId', DWORD), + ('dwThreadId', DWORD) + ] + class STARTUPINFOW(Structure): _fields_ = [ ('cb', DWORD), @@ -31,14 +40,7 @@ class STARTUPINFOW(Structure): ('hStdError', HANDLE) ] -class PROCESS_INFORMATION(Structure): - _fields_ = [ - ('hProcess', HANDLE), - ('hThread', HANDLE), - ('dwProcessId', DWORD), - ('dwThreadId', DWORD) - ] - +# minwinbase.h line 13 class SECURITY_ATTRIBUTES(Structure): _fields_ = [ ("nLength", DWORD), @@ -46,6 +48,7 @@ class SECURITY_ATTRIBUTES(Structure): ("bInheritHandle", BOOL) ] +# tlhelp32.h line 62 class PROCESSENTRY32(Structure): _fields_ = [ ("dwSize", DWORD), @@ -71,6 +74,7 @@ class THREADENTRY32(Structure): ("dwFlags", DWORD) ] +# winuser.h line 1801 class WINDOWPLACEMENT(Structure): _fields_ = [ ("length", UINT), @@ -81,13 +85,8 @@ class WINDOWPLACEMENT(Structure): ("rcNormalPosition", RECT) ] -class LIST_ENTRY(Structure): - _fields_ = [ - ("Flink", POINTER(LPVOID)), - ("Blink", POINTER(LPVOID)) - ] -# ntbasic.h line 111 +# winternl.h line 25 class UNICODE_STRING(Structure): _fields_ = [ ("Length", USHORT), @@ -95,147 +94,33 @@ class UNICODE_STRING(Structure): ("Buffer", PWCHAR) ] -# ntpsapi.h line 63 -class PEB_LDR_DATA(Structure): - _fields_ = [ - ("Length", ULONG), - ("Initialized", BOOLEAN), - ("SsHandle", HANDLE), - ("InLoadOrderModuleList", LIST_ENTRY), - ("InMemoryOrderModuleList", LIST_ENTRY), - ("InInitializationOrderModuleList", LIST_ENTRY), - ("EntryInProgress", LPVOID), - ("ShutdownInProgress", BOOLEAN), - ("ShutdownThreadId", HANDLE) - ] - -# ntpebteb.h line 8 -class PEB(Structure): - _fields_ = [ - ("InheritedAddressSpace", BOOLEAN), - ("ReadImageFileExecOptions", BOOLEAN), - ("BeingDebugged", BOOLEAN), - ("Spare", BOOLEAN), - ("Mutant", HANDLE), - ("ImageBaseAddress", LPVOID), - ("Ldr", POINTER(PEB_LDR_DATA)), - ("ProcessParameters", LPVOID), - ("SubSystemData", LPVOID), - ("ProcessHeap", LPVOID), - ("FastPebLock", LPVOID), - ("FastPebLockRoutine", LPVOID), - ("FastPebUnlockRoutine", LPVOID), - ("EnvironmentUpdateCount", ULONG), - ("KernelCallbackTable", LPVOID), - ("EventLogSection", LPVOID), - ("EventLog", LPVOID), - ("FreeList", LPVOID), - ("TlsExpansionCounter", ULONG), - ("TlsBitmap", LPVOID), - ("TlsBitmapBits", ULONG * 2), - ("ReadOnlySharedMemoryBase", LPVOID), - ("ReadOnlySharedMemoryHeap", LPVOID), - ("ReadOnlyStaticServerData", LPVOID), - ("AnsiCodePageData", LPVOID), - ("OemCodePageData", LPVOID), - ("UnicodeCaseTableData", LPVOID), - ("NumberOfProcessors", ULONG), - ("NtGlobalFlag", ULONG), - ("Spare2", BYTE * 4), - ("CriticalSectionTimeout", LARGE_INTEGER), - ("HeapSegmentReserve", ULONG), - ("HeapSegmentCommit", ULONG), - ("HeapDeCommitTotalFreeThreshold", ULONG), - ("HeapDeCommitFreeBlockThreshold", ULONG), - ("NumberOfHeaps", ULONG), - ("MaximumNumberOfHeaps", ULONG), - ("ProcessHeaps", POINTER(LPVOID)), - ("GdiSharedHandleTable", LPVOID), - ("ProcessStarterHelper", LPVOID), - ("GdiDCAttributeList", LPVOID), - ("LoaderLock", LPVOID), - ("OSMajorVersion", ULONG), - ("OSMinorVersion", ULONG), - ("OSBuildNumber", ULONG), - ("OSPlatformId", ULONG), - ("ImageSubSystem", ULONG), - ("ImageSubSystemMajorVersion", ULONG), - ("ImageSubSystemMinorVersion", ULONG), - ("GdiHandleBuffer", ULONG * 34), - ("PostProcessInitRoutine", ULONG), - ("TlsExpansionBitmap", ULONG), - ("TlsExpansionBitmapBits", BYTE * 32), - ("SessionId", ULONG) - ] - -# ntrtl.h line 2320 -class CURDIR(Structure): - _fields_ = [ - ("DosPath", UNICODE_STRING), - ("Handle", HANDLE) - ] -# ntrtl.h line 2329 -class RTL_DRIVE_LETTER_CURDIR(Structure): +# winternl.h line 54 +class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ - ("Flags", USHORT), - ("Length", USHORT), - ("TimeStamp", ULONG), - ("DosPath", UNICODE_STRING) + ("Reserved", LPVOID * 12), + ("ImagePathName", UNICODE_STRING), + ("CommandLine", UNICODE_STRING) ] -# ntrtl.h line 2340 -class RTL_USER_PROCESS_PARAMETERS(Structure): +class PEB(Structure): _fields_ = [ - ("MaximumLength", ULONG), - ("Length", ULONG), - - ("Flags", ULONG), - ("DebugFlags", ULONG), - - ("ConsoleHandle", LPVOID), - ("ConsoleFlags", ULONG), - ("StandardInput", LPVOID), - ("StandardOutput", LPVOID), - ("StandardError", LPVOID), - - ("CurrentDirectory", CURDIR), - ("DllPath", UNICODE_STRING), - ("ImagePathName", UNICODE_STRING), - ("CommandLine", UNICODE_STRING), - ("Environment", LPVOID), - - ("StartingX", ULONG), - ("StartingY", ULONG), - ("CountX", ULONG), - ("CountY", ULONG), - ("CountCharsX", ULONG), - ("CountCharsY", ULONG), - ("FillAttribute", ULONG), - - ("WindowFlags", ULONG), - ("ShowWindowFlags", ULONG), - ("WindowTitle", UNICODE_STRING), - ("DesktopInfo", UNICODE_STRING), - ("ShellInfo", UNICODE_STRING), - ("RuntimeData", UNICODE_STRING), - ("CurrentDirectories", RTL_DRIVE_LETTER_CURDIR * 32), - - ("EnvironmentSize", ULONG), - ("EnvironmentVersion", ULONG), - ("PackageDependencyData", LPVOID), - ("ProcessGroupId", ULONG), - ("LoaderThreads", ULONG) + ("Reserved", BYTE * 28), + ("ProcessParameters", POINTER(RTL_USER_PROCESS_PARAMETERS)), ] class PROCESS_BASIC_INFORMATION(Structure): + NTSTATUS = LONG + KPRIORITY = LONG + KAFFINITY = PULONG _fields_ = [ - ("Reserved1", LPVOID), - ("PebBaseAddress", POINTER(PEB)), - ("Reserved2", LPVOID * 2), - ("UniqueProcessId", ULONG), - ("Reserved3", LPVOID) + ("ExitStatus", NTSTATUS), + ("PebBaseAddress", POINTER(PEB)), + ("AffinityMask", KAFFINITY), + ("BasePriority", KPRIORITY), + ("UniqueProcessId", PULONG), + ("InheritedFromUniqueProcessId", PULONG), ] -def to_int(time: FILETIME): - return (time.dwHighDateTime << 32) + time.dwLowDateTime +def to_int(filetime: FILETIME): + return (filetime.dwHighDateTime << 32) + filetime.dwLowDateTime From 86a49bc92a56e7f710f2c4b8790e8eae0d63e405 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 11 Jul 2024 19:46:50 +0800 Subject: [PATCH 101/161] Upd: fix. --- module/device/platform/api_windows.py | 275 ++++++++++++++++++++++---- 1 file changed, 240 insertions(+), 35 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 4f88dab147..bf8159dab8 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,4 +1,5 @@ -import re +import threading +import asyncio from ctypes import byref, sizeof, create_unicode_buffer, wstring_at, addressof @@ -8,6 +9,21 @@ def __yieldloop(entry32, snapshot, func: callable): + """ + Generates a loop that yields entries from a snapshot until the function fails or finishes. + + Args: + entry32 (PROCESSENTRY32 or THREADENTRY32): Entry structure to be yielded, either for processes or threads. + snapshot (int): Handle to the snapshot. + func (callable): Next entry (e.g., Process32Next or Thread32Next). + + Yields: + PROCESSENTRY32 or THREADENTRY32: The current entry in the snapshot. + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. + """ while 1: yield entry32 if func(snapshot, byref(entry32)): @@ -24,8 +40,11 @@ def _enum_processes(): Enumerates all the processes currently running on the system. Yields: - lppe32 (PROCESSENTRY32) | - None (if enum failed) + PROCESSENTRY32 or None: The current process entry or None if enumeration failed. + + Raises: + OSError if CreateToolhelp32Snapshot or any winapi failed. + IterationFinished if enumeration completed. """ lppe32 = PROCESSENTRY32() lppe32.dwSize = sizeof(PROCESSENTRY32) @@ -40,8 +59,11 @@ def _enum_threads(): Enumerates all the threads currintly running on the system. Yields: - lpte32 (THREADENTRY32) | - None (if enum failed) + THREADENTRY32 or None: The current thread entry or None if enumeration failed. + + Raises: + OSError if CreateToolhelp32Snapshot or any winapi failed. + IterationFinished if enumeration completed. """ lpte32 = THREADENTRY32() lpte32.dwSize = sizeof(THREADENTRY32) @@ -51,13 +73,63 @@ def _enum_threads(): yield from __yieldloop(lpte32, snapshot, Thread32Next) +def _enum_events(hevent): + event = EVT_HANDLE() + returned = DWORD(0) + while EvtNext(hevent, 1, byref(event), INFINITE, 0, byref(returned)): + if event == INVALID_HANDLE_VALUE: + report(f"Invalid handle: 0x{event}", raiseexcept=False) + continue + + buffer_size = DWORD(0) + buffer_used = DWORD(0) + property_count = DWORD(0) + rendered_content = None + + EvtRender( + None, + event, + EVT_RENDER_EVENT_XML, + buffer_size, + rendered_content, + byref(buffer_used), + byref(property_count) + ) + if GetLastError() == ERROR_SUCCESS: + yield rendered_content + continue + + buffer_size = buffer_used.value + rendered_content = create_unicode_buffer(buffer_size) + if not rendered_content: + report("malloc failed.", raiseexcept=False) + continue + + if not EvtRender( + None, + event, + EVT_RENDER_EVENT_XML, + buffer_size, + rendered_content, + byref(buffer_used), + byref(property_count) + ): + report(f"EvtRender failed with {GetLastError()}", raiseexcept=False) + continue + + if GetLastError() == ERROR_SUCCESS: + yield rendered_content.value + + EvtClose(event) + + def getfocusedwindow(): """ Get focused window. Returns: hwnd (int): Focused window hwnd - WINDOWPLACEMENT: + WINDOWPLACEMENT: The window placement or None if it couldn't be retrieved. """ hwnd = GetForegroundWindow() if not hwnd: @@ -90,22 +162,56 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def execute(command: str, sstart: bool = False): +def flash_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5): + from time import sleep + attempts = 0 + failed = 0 + + while attempts < max_attempts: + currentwindow = getfocusedwindow() + + if not (focusedwindow[0] and currentwindow[0]): + failed += 1 + if failed >= max_attempts: + report("Flash window failed.") + sleep(interval) + continue + + if focusedwindow[0] != currentwindow[0]: + logger.info(f"Current window is {currentwindow[0]}, flash back to {focusedwindow[0]}") + setforegroundwindow(focusedwindow) + attempts += 1 + continue + + attempts += 1 + sleep(interval) + + +def execute(command: str, silentstart: bool, start: bool): """ Create a new process. Args: command (str): process's commandline - sstart (bool): process's windowplacement + silentstart (bool): process's windowplacement + start (bool): True if start emulator, False if not Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' Returns: process: tuple(processhandle, threadhandle, processid, mainthreadid), focusedwindow: tuple(hwnd, WINDOWPLACEMENT) + + Raises: + EmulatorLaunchFailedError if CreateProcessW failed. """ from shlex import split from os.path import dirname + focusedwindow = getfocusedwindow() + if start: + focus_thread = threading.Thread(target=flash_window, args=(focusedwindow, )) + focus_thread.start() + lpApplicationName = split(command)[0] lpCommandLine = command lpProcessAttributes = None @@ -113,7 +219,7 @@ def execute(command: str, sstart: bool = False): bInheritHandles = False dwCreationFlags = ( CREATE_NO_WINDOW | - NORMAL_PRIORITY_CLASS | + IDLE_PRIORITY_CLASS | CREATE_NEW_PROCESS_GROUP | CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT @@ -123,11 +229,12 @@ def execute(command: str, sstart: bool = False): lpStartupInfo = STARTUPINFOW() lpStartupInfo.cb = sizeof(STARTUPINFOW) lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW - lpStartupInfo.wShowWindow = SW_HIDE if sstart else SW_MINIMIZE + if start: + lpStartupInfo.wShowWindow = SW_HIDE if silentstart else SW_MINIMIZE + else: + lpStartupInfo.wShowWindow = SW_HIDE lpProcessInformation = PROCESS_INFORMATION() - focusedwindow = getfocusedwindow() - success = CreateProcessW( lpApplicationName, lpCommandLine, @@ -143,7 +250,7 @@ def execute(command: str, sstart: bool = False): if not success: report("Failed to start emulator.", exception=EmulatorLaunchFailedError) - + process = ( lpProcessInformation.hProcess, lpProcessInformation.hThread, @@ -159,6 +266,9 @@ def terminate_process(pid: int): Args: pid (int): Emulator's pid + + Raises: + OSError if OpenProcess failed. """ with open_process(PROCESS_TERMINATE, pid) as hProcess: if TerminateProcess(hProcess, 0) == 0: @@ -175,6 +285,9 @@ def get_hwnds(pid: int) -> list: Returns: hwnds (list): Emulator's possible window hwnds + + Raises: + HwndNotFoundError if EnumWindows failed. """ hwnds = [] @@ -203,7 +316,7 @@ def get_cmdline(pid: int) -> str: pid (int): Emulator's pid Returns: - command line (str): process's command line + cmdline (str): process's command line Example: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' """ @@ -234,18 +347,22 @@ def get_cmdline(pid: int) -> str: cmdline = wstring_at(addressof(commandLine), len(commandLine)) except OSError: return '' - return cmdline.replace(r"\\", "/").replace("\\", "/").replace('"', '"') + return fstr(cmdline) def kill_process_by_regex(regex: str) -> int: """ - Kill processes with cmdline match the given regex. + Kill processes with cmdline match the given regex. - Args: - regex: + Args: + regex: - Returns: - int: Number of processes killed + Returns: + int: Number of processes killed + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. """ count = 0 @@ -264,7 +381,7 @@ def kill_process_by_regex(regex: str) -> int: return count -def _get_thread_creation_time(tid): +def _get_thread_creation_time(tid: int): """ Get thread's creation time. @@ -273,6 +390,9 @@ def _get_thread_creation_time(tid): Returns: threadstarttime (int): Thread's start time + + Raises: + OSError if OpenThread failed. """ with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: creationtime = FILETIME() @@ -296,8 +416,12 @@ def get_thread(pid: int): Args: pid (int): Emulator's pid - Returns + Returns: mainthreadid (int): Emulator's main thread id + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. """ mainthreadid = 0 minstarttime = MAXULONGLONG @@ -327,7 +451,7 @@ def _get_process(pid: int): Returns: tuple(processhandle, threadhandle, processid, mainthreadid) | - tuple(None, None, processid, mainthreadid) | (if enum_process() failed) + tuple(None, None, processid, mainthreadid) """ tid = get_thread(pid) try: @@ -356,6 +480,10 @@ def get_process(instance: EmulatorInstance): tuple(processhandle, threadhandle, processid, mainthreadid) | tuple(None, None, processid, mainthreadid) | (if enum_process() failed) None (if enum_process() failed) + + Raises: + OSError if any winapi failed. + IterationFinished if enumeration completed. """ processes = _enum_processes() for lppe32 in processes: @@ -364,20 +492,29 @@ def get_process(instance: EmulatorInstance): if not instance.path in cmdline: continue if instance == Emulator.MuMuPlayer12: - match = re.search(r'\d+$', cmdline) - if match and int(match.group()) == instance.MuMuPlayer12_id: - processes.close() - return _get_process(pid) + match = re.search(r'-v\s*(\d+)', cmdline) + if not match: + continue + if int(match.group(1)) != instance.MuMuPlayer12_id: + continue + processes.close() + return _get_process(pid) elif instance == Emulator.LDPlayerFamily: - match = re.search(r'\d+$', cmdline) - if match and int(match.group()) == instance.LDPlayer_id: - processes.close() - return _get_process(pid) + match = re.search(r'index=\s*(\d+)', cmdline) + if not match: + continue + if int(match.group(1)) != instance.LDPlayer_id: + continue + processes.close() + return _get_process(pid) else: - matchstr = re.search(fr'\b{instance.name}$', cmdline) - if matchstr and matchstr.group() == instance.name: - processes.close() - return _get_process(pid) + matchname = re.search(fr'{instance.name}(\s+|$)', cmdline) + if not matchname: + continue + if matchname.group(0).strip() != instance.name: + continue + processes.close() + return _get_process(pid) def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): @@ -402,3 +539,71 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): continue ShowWindow(hwnd, arg) return True + +class ProcessManager: + # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + def __init__(self, pid: int): + self.mainpid = pid + self.datas = [] + self.pids = [] + self.evttree = EventTree() + self.lock = threading.Lock() + self.loop = asyncio.get_event_loop() + self.loop.create_task(self.listener()) + + async def listener(self): + # TODO listening grab/kill event + while True: + event = await self.get_event() + if event == "grab": + await self.grab_pids() + elif event == "kill": + await self.kill_pids() + await asyncio.sleep(1) + + async def get_event(self): + # TODO get event + await asyncio.sleep(1) + return "grab" + + async def grab_pids(self, pid: int): + if not IsUserAnAdmin(): + return + with evt_query() as hevent: + events = _enum_events(hevent) + for content in events: + data = self.evttree.parse_event(content) + with self.lock: + self.datas.append(data) + if data.process_id == pid: + break + self.datas = self.datas[::-1] + await self.filltree() + + async def filltree(self): + self.evttree.root = Node(self.datas[0]) + for data in self.datas[1::]: + evtiter = self.evttree.pre_traversal(self.evttree.root) + for node in evtiter: + if data != node.data: + continue + cmdline = get_cmdline(data.process_id) + if data.process_name not in cmdline: + continue + node.add_children(data) + + async def kill_pids(self): + # TODO kill process by enumerating tree + with self.lock: + evtiter = self.evttree.post_traversal(self.evttree.root) + for node in evtiter: + terminate_process(node.data.process_id) + while self.pids: + pass + + def start(self): + threading.Thread(target=self.loop.run_forever).start() + +if __name__ == '__main__': + p = 1234 + PM = ProcessManager(1234) From 2249c4e9e6898c196ef1b67d8af6ae77fa7bf55a Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Fri, 12 Jul 2024 17:02:58 +0800 Subject: [PATCH 102/161] Upd: fix texts. --- module/device/platform/api_windows.py | 26 ++++++++++++------- module/device/platform/platform_windows.py | 4 +-- .../platform/winapi/functions_windows.py | 6 ++--- .../platform/winapi/structures_windows.py | 2 -- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index bf8159dab8..cdfba02261 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -122,7 +122,6 @@ def _enum_events(hevent): EvtClose(event) - def getfocusedwindow(): """ Get focused window. @@ -162,7 +161,7 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def flash_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5): +def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5): from time import sleep attempts = 0 failed = 0 @@ -181,10 +180,19 @@ def flash_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = logger.info(f"Current window is {currentwindow[0]}, flash back to {focusedwindow[0]}") setforegroundwindow(focusedwindow) attempts += 1 + sleep(interval) + + newwindow = getfocusedwindow() + + if not newwindow[0]: + failed += 1 + if failed >= max_attempts: + report("Flash window failed.") + sleep(interval) continue - attempts += 1 - sleep(interval) + if newwindow[0] != currentwindow[0] and newwindow[0] != focusedwindow[0]: + break def execute(command: str, silentstart: bool, start: bool): @@ -209,7 +217,7 @@ def execute(command: str, silentstart: bool, start: bool): from os.path import dirname focusedwindow = getfocusedwindow() if start: - focus_thread = threading.Thread(target=flash_window, args=(focusedwindow, )) + focus_thread = threading.Thread(target=refresh_window, args=(focusedwindow,)) focus_thread.start() lpApplicationName = split(command)[0] @@ -541,7 +549,7 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): return True class ProcessManager: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, pid: int): self.mainpid = pid self.datas = [] @@ -552,7 +560,7 @@ def __init__(self, pid: int): self.loop.create_task(self.listener()) async def listener(self): - # TODO listening grab/kill event + # TODO:listening grab/kill event while True: event = await self.get_event() if event == "grab": @@ -562,7 +570,7 @@ async def listener(self): await asyncio.sleep(1) async def get_event(self): - # TODO get event + # TODO:get event await asyncio.sleep(1) return "grab" @@ -593,7 +601,7 @@ async def filltree(self): node.add_children(data) async def kill_pids(self): - # TODO kill process by enumerating tree + # TODO:kill process by enumerating tree with self.lock: evtiter = self.evttree.post_traversal(self.evttree.root) for node in evtiter: diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 07ff0545c5..337c57d8da 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -11,13 +11,11 @@ class EmulatorUnknown(Exception): pass -class EmulatorStatus: +class PlatformWindows(PlatformBase, EmulatorManager): process: tuple = () hwnds: list = [] focusedwindow: tuple = () - -class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): def __execute(self, command: str, start: bool): command = api_windows.fstr(command) logger.info(f'Execute: {command}') diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index a186f6563c..aacb399b38 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -226,7 +226,7 @@ def _is_invalid_handle(self): return self._handle is None class Data: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, data: dict, time: datetime): self.system_time: datetime = time self.new_process_id: int = data.get("NewProcessId", 0) @@ -240,7 +240,7 @@ def __eq__(self, other: 'Data'): return NotImplemented class Node: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, data: Data = None): self.data = data self.children = [] @@ -255,7 +255,7 @@ def add_children(self, data): self.children.append(Node(data)) class EventTree: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! + # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! root: Node = None @staticmethod diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index 35346abf24..25c6feb69c 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -85,7 +85,6 @@ class WINDOWPLACEMENT(Structure): ("rcNormalPosition", RECT) ] - # winternl.h line 25 class UNICODE_STRING(Structure): _fields_ = [ @@ -94,7 +93,6 @@ class UNICODE_STRING(Structure): ("Buffer", PWCHAR) ] - # winternl.h line 54 class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ From 3e2c9c0a53bf8e62db8927ba9905fea73bd6e4c4 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 15 Jul 2024 23:45:43 +0800 Subject: [PATCH 103/161] Fix: [ALAS] Device.config was never updated during scheduler run --- alas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/alas.py b/alas.py index 650f41f22d..a525e272b5 100644 --- a/alas.py +++ b/alas.py @@ -521,6 +521,7 @@ def loop(self): task = self.get_next_task() # Init device and change server _ = self.device + self.device.config = self.config # Skip first restart if self.is_first_task and task == 'Restart': logger.info('Skip task `Restart` at scheduler start') From 3a70e0f207b59958aef8f276d3d1beb45cac47d1 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 13:45:21 +0800 Subject: [PATCH 104/161] Upd: fix --- module/device/platform/api_windows.py | 90 +++++++++++++------ .../device/platform/winapi/const_windows.py | 4 +- .../platform/winapi/functions_windows.py | 75 ++++++++++++---- 3 files changed, 124 insertions(+), 45 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index cdfba02261..b9d9b1a166 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -194,6 +194,9 @@ def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float if newwindow[0] != currentwindow[0] and newwindow[0] != focusedwindow[0]: break + attempts += 1 + sleep(interval) + def execute(command: str, silentstart: bool, start: bool): """ @@ -389,6 +392,37 @@ def kill_process_by_regex(regex: str) -> int: return count +def __get_creation_time(fopen, fgettime, access, identification): + with fopen(access, identification, uselog=False, raiseexcept=False) as handle: + creationtime = FILETIME() + exittime = FILETIME() + kerneltime = FILETIME() + usertime = FILETIME() + if not fgettime( + handle, + byref(creationtime), + byref(exittime), + byref(kerneltime), + byref(usertime) + ): + return None + return to_int(creationtime) + +def _get_process_creation_time(pid: int): + """ + Get thread's creation time. + + Args: + pid (int): Process id + + Returns: + threadstarttime (int): Thread's start time + + Raises: + OSError if OpenThread failed. + """ + return __get_creation_time(open_process, GetProcessTimes, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, pid) + def _get_thread_creation_time(tid: int): """ Get thread's creation time. @@ -402,20 +436,8 @@ def _get_thread_creation_time(tid: int): Raises: OSError if OpenThread failed. """ - with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: - creationtime = FILETIME() - exittime = FILETIME() - kerneltime = FILETIME() - usertime = FILETIME() - if not GetThreadTimes( - hThread, - byref(creationtime), - byref(exittime), - byref(kerneltime), - byref(usertime) - ): - return None - return to_int(creationtime) + return __get_creation_time(open_thread, GetThreadTimes, THREAD_QUERY_INFORMATION, tid) + def get_thread(pid: int): """ @@ -553,7 +575,6 @@ class ProcessManager: def __init__(self, pid: int): self.mainpid = pid self.datas = [] - self.pids = [] self.evttree = EventTree() self.lock = threading.Lock() self.loop = asyncio.get_event_loop() @@ -574,7 +595,7 @@ async def get_event(self): await asyncio.sleep(1) return "grab" - async def grab_pids(self, pid: int): + async def grab_pids(self): if not IsUserAnAdmin(): return with evt_query() as hevent: @@ -583,35 +604,48 @@ async def grab_pids(self, pid: int): data = self.evttree.parse_event(content) with self.lock: self.datas.append(data) - if data.process_id == pid: + if data.new_process_id == self.mainpid: break self.datas = self.datas[::-1] - await self.filltree() + await self.build_tree() - async def filltree(self): - self.evttree.root = Node(self.datas[0]) - for data in self.datas[1::]: - evtiter = self.evttree.pre_traversal(self.evttree.root) + async def build_tree(self): + count = 0 + for data in self.datas: + if data.process_id == self.mainpid: + break + count += 1 + self.evttree.root = Node(self.datas[count]) + for data in self.datas[count+1::]: + evtiter = self.evttree.pre_order_traversal(self.evttree.root) for node in evtiter: if data != node.data: continue cmdline = get_cmdline(data.process_id) - if data.process_name not in cmdline: + if node.data.new_process_name not in cmdline: continue node.add_children(data) + self.logtree() + + def logtree(self): + evtiter = self.evttree.level_order_traversal(self.evttree.root) + for node in evtiter: + if node is None: + break + logger.info(node.data) async def kill_pids(self): # TODO:kill process by enumerating tree with self.lock: - evtiter = self.evttree.post_traversal(self.evttree.root) + evtiter = self.evttree.post_order_traversal(self.evttree.root) for node in evtiter: terminate_process(node.data.process_id) - while self.pids: - pass + del self.datas, self.evttree + self.datas, self.evttree = [], EventTree() def start(self): threading.Thread(target=self.loop.run_forever).start() if __name__ == '__main__': - p = 1234 - PM = ProcessManager(1234) + PM = ProcessManager(27232) + PM.grab_pids() diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 2139d1ffb8..fd81c1e83c 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -166,8 +166,8 @@ EVT_QUERY_TOLERATE_QUERY_ERRORS = 0x1000 # line 176 EVT_RENDER_EVENT_VALUES = 0 -EVT_RENDER_EVENT_XML = 1 -EVT_RENDER_BOOK_MARK = 2 +EVT_RENDER_EVENT_XML = 1 +EVT_RENDER_BOOK_MARK = 2 # line 242 EVT_VAR_TYPE_NULL = 0 EVT_VAR_TYPE_STRING = 1 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index aacb399b38..f30b92c3b5 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod from datetime import datetime -import xml.etree.ElementTree as Et import re +from queue import Queue from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( @@ -112,6 +112,16 @@ Thread32Next.argtypes = [HANDLE, POINTER(THREADENTRY32)] Thread32Next.restype = BOOL +GetProcessTimes = kernel32.GetProcessTimes +GetProcessTimes.argtypes = [ + HANDLE, + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME) +] +GetProcessTimes.restype = BOOL + GetThreadTimes = kernel32.GetThreadTimes GetThreadTimes.argtypes = [ HANDLE, @@ -122,6 +132,10 @@ ] GetThreadTimes.restype = BOOL +GetExitCodeProcess = kernel32.GetExitCodeProcess +GetExitCodeProcess.argtypes = [HANDLE, POINTER(DWORD)] +GetExitCodeProcess.restype = BOOL + GetLastError = kernel32.GetLastError GetLastError.argtypes = [] GetLastError.restype = DWORD @@ -164,7 +178,9 @@ class Handle(metaclass=ABCMeta): def __init__(self, *args, **kwargs): self._handle = self._func(*self.__getinitargs__(*args, **kwargs)) if not self: - report(f"{self._func.__name__} failed.", uselog=kwargs.get('uselog', True)) + report(f"{self._func.__name__} failed.", + uselog=kwargs.get('uselog', True), + raiseexcept=kwargs.get("raiseexcept", True)) def __enter__(self): return self._handle @@ -186,7 +202,7 @@ class ProcessHandle(Handle): _func = OpenProcess _exitfunc = CloseHandle - def __getinitargs__(self, access, pid, uselog): + def __getinitargs__(self, access, pid, uselog, raiseexcept): return access, False, pid def _is_invalid_handle(self): @@ -196,7 +212,7 @@ class ThreadHandle(Handle): _func = OpenThread _exitfunc = CloseHandle - def __getinitargs__(self, access, pid, uselog): + def __getinitargs__(self, access, pid, uselog, raiseexcept): return access, False, pid def _is_invalid_handle(self): @@ -234,11 +250,22 @@ def __init__(self, data: dict, time: datetime): self.process_id: int = data.get("ProcessId", 0) self.process_name: str = data.get("ParentProcessName", '') - def __eq__(self, other: 'Data'): + def __eq__(self, other): if isinstance(other, Data): return self.process_id == other.new_process_id return NotImplemented + def __str__(self): + return ( + f"Data(system time={self.system_time}, " + f"new process ID={self.new_process_id}, " + f"new process name={self.new_process_name}, " + f"process ID={self.process_id}, " + f"process name={self.process_name})" + ) + + __repr__ = __str__ + class Node: # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, data: Data = None): @@ -251,15 +278,21 @@ def __del__(self): if self.children: del self.children + def __str__(self) -> str: + return f"Node(data={self.data})" + + __repr__ = __str__ + def add_children(self, data): self.children.append(Node(data)) class EventTree: # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! - root: Node = None + root = None @staticmethod def parse_event(event: str): + import xml.etree.ElementTree as Et ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'} root = Et.fromstring(event) system_time_str = root.find('.//ns:TimeCreated', ns).attrib['SystemTime'] @@ -272,19 +305,31 @@ def parse_event(event: str): return Data(data, system_time) - def pre_traversal(self, node: Node = None): + def pre_order_traversal(self, node: Node): if node is not None: yield node for child in node.children: - yield from self.pre_traversal(child) + yield from self.pre_order_traversal(child) - def post_traversal(self, node: Node = None): + def post_order_traversal(self, node: Node): if node is not None: for child in node.children: - yield from self.post_traversal(child) + yield from self.post_order_traversal(child) yield node - def delete_tree(self): + @staticmethod + def level_order_traversal(node: Node): + q = Queue() + q.put(node) + while not q.empty(): + out = q.get() + yield out + if not out.children: + continue + for child in out.children: + q.put(child) + + def release_tree(self): del self.root self.root = None @@ -329,11 +374,11 @@ def fstr(formatstr: str): except ValueError: return formatstr.replace(r"\\", "/").replace("\\", "/").replace('"', '"') -def open_process(access, pid, uselog=False): - return ProcessHandle(access, pid, uselog=uselog) +def open_process(access, pid, uselog=False, raiseexcept=True): + return ProcessHandle(access, pid, uselog=uselog, raiseexcept=raiseexcept) -def open_thread(access, tid, uselog=False): - return ThreadHandle(access, tid, uselog=uselog) +def open_thread(access, tid, uselog=False, raiseexcept=True): + return ThreadHandle(access, tid, uselog=uselog, raiseexcept=raiseexcept) def create_snapshot(arg): return CreateSnapshot(arg) From ad041155294f50d0c72f71c8d27503acaa9c309c Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 13:53:48 +0800 Subject: [PATCH 105/161] Upd: fix --- module/device/platform/api_windows.py | 185 +++++------------- .../device/platform/winapi/const_windows.py | 36 ---- .../platform/winapi/functions_windows.py | 128 +++--------- .../platform/winapi/structures_windows.py | 2 - 4 files changed, 73 insertions(+), 278 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index bf8159dab8..eae948098e 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,5 +1,5 @@ import threading -import asyncio +import re from ctypes import byref, sizeof, create_unicode_buffer, wstring_at, addressof @@ -73,56 +73,6 @@ def _enum_threads(): yield from __yieldloop(lpte32, snapshot, Thread32Next) -def _enum_events(hevent): - event = EVT_HANDLE() - returned = DWORD(0) - while EvtNext(hevent, 1, byref(event), INFINITE, 0, byref(returned)): - if event == INVALID_HANDLE_VALUE: - report(f"Invalid handle: 0x{event}", raiseexcept=False) - continue - - buffer_size = DWORD(0) - buffer_used = DWORD(0) - property_count = DWORD(0) - rendered_content = None - - EvtRender( - None, - event, - EVT_RENDER_EVENT_XML, - buffer_size, - rendered_content, - byref(buffer_used), - byref(property_count) - ) - if GetLastError() == ERROR_SUCCESS: - yield rendered_content - continue - - buffer_size = buffer_used.value - rendered_content = create_unicode_buffer(buffer_size) - if not rendered_content: - report("malloc failed.", raiseexcept=False) - continue - - if not EvtRender( - None, - event, - EVT_RENDER_EVENT_XML, - buffer_size, - rendered_content, - byref(buffer_used), - byref(property_count) - ): - report(f"EvtRender failed with {GetLastError()}", raiseexcept=False) - continue - - if GetLastError() == ERROR_SUCCESS: - yield rendered_content.value - - EvtClose(event) - - def getfocusedwindow(): """ Get focused window. @@ -162,7 +112,8 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def flash_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5): +def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5): + # TODO:Something error to fix. from time import sleep attempts = 0 failed = 0 @@ -181,13 +132,26 @@ def flash_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = logger.info(f"Current window is {currentwindow[0]}, flash back to {focusedwindow[0]}") setforegroundwindow(focusedwindow) attempts += 1 + sleep(interval) + + newwindow = getfocusedwindow() + + if not newwindow[0]: + failed += 1 + if failed >= max_attempts: + report("Flash window failed.") + sleep(interval) continue + if newwindow[0] != currentwindow[0] and newwindow[0] != focusedwindow[0]: + break + attempts += 1 sleep(interval) def execute(command: str, silentstart: bool, start: bool): + # TODO:Create Process with non-administrator privileges """ Create a new process. @@ -209,7 +173,7 @@ def execute(command: str, silentstart: bool, start: bool): from os.path import dirname focusedwindow = getfocusedwindow() if start: - focus_thread = threading.Thread(target=flash_window, args=(focusedwindow, )) + focus_thread = threading.Thread(target=refresh_window, args=(focusedwindow,)) focus_thread.start() lpApplicationName = split(command)[0] @@ -381,6 +345,37 @@ def kill_process_by_regex(regex: str) -> int: return count +def __get_creation_time(fopen, fgettime, access, identification): + with fopen(access, identification, uselog=False, raiseexcept=False) as handle: + creationtime = FILETIME() + exittime = FILETIME() + kerneltime = FILETIME() + usertime = FILETIME() + if not fgettime( + handle, + byref(creationtime), + byref(exittime), + byref(kerneltime), + byref(usertime) + ): + return None + return to_int(creationtime) + +def _get_process_creation_time(pid: int): + """ + Get thread's creation time. + + Args: + pid (int): Process id + + Returns: + threadstarttime (int): Thread's start time + + Raises: + OSError if OpenThread failed. + """ + return __get_creation_time(open_process, GetProcessTimes, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, pid) + def _get_thread_creation_time(tid: int): """ Get thread's creation time. @@ -394,20 +389,8 @@ def _get_thread_creation_time(tid: int): Raises: OSError if OpenThread failed. """ - with open_thread(THREAD_QUERY_INFORMATION, tid) as hThread: - creationtime = FILETIME() - exittime = FILETIME() - kerneltime = FILETIME() - usertime = FILETIME() - if not GetThreadTimes( - hThread, - byref(creationtime), - byref(exittime), - byref(kerneltime), - byref(usertime) - ): - return None - return to_int(creationtime) + return __get_creation_time(open_thread, GetThreadTimes, THREAD_QUERY_INFORMATION, tid) + def get_thread(pid: int): """ @@ -539,71 +522,3 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): continue ShowWindow(hwnd, arg) return True - -class ProcessManager: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! - def __init__(self, pid: int): - self.mainpid = pid - self.datas = [] - self.pids = [] - self.evttree = EventTree() - self.lock = threading.Lock() - self.loop = asyncio.get_event_loop() - self.loop.create_task(self.listener()) - - async def listener(self): - # TODO listening grab/kill event - while True: - event = await self.get_event() - if event == "grab": - await self.grab_pids() - elif event == "kill": - await self.kill_pids() - await asyncio.sleep(1) - - async def get_event(self): - # TODO get event - await asyncio.sleep(1) - return "grab" - - async def grab_pids(self, pid: int): - if not IsUserAnAdmin(): - return - with evt_query() as hevent: - events = _enum_events(hevent) - for content in events: - data = self.evttree.parse_event(content) - with self.lock: - self.datas.append(data) - if data.process_id == pid: - break - self.datas = self.datas[::-1] - await self.filltree() - - async def filltree(self): - self.evttree.root = Node(self.datas[0]) - for data in self.datas[1::]: - evtiter = self.evttree.pre_traversal(self.evttree.root) - for node in evtiter: - if data != node.data: - continue - cmdline = get_cmdline(data.process_id) - if data.process_name not in cmdline: - continue - node.add_children(data) - - async def kill_pids(self): - # TODO kill process by enumerating tree - with self.lock: - evtiter = self.evttree.post_traversal(self.evttree.root) - for node in evtiter: - terminate_process(node.data.process_id) - while self.pids: - pass - - def start(self): - threading.Thread(target=self.loop.run_forever).start() - -if __name__ == '__main__': - p = 1234 - PM = ProcessManager(1234) diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 2139d1ffb8..ebfc750c0b 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -158,41 +158,5 @@ # winbase.h line 822 INFINITE = 0xFFFFFFFF -# winevt.h line 156 -EVT_QUERY_CHANNEL_PATH = 0x1 -EVT_QUERY_FILE_PATH = 0x2 -EVT_QUERY_FORWARD_DIRECTION = 0x100 -EVT_QUERY_REVERSE_DIRECTION = 0x200 -EVT_QUERY_TOLERATE_QUERY_ERRORS = 0x1000 -# line 176 -EVT_RENDER_EVENT_VALUES = 0 -EVT_RENDER_EVENT_XML = 1 -EVT_RENDER_BOOK_MARK = 2 -# line 242 -EVT_VAR_TYPE_NULL = 0 -EVT_VAR_TYPE_STRING = 1 -EVT_VAR_TYPE_ANSISTRING = 2 -EVT_VAR_TYPE_SBYTE = 3 -EVT_VAR_TYPE_BYTE = 4 -EVT_VAR_TYPE_INT16 = 5 -EVT_VAR_TYPE_UINT16 = 6 -EVT_VAR_TYPE_INT32 = 7 -EVT_VAR_TYPE_UINT32 = 8 -EVT_VAR_TYPE_INT64 = 9 -EVT_VAR_TYPE_UINT64 = 10 -EVT_VAR_TYPE_SINGLE = 11 -EVT_VAR_TYPE_DOUBLE = 12 -EVT_VAR_TYPE_BOOLEAN = 13 -EVT_VAR_TYPE_BINARY = 14 -EVT_VAR_TYPE_GUID = 15 -EVT_VAR_TYPE_SIZET = 16 -EVT_VAR_TYPE_FILETIME = 17 -EVT_VAR_TYPE_SYSTIME = 18 -EVT_VAR_TYPE_SID = 19 -EVT_VAR_TYPE_HEXINT32 = 20 -EVT_VAR_TYPE_HEXINT64 = 21 -EVT_VAR_TYPE_EVTHANDLE = 32 -EVT_VAR_TYPE_EVTXML = 35 - MAXULONGLONG = LPVOID(-1).value INVALID_HANDLE_VALUE = -1 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index a186f6563c..1b7dd2cfac 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,7 +1,4 @@ from abc import ABCMeta, abstractmethod -from datetime import datetime -import xml.etree.ElementTree as Et -import re from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( @@ -112,6 +109,16 @@ Thread32Next.argtypes = [HANDLE, POINTER(THREADENTRY32)] Thread32Next.restype = BOOL +GetProcessTimes = kernel32.GetProcessTimes +GetProcessTimes.argtypes = [ + HANDLE, + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME), + POINTER(FILETIME) +] +GetProcessTimes.restype = BOOL + GetThreadTimes = kernel32.GetThreadTimes GetThreadTimes.argtypes = [ HANDLE, @@ -122,6 +129,10 @@ ] GetThreadTimes.restype = BOOL +GetExitCodeProcess = kernel32.GetExitCodeProcess +GetExitCodeProcess.argtypes = [HANDLE, POINTER(DWORD)] +GetExitCodeProcess.restype = BOOL + GetLastError = kernel32.GetLastError GetLastError.argtypes = [] GetLastError.restype = DWORD @@ -135,23 +146,6 @@ NtQueryInformationProcess.argtypes = [HANDLE, INT, LPVOID, ULONG, PULONG] NtQueryInformationProcess.restype = NTSTATUS -EVT_HANDLE = HANDLE -EvtQuery = wevtapi.EvtQuery -EvtQuery.argtypes = [EVT_HANDLE, LPCWSTR, LPCWSTR, DWORD] -EvtQuery.restype = HANDLE - -EvtNext = wevtapi.EvtNext -EvtNext.argtypes = [EVT_HANDLE, DWORD, POINTER(EVT_HANDLE), DWORD, DWORD, POINTER(DWORD)] -EvtNext.restype = BOOL - -EvtRender = wevtapi.EvtRender -EvtRender.argtypes = [EVT_HANDLE, EVT_HANDLE, DWORD, DWORD, LPVOID, POINTER(DWORD), POINTER(DWORD)] -EvtRender.restype = BOOL - -EvtClose = wevtapi.EvtClose -EvtClose.argtypes = [EVT_HANDLE] -EvtClose.restype = BOOL - class Handle(metaclass=ABCMeta): """ Abstract base Handle class. @@ -164,7 +158,9 @@ class Handle(metaclass=ABCMeta): def __init__(self, *args, **kwargs): self._handle = self._func(*self.__getinitargs__(*args, **kwargs)) if not self: - report(f"{self._func.__name__} failed.", uselog=kwargs.get('uselog', True)) + report(f"{self._func.__name__} failed.", + uselog=kwargs.get('uselog', True), + raiseexcept=kwargs.get("raiseexcept", True)) def __enter__(self): return self._handle @@ -186,7 +182,7 @@ class ProcessHandle(Handle): _func = OpenProcess _exitfunc = CloseHandle - def __getinitargs__(self, access, pid, uselog): + def __getinitargs__(self, access, pid, uselog, raiseexcept): return access, False, pid def _is_invalid_handle(self): @@ -196,7 +192,7 @@ class ThreadHandle(Handle): _func = OpenThread _exitfunc = CloseHandle - def __getinitargs__(self, access, pid, uselog): + def __getinitargs__(self, access, pid, uselog, raiseexcept): return access, False, pid def _is_invalid_handle(self): @@ -213,81 +209,6 @@ def _is_invalid_handle(self): from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE return self._handle == INVALID_HANDLE_VALUE -class QueryEvt(Handle): - _func = EvtQuery - _exitfunc = EvtClose - - def __getinitargs__(self): - query = "Event/System[EventID=4688]" - from module.device.platform.winapi.const_windows import EVT_QUERY_REVERSE_DIRECTION, EVT_QUERY_CHANNEL_PATH - return None, "Security", query, EVT_QUERY_REVERSE_DIRECTION | EVT_QUERY_CHANNEL_PATH - - def _is_invalid_handle(self): - return self._handle is None - -class Data: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! - def __init__(self, data: dict, time: datetime): - self.system_time: datetime = time - self.new_process_id: int = data.get("NewProcessId", 0) - self.new_process_name: str = data.get("NewProcessName", '') - self.process_id: int = data.get("ProcessId", 0) - self.process_name: str = data.get("ParentProcessName", '') - - def __eq__(self, other: 'Data'): - if isinstance(other, Data): - return self.process_id == other.new_process_id - return NotImplemented - -class Node: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! - def __init__(self, data: Data = None): - self.data = data - self.children = [] - - def __del__(self): - if self.data is not None: - del self.data - if self.children: - del self.children - - def add_children(self, data): - self.children.append(Node(data)) - -class EventTree: - # TODO UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! - root: Node = None - - @staticmethod - def parse_event(event: str): - ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'} - root = Et.fromstring(event) - system_time_str = root.find('.//ns:TimeCreated', ns).attrib['SystemTime'] - match = re.match(r'(.*\.\d{6})\d?(Z)', system_time_str) - modifiedtime = match.group(1) + match.group(2) if match else system_time_str - system_time = datetime.strptime(modifiedtime, '%Y-%m-%dT%H:%M:%S.%f%z').astimezone() - - fields = ["NewProcessId", "NewProcessName", "ProcessId", "ParentProcessName"] - data = {field: fstr(root.find(f'.//ns:Data[@Name="{field}"]', ns).text) for field in fields} - - return Data(data, system_time) - - def pre_traversal(self, node: Node = None): - if node is not None: - yield node - for child in node.children: - yield from self.pre_traversal(child) - - def post_traversal(self, node: Node = None): - if node is not None: - for child in node.children: - yield from self.post_traversal(child) - yield node - - def delete_tree(self): - del self.root - self.root = None - def report( msg: str = '', statuscode: int = -1, @@ -329,14 +250,11 @@ def fstr(formatstr: str): except ValueError: return formatstr.replace(r"\\", "/").replace("\\", "/").replace('"', '"') -def open_process(access, pid, uselog=False): - return ProcessHandle(access, pid, uselog=uselog) +def open_process(access, pid, uselog=False, raiseexcept=True): + return ProcessHandle(access, pid, uselog=uselog, raiseexcept=raiseexcept) -def open_thread(access, tid, uselog=False): - return ThreadHandle(access, tid, uselog=uselog) +def open_thread(access, tid, uselog=False, raiseexcept=True): + return ThreadHandle(access, tid, uselog=uselog, raiseexcept=raiseexcept) def create_snapshot(arg): return CreateSnapshot(arg) - -def evt_query(): - return QueryEvt() diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index 35346abf24..25c6feb69c 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -85,7 +85,6 @@ class WINDOWPLACEMENT(Structure): ("rcNormalPosition", RECT) ] - # winternl.h line 25 class UNICODE_STRING(Structure): _fields_ = [ @@ -94,7 +93,6 @@ class UNICODE_STRING(Structure): ("Buffer", PWCHAR) ] - # winternl.h line 54 class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ From ee7649235411bf402fc4a3992e1bfae73386334e Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 14:01:37 +0800 Subject: [PATCH 106/161] Upd: fix texts --- module/device/platform/api_windows.py | 1 + module/device/platform/winapi/const_windows.py | 1 - .../device/platform/winapi/functions_windows.py | 17 +++++------------ 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index eae948098e..ec5da25c2f 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -414,6 +414,7 @@ def get_thread(pid: int): if lpte32.th32OwnerProcessID != pid: continue + # In general, the first tid obtained by traversing is always the main tid, so these code can be commented. threadstarttime = _get_thread_creation_time(lpte32.th32ThreadID) if threadstarttime is None or threadstarttime >= minstarttime: continue diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index ebfc750c0b..64a9490660 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -153,7 +153,6 @@ # winerror.h ERROR_SUCCESS = 0 # line 227 -ERROR_INSUFFICIENT_BUFFER = 122 # line 1041 # winbase.h line 822 INFINITE = 0xFFFFFFFF diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 1b7dd2cfac..915168846c 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -17,11 +17,6 @@ kernel32 = WinDLL(name='kernel32', use_last_error=True) ntdll = WinDLL(name='ntdll', use_last_error=True) wevtapi = WinDLL(name='wevtapi', use_last_error=True) -shell32 = WinDLL(name='shell32', use_last_error=True) - -IsUserAnAdmin = shell32.IsUserAnAdmin -IsUserAnAdmin.argtypes = [] -IsUserAnAdmin.restype = BOOL CreateProcessW = kernel32.CreateProcessW CreateProcessW.argtypes = [ @@ -129,10 +124,6 @@ ] GetThreadTimes.restype = BOOL -GetExitCodeProcess = kernel32.GetExitCodeProcess -GetExitCodeProcess.argtypes = [HANDLE, POINTER(DWORD)] -GetExitCodeProcess.restype = BOOL - GetLastError = kernel32.GetLastError GetLastError.argtypes = [] GetLastError.restype = DWORD @@ -158,9 +149,11 @@ class Handle(metaclass=ABCMeta): def __init__(self, *args, **kwargs): self._handle = self._func(*self.__getinitargs__(*args, **kwargs)) if not self: - report(f"{self._func.__name__} failed.", - uselog=kwargs.get('uselog', True), - raiseexcept=kwargs.get("raiseexcept", True)) + report( + f"{self._func.__name__} failed.", + uselog=kwargs.get('uselog', True), + raiseexcept=kwargs.get("raiseexcept", True) + ) def __enter__(self): return self._handle From b9d7b0a7733773f8361a07ece7adb4ab779f5c4d Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 15:47:37 +0800 Subject: [PATCH 107/161] Fix. --- module/device/platform/api_windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index b9d9b1a166..e6999c56d8 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -8,7 +8,7 @@ from module.logger import logger -def __yieldloop(entry32, snapshot, func: callable): +def __yield_entries(entry32, snapshot, func: callable): """ Generates a loop that yields entries from a snapshot until the function fails or finishes. @@ -51,7 +51,7 @@ def _enum_processes(): with create_snapshot(TH32CS_SNAPPROCESS) as snapshot: if not Process32First(snapshot, byref(lppe32)): report("Process32First failed.") - yield from __yieldloop(lppe32, snapshot, Process32Next) + yield from __yield_entries(lppe32, snapshot, Process32Next) def _enum_threads(): @@ -70,7 +70,7 @@ def _enum_threads(): with create_snapshot(TH32CS_SNAPTHREAD) as snapshot: if not Thread32First(snapshot, byref(lpte32)): report("Thread32First failed.") - yield from __yieldloop(lpte32, snapshot, Thread32Next) + yield from __yield_entries(lpte32, snapshot, Thread32Next) def _enum_events(hevent): From cc6fef7b63a6a98751a39eea879535fd44fce962 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 15:49:52 +0800 Subject: [PATCH 108/161] Upd: fix texts. --- module/device/platform/platform_windows.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 07ff0545c5..5e670b2fbf 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -11,13 +11,11 @@ class EmulatorUnknown(Exception): pass -class EmulatorStatus: +class PlatformWindows(PlatformBase, EmulatorManager): process: tuple = () hwnds: list = [] focusedwindow: tuple = () - - -class PlatformWindows(PlatformBase, EmulatorManager, EmulatorStatus): + def __execute(self, command: str, start: bool): command = api_windows.fstr(command) logger.info(f'Execute: {command}') @@ -36,10 +34,10 @@ def __execute(self, command: str, start: bool): return True def _start(self, command: str): - self.__execute(command, start=True) + return self.__execute(command, start=True) def _stop(self, command: str): - self.__execute(command, start=False) + return self.__execute(command, start=False) @staticmethod def CloseHandle(*args, **kwargs): From c59490c723992c1d10a47b87e97a84a74f285c30 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 15:53:33 +0800 Subject: [PATCH 109/161] fix --- module/device/platform/platform_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 337c57d8da..451f58c001 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -34,10 +34,10 @@ def __execute(self, command: str, start: bool): return True def _start(self, command: str): - self.__execute(command, start=True) + return self.__execute(command, start=True) def _stop(self, command: str): - self.__execute(command, start=False) + return self.__execute(command, start=False) @staticmethod def CloseHandle(*args, **kwargs): From 20a219aa8acdda185e01fa406a1dee8595c3d9f3 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 16:38:06 +0800 Subject: [PATCH 110/161] Upd: fix comment --- module/device/platform/api_windows.py | 114 +++++++++++++----- module/device/platform/platform_windows.py | 38 ++---- .../platform/winapi/functions_windows.py | 35 +++--- 3 files changed, 110 insertions(+), 77 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index ec5da25c2f..950b43a396 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -8,7 +8,31 @@ from module.logger import logger -def __yieldloop(entry32, snapshot, func: callable): +def CloseHandle(*args, **kwargs) -> bool: + """ + Args: + *args: + **kwargs: + + Returns: + bool: + """ + for handle in args: + if isinstance(handle, tuple): + for h in handle: + functions_windows.CloseHandle(h) + else: + functions_windows.CloseHandle(handle) + for _, handle in kwargs.items(): + if isinstance(handle, tuple): + for h in handle: + functions_windows.CloseHandle(h) + else: + functions_windows.CloseHandle(handle) + return True + + +def __yield_entries(entry32, snapshot, func: callable) -> t.Generator: """ Generates a loop that yields entries from a snapshot until the function fails or finishes. @@ -35,7 +59,7 @@ def __yieldloop(entry32, snapshot, func: callable): report("Finished querying.", statuscode=errorcode, uselog=False, exception=IterationFinished) -def _enum_processes(): +def _enum_processes() -> t.Generator: """ Enumerates all the processes currently running on the system. @@ -51,10 +75,10 @@ def _enum_processes(): with create_snapshot(TH32CS_SNAPPROCESS) as snapshot: if not Process32First(snapshot, byref(lppe32)): report("Process32First failed.") - yield from __yieldloop(lppe32, snapshot, Process32Next) + yield from __yield_entries(lppe32, snapshot, Process32Next) -def _enum_threads(): +def _enum_threads() -> t.Generator: """ Enumerates all the threads currintly running on the system. @@ -70,10 +94,10 @@ def _enum_threads(): with create_snapshot(TH32CS_SNAPTHREAD) as snapshot: if not Thread32First(snapshot, byref(lpte32)): report("Thread32First failed.") - yield from __yieldloop(lpte32, snapshot, Thread32Next) + yield from __yield_entries(lpte32, snapshot, Thread32Next) -def getfocusedwindow(): +def getfocusedwindow() -> tuple: """ Get focused window. @@ -92,18 +116,16 @@ def getfocusedwindow(): report("Failed to get windowplacement.", level=30, raiseexcept=False) return hwnd, None -def setforegroundwindow(focusedwindow: tuple = ()) -> bool: +def setforegroundwindow(focusedwindow: tuple) -> bool: """ Refocus foreground window. Args: - focusedwindow: tuple(hwnd, WINDOWPLACEMENT) | tuple(hwnd, None) + focusedwindow (tuple(hwnd, WINDOWPLACEMENT) or tuple(hwnd, None)): Returns: bool: """ - if not focusedwindow: - return False SetForegroundWindow(focusedwindow[0]) if focusedwindow[1] is None: ShowWindow(focusedwindow[0], SW_SHOWNORMAL) @@ -112,8 +134,19 @@ def setforegroundwindow(focusedwindow: tuple = ()) -> bool: return True -def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5): +def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5) -> None: # TODO:Something error to fix. + """ + Try to refresh window if previous window was out of focus. + + Args: + focusedwindow (tuple): Previous focused window + max_attempts (int): + interval (float): + + Returns: + + """ from time import sleep attempts = 0 failed = 0 @@ -150,7 +183,7 @@ def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float sleep(interval) -def execute(command: str, silentstart: bool, start: bool): +def execute(command: str, silentstart: bool, start: bool) -> tuple: # TODO:Create Process with non-administrator privileges """ Create a new process. @@ -215,16 +248,21 @@ def execute(command: str, silentstart: bool, start: bool): if not success: report("Failed to start emulator.", exception=EmulatorLaunchFailedError) - process = ( - lpProcessInformation.hProcess, - lpProcessInformation.hThread, - lpProcessInformation.dwProcessId, - lpProcessInformation.dwThreadId - ) + if start: + process = ( + lpProcessInformation.hProcess, + lpProcessInformation.hThread, + lpProcessInformation.dwProcessId, + lpProcessInformation.dwThreadId + ) + else: + CloseHandle(lpProcessInformation.hProcess, lpProcessInformation.hThread) + process = () + return process, focusedwindow -def terminate_process(pid: int): +def terminate_process(pid: int) -> bool: """ Terminate emulator process. @@ -242,7 +280,7 @@ def terminate_process(pid: int): def get_hwnds(pid: int) -> list: """ - Get process's window hwnds from this processid. + Get window hwnds of the process by its ID. Args: pid (int): Emulator's pid @@ -274,7 +312,7 @@ def callback(hwnd: int, lparam): def get_cmdline(pid: int) -> str: """ - Get a process's command line from this processid. + Get command line of the process by its ID. Args: pid (int): Emulator's pid @@ -345,7 +383,17 @@ def kill_process_by_regex(regex: str) -> int: return count -def __get_creation_time(fopen, fgettime, access, identification): +def __get_creation_time(fopen: callable, fgettime: callable, access: int, identification: int) -> t.Optional[int]: + """ + Args: + fopen (callable): + fgettime (callable): + access (int): + identification (int): + + Returns: + int: creation time + """ with fopen(access, identification, uselog=False, raiseexcept=False) as handle: creationtime = FILETIME() exittime = FILETIME() @@ -361,9 +409,9 @@ def __get_creation_time(fopen, fgettime, access, identification): return None return to_int(creationtime) -def _get_process_creation_time(pid: int): +def _get_process_creation_time(pid: int) -> t.Optional[int]: """ - Get thread's creation time. + Get creation time of the process by its ID. Args: pid (int): Process id @@ -372,13 +420,13 @@ def _get_process_creation_time(pid: int): threadstarttime (int): Thread's start time Raises: - OSError if OpenThread failed. + OSError if OpenProcess failed. """ return __get_creation_time(open_process, GetProcessTimes, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, pid) -def _get_thread_creation_time(tid: int): +def _get_thread_creation_time(tid: int) -> t.Optional[int]: """ - Get thread's creation time. + Get creation time of the thread by its ID. Args: tid (int): Thread id @@ -392,9 +440,9 @@ def _get_thread_creation_time(tid: int): return __get_creation_time(open_thread, GetThreadTimes, THREAD_QUERY_INFORMATION, tid) -def get_thread(pid: int): +def get_thread(pid: int) -> int: """ - Get process's main thread id. + Get the main thread ID of the process by its ID. Args: pid (int): Emulator's pid @@ -426,7 +474,7 @@ def get_thread(pid: int): return mainthreadid -def _get_process(pid: int): +def _get_process(pid: int) -> tuple: """ Get emulator's handle. @@ -453,7 +501,7 @@ def _get_process(pid: int): logger.warning(f"Failed to get process and thread handles: {e}") return None, None, pid, tid -def get_process(instance: EmulatorInstance): +def get_process(instance: EmulatorInstance) -> tuple: """ Get emulator's process. @@ -501,9 +549,9 @@ def get_process(instance: EmulatorInstance): return _get_process(pid) -def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL): +def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL) -> bool: """ - Switch emulator's windowplacement to the given arg + Switch emulator's windowplacement to the given argument. Args: hwnds (list): Possible emulator's window hwnds diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 5e670b2fbf..14d3d97d50 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -16,7 +16,7 @@ class PlatformWindows(PlatformBase, EmulatorManager): hwnds: list = [] focusedwindow: tuple = () - def __execute(self, command: str, start: bool): + def __execute(self, command: str, start: bool) -> bool: command = api_windows.fstr(command) logger.info(f'Execute: {command}') @@ -27,44 +27,28 @@ def __execute(self, command: str, start: bool): if self.process: if self.process[0] is not None and self.process[1] is not None: - self.CloseHandle(self.process[:2]) + api_windows.CloseHandle(self.process[:2]) self.process = () self.process, self.focusedwindow = api_windows.execute(command, silentstart, start) return True - def _start(self, command: str): + def _start(self, command: str) -> bool: return self.__execute(command, start=True) - def _stop(self, command: str): + def _stop(self, command: str) -> bool: return self.__execute(command, start=False) - @staticmethod - def CloseHandle(*args, **kwargs): - for handle in args: - if isinstance(handle, tuple): - for h in handle: - api_windows.CloseHandle(h) - else: - api_windows.CloseHandle(handle) - for _, handle in kwargs.items(): - if isinstance(handle, tuple): - for h in handle: - api_windows.CloseHandle(h) - else: - api_windows.CloseHandle(handle) - return True - @staticmethod def kill_process_by_regex(regex: str) -> int: return api_windows.kill_process_by_regex(regex) @staticmethod - def getfocusedwindow(): + def getfocusedwindow() -> tuple: return api_windows.getfocusedwindow() @staticmethod - def setforegroundwindow(focusedwindow: tuple): + def setforegroundwindow(focusedwindow: tuple) -> bool: return api_windows.setforegroundwindow(focusedwindow) @staticmethod @@ -72,14 +56,14 @@ def get_hwnds(pid: int) -> list: return api_windows.get_hwnds(pid) @staticmethod - def get_process(instance: EmulatorInstance): + def get_process(instance: EmulatorInstance) -> tuple: return api_windows.get_process(instance) @staticmethod - def get_cmdline(pid: int): + def get_cmdline(pid: int) -> str: return api_windows.get_cmdline(pid) - def switch_window(self): + def switch_window(self) -> bool: if not self.process: self.process = self.get_process(self.emulator_instance) if not self.hwnds: @@ -339,7 +323,7 @@ def emulator_stop(self): logger.error('Failed to stop emulator 3 times, stopped') return False - def emulator_check(self): + def emulator_check(self) -> bool: try: if not self.process: self.process = self.get_process(self.emulator_instance) @@ -349,7 +333,7 @@ def emulator_check(self): return True else: if self.process[0] is not None and self.process[1] is not None: - self.CloseHandle(self.process[:2]) + api_windows.CloseHandle(self.process[:2]) self.process = () raise ProcessLookupError except api_windows.IterationFinished: diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 915168846c..0b5076cbb4 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -1,4 +1,5 @@ from abc import ABCMeta, abstractmethod +import typing as t from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t from ctypes.wintypes import ( @@ -146,7 +147,7 @@ class Handle(metaclass=ABCMeta): _func = None _exitfunc = None - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: self._handle = self._func(*self.__getinitargs__(*args, **kwargs)) if not self: report( @@ -155,50 +156,50 @@ def __init__(self, *args, **kwargs): raiseexcept=kwargs.get("raiseexcept", True) ) - def __enter__(self): + def __enter__(self) -> int: return self._handle - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type, exc_val, exc_tb) -> None: if self: self._exitfunc(self._handle) self._handle = None - def __bool__(self): + def __bool__(self) -> bool: return not self._is_invalid_handle() @abstractmethod - def __getinitargs__(self, *args, **kwargs): ... + def __getinitargs__(self, *args, **kwargs) -> tuple: ... @abstractmethod - def _is_invalid_handle(self): ... + def _is_invalid_handle(self) -> bool: ... class ProcessHandle(Handle): _func = OpenProcess _exitfunc = CloseHandle - def __getinitargs__(self, access, pid, uselog, raiseexcept): + def __getinitargs__(self, access, pid, uselog, raiseexcept) -> tuple: return access, False, pid - def _is_invalid_handle(self): + def _is_invalid_handle(self) -> bool: return self._handle is None class ThreadHandle(Handle): _func = OpenThread _exitfunc = CloseHandle - def __getinitargs__(self, access, pid, uselog, raiseexcept): + def __getinitargs__(self, access, pid, uselog, raiseexcept) -> tuple: return access, False, pid - def _is_invalid_handle(self): + def _is_invalid_handle(self) -> bool: return self._handle is None class CreateSnapshot(Handle): _func = CreateToolhelp32Snapshot _exitfunc = CloseHandle - def __getinitargs__(self, arg): + def __getinitargs__(self, arg) -> tuple: return arg, DWORD(0) - def _is_invalid_handle(self): + def _is_invalid_handle(self) -> bool: from module.device.platform.winapi.const_windows import INVALID_HANDLE_VALUE return self._handle == INVALID_HANDLE_VALUE @@ -210,7 +211,7 @@ def report( handle: int = 0, raiseexcept: bool = True, exception: type = OSError, -): +) -> None: """ Report any exception. @@ -237,17 +238,17 @@ def report( if raiseexcept: raise exception(message) -def fstr(formatstr: str): +def fstr(formatstr: str) -> t.Union[int, str]: try: return int(formatstr, 16) except ValueError: return formatstr.replace(r"\\", "/").replace("\\", "/").replace('"', '"') -def open_process(access, pid, uselog=False, raiseexcept=True): +def open_process(access, pid, uselog=False, raiseexcept=True) -> ProcessHandle: return ProcessHandle(access, pid, uselog=uselog, raiseexcept=raiseexcept) -def open_thread(access, tid, uselog=False, raiseexcept=True): +def open_thread(access, tid, uselog=False, raiseexcept=True) -> ThreadHandle: return ThreadHandle(access, tid, uselog=uselog, raiseexcept=raiseexcept) -def create_snapshot(arg): +def create_snapshot(arg) -> CreateSnapshot: return CreateSnapshot(arg) From 0136b90495f2603e406081d3ad1d6931e25c7ae5 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 17:08:34 +0800 Subject: [PATCH 111/161] Upd: fix --- module/device/platform/api_windows.py | 13 ++++++------- module/device/platform/platform_windows.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 3b46af1670..3cbed0cb2d 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,6 +1,5 @@ import threading import asyncio -import re from ctypes import byref, sizeof, create_unicode_buffer, wstring_at, addressof @@ -9,7 +8,7 @@ from module.logger import logger -def CloseHandle(*args, **kwargs) -> bool: +def closehandle(*args, **kwargs) -> bool: """ Args: *args: @@ -21,15 +20,15 @@ def CloseHandle(*args, **kwargs) -> bool: for handle in args: if isinstance(handle, tuple): for h in handle: - functions_windows.CloseHandle(h) + CloseHandle(h) else: - functions_windows.CloseHandle(handle) + CloseHandle(handle) for _, handle in kwargs.items(): if isinstance(handle, tuple): for h in handle: - functions_windows.CloseHandle(h) + CloseHandle(h) else: - functions_windows.CloseHandle(handle) + CloseHandle(handle) return True @@ -306,7 +305,7 @@ def execute(command: str, silentstart: bool, start: bool) -> tuple: lpProcessInformation.dwThreadId ) else: - CloseHandle(lpProcessInformation.hProcess, lpProcessInformation.hThread) + closehandle(lpProcessInformation.hProcess, lpProcessInformation.hThread) process = () return process, focusedwindow diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 14d3d97d50..e0ef35967a 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -27,7 +27,7 @@ def __execute(self, command: str, start: bool) -> bool: if self.process: if self.process[0] is not None and self.process[1] is not None: - api_windows.CloseHandle(self.process[:2]) + api_windows.closehandle(self.process[:2]) self.process = () self.process, self.focusedwindow = api_windows.execute(command, silentstart, start) @@ -333,7 +333,7 @@ def emulator_check(self) -> bool: return True else: if self.process[0] is not None and self.process[1] is not None: - api_windows.CloseHandle(self.process[:2]) + api_windows.closehandle(self.process[:2]) self.process = () raise ProcessLookupError except api_windows.IterationFinished: From 456d9ad19bdfe9947302283712bbb3209717d734 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 16 Jul 2024 17:13:13 +0800 Subject: [PATCH 112/161] Upd: fix --- module/device/platform/platform_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index e0ef35967a..341a61ad99 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -103,7 +103,7 @@ def _emulator_start(self, instance: EmulatorInstance): self._start(f'"{exe}" -clone:{instance.name}') elif instance == Emulator.BlueStacks5: # HD-Player.exe -instance Pie64 - self._start(f'"{exe}" -instance {instance.name}') + self._start(f'"{exe}" --instance {instance.name}') elif instance == Emulator.BlueStacks4: # Bluestacks.exe -vmname Android_1 self._start(f'"{exe}" -vmname {instance.name}') From e1a469fcee978cb7698ce065417b3a7f47ef4f4c Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:09:21 +0800 Subject: [PATCH 113/161] Add: [ALAS] Add interval on dump_hierarchy() (cherry picked from commit 00f3bf749542c6db1aee77d2cc08081e1c3f5189) --- module/device/app_control.py | 20 ++++++++++++++++++++ module/device/device.py | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/module/device/app_control.py b/module/device/app_control.py index 1368a74f1d..b483e3440a 100644 --- a/module/device/app_control.py +++ b/module/device/app_control.py @@ -1,15 +1,18 @@ from lxml import etree +from module.base.timer import Timer from module.device.method.adb import Adb from module.device.method.uiautomator_2 import Uiautomator2 from module.device.method.utils import HierarchyButton from module.device.method.wsa import WSA +from module.exception import ScriptError from module.logger import logger class AppControl(Adb, WSA, Uiautomator2): hierarchy: etree._Element _app_u2_family = ['uiautomator2', 'minitouch', 'scrcpy', 'MaaTouch', 'nemu_ipc'] + _hierarchy_interval = Timer(0.1) def app_is_running(self) -> bool: method = self.config.Emulator_ControlMethod @@ -42,11 +45,28 @@ def app_stop(self): else: self.app_stop_adb() + def hierarchy_timer_set(self, interval=None): + if interval is None: + interval = 0.1 + elif isinstance(interval, (int, float)): + # No limitation for manual set in code + pass + else: + logger.warning(f'Unknown hierarchy interval: {interval}') + raise ScriptError(f'Unknown hierarchy interval: {interval}') + + if interval != self._hierarchy_interval.limit: + logger.info(f'Hierarchy interval set to {interval}s') + self._hierarchy_interval.limit = interval + def dump_hierarchy(self) -> etree._Element: """ Returns: etree._Element: Select elements with `self.hierarchy.xpath('//*[@text="Hermit"]')` for example. """ + self._hierarchy_interval.wait() + self._hierarchy_interval.reset() + method = self.config.Emulator_ControlMethod if method in AppControl._app_u2_family: self.hierarchy = self.dump_hierarchy_uiautomator2() diff --git a/module/device/device.py b/module/device/device.py index 26b6313230..8506118f36 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -1,6 +1,8 @@ import collections from datetime import datetime +from lxml import etree + # Patch pkg_resources before importing adbutils and uiautomator2 from module.device.pkg_resources import get_distribution @@ -175,6 +177,10 @@ def screenshot(self): return self.image + def dump_hierarchy(self) -> etree._Element: + self.stuck_record_check() + return super().dump_hierarchy() + def release_during_wait(self): # Scrcpy server is still sending video stream, # stop it during wait From 4a16b0c5acbafb3e3f04f14873eb2b812ee45f44 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:59:55 +0800 Subject: [PATCH 114/161] Revert "Del: Remove AzurStats upload" This reverts commit 41e9f197 --- module/statistics/azurstats.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/module/statistics/azurstats.py b/module/statistics/azurstats.py index 45fbfa200d..551403fc41 100644 --- a/module/statistics/azurstats.py +++ b/module/statistics/azurstats.py @@ -217,10 +217,10 @@ def commit(self, images, genre, save=False, upload=False, info=''): save_thread = threading.Thread( target=self._save, args=(image, genre, filename)) save_thread.start() - # if upload: - # upload_thread = threading.Thread( - # target=self._upload, args=(image, genre, filename)) - # upload_thread.start() + if upload: + upload_thread = threading.Thread( + target=self._upload, args=(image, genre, filename)) + upload_thread.start() return True From 6a0466d842fa60f9de55d169e4977d03f3ade8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=A6=E5=BF=B5=E9=80=8D=E9=81=A5?= <2589141604@qq.com> Date: Thu, 18 Jul 2024 21:38:27 +0800 Subject: [PATCH 115/161] Add: Event Pledge of the Radiant Court Rerun (#4001) Co-authored-by: LmeSzinc --- campaign/Readme.md | 1 + module/config/argument/args.json | 64 ++++++++++++++++---------------- module/config/i18n/en-US.json | 2 +- module/config/i18n/ja-JP.json | 2 +- module/config/i18n/zh-CN.json | 2 +- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/campaign/Readme.md b/campaign/Readme.md index 3bf9dc29c5..beb4158cc9 100644 --- a/campaign/Readme.md +++ b/campaign/Readme.md @@ -201,3 +201,4 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20240627 | event 20231026 cn | Tempesta and the Fountain of Youth | - | - | - | 飓風與青春之泉 | | 20240627 | coalition 20240627 | Welcome to Little Academy | 欢迎来到童心学院 | Welcome to Little Academy | リトル学園へようこそ | - | | 20240711 | event 20211229 cn | Tower of Transcendence Rerun | - | - | -  | 復刻逆轉彩虹之塔 | +| 20240718 | event 20220526 cn | Pledge of the Radiant Court Rerun | 复刻泠誓光庭 | Pledge of the Radiant Court Rerun | 復刻诚閃の剣 搖光の城 | - | diff --git a/module/config/argument/args.json b/module/config/argument/args.json index c8256acb2c..b3b1b78032 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -1707,11 +1707,11 @@ "display": "hide", "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -2042,11 +2042,11 @@ ], "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -2492,11 +2492,11 @@ ], "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -3901,11 +3901,11 @@ ], "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -4368,11 +4368,11 @@ ], "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -4835,11 +4835,11 @@ ], "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -5302,11 +5302,11 @@ ], "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -5759,11 +5759,11 @@ ], "option_bold": [ "event_20211229_cn", - "event_20220428_cn" + "event_20220526_cn" ], - "cn": "event_20220428_cn", - "en": "event_20220428_cn", - "jp": "event_20220428_cn", + "cn": "event_20220526_cn", + "en": "event_20220526_cn", + "jp": "event_20220526_cn", "tw": "event_20211229_cn" }, "Mode": { diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 56ee7c8322..41ea91eb9b 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -709,7 +709,7 @@ "event_20220407_tw": "蒼紅的迴響(復刻)", "event_20220414_cn": "Aurora Noctis Rerun", "event_20220428_cn": "Rondo at Rainbows End Rerun", - "event_20220526_cn": "Pledge of the Radiant Court", + "event_20220526_cn": "Pledge of the Radiant Court Rerun", "event_20220728_cn": "Aquilifers Ballade", "event_20220818_cn": "Operation Convergence", "event_20220915_cn": "Violet Tempest Blooming Lycoris", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index c112afd7e6..f55e7768ac 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -709,7 +709,7 @@ "event_20220407_tw": "蒼紅的迴響(復刻)", "event_20220414_cn": "極夜照らす幻光(復刻)", "event_20220428_cn": "吟ずる瑠璃の楽章(復刻)", - "event_20220526_cn": "诚閃の剣 搖光の城", + "event_20220526_cn": "復刻诚閃の剣 搖光の城", "event_20220728_cn": "鋼鷲の冒険譚", "event_20220818_cn": "結像点作戦", "event_20220915_cn": "赫の涙月 菫の暁風", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index e3f49ba7f5..a303dbbf99 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -709,7 +709,7 @@ "event_20220407_tw": "蒼紅的迴響(復刻)", "event_20220414_cn": "复刻永夜幻光", "event_20220428_cn": "复刻虹彩的终幕曲", - "event_20220526_cn": "泠誓光庭", + "event_20220526_cn": "复刻泠誓光庭", "event_20220728_cn": "雄鹰的叙事歌", "event_20220818_cn": "远汇点作战", "event_20220915_cn": "紫绛槿岚", From 820eea8550f0d65ed6b0caa1c1e44602a15c5e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9C=9E=E9=A3=9B?= Date: Thu, 18 Jul 2024 21:39:03 +0800 Subject: [PATCH 116/161] fix: SHOP_BUY_CONFIRM_AMOUNT behavior on slower devices (#3998) * fix: SHOP_BUY_CONFIRM_AMOUNT behavior on slower devices * fix: set interval and reset flag --- module/os_shop/shop.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/module/os_shop/shop.py b/module/os_shop/shop.py index a9d6830dc9..5c041a612b 100644 --- a/module/os_shop/shop.py +++ b/module/os_shop/shop.py @@ -22,6 +22,7 @@ def os_shop_buy_execute(self, button, skip_first_screenshot=True) -> bool: in: PORT_SUPPLY_CHECK """ success = False + amount_finish = False self.interval_clear(PORT_SUPPLY_CHECK) self.interval_clear(SHOP_BUY_CONFIRM) self.interval_clear(SHOP_BUY_CONFIRM_AMOUNT) @@ -33,22 +34,25 @@ def os_shop_buy_execute(self, button, skip_first_screenshot=True) -> bool: else: self.device.screenshot() - if self.handle_map_get_items(interval=1): + if self.handle_map_get_items(interval=3): self.interval_reset(PORT_SUPPLY_CHECK) success = True continue - if self.appear_then_click(SHOP_BUY_CONFIRM, offset=(20, 20), interval=1): + if self.appear_then_click(SHOP_BUY_CONFIRM, offset=(20, 20), interval=3): self.interval_reset(SHOP_BUY_CONFIRM) continue - if self.appear_then_click(OS_SHOP_BUY_CONFIRM, offset=(20, 20), interval=1): + if self.appear_then_click(OS_SHOP_BUY_CONFIRM, offset=(20, 20), interval=3): self.interval_reset(OS_SHOP_BUY_CONFIRM) continue - if self.appear(SHOP_BUY_CONFIRM_AMOUNT, offset=(20, 20), interval=1): + if not amount_finish and self.appear(SHOP_BUY_CONFIRM_AMOUNT, offset=(20, 20)): self.shop_buy_amount_handler(button) - self.device.click(SHOP_BUY_CONFIRM_AMOUNT) + amount_finish = True + continue + + if amount_finish and self.appear_then_click(SHOP_BUY_CONFIRM_AMOUNT, offset=(20, 20), interval=3): self.interval_reset(SHOP_BUY_CONFIRM_AMOUNT) continue @@ -56,6 +60,7 @@ def os_shop_buy_execute(self, button, skip_first_screenshot=True) -> bool: continue if not success and self.appear(PORT_SUPPLY_CHECK, offset=(20, 20), interval=5): + amount_finish = False self.device.click(button) continue @@ -86,7 +91,7 @@ def os_shop_buy(self, select_func) -> int: logger.warning('Too many items to buy, stopped') return count - def shop_buy_amount_handler(self, item): + def shop_buy_amount_handler(self, item, skip_first_screenshot=True): """ Handler item amount to buy. @@ -137,17 +142,18 @@ def shop_buy_amount_handler(self, item): else: limit = 10 - if set_to_max: - while True: - self.appear_then_click(AMOUNT_MAX, offset=(50, 50)) - self.device.sleep((0.3, 0.5)) + self.interval_clear(AMOUNT_MAX) + while set_to_max: + if skip_first_screenshot: + skip_first_screenshot = False + else: self.device.screenshot() - amount = OCR_SHOP_AMOUNT.ocr(self.device.image) - if amount > 1: - break - if retry.reached(): - raise GameStuckError('Amount OCR failed.') - retry.reset() + + if self.appear_then_click(AMOUNT_MAX, offset=(50, 50), interval=3): + continue + + if OCR_SHOP_AMOUNT.ocr(self.device.image) > 1: + break self.ui_ensure_index(limit, letter=OCR_SHOP_AMOUNT, prev_button=AMOUNT_MINUS, next_button=AMOUNT_PLUS, skip_first_screenshot=True) From ff6307b09df73d498f93c01f68d2587fb4b89fb4 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:52:47 +0800 Subject: [PATCH 117/161] Fix: Allow Hermit on VMOS only --- module/device/connection_attr.py | 10 +++++++++- module/device/device.py | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/module/device/connection_attr.py b/module/device/connection_attr.py index 1e75fcf7e8..cd4fc89e82 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -9,6 +9,7 @@ from module.config.config import AzurLaneConfig from module.config.env import IS_ON_PHONE_CLOUD from module.config.utils import deep_iter +from module.device.method.utils import get_serial_pair from module.exception import RequestHumanTakeover from module.logger import logger @@ -148,8 +149,11 @@ def is_wsa(self): @cached_property def port(self) -> int: + port_serial, _ = get_serial_pair(self.serial) + if port_serial is None: + port_serial = self.serial try: - return int(self.serial.split(':')[1]) + return int(port_serial.split(':')[1]) except (IndexError, ValueError): return 0 @@ -168,6 +172,10 @@ def is_mumu_family(self): def is_nox_family(self): return 62001 <= self.port <= 63025 + @cached_property + def is_vmos(self): + return 5667 <= self.port <= 5699 + @cached_property def is_emulator(self): return self.serial.startswith('emulator-') or self.serial.startswith('127.0.0.1:') diff --git a/module/device/device.py b/module/device/device.py index 8506118f36..e7ed745612 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -131,6 +131,10 @@ def method_check(self): # 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' + # Allow Hermit on VMOS only + if self.config.Emulator_ControlMethod == 'Hermit' and not self.is_vmos: + logger.warning('ControlMethod is allowed on VMOS only') + self.config.Emulator_ControlMethod = 'minitouch' pass def handle_night_commission(self, daily_trigger='21:00', threshold=30): From ccd19417ec3965fb2fa5a8040a00d88e4070ec9c Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:59:24 +0800 Subject: [PATCH 118/161] Fix: Auto-fill emulator info on Windows only --- module/device/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/device/device.py b/module/device/device.py index e7ed745612..456eca5fd7 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -3,6 +3,7 @@ from lxml import etree +from module.device.env import IS_WINDOWS # Patch pkg_resources before importing adbutils and uiautomator2 from module.device.pkg_resources import get_distribution @@ -85,7 +86,7 @@ def __init__(self, *args, **kwargs): raise # Auto-fill emulator info - if self.config.EmulatorInfo_Emulator == 'auto': + if IS_WINDOWS and self.config.EmulatorInfo_Emulator == 'auto': _ = self.emulator_instance self.screenshot_interval_set() From 06143434a86a2675876c11f17bdafb476f10efd9 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 18 Jul 2024 22:20:50 +0800 Subject: [PATCH 119/161] Add: Claim mail rewards --- assets/cn/freebies/MAIL_BATCH_CLAIM.png | Bin 0 -> 7824 bytes assets/cn/freebies/MAIL_BATCH_DELETE.png | Bin 0 -> 7733 bytes assets/cn/freebies/MAIL_MANAGE.png | Bin 0 -> 7056 bytes assets/cn/freebies/MAIL_SELECT_COINS.png | Bin 0 -> 5773 bytes assets/cn/freebies/MAIL_SELECT_CUBE.png | Bin 0 -> 5771 bytes assets/cn/freebies/MAIL_SELECT_GEMS.png | Bin 0 -> 5772 bytes assets/cn/freebies/MAIL_SELECT_MERIT.png | Bin 0 -> 6176 bytes assets/cn/freebies/MAIL_SELECT_OIL.png | Bin 0 -> 5773 bytes assets/en/freebies/MAIL_BATCH_CLAIM.png | Bin 0 -> 9105 bytes assets/en/freebies/MAIL_BATCH_DELETE.png | Bin 0 -> 9336 bytes assets/en/freebies/MAIL_MANAGE.png | Bin 0 -> 9483 bytes assets/en/freebies/MAIL_SELECT_COINS.png | Bin 0 -> 5773 bytes assets/en/freebies/MAIL_SELECT_CUBE.png | Bin 0 -> 5771 bytes assets/en/freebies/MAIL_SELECT_GEMS.png | Bin 0 -> 5772 bytes assets/en/freebies/MAIL_SELECT_MERIT.png | Bin 0 -> 6176 bytes assets/en/freebies/MAIL_SELECT_OIL.png | Bin 0 -> 5773 bytes assets/jp/freebies/MAIL_SELECT_COINS.png | Bin 0 -> 5773 bytes assets/jp/freebies/MAIL_SELECT_CUBE.png | Bin 0 -> 5771 bytes assets/jp/freebies/MAIL_SELECT_GEMS.png | Bin 0 -> 5772 bytes assets/jp/freebies/MAIL_SELECT_MERIT.png | Bin 0 -> 6176 bytes assets/jp/freebies/MAIL_SELECT_OIL.png | Bin 0 -> 5773 bytes assets/tw/freebies/MAIL_SELECT_COINS.png | Bin 0 -> 5773 bytes assets/tw/freebies/MAIL_SELECT_CUBE.png | Bin 0 -> 5771 bytes assets/tw/freebies/MAIL_SELECT_GEMS.png | Bin 0 -> 5772 bytes assets/tw/freebies/MAIL_SELECT_MERIT.png | Bin 0 -> 6176 bytes assets/tw/freebies/MAIL_SELECT_OIL.png | Bin 0 -> 5773 bytes module/freebies/assets.py | 8 + module/freebies/mail_white.py | 221 +++++++++++++++++++++++ module/ui/page.py | 2 +- module/ui/setting.py | 5 + 30 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 assets/cn/freebies/MAIL_BATCH_CLAIM.png create mode 100644 assets/cn/freebies/MAIL_BATCH_DELETE.png create mode 100644 assets/cn/freebies/MAIL_MANAGE.png create mode 100644 assets/cn/freebies/MAIL_SELECT_COINS.png create mode 100644 assets/cn/freebies/MAIL_SELECT_CUBE.png create mode 100644 assets/cn/freebies/MAIL_SELECT_GEMS.png create mode 100644 assets/cn/freebies/MAIL_SELECT_MERIT.png create mode 100644 assets/cn/freebies/MAIL_SELECT_OIL.png create mode 100644 assets/en/freebies/MAIL_BATCH_CLAIM.png create mode 100644 assets/en/freebies/MAIL_BATCH_DELETE.png create mode 100644 assets/en/freebies/MAIL_MANAGE.png create mode 100644 assets/en/freebies/MAIL_SELECT_COINS.png create mode 100644 assets/en/freebies/MAIL_SELECT_CUBE.png create mode 100644 assets/en/freebies/MAIL_SELECT_GEMS.png create mode 100644 assets/en/freebies/MAIL_SELECT_MERIT.png create mode 100644 assets/en/freebies/MAIL_SELECT_OIL.png create mode 100644 assets/jp/freebies/MAIL_SELECT_COINS.png create mode 100644 assets/jp/freebies/MAIL_SELECT_CUBE.png create mode 100644 assets/jp/freebies/MAIL_SELECT_GEMS.png create mode 100644 assets/jp/freebies/MAIL_SELECT_MERIT.png create mode 100644 assets/jp/freebies/MAIL_SELECT_OIL.png create mode 100644 assets/tw/freebies/MAIL_SELECT_COINS.png create mode 100644 assets/tw/freebies/MAIL_SELECT_CUBE.png create mode 100644 assets/tw/freebies/MAIL_SELECT_GEMS.png create mode 100644 assets/tw/freebies/MAIL_SELECT_MERIT.png create mode 100644 assets/tw/freebies/MAIL_SELECT_OIL.png create mode 100644 module/freebies/mail_white.py diff --git a/assets/cn/freebies/MAIL_BATCH_CLAIM.png b/assets/cn/freebies/MAIL_BATCH_CLAIM.png new file mode 100644 index 0000000000000000000000000000000000000000..1d1889bcc88045e52f6d6e7e1535e9ecbf410f93 GIT binary patch literal 7824 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 UpY@jR`wN4Cj`5vJ?fXyu1L)^pF8}}l literal 0 HcmV?d00001 diff --git a/assets/cn/freebies/MAIL_BATCH_DELETE.png b/assets/cn/freebies/MAIL_BATCH_DELETE.png new file mode 100644 index 0000000000000000000000000000000000000000..2e49243506f8434bed645cf02a5a63fdd74dc907 GIT binary patch literal 7733 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 literal 0 HcmV?d00001 diff --git a/assets/cn/freebies/MAIL_MANAGE.png b/assets/cn/freebies/MAIL_MANAGE.png new file mode 100644 index 0000000000000000000000000000000000000000..bda63000b9fa0b17cde970a2df4598655f7e557c GIT binary patch literal 7056 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 literal 0 HcmV?d00001 diff --git a/assets/cn/freebies/MAIL_SELECT_COINS.png b/assets/cn/freebies/MAIL_SELECT_COINS.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d17af29e8ff6a49174c0f6851e3f90b549c50c GIT binary patch literal 5773 zcmeI0`8yQe_rR|~8M0&>OR~kgLiXOsE*Y{lvL)HF8)P5rM3$0$YwV$HLzFVfR(4~F zBH4FoF!ss*nZCb#|AEg>pT~Wkd(S=3IrrT2ynZ?7-lvB8TJ*GBv;Y8lZTL+D0Mz6y z!_aTAA)zyO^2lY$YONV`^en#|pX5pm80v8~U zh*VS<;Y~1rw{|t4A#CtzDn6z~{1->+HUQQsQ30VoRVs%RZ#)15cn)2=#N07+5>aO; zYzp|2z_bAK5*u(|*!qX#S7+RD^mq%>t521W`l zl+u7W3V^w+bqS*S30NCHMu=1K~*`3EU22)cn9_{EzgGOAhwG)7&t z2k^;3fW}3ZUP?Y)%6y5zrs+~E&qy4c0yN{(dNXbhO0(k9zH<3*u20YM&wg>MR=0s& zVRC@Z3uI?x39`VKD0TbJ0l>94@cCFyC2?tSerC~O$&I+(R&eaP?jfMezcF=O{~SUE znDqOJqgD$G!<3#K6ji=WuB8@W)C4$9?O95XLG$m?ehu7bgT!2{@`?0}$8>DyF4=lK7Pt-`WvXH-nhPV29@)72RU+b-@KoxO?xZEb!PfKyOCT902vdIPRCE!gSkd0Jxd| z_-d8f*>j&+vCRzGpLyz=VGx@$H&xGFYF1=YKQoWMK>Ly-U5)c@v%(0~`zlJV{PUSJ zOf{b+<>;5DneRU)c*4$DbL4*EsE>kDsddt@*zhaFoVSf15$L0VVdAL-Y~V7<=pw%5 z7}h%iZJl04kWzJ*Bulv9rkEG2eB6E2{lu|OKB;Gi&+4@#Qy;&*L-`%ztNO8om+M)3 zzW0ES45KAFy})&VtLCB-`a?nS!0lhYJ|N^-yd8Q#lELBmPn-MCqcyFcnsEttu-h$| z$(kg{+VPnE>&M33oNGg*xde$tv#iiOc*69>Ej%$%%S_iyqeN&$u!gyg#uOUyMDoi& z0x2a1H9~>Rd#wSs3e|~bx3I!%{1^Fig&zFFz+#mGzr@g$Jg-m7FO(>yO{eV&AA^e( z85JoHGC61K3KJ5MU6b=PZ_!x3jHKE`#zceUgRYkOT2_U&m|0zD!6igdDzlEPy~+aB zt2`-nOn%v&-qPIfG-WXgdH1xD`F@7tM$x4#GQOEYy;AWlPLy8;Y z_=53;hq2k!-K)WDN!`q83TY;3p=nEHca6P`RfbSyba%^*8OtA-guK3G{LXN$6#AXL ze5jPi_*3ci%07fYBEhi#j#Xi!)lvAq9sS#=$XoeQ@WQ)xwe6g7{Os*g_8VsE!(Jwo z1X0%)dbSpC_f~ZMs1LaP#DjE=G`!0kuntKMIg5{aDHz(Tr`P3wX6y9v_tEzSiBXEF zh*b}}R5(&AThe=C2p zBcPU{JKZ}B%M$}yJIL+A9oTQ;FmZtQUkMqS>xrChl5%$IJ&AU|Twgfj2G*a-U_CC_Vr3@n$6Xa|Y@MaXidu`><{Pi{rn+W2)D6^p z^Xcj76dlKoV7pf0<%hCeb8Q-b){h;=MZVI^Zqtdq9@~*VnjVa&PS;B(VPBXio1jc~ zOnzXshwl?a3CLknH$!gItaDikS@;hn6+nsD}z^djNC^hR}NQbS29=5 ztjh1&>>9-|)E+?B(OzS?W0IPiMr>DYL1cgVLMxdZlxNTS2odGiCuhWL$y z(&ry?#+=3qHaYm;3O!{)*2*AM`;P<{uPdOa_-V>w?XAf>jtTMz^y&J&=m~TuAG--Zf7mPK6`CpT?MASVD za9$q_!8_sKrXLy3njwrgT~Onj@ra1QSFbcg!cAq;N>jV=?v>m3KH9O_q&+kyx?~Jl zn1o%0rCDJ;u_cv%s&3&HXHE63hJtrc$kEYBo!ahI0-R7paL1C&o8IU&xTp<%jasL# z;NFN^;I=~QO!~YGblHjTt?BLVrIfXiUA5@>fF0X>dA)EqFel6sZ*o*oUfcLncyzdF z!arz>A%chP#w0OyrT)E0fT?SPX2_2F;9B@Z0+%g!UDIOd1WwD}pe;8c=vC0!T}>jv z#mB>K{7qBSTPPB7J?)|CxFn95iS6>t$cdcuVgT zCaiXch5$#23@SAFV#NxP`ARPvh`*i}=I}h=(Q2$c2_;I-+4*+JCYa{t=Ui6W*qaYe znJa!+oSUPsqVydzjw0zEVq7inST;Cn^yjL;eAaYptw>#4lS>Jf>-C=70wmipq1xLO zN|j1lp`J%G@HZA71bY&G)l{+4XIqbOV}STAY~Q7M$ot2iAJ;7NPz}WP74BMh;fV`4 zp}L(ve!HtMf7&(Oam&DjfH#MYqes8b?lb-lt2wT=Z$?i4v2M2RZ!u>iGC-qqk5f;+ zMb8Xs^=J(V-V%&U$l>MXrsrzdeAt&VQ~b7=rN2AWsP4ZN7khpNYhTLb%c9mQV_NXkpW z$A{viCtC+XgL%^AZj;Xrj?e=jPzV5Y1OR&{WVsB0uLJ7?7#*zb3u>F#08v`1F2 z?}r#eq zV#T&fKowA~6D>kT$(4%i0RW9PdHMexQ!`m5pCkXX07`O^182xO1^KNHARK6k(o-O;DtszQGvP@-d zEFlugZqm?LVzPhd^V|0i`2O@g?m5qM?>Wys?|aYdm*<}Q*!+ee2dgkE0D!{?b`=f) z6D_3w456nv9dTninq%>U*#!W=#{1u)137tu0I-^R>g$`Ed!W#$01uR(gpt0!gr7gk z&C|yffRKrN%V66ys(|)1)s@RyZ+xvj%jW@*lEy)Lxa~@~p91HH+&ns+F>$QceEcW# zU{)PbqpYy}xVa)Zyu0p`xLfg7w=et_pQ?Bh9!gnV-TS)NLdqlU4`2qluqSzBQWzBK%WHVStZ!4CbA2!1%!M#NM;Lg+uz#0Q4Bm!M`WuNXP3eW?;QKi#7-Lps0 z&E~Q;Kq3{)Nb!)^=u+UIN9kq{ALtPTb(XL1vVwE~d@x~pQlOdxto$+3W&-0?X;V-z zUUI4m0ut#!ww&Q?|l(yvh^kGW^ zJtN@0F-VzkTwEMu@av|l3vBnUvIi5^z+-yH;oKxk(Jj``p*!)I3;Q}^1;yq)mqkyp zvazkoxD>al%*SgVots6?#7At?UJoZj*TB6To$MMyY4pK8XWyV&coK=osHf^nB}pH4 z$3z|^of(=_ufMIp|76|vG2UFtv>Wplo_bZXH)D=xIZ|z1JoA7hEtmt~9+?qtG%~*- z`Vb<1)6vIs+71T+orq1v1Yx#R!0oQChM@wG*1nUhBTonVv&!ZHX!$94yD}YW(ggwF zYSDwUb-K*#@A+^9&b;@cEd(gN^NFiE?57Erx%EygU{A3Y3ufvH-6Uv^Grp-~5H32I zGt1rZUiAV8X@=+a1F|3Vgp**w2f>y{ER4E65MF0V&A5{;N#jxj5NLK1qm(mD{VBFg zVmXe_LaMXJzl^?0&nwjdCcUBP&v!BLw$5(KWRHZ}ll>Sl$)Lorl~Xe4 zBb8QZ)*utgv(pjcqFJ9}dkrW1TT)E2K<3UroV<={u+yBqPZw^mO3I|D8L=68!zN*h zWtL^y!`z;ECbHxdWbcm!$SW*PA}h5ig)7DE>0a;0g(g1D&bT=fmXg!(vUDC}4c9A+ zjL!?z^sn1KDQ6nV%I>+T@>NYF z+x;2WGnAF>+3mAo{HdRKGBh)+Ga@ob)iIb`6`w# z0xw3ZM6KRcov$5$2g8%i2Q3^++Z+#~b`cz}9>rWMdIT%IiD>E)N|Y4nQgdCm)f@A- zW*{qgKQnc)f3>q>62!D85~O_J#8N+?+70KH>XyHBub+;ytA1wf;od}(1n{Zk6Ulmv-Pf!B=@^$hR^0zdRn$$p+=JU-K%`wfBfrG!a zM-XJpGRxMdEzafdLxx+(Kc!pHUz9P*kodnU>X7p(LO!Y&5NmxYh@aliJn=(o4`(Y+ z5jD@hke^Sle3+rG6-?kC1PH^}QWjz1sYojH*iMr6t~&7|U_lNHFwF&iIq5u050T+Li!V`WWlO-E2Ja++_BFZs;ynJr7-3DuST71ouU zl@qHMx1G0TwvpR|qJ5`$M8__nFNOJ|1N(w}{94Fmv+5tKI;uV{+qjFk7YF2TYCy}c zrx1Cvg0i5N$ciTwzJ;O0j78z!&BJd-wn}r~}Ohu_nkDgwOn@xmKn6W zwCM{&3uD{L+oYzo+N2}KBC;cx_Y3we?Gg`?|F+L!HU!phFgek^r(2|JeIWecHG?;U zIAb>Bb7l>uM5cEql-afU_1IZB?8N=kPz30P$PVJ%l)FOC5dSAgKL0I|U6npr6S1+A zNsk60)7%lL@*R`qzD7vgjG<2)UffeC8@dzDcjl~GwbCz(221okG*w{?w_vyD-kP+Y zT$S+l(xg|Te~>%Q%Lnd*Sjoj>@zwYTA+diA*0Xrfyr)@KA1`!_cW6DH51=Rcwg|De7@5aO27&C*m zlEe~=`X;>b<)=&e)*0IuzF!L0-}QK8b}g{$9E%xT9+N8c^sV%!srA*dAGpD9_r57! zOwUWtD}{4-W;V!wRcVrIGINucPvcm?EFnLMq{V)sE=cbLY)>|?Kz7+8tZy0RWS&)4H`F-YX z0}9y-jq;g#+1~z&1qnZ&C4}6^Z z*gu^e*|Y^A!!Yv0n(cu&#ZqLER&gukXF_Sd=p9kRwx**9it0Qfuv;V9rl2TaPHTN< zAu4UY;%-Gj{*5bIU$Un#RFnN|ZwCv9R(Jiuf-6w;Z<8iRYVYO`QnJHZi{Bq9s>`HI z)AbsyS}nr}zk^xWOM4XAl}cQ-xm=6>V@kd>L>YBQuGad8n2;nt1218K7ruPS&4KSfv{VaNe5NYe*%d)EhT_s7@R zcH#%yYcW-u{*)J0FEzWRyTw}uFk0*7`!xfT;S@ZjdAhq*JX>yG=g@p}Xcy64@!5>3 zO$s~QSEfGP+>;qDJV%>0B@i&UDFC4|0AQm5*g2y4WdH(I0QltufaWs*ps1&go!4m7 zDBkF*?#+;im2dT4lRBV#_WHe9UkI#7Hww+Ya4;?5z-pJzELRyl>nqp89nI>2rOyHQnF@gUt1j6KkWq~ciE|XhNdtu}C?E#yz!82Xs z>iugf54qvSCm6wBQV=b}b5~U~xx0_6~g%jDpG6I z9yMx-q}bC6L!7Yv9?fo=CbMD0{D5bimLnQt4F4(Ss-pF$IuD2ui;**-)q7dtQ=P>k zIA*XWHkA#W5?$@QYDTD4W5NjbcP~(d9~a-O-%C&}PiKex*U5QaLYBVL+w_g?zcy!{ zw~=FGXIF=?v<{B#n7}cCV*>xj1PI-0%m5r6F@Um^+#h!833mS#FfzDtwN}q5>VE*Q Cnev_h literal 0 HcmV?d00001 diff --git a/assets/cn/freebies/MAIL_SELECT_GEMS.png b/assets/cn/freebies/MAIL_SELECT_GEMS.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7270022f903e8d0aa5fc53e437b5db4c86f15a GIT binary patch literal 5772 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%0D?F2a7G3iL0DZ*nhv#(ztu8RLwf{KZ5`9 AZU6uP literal 0 HcmV?d00001 diff --git a/assets/cn/freebies/MAIL_SELECT_MERIT.png b/assets/cn/freebies/MAIL_SELECT_MERIT.png new file mode 100644 index 0000000000000000000000000000000000000000..117fdbd3b0728593090b20416cc06313e15a38ad GIT binary patch literal 6176 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 literal 0 HcmV?d00001 diff --git a/assets/cn/freebies/MAIL_SELECT_OIL.png b/assets/cn/freebies/MAIL_SELECT_OIL.png new file mode 100644 index 0000000000000000000000000000000000000000..bd02ba74788df4a2ecd32e5b9236817635226c22 GIT binary patch literal 5773 zcmeI0`8yPD_s4I84651b4Bjp z5+YhnbyzUT1m4tMkA`rrGN<$ws}UXWCUoFzS+hB+PJ}6vv+qGKpFs^s4yK7P|gIFe;a7ff?-VRC=3i2 zoy9;w0u{(QuYV4r^$9qbKSW4E2wy+}R!c;hhEPd`w*DZm4?)+{f}ljJWEL$4fX>p9 z>;yt85TGl--bF2BL|q`;*EoT(_l?HEsQ`hP-j#W+Pk{rM{*5!Cj7V-~jwm~Pp4zDUGyuFiy`K+NG?VA&W+vxc<~_;3n~M%T)_g=XgxALp>z+br z0PFQ0GRb~+c97b)mFjI^qX)(YkSxI6@f}-*5&D8#4BtX`;!!I5S_1inrfq8cXBik7 zSHzqO>t(0oHI5V}eJ0`~wvSH_M?%-Yy=<+l3PN$z!97Rsph`sIJds96&YLnXde|EM z;2`lr@04QIZAs22>sF6(rXt3zsE>%0E5hyRQ*4V5-Fu=Z zA!3yZ63=Ya7X-8-Hl?2OGNu4lS4DXg1%TAXog^&@D$tc#G7Ui80{87#X)vP}C;(Rq z9$tK_O?Ub;2bRE``Y%SxmXafB;ALzf6G z#&O&bX>Ri?fnappQ*7a)8&ZB8mlAGk?Iw@33CTa%e^RF}m-evw2K9GHpw`D%g1k>! z3jBKm6j^N18ATqwyfp&q=nq9@z1J2313>tbL?pUbj@jku569b2WAz*!Tk(pwav^7} zlq`~zko;Ev266Hc@*T<0bC7s6`!dw-5$hMv$mC>wDA{?2q7L;s!Y z^#F$7{1ZmGvKtYMNHXoYVPD)}e-OEgWU7vdzFH6iFTRPaZQ)4}=4z35UboU2^s}H| zlJt0P>||5DvuqSZyT=zKbKl5J*T38a>yqM=0Qq)qK zQdNWQ6)qJK75|b#Nev{Y6~7g(l_e56&^J#jPcm;y)k~EUNRL;>-@r%XM*@3(Y4jtP zP>b|i9b3$cW4-!YUcZaCV86(NJ!HKWNx=t&Fz%*R2K4+b7WN3p}$_T|J&1GyggjtxKRM)nh;U+U#H8^$Zgw`Pzs!icmP#u*gsa|;a%l*N|C z5Y}Mu_LAh1*Px}RDW7G|=^WJ@;)hoiITcMoIbP!&QyfVb`Yvpld6VRp_m>%#vzJe; zT-tWr7TfmP?&0q|%f>%=IpA`bUqE1Ikf(3mQpu#^S4!PJApKGCi-lh_)^jb2J zEi)(+X69A)q|Ez8C^3DOcNO3FzJIGY=TpvT#SgFVW1Z^F;jj0MrE~b&ZNI9MwSTWs ztfuaDPUn^7lzrJuo|mx;va`0^s~0eNmn)t>DC^ZW(quDHPrUD-s-f<@+r3Dw(V_u) z5&9ywp|n9{T)jawVlW~rf^I*5@A5A3An8xzBx-|e{W`4!)n}?%s``h#58qLHPz%yz z(Y&NnqD`RvbV}y52B*$xdM0Z@zf>OrY=dtHsW9p)ncd6T0nOvQ#kVWlDQ+Y%cqTEX z2RhCg;ZwR}wAlFp8aJWu8HW>e^UgmYZDC|@r9>qd=Pz`X#9AS6&JJkLIgZW>!$8T^Uu`El=u)TOlC zwA^9@lUqiO#E@+5`C1bf35isu8PuFt2VZJz2W3Wd$A5E#(=*zrx3~sTY0AuHB=brl z#D?1@1P8B&-}hD}Uemh~#ukzhh2z&m6F^ zcyJMxZjbfFzN-B5_9|{}%JRDXK-d<_i$wZqSlh9(1Yasy^2SoE8{Zh#yK4`8i&b{lORnyhcMXlthv|`iw0Xwo$s9d}qn)kq#XmL>Sy0+nmIBBqP zEcpH=a}+=4<)7rV<+}G0A(kHXdf{8%eXEgUNxV*c_{O=2F`Ry|NppVE{g?OYw)MzM z?g2iYqi-4;tLeQE$_WB+Ro~ehv=1p~B|G@N@2!i>U$kdG|1(}1c7NJ*p|Jbw=-00C ztOvDQ&?PuZqEEFk5Gz&eRiIv2PhNOhoX2m+uisF66hW4oMh3PjC0XVdTZG~?B365? zLcLO5Kf?E568^@=c_dfnVCK||1j;S zKX$TDzf*re^r~n=Ql6k79}{o=#+~le$+GG)_MVOiizC#M|LghjgQ>l1Jy!d}Ym7Vb zJ&lzp%%>HRg?Kt7xlWT{lX7y>!2#dnBBUBjd+g>jksU?`s{JZuagXTg$$gP&DSl z4)vqZ5+$@6N0bBK(Q1mLv$-z*{z7WN{-xO;J}qzZd_%2Xz1re5CJ#@9plt zu6%D?vGq5e;G$@&P;W9B(3NNQ{AXFI2bS)yj52Z|1{c3KbP!D@1Xk<~g^xBD@?q%+ zL1S3}cpG2J2rcjL!3^>uPk(w>W-_@s{712cyFXlB=>IA<0uLf)F7?EIO!J%bHN=p2 y3y&z<5`XJF87Blz2%Hf3FB4E%k~vm9I-&+@hdHDoyyUOH2@LeEU#Zk_i2M&QkN4jI literal 0 HcmV?d00001 diff --git a/assets/en/freebies/MAIL_BATCH_CLAIM.png b/assets/en/freebies/MAIL_BATCH_CLAIM.png new file mode 100644 index 0000000000000000000000000000000000000000..975bc92a8a85626569943574bf076e930a0293ab GIT binary patch literal 9105 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 ko381mP4xfEKVLTn_-C5)5YD41Dg&%2~DDOOb@^ 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 G=YIg+zbwH3 literal 0 HcmV?d00001 diff --git a/assets/en/freebies/MAIL_MANAGE.png b/assets/en/freebies/MAIL_MANAGE.png new file mode 100644 index 0000000000000000000000000000000000000000..425d48beac2927dc1110ce691c427d705ba15ba5 GIT binary patch literal 9483 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 T-$ua{0&wQkg_9+gH}3xrEOdJE literal 0 HcmV?d00001 diff --git a/assets/en/freebies/MAIL_SELECT_COINS.png b/assets/en/freebies/MAIL_SELECT_COINS.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d17af29e8ff6a49174c0f6851e3f90b549c50c GIT binary patch literal 5773 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 Cnev_h literal 0 HcmV?d00001 diff --git a/assets/en/freebies/MAIL_SELECT_GEMS.png b/assets/en/freebies/MAIL_SELECT_GEMS.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7270022f903e8d0aa5fc53e437b5db4c86f15a GIT binary patch literal 5772 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%0D?F2a7G3iL0DZ*nhv#(ztu8RLwf{KZ5`9 AZU6uP literal 0 HcmV?d00001 diff --git a/assets/en/freebies/MAIL_SELECT_MERIT.png b/assets/en/freebies/MAIL_SELECT_MERIT.png new file mode 100644 index 0000000000000000000000000000000000000000..117fdbd3b0728593090b20416cc06313e15a38ad GIT binary patch literal 6176 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 literal 0 HcmV?d00001 diff --git a/assets/en/freebies/MAIL_SELECT_OIL.png b/assets/en/freebies/MAIL_SELECT_OIL.png new file mode 100644 index 0000000000000000000000000000000000000000..bd02ba74788df4a2ecd32e5b9236817635226c22 GIT binary patch literal 5773 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 literal 0 HcmV?d00001 diff --git a/assets/jp/freebies/MAIL_SELECT_COINS.png b/assets/jp/freebies/MAIL_SELECT_COINS.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d17af29e8ff6a49174c0f6851e3f90b549c50c GIT binary patch literal 5773 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 Cnev_h literal 0 HcmV?d00001 diff --git a/assets/jp/freebies/MAIL_SELECT_GEMS.png b/assets/jp/freebies/MAIL_SELECT_GEMS.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7270022f903e8d0aa5fc53e437b5db4c86f15a GIT binary patch literal 5772 zcmeI0f*V^9ELb@gEQpms51@vs-w$A~`5ERwL0;|#7N z!W9)p_~P{7EuD2}GB)@W1wYdQzR7{I6@Ybelz%X?{N^E~%ma`C&q0av%zSlvw>Y= zvZtOC%t*xuvA`F}b$aOl;M(i|dVK9>{Nlpg^n(4OD`C4e=h$W4Lr_^@WAeE6B^d=^ zy4yz>wVIzFBKK^El>0Qe6q$ojW8gTsXDK^IoqeC?Tfkm4>e`_SZ`ND=4n>}GG_2sXEpl&+GlxD$^Ohq;mD8|MVT9siIXPGM+4O0q zs;|=5=ohD$EuJoW!p>N8WHoWrMo?3zc0gHd1Qepq+QyCu_CjH4u@r(faJg6LJpQF9 z)_a1j9bS24MQY9omT;j>aWB^EF%~NO@naqQGA|Ba)M`p6J*~V){)5a%<#Qn)*Ne7n z?|!5lqa`{e$EBaEidPB!DW{8R3iQDUhR@y;ldF&2!bP3awEyI@CGm5qU|>+VYQX&QrX5 zBcqm3g4ulZ)RJ zuNZPJu`dZOxjGs!T0d&L>b1(gx;#qo@yt}o6wBOEa8n@qP}f|kxmOcjGv?FxNBO(m zGHQu>r)!5{=~utzj@x$b4(tzMh|te>RZ0$eC7#n&`kLK(cf8$im)B0X{`F_mg=g`O zH3=<_x&2zX7jPnLkq)!+{xD^Gtj9T9tlY$-aTi4!TPK;Zyq3Jyx%x{zNiOO3HT_lJ zk=@-LV&m8mZ0Abs^}!66ESvhDwPS}d;qNpuTD7CEM7O7mrUc?CQ*={^*w@C&#wg<* z<6*4Ukj1jtvfGe}t3J00h7O~E!G9_&!IZT4V%#QKXISGd4P4qWa37UkIb5MxNnbg$ zdVSYsS9sTLw~wd$95c_*4djhLFQiYmud8S6a^AFDb5ToC^OA`Jx5HcS%q@9X{+)O{ zbEYRJYfO zrZbPbXEO^h1x;J=i;@p~ADBHjsN>c9kRg&aB<0pI)?z+Yhks(Npse(0zjujTxlNhu zO~9MT`uuvqNu_$B;Gy8OV5-BcgB$z!qqx5f)2L1Mjk}cAkgt$=NZnJerys~&$oVMJ zDBe-YQ^rt!IU`A@%%(<1O>f5MmFUq3+vMJ}lO1;uOYdjvf@ZSa=iZm<7SZ7yIvX3& z2c2XJ_Q>DUS?c}-jhfPQjl%Icai+ocf>CU;cB;n!J%=*xTpS+e>FBznUO5#saK!RwT!eVJ)iX^TXapA&6rC! zOhMX}&X4DOhAk}h;*#EA=5Z{_u8);LF)UC5qh_M+GMHq5Td9A&R3s>wsehe4NYMCem?#wemyB8 zDI*s_@03y{IxJOvv0BewR5X!(4z=Lc#hn=0MVu4b^WGX`a}C$&&#gj~>NBwGNEV9v zn{$}^*LBljNtsuHl_p{G`_G4MH|5UB&017H4pur z)(3*{j`+%yBmEgugwdumYJ4*m5jODdow{hKiCl6~QYYTMbo>5iJ2so-heibF)IoFO zkjt=SE37BBu=H>FZQR0)$z7|#z#Wv^=;(xYb=T@Ld^vB~9ZNK8DATTURvr8nu})vY zy%96dZRMstfqWa_yc63~)zj5OE^i~hYTo?`JGS}uO73nzW{4%;_^9N4b^T9~(V>Q4 zeowX-!g$zjOc0V*YCnqlo4C|z1nsyFtcCuHJw`FB>3$zQ?_XwUQl0;LVKP7P$*lhGx4q5d%{`N8 zA=Nw3WjIQ7K%v10E1v6?t@O5z@cU(MCeH&N&HCz-V1o3lolm=boJm%8=0&BAy}8iD z*@A}!S($flD*Z?sM-g=n(_Ad?S=KqI_hsFLA=h-Ot%#jl6N_<{>$RTSf<)Uf;p#gj zN~KDg!JbFc@G^6c|FXmS(W5_9`;31=s*WojHM&jxwQjWTYc^vfFi=Nk9VeZ9 zkDMOR?A9CT6zNk#0U8tR}wil(ek$+gyI~GL15o#ve>-f?x9;zJcZ}sonwHJKTBPuTj z9v?~)pKToo54@2jO`H67aD*-Z0m1;F!vNSjA?YOme53&QV-0}9YXD##udG^clcv$n zTDMdU{YO`RRydETfcEJ-kEh+C@NCskB-7l{B)=t%StixR!mw%giycg149<>J>iM>F z!ZBS$-3#BuPgK{{1xN``<&?lFfl~sf1WpP3_Xw<)uIYgCwn{O9XMr8_4xcm1;BO3G zRyd_k+du{KlIWmdy>FG|F|ci?z#A7d8OJ!g)8|kkpK769^f5Lrm(&8m$jgka@52SC ztdO!#wGw@kS>C-voS1yG5aG5FVvqzG2T%dzuI-9_Zo?D44zSVE zk^*NX03dYhC+5N8r0&VoND9f1CT(~~j-)0F&XDd9(yrnEEkKF_00pV_0;o0G&5pi0y*gTdC`L~BUl~V$z1pb!^Of>V6%0D?F2a7G3iL0DZ*nhv#(ztu8RLwf{KZ5`9 AZU6uP literal 0 HcmV?d00001 diff --git a/assets/jp/freebies/MAIL_SELECT_MERIT.png b/assets/jp/freebies/MAIL_SELECT_MERIT.png new file mode 100644 index 0000000000000000000000000000000000000000..117fdbd3b0728593090b20416cc06313e15a38ad GIT binary patch literal 6176 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 literal 0 HcmV?d00001 diff --git a/assets/jp/freebies/MAIL_SELECT_OIL.png b/assets/jp/freebies/MAIL_SELECT_OIL.png new file mode 100644 index 0000000000000000000000000000000000000000..bd02ba74788df4a2ecd32e5b9236817635226c22 GIT binary patch literal 5773 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 literal 0 HcmV?d00001 diff --git a/assets/tw/freebies/MAIL_SELECT_COINS.png b/assets/tw/freebies/MAIL_SELECT_COINS.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d17af29e8ff6a49174c0f6851e3f90b549c50c GIT binary patch literal 5773 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 Cnev_h literal 0 HcmV?d00001 diff --git a/assets/tw/freebies/MAIL_SELECT_GEMS.png b/assets/tw/freebies/MAIL_SELECT_GEMS.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7270022f903e8d0aa5fc53e437b5db4c86f15a GIT binary patch literal 5772 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%0D?F2a7G3iL0DZ*nhv#(ztu8RLwf{KZ5`9 AZU6uP literal 0 HcmV?d00001 diff --git a/assets/tw/freebies/MAIL_SELECT_MERIT.png b/assets/tw/freebies/MAIL_SELECT_MERIT.png new file mode 100644 index 0000000000000000000000000000000000000000..117fdbd3b0728593090b20416cc06313e15a38ad GIT binary patch literal 6176 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 literal 0 HcmV?d00001 diff --git a/assets/tw/freebies/MAIL_SELECT_OIL.png b/assets/tw/freebies/MAIL_SELECT_OIL.png new file mode 100644 index 0000000000000000000000000000000000000000..bd02ba74788df4a2ecd32e5b9236817635226c22 GIT binary patch literal 5773 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 literal 0 HcmV?d00001 diff --git a/module/freebies/assets.py b/module/freebies/assets.py index ab9a70285b..6e9e4cd225 100644 --- a/module/freebies/assets.py +++ b/module/freebies/assets.py @@ -9,6 +9,8 @@ DATA_KEY_COLLECT = Button(area={'cn': (251, 38, 339, 73), 'en': (256, 42, 337, 68), 'jp': (254, 40, 340, 72), 'tw': (251, 38, 339, 73)}, color={'cn': (144, 116, 77), 'en': (145, 109, 72), 'jp': (144, 111, 69), 'tw': (144, 116, 77)}, button={'cn': (251, 38, 339, 73), 'en': (256, 42, 337, 68), 'jp': (254, 40, 340, 72), 'tw': (251, 38, 339, 73)}, file={'cn': './assets/cn/freebies/DATA_KEY_COLLECT.png', 'en': './assets/en/freebies/DATA_KEY_COLLECT.png', 'jp': './assets/jp/freebies/DATA_KEY_COLLECT.png', 'tw': './assets/tw/freebies/DATA_KEY_COLLECT.png'}) DATA_KEY_COLLECTED = Button(area={'cn': (251, 38, 339, 73), 'en': (255, 42, 338, 68), 'jp': (254, 41, 340, 71), 'tw': (251, 38, 339, 73)}, color={'cn': (102, 103, 103), 'en': (113, 113, 115), 'jp': (102, 103, 103), 'tw': (102, 103, 103)}, button={'cn': (251, 38, 339, 73), 'en': (255, 42, 338, 68), 'jp': (254, 41, 340, 71), 'tw': (251, 38, 339, 73)}, file={'cn': './assets/cn/freebies/DATA_KEY_COLLECTED.png', 'en': './assets/en/freebies/DATA_KEY_COLLECTED.png', 'jp': './assets/jp/freebies/DATA_KEY_COLLECTED.png', 'tw': './assets/tw/freebies/DATA_KEY_COLLECTED.png'}) FREE_SUPPLY_PACK = Button(area={'cn': (525, 533, 579, 560), 'en': (523, 533, 582, 553), 'jp': (523, 530, 583, 559), 'tw': (524, 532, 582, 562)}, color={'cn': (144, 154, 164), 'en': (150, 160, 169), 'jp': (123, 137, 148), 'tw': (130, 143, 154)}, button={'cn': (378, 155, 577, 352), 'en': (426, 181, 557, 319), 'jp': (373, 177, 583, 356), 'tw': (388, 194, 554, 352)}, file={'cn': './assets/cn/freebies/FREE_SUPPLY_PACK.png', 'en': './assets/en/freebies/FREE_SUPPLY_PACK.png', 'jp': './assets/jp/freebies/FREE_SUPPLY_PACK.png', 'tw': './assets/tw/freebies/FREE_SUPPLY_PACK.png'}) +MAIL_BATCH_CLAIM = Button(area={'cn': (593, 524, 687, 546), 'en': (643, 525, 704, 543), 'jp': (593, 524, 687, 546), 'tw': (593, 524, 687, 546)}, color={'cn': (114, 209, 255), 'en': (147, 220, 255), 'jp': (114, 209, 255), 'tw': (114, 209, 255)}, button={'cn': (593, 524, 687, 546), 'en': (643, 525, 704, 543), 'jp': (593, 524, 687, 546), 'tw': (593, 524, 687, 546)}, file={'cn': './assets/cn/freebies/MAIL_BATCH_CLAIM.png', 'en': './assets/en/freebies/MAIL_BATCH_CLAIM.png', 'jp': './assets/cn/freebies/MAIL_BATCH_CLAIM.png', 'tw': './assets/cn/freebies/MAIL_BATCH_CLAIM.png'}) +MAIL_BATCH_DELETE = Button(area={'cn': (770, 523, 865, 547), 'en': (817, 526, 887, 544), 'jp': (770, 523, 865, 547), 'tw': (770, 523, 865, 547)}, color={'cn': (112, 209, 255), 'en': (150, 221, 255), 'jp': (112, 209, 255), 'tw': (112, 209, 255)}, button={'cn': (770, 523, 865, 547), 'en': (817, 526, 887, 544), 'jp': (770, 523, 865, 547), 'tw': (770, 523, 865, 547)}, file={'cn': './assets/cn/freebies/MAIL_BATCH_DELETE.png', 'en': './assets/en/freebies/MAIL_BATCH_DELETE.png', 'jp': './assets/cn/freebies/MAIL_BATCH_DELETE.png', 'tw': './assets/cn/freebies/MAIL_BATCH_DELETE.png'}) MAIL_COLLECT = Button(area={'cn': (841, 577, 970, 608), 'en': (865, 583, 947, 601), 'jp': (842, 575, 964, 609), 'tw': (838, 575, 973, 611)}, color={'cn': (155, 184, 219), 'en': (151, 180, 216), 'jp': (116, 154, 203), 'tw': (145, 174, 212)}, button={'cn': (841, 577, 970, 608), 'en': (865, 583, 947, 601), 'jp': (842, 575, 964, 609), 'tw': (838, 575, 973, 611)}, file={'cn': './assets/cn/freebies/MAIL_COLLECT.png', 'en': './assets/en/freebies/MAIL_COLLECT.png', 'jp': './assets/jp/freebies/MAIL_COLLECT.png', 'tw': './assets/tw/freebies/MAIL_COLLECT.png'}) MAIL_COLLECTED = Button(area={'cn': (893, 578, 986, 607), 'en': (835, 578, 975, 606), 'jp': (861, 575, 951, 608), 'tw': (891, 576, 987, 609)}, color={'cn': (55, 61, 70), 'en': (54, 63, 71), 'jp': (48, 57, 65), 'tw': (55, 62, 72)}, button={'cn': (893, 578, 986, 607), 'en': (835, 578, 975, 606), 'jp': (861, 575, 951, 608), 'tw': (891, 576, 987, 609)}, file={'cn': './assets/cn/freebies/MAIL_COLLECTED.png', 'en': './assets/en/freebies/MAIL_COLLECTED.png', 'jp': './assets/jp/freebies/MAIL_COLLECTED.png', 'tw': './assets/tw/freebies/MAIL_COLLECTED.png'}) MAIL_DELETE = Button(area={'cn': (176, 560, 306, 590), 'en': (428, 567, 500, 584), 'jp': (177, 556, 307, 591), 'tw': (175, 559, 308, 592)}, color={'cn': (221, 171, 166), 'en': (216, 173, 169), 'jp': (210, 151, 146), 'tw': (217, 166, 162)}, button={'cn': (176, 560, 306, 590), 'en': (428, 567, 500, 584), 'jp': (177, 556, 307, 591), 'tw': (175, 559, 308, 592)}, file={'cn': './assets/cn/freebies/MAIL_DELETE.png', 'en': './assets/en/freebies/MAIL_DELETE.png', 'jp': './assets/jp/freebies/MAIL_DELETE.png', 'tw': './assets/tw/freebies/MAIL_DELETE.png'}) @@ -16,6 +18,12 @@ MAIL_EMPTY_2 = Button(area={'cn': (507, 364, 596, 391), 'en': (507, 364, 596, 391), 'jp': (507, 364, 596, 391), 'tw': (507, 364, 596, 391)}, color={'cn': (181, 185, 194), 'en': (181, 185, 194), 'jp': (181, 185, 194), 'tw': (181, 185, 194)}, button={'cn': (507, 364, 596, 391), 'en': (507, 364, 596, 391), 'jp': (507, 364, 596, 391), 'tw': (507, 364, 596, 391)}, file={'cn': './assets/cn/freebies/MAIL_EMPTY_2.png', 'en': './assets/en/freebies/MAIL_EMPTY_2.png', 'jp': './assets/jp/freebies/MAIL_EMPTY_2.png', 'tw': './assets/tw/freebies/MAIL_EMPTY_2.png'}) MAIL_ENTER = Button(area={'cn': (1207, 393, 1253, 429), 'en': (1207, 393, 1253, 429), 'jp': (1207, 393, 1253, 429), 'tw': (1207, 393, 1253, 429)}, color={'cn': (109, 107, 95), 'en': (109, 107, 95), 'jp': (109, 107, 95), 'tw': (109, 107, 95)}, button={'cn': (1207, 393, 1253, 429), 'en': (1207, 393, 1253, 429), 'jp': (1207, 393, 1253, 429), 'tw': (1207, 393, 1253, 429)}, file={'cn': './assets/cn/freebies/MAIL_ENTER.png', 'en': './assets/en/freebies/MAIL_ENTER.png', 'jp': './assets/jp/freebies/MAIL_ENTER.png', 'tw': './assets/tw/freebies/MAIL_ENTER.png'}) MAIL_GUILD_MESSAGE = Button(area={'cn': (412, 214, 461, 235), 'en': (412, 214, 461, 235), 'jp': (412, 214, 461, 235), 'tw': (412, 214, 461, 235)}, color={'cn': (123, 124, 126), 'en': (123, 124, 126), 'jp': (123, 124, 126), 'tw': (123, 124, 126)}, button={'cn': (412, 214, 461, 235), 'en': (412, 214, 461, 235), 'jp': (412, 214, 461, 235), 'tw': (412, 214, 461, 235)}, file={'cn': './assets/cn/freebies/MAIL_GUILD_MESSAGE.png', 'en': './assets/en/freebies/MAIL_GUILD_MESSAGE.png', 'jp': './assets/jp/freebies/MAIL_GUILD_MESSAGE.png', 'tw': './assets/tw/freebies/MAIL_GUILD_MESSAGE.png'}) +MAIL_MANAGE = Button(area={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (415, 639, 485, 658), 'tw': (415, 639, 485, 658)}, color={'cn': (116, 210, 255), 'en': (131, 214, 255), 'jp': (116, 210, 255), 'tw': (116, 210, 255)}, button={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (415, 639, 485, 658), 'tw': (415, 639, 485, 658)}, file={'cn': './assets/cn/freebies/MAIL_MANAGE.png', 'en': './assets/en/freebies/MAIL_MANAGE.png', 'jp': './assets/cn/freebies/MAIL_MANAGE.png', 'tw': './assets/cn/freebies/MAIL_MANAGE.png'}) +MAIL_SELECT_COINS = Button(area={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 401, 582, 421), 'tw': (562, 401, 582, 421)}, color={'cn': (241, 240, 241), 'en': (241, 240, 241), 'jp': (241, 240, 241), 'tw': (241, 240, 241)}, button={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 401, 582, 421), 'tw': (562, 401, 582, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_COINS.png', 'en': './assets/en/freebies/MAIL_SELECT_COINS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_COINS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_COINS.png'}) +MAIL_SELECT_CUBE = Button(area={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 401, 462, 421), 'tw': (442, 401, 462, 421)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (241, 241, 241), 'tw': (241, 241, 241)}, button={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 401, 462, 421), 'tw': (442, 401, 462, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_CUBE.png', 'en': './assets/en/freebies/MAIL_SELECT_CUBE.png', 'jp': './assets/jp/freebies/MAIL_SELECT_CUBE.png', 'tw': './assets/tw/freebies/MAIL_SELECT_CUBE.png'}) +MAIL_SELECT_GEMS = Button(area={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 441, 462, 461), 'tw': (442, 441, 462, 461)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (241, 241, 241), 'tw': (241, 241, 241)}, button={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 441, 462, 461), 'tw': (442, 441, 462, 461)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_GEMS.png', 'en': './assets/en/freebies/MAIL_SELECT_GEMS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_GEMS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_GEMS.png'}) +MAIL_SELECT_MERIT = Button(area={'cn': (802, 401, 822, 421), 'en': (802, 401, 822, 421), 'jp': (802, 401, 822, 421), 'tw': (802, 401, 822, 421)}, color={'cn': (87, 87, 88), 'en': (87, 87, 88), 'jp': (87, 87, 88), 'tw': (87, 87, 88)}, button={'cn': (802, 401, 822, 421), 'en': (802, 401, 822, 421), 'jp': (802, 401, 822, 421), 'tw': (802, 401, 822, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_MERIT.png', 'en': './assets/en/freebies/MAIL_SELECT_MERIT.png', 'jp': './assets/jp/freebies/MAIL_SELECT_MERIT.png', 'tw': './assets/tw/freebies/MAIL_SELECT_MERIT.png'}) +MAIL_SELECT_OIL = Button(area={'cn': (682, 401, 702, 421), 'en': (682, 401, 702, 421), 'jp': (682, 401, 702, 421), 'tw': (682, 401, 702, 421)}, color={'cn': (241, 240, 241), 'en': (241, 240, 241), 'jp': (241, 240, 241), 'tw': (241, 240, 241)}, button={'cn': (682, 401, 702, 421), 'en': (682, 401, 702, 421), 'jp': (682, 401, 702, 421), 'tw': (682, 401, 702, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_OIL.png', 'en': './assets/en/freebies/MAIL_SELECT_OIL.png', 'jp': './assets/jp/freebies/MAIL_SELECT_OIL.png', 'tw': './assets/tw/freebies/MAIL_SELECT_OIL.png'}) OCR_DATA_KEY = Button(area={'cn': (132, 42, 233, 70), 'en': (132, 42, 233, 70), 'jp': (132, 42, 233, 70), 'tw': (132, 42, 233, 70)}, color={'cn': (74, 75, 86), 'en': (74, 75, 86), 'jp': (74, 75, 86), 'tw': (74, 75, 86)}, button={'cn': (132, 42, 233, 70), 'en': (132, 42, 233, 70), 'jp': (132, 42, 233, 70), 'tw': (132, 42, 233, 70)}, file={'cn': './assets/cn/freebies/OCR_DATA_KEY.png', 'en': './assets/en/freebies/OCR_DATA_KEY.png', 'jp': './assets/jp/freebies/OCR_DATA_KEY.png', 'tw': './assets/tw/freebies/OCR_DATA_KEY.png'}) PURCHASE_POPUP = Button(area={'cn': (907, 204, 934, 229), 'en': (907, 204, 934, 229), 'jp': (907, 204, 934, 229), 'tw': (907, 204, 934, 229)}, color={'cn': (176, 130, 110), 'en': (176, 130, 110), 'jp': (176, 130, 110), 'tw': (176, 130, 110)}, button={'cn': (907, 204, 934, 229), 'en': (907, 204, 934, 229), 'jp': (907, 204, 934, 229), 'tw': (907, 204, 934, 229)}, file={'cn': './assets/cn/freebies/PURCHASE_POPUP.png', 'en': './assets/en/freebies/PURCHASE_POPUP.png', 'jp': './assets/jp/freebies/PURCHASE_POPUP.png', 'tw': './assets/tw/freebies/PURCHASE_POPUP.png'}) REWARD_RECEIVE = Button(area={'cn': (1192, 520, 1255, 536), 'en': (1192, 522, 1254, 534), 'jp': (1186, 518, 1259, 536), 'tw': (1192, 520, 1255, 536)}, color={'cn': (191, 178, 163), 'en': (195, 182, 168), 'jp': (208, 197, 183), 'tw': (191, 178, 163)}, button={'cn': (1192, 520, 1255, 536), 'en': (1192, 522, 1254, 534), 'jp': (1186, 518, 1259, 536), 'tw': (1192, 520, 1255, 536)}, file={'cn': './assets/cn/freebies/REWARD_RECEIVE.png', 'en': './assets/en/freebies/REWARD_RECEIVE.png', 'jp': './assets/jp/freebies/REWARD_RECEIVE.png', 'tw': './assets/cn/freebies/REWARD_RECEIVE.png'}) diff --git a/module/freebies/mail_white.py b/module/freebies/mail_white.py new file mode 100644 index 0000000000..dc99cd8ee1 --- /dev/null +++ b/module/freebies/mail_white.py @@ -0,0 +1,221 @@ +from module.base.decorator import cached_property +from module.combat.assets import GET_ITEMS_1, GET_ITEMS_2 +from module.freebies.assets import * +from module.logger import logger +from module.ui.page import GOTO_MAIN_WHITE, page_mail, page_main +from module.ui.setting import Setting +from module.ui.ui import UI + + +class MailSelectSetting(Setting): + def is_option_active(self, option: Button) -> bool: + return self.main.image_color_count(option, color=(57, 56, 57), threshold=221, count=50) + + +class MailWhite(UI): + @cached_property + def mail_select_setting(self): + setting = MailSelectSetting('Mail', main=self) + setting.reset_first = False + setting.need_deselect = True + setting.add_setting( + setting='contains', + option_buttons=[MAIL_SELECT_CUBE, MAIL_SELECT_COINS, MAIL_SELECT_OIL, MAIL_SELECT_MERIT, MAIL_SELECT_GEMS], + option_names=['cube', 'coins', 'oil', 'merit', 'gems'], + option_default='merit' + ) + return setting + + def _mail_enter(self, skip_first_screenshot=True): + """ + Page: + in: page_main_white or MAIL_MANAGE + out: MAIL_BATCH_CLAIM + """ + logger.info('Mail enter') + self.interval_clear([ + MAIL_MANAGE + ]) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.appear(MAIL_BATCH_CLAIM, offset=(20, 20)): + logger.info('Mail entered') + break + + # Click + if self.appear_then_click(MAIL_MANAGE, offset=(30, 30), interval=3): + continue + if self.ui_main_appear_then_click(page_mail, offset=(30, 30), interval=3): + continue + + def _mail_quit(self, skip_first_screenshot=True): + """ + Page: + in: Any page in page_mail + out: page_main_white + """ + logger.info('Mail quit') + self.interval_clear([ + MAIL_BATCH_CLAIM, + GOTO_MAIN_WHITE, + GET_ITEMS_1, + GET_ITEMS_2, + ]) + self.popup_interval_clear() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.ui_page_appear(page_main): + logger.info('Mail quit to page_main') + break + + # Click + if self.handle_popup_confirm('MAIL_QUIT'): + continue + if self.appear(MAIL_BATCH_CLAIM, offset=(30, 30), interval=3): + logger.info(f'{MAIL_BATCH_CLAIM} -> {MAIL_MANAGE}') + self.device.click(MAIL_MANAGE) + continue + if self.appear_then_click(GOTO_MAIN_WHITE, offset=(30, 30), interval=3): + continue + if self._handle_mail_reward(): + continue + + def _handle_mail_reward(self): + if self.appear(GET_ITEMS_1, offset=(30, 30), interval=3): + logger.info(f'{GET_ITEMS_1} -> {MAIL_BATCH_CLAIM}') + self.device.click(MAIL_BATCH_CLAIM) + return True + if self.appear(GET_ITEMS_2, offset=(30, 30), interval=3): + logger.info(f'{GET_ITEMS_2} -> {MAIL_BATCH_CLAIM}') + self.device.click(MAIL_BATCH_CLAIM) + return True + return False + + def _mail_claim_execute(self, skip_first_screenshot=True): + """ + Page: + in: MAIL_BATCH_CLAIM + out: page_main_white, may have info_bar + + Returns: + int: If success to claim + """ + self.handle_info_bar() + self.interval_clear([ + MAIL_BATCH_CLAIM, + GET_ITEMS_1, + GET_ITEMS_2, + ]) + self.popup_interval_clear() + + claimed = False + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if claimed and self.appear(MAIL_BATCH_CLAIM, offset=(30, 30)): + break + # Click + if not claimed and self.appear_then_click(MAIL_BATCH_CLAIM, offset=(30, 30), interval=3): + continue + if self.handle_popup_confirm('MAIL_CLAIM'): + claimed = True + continue + if self._handle_mail_reward(): + claimed = True + continue + + success = self.info_bar_count() > 0 + logger.info(f'Mail claim success: {success}') + return success + + def _mail_delete(self, skip_first_screenshot=True): + """ + Pages: + in: MAIL_BATCH_DELETE + out: MAIL_BATCH_DELETE + """ + self.handle_info_bar() + self.interval_clear([ + MAIL_BATCH_DELETE + ]) + self.popup_interval_clear() + + deleted = False + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if deleted and self.appear(MAIL_BATCH_DELETE, offset=(30, 30)): + break + # Click + if not deleted and self.appear_then_click(MAIL_BATCH_DELETE, offset=(30, 30), interval=3): + continue + if self.handle_popup_confirm('MAIL_CLAIM'): + deleted = True + continue + + # info_bar appears if mail success to delete and no mail deleted + return True + + def mail_claim( + self, + merit=True, + maintenance=True, + trade_license=True, + delete=True, + ): + """ + Pages: + in: page_main_white or MAIL_MANAGE + out: MAIL_BATCH_CLAIM + """ + if merit: + logger.hr('Mail merit', level=2) + self._mail_enter() + self.mail_select_setting.set(contains=['merit']) + self._mail_claim_execute() + if maintenance: + logger.hr('Mail maintenance', level=2) + self._mail_enter() + self.mail_select_setting.set(contains=['coins', 'oil']) + self._mail_claim_execute() + self._mail_enter() + self.mail_select_setting.set(contains=['coins', 'oil', 'gems']) + self._mail_claim_execute() + if trade_license: + logger.hr('Mail trade license', level=2) + self._mail_enter() + self.mail_select_setting.set(contains=['coins', 'oil', 'cube']) + self._mail_claim_execute() + if delete: + logger.hr('Mail delete', level=2) + self._mail_enter() + self._mail_delete() + + self._mail_quit() + + def run(self): + pass + + +if __name__ == '__main__': + self = MailWhite('alas') + self.device.screenshot() + self.mail_claim() diff --git a/module/ui/page.py b/module/ui/page.py index 64bdbe9e9d..c6dee50231 100644 --- a/module/ui/page.py +++ b/module/ui/page.py @@ -284,7 +284,7 @@ def link(self, button, destination): page_mail = Page(MAIL_CHECK) page_mail.link(button=GOTO_MAIN_WHITE, destination=page_main) # Mail enter varies from different UI -# page_main.link(button=MAIL_ENTER_WHITE, destination=page_mail) +page_main_white.link(button=MAIL_ENTER_WHITE, destination=page_mail) # RPG event (raid_20240328) # page_rpg_stage = Page(RPG_GOTO_STORY) diff --git a/module/ui/setting.py b/module/ui/setting.py index 9be158735e..7a87fd7ef7 100644 --- a/module/ui/setting.py +++ b/module/ui/setting.py @@ -16,6 +16,8 @@ def __init__(self, name='Setting', main: ModuleBase = None): self.main: ModuleBase = main # Reset options before setting any options self.reset_first = True + # Deselect active options + self.need_deselect = False # (setting, opiton_name): option_button # { # ('sort', 'rarity'): Button(), @@ -108,6 +110,9 @@ def get_buttons_to_click(self, status: t.Dict[Button, bool]) -> t.List[Button]: active = self.is_option_active(option_button) if enable and not active: click.append(option_button) + if self.need_deselect: + if not enable and active: + click.append(option_button) return click def _set_execute(self, **kwargs): From e66b2a84b0eb6211bebbf63b96811cd7ca842a35 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 18 Jul 2024 22:48:14 +0800 Subject: [PATCH 120/161] Add: Claim mails in task freebies - TODO: [JP][TW] Update mail assets --- config/template.json | 7 +++-- module/config/argument/args.json | 14 ++++++--- module/config/argument/argument.yaml | 8 ++--- module/config/config_generated.py | 7 +++-- module/config/i18n/en-US.json | 18 ++++++----- module/config/i18n/ja-JP.json | 22 +++++++------ module/config/i18n/zh-CN.json | 16 ++++++---- module/config/i18n/zh-TW.json | 16 ++++++---- module/freebies/freebies.py | 7 ++--- module/freebies/mail_white.py | 46 ++++++++++++++++++++++------ 10 files changed, 104 insertions(+), 57 deletions(-) diff --git a/config/template.json b/config/template.json index f88dfc4a84..6d1e2467de 100644 --- a/config/template.json +++ b/config/template.json @@ -1531,9 +1531,10 @@ "ForceCollect": false }, "Mail": { - "Collect": true, - "Filter": "Merit > Coolant", - "Delete": false + "ClaimMerit": true, + "ClaimMaintenance": false, + "ClaimTradeLicense": false, + "DeleteCollected": true }, "SupplyPack": { "Collect": true, diff --git a/module/config/argument/args.json b/module/config/argument/args.json index b3b1b78032..11b474b9f7 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -8085,17 +8085,21 @@ } }, "Mail": { - "Collect": { + "ClaimMerit": { "type": "checkbox", "value": true }, - "Filter": { - "type": "textarea", - "value": "Merit > Coolant" + "ClaimMaintenance": { + "type": "checkbox", + "value": false }, - "Delete": { + "ClaimTradeLicense": { "type": "checkbox", "value": false + }, + "DeleteCollected": { + "type": "checkbox", + "value": true } }, "SupplyPack": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 5a0a675bce..7063ee6d8d 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -562,10 +562,10 @@ DataKey: Collect: true ForceCollect: false Mail: - Collect: true - Filter: |- - Merit > Coolant - Delete: false + ClaimMerit: true + ClaimMaintenance: false + ClaimTradeLicense: false + DeleteCollected: true SupplyPack: Collect: true DayOfWeek: diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 89febe56cf..4c594abde5 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -315,9 +315,10 @@ class GeneratedConfig: DataKey_ForceCollect = False # Group `Mail` - Mail_Collect = True - Mail_Filter = 'Merit > Coolant' - Mail_Delete = False + Mail_ClaimMerit = True + Mail_ClaimMaintenance = False + Mail_ClaimTradeLicense = False + Mail_DeleteCollected = True # Group `SupplyPack` SupplyPack_Collect = True diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 41ea91eb9b..6a7a7a53ab 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -1926,15 +1926,19 @@ "name": "Mail Settings", "help": "" }, - "Collect": { - "name": "Collect Mail Rewards", - "help": "Only collects the top 3 mail entries" + "ClaimMerit": { + "name": "Claim Merit Mails", + "help": "" }, - "Filter": { - "name": "Mail Filter", - "help": "Filter as to which mail types should be collected\nSupports Merit, Coolant, Coin, DecorCoin, Cube, Oil, and/or Gem" + "ClaimMaintenance": { + "name": "Claim Maintenance Mails", + "help": "Mail.ClaimMaintenance.help" + }, + "ClaimTradeLicense": { + "name": "Claim Trade License Mails", + "help": "" }, - "Delete": { + "DeleteCollected": { "name": "Delete Collected Mail Rewards", "help": "Delete the already collected mail entries\nMark any entries \"important\" in-game to avoid accidental deletion" } diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index f55e7768ac..2d4bf052bb 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -1926,17 +1926,21 @@ "name": "Mail._info.name", "help": "Mail._info.help" }, - "Collect": { - "name": "Mail.Collect.name", - "help": "Mail.Collect.help" + "ClaimMerit": { + "name": "Mail.ClaimMerit.name", + "help": "Mail.ClaimMerit.help" }, - "Filter": { - "name": "Mail.Filter.name", - "help": "Mail.Filter.help" + "ClaimMaintenance": { + "name": "Mail.ClaimMaintenance.name", + "help": "Mail.ClaimMaintenance.help" + }, + "ClaimTradeLicense": { + "name": "Mail.ClaimTradeLicense.name", + "help": "Mail.ClaimTradeLicense.help" }, - "Delete": { - "name": "Mail.Delete.name", - "help": "Mail.Delete.help" + "DeleteCollected": { + "name": "Mail.DeleteCollected.name", + "help": "Mail.DeleteCollected.help" } }, "SupplyPack": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index a303dbbf99..66263256f8 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -1926,15 +1926,19 @@ "name": "领取邮件", "help": "" }, - "Collect": { - "name": "领取邮件", + "ClaimMerit": { + "name": "领取功勋邮件", "help": "" }, - "Filter": { - "name": "邮件过滤器", - "help": "默认只领取演习功勋邮件" + "ClaimMaintenance": { + "name": "领取维护邮件", + "help": "" + }, + "ClaimTradeLicense": { + "name": "领取月卡邮件", + "help": "" }, - "Delete": { + "DeleteCollected": { "name": "删除已读邮件", "help": "" } diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 2548f67d20..bd704a5f75 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -1926,15 +1926,19 @@ "name": "領取郵件", "help": "" }, - "Collect": { - "name": "領取郵件", + "ClaimMerit": { + "name": "領取功勳郵件", "help": "" }, - "Filter": { - "name": "郵件過濾器", - "help": "默認只領取演習功勳郵件" + "ClaimMaintenance": { + "name": "領取維护郵件", + "help": "" + }, + "ClaimTradeLicense": { + "name": "領取月卡郵件", + "help": "" }, - "Delete": { + "DeleteCollected": { "name": "刪除已讀郵件", "help": "" } diff --git a/module/freebies/freebies.py b/module/freebies/freebies.py index 1f836fc948..19f02f52f1 100644 --- a/module/freebies/freebies.py +++ b/module/freebies/freebies.py @@ -1,7 +1,7 @@ from module.base.base import ModuleBase from module.freebies.battle_pass import BattlePass from module.freebies.data_key import DataKey -from module.freebies.mail import Mail +from module.freebies.mail_white import MailWhite from module.freebies.supply_pack import SupplyPack from module.logger import logger @@ -19,9 +19,8 @@ def run(self): logger.hr('Data key', level=1) DataKey(self.config, self.device).run() - # if self.config.Mail_Collect: - # logger.hr('Mail', level=1) - # Mail(self.config, self.device).run() + logger.hr('Mail', level=1) + MailWhite(self.config, self.device).run() if self.config.SupplyPack_Collect: logger.hr('Supply pack', level=1) diff --git a/module/freebies/mail_white.py b/module/freebies/mail_white.py index dc99cd8ee1..42f1feeebe 100644 --- a/module/freebies/mail_white.py +++ b/module/freebies/mail_white.py @@ -2,7 +2,7 @@ from module.combat.assets import GET_ITEMS_1, GET_ITEMS_2 from module.freebies.assets import * from module.logger import logger -from module.ui.page import GOTO_MAIN_WHITE, page_mail, page_main +from module.ui.page import GOTO_MAIN_WHITE, page_mail, page_main, page_main_white from module.ui.setting import Setting from module.ui.ui import UI @@ -177,8 +177,8 @@ def _mail_delete(self, skip_first_screenshot=True): def mail_claim( self, merit=True, - maintenance=True, - trade_license=True, + maintenance=False, + trade_license=False, delete=True, ): """ @@ -212,10 +212,36 @@ def mail_claim( self._mail_quit() def run(self): - pass - - -if __name__ == '__main__': - self = MailWhite('alas') - self.device.screenshot() - self.mail_claim() + merit = self.config.Mail_ClaimMerit + maintenance = self.config.Mail_ClaimMaintenance + trade_license = self.config.Mail_ClaimTradeLicense + delete = self.config.Mail_DeleteCollected + logger.info(f'Mail reward: merit={merit}, maintenance={maintenance}, ' + f'trade_license={trade_license}, delete={delete}') + if not merit and not maintenance and not trade_license: + logger.warning('Nothing to claim') + return False + if self.config.SERVER not in ['cn', 'en']: + logger.warning(f'Mail is not supported in {self.config.SERVER}, please contact server maintainers') + return False + + # Must using white UI + self.ui_ensure(page_main) + if self.appear(page_main_white.check_button, offset=(30, 30)): + logger.info('At page_main_white') + pass + elif self.appear(page_main.check_button, offset=(5, 5)): + logger.warning('At page_main, cannot enter mail page from old UI') + return False + else: + logger.warning('Unknown page_main, cannot enter mail page') + return False + + # Claim + self.mail_claim( + merit=merit, + maintenance=maintenance, + trade_license=trade_license, + delete=delete, + ) + return True From eeb516146c99c4f53205b978fb28f3b7926e3ef9 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:27:34 +0800 Subject: [PATCH 121/161] Revert "Fix: [TW] Keep SHIPYARD_SERIES_SELECT_CHECK in series 6" This reverts commit 9a920233 --- .../shipyard/SHIPYARD_SERIES_SELECT_CHECK.png | Bin 4450 -> 6360 bytes module/shipyard/assets.py | 2 +- module/shipyard/ui_globals.py | 12 +++--------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/assets/tw/shipyard/SHIPYARD_SERIES_SELECT_CHECK.png b/assets/tw/shipyard/SHIPYARD_SERIES_SELECT_CHECK.png index cb641717031af9b24697cdec7e271bfb732c02a2..ea6d536fca21a510b379e824fe11beb45d1ca040 100644 GIT binary patch delta 3813 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 Date: Tue, 9 Jul 2024 14:36:34 +0800 Subject: [PATCH 122/161] Add: scroll retire comfirm page to find more common cv --- .../cn/retire/RETIRE_CONFIRM_SCROLL_AREA.png | Bin 0 -> 7649 bytes .../en/retire/RETIRE_CONFIRM_SCROLL_AREA.png | Bin 0 -> 7649 bytes .../jp/retire/RETIRE_CONFIRM_SCROLL_AREA.png | Bin 0 -> 7649 bytes .../tw/retire/RETIRE_CONFIRM_SCROLL_AREA.png | Bin 0 -> 7649 bytes module/retire/assets.py | 1 + module/retire/retirement.py | 22 +++++++++++++++++- 6 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 assets/cn/retire/RETIRE_CONFIRM_SCROLL_AREA.png create mode 100644 assets/en/retire/RETIRE_CONFIRM_SCROLL_AREA.png create mode 100644 assets/jp/retire/RETIRE_CONFIRM_SCROLL_AREA.png create mode 100644 assets/tw/retire/RETIRE_CONFIRM_SCROLL_AREA.png diff --git a/assets/cn/retire/RETIRE_CONFIRM_SCROLL_AREA.png b/assets/cn/retire/RETIRE_CONFIRM_SCROLL_AREA.png new file mode 100644 index 0000000000000000000000000000000000000000..700269dd3923405f7cedad1b8e9f3b9d994eb00f GIT binary patch literal 7649 zcmeHLeNYtV8DBsSMNkw-6m4Bk^I>G~_V)Jf_ErumcfcKba>6AL2WD9I1GjJ=-mct% zL5&|+OeIa5Ni-FbSS6j6miiRL4)@HTjLQo`VhMyWA4!(XiD?1E=!e&)D z3&dQ?4|{zcjxT3nvC_-JY=Gw=DA3_@xZIJm$6xQCS2ZrdVyx@Gtp7Z=>iVP|wdX3P zQWs|Y{l<;2x24WZetGBqrpD~GTMMSc_2KmK#n{EnqBEOJMHcqm@OZO89IQEcu5}_( zA5~zA{`#7)rD4)0jdFI^vANDprQPpxG{ac|g z?Pg7HQDXIhmw&5`pS!mW>dTorIc{TH)braSxBq8G;U~|{eNj)owadTnn6dAp=XMk< zsPfN$aaD3b%|GLr&Zx_{s`b;;@29^xyTS3ric1lzw%purEm;3q(t$;l&tIGVNbQcu z?;D>DJKxi-Sd$+X|D}E#|MdA|y#M$HJTG%;PF{nsYu4itTdd!8!XE*tHt=S%!)7)Q zN(BVkUj35E`WHvS#+ic#m>vnC<*yHGk{v)}y<*UiB;dAGy>eiKg z@WJax-d_L0-lj=S>)>x9y8F6zUQ#~xuU8Y6wSCidCH3U09&cy$+EahP{?S((_BJ>D zb4y|S#rz|O=YJd5m=rOW`{J+dC66@p_Wm@rwKJpgm1}=aJNrg1I=w5tmR@W~?vCyL z#J;GfC1+*{Ynz?=L<9c&h22?ACsc=4sEo;9w*TrS``_X>{o+XOqbG{mkJn_Um}f3^ zZn?5?`-$itvQVT zGF00N6H#CC8;ZnVe;Rdjedk`?3#aqkx#*;?T0g#el!*H{a#`%T?^E|*J~N?fjiPB^ z-Jvt%K1;QE#C^x6p!;GA3;lTI#ts44Y9SpHw*q`T@6yYf(NNvU|&qq9+!$@ zJSD6u;PQghhM<(RfS01nSP?E^OL@0R+24Ff3G<9enXj{>cCVRT&ReT|Y+lt8CtX!W z8yRI<-xM@v$(j!c{1e5#TE@ zWok5>;$t{6*OECv0iI0C<)Y{%)oQ=rukveE9$%?iV>BAoD5l0R1Xv(KrCX!|h+CK= zQ3N?GtU&vCugH7cu*6A~cvgxgr4o$8gZ{a^cKZ;$TNqFQ=%Ef!UbRMrs$DMiNQ59} zQ~;6zhi;1yoS*`zb6LT&(nqry6|7sFGm?U#hw$E&zH&JnhE}uXtP7Y5U{=kjDYI;L z#}Go2ppdX!(xr{O&XEI8R-as5Alw&9*kW!23B@EY4Ol2CHHI=lTwxzuh;wYZoW&61d9^79#RDk_v_2k6)llw Rbi5RyEn|tLDZO~z{{ZUN{)+$r literal 0 HcmV?d00001 diff --git a/assets/en/retire/RETIRE_CONFIRM_SCROLL_AREA.png b/assets/en/retire/RETIRE_CONFIRM_SCROLL_AREA.png new file mode 100644 index 0000000000000000000000000000000000000000..700269dd3923405f7cedad1b8e9f3b9d994eb00f GIT binary patch literal 7649 zcmeHLeNYtV8DBsSMNkw-6m4Bk^I>G~_V)Jf_ErumcfcKba>6AL2WD9I1GjJ=-mct% zL5&|+OeIa5Ni-FbSS6j6miiRL4)@HTjLQo`VhMyWA4!(XiD?1E=!e&)D z3&dQ?4|{zcjxT3nvC_-JY=Gw=DA3_@xZIJm$6xQCS2ZrdVyx@Gtp7Z=>iVP|wdX3P zQWs|Y{l<;2x24WZetGBqrpD~GTMMSc_2KmK#n{EnqBEOJMHcqm@OZO89IQEcu5}_( zA5~zA{`#7)rD4)0jdFI^vANDprQPpxG{ac|g z?Pg7HQDXIhmw&5`pS!mW>dTorIc{TH)braSxBq8G;U~|{eNj)owadTnn6dAp=XMk< zsPfN$aaD3b%|GLr&Zx_{s`b;;@29^xyTS3ric1lzw%purEm;3q(t$;l&tIGVNbQcu z?;D>DJKxi-Sd$+X|D}E#|MdA|y#M$HJTG%;PF{nsYu4itTdd!8!XE*tHt=S%!)7)Q zN(BVkUj35E`WHvS#+ic#m>vnC<*yHGk{v)}y<*UiB;dAGy>eiKg z@WJax-d_L0-lj=S>)>x9y8F6zUQ#~xuU8Y6wSCidCH3U09&cy$+EahP{?S((_BJ>D zb4y|S#rz|O=YJd5m=rOW`{J+dC66@p_Wm@rwKJpgm1}=aJNrg1I=w5tmR@W~?vCyL z#J;GfC1+*{Ynz?=L<9c&h22?ACsc=4sEo;9w*TrS``_X>{o+XOqbG{mkJn_Um}f3^ zZn?5?`-$itvQVT zGF00N6H#CC8;ZnVe;Rdjedk`?3#aqkx#*;?T0g#el!*H{a#`%T?^E|*J~N?fjiPB^ z-Jvt%K1;QE#C^x6p!;GA3;lTI#ts44Y9SpHw*q`T@6yYf(NNvU|&qq9+!$@ zJSD6u;PQghhM<(RfS01nSP?E^OL@0R+24Ff3G<9enXj{>cCVRT&ReT|Y+lt8CtX!W z8yRI<-xM@v$(j!c{1e5#TE@ zWok5>;$t{6*OECv0iI0C<)Y{%)oQ=rukveE9$%?iV>BAoD5l0R1Xv(KrCX!|h+CK= zQ3N?GtU&vCugH7cu*6A~cvgxgr4o$8gZ{a^cKZ;$TNqFQ=%Ef!UbRMrs$DMiNQ59} zQ~;6zhi;1yoS*`zb6LT&(nqry6|7sFGm?U#hw$E&zH&JnhE}uXtP7Y5U{=kjDYI;L z#}Go2ppdX!(xr{O&XEI8R-as5Alw&9*kW!23B@EY4Ol2CHHI=lTwxzuh;wYZoW&61d9^79#RDk_v_2k6)llw Rbi5RyEn|tLDZO~z{{ZUN{)+$r literal 0 HcmV?d00001 diff --git a/assets/jp/retire/RETIRE_CONFIRM_SCROLL_AREA.png b/assets/jp/retire/RETIRE_CONFIRM_SCROLL_AREA.png new file mode 100644 index 0000000000000000000000000000000000000000..700269dd3923405f7cedad1b8e9f3b9d994eb00f GIT binary patch literal 7649 zcmeHLeNYtV8DBsSMNkw-6m4Bk^I>G~_V)Jf_ErumcfcKba>6AL2WD9I1GjJ=-mct% zL5&|+OeIa5Ni-FbSS6j6miiRL4)@HTjLQo`VhMyWA4!(XiD?1E=!e&)D z3&dQ?4|{zcjxT3nvC_-JY=Gw=DA3_@xZIJm$6xQCS2ZrdVyx@Gtp7Z=>iVP|wdX3P zQWs|Y{l<;2x24WZetGBqrpD~GTMMSc_2KmK#n{EnqBEOJMHcqm@OZO89IQEcu5}_( zA5~zA{`#7)rD4)0jdFI^vANDprQPpxG{ac|g z?Pg7HQDXIhmw&5`pS!mW>dTorIc{TH)braSxBq8G;U~|{eNj)owadTnn6dAp=XMk< zsPfN$aaD3b%|GLr&Zx_{s`b;;@29^xyTS3ric1lzw%purEm;3q(t$;l&tIGVNbQcu z?;D>DJKxi-Sd$+X|D}E#|MdA|y#M$HJTG%;PF{nsYu4itTdd!8!XE*tHt=S%!)7)Q zN(BVkUj35E`WHvS#+ic#m>vnC<*yHGk{v)}y<*UiB;dAGy>eiKg z@WJax-d_L0-lj=S>)>x9y8F6zUQ#~xuU8Y6wSCidCH3U09&cy$+EahP{?S((_BJ>D zb4y|S#rz|O=YJd5m=rOW`{J+dC66@p_Wm@rwKJpgm1}=aJNrg1I=w5tmR@W~?vCyL z#J;GfC1+*{Ynz?=L<9c&h22?ACsc=4sEo;9w*TrS``_X>{o+XOqbG{mkJn_Um}f3^ zZn?5?`-$itvQVT zGF00N6H#CC8;ZnVe;Rdjedk`?3#aqkx#*;?T0g#el!*H{a#`%T?^E|*J~N?fjiPB^ z-Jvt%K1;QE#C^x6p!;GA3;lTI#ts44Y9SpHw*q`T@6yYf(NNvU|&qq9+!$@ zJSD6u;PQghhM<(RfS01nSP?E^OL@0R+24Ff3G<9enXj{>cCVRT&ReT|Y+lt8CtX!W z8yRI<-xM@v$(j!c{1e5#TE@ zWok5>;$t{6*OECv0iI0C<)Y{%)oQ=rukveE9$%?iV>BAoD5l0R1Xv(KrCX!|h+CK= zQ3N?GtU&vCugH7cu*6A~cvgxgr4o$8gZ{a^cKZ;$TNqFQ=%Ef!UbRMrs$DMiNQ59} zQ~;6zhi;1yoS*`zb6LT&(nqry6|7sFGm?U#hw$E&zH&JnhE}uXtP7Y5U{=kjDYI;L z#}Go2ppdX!(xr{O&XEI8R-as5Alw&9*kW!23B@EY4Ol2CHHI=lTwxzuh;wYZoW&61d9^79#RDk_v_2k6)llw Rbi5RyEn|tLDZO~z{{ZUN{)+$r literal 0 HcmV?d00001 diff --git a/assets/tw/retire/RETIRE_CONFIRM_SCROLL_AREA.png b/assets/tw/retire/RETIRE_CONFIRM_SCROLL_AREA.png new file mode 100644 index 0000000000000000000000000000000000000000..700269dd3923405f7cedad1b8e9f3b9d994eb00f GIT binary patch literal 7649 zcmeHLeNYtV8DBsSMNkw-6m4Bk^I>G~_V)Jf_ErumcfcKba>6AL2WD9I1GjJ=-mct% zL5&|+OeIa5Ni-FbSS6j6miiRL4)@HTjLQo`VhMyWA4!(XiD?1E=!e&)D z3&dQ?4|{zcjxT3nvC_-JY=Gw=DA3_@xZIJm$6xQCS2ZrdVyx@Gtp7Z=>iVP|wdX3P zQWs|Y{l<;2x24WZetGBqrpD~GTMMSc_2KmK#n{EnqBEOJMHcqm@OZO89IQEcu5}_( zA5~zA{`#7)rD4)0jdFI^vANDprQPpxG{ac|g z?Pg7HQDXIhmw&5`pS!mW>dTorIc{TH)braSxBq8G;U~|{eNj)owadTnn6dAp=XMk< zsPfN$aaD3b%|GLr&Zx_{s`b;;@29^xyTS3ric1lzw%purEm;3q(t$;l&tIGVNbQcu z?;D>DJKxi-Sd$+X|D}E#|MdA|y#M$HJTG%;PF{nsYu4itTdd!8!XE*tHt=S%!)7)Q zN(BVkUj35E`WHvS#+ic#m>vnC<*yHGk{v)}y<*UiB;dAGy>eiKg z@WJax-d_L0-lj=S>)>x9y8F6zUQ#~xuU8Y6wSCidCH3U09&cy$+EahP{?S((_BJ>D zb4y|S#rz|O=YJd5m=rOW`{J+dC66@p_Wm@rwKJpgm1}=aJNrg1I=w5tmR@W~?vCyL z#J;GfC1+*{Ynz?=L<9c&h22?ACsc=4sEo;9w*TrS``_X>{o+XOqbG{mkJn_Um}f3^ zZn?5?`-$itvQVT zGF00N6H#CC8;ZnVe;Rdjedk`?3#aqkx#*;?T0g#el!*H{a#`%T?^E|*J~N?fjiPB^ z-Jvt%K1;QE#C^x6p!;GA3;lTI#ts44Y9SpHw*q`T@6yYf(NNvU|&qq9+!$@ zJSD6u;PQghhM<(RfS01nSP?E^OL@0R+24Ff3G<9enXj{>cCVRT&ReT|Y+lt8CtX!W z8yRI<-xM@v$(j!c{1e5#TE@ zWok5>;$t{6*OECv0iI0C<)Y{%)oQ=rukveE9$%?iV>BAoD5l0R1Xv(KrCX!|h+CK= zQ3N?GtU&vCugH7cu*6A~cvgxgr4o$8gZ{a^cKZ;$TNqFQ=%Ef!UbRMrs$DMiNQ59} zQ~;6zhi;1yoS*`zb6LT&(nqry6|7sFGm?U#hw$E&zH&JnhE}uXtP7Y5U{=kjDYI;L z#}Go2ppdX!(xr{O&XEI8R-as5Alw&9*kW!23B@EY4Ol2CHHI=lTwxzuh;wYZoW&61d9^79#RDk_v_2k6)llw Rbi5RyEn|tLDZO~z{{ZUN{)+$r literal 0 HcmV?d00001 diff --git a/module/retire/assets.py b/module/retire/assets.py index 2cf3da530a..25ad35ccae 100644 --- a/module/retire/assets.py +++ b/module/retire/assets.py @@ -27,6 +27,7 @@ RETIRE_APPEAR_2 = Button(area={'cn': (604, 501, 677, 533), 'en': (585, 496, 694, 527), 'jp': (604, 497, 677, 529), 'tw': (603, 501, 677, 533)}, color={'cn': (146, 178, 219), 'en': (146, 179, 220), 'jp': (136, 171, 215), 'tw': (145, 177, 218)}, button={'cn': (604, 501, 677, 533), 'en': (585, 496, 694, 527), 'jp': (604, 497, 677, 529), 'tw': (603, 501, 677, 533)}, file={'cn': './assets/cn/retire/RETIRE_APPEAR_2.png', 'en': './assets/en/retire/RETIRE_APPEAR_2.png', 'jp': './assets/jp/retire/RETIRE_APPEAR_2.png', 'tw': './assets/tw/retire/RETIRE_APPEAR_2.png'}) RETIRE_APPEAR_3 = Button(area={'cn': (804, 501, 876, 533), 'en': (776, 496, 904, 521), 'jp': (804, 497, 876, 529), 'tw': (804, 501, 877, 533)}, color={'cn': (148, 179, 219), 'en': (155, 184, 222), 'jp': (136, 170, 214), 'tw': (147, 179, 219)}, button={'cn': (804, 501, 876, 533), 'en': (776, 496, 904, 521), 'jp': (804, 497, 876, 529), 'tw': (804, 501, 877, 533)}, file={'cn': './assets/cn/retire/RETIRE_APPEAR_3.png', 'en': './assets/en/retire/RETIRE_APPEAR_3.png', 'jp': './assets/jp/retire/RETIRE_APPEAR_3.png', 'tw': './assets/tw/retire/RETIRE_APPEAR_3.png'}) RETIRE_COIN = Button(area={'cn': (307, 638, 351, 661), 'en': (361, 638, 401, 661), 'jp': (326, 637, 365, 662), 'tw': (307, 638, 351, 661)}, color={'cn': (150, 158, 165), 'en': (152, 157, 165), 'jp': (173, 176, 182), 'tw': (150, 158, 165)}, button={'cn': (307, 638, 351, 661), 'en': (361, 638, 401, 661), 'jp': (326, 637, 365, 662), 'tw': (307, 638, 351, 661)}, file={'cn': './assets/cn/retire/RETIRE_COIN.png', 'en': './assets/en/retire/RETIRE_COIN.png', 'jp': './assets/jp/retire/RETIRE_COIN.png', 'tw': './assets/tw/retire/RETIRE_COIN.png'}) +RETIRE_CONFIRM_SCROLL_AREA = Button(area={'cn': (1092, 114, 1095, 592), 'en': (1092, 114, 1095, 592), 'jp': (1092, 114, 1095, 592), 'tw': (1092, 114, 1095, 592)}, color={'cn': (255, 255, 255), 'en': (255, 255, 255), 'jp': (255, 255, 255), 'tw': (255, 255, 255)}, button={'cn': (1092, 114, 1095, 592), 'en': (1092, 114, 1095, 592), 'jp': (1092, 114, 1095, 592), 'tw': (1092, 114, 1095, 592)}, file={'cn': './assets/cn/retire/RETIRE_CONFIRM_SCROLL_AREA.png', 'en': './assets/en/retire/RETIRE_CONFIRM_SCROLL_AREA.png', 'jp': './assets/jp/retire/RETIRE_CONFIRM_SCROLL_AREA.png', 'tw': './assets/tw/retire/RETIRE_CONFIRM_SCROLL_AREA.png'}) RETIRE_SETTING_1 = Button(area={'cn': (818, 259, 847, 290), 'en': (989, 260, 1017, 289), 'jp': (965, 265, 996, 296), 'tw': (818, 259, 847, 290)}, color={'cn': (102, 122, 147), 'en': (61, 71, 92), 'jp': (61, 70, 91), 'tw': (102, 122, 147)}, button={'cn': (818, 259, 847, 290), 'en': (989, 260, 1017, 289), 'jp': (965, 265, 996, 296), 'tw': (818, 259, 847, 290)}, file={'cn': './assets/cn/retire/RETIRE_SETTING_1.png', 'en': './assets/en/retire/RETIRE_SETTING_1.png', 'jp': './assets/jp/retire/RETIRE_SETTING_1.png', 'tw': './assets/cn/retire/RETIRE_SETTING_1.png'}) RETIRE_SETTING_2 = Button(area={'cn': (746, 316, 775, 346), 'en': (990, 317, 1017, 345), 'jp': (965, 325, 996, 356), 'tw': (746, 316, 775, 346)}, color={'cn': (104, 124, 149), 'en': (62, 73, 93), 'jp': (62, 73, 91), 'tw': (104, 124, 149)}, button={'cn': (746, 316, 775, 346), 'en': (990, 317, 1017, 345), 'jp': (965, 325, 996, 356), 'tw': (746, 316, 775, 346)}, file={'cn': './assets/cn/retire/RETIRE_SETTING_2.png', 'en': './assets/en/retire/RETIRE_SETTING_2.png', 'jp': './assets/jp/retire/RETIRE_SETTING_2.png', 'tw': './assets/cn/retire/RETIRE_SETTING_2.png'}) RETIRE_SETTING_3 = Button(area={'cn': (894, 372, 923, 404), 'en': (990, 374, 1017, 402), 'jp': (965, 385, 996, 416), 'tw': (894, 372, 923, 404)}, color={'cn': (101, 120, 145), 'en': (107, 129, 155), 'jp': (103, 121, 143), 'tw': (101, 120, 145)}, button={'cn': (894, 372, 923, 404), 'en': (990, 374, 1017, 402), 'jp': (965, 385, 996, 416), 'tw': (894, 372, 923, 404)}, file={'cn': './assets/cn/retire/RETIRE_SETTING_3.png', 'en': './assets/en/retire/RETIRE_SETTING_3.png', 'jp': './assets/jp/retire/RETIRE_SETTING_3.png', 'tw': './assets/cn/retire/RETIRE_SETTING_3.png'}) diff --git a/module/retire/retirement.py b/module/retire/retirement.py index 505144b64f..3e1635598a 100644 --- a/module/retire/retirement.py +++ b/module/retire/retirement.py @@ -9,6 +9,7 @@ from module.retire.enhancement import Enhancement from module.retire.scanner import ShipScanner from module.retire.setting import QuickRetireSettingHandler +from module.ui.scroll import Scroll CARD_GRIDS = ButtonGrid( origin=(93, 76), delta=(164 + 2 / 3, 227), button_shape=(138, 204), grid_shape=(7, 2), name='CARD') @@ -23,6 +24,9 @@ # Not support marriage cards. } +RETIRE_CONFIRM_SCROLL = Scroll(RETIRE_CONFIRM_SCROLL_AREA, color=(74, 77, 110), name='STRATEGIC_SEARCH_SCROLL') +RETIRE_CONFIRM_SCROLL.color_threshold = 240 # Background color is (66, 72, 77), so default (256-221)=35 is not enough to dintinguish. + class Retirement(Enhancement, QuickRetireSettingHandler): _unable_to_enhance = False @@ -456,7 +460,7 @@ def _retire_select_one(self, button, skip_first_screenshot=True): return True return False - def retirement_get_common_rarity_cv(self): + def retirement_get_common_rarity_cv_in_page(self): """ Returns: Button: @@ -486,6 +490,22 @@ def retirement_get_common_rarity_cv(self): return None + def retirement_get_common_rarity_cv(self, skip_first_screenshot=False): + button = self.retirement_get_common_rarity_cv_in_page() + if button is not None: + return button + + while RETIRE_CONFIRM_SCROLL.appear(main=self): + RETIRE_CONFIRM_SCROLL.next_page(main=self) + button = self.retirement_get_common_rarity_cv_in_page() + if button is not None: + return button + if RETIRE_CONFIRM_SCROLL.at_bottom(main=self): + logger.info('Scroll bar reached end, stop') + break + + return button + def keep_one_common_cv(self): button = self.retirement_get_common_rarity_cv() if button is not None: From 11e3580c6172290dc2fb70e5fa3f54674a45bbd4 Mon Sep 17 00:00:00 2001 From: guoh064 <50830808+guoh064@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:36:34 +0800 Subject: [PATCH 123/161] Opt: always keep common cv at retirement if GemsFarming enabled --- module/campaign/gems_farming.py | 1 - module/config/config_manual.py | 1 - module/retire/retirement.py | 10 +++++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/module/campaign/gems_farming.py b/module/campaign/gems_farming.py index 845eaf4aa5..dc855f211e 100644 --- a/module/campaign/gems_farming.py +++ b/module/campaign/gems_farming.py @@ -400,7 +400,6 @@ def run(self, name, folder='campaign_main', mode='normal', total=0): total (int): """ self.config.STOP_IF_REACH_LV32 = self.change_flagship - self.config.RETIRE_KEEP_COMMON_CV = True while 1: self._trigger_lv32 = False diff --git a/module/config/config_manual.py b/module/config/config_manual.py index 22261d2051..cd795c7e86 100644 --- a/module/config/config_manual.py +++ b/module/config/config_manual.py @@ -353,7 +353,6 @@ def SERVER(self): """ DOCK_FULL_TRIGGERED = False GET_SHIP_TRIGGERED = False - RETIRE_KEEP_COMMON_CV = False COMMON_CV_THRESHOLD = 0.9 """ diff --git a/module/retire/retirement.py b/module/retire/retirement.py index 3e1635598a..e1efed738c 100644 --- a/module/retire/retirement.py +++ b/module/retire/retirement.py @@ -35,6 +35,10 @@ class Retirement(Enhancement, QuickRetireSettingHandler): # From MapOperation map_cat_attack_timer = Timer(2) + @property + def retire_keep_common_cv(self): + return self.config.is_task_enabled('GemsFarming') + def _retirement_choose(self, amount=10, target_rarity=('N',)): """ Args: @@ -114,7 +118,7 @@ def _retirement_confirm(self, skip_first_screenshot=True): else: self.interval_clear(SHIP_CONFIRM) if self.appear(SHIP_CONFIRM_2, offset=(30, 30), interval=2): - if self.config.RETIRE_KEEP_COMMON_CV and not self._have_kept_cv: + if self.retire_keep_common_cv and not self._have_kept_cv: self.keep_one_common_cv() self.device.click(SHIP_CONFIRM_2) self.interval_clear(GET_ITEMS_1) @@ -171,7 +175,7 @@ def retire_ships_one_click(self): end = False total = 0 - if self.config.RETIRE_KEEP_COMMON_CV: + if self.retire_keep_common_cv: self._have_kept_cv = False while 1: @@ -246,7 +250,7 @@ def retire_ships_old(self, amount=None, rarity=None): self.dock_favourite_set(False) total = 0 - if self.config.RETIRE_KEEP_COMMON_CV: + if self.retire_keep_common_cv: self._have_kept_cv = False while amount: From 0fe1479a27ab700cd242d62b996ed10ec6e4571e Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sat, 20 Jul 2024 05:40:00 +0800 Subject: [PATCH 124/161] Fix: Handle empty mail list (#4011) --- module/freebies/mail_white.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/module/freebies/mail_white.py b/module/freebies/mail_white.py index 42f1feeebe..e717d39e0d 100644 --- a/module/freebies/mail_white.py +++ b/module/freebies/mail_white.py @@ -1,4 +1,5 @@ from module.base.decorator import cached_property +from module.base.timer import Timer from module.combat.assets import GET_ITEMS_1, GET_ITEMS_2 from module.freebies.assets import * from module.logger import logger @@ -28,6 +29,9 @@ def mail_select_setting(self): def _mail_enter(self, skip_first_screenshot=True): """ + Returns: + int: If having mails + Page: in: page_main_white or MAIL_MANAGE out: MAIL_BATCH_CLAIM @@ -36,6 +40,7 @@ def _mail_enter(self, skip_first_screenshot=True): self.interval_clear([ MAIL_MANAGE ]) + timeout = Timer(0.6, count=1) while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -45,7 +50,12 @@ def _mail_enter(self, skip_first_screenshot=True): # End if self.appear(MAIL_BATCH_CLAIM, offset=(20, 20)): logger.info('Mail entered') - break + return True + if self.appear(GOTO_MAIN_WHITE, offset=(20, 20)): + timeout.start() + if timeout.reached(): + logger.info('Mail empty') + return False # Click if self.appear_then_click(MAIL_MANAGE, offset=(30, 30), interval=3): @@ -186,6 +196,9 @@ def mail_claim( in: page_main_white or MAIL_MANAGE out: MAIL_BATCH_CLAIM """ + if not self._mail_enter(): + return + if merit: logger.hr('Mail merit', level=2) self._mail_enter() From 39fceddb52217eb33e05f87a82a66c65e4b623e0 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sun, 21 Jul 2024 02:27:04 +0800 Subject: [PATCH 125/161] Upd: [EN] POPUP_CANCEL_WHITE and POPUP_CONFIRM_WHITE --- assets/en/ui_white/POPUP_CANCEL_WHITE.png | Bin 0 -> 7028 bytes assets/en/ui_white/POPUP_CONFIRM_WHITE.png | Bin 0 -> 7133 bytes module/ui_white/assets.py | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 assets/en/ui_white/POPUP_CANCEL_WHITE.png create mode 100644 assets/en/ui_white/POPUP_CONFIRM_WHITE.png diff --git a/assets/en/ui_white/POPUP_CANCEL_WHITE.png b/assets/en/ui_white/POPUP_CANCEL_WHITE.png new file mode 100644 index 0000000000000000000000000000000000000000..bdca3fa357e821fa4a0eaf0ae9fc8604a2242a83 GIT binary patch literal 7028 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 Date: Sun, 21 Jul 2024 02:28:43 +0800 Subject: [PATCH 126/161] Fix: Detected as mail empty if MAIL_MANAGE click missed --- module/freebies/mail_white.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/module/freebies/mail_white.py b/module/freebies/mail_white.py index e717d39e0d..1907b62c31 100644 --- a/module/freebies/mail_white.py +++ b/module/freebies/mail_white.py @@ -41,6 +41,7 @@ def _mail_enter(self, skip_first_screenshot=True): MAIL_MANAGE ]) timeout = Timer(0.6, count=1) + has_mail = False while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -51,7 +52,7 @@ def _mail_enter(self, skip_first_screenshot=True): if self.appear(MAIL_BATCH_CLAIM, offset=(20, 20)): logger.info('Mail entered') return True - if self.appear(GOTO_MAIN_WHITE, offset=(20, 20)): + if not has_mail and self.appear(GOTO_MAIN_WHITE, offset=(20, 20)): timeout.start() if timeout.reached(): logger.info('Mail empty') @@ -59,6 +60,7 @@ def _mail_enter(self, skip_first_screenshot=True): # Click if self.appear_then_click(MAIL_MANAGE, offset=(30, 30), interval=3): + has_mail = True continue if self.ui_main_appear_then_click(page_mail, offset=(30, 30), interval=3): continue From 4446e67f877ba2a46ca97fbdee523401ba78b8dc Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 23 Jul 2024 01:40:39 +0800 Subject: [PATCH 127/161] Upd: opt. ProcessManager. --- module/device/platform/api_windows.py | 173 ++++++++++++------ module/device/platform/platform_windows.py | 7 +- .../platform/winapi/functions_windows.py | 33 ++-- .../platform/winapi/structures_windows.py | 19 +- 4 files changed, 139 insertions(+), 93 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 037667fb4b..334eee1365 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -1,5 +1,4 @@ import threading -import re import asyncio from ctypes import byref, create_unicode_buffer, wstring_at, addressof @@ -63,8 +62,8 @@ def _enum_processes() -> t.Generator: OSError if CreateToolhelp32Snapshot or any winapi failed. IterationFinished if enumeration completed. """ - lppe32 = PROCESSENTRY32() - lppe32.dwSize = sizeof(PROCESSENTRY32) + lppe32 = PROCESSENTRY32W() + lppe32.dwSize = sizeof(PROCESSENTRY32W) with create_snapshot(TH32CS_SNAPPROCESS) as snapshot: if not Process32First(snapshot, byref(lppe32)): report("Process32First failed.") @@ -176,7 +175,6 @@ def setforegroundwindow(focusedwindow: tuple) -> bool: def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float = 0.5) -> None: - # TODO:Something error to fix. """ Try to refresh window if previous window was out of focus. @@ -189,18 +187,24 @@ def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float """ from time import sleep - from itertools import combinations - unique = lambda *args: all(x != y for x, y in combinations(args, 2)) + + def unique(*args): + from itertools import combinations + if not all(len(x) == len(args[0]) for x in args): + return False + + return all(any(i != j for i, j in zip(x, y)) for x, y in combinations(args, 2)) + attempts = 0 prevwindow = () while attempts < max_attempts: currentwindow = getfocusedwindow() if prevwindow: - if unique(currentwindow[0], prevwindow[0], focusedwindow[0]): + if unique(currentwindow, prevwindow, focusedwindow): break - if focusedwindow[0] != currentwindow[0]: + if unique(focusedwindow, currentwindow): logger.info(f"Current window is {currentwindow[0]}, flash back to {focusedwindow[0]}") setforegroundwindow(focusedwindow) attempts += 1 @@ -245,7 +249,7 @@ def execute(command: str, silentstart: bool, start: bool) -> tuple: bInheritHandles = False dwCreationFlags = ( DETACHED_PROCESS | - NORMAL_PRIORITY_CLASS | + IDLE_PRIORITY_CLASS | CREATE_NEW_PROCESS_GROUP | CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT @@ -587,82 +591,129 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL) -> bool: ShowWindow(hwnd, arg) return True +def is_running(pid: int): ... + class ProcessManager: - # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! - def __init__(self, pid: int): - self.mainpid = pid - self.datas = [] - self.evttree = EventTree() - self.lock = threading.Lock() - self.loop = asyncio.get_event_loop() - self.loop.create_task(self.listener()) - - async def listener(self): - # TODO:listening grab/kill event - while True: - event = await self.get_event() - if event == "grab": - await self.grab_pids() - elif event == "kill": - await self.kill_pids() - await asyncio.sleep(1) + _instance = None + _lock = threading.Lock() - async def get_event(self): - # TODO:get event - await asyncio.sleep(1) - return "grab" + def __new__(cls, *args, **kwargs): + with cls._lock: + if cls._instance is None: + cls._instance = super(ProcessManager, cls).__new__(cls) + return cls._instance - async def grab_pids(self): - if not IsUserAnAdmin(): + def __init__(self, pid: int): + if hasattr(self, '_initialized') and self._initialized: return - with evt_query() as hevent: - events = _enum_events(hevent) - for content in events: - data = self.evttree.parse_event(content) + self.mainpid = pid + self.datas = [] + self.evttree = EventTree() + self.lock = threading.Lock() + self.loop = asyncio.new_event_loop() + self.change_event = asyncio.Event() + self.kill_event = asyncio.Event() + self.exit_event = asyncio.Event() + self._initialized = True + threading.Thread(target=self.run_loop, daemon=True).start() + self.loop.call_soon_threadsafe(self.grab_pids) + + def run_loop(self): + asyncio.set_event_loop(self.loop) + self.loop.run_forever() + + def schedule_task(self): + self.loop.call_later(60, self.scheduled_grab) + + def scheduled_grab(self): + if not self.kill_event.is_set(): + asyncio.run_coroutine_threadsafe(self.grab_pids(), self.loop) + self.schedule_task() + + async def grab_pids(self): + try: + if not IsUserAnAdmin(): + report("Currently not running in administrator mode", statuscode=GetLastError()) + with evt_query() as hevent: + events = _enum_events(hevent) + for content in events: + data = self.evttree.parse_event(content) + with self.lock: + self.datas.append(data) + if data.new_process_id == self.mainpid: + break with self.lock: - self.datas.append(data) - if data.new_process_id == self.mainpid: - break - self.datas = self.datas[::-1] - await self.build_tree() - - async def build_tree(self): - count = 0 - for data in self.datas: - if data.process_id == self.mainpid: - break - count += 1 - self.evttree.root = Node(self.datas[count]) - for data in self.datas[count+1::]: + self.datas = self.datas[::-1] + self.build_tree() + except OSError: + self.exit_event.set() + exit(1) + + def build_tree(self): + if not self.datas: + return + self.evttree.root = Node(self.datas[0]) + for data in self.datas[1:]: evtiter = self.evttree.pre_order_traversal(self.evttree.root) for node in evtiter: - if data != node.data: + if node.data != data: continue cmdline = get_cmdline(data.process_id) - if node.data.new_process_name not in cmdline: + if data.process_name not in cmdline: continue node.add_children(data) - self.logtree() + # self.logtree() def logtree(self): evtiter = self.evttree.level_order_traversal(self.evttree.root) for node in evtiter: if node is None: - break + continue logger.info(node.data) async def kill_pids(self): - # TODO:kill process by enumerating tree with self.lock: evtiter = self.evttree.post_order_traversal(self.evttree.root) for node in evtiter: terminate_process(node.data.process_id) - del self.datas, self.evttree - self.datas, self.evttree = [], EventTree() + self.datas = [] + self.evttree = EventTree() + + async def handle_event(self, event_type, pid=None): + if event_type == "kill": + await self.kill_pids() + elif event_type == "change" and pid is not None: + with self.lock: + self.datas = [] + self.evttree.release_tree() + self.mainpid = pid + await self.grab_pids() def start(self): - threading.Thread(target=self.loop.run_forever).start() + self.schedule_task() + + def stop(self): + self.loop.call_soon_threadsafe(self.loop.stop) + + def send_change_event(self, pid: int): + self.kill_event.clear() + self.change_event.set() + asyncio.run_coroutine_threadsafe(self.handle_event('change', pid), self.loop) + + def send_kill_event(self): + self.kill_event.set() + self.change_event.clear() + asyncio.run_coroutine_threadsafe(self.handle_event('kill'), self.loop) if __name__ == '__main__': - PM = ProcessManager(27232) - PM.grab_pids() + import time + PM = ProcessManager(9196) + PM.start() + PM.logtree() + time.sleep(120) + PM.logtree() + p = 1234 + PM.send_change_event(p) + time.sleep(60) + PM.send_kill_event() + PM.stop() diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index b3e6a4c2c1..8ee15bb1cc 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -33,6 +33,9 @@ def __execute(self, command: str, start: bool) -> bool: api_windows.closehandle(*self.process[:2]) self.process = () + if self.hwnds: + self.hwnds = [] + self.process, self.focusedwindow = api_windows.execute(command, silentstart, start) return True @@ -59,7 +62,7 @@ def get_hwnds(pid: int) -> list: return api_windows.get_hwnds(pid) @staticmethod - def get_process(instance: EmulatorInstance) -> tuple: + def get_process(instance: api_windows.t.Optional[EmulatorInstance]) -> tuple: return api_windows.get_process(instance) @staticmethod @@ -349,7 +352,7 @@ def emulator_check(self) -> bool: logger.error(e) raise except Exception as e: - logger.error(e) + logger.exception(e) raise diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index b25ca2d13d..2a2056202e 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -13,7 +13,7 @@ from module.device.platform.winapi.structures_windows import ( SECURITY_ATTRIBUTES, STARTUPINFOW, WINDOWPLACEMENT, - PROCESS_INFORMATION, PROCESSENTRY32, THREADENTRY32, + PROCESS_INFORMATION, PROCESSENTRY32W, THREADENTRY32, FILETIME ) @@ -136,11 +136,11 @@ CloseHandle.restype = BOOL Process32First = kernel32.Process32First -Process32First.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] +Process32First.argtypes = [HANDLE, POINTER(PROCESSENTRY32W)] Process32First.restype = BOOL Process32Next = kernel32.Process32Next -Process32Next.argtypes = [HANDLE, POINTER(PROCESSENTRY32)] +Process32Next.argtypes = [HANDLE, POINTER(PROCESSENTRY32W)] Process32Next.restype = BOOL Thread32First = kernel32.Thread32First @@ -293,19 +293,16 @@ def __init__(self, data: dict, time: datetime): def __eq__(self, other): if isinstance(other, Data): - return self.process_id == other.new_process_id + return self.new_process_id == other.process_id return NotImplemented def __str__(self): - return ( - f"Data(system time={self.system_time}, " - f"new process ID={self.new_process_id}, " - f"new process name={self.new_process_name}, " - f"process ID={self.process_id}, " - f"process name={self.process_name})" - ) + attrs = ', '.join(f"{key}={value}" for key, value in self.__dict__.items()) + return f"Data({attrs})" - __repr__ = __str__ + def __repr__(self): + attrs = ', '.join(f"{key}={value!r}" for key, value in self.__dict__.items()) + return f"Data({attrs})" class Node: # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! @@ -313,16 +310,11 @@ def __init__(self, data: Data = None): self.data = data self.children = [] - def __del__(self): - if self.data is not None: - del self.data - if self.children: - del self.children + def __repr__(self): + return f"{self.__class__.__name__}(data={self.data!r})" def __str__(self) -> str: - return f"Node(data={self.data})" - - __repr__ = __str__ + return f"{self.__class__.__name__}(data={self.data})" def add_children(self, data): self.children.append(Node(data)) @@ -371,7 +363,6 @@ def level_order_traversal(node: Node): q.put(child) def release_tree(self): - del self.root self.root = None def report( diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index f520b1e326..80b7ea8d2c 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -1,7 +1,7 @@ from ctypes import POINTER, sizeof, Structure as _Structure from ctypes.wintypes import ( HANDLE, DWORD, WORD, BYTE, BOOL, USHORT, - UINT, LONG, CHAR, LPWSTR, LPVOID, MAX_PATH, + UINT, LONG, WCHAR, LPWSTR, LPVOID, MAX_PATH, RECT, PULONG, POINT, PWCHAR, FILETIME as _FILETIME ) @@ -13,6 +13,7 @@ class Structure(_Structure): def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls.field_name = tuple(name for name, _ in cls._fields_) + cls.field_type = tuple(_type for _, _type in cls._fields_) def __eq__(self, other): if not isinstance(other, self.__class__): @@ -98,14 +99,12 @@ def __len__(self): return len(self.field_name) def __dir__(self): - return [*super().__dir__()] + return super().__dir__() def __format__(self, format_spec): if format_spec == '': return str(self) - elif format_spec not in {'b', 'x'}: - raise ValueError(f"Unsupported format specifier: {format_spec}") - if format_spec == 'b': + elif format_spec == 'b': field_values = ', '.join( f"{name}=0b{getattr(self, name):b}" if isinstance(getattr(self, name), int) @@ -121,6 +120,8 @@ def __format__(self, format_spec): for name in self.field_name ) return f"{self.__class__.__name__}({field_values})" + else: + raise ValueError(f"Unsupported format specifier: {format_spec}") def __enter__(self): return self @@ -170,7 +171,7 @@ class SECURITY_ATTRIBUTES(Structure): ] # tlhelp32.h line 62 -class PROCESSENTRY32(Structure): +class PROCESSENTRY32W(Structure): _fields_ = [ ("dwSize", DWORD), ("cntUsage", DWORD), @@ -181,7 +182,7 @@ class PROCESSENTRY32(Structure): ("th32ParentProcessID", DWORD), ("pcPriClassBase", LONG), ("dwFlags", DWORD), - ("szExeFile", CHAR * MAX_PATH) + ("szExeFile", WCHAR * MAX_PATH) ] class THREADENTRY32(Structure): @@ -224,8 +225,8 @@ class RTL_USER_PROCESS_PARAMETERS(Structure): class PEB(Structure): _fields_ = [ - ("Reserved", BYTE * 28), - ("ProcessParameters", POINTER(RTL_USER_PROCESS_PARAMETERS)), + ("Reserved", BYTE * 28), + ("ProcessParameters", POINTER(RTL_USER_PROCESS_PARAMETERS)), ] class PROCESS_BASIC_INFORMATION(Structure): From 5074963ed224fdd3ab1d812e3544656ee5af2456 Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Tue, 23 Jul 2024 16:40:35 +0800 Subject: [PATCH 128/161] Upd: fix. --- module/device/platform/api_windows.py | 50 +++++-- .../device/platform/winapi/const_windows.py | 5 +- .../platform/winapi/functions_windows.py | 25 ++-- .../platform/winapi/structures_windows.py | 138 ++++++++++-------- 4 files changed, 128 insertions(+), 90 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 334eee1365..f73233622e 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -187,16 +187,11 @@ def refresh_window(focusedwindow: tuple, max_attempts: int = 10, interval: float """ from time import sleep - - def unique(*args): - from itertools import combinations - if not all(len(x) == len(args[0]) for x in args): - return False - - return all(any(i != j for i, j in zip(x, y)) for x, y in combinations(args, 2)) + from itertools import combinations attempts = 0 prevwindow = () + unique = lambda *args: all(x[0] != y[0] for x, y in combinations(args, 2)) while attempts < max_attempts: currentwindow = getfocusedwindow() @@ -354,7 +349,7 @@ def get_cmdline(pid: int) -> str: with open_process(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, pid) as hProcess: # Query process infomation pbi = PROCESS_BASIC_INFORMATION() - returnlength = ULONG() + returnlength = ULONG(sizeof(pbi)) status = NtQueryInformationProcess(hProcess, 0, byref(pbi), sizeof(pbi), byref(returnlength)) if status != STATUS_SUCCESS: report(f"NtQueryInformationProcess failed. Status: 0x{status:x}.", level=30) @@ -591,7 +586,36 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL) -> bool: ShowWindow(hwnd, arg) return True -def is_running(pid: int): ... +def get_parent_pid(pid: int) -> int: + try: + with open_process(PROCESS_QUERY_INFORMATION, pid) as hProcess: + # Query process infomation + pbi = PROCESS_BASIC_INFORMATION() + returnlength = ULONG(sizeof(pbi)) + status = NtQueryInformationProcess(hProcess, 0, byref(pbi), returnlength, byref(returnlength)) + if status != STATUS_SUCCESS: + report(f"NtQueryInformationProcess failed. Status: 0x{status:x}.", level=30) + except OSError: + return -1 + return pbi.InheritedFromUniqueProcessId + +def get_exit_code(pid: int) -> int: + try: + with open_process(PROCESS_QUERY_INFORMATION, pid) as hProcess: + exit_code = ULONG() + success = GetExitCodeProcess(hProcess, byref(exit_code)) + if not success: + report("Failed to get Exit code.", level=30) + except OSError: + return -1 + return exit_code.value + +def is_running(ppid: int = 0, pid: int = 0) -> bool: + if pid and get_exit_code(pid) != STILL_ACTIVE: + return False + if ppid and ppid != get_parent_pid(pid): + return False + return True class ProcessManager: _instance = None @@ -658,6 +682,8 @@ def build_tree(self): for node in evtiter: if node.data != data: continue + if is_running(node.data.process_id, data.process_id): + break cmdline = get_cmdline(data.process_id) if data.process_name not in cmdline: continue @@ -706,6 +732,11 @@ def send_kill_event(self): asyncio.run_coroutine_threadsafe(self.handle_event('kill'), self.loop) if __name__ == '__main__': + a = get_parent_pid(29076) + b = get_cmdline(29076) + logger.info(a) + logger.info(b) + """ import time PM = ProcessManager(9196) PM.start() @@ -717,3 +748,4 @@ def send_kill_event(self): time.sleep(60) PM.send_kill_event() PM.stop() + """ diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 1eef50a49f..4f9a9de5a2 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -1,5 +1,5 @@ from sys import getwindowsversion -from ctypes.wintypes import LPVOID +from ctypes import c_void_p # winnt.h line 3961 PROCESS_TERMINATE = 0x0001 @@ -257,5 +257,6 @@ EVT_VAR_TYPE_EVTHANDLE = 32 EVT_VAR_TYPE_EVTXML = 35 -MAXULONGLONG = LPVOID(-1).value +MAXULONGLONG = c_void_p(-1).value INVALID_HANDLE_VALUE = -1 +STILL_ACTIVE = 0x00000103 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 2a2056202e..e563c3bb26 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -5,17 +5,15 @@ from queue import Queue from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t -from ctypes.wintypes import ( - HANDLE, DWORD, HWND, BOOL, INT, UINT, - LONG, ULONG, LPWSTR, LPCWSTR, LPRECT, +from ctypes.wintypes import \ + HANDLE, DWORD, HWND, BOOL, INT, UINT, \ + LONG, ULONG, LPWSTR, LPCWSTR, \ LPVOID, LPCVOID, LPARAM, PULONG -) -from module.device.platform.winapi.structures_windows import ( - SECURITY_ATTRIBUTES, STARTUPINFOW, WINDOWPLACEMENT, - PROCESS_INFORMATION, PROCESSENTRY32W, THREADENTRY32, - FILETIME -) +from module.device.platform.winapi.structures_windows import \ + SECURITY_ATTRIBUTES, STARTUPINFOW, WINDOWPLACEMENT, \ + PROCESS_INFORMATION, PROCESSENTRY32W, THREADENTRY32, \ + FILETIME, RECT user32 = WinDLL(name='user32', use_last_error=True) kernel32 = WinDLL(name='kernel32', use_last_error=True) @@ -109,7 +107,7 @@ GetParent.argtypes = [HWND] GetParent.restype = HWND GetWindowRect = user32.GetWindowRect -GetWindowRect.argtypes = [HWND, LPRECT] +GetWindowRect.argtypes = [HWND, POINTER(RECT)] GetWindowRect.restype = BOOL EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM, use_last_error=True) @@ -283,7 +281,6 @@ def _is_invalid_handle(self): return self._handle is None class Data: - # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, data: dict, time: datetime): self.system_time: datetime = time self.new_process_id: int = data.get("NewProcessId", 0) @@ -305,7 +302,6 @@ def __repr__(self): return f"Data({attrs})" class Node: - # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! def __init__(self, data: Data = None): self.data = data self.children = [] @@ -320,7 +316,6 @@ def add_children(self, data): self.children.append(Node(data)) class EventTree: - # TODO:UNDER DEVELOPMENT!!!!!! DO NOT USE!!!! root = None @staticmethod @@ -367,14 +362,14 @@ def release_tree(self): def report( msg: str = '', - *args, + *args: tuple, statuscode: int = -1, uselog: bool = True, level: int = 40, handle: int = 0, raiseexcept: bool = True, exception: type = OSError, - **kwargs, + **kwargs: dict, ) -> None: """ Report any exception. diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index 80b7ea8d2c..cb05b8a177 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -1,9 +1,8 @@ -from ctypes import POINTER, sizeof, Structure as _Structure -from ctypes.wintypes import ( - HANDLE, DWORD, WORD, BYTE, BOOL, USHORT, - UINT, LONG, WCHAR, LPWSTR, LPVOID, MAX_PATH, - RECT, PULONG, POINT, PWCHAR, FILETIME as _FILETIME -) +from ctypes import \ + POINTER, sizeof, Structure as _Structure, \ + c_int32, c_uint32, c_uint64, c_uint16, \ + c_wchar, c_void_p, c_ubyte, c_byte, c_long, c_ulong +from ctypes.wintypes import MAX_PATH, FILETIME as _FILETIME class EmulatorLaunchFailedError(Exception): ... class HwndNotFoundError(Exception): ... @@ -134,74 +133,88 @@ def __bytes__(self): # processthreadsapi.h line 28 class PROCESS_INFORMATION(Structure): _fields_ = [ - ('hProcess', HANDLE), - ('hThread', HANDLE), - ('dwProcessId', DWORD), - ('dwThreadId', DWORD) + ('hProcess', c_void_p), + ('hThread', c_void_p), + ('dwProcessId', c_uint32), + ('dwThreadId', c_uint32) ] class STARTUPINFOW(Structure): _fields_ = [ - ('cb', DWORD), - ('lpReserved', LPWSTR), - ('lpDesktop', LPWSTR), - ('lpTitle', LPWSTR), - ('dwX', DWORD), - ('dwY', DWORD), - ('dwXSize', DWORD), - ('dwYSize', DWORD), - ('dwXCountChars', DWORD), - ('dwYCountChars', DWORD), - ('dwFillAttribute', DWORD), - ('dwFlags', DWORD), - ('wShowWindow', WORD), - ('cbReserved2', WORD), - ('lpReserved2', POINTER(BYTE)), - ('hStdInput', HANDLE), - ('hStdOutput', HANDLE), - ('hStdError', HANDLE) + ("cb", c_uint32), + ("lpReserved", POINTER(c_wchar)), + ("lpDesktop", POINTER(c_wchar)), + ("lpTitle", POINTER(c_wchar)), + ("dwX", c_uint32), + ("dwY", c_uint32), + ("dwXSize", c_uint32), + ("dwYSize", c_uint32), + ("dwXCountChars", c_uint32), + ("dwYCountChars", c_uint32), + ("dwFillAttribute", c_uint32), + ("dwFlags", c_uint32), + ("wShowWindow", c_uint16), + ("cbReserved2", c_uint16), + ("lpReserved2", POINTER(c_ubyte)), + ("hStdInput", c_void_p), + ("hStdOutput", c_void_p), + ("hStdError", c_void_p) ] # minwinbase.h line 13 class SECURITY_ATTRIBUTES(Structure): _fields_ = [ - ("nLength", DWORD), - ("lpSecurityDescriptor", LPVOID), - ("bInheritHandle", BOOL) + ("nLength", c_uint32), + ("lpSecurityDescriptor", c_void_p), + ("bInheritHandle", c_int32) ] # tlhelp32.h line 62 class PROCESSENTRY32W(Structure): _fields_ = [ - ("dwSize", DWORD), - ("cntUsage", DWORD), - ("th32ProcessID", DWORD), - ("th32DefaultHeapID", PULONG), - ("th32ModuleID", DWORD), - ("cntThreads", DWORD), - ("th32ParentProcessID", DWORD), - ("pcPriClassBase", LONG), - ("dwFlags", DWORD), - ("szExeFile", WCHAR * MAX_PATH) + ("dwSize", c_ulong), + ("cntUsage", c_ulong), + ("th32ProcessID", c_ulong), + ("th32DefaultHeapID", c_uint64), + ("th32ModuleID", c_ulong), + ("cntThreads", c_ulong), + ("th32ParentProcessID", c_ulong), + ("pcPriClassBase", c_long), + ("dwFlags", c_ulong), + ("szExeFile", c_wchar * MAX_PATH) ] class THREADENTRY32(Structure): _fields_ = [ - ("dwSize", DWORD), - ("cntUsage", DWORD), - ("th32ThreadID", DWORD), - ("th32OwnerProcessID", DWORD), - ("tpBasePri", LONG), - ("tpDeltaPri", LONG), - ("dwFlags", DWORD) + ("dwSize", c_ulong), + ("cntUsage", c_ulong), + ("th32ThreadID", c_ulong), + ("th32OwnerProcessID", c_ulong), + ("tpBasePri", c_long), + ("tpDeltaPri", c_long), + ("dwFlags", c_ulong) + ] + +class POINT(Structure): + _fields_ = [ + ("x", c_long), + ("y", c_long) + ] + +class RECT(Structure): + _fields_ = [ + ("left", c_long), + ("top", c_long), + ("right", c_long), + ("bottom", c_long) ] # winuser.h line 1801 class WINDOWPLACEMENT(Structure): _fields_ = [ - ("length", UINT), - ("flags", UINT), - ("showCmd", UINT), + ("length", c_uint32), + ("flags", c_uint32), + ("showCmd", c_uint32), ("ptMinPosition", POINT), ("ptMaxPosition", POINT), ("rcNormalPosition", RECT) @@ -210,36 +223,33 @@ class WINDOWPLACEMENT(Structure): # winternl.h line 25 class UNICODE_STRING(Structure): _fields_ = [ - ("Length", USHORT), - ("MaximumLength", USHORT), - ("Buffer", PWCHAR) + ("Length", c_uint16), + ("MaximumLength", c_uint16), + ("Buffer", POINTER(c_wchar)) ] # winternl.h line 54 class RTL_USER_PROCESS_PARAMETERS(Structure): _fields_ = [ - ("Reserved", LPVOID * 12), + ("Reserved", c_byte * 96), ("ImagePathName", UNICODE_STRING), ("CommandLine", UNICODE_STRING) ] class PEB(Structure): _fields_ = [ - ("Reserved", BYTE * 28), + ("Reserved1", c_byte * 32), ("ProcessParameters", POINTER(RTL_USER_PROCESS_PARAMETERS)), ] class PROCESS_BASIC_INFORMATION(Structure): - NTSTATUS = LONG - KPRIORITY = LONG - KAFFINITY = PULONG _fields_ = [ - ("ExitStatus", NTSTATUS), + ("ExitStatus", c_int32), ("PebBaseAddress", POINTER(PEB)), - ("AffinityMask", KAFFINITY), - ("BasePriority", KPRIORITY), - ("UniqueProcessId", PULONG), - ("InheritedFromUniqueProcessId", PULONG), + ("AffinityMask", c_uint64), + ("BasePriority", c_int32), + ("UniqueProcessId", c_uint64), + ("InheritedFromUniqueProcessId", c_uint64), ] class FILETIME(Structure, _FILETIME): From 59cef0fa9f71a4ee091af95d55198e1f918daca9 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:21:10 +0800 Subject: [PATCH 129/161] Fix: [ALAS] Device.__init__() was never called if EmulatorNotRunningError handled (cherry picked from commit 37d45af63f65ed851c48116d4a9872b43dd2f5c9) --- module/device/device.py | 7 +++++-- module/device/platform/platform_windows.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/module/device/device.py b/module/device/device.py index 456eca5fd7..816db16bd3 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -70,11 +70,14 @@ class Device(Screenshot, Control, AppControl): stuck_long_wait_list = ['BATTLE_STATUS_S', 'PAUSE', 'LOGIN_CHECK'] def __init__(self, *args, **kwargs): - for _ in range(2): + for trial in range(4): try: super().__init__(*args, **kwargs) break except EmulatorNotRunningError: + if trial >= 3: + logger.critical('Failed to start emulator after 3 trial') + raise RequestHumanTakeover # Try to start emulator if self.emulator_instance is not None: self.emulator_start() @@ -83,7 +86,7 @@ def __init__(self, *args, **kwargs): f'No emulator with serial "{self.config.Emulator_Serial}" found, ' f'please set a correct serial' ) - raise + raise RequestHumanTakeover # Auto-fill emulator info if IS_WINDOWS and self.config.EmulatorInfo_Emulator == 'auto': diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 46000abe36..9775b97636 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -235,7 +235,7 @@ def show_package(m): logger.info(f'Found azurlane packages: {m}') interval = Timer(0.5).start() - timeout = Timer(300).start() + timeout = Timer(180).start() new_window = 0 while 1: interval.wait() From 44b623c06c8a41547be6aa47b149808d13e27cb5 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 23 Jul 2024 22:43:35 +0800 Subject: [PATCH 130/161] Fix: [ALAS] Trying to handle AdbError: unknown host service (cherry picked from commit 59ad8bd6fcd4b63ab9a557b1bd11aa92fb071e13) --- module/device/connection.py | 33 +++++++++++++++++++++++---- module/device/method/adb.py | 10 +++++--- module/device/method/ascreencap.py | 8 +++++-- module/device/method/droidcast.py | 6 ++++- module/device/method/hermit.py | 6 ++++- module/device/method/maatouch.py | 9 ++++++++ module/device/method/minitouch.py | 9 +++++++- module/device/method/scrcpy/scrcpy.py | 6 ++++- module/device/method/uiautomator_2.py | 8 +++++-- module/device/method/utils.py | 25 +++++++++++++++----- module/device/method/wsa.py | 8 +++++-- 11 files changed, 105 insertions(+), 23 deletions(-) diff --git a/module/device/connection.py b/module/device/connection.py index e5a588a298..21d01f0583 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -16,7 +16,8 @@ from module.device.connection_attr import ConnectionAttr from module.device.env import IS_LINUX, IS_MACINTOSH, IS_WINDOWS from module.device.method.utils import (PackageNotInstalled, RETRY_TRIES, get_serial_pair, handle_adb_error, - possible_reasons, random_port, recv_all, remove_shell_warning, retry_sleep) + handle_unknown_host_service, possible_reasons, random_port, recv_all, + remove_shell_warning, retry_sleep) from module.exception import EmulatorNotRunningError, RequestHumanTakeover from module.logger import logger from module.map.map_grids import SelectedGrids @@ -50,6 +51,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # Package not installed @@ -137,8 +142,18 @@ def adb_command(self, cmd, timeout=10): """ cmd = list(map(str, cmd)) cmd = [self.adb_binary, '-s', self.serial] + cmd - logger.info(f'Execute: {cmd}') + return self.subprocess_run(cmd, timeout=timeout) + def subprocess_run(self, cmd, timeout=10): + """ + Args: + cmd (list): + timeout (int): + + Returns: + str: + """ + logger.info(f'Execute: {cmd}') # Use shell=True to disable console window when using GUI. # Although, there's still a window when you stop running in GUI, which cause by gooey. # To disable it, edit gooey/gui/util/taskkill.py @@ -155,11 +170,21 @@ def adb_command(self, cmd, timeout=10): @Config.when(DEVICE_OVER_HTTP=True) def adb_command(self, cmd, timeout=10): - logger.warning( - f'adb_command() is not available when connecting over http: {self.serial}, ' + logger.critical( + f'Trying to execute {cmd}, ' + f'but adb_command() is not available when connecting over http: {self.serial}, ' ) raise RequestHumanTakeover + def adb_start_server(self): + """ + Use `adb devices` as `adb start-server`, result is actually useless + Start ADB using subprocess instead of connecting via socket to kill the other ADBs + """ + stdout = self.subprocess_run([self.adb_binary, 'devices']) + logger.info(stdout) + return stdout + @Config.when(DEVICE_OVER_HTTP=False) def adb_shell(self, cmd, stream=False, recvall=True, timeout=10, rstrip=True): """ diff --git a/module/device/method/adb.py b/module/device/method/adb.py index bd3397edf7..8f7a66c578 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -1,16 +1,16 @@ import re +import time from functools import wraps import cv2 import numpy as np -import time from adbutils.errors import AdbError from lxml import etree from module.base.decorator import Config from module.device.connection import Connection -from module.device.method.utils import (RETRY_TRIES, retry_sleep, remove_prefix, handle_adb_error, - ImageTruncated, PackageNotInstalled) +from module.device.method.utils import (ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, + handle_unknown_host_service, remove_prefix, retry_sleep) from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -43,6 +43,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # Package not installed diff --git a/module/device/method/ascreencap.py b/module/device/method/ascreencap.py index c93321cfe2..24ba9902c0 100644 --- a/module/device/method/ascreencap.py +++ b/module/device/method/ascreencap.py @@ -6,8 +6,8 @@ from module.base.utils import * from module.device.connection import Connection -from module.device.method.utils import (RETRY_TRIES, retry_sleep, - handle_adb_error, ImageTruncated) +from module.device.method.utils import (ImageTruncated, RETRY_TRIES, handle_adb_error, handle_unknown_host_service, + retry_sleep) from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -50,6 +50,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # ImageTruncated diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index 73c0f22b9b..43dad33cc0 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -10,7 +10,7 @@ from module.base.timer import Timer from module.device.method.uiautomator_2 import ProcessInfo, Uiautomator2 from module.device.method.utils import ( - ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, retry_sleep) + ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, handle_unknown_host_service, retry_sleep) from module.exception import RequestHumanTakeover from module.logger import logger @@ -47,6 +47,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # Package not installed diff --git a/module/device/method/hermit.py b/module/device/method/hermit.py index ad0c39746e..8b1da67abc 100644 --- a/module/device/method/hermit.py +++ b/module/device/method/hermit.py @@ -8,7 +8,7 @@ from module.base.timer import Timer from module.base.utils import point2str, random_rectangle_point from module.device.method.adb import Adb -from module.device.method.utils import (RETRY_TRIES, retry_sleep, +from module.device.method.utils import (RETRY_TRIES, handle_unknown_host_service, retry_sleep, HierarchyButton, handle_adb_error) from module.exception import RequestHumanTakeover from module.logger import logger @@ -62,6 +62,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # HermitError: {"code":-1,"msg":"error"} diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index eb015fad7b..5e135fe399 100644 --- a/module/device/method/maatouch.py +++ b/module/device/method/maatouch.py @@ -15,6 +15,10 @@ from module.logger import logger +def handle_unknown_host_service(e): + pass + + def retry(func): @wraps(func) def retry_wrapper(self, *args, **kwargs): @@ -61,6 +65,11 @@ def init(): def init(): self.adb_reconnect() del_cached_property(self, '_maatouch_builder') + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() + del_cached_property(self, '_maatouch_builder') else: break # MaaTouchNotInstalledError: Received "Aborted" from MaaTouch diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index d48ed4191f..732cc1bdd3 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -14,7 +14,7 @@ from module.base.timer import Timer from module.base.utils import * from module.device.connection import Connection -from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep +from module.device.method.utils import RETRY_TRIES, handle_adb_error, handle_unknown_host_service, retry_sleep from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -433,6 +433,13 @@ def init(): if self._minitouch_port: self.adb_forward_remove(f'tcp:{self._minitouch_port}') del_cached_property(self, '_minitouch_builder') + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() + if self._minitouch_port: + self.adb_forward_remove(f'tcp:{self._minitouch_port}') + del_cached_property(self, '_minitouch_builder') else: break except BrokenPipeError as e: diff --git a/module/device/method/scrcpy/scrcpy.py b/module/device/method/scrcpy/scrcpy.py index 0730e9bac7..d666a11ae6 100644 --- a/module/device/method/scrcpy/scrcpy.py +++ b/module/device/method/scrcpy/scrcpy.py @@ -10,7 +10,7 @@ from module.device.method.minitouch import insert_swipe from module.device.method.scrcpy.core import ScrcpyCore, ScrcpyError from module.device.method.uiautomator_2 import Uiautomator2 -from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep +from module.device.method.utils import RETRY_TRIES, handle_adb_error, handle_unknown_host_service, retry_sleep from module.exception import RequestHumanTakeover from module.logger import logger @@ -62,6 +62,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # Unknown, probably a trucked image diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index c116bce94a..15110eb873 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -10,8 +10,8 @@ from module.base.utils import * from module.device.connection import Connection -from module.device.method.utils import (RETRY_TRIES, retry_sleep, handle_adb_error, - ImageTruncated, PackageNotInstalled, possible_reasons) +from module.device.method.utils import (ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, + handle_unknown_host_service, possible_reasons, retry_sleep) from module.exception import RequestHumanTakeover from module.logger import logger @@ -51,6 +51,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # RuntimeError: USB device 127.0.0.1:5555 is offline diff --git a/module/device/method/utils.py b/module/device/method/utils.py index b47dc46de2..dc8e7ae3be 100644 --- a/module/device/method/utils.py +++ b/module/device/method/utils.py @@ -206,12 +206,6 @@ def handle_adb_error(e): # Raised by uiautomator2 when current adb service is killed by another version of adb service. logger.error(e) return True - elif 'unknown host service' in text: - # AdbError(unknown host service) - # Another version of ADB service started, current ADB service has been killed. - # Usually because user opened a Chinese emulator, which uses ADB from the Stone Age. - logger.error(e) - return True else: # AdbError() logger.exception(e) @@ -223,6 +217,25 @@ def handle_adb_error(e): return False +def handle_unknown_host_service(e): + """ + Args: + e (Exception): + + Returns: + bool: If should retry + """ + text = str(e) + if 'unknown host service' in text: + # AdbError(unknown host service) + # Another version of ADB service started, current ADB service has been killed. + # Usually because user opened a Chinese emulator, which uses ADB from the Stone Age. + logger.error(e) + return True + else: + return False + + def get_serial_pair(serial): """ Args: diff --git a/module/device/method/wsa.py b/module/device/method/wsa.py index 56f0ea23f6..6ae1c60281 100644 --- a/module/device/method/wsa.py +++ b/module/device/method/wsa.py @@ -4,8 +4,8 @@ from adbutils.errors import AdbError from module.device.connection import Connection -from module.device.method.utils import (RETRY_TRIES, retry_sleep, - handle_adb_error, PackageNotInstalled) +from module.device.method.utils import (PackageNotInstalled, RETRY_TRIES, handle_adb_error, handle_unknown_host_service, + retry_sleep) from module.exception import RequestHumanTakeover from module.logger import logger @@ -38,6 +38,10 @@ def init(): if handle_adb_error(e): def init(): self.adb_reconnect() + elif handle_unknown_host_service(e): + def init(): + self.adb_start_server() + self.adb_reconnect() else: break # Package not installed From 790fee8aa7acc93eb7e85052d46ecef573591268 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:01:48 +0800 Subject: [PATCH 131/161] Fix: [ALAS] _maatouch_builder wasn't removed after adb_disconnect() (cherry picked from commit e10087aaf108f629ed689224e80e45f76905cfac) --- module/device/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/device/connection.py b/module/device/connection.py index 21d01f0583..46f563f078 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -673,7 +673,8 @@ def adb_disconnect(self, serial): del_cached_property(self, 'hermit_session') del_cached_property(self, 'droidcast_session') - del_cached_property(self, 'minitouch_builder') + del_cached_property(self, '_minitouch_builder') + del_cached_property(self, '_maatouch_builder') del_cached_property(self, 'reverse_server') def adb_restart(self): From 8da1848a4b0e4b807969734e348a8fa2263931fb Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:46:40 +0800 Subject: [PATCH 132/161] Fix: [ALAS] Trying to handle MuMu12 port switches (cherry picked from commit 73a84d10d2e2cfcec1e48900c352ef50fbecc186) --- module/device/connection.py | 61 +++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/module/device/connection.py b/module/device/connection.py index 46f563f078..4aca8edaa8 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -113,7 +113,7 @@ def __init__(self, config): self.detect_device() # Connect - self.adb_connect(self.serial) + self.adb_connect() logger.attr('AdbDevice', self.adb) # Package @@ -604,7 +604,7 @@ def adb_push(self, local, remote): return self.adb_command(cmd) @Config.when(DEVICE_OVER_HTTP=False) - def adb_connect(self, serial): + def adb_connect(self): """ Connect to a serial, try 3 times at max. If there's an old ADB server running while Alas is using a newer one, which happens on Chinese emulators, @@ -620,7 +620,9 @@ def adb_connect(self, serial): for device in self.list_device(): if device.status == 'offline': logger.warning(f'Device {device.serial} is offline, disconnect it before connecting') - self.adb_disconnect(device.serial) + msg = self.adb_client.disconnect(device.serial) + if msg: + logger.info(msg) elif device.status == 'unauthorized': logger.error(f'Device {device.serial} is unauthorized, please accept ADB debugging on your device') elif device.status == 'device': @@ -629,45 +631,58 @@ def adb_connect(self, serial): logger.warning(f'Device {device.serial} is is having a unknown status: {device.status}') # Skip for emulator-5554 - if 'emulator-' in serial: - logger.info(f'"{serial}" is a `emulator-*` serial, skip adb connect') + if 'emulator-' in self.serial: + logger.info(f'"{self.serial}" is a `emulator-*` serial, skip adb connect') return True - if re.match(r'^[a-zA-Z0-9]+$', serial): - logger.info(f'"{serial}" seems to be a Android serial, skip adb connect') + if re.match(r'^[a-zA-Z0-9]+$', self.serial): + logger.info(f'"{self.serial}" seems to be a Android serial, skip adb connect') return True # Try to connect for _ in range(3): - msg = self.adb_client.connect(serial) + msg = self.adb_client.connect(self.serial) logger.info(msg) + # Connected to 127.0.0.1:59865 + # Already connected to 127.0.0.1:59865 if 'connected' in msg: - # Connected to 127.0.0.1:59865 - # Already connected to 127.0.0.1:59865 return True + # bad port number '598265' in '127.0.0.1:598265' elif 'bad port' in msg: - # bad port number '598265' in '127.0.0.1:598265' - logger.error(msg) possible_reasons('Serial incorrect, might be a typo') raise RequestHumanTakeover + # cannot connect to 127.0.0.1:55555: + # No connection could be made because the target machine actively refused it. (10061) elif '(10061)' in msg: - # cannot connect to 127.0.0.1:55555: - # No connection could be made because the target machine actively refused it. (10061) - logger.info(msg) + # MuMu12 may switch serial if port is occupied + # Brute force connect nearby ports to handle serial switches + if self.is_mumu12_family: + before = self.serial + for port_offset in [1, -1, 2, -2]: + port = self.port + port_offset + serial = self.serial.replace(str(self.port), str(port)) + msg = self.adb_client.connect(serial) + logger.info(msg) + if 'connected' in msg: + break + self.detect_device() + if self.serial != before: + return True + # No such device logger.warning('No such device exists, please restart the emulator or set a correct serial') raise EmulatorNotRunningError # Failed to connect - logger.warning(f'Failed to connect {serial} after 3 trial, assume connected') + logger.warning(f'Failed to connect {self.serial} after 3 trial, assume connected') self.detect_device() return False @Config.when(DEVICE_OVER_HTTP=True) - def adb_connect(self, serial): + def adb_connect(self): # No adb connect if over http return True - def adb_disconnect(self, serial): - msg = self.adb_client.disconnect(serial) + def adb_disconnect(self): + msg = self.adb_client.disconnect(self.serial) if msg: logger.info(msg) @@ -697,11 +712,11 @@ def adb_reconnect(self): # Restart Adb self.adb_restart() # Connect to device - self.adb_connect(self.serial) + self.adb_connect() self.detect_device() else: - self.adb_disconnect(self.serial) - self.adb_connect(self.serial) + self.adb_disconnect() + self.adb_connect() self.detect_device() @Config.when(DEVICE_OVER_HTTP=True) @@ -976,7 +991,7 @@ def brute_force_connect(): for device in available.select(may_mumu12_family=True): if -2 <= device.port - self.port <= 2: # Port switched - logger.info(f'MuMu12 port switches from {self.serial} to {device.serial}') + logger.info(f'MuMu12 serial switched {self.serial} -> {device.serial}') del_cached_property(self, 'port') del_cached_property(self, 'is_mumu12_family') del_cached_property(self, 'is_mumu_family') From 1375eba095d2e408ff97d3da826fb31c0cd40300 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:49:58 +0800 Subject: [PATCH 133/161] Fix: [ALAS] Release cache after adb_restart() (cherry picked from commit 8d9e39fe5b9f359702c527e012eb03a04901b575) --- module/device/connection.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/module/device/connection.py b/module/device/connection.py index 4aca8edaa8..9638e186d0 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -681,17 +681,19 @@ def adb_connect(self): # No adb connect if over http return True - def adb_disconnect(self): - msg = self.adb_client.disconnect(self.serial) - if msg: - logger.info(msg) - + def release_resource(self): del_cached_property(self, 'hermit_session') del_cached_property(self, 'droidcast_session') del_cached_property(self, '_minitouch_builder') del_cached_property(self, '_maatouch_builder') del_cached_property(self, 'reverse_server') + def adb_disconnect(self): + msg = self.adb_client.disconnect(self.serial) + if msg: + logger.info(msg) + self.release_resource() + def adb_restart(self): """ Reboot adb client @@ -701,6 +703,7 @@ def adb_restart(self): self.adb_client.server_kill() # Init adb client del_cached_property(self, 'adb_client') + self.release_resource() _ = self.adb_client @Config.when(DEVICE_OVER_HTTP=False) From 70db7624218d830274aaf07426825b89cba1b873 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:51:22 +0800 Subject: [PATCH 134/161] Fix: [ALAS] use ldconsole.exe --- module/device/platform/emulator_windows.py | 4 +++- module/device/platform/platform_windows.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/module/device/platform/emulator_windows.py b/module/device/platform/emulator_windows.py index 36f0d9927c..d1eadbf11d 100644 --- a/module/device/platform/emulator_windows.py +++ b/module/device/platform/emulator_windows.py @@ -169,7 +169,9 @@ def single_to_console(exe: str): if 'MuMuPlayer.exe' in exe: return exe.replace('MuMuPlayer.exe', 'MuMuManager.exe') elif 'LDPlayer.exe' in exe: - return exe.replace('LDPlayer.exe', 'dnconsole.exe') + return exe.replace('LDPlayer.exe', 'ldconsole.exe') + elif 'dnplayer.exe' in exe: + return exe.replace('dnplayer.exe', 'ldconsole.exe') elif 'Bluestacks.exe' in exe: return exe.replace('Bluestacks.exe', 'bsconsole.exe') elif 'MEmu.exe' in exe: diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 9775b97636..0936d93fe8 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -95,8 +95,8 @@ def _emulator_start(self, instance: EmulatorInstance): logger.warning(f'Cannot get MuMu instance index from name {instance.name}') self.execute(f'"{exe}" -v {instance.MuMuPlayer12_id}') elif instance == Emulator.LDPlayerFamily: - # LDPlayer.exe index=0 - self.execute(f'"{exe}" index={instance.LDPlayer_id}') + # ldconsole.exe launch --index 0 + self.execute(f'"{Emulator.single_to_console(exe)}" launch --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 self.execute(f'"{exe}" -clone:{instance.name}') @@ -146,12 +146,12 @@ def _emulator_stop(self, instance: EmulatorInstance): rf')' ) elif instance == Emulator.MuMuPlayer12: - # E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player + # MuMuManager.exe api -v 1 shutdown_player if instance.MuMuPlayer12_id is None: logger.warning(f'Cannot get MuMu instance index from name {instance.name}') self.execute(f'"{Emulator.single_to_console(exe)}" api -v {instance.MuMuPlayer12_id} shutdown_player') elif instance == Emulator.LDPlayerFamily: - # E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0 + # ldconsole.exe quit --index 0 self.execute(f'"{Emulator.single_to_console(exe)}" quit --index {instance.LDPlayer_id}') elif instance == Emulator.NoxPlayerFamily: # Nox.exe -clone:Nox_1 -quit From dce6f5c150a2a008cf7d8a4eb5c1d706e0e429c7 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:17:37 +0800 Subject: [PATCH 135/161] Opt: [ALAS] Skip nemud_app_keep_alive check on non-mumu --- module/device/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/module/device/connection.py b/module/device/connection.py index 9638e186d0..5e1ef53000 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -332,6 +332,8 @@ def is_mumu_over_version_356(self) -> bool: which has nemud.app_keep_alive and always be a vertical device MuMu PRO on mac has the same feature """ + if not self.is_mumu_family: + return False if self.nemud_app_keep_alive != '': return True if IS_MACINTOSH: From 13fed12ff6dbb7e28623af861e34f0ff05fe6f1b Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:34:22 +0800 Subject: [PATCH 136/161] Fix: [ALAS] Handle dynamic mumu serial in serial_to_id --- module/device/method/nemu_ipc.py | 42 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/module/device/method/nemu_ipc.py b/module/device/method/nemu_ipc.py index b69cf9dd5e..315c1897eb 100644 --- a/module/device/method/nemu_ipc.py +++ b/module/device/method/nemu_ipc.py @@ -419,26 +419,28 @@ def up(self): if ret > 0: raise NemuIpcError('nemu_input_event_touch_up failed') + @staticmethod + 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 + Port from 16414 to 16418 -> 1 -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 + 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 + 16, 32) + offset -= 16 + if 0 <= index < 32 and offset in [-2, -1, 0, 1, 2]: + return index + else: + return None class NemuIpc(Platform): @@ -452,7 +454,7 @@ def nemu_ipc(self) -> NemuIpcImpl: # 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) + index = NemuIpcImpl.serial_to_id(self.serial) if index is not None: try: return NemuIpcImpl( From b285beed2b9ecb530d637805f9d1352c42450946 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:01:29 +0800 Subject: [PATCH 137/161] Add: [ALAS] Add screenshot method ldopengl --- module/device/connection_attr.py | 5 + module/device/method/ldopengl.py | 339 +++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 module/device/method/ldopengl.py diff --git a/module/device/connection_attr.py b/module/device/connection_attr.py index cd4fc89e82..cb82577833 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -168,6 +168,11 @@ def is_mumu_family(self): # 127.0.0.1:16384 + 32*n return self.serial == '127.0.0.1:7555' or self.is_mumu12_family + @cached_property + def is_ldplayer_bluestacks_family(self): + # Note that LDPlayer and BlueStacks have the same serial range + return self.serial.startswith('emulator-') or 5555 <= self.port <= 5587 + @cached_property def is_nox_family(self): return 62001 <= self.port <= 63025 diff --git a/module/device/method/ldopengl.py b/module/device/method/ldopengl.py new file mode 100644 index 0000000000..99034b041d --- /dev/null +++ b/module/device/method/ldopengl.py @@ -0,0 +1,339 @@ +import ctypes +import os +import subprocess +import typing as t +from dataclasses import dataclass +from functools import wraps + +import cv2 +import numpy as np + +from module.base.decorator import cached_property +from module.device.method.utils import RETRY_TRIES, get_serial_pair, retry_sleep +from module.device.platform import Platform +from module.exception import RequestHumanTakeover +from module.logger import logger + + +class LDOpenGLIncompatible(Exception): + pass + + +class LDOpenGLError(Exception): + pass + + +def bytes_to_str(b: bytes) -> str: + for encoding in ['utf-8', 'gbk']: + try: + return b.decode(encoding) + except UnicodeDecodeError: + pass + return str(b) + + +@dataclass +class DataLDPlayerInfo: + # Emulator instance index, starting from 0 + index: int + # Instance name + name: str + # Handle of top window + topWnd: int + # Handle of bind window + bndWnd: int + # If instance is running, 1 for True, 0 for False + sysboot: int + # PID of the instance process, or -1 if instance is not running + playerpid: int + # PID of the vbox process, or -1 if instance is not running + vboxpid: int + # Resolution + width: int + height: int + dpi: int + + def __post_init__(self): + self.index = int(self.index) + self.name = bytes_to_str(self.name) + self.topWnd = int(self.topWnd) + self.bndWnd = int(self.bndWnd) + self.sysboot = int(self.sysboot) + self.playerpid = int(self.playerpid) + self.vboxpid = int(self.vboxpid) + self.width = int(self.width) + self.height = int(self.height) + self.dpi = int(self.dpi) + + +class LDConsole: + def __init__(self, ld_folder: str): + """ + Args: + ld_folder: Installation path of MuMu12, e.g. E:/ProgramFiles/LDPlayer9 + which should have a `ldconsole.exe` in it. + """ + self.ld_console = os.path.abspath(os.path.join(ld_folder, './ldconsole.exe')) + + def subprocess_run(self, cmd, timeout=10): + """ + Args: + cmd (list): + timeout (int): + + Returns: + bytes: + """ + cmd = [self.ld_console] + cmd + logger.info(f'Execute: {cmd}') + + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False) + try: + stdout, stderr = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + process.kill() + stdout, stderr = process.communicate() + logger.warning(f'TimeoutExpired when calling {cmd}, stdout={stdout}, stderr={stderr}') + except FileNotFoundError: + process.kill() + stdout, stderr = process.communicate() + logger.warning(f'warning when calling {cmd}, stdout={stdout}, stderr={stderr}') + raise LDOpenGLIncompatible(f'ld_folder does not have ldconsole.exe') + return stdout + + def list2(self) -> t.List[DataLDPlayerInfo]: + """ + > ldconsole.exe list2 + 0,雷电模拟器,28053900,42935798,1,59776,36816,1280,720,240 + 1,雷电模拟器-1,0,0,0,-1,-1,1280,720,240 + """ + out = [] + data = self.subprocess_run(['list2']) + for row in data.strip().split(b'\n'): + info = row.strip().split(b',') + info = DataLDPlayerInfo(*info) + out.append(info) + return out + + +IScreenShotClassDtorType = ctypes.WINFUNCTYPE(None, ctypes.c_int32) +IScreenShotClassCapType = ctypes.WINFUNCTYPE(ctypes.c_void_p) +IScreenShotClassReleaseType = ctypes.WINFUNCTYPE(None) +IScreenShotClass_Dtor = IScreenShotClassDtorType(0, "IScreenShotClass_Dtor") +IScreenShotClass_Cap = IScreenShotClassCapType(1, "IScreenShotClass_Cap") +IScreenShotClass_Release = IScreenShotClassReleaseType(2, "IScreenShotClass_Release") + +CreateScreenshotInstanceType = ctypes.WINFUNCTYPE(ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint) + + +class IScreenShotClass: + def __init__(self, ptr): + self.ptr = ptr + + def cap(self): + return IScreenShotClass_Cap(self.ptr) + + def __del__(self): + IScreenShotClass_Release(self.ptr) + + +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 LDOpenGLIncompatible as e: + logger.error(e) + break + # NemuIpcError + except LDOpenGLError as e: + logger.error(e) + + def init(): + pass + # 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 LDOpenGLImpl: + def __init__(self, ld_folder: str, instance_id: int): + """ + Args: + ld_folder: Installation path of MuMu12, e.g. E:/ProgramFiles/LDPlayer9 + instance_id: Emulator instance ID, starting from 0 + """ + ldopengl_dll = os.path.abspath(os.path.join(ld_folder, './ldopengl64.dll')) + logger.info( + f'LDOpenGL init, ' + f'nemu_folder={ld_folder}, ' + f'ipc_dll={ldopengl_dll}, ' + f'instance_id={instance_id}' + ) + self.console = LDConsole(ld_folder) + self.info = self.get_player_info_by_index(instance_id) + + # Load dll + try: + self.lib = ctypes.WinDLL(ldopengl_dll) + except OSError as e: + logger.error(e) + if not os.path.exists(ldopengl_dll): + raise LDOpenGLIncompatible( + f'ldopengl_dll={ldopengl_dll} does not exist, ' + f'ldopengl requires LDPlayer >= 9.0.75, please check your version' + ) + else: + raise LDOpenGLIncompatible( + f'ldopengl_dll={ldopengl_dll} exist, ' + f'but cannot be loaded' + ) + self.lib.CreateScreenShotInstance.restype = ctypes.c_void_p + + # Get screenshot instance + instance_ptr = ctypes.c_void_p(self.lib.CreateScreenShotInstance(instance_id, self.info.playerpid)) + self.screenshot_instance = IScreenShotClass(instance_ptr) + + def get_player_info_by_index(self, instance_id: int): + """ + Args: + instance_id: + + Returns: + DataLDPlayerInfo: + + Raises: + LDOpenGLError: + """ + for info in self.console.list2(): + if info.index == instance_id: + logger.info(f'Match LDPlayer instance: {info}') + if not info.sysboot: + raise LDOpenGLError('Trying to connect LDPlayer instance but emulator is not running') + return info + raise LDOpenGLError(f'No LDPlayer instance with index {instance_id}') + + @retry + def screenshot(self): + """ + Returns: + np.ndarray: Image array in BGR color space + Note that image is upside down + """ + width, height = self.info.width, self.info.height + + img_ptr = self.screenshot_instance.cap() + # ValueError: NULL pointer access + if img_ptr is None: + raise LDOpenGLError('Empty image pointer') + + img = ctypes.cast(img_ptr, ctypes.POINTER(ctypes.c_ubyte * (height * width * 3))).contents + + image = np.ctypeslib.as_array(img).reshape((height, width, 3)) + return image + + @staticmethod + def serial_to_id(serial: str): + """ + Predict instance ID from serial + E.g. + "127.0.0.1:5555" -> 0 + "127.0.0.1:5557" -> 1 + "emulator-5554" -> 0 + + Returns: + int: instance_id, or None if failed to predict + """ + serial, _ = get_serial_pair(serial) + try: + port = int(serial.split(':')[1]) + except (IndexError, ValueError): + return None + if 5555 <= port <= 5555 + 32: + return int((port - 5555) // 2) + return None + + +class LDOpenGL(Platform): + @cached_property + def ldopengl(self): + """ + Initialize a ldopengl implementation + """ + # Try existing settings first + if self.config.EmulatorInfo_path: + folder = os.path.abspath(os.path.join(self.config.EmulatorInfo_path, '../')) + index = LDOpenGLImpl.serial_to_id(self.serial) + if index is not None: + try: + return LDOpenGLImpl( + ld_folder=folder, + instance_id=index, + ) + except (LDOpenGLIncompatible, LDOpenGLError) as e: + logger.error(e) + logger.error('Emulator info incorrect') + + # Search emulator instance + # with E:/ProgramFiles/LDPlayer9/dnplayer.exe + # installation path is E:/ProgramFiles/LDPlayer9 + if self.emulator_instance is None: + logger.error('Unable to use NemuIpc because emulator instance not found') + raise RequestHumanTakeover + try: + return LDOpenGLImpl( + ld_folder=self.emulator_instance.emulator.abspath('./'), + instance_id=self.emulator_instance.LDPlayer_id, + ) + except (LDOpenGLIncompatible, LDOpenGLError) as e: + logger.error(e) + logger.error('Unable to initialize NemuIpc') + raise RequestHumanTakeover + + def ldopengl_available(self) -> bool: + if not self.is_ldplayer_bluestacks_family: + return False + + try: + _ = self.ldopengl + except RequestHumanTakeover: + return False + return True + + def screenshot_ldopengl(self): + image = self.ldopengl.screenshot() + + cv2.flip(image, 0, dst=image) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) + return image + + +if __name__ == '__main__': + self = LDOpenGLImpl('E:/ProgramFiles/LDPlayer9', instance_id=1) + for _ in range(5): + import time + + start = time.time() + self.screenshot() + print(time.time() - start) From 733bb89b2d16ceb5a262ba5a97795dd393304ea1 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:22:01 +0800 Subject: [PATCH 138/161] Add: [ALAS] Add ldopengl setting --- module/config/argument/args.json | 3 ++- module/config/argument/argument.yaml | 1 + module/config/config_generated.py | 2 +- module/config/i18n/en-US.json | 3 ++- module/config/i18n/ja-JP.json | 3 ++- module/config/i18n/zh-CN.json | 3 ++- module/config/i18n/zh-TW.json | 3 ++- module/daemon/benchmark.py | 2 ++ module/device/screenshot.py | 4 +++- 9 files changed, 17 insertions(+), 7 deletions(-) diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 11b474b9f7..847daf9f40 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -117,7 +117,8 @@ "DroidCast", "DroidCast_raw", "scrcpy", - "nemu_ipc" + "nemu_ipc", + "ldopengl" ] }, "ControlMethod": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 7063ee6d8d..1cd59e4adb 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -43,6 +43,7 @@ Emulator: DroidCast_raw, scrcpy, nemu_ipc, + ldopengl, ] ControlMethod: value: minitouch diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 4c594abde5..bb23dd80f0 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -21,7 +21,7 @@ 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, nemu_ipc + Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy, nemu_ipc, ldopengl Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 6a7a7a53ab..da585e2246 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -410,7 +410,8 @@ "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", "scrcpy": "scrcpy", - "nemu_ipc": "nemu_ipc" + "nemu_ipc": "nemu_ipc", + "ldopengl": "ldopengl" }, "ControlMethod": { "name": "Control Method", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 2d4bf052bb..7e9399f07c 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -410,7 +410,8 @@ "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", "scrcpy": "scrcpy", - "nemu_ipc": "nemu_ipc" + "nemu_ipc": "nemu_ipc", + "ldopengl": "ldopengl" }, "ControlMethod": { "name": "Emulator.ControlMethod.name", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 66263256f8..b288623326 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -410,7 +410,8 @@ "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", "scrcpy": "scrcpy", - "nemu_ipc": "nemu_ipc" + "nemu_ipc": "nemu_ipc", + "ldopengl": "ldopengl" }, "ControlMethod": { "name": "模拟器控制方案", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index bd704a5f75..e887969ebf 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -410,7 +410,8 @@ "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", "scrcpy": "scrcpy", - "nemu_ipc": "nemu_ipc" + "nemu_ipc": "nemu_ipc", + "ldopengl": "ldopengl" }, "ControlMethod": { "name": "模擬器控制方案", diff --git a/module/daemon/benchmark.py b/module/daemon/benchmark.py index cc43a8aeeb..64f7fdf4b2 100644 --- a/module/daemon/benchmark.py +++ b/module/daemon/benchmark.py @@ -193,6 +193,8 @@ def remove(*args): click = ['ADB', 'Hermit', 'MaaTouch'] if self.device.nemu_ipc_available(): screenshot.append('nemu_ipc') + if self.device.ldopengl_available(): + screenshot.append('ldopengl') scene = self.config.Benchmark_TestScene if 'screenshot' not in scene: diff --git a/module/device/screenshot.py b/module/device/screenshot.py index 83685be2bd..74822a5a9d 100644 --- a/module/device/screenshot.py +++ b/module/device/screenshot.py @@ -13,6 +13,7 @@ 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.ldopengl import LDOpenGL from module.device.method.nemu_ipc import NemuIpc from module.device.method.scrcpy import Scrcpy from module.device.method.wsa import WSA @@ -20,7 +21,7 @@ from module.logger import logger -class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy, NemuIpc): +class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy, NemuIpc, LDOpenGL): _screen_size_checked = False _screen_black_checked = False _minicap_uninstalled = False @@ -40,6 +41,7 @@ def screenshot_methods(self): 'DroidCast_raw': self.screenshot_droidcast_raw, 'scrcpy': self.screenshot_scrcpy, 'nemu_ipc': self.screenshot_nemu_ipc, + 'ldopengl': self.screenshot_ldopengl, } def screenshot(self): From acbd5536bb1e00f93ba7513b7a3aec05cee09531 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:36:45 +0800 Subject: [PATCH 139/161] Fix: FileNotFoundError wasn't handled --- module/device/method/ldopengl.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/module/device/method/ldopengl.py b/module/device/method/ldopengl.py index 99034b041d..4abd686c35 100644 --- a/module/device/method/ldopengl.py +++ b/module/device/method/ldopengl.py @@ -87,18 +87,17 @@ def subprocess_run(self, cmd, timeout=10): cmd = [self.ld_console] + cmd logger.info(f'Execute: {cmd}') - process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False) + try: + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False) + except FileNotFoundError as e: + logger.warning(f'warning when calling {cmd}, {str(e)}') + raise LDOpenGLIncompatible(f'ld_folder does not have ldconsole.exe') try: stdout, stderr = process.communicate(timeout=timeout) except subprocess.TimeoutExpired: process.kill() stdout, stderr = process.communicate() logger.warning(f'TimeoutExpired when calling {cmd}, stdout={stdout}, stderr={stderr}') - except FileNotFoundError: - process.kill() - stdout, stderr = process.communicate() - logger.warning(f'warning when calling {cmd}, stdout={stdout}, stderr={stderr}') - raise LDOpenGLIncompatible(f'ld_folder does not have ldconsole.exe') return stdout def list2(self) -> t.List[DataLDPlayerInfo]: @@ -299,7 +298,7 @@ def ldopengl(self): # with E:/ProgramFiles/LDPlayer9/dnplayer.exe # installation path is E:/ProgramFiles/LDPlayer9 if self.emulator_instance is None: - logger.error('Unable to use NemuIpc because emulator instance not found') + logger.error('Unable to use LDOpenGL because emulator instance not found') raise RequestHumanTakeover try: return LDOpenGLImpl( @@ -308,7 +307,7 @@ def ldopengl(self): ) except (LDOpenGLIncompatible, LDOpenGLError) as e: logger.error(e) - logger.error('Unable to initialize NemuIpc') + logger.error('Unable to initialize LDOpenGL') raise RequestHumanTakeover def ldopengl_available(self) -> bool: From 200a5116cece4bd05c9b966948bf83a218fc4557 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:13:04 +0800 Subject: [PATCH 140/161] Fix: Keep IScreenShotClass_Cap referenced --- module/device/method/ldopengl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/module/device/method/ldopengl.py b/module/device/method/ldopengl.py index 4abd686c35..aebccd1e57 100644 --- a/module/device/method/ldopengl.py +++ b/module/device/method/ldopengl.py @@ -128,12 +128,15 @@ def list2(self) -> t.List[DataLDPlayerInfo]: class IScreenShotClass: def __init__(self, ptr): self.ptr = ptr + # Keep reference count + # so __del__ won't have an empty IScreenShotClass_Cap + self.release = IScreenShotClass_Release def cap(self): return IScreenShotClass_Cap(self.ptr) def __del__(self): - IScreenShotClass_Release(self.ptr) + self.release(self.ptr) def retry(func): From 2454d2aa2cff55e2c67e5d933f325c3ada52cb50 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:19:45 +0800 Subject: [PATCH 141/161] Opt: Use MaaTouch as default control method --- config/template.json | 2 +- module/config/argument/args.json | 2 +- module/config/argument/argument.yaml | 2 +- module/config/config_generated.py | 2 +- module/device/device.py | 6 +++++- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/config/template.json b/config/template.json index 6d1e2467de..bd93fe74d4 100644 --- a/config/template.json +++ b/config/template.json @@ -5,7 +5,7 @@ "PackageName": "auto", "ServerName": "disabled", "ScreenshotMethod": "auto", - "ControlMethod": "minitouch", + "ControlMethod": "MaaTouch", "ScreenshotDedithering": false, "AdbRestart": false }, diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 847daf9f40..c1369fee87 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -123,7 +123,7 @@ }, "ControlMethod": { "type": "select", - "value": "minitouch", + "value": "MaaTouch", "option": [ "ADB", "uiautomator2", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 1cd59e4adb..9b48894914 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -46,7 +46,7 @@ Emulator: ldopengl, ] ControlMethod: - value: minitouch + value: MaaTouch option: [ ADB, uiautomator2, diff --git a/module/config/config_generated.py b/module/config/config_generated.py index bb23dd80f0..2deaa1b1f0 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -22,7 +22,7 @@ class GeneratedConfig: Emulator_PackageName = 'auto' # auto, com.bilibili.azurlane, com.YoStarEN.AzurLane, com.YoStarJP.AzurLane, com.hkmanjuu.azurlane.gp, com.bilibili.blhx.huawei, com.bilibili.blhx.mi, com.tencent.tmgp.bilibili.blhx, com.bilibili.blhx.baidu, com.bilibili.blhx.qihoo, com.bilibili.blhx.nearme.gamecenter, com.bilibili.blhx.vivo, com.bilibili.blhx.mz, com.bilibili.blhx.dl, com.bilibili.blhx.lenovo, com.bilibili.blhx.uc, com.bilibili.blhx.mzw, com.yiwu.blhx.yx15, com.bilibili.blhx.m4399, com.bilibili.blhx.bilibiliMove, com.hkmanjuu.azurlane.gp.mc Emulator_ServerName = 'disabled' # disabled, cn_android-0, cn_android-1, cn_android-2, cn_android-3, cn_android-4, cn_android-5, cn_android-6, cn_android-7, cn_android-8, cn_android-9, cn_android-10, cn_android-11, cn_android-12, cn_android-13, cn_android-14, cn_android-15, cn_android-16, cn_android-17, cn_android-18, cn_android-19, cn_android-20, cn_android-21, cn_android-22, cn_android-23, cn_ios-0, cn_ios-1, cn_ios-2, cn_ios-3, cn_ios-4, cn_ios-5, cn_ios-6, cn_ios-7, cn_ios-8, cn_ios-9, cn_ios-10, cn_channel-0, cn_channel-1, cn_channel-2, cn_channel-3, cn_channel-4, en-0, en-1, en-2, en-3, en-4, en-5, jp-0, jp-1, jp-2, jp-3, jp-4, jp-5, jp-6, jp-7, jp-8, jp-9, jp-10, jp-11, jp-12, jp-13, jp-14, jp-15, jp-16, jp-17 Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy, nemu_ipc, ldopengl - Emulator_ControlMethod = 'minitouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch + Emulator_ControlMethod = 'MaaTouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False Emulator_AdbRestart = False diff --git a/module/device/device.py b/module/device/device.py index 816db16bd3..11a10172a3 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -138,7 +138,11 @@ def method_check(self): # Allow Hermit on VMOS only if self.config.Emulator_ControlMethod == 'Hermit' and not self.is_vmos: logger.warning('ControlMethod is allowed on VMOS only') - self.config.Emulator_ControlMethod = 'minitouch' + self.config.Emulator_ControlMethod = 'MaaTouch' + if self.config.Emulator_ScreenshotMethod == 'ldopengl' \ + and self.config.Emulator_ControlMethod == 'minitouch': + logger.warning('Use MaaTouch on ldplayer') + self.config.Emulator_ControlMethod = 'MaaTouch' pass def handle_night_commission(self, daily_trigger='21:00', threshold=30): From ff3971b0234a4908d6dfe618eb417422a15a0090 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:14:49 +0800 Subject: [PATCH 142/161] Fix: ctypes.WINFUNCTYPE is called on non-Windows --- module/device/method/ldopengl.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/module/device/method/ldopengl.py b/module/device/method/ldopengl.py index aebccd1e57..9fa00ad031 100644 --- a/module/device/method/ldopengl.py +++ b/module/device/method/ldopengl.py @@ -115,28 +115,23 @@ def list2(self) -> t.List[DataLDPlayerInfo]: return out -IScreenShotClassDtorType = ctypes.WINFUNCTYPE(None, ctypes.c_int32) -IScreenShotClassCapType = ctypes.WINFUNCTYPE(ctypes.c_void_p) -IScreenShotClassReleaseType = ctypes.WINFUNCTYPE(None) -IScreenShotClass_Dtor = IScreenShotClassDtorType(0, "IScreenShotClass_Dtor") -IScreenShotClass_Cap = IScreenShotClassCapType(1, "IScreenShotClass_Cap") -IScreenShotClass_Release = IScreenShotClassReleaseType(2, "IScreenShotClass_Release") - -CreateScreenshotInstanceType = ctypes.WINFUNCTYPE(ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint) - - class IScreenShotClass: def __init__(self, ptr): self.ptr = ptr + + # Define in class since ctypes.WINFUNCTYPE is windows only + cap_type = ctypes.WINFUNCTYPE(ctypes.c_void_p) + release_type = ctypes.WINFUNCTYPE(None) + self.class_cap = cap_type(1, "IScreenShotClass_Cap") # Keep reference count # so __del__ won't have an empty IScreenShotClass_Cap - self.release = IScreenShotClass_Release + self.class_release = release_type(2, "IScreenShotClass_Release") def cap(self): - return IScreenShotClass_Cap(self.ptr) + return self.class_cap(self.ptr) def __del__(self): - self.release(self.ptr) + self.class_release(self.ptr) def retry(func): @@ -189,8 +184,8 @@ def __init__(self, ld_folder: str, instance_id: int): ldopengl_dll = os.path.abspath(os.path.join(ld_folder, './ldopengl64.dll')) logger.info( f'LDOpenGL init, ' - f'nemu_folder={ld_folder}, ' - f'ipc_dll={ldopengl_dll}, ' + f'ld_folder={ld_folder}, ' + f'ldopengl_dll={ldopengl_dll}, ' f'instance_id={instance_id}' ) self.console = LDConsole(ld_folder) @@ -332,10 +327,10 @@ def screenshot_ldopengl(self): if __name__ == '__main__': - self = LDOpenGLImpl('E:/ProgramFiles/LDPlayer9', instance_id=1) + ld = LDOpenGLImpl('E:/ProgramFiles/LDPlayer9', instance_id=1) for _ in range(5): import time start = time.time() - self.screenshot() + ld.screenshot() print(time.time() - start) From 32649283ff569409bb71f3c38800f37bfd1bb1c3 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:18:28 +0800 Subject: [PATCH 143/161] Opt: Lower default screenshot interval on ldopengl --- module/daemon/benchmark.py | 2 ++ module/device/screenshot.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/module/daemon/benchmark.py b/module/daemon/benchmark.py index 64f7fdf4b2..9bbdc362db 100644 --- a/module/daemon/benchmark.py +++ b/module/daemon/benchmark.py @@ -233,6 +233,8 @@ def remove(*args): screenshot = remove('ADB_nc', 'aScreenCap_nc') if self.device.nemu_ipc_available(): screenshot.append('nemu_ipc') + if self.device.ldopengl_available(): + screenshot.append('ldopengl') screenshot = tuple(screenshot) self.TEST_TOTAL = 3 diff --git a/module/device/screenshot.py b/module/device/screenshot.py index 74822a5a9d..8a1d43fac7 100644 --- a/module/device/screenshot.py +++ b/module/device/screenshot.py @@ -164,7 +164,7 @@ def screenshot_interval_set(self, interval=None): 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': + if self.config.Emulator_ScreenshotMethod in ['nemu_ipc', 'ldopengl']: interval = limit_in(origin, 0.1, 0.2) elif interval == 'combat': origin = self.config.Optimization_CombatScreenshotInterval From 0a8aa9068f565163d7b1156278ac15c88b34399e Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Thu, 25 Jul 2024 17:11:50 +0800 Subject: [PATCH 144/161] Upd: fix. --- module/device/platform/winapi/functions_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 27eda76e66..78771b958d 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -5,7 +5,7 @@ from queue import Queue import threading import time -import functools +from functools import wraps import logging from ctypes import POINTER, WINFUNCTYPE, WinDLL, c_size_t @@ -450,7 +450,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def Timer(timeout=1): def decorator(func): - @functools.wraps(func) + @wraps(func) def wrapper(*args, **kwargs): func_path = get_func_path(func) result = [TimeoutError(f"Function '{func_path}' timed out after {timeout} seconds")] From bec185c855b1b54d2ca45f6bb66fb7c15d727ffc Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:37:48 +0800 Subject: [PATCH 145/161] Add: Event entrance of Interlude of Illusions (event_20240725_cn) --- campaign/Readme.md | 1 + module/config/argument/args.json | 88 +++++++++++++++++--------------- module/config/i18n/en-US.json | 1 + module/config/i18n/ja-JP.json | 1 + module/config/i18n/zh-CN.json | 1 + module/config/i18n/zh-TW.json | 1 + 6 files changed, 53 insertions(+), 40 deletions(-) diff --git a/campaign/Readme.md b/campaign/Readme.md index beb4158cc9..499f0110dc 100644 --- a/campaign/Readme.md +++ b/campaign/Readme.md @@ -202,3 +202,4 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20240627 | coalition 20240627 | Welcome to Little Academy | 欢迎来到童心学院 | Welcome to Little Academy | リトル学園へようこそ | - | | 20240711 | event 20211229 cn | Tower of Transcendence Rerun | - | - | -  | 復刻逆轉彩虹之塔 | | 20240718 | event 20220526 cn | Pledge of the Radiant Court Rerun | 复刻泠誓光庭 | Pledge of the Radiant Court Rerun | 復刻诚閃の剣 搖光の城 | - | +| 20240725 | event 20240725 cn | Interlude of Illusions | 幻梦间奏曲 | Interlude of Illusions | 夢幻の間奏曲 | - | diff --git a/module/config/argument/args.json b/module/config/argument/args.json index c1369fee87..5eab0a05e3 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -1703,16 +1703,17 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "display": "hide", "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -2039,15 +2040,16 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -2489,15 +2491,16 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -3898,15 +3901,16 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -4365,15 +4369,16 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -4832,15 +4837,16 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -5299,15 +5305,16 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { @@ -5756,15 +5763,16 @@ "event_20231221_cn", "event_20240229_cn", "event_20240425_cn", - "event_20240521_cn" + "event_20240521_cn", + "event_20240725_cn" ], "option_bold": [ "event_20211229_cn", - "event_20220526_cn" + "event_20240725_cn" ], - "cn": "event_20220526_cn", - "en": "event_20220526_cn", - "jp": "event_20220526_cn", + "cn": "event_20240725_cn", + "en": "event_20240725_cn", + "jp": "event_20240725_cn", "tw": "event_20211229_cn" }, "Mode": { diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index da585e2246..9faab78629 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -727,6 +727,7 @@ "event_20240229_cn": "Snowrealm Peregrination", "event_20240425_cn": "Heart-Linking Harmony", "event_20240521_cn": "Light of the Martyrium", + "event_20240725_cn": "Interlude of Illusions", "raid_20200624": "Air Raid Drills with Essex Rerun", "raid_20210708": "Cross Wave rerun", "raid_20220127": "Mystery Investigation", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 7e9399f07c..4dfbabfd62 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -727,6 +727,7 @@ "event_20240229_cn": "銀界遊廻", "event_20240425_cn": "共鳴のパッション", "event_20240521_cn": "赫輝のマルティリウム", + "event_20240725_cn": "夢幻の間奏曲", "raid_20200624": "特別演習超空強襲波(復刻)", "raid_20210708": "交錯する新たな波 (復刻)", "raid_20220127": "秘密事件調査", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index b288623326..4926eadb51 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -727,6 +727,7 @@ "event_20240229_cn": "雪境迷踪", "event_20240425_cn": "共鸣的PASSION", "event_20240521_cn": "绽放于辉光之城", + "event_20240725_cn": "幻梦间奏曲", "raid_20200624": "复刻特别演习埃塞克斯级", "raid_20210708": "复刻穿越彼方的水线", "raid_20220127": "演习神秘事件调查", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index e887969ebf..13c1735a6f 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -727,6 +727,7 @@ "event_20240229_cn": "Snowrealm Peregrination", "event_20240425_cn": "Heart-Linking Harmony", "event_20240521_cn": "Light of the Martyrium", + "event_20240725_cn": "Interlude of Illusions", "raid_20200624": "特別演習埃塞克斯級(復刻)", "raid_20210708": "復刻穿越彼方的水線", "raid_20220127": "演習神秘事件調查", From e2d1d2ff8ae239036fa363a5e2eaa798cc014fcd Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:23:57 +0800 Subject: [PATCH 146/161] Upd: Handle yet another mode switch in event_20240725_cn --- assets/cn/campaign/SWITCH_20240725_COMBAT.png | Bin 0 -> 7698 bytes assets/cn/campaign/SWITCH_20240725_STORY.png | Bin 0 -> 6779 bytes assets/en/campaign/SWITCH_20240725_COMBAT.png | Bin 0 -> 7698 bytes assets/en/campaign/SWITCH_20240725_STORY.png | Bin 0 -> 6779 bytes assets/jp/campaign/SWITCH_20240725_COMBAT.png | Bin 0 -> 7698 bytes assets/jp/campaign/SWITCH_20240725_STORY.png | Bin 0 -> 6779 bytes assets/tw/campaign/SWITCH_20240725_COMBAT.png | Bin 0 -> 7698 bytes assets/tw/campaign/SWITCH_20240725_STORY.png | Bin 0 -> 6779 bytes campaign/event_20240725_cn/campaign_base.py | 25 ++++++++++++++++++ module/campaign/assets.py | 2 ++ module/campaign/run.py | 1 + module/handler/fast_forward.py | 3 ++- 12 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 assets/cn/campaign/SWITCH_20240725_COMBAT.png create mode 100644 assets/cn/campaign/SWITCH_20240725_STORY.png create mode 100644 assets/en/campaign/SWITCH_20240725_COMBAT.png create mode 100644 assets/en/campaign/SWITCH_20240725_STORY.png create mode 100644 assets/jp/campaign/SWITCH_20240725_COMBAT.png create mode 100644 assets/jp/campaign/SWITCH_20240725_STORY.png create mode 100644 assets/tw/campaign/SWITCH_20240725_COMBAT.png create mode 100644 assets/tw/campaign/SWITCH_20240725_STORY.png create mode 100644 campaign/event_20240725_cn/campaign_base.py diff --git a/assets/cn/campaign/SWITCH_20240725_COMBAT.png b/assets/cn/campaign/SWITCH_20240725_COMBAT.png new file mode 100644 index 0000000000000000000000000000000000000000..dea5a57fc79066cdbad0c123cae626b546352784 GIT binary patch literal 7698 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 B2 > B3', 'C1 > C2 > C3', 'D1 > D2 > D3', - 'SP1 > SP2 > SP3 > SP4', + 'SP1 > SP2 > SP3 > SP4 > SP5', 'T1 > T2 > T3 > T4', + 'HT1 > HT2 > HT3 > HT4', ] map_fleet_checked = False From 69b573025f249e0a62ef7f8ef6907a1cc393dd46 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:40:16 +0800 Subject: [PATCH 147/161] Dev: Convert siren names --- dev_tools/map_extractor.py | 40 +++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/dev_tools/map_extractor.py b/dev_tools/map_extractor.py index 7dcafcf038..5d561ca865 100644 --- a/dev_tools/map_extractor.py +++ b/dev_tools/map_extractor.py @@ -256,6 +256,16 @@ 'yilishabai_3': 'Elizabeth3', 'jiasikenie_idol': 'GascogneIdol', 'dafeng_idol': 'TaihouIdol', + + # Interlude of Illusions + 'tianlangxing': 'Sirius', + 'daiduo': 'Dido', + 'z23_g': 'Z23_g', + 'laibixi_g': 'Leipzig_g', + 'pangpeimagenuo': 'PompeoMagno', + 'aerfuleiduo': 'AlfredoOriani', + 'guogan': 'LAudacieux', + 'dipulaikesi': 'Dupleix', } @@ -304,15 +314,15 @@ def __init__(self, data, data_loop): # portal self.portal = [] - if self.map_id in MAP_EVENT_LIST: - for event_id in MAP_EVENT_LIST[self.map_id]['event_list'].values(): - event = MAP_EVENT_TEMPLATE[event_id] - for effect in event['effect'].values(): - if effect[0] == 'jump': - address = event['address'] - address = location2node((address[1], address[0])) - target = location2node((effect[2], effect[1])) - self.portal.append((address, target)) + # if self.map_id in MAP_EVENT_LIST: + # for event_id in MAP_EVENT_LIST[self.map_id]['event_list'].values(): + # event = MAP_EVENT_TEMPLATE[event_id] + # for effect in event['effect'].values(): + # if effect[0] == 'jump': + # address = event['address'] + # address = location2node((address[1], address[0])) + # target = location2node((effect[2], effect[1])) + # self.portal.append((address, target)) # land_based # land_based = {{6, 7, 1}, ...} @@ -596,6 +606,10 @@ def get_chapter_by_name(self, name, select=False): Returns: list(MapData): """ + def is_extra(name): + name = name.lower().replace('.', '') + return name in ['extra', 'ex'] + print('<<< SEARCH MAP >>>') name = name.strip() name = int(name) if name.isdigit() else name @@ -603,7 +617,7 @@ def get_chapter_by_name(self, name, select=False): if isinstance(name, str): maps = [] for map_id, data in DATA.items(): - if not isinstance(map_id, int) or data['chapter_name'] == 'EXTRA': + if not isinstance(map_id, int) or is_extra(data['chapter_name']): continue if not re.search(name, data['name']): continue @@ -629,7 +643,7 @@ def get_event_id(map_id): event_id = get_event_id(maps[0].map_id) new = [] for map_id, data in DATA.items(): - if not isinstance(map_id, int) or data['chapter_name'] == 'EXTRA': + if not isinstance(map_id, int) or is_extra(data['chapter_name']): continue if get_event_id(data['id']) == event_id: data = MapData(data, DATA_LOOP.get(map_id, None)) @@ -686,8 +700,8 @@ def extract(self, maps, folder): LOADER = LuaLoader(FILE, server='CN') DATA = LOADER.load('./sharecfgdata/chapter_template.lua') DATA_LOOP = LOADER.load('./sharecfgdata/chapter_template_loop.lua') -MAP_EVENT_LIST = LOADER.load('./sharecfg/map_event_list.lua') -MAP_EVENT_TEMPLATE = LOADER.load('./sharecfg/map_event_template.lua') +# MAP_EVENT_LIST = LOADER.load('./sharecfg/map_event_list.lua') +# MAP_EVENT_TEMPLATE = LOADER.load('./sharecfg/map_event_template.lua') EXPECTATION_DATA = LOADER.load('./sharecfgdata/expedition_data_template.lua') ct = ChapterTemplate() From 7ec61a72c99b35d223de621b5df138aba82f5165 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:41:06 +0800 Subject: [PATCH 148/161] Dev: Faster relative_record --- dev_tools/relative_record.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dev_tools/relative_record.py b/dev_tools/relative_record.py index e101dde4ad..c716a45ebc 100644 --- a/dev_tools/relative_record.py +++ b/dev_tools/relative_record.py @@ -17,7 +17,19 @@ class Config: """ Paste the config of map file here """ - pass + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (80, 255 - 17), + 'width': (0.9, 10), + 'prominence': 10, + 'distance': 35, + } + EDGE_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (255 - 17, 255), + 'prominence': 10, + 'distance': 50, + 'wlen': 1000 + } + HOMO_EDGE_COLOR_RANGE = (0, 17) """ @@ -54,6 +66,7 @@ class Config: cfg = AzurLaneConfig(CONFIG).merge(Config()) al = ModuleBase(cfg) al.device.disable_stuck_detection() + al.device.screenshot_interval_set(0.11) view = View(cfg) al.device.screenshot() view.load(al.device.image) From 2da2d58297c71386cf43186ebb609c6a0b4e50b8 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:50:40 +0800 Subject: [PATCH 149/161] Fix: Handle yet another CLEAR icon --- .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 0 -> 1426 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 0 -> 1426 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 0 -> 1426 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 0 -> 1426 bytes module/campaign/campaign_ocr.py | 12 ++++++++++++ module/template/assets.py | 1 + 6 files changed, 13 insertions(+) create mode 100644 assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.png create mode 100644 assets/en/template/TEMPLATE_STAGE_CLEAR_20240725.png create mode 100644 assets/jp/template/TEMPLATE_STAGE_CLEAR_20240725.png create mode 100644 assets/tw/template/TEMPLATE_STAGE_CLEAR_20240725.png diff --git a/assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.png b/assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.png new file mode 100644 index 0000000000000000000000000000000000000000..af27d9a11243757cea08c43e0dd468d0caf8b016 GIT binary patch literal 1426 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 Date: Fri, 26 Jul 2024 01:18:04 +0800 Subject: [PATCH 150/161] Add: Chapter T --- .../template/TEMPLATE_SIREN_AlfredoOriani.gif | Bin 0 -> 2152 bytes assets/cn/template/TEMPLATE_SIREN_Dido.gif | Bin 0 -> 2137 bytes .../cn/template/TEMPLATE_SIREN_Leipzig_g.gif | Bin 0 -> 3197 bytes .../template/TEMPLATE_SIREN_PompeoMagno.gif | Bin 0 -> 4297 bytes assets/cn/template/TEMPLATE_SIREN_Sirius.gif | Bin 0 -> 3117 bytes assets/cn/template/TEMPLATE_SIREN_Z23_g.gif | Bin 0 -> 3200 bytes .../template/TEMPLATE_SIREN_AlfredoOriani.gif | Bin 0 -> 2152 bytes assets/en/template/TEMPLATE_SIREN_Dido.gif | Bin 0 -> 2137 bytes .../en/template/TEMPLATE_SIREN_Leipzig_g.gif | Bin 0 -> 3197 bytes .../template/TEMPLATE_SIREN_PompeoMagno.gif | Bin 0 -> 4297 bytes assets/en/template/TEMPLATE_SIREN_Sirius.gif | Bin 0 -> 3117 bytes assets/en/template/TEMPLATE_SIREN_Z23_g.gif | Bin 0 -> 3200 bytes .../template/TEMPLATE_SIREN_AlfredoOriani.gif | Bin 0 -> 2152 bytes assets/jp/template/TEMPLATE_SIREN_Dido.gif | Bin 0 -> 2137 bytes .../jp/template/TEMPLATE_SIREN_Leipzig_g.gif | Bin 0 -> 3197 bytes .../template/TEMPLATE_SIREN_PompeoMagno.gif | Bin 0 -> 4297 bytes assets/jp/template/TEMPLATE_SIREN_Sirius.gif | Bin 0 -> 3117 bytes assets/jp/template/TEMPLATE_SIREN_Z23_g.gif | Bin 0 -> 3200 bytes .../template/TEMPLATE_SIREN_AlfredoOriani.gif | Bin 0 -> 2152 bytes assets/tw/template/TEMPLATE_SIREN_Dido.gif | Bin 0 -> 2137 bytes .../tw/template/TEMPLATE_SIREN_Leipzig_g.gif | Bin 0 -> 3197 bytes .../template/TEMPLATE_SIREN_PompeoMagno.gif | Bin 0 -> 4297 bytes assets/tw/template/TEMPLATE_SIREN_Sirius.gif | Bin 0 -> 3117 bytes assets/tw/template/TEMPLATE_SIREN_Z23_g.gif | Bin 0 -> 3200 bytes campaign/event_20240725_cn/t1.py | 76 ++++++++++++++++ campaign/event_20240725_cn/t2.py | 77 +++++++++++++++++ campaign/event_20240725_cn/t3.py | 81 ++++++++++++++++++ module/template/assets.py | 6 ++ 28 files changed, 240 insertions(+) create mode 100644 assets/cn/template/TEMPLATE_SIREN_AlfredoOriani.gif create mode 100644 assets/cn/template/TEMPLATE_SIREN_Dido.gif create mode 100644 assets/cn/template/TEMPLATE_SIREN_Leipzig_g.gif create mode 100644 assets/cn/template/TEMPLATE_SIREN_PompeoMagno.gif create mode 100644 assets/cn/template/TEMPLATE_SIREN_Sirius.gif create mode 100644 assets/cn/template/TEMPLATE_SIREN_Z23_g.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_AlfredoOriani.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_Dido.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_Leipzig_g.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_PompeoMagno.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_Sirius.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_Z23_g.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_AlfredoOriani.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_Dido.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_Leipzig_g.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_PompeoMagno.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_Sirius.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_Z23_g.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_AlfredoOriani.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_Dido.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_Leipzig_g.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_PompeoMagno.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_Sirius.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_Z23_g.gif create mode 100644 campaign/event_20240725_cn/t1.py create mode 100644 campaign/event_20240725_cn/t2.py create mode 100644 campaign/event_20240725_cn/t3.py diff --git a/assets/cn/template/TEMPLATE_SIREN_AlfredoOriani.gif b/assets/cn/template/TEMPLATE_SIREN_AlfredoOriani.gif new file mode 100644 index 0000000000000000000000000000000000000000..ea60a90c966df8e7f47fd2560bd450a151ea2ca8 GIT binary patch literal 2152 zcmeIy=}*%K7zglQ4@xO5^Z>>sv_M(KibaXG(3%!|AV*V<1a!<$so0@lxfD;@+S-=G z6y_$^Oo!mcf`%)G7(*;&Ajm0*puFH7cuYV1J9FhzRyQ+kWXib z?EyTX0RXevyt%pg@#Dvhjg9s7^|iIN)z#Ja@87Sith{^oZh3imX=!O;Vd2f2H?Lp6 ze)a0r%a<=-ym&D?J3BKoGd(>$K0ZD+Hun7a^U=}Kr%#^_4-XFx4nBVTcwk_lzrVk) zudlbax2LD)(W6J5ot^FN?QLysCX=b9rRCned(F+wckkY9YHDg|XsEBRzkU05ZEbB$ zO-)r*Re5=NX=!OmNy+u=*RNf>R$N@1pP!$Xm#5R|v|8FMF&;qLD4>gwv` z8BKD`NmGy}Whe`j-AVQ4!9C4nwim^8LntcU{0|J2w!Qc93)DEb?U zYvNFRB3q84aI@wN_0>=q8J0*ficQu~M8<3a<-r3R0+F31#}efL1P##=7a?t>p59z0 zC1psWv?uMaY7R*5Be?m|1LTb8lP zo>FUm!F{jC9$J!w7VqLS=aA9Lusp}e+Snf&ZfejT5oO$o-8V1eS-I;&Q3v=DrCu(# zLW?HgA_F6JtodxFHP#-W%BSnLY3<7cr6a59jcSCvSMhf@qXkh|!4fxVjW|U94QBdC z3A#oo5MnUp&nwpxN zoSc}Lu%Pnn*)x!nCr_RX4Gn>)d?8AAcXwA;S4T(3XH;5STa8BJ{rmSpS}d+KHa6b5 za|cAFy1Lq6FjQ1jl$Di%sDPXl6&30A`ohA(f`S5&7K*vFHL?a`^D!xVSiwlbD#8=;&w*Dng+!A|e7r1>^*@ zgu~&m*=!bz6&xHK6cof@FhE!K?b`>^5*QdrqtQTGcJACsrBc73#oOB()a5fRZfj@;8Oa?!ziNy2BU?o>L(Qkf3$UQM(RiiTLRhXVZsA81gZxl{Qx9H zr7T6J`NPL2t&n&hI4vQF&4yw~dcMmd4u}E#L|URE$<~ETCI-@o1}b3h9hAdKn(3*e z$4RBl2BT7ia;eLKG|qIna*ofEQC%JU9>g4IwA7 rZ&6Xxe_eHFWTSfKy^;lX+jtIZxY0qru!5mp=_XdrcK%ET8yEZ!&Ovgh literal 0 HcmV?d00001 diff --git a/assets/cn/template/TEMPLATE_SIREN_Dido.gif b/assets/cn/template/TEMPLATE_SIREN_Dido.gif new file mode 100644 index 0000000000000000000000000000000000000000..07f0134f539313dffacf086a611b00aa772c1f01 GIT binary patch literal 2137 zcmeIyTTGK@7zgn8J3y;cC`=s;h@{;-G-{w4jEIbYK`jW_sZBUJ!6V2a9uevZJaw-j;bu3x+>-t8p3CQ-=lMm; zr2=7!6L0|)0N8A{j~_pN`0(NV`}gnOy?gui?c(C%!otFvH*a3Qe*NmztCufdTCLW( zxw+@hpU=+DK701;>C>l=A3uKh@ZrqN%=Gm1)YR0(#KgUO_io?5ee2e((b3VHH*b!N zj0_D84GaueESBrnuV1-xrLV8Ax3~AwrArqsTNJ-55V-8Vo*MQ02mAr(JNROvxROwM!D*NV~JpJ(oE5?V+RrfFgCd&@JQ5I z;xkPxKeCKTctl-w;HZ)W-l)X#t8d~^_8+Mk3^(4kj^v%x%u3fyJFGvVUfUog?+lT1wl#6F-)K}bRqy<9l{ZO zok*g%-rvjz$Ys=GlGsPFwRc1m!(juC;D2wW z;-Ap*;>8Q7$Nc>KG9XZoCr_S0Kps7Mv<%4Pm>0(`vO#m}oSb^73-ES`9IwD5|uy6p~U>Qc_%8TvSxFtV&*9UT$t~ zPEOA5-MgVJ5SEOLjP&$$XiG{;N^){?Vq#)KLV{AMTowlMBA3e{FVWG_5SHNJUB@h)J(@ip+#n+yuPveH(}179sc?K2afnE zniXezI${obe`Db1S*JuO&6)GnXxE|$!R&B63-C#D7(3#{<(hU<&O(~mkvR@<6m!Jc z1EtvP7{->-xquo*Jvf>M%b`MhybX~RiMoUU`68KZP^sEM6D~HaI?MC?ecLRuj-w-4 HED-z`qIgdU literal 0 HcmV?d00001 diff --git a/assets/cn/template/TEMPLATE_SIREN_Leipzig_g.gif b/assets/cn/template/TEMPLATE_SIREN_Leipzig_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..20f184edcbcf35d74c426f6fe971e568cefa22b8 GIT binary patch literal 3197 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 B|Be6v literal 0 HcmV?d00001 diff --git a/assets/cn/template/TEMPLATE_SIREN_Sirius.gif b/assets/cn/template/TEMPLATE_SIREN_Sirius.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e7d635f243d43ffa8c96f3b43df020ed81a6666 GIT binary patch literal 3117 zcmeI!Sxl2z6u|M@uPqcoD3;xVEsGQ_SQKgjhqOqU>ZmM(8y0O51V$u)1QCsvvWf@= z(J-=zjAG+P0dc{tvRMcsC<-cShec5o_XRiRm?mVR@nK#jKKQ+TZ#O6R{OqQdG7uUY8~gnE^QTXrK7Rc8;lqdb@86G(j=p>M?(N&RZ{ECl_3G7&7cZVafBx** zvyqXJCr_R{e*AcNc=*wyM?*tH4<0CW*ZQZ(c)22-c2?^`huaAq1i;0Pej*ect zc5P&2WO#UZXlUr_)vHA!(W+Ief`fyFLgCV-O9KM~eSLiwE?l@^!Giho=L-Y^KA-RH z?d|F5$!4=%U0t1>ogEz=9UL5NY;3Hpt*xxAEG;dmRH}uAg_)U|si~=vk&&UHp`M-| ziA2)Y*4EO}A`*#sJRbf0e$)xj$8;C*1Am{(^W)ESV&QRPV~9RVAkpEx%r9sDvL=W$ zf{NlfvJiocfJ~y4ED2YaA>^=R{-N`dF|wBsv6Os@oBz~8R=%B-Fo`}jP$5g%W;kW} z5!&hM1|8BgfA4}GUNbJ$)Fs-;&6$QtwSK8feHhZOt2@IUl6?In% zwIzc|T;(!EY}aC`Uo(Shf}83F85^7sHi7LOF`Te2#nx&9RjtdGrkjP8X20X&l=v#l z0;&AUw;8ckmI{^|;?l^*)J(t8zKZrDc4S*-i&U%KY)1S@+crvrPLWIajVSuuOF4Ti zSf@mEvUt9^BOO!7<3bw|k;Wttz8yiv_nZJEUcY_~PP}~i@_SS~eE1Nw7#tiNcLI>O zbLS2?ar^ddprX6G`^JqMSFc{}?Ck95=(uv_3Yei%shXRc>+9<;Uc3lqoI7{!?Afzt z&YY>MtJBC)TU)Eaqq@4fs;Ww%P-ql6di3ZxByzdDqM`yUDJdz@-~nLd<>i4H07hnJ zCQy-@nktn_larITY}vAT^X8|i^bqXP*6}nK!Cr$ zKOo`b<1=T@91jlJ~YO3}3zD&y(>gWD;T-%hahRfXx9&sx`2>_AChV8Mx2 z74vF*a*DAz9dhx@!Sn)7+_7bu%D?z{%=6GIBvIqG8<^^6lk=@<|3LvwEqn{X)2C0z zvjLhx1A!(RfCDIpe`kZnf}1yQLOFDGb%6zt4Q*|0t*xyP55PfFQBZ1AeF6{y zxxEa(kd|BwS;z!=Ws-@Go=X@V^w4=2BD=dl2&C^~dr%wws);9Ov_{urX`2(3``=ImtV_7_@7Ku`y zwTmY9BBF>H<-xjYwBk6PTs@JdcI?F^bY3kfTFR9W^Yc(sh aSLs|<-bQs@EaZw;`?TUQ_myzvJpKU@lZY<> literal 0 HcmV?d00001 diff --git a/assets/cn/template/TEMPLATE_SIREN_Z23_g.gif b/assets/cn/template/TEMPLATE_SIREN_Z23_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..c853ba694140c147b8c0188a686881a821b346f6 GIT binary patch literal 3200 zcmeI!`BT!}9>DQ00=bbOl46Q|5i1umF)U0oR1`FFFKxZJC6$(_6|DlHl3HO}T3Lbm zsJE;%t*orva_LboYSYwYr9QSyIi_AKZS$VyJHs^f%-mm|xxd`!`~l~j`OG=5d2{`I zS+0?IhzMms(Dd~5w{PFReEIVE^XE^WK21(ee*F0H!-o&=-@l)jm>3%y8yy`T9v*)4 z=FO{DuU@`<+27ya*Vp&>@#Eg!-tO*hjYiYi+1b(2(c0SD+}zyI&~WF@o!hr>*VWbC zx^=6zw)Wb!Yc(}Bm6es3FJHcN=~6{S#f1wO%FD}3OG`^iN{Wk%i;9ZWYW2Bu=gyuz zd*;lU!otE+r%vVP=N~_QJU2HtCnra#R31Hg^vIDT+1c4ySy`ExnTHM?QYaMZ>FH@{ zX>z$-CX-2}(gOz$?Afzt_wL<0cI-$_PEJZn+P-~ze0=R5C}GI+_+)GhIQ-KtzElz&6+h45fS0x;j31y3JVJh2?+@f4h{+m3JeSk z2nblYawVV7U$J6^zrR11%k}m3_4fAm^73M{*~^zN_wevowrrWZyStm4+tQ^=SuEC) zB}-gfT$oHIgTZiea$2}>;erJV?CtIC?Cj>xpFeNjJR2JuYinyNl?ub~?Af!;%*;$p zO^HOJfq?-AgMt2D|Emd5rws#q!a}`0gM6GFozST1X^1+7(W8R%>EG}0{hL5~(~yM| zxlQ0HJ*a1F6{LbuGC9GJz_wMf?kvT|~etH{!)mO~JH@8qI{cnnJ@C`P-4-zmvC zhz>NvL-ZMXkP17~JypY(GFtd)y7+nVu<_90a~^RML!&0IeDUrHb(6u)Wq7r#JP$XR zOKZ(0Eev6|TW|Uub`xMw$9i7|sto1jEcKP5Be^dBxY=jd!wERfQd>0rFm`txRX56;CIlGi6PN3E#vuM^5Z z82S43t5%Y!sVOZcK$7wCaUjXNckf0>C-@!lP6CW6cps;X3c(dr?QNK#W%fg$_%@87p?A0o-F zUAuPf+zC8MNl8IS5s5^JiHQJ~goFfO%hs)1f8r%NI$FyMux0)F_5YQZA8q0Bc)%7P zA0I6*z!pRmS65d6%i_h05m=m^owc?&IywSg92^{gFt)a~h%hu7O)CtALb0^8)H*{V zkpMN{U-#eZFPZ=lg65vs4>tlKOuPszCv_9rfJ~2I3nbDET>}zJ2}#mSj4(DlA7M{(vkwq>4vb1ETw3(wIHe?<@JS-aLbm3-2y{UO5ZPvGNv=Wol<35Uf zy0{{xRweZ#LAqq7xNKbXF$QE9Bl(vad~o%P z4jhSDS6Qi4>!eBk+z6lph>Ra;Ksw`B8W0f%1_po#NN_xS_z-XavZJS`2Pu!Pt}aj> z?d|PaAex$*K!PAl03dGMxB-l~e*HSAkgBRGU<62zix)4RKYzZgtW4V<00@vBz=#tk zP9WIZ&S0Ir1h>3~$(Fi0#024nZ z$m-RrL4t&ahJp(DDM3Jg`1$z(CXfmNQh*Ba^z_t*h_*t27g{N_2|}mS0TZA?v`l~y z0aAbvnKNe&!UPBr1PEheV;l~rtE>AL{gMCoZGaIbEFoKi`d$&%f_{Ni2cu8(FBB8l zD6|fl&x6@fG9w*`N9ZX{mqW&O6;oA>{#K4s6-;KUB%Nr_I;X~i+zg5n6VjusrDecA zq6c-VKjN}x*#?&Kqzx?^szbgVQx?#UiE8{IaHoxZ+c{O38(78lY zFU8KD$G!8tiyd|q4MSlA9Yj-QwoqMM9ze99);62NVOaA;+Y1wOOro(AQ-6BmBSN&1 z2_ry~cf%7KaG2o{A{2_SXsIFW-Y2SV;^58Z#Hd(FQMQl^Hxq6MDP2Jz(!_}ZC8Tqf XliF7@K%9}GH#^*ygAtFyFy!_h${@wV literal 0 HcmV?d00001 diff --git a/assets/en/template/TEMPLATE_SIREN_AlfredoOriani.gif b/assets/en/template/TEMPLATE_SIREN_AlfredoOriani.gif new file mode 100644 index 0000000000000000000000000000000000000000..ea60a90c966df8e7f47fd2560bd450a151ea2ca8 GIT binary patch literal 2152 zcmeIy=}*%K7zglQ4@xO5^Z>>sv_M(KibaXG(3%!|AV*V<1a!<$so0@lxfD;@+S-=G z6y_$^Oo!mcf`%)G7(*;&Ajm0*puFH7cuYV1J9FhzRyQ+kWXib z?EyTX0RXevyt%pg@#Dvhjg9s7^|iIN)z#Ja@87Sith{^oZh3imX=!O;Vd2f2H?Lp6 ze)a0r%a<=-ym&D?J3BKoGd(>$K0ZD+Hun7a^U=}Kr%#^_4-XFx4nBVTcwk_lzrVk) zudlbax2LD)(W6J5ot^FN?QLysCX=b9rRCned(F+wckkY9YHDg|XsEBRzkU05ZEbB$ zO-)r*Re5=NX=!OmNy+u=*RNf>R$N@1pP!$Xm#5R|v|8FMF&;qLD4>gwv` z8BKD`NmGy}Whe`j-AVQ4!9C4nwim^8LntcU{0|J2w!Qc93)DEb?U zYvNFRB3q84aI@wN_0>=q8J0*ficQu~M8<3a<-r3R0+F31#}efL1P##=7a?t>p59z0 zC1psWv?uMaY7R*5Be?m|1LTb8lP zo>FUm!F{jC9$J!w7VqLS=aA9Lusp}e+Snf&ZfejT5oO$o-8V1eS-I;&Q3v=DrCu(# zLW?HgA_F6JtodxFHP#-W%BSnLY3<7cr6a59jcSCvSMhf@qXkh|!4fxVjW|U94QBdC z3A#oo5MnUp&nwpxN zoSc}Lu%Pnn*)x!nCr_RX4Gn>)d?8AAcXwA;S4T(3XH;5STa8BJ{rmSpS}d+KHa6b5 za|cAFy1Lq6FjQ1jl$Di%sDPXl6&30A`ohA(f`S5&7K*vFHL?a`^D!xVSiwlbD#8=;&w*Dng+!A|e7r1>^*@ zgu~&m*=!bz6&xHK6cof@FhE!K?b`>^5*QdrqtQTGcJACsrBc73#oOB()a5fRZfj@;8Oa?!ziNy2BU?o>L(Qkf3$UQM(RiiTLRhXVZsA81gZxl{Qx9H zr7T6J`NPL2t&n&hI4vQF&4yw~dcMmd4u}E#L|URE$<~ETCI-@o1}b3h9hAdKn(3*e z$4RBl2BT7ia;eLKG|qIna*ofEQC%JU9>g4IwA7 rZ&6Xxe_eHFWTSfKy^;lX+jtIZxY0qru!5mp=_XdrcK%ET8yEZ!&Ovgh literal 0 HcmV?d00001 diff --git a/assets/en/template/TEMPLATE_SIREN_Dido.gif b/assets/en/template/TEMPLATE_SIREN_Dido.gif new file mode 100644 index 0000000000000000000000000000000000000000..07f0134f539313dffacf086a611b00aa772c1f01 GIT binary patch literal 2137 zcmeIyTTGK@7zgn8J3y;cC`=s;h@{;-G-{w4jEIbYK`jW_sZBUJ!6V2a9uevZJaw-j;bu3x+>-t8p3CQ-=lMm; zr2=7!6L0|)0N8A{j~_pN`0(NV`}gnOy?gui?c(C%!otFvH*a3Qe*NmztCufdTCLW( zxw+@hpU=+DK701;>C>l=A3uKh@ZrqN%=Gm1)YR0(#KgUO_io?5ee2e((b3VHH*b!N zj0_D84GaueESBrnuV1-xrLV8Ax3~AwrArqsTNJ-55V-8Vo*MQ02mAr(JNROvxROwM!D*NV~JpJ(oE5?V+RrfFgCd&@JQ5I z;xkPxKeCKTctl-w;HZ)W-l)X#t8d~^_8+Mk3^(4kj^v%x%u3fyJFGvVUfUog?+lT1wl#6F-)K}bRqy<9l{ZO zok*g%-rvjz$Ys=GlGsPFwRc1m!(juC;D2wW z;-Ap*;>8Q7$Nc>KG9XZoCr_S0Kps7Mv<%4Pm>0(`vO#m}oSb^73-ES`9IwD5|uy6p~U>Qc_%8TvSxFtV&*9UT$t~ zPEOA5-MgVJ5SEOLjP&$$XiG{;N^){?Vq#)KLV{AMTowlMBA3e{FVWG_5SHNJUB@h)J(@ip+#n+yuPveH(}179sc?K2afnE zniXezI${obe`Db1S*JuO&6)GnXxE|$!R&B63-C#D7(3#{<(hU<&O(~mkvR@<6m!Jc z1EtvP7{->-xquo*Jvf>M%b`MhybX~RiMoUU`68KZP^sEM6D~HaI?MC?ecLRuj-w-4 HED-z`qIgdU literal 0 HcmV?d00001 diff --git a/assets/en/template/TEMPLATE_SIREN_Leipzig_g.gif b/assets/en/template/TEMPLATE_SIREN_Leipzig_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..20f184edcbcf35d74c426f6fe971e568cefa22b8 GIT binary patch literal 3197 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 B|Be6v literal 0 HcmV?d00001 diff --git a/assets/en/template/TEMPLATE_SIREN_Sirius.gif b/assets/en/template/TEMPLATE_SIREN_Sirius.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e7d635f243d43ffa8c96f3b43df020ed81a6666 GIT binary patch literal 3117 zcmeI!Sxl2z6u|M@uPqcoD3;xVEsGQ_SQKgjhqOqU>ZmM(8y0O51V$u)1QCsvvWf@= z(J-=zjAG+P0dc{tvRMcsC<-cShec5o_XRiRm?mVR@nK#jKKQ+TZ#O6R{OqQdG7uUY8~gnE^QTXrK7Rc8;lqdb@86G(j=p>M?(N&RZ{ECl_3G7&7cZVafBx** zvyqXJCr_R{e*AcNc=*wyM?*tH4<0CW*ZQZ(c)22-c2?^`huaAq1i;0Pej*ect zc5P&2WO#UZXlUr_)vHA!(W+Ief`fyFLgCV-O9KM~eSLiwE?l@^!Giho=L-Y^KA-RH z?d|F5$!4=%U0t1>ogEz=9UL5NY;3Hpt*xxAEG;dmRH}uAg_)U|si~=vk&&UHp`M-| ziA2)Y*4EO}A`*#sJRbf0e$)xj$8;C*1Am{(^W)ESV&QRPV~9RVAkpEx%r9sDvL=W$ zf{NlfvJiocfJ~y4ED2YaA>^=R{-N`dF|wBsv6Os@oBz~8R=%B-Fo`}jP$5g%W;kW} z5!&hM1|8BgfA4}GUNbJ$)Fs-;&6$QtwSK8feHhZOt2@IUl6?In% zwIzc|T;(!EY}aC`Uo(Shf}83F85^7sHi7LOF`Te2#nx&9RjtdGrkjP8X20X&l=v#l z0;&AUw;8ckmI{^|;?l^*)J(t8zKZrDc4S*-i&U%KY)1S@+crvrPLWIajVSuuOF4Ti zSf@mEvUt9^BOO!7<3bw|k;Wttz8yiv_nZJEUcY_~PP}~i@_SS~eE1Nw7#tiNcLI>O zbLS2?ar^ddprX6G`^JqMSFc{}?Ck95=(uv_3Yei%shXRc>+9<;Uc3lqoI7{!?Afzt z&YY>MtJBC)TU)Eaqq@4fs;Ww%P-ql6di3ZxByzdDqM`yUDJdz@-~nLd<>i4H07hnJ zCQy-@nktn_larITY}vAT^X8|i^bqXP*6}nK!Cr$ zKOo`b<1=T@91jlJ~YO3}3zD&y(>gWD;T-%hahRfXx9&sx`2>_AChV8Mx2 z74vF*a*DAz9dhx@!Sn)7+_7bu%D?z{%=6GIBvIqG8<^^6lk=@<|3LvwEqn{X)2C0z zvjLhx1A!(RfCDIpe`kZnf}1yQLOFDGb%6zt4Q*|0t*xyP55PfFQBZ1AeF6{y zxxEa(kd|BwS;z!=Ws-@Go=X@V^w4=2BD=dl2&C^~dr%wws);9Ov_{urX`2(3``=ImtV_7_@7Ku`y zwTmY9BBF>H<-xjYwBk6PTs@JdcI?F^bY3kfTFR9W^Yc(sh aSLs|<-bQs@EaZw;`?TUQ_myzvJpKU@lZY<> literal 0 HcmV?d00001 diff --git a/assets/en/template/TEMPLATE_SIREN_Z23_g.gif b/assets/en/template/TEMPLATE_SIREN_Z23_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..c853ba694140c147b8c0188a686881a821b346f6 GIT binary patch literal 3200 zcmeI!`BT!}9>DQ00=bbOl46Q|5i1umF)U0oR1`FFFKxZJC6$(_6|DlHl3HO}T3Lbm zsJE;%t*orva_LboYSYwYr9QSyIi_AKZS$VyJHs^f%-mm|xxd`!`~l~j`OG=5d2{`I zS+0?IhzMms(Dd~5w{PFReEIVE^XE^WK21(ee*F0H!-o&=-@l)jm>3%y8yy`T9v*)4 z=FO{DuU@`<+27ya*Vp&>@#Eg!-tO*hjYiYi+1b(2(c0SD+}zyI&~WF@o!hr>*VWbC zx^=6zw)Wb!Yc(}Bm6es3FJHcN=~6{S#f1wO%FD}3OG`^iN{Wk%i;9ZWYW2Bu=gyuz zd*;lU!otE+r%vVP=N~_QJU2HtCnra#R31Hg^vIDT+1c4ySy`ExnTHM?QYaMZ>FH@{ zX>z$-CX-2}(gOz$?Afzt_wL<0cI-$_PEJZn+P-~ze0=R5C}GI+_+)GhIQ-KtzElz&6+h45fS0x;j31y3JVJh2?+@f4h{+m3JeSk z2nblYawVV7U$J6^zrR11%k}m3_4fAm^73M{*~^zN_wevowrrWZyStm4+tQ^=SuEC) zB}-gfT$oHIgTZiea$2}>;erJV?CtIC?Cj>xpFeNjJR2JuYinyNl?ub~?Af!;%*;$p zO^HOJfq?-AgMt2D|Emd5rws#q!a}`0gM6GFozST1X^1+7(W8R%>EG}0{hL5~(~yM| zxlQ0HJ*a1F6{LbuGC9GJz_wMf?kvT|~etH{!)mO~JH@8qI{cnnJ@C`P-4-zmvC zhz>NvL-ZMXkP17~JypY(GFtd)y7+nVu<_90a~^RML!&0IeDUrHb(6u)Wq7r#JP$XR zOKZ(0Eev6|TW|Uub`xMw$9i7|sto1jEcKP5Be^dBxY=jd!wERfQd>0rFm`txRX56;CIlGi6PN3E#vuM^5Z z82S43t5%Y!sVOZcK$7wCaUjXNckf0>C-@!lP6CW6cps;X3c(dr?QNK#W%fg$_%@87p?A0o-F zUAuPf+zC8MNl8IS5s5^JiHQJ~goFfO%hs)1f8r%NI$FyMux0)F_5YQZA8q0Bc)%7P zA0I6*z!pRmS65d6%i_h05m=m^owc?&IywSg92^{gFt)a~h%hu7O)CtALb0^8)H*{V zkpMN{U-#eZFPZ=lg65vs4>tlKOuPszCv_9rfJ~2I3nbDET>}zJ2}#mSj4(DlA7M{(vkwq>4vb1ETw3(wIHe?<@JS-aLbm3-2y{UO5ZPvGNv=Wol<35Uf zy0{{xRweZ#LAqq7xNKbXF$QE9Bl(vad~o%P z4jhSDS6Qi4>!eBk+z6lph>Ra;Ksw`B8W0f%1_po#NN_xS_z-XavZJS`2Pu!Pt}aj> z?d|PaAex$*K!PAl03dGMxB-l~e*HSAkgBRGU<62zix)4RKYzZgtW4V<00@vBz=#tk zP9WIZ&S0Ir1h>3~$(Fi0#024nZ z$m-RrL4t&ahJp(DDM3Jg`1$z(CXfmNQh*Ba^z_t*h_*t27g{N_2|}mS0TZA?v`l~y z0aAbvnKNe&!UPBr1PEheV;l~rtE>AL{gMCoZGaIbEFoKi`d$&%f_{Ni2cu8(FBB8l zD6|fl&x6@fG9w*`N9ZX{mqW&O6;oA>{#K4s6-;KUB%Nr_I;X~i+zg5n6VjusrDecA zq6c-VKjN}x*#?&Kqzx?^szbgVQx?#UiE8{IaHoxZ+c{O38(78lY zFU8KD$G!8tiyd|q4MSlA9Yj-QwoqMM9ze99);62NVOaA;+Y1wOOro(AQ-6BmBSN&1 z2_ry~cf%7KaG2o{A{2_SXsIFW-Y2SV;^58Z#Hd(FQMQl^Hxq6MDP2Jz(!_}ZC8Tqf XliF7@K%9}GH#^*ygAtFyFy!_h${@wV literal 0 HcmV?d00001 diff --git a/assets/jp/template/TEMPLATE_SIREN_AlfredoOriani.gif b/assets/jp/template/TEMPLATE_SIREN_AlfredoOriani.gif new file mode 100644 index 0000000000000000000000000000000000000000..ea60a90c966df8e7f47fd2560bd450a151ea2ca8 GIT binary patch literal 2152 zcmeIy=}*%K7zglQ4@xO5^Z>>sv_M(KibaXG(3%!|AV*V<1a!<$so0@lxfD;@+S-=G z6y_$^Oo!mcf`%)G7(*;&Ajm0*puFH7cuYV1J9FhzRyQ+kWXib z?EyTX0RXevyt%pg@#Dvhjg9s7^|iIN)z#Ja@87Sith{^oZh3imX=!O;Vd2f2H?Lp6 ze)a0r%a<=-ym&D?J3BKoGd(>$K0ZD+Hun7a^U=}Kr%#^_4-XFx4nBVTcwk_lzrVk) zudlbax2LD)(W6J5ot^FN?QLysCX=b9rRCned(F+wckkY9YHDg|XsEBRzkU05ZEbB$ zO-)r*Re5=NX=!OmNy+u=*RNf>R$N@1pP!$Xm#5R|v|8FMF&;qLD4>gwv` z8BKD`NmGy}Whe`j-AVQ4!9C4nwim^8LntcU{0|J2w!Qc93)DEb?U zYvNFRB3q84aI@wN_0>=q8J0*ficQu~M8<3a<-r3R0+F31#}efL1P##=7a?t>p59z0 zC1psWv?uMaY7R*5Be?m|1LTb8lP zo>FUm!F{jC9$J!w7VqLS=aA9Lusp}e+Snf&ZfejT5oO$o-8V1eS-I;&Q3v=DrCu(# zLW?HgA_F6JtodxFHP#-W%BSnLY3<7cr6a59jcSCvSMhf@qXkh|!4fxVjW|U94QBdC z3A#oo5MnUp&nwpxN zoSc}Lu%Pnn*)x!nCr_RX4Gn>)d?8AAcXwA;S4T(3XH;5STa8BJ{rmSpS}d+KHa6b5 za|cAFy1Lq6FjQ1jl$Di%sDPXl6&30A`ohA(f`S5&7K*vFHL?a`^D!xVSiwlbD#8=;&w*Dng+!A|e7r1>^*@ zgu~&m*=!bz6&xHK6cof@FhE!K?b`>^5*QdrqtQTGcJACsrBc73#oOB()a5fRZfj@;8Oa?!ziNy2BU?o>L(Qkf3$UQM(RiiTLRhXVZsA81gZxl{Qx9H zr7T6J`NPL2t&n&hI4vQF&4yw~dcMmd4u}E#L|URE$<~ETCI-@o1}b3h9hAdKn(3*e z$4RBl2BT7ia;eLKG|qIna*ofEQC%JU9>g4IwA7 rZ&6Xxe_eHFWTSfKy^;lX+jtIZxY0qru!5mp=_XdrcK%ET8yEZ!&Ovgh literal 0 HcmV?d00001 diff --git a/assets/jp/template/TEMPLATE_SIREN_Dido.gif b/assets/jp/template/TEMPLATE_SIREN_Dido.gif new file mode 100644 index 0000000000000000000000000000000000000000..07f0134f539313dffacf086a611b00aa772c1f01 GIT binary patch literal 2137 zcmeIyTTGK@7zgn8J3y;cC`=s;h@{;-G-{w4jEIbYK`jW_sZBUJ!6V2a9uevZJaw-j;bu3x+>-t8p3CQ-=lMm; zr2=7!6L0|)0N8A{j~_pN`0(NV`}gnOy?gui?c(C%!otFvH*a3Qe*NmztCufdTCLW( zxw+@hpU=+DK701;>C>l=A3uKh@ZrqN%=Gm1)YR0(#KgUO_io?5ee2e((b3VHH*b!N zj0_D84GaueESBrnuV1-xrLV8Ax3~AwrArqsTNJ-55V-8Vo*MQ02mAr(JNROvxROwM!D*NV~JpJ(oE5?V+RrfFgCd&@JQ5I z;xkPxKeCKTctl-w;HZ)W-l)X#t8d~^_8+Mk3^(4kj^v%x%u3fyJFGvVUfUog?+lT1wl#6F-)K}bRqy<9l{ZO zok*g%-rvjz$Ys=GlGsPFwRc1m!(juC;D2wW z;-Ap*;>8Q7$Nc>KG9XZoCr_S0Kps7Mv<%4Pm>0(`vO#m}oSb^73-ES`9IwD5|uy6p~U>Qc_%8TvSxFtV&*9UT$t~ zPEOA5-MgVJ5SEOLjP&$$XiG{;N^){?Vq#)KLV{AMTowlMBA3e{FVWG_5SHNJUB@h)J(@ip+#n+yuPveH(}179sc?K2afnE zniXezI${obe`Db1S*JuO&6)GnXxE|$!R&B63-C#D7(3#{<(hU<&O(~mkvR@<6m!Jc z1EtvP7{->-xquo*Jvf>M%b`MhybX~RiMoUU`68KZP^sEM6D~HaI?MC?ecLRuj-w-4 HED-z`qIgdU literal 0 HcmV?d00001 diff --git a/assets/jp/template/TEMPLATE_SIREN_Leipzig_g.gif b/assets/jp/template/TEMPLATE_SIREN_Leipzig_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..20f184edcbcf35d74c426f6fe971e568cefa22b8 GIT binary patch literal 3197 zcmeI!=Tp;L8o==rAhcizAI4OL3egoF}7il8E+qSw$Ak!Hafq$Gg=p@_gm z4WZfrQ9)4yETK79Kt;ssh>8rNgP^FOD0}YyW-`w8&g{FpZ+!lNbLN?IKJ)eBcse<6 z(*}6J2Vh}g;oG-wU%!5xpP&Eo<;&;KpJ!)hXJ%&JzkfeHJ^l9W+c$6Cyng+9Vq)Ue zt5@UW<1b&n9335f{`~pVr%#_ec``gaJTx@)@ZrP3!NCU)9^Aiwzpt+0$*UAlDf z;>Gjl&!0PY?#!7pH8nNW)zy`im1SjRrKP3C#l8FXJllgr>Cc;rXD+XEF~p{$KxG7eE8tOg9i>A*t>UcQc_Yv zLPA_zTx@LYu3fu!?%cU!$ByXe=%}cu@bGXhmm3xqwrSI*(9qEJ>({Saw=Ot1I4CG+ z?b@~e{{DV`e!jlGK0ZDi4#(Tu+sn(#)6gwX+ z;_U3~FVm@a5(UD{vR8l zEMWXRf`eGh01w+`G-cGn0-(&Ps8irwtp2#qAKwJ1FMz7zy1q`P$eI)C>%BHz%fLky zfVg`sOg<^7mY$c_v8F%`XE2Yls{lNVu^tdGOgOn30TXdzZ(Rlu;)8*XeMPmu&>;AC z=jnTqi!feMrnrDakn$_w7HW)Eu~d~+G_C4*DXsTt+`9d(4ekHtYg0VM-hoQ&GU_HN zBQMpWQ&X2{3!0^JZSu+^!z~+?m0CgRKd#?-RMGhxo?so@I26ZVph=FujYesgs6hkJ`}-)pq+kUxB`qxtF$IEhw@)LHKZVbaZfVfQUdr zARpG&)=-e8OP5lqR0|6WGcz-&iII_!p`jtvgg_wR@$k+5oWE}a(1$rI;dFIszPYt$ z5>3kWAuszE09}dO(g}F<-KH}28L2t)qrOhY73|oH4GX{{x$?~3}c{Vj%uo- zq+xY`Wta{A-cQNSEM@Let?Og5#HyS;rID1jOrn~mN|Zw#kEtYFaZ#e9Yu1TU!MR96 z0=EI+nAqSOZCk0Ciz7L5sra$1CGLklPgw@nxT|QORefpo9^zAOyv7kp+bAkPJK_F_ zFfl?yxX4~SHA|MN60{=TNo*1;F$9u@X4iPWl%MOEAQzxmXjL5o;Kei#q@^1=hRH;h z+aov_A;-Q;++Z&HK;xFFCu14tw13hE1dH@_RkcdYna0E1X%q7Q5CUn8j~_omA&}C5 zNWj>bnwo-COioTBy#ZqbY5{u#QUOZ?aRMm~m>JLsga=p}-QC@ggqt^S!p699=)s{whhShNCnxXQ zw-08<_dNXA7+be)g_!}7*t~f&;zUSD$e)}D3=D)+Al0#E%^DaU3NR2Ykm^{qY89*w zSQ;=hU}GSGv3&V*Lle1i!nsoHyquqlI|441Gw(Mu}pS>(l44sB8yGI zTO?v<0#F`VN-q8xsXmUOr{)DK`)cecbf>rCmiA#!TX(M%08j7O(z}l>i|L5jCME5Rpa10Ir6nNgxypMn%LnCI~KA0THpMPz5C*Dzb`E zIDj3nVGjt33L5NKX-C0=*c(Uo=iECPM|aLk_r*ESACM=xC--N*S72~}z*A1gm{=Ny z_4W0A{ra`HxA)7JFP}eu{`Be7$B!RBeE9JG{rk6X-@bnR`qisfJv}`yU%u?_?tby& z#j|J6y1KfaJbCi?@#FUP_O`aRM~@yoeE9J0-Mh`r&9`pdx^d%1Q&W>(ufKZr>Xj>3 z8XFrM8XD^A>S}9i&z(D0Q&XeU>CT)vbK=B_luG5=wQJX`S+ioriln5Z#KgoUOP0jN#l^lmllqpj}LqjJ{oG6t_LqbBvj~_pF?AYMo;DCSti9{k6i-khr=+UD` zjT+_a>+9p=e(12u6+^Iby^JcXxL_pYQ7GI&|pJAwz~ZJ3Bi%Iu05%h{xkO zI5^nb+jBS^J3BjDTU#3&8*6K8D=RBYOG_4uHE`g-{{8zi7z{d{PNUJNRH})I357x- z5{Y;`9{c%gN9KU!dNXY08g{^(FG9{moYqeh*bN?Paj?DH`l8xQ{owC_3V-l0)Z&EV1c}Ay4ww^Q6Z?$tMXT1_7hivx^ zODt=i9GGS0EF9h-hzQKIW%U=-#%xTb^cxm*LgVR?8l<-LNOZxyO{H4>GpW0{tDP5+ zH*oxlPR%6Bl>+azy*jC0hEJtiuz2A*bqLp$h?Q3{I2dIB-ga|7vo(OpmTcIjVbox$ z4!rQ0}C`}gk$SoZGS3#1em z7b8`)TJ4SFGIq_;3#o58%bc#RXt-a&j6xcrfq+w)_AKQiaWC4;V1O!oq^dWSX0s z8*CwwNZXFhJo5PH(XV^_Stl2G!x?<~{bZZ|w zvQxjq(b@el`_OM=ZG9AlPEG1kx~0HNn-|+ulx;O?{1^(C7ihNGIV{xQ(;;nqz6o2H zWbROZd5a+CX??J|)Y!<`UBOfKtIz(oAgTBMC%jlfqkD&i2MIgO3VqkEvtV(AI;9DN zSUj$!HYAC*uS2ocZ$LIh}l76Bbliy$NX{QL|+pcX+P0u=&MK!~`xxd9f#h7E%p zK`mk+0~HCV!R2xdIbvuLPy<>7;P_q*R3yLT%KYVC0!FZ%1X8V5WNb|3g-aD$k-DF1 zm0lQOl&m7#GG*rY)b*A#+=+ZXr>L6h6`5bvS|l>U_->ZMSdmI?LZ^1H<^r2hQ~ zSU9V)wvORw8)3a%nG&eRH{p5*Mz@y);$0WlIEi-J6Ou_L0;ibVHu%(^((AuH@y8#3 z{LhKNzRE&v_Kjq2)ZXbYeN z+<>ie=+Gfl6(}iSrGOGJQ4CO^o&YPhZrzHY$j!~o$;kmLva_={Y}oL_Zb4jp9~R4& zEnB*DDY${A3+$HY=xA6hV8yIivmhpbi^-EG8!`f*fVmPB6l4epSSctVV5R&l0}K`D z3Ge}x1X?M;0t^+jQedAzNx(!wETEV`{#2(CrHuswL9}q1TZbfr z8Swc2E5FjlbvRR7eh)*A1r1kyPLjupf*7XQs0K|&oBs}b^CW+_>WDG946bvJpb{UP z>=??^Q+t%gc|*oncWPA0rdc#Inu3)qKi@z%S=yP`)r!B6LFa{)S5*{AHYhOf{{YrH B|Be6v literal 0 HcmV?d00001 diff --git a/assets/jp/template/TEMPLATE_SIREN_Sirius.gif b/assets/jp/template/TEMPLATE_SIREN_Sirius.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e7d635f243d43ffa8c96f3b43df020ed81a6666 GIT binary patch literal 3117 zcmeI!Sxl2z6u|M@uPqcoD3;xVEsGQ_SQKgjhqOqU>ZmM(8y0O51V$u)1QCsvvWf@= z(J-=zjAG+P0dc{tvRMcsC<-cShec5o_XRiRm?mVR@nK#jKKQ+TZ#O6R{OqQdG7uUY8~gnE^QTXrK7Rc8;lqdb@86G(j=p>M?(N&RZ{ECl_3G7&7cZVafBx** zvyqXJCr_R{e*AcNc=*wyM?*tH4<0CW*ZQZ(c)22-c2?^`huaAq1i;0Pej*ect zc5P&2WO#UZXlUr_)vHA!(W+Ief`fyFLgCV-O9KM~eSLiwE?l@^!Giho=L-Y^KA-RH z?d|F5$!4=%U0t1>ogEz=9UL5NY;3Hpt*xxAEG;dmRH}uAg_)U|si~=vk&&UHp`M-| ziA2)Y*4EO}A`*#sJRbf0e$)xj$8;C*1Am{(^W)ESV&QRPV~9RVAkpEx%r9sDvL=W$ zf{NlfvJiocfJ~y4ED2YaA>^=R{-N`dF|wBsv6Os@oBz~8R=%B-Fo`}jP$5g%W;kW} z5!&hM1|8BgfA4}GUNbJ$)Fs-;&6$QtwSK8feHhZOt2@IUl6?In% zwIzc|T;(!EY}aC`Uo(Shf}83F85^7sHi7LOF`Te2#nx&9RjtdGrkjP8X20X&l=v#l z0;&AUw;8ckmI{^|;?l^*)J(t8zKZrDc4S*-i&U%KY)1S@+crvrPLWIajVSuuOF4Ti zSf@mEvUt9^BOO!7<3bw|k;Wttz8yiv_nZJEUcY_~PP}~i@_SS~eE1Nw7#tiNcLI>O zbLS2?ar^ddprX6G`^JqMSFc{}?Ck95=(uv_3Yei%shXRc>+9<;Uc3lqoI7{!?Afzt z&YY>MtJBC)TU)Eaqq@4fs;Ww%P-ql6di3ZxByzdDqM`yUDJdz@-~nLd<>i4H07hnJ zCQy-@nktn_larITY}vAT^X8|i^bqXP*6}nK!Cr$ zKOo`b<1=T@91jlJ~YO3}3zD&y(>gWD;T-%hahRfXx9&sx`2>_AChV8Mx2 z74vF*a*DAz9dhx@!Sn)7+_7bu%D?z{%=6GIBvIqG8<^^6lk=@<|3LvwEqn{X)2C0z zvjLhx1A!(RfCDIpe`kZnf}1yQLOFDGb%6zt4Q*|0t*xyP55PfFQBZ1AeF6{y zxxEa(kd|BwS;z!=Ws-@Go=X@V^w4=2BD=dl2&C^~dr%wws);9Ov_{urX`2(3``=ImtV_7_@7Ku`y zwTmY9BBF>H<-xjYwBk6PTs@JdcI?F^bY3kfTFR9W^Yc(sh aSLs|<-bQs@EaZw;`?TUQ_myzvJpKU@lZY<> literal 0 HcmV?d00001 diff --git a/assets/jp/template/TEMPLATE_SIREN_Z23_g.gif b/assets/jp/template/TEMPLATE_SIREN_Z23_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..c853ba694140c147b8c0188a686881a821b346f6 GIT binary patch literal 3200 zcmeI!`BT!}9>DQ00=bbOl46Q|5i1umF)U0oR1`FFFKxZJC6$(_6|DlHl3HO}T3Lbm zsJE;%t*orva_LboYSYwYr9QSyIi_AKZS$VyJHs^f%-mm|xxd`!`~l~j`OG=5d2{`I zS+0?IhzMms(Dd~5w{PFReEIVE^XE^WK21(ee*F0H!-o&=-@l)jm>3%y8yy`T9v*)4 z=FO{DuU@`<+27ya*Vp&>@#Eg!-tO*hjYiYi+1b(2(c0SD+}zyI&~WF@o!hr>*VWbC zx^=6zw)Wb!Yc(}Bm6es3FJHcN=~6{S#f1wO%FD}3OG`^iN{Wk%i;9ZWYW2Bu=gyuz zd*;lU!otE+r%vVP=N~_QJU2HtCnra#R31Hg^vIDT+1c4ySy`ExnTHM?QYaMZ>FH@{ zX>z$-CX-2}(gOz$?Afzt_wL<0cI-$_PEJZn+P-~ze0=R5C}GI+_+)GhIQ-KtzElz&6+h45fS0x;j31y3JVJh2?+@f4h{+m3JeSk z2nblYawVV7U$J6^zrR11%k}m3_4fAm^73M{*~^zN_wevowrrWZyStm4+tQ^=SuEC) zB}-gfT$oHIgTZiea$2}>;erJV?CtIC?Cj>xpFeNjJR2JuYinyNl?ub~?Af!;%*;$p zO^HOJfq?-AgMt2D|Emd5rws#q!a}`0gM6GFozST1X^1+7(W8R%>EG}0{hL5~(~yM| zxlQ0HJ*a1F6{LbuGC9GJz_wMf?kvT|~etH{!)mO~JH@8qI{cnnJ@C`P-4-zmvC zhz>NvL-ZMXkP17~JypY(GFtd)y7+nVu<_90a~^RML!&0IeDUrHb(6u)Wq7r#JP$XR zOKZ(0Eev6|TW|Uub`xMw$9i7|sto1jEcKP5Be^dBxY=jd!wERfQd>0rFm`txRX56;CIlGi6PN3E#vuM^5Z z82S43t5%Y!sVOZcK$7wCaUjXNckf0>C-@!lP6CW6cps;X3c(dr?QNK#W%fg$_%@87p?A0o-F zUAuPf+zC8MNl8IS5s5^JiHQJ~goFfO%hs)1f8r%NI$FyMux0)F_5YQZA8q0Bc)%7P zA0I6*z!pRmS65d6%i_h05m=m^owc?&IywSg92^{gFt)a~h%hu7O)CtALb0^8)H*{V zkpMN{U-#eZFPZ=lg65vs4>tlKOuPszCv_9rfJ~2I3nbDET>}zJ2}#mSj4(DlA7M{(vkwq>4vb1ETw3(wIHe?<@JS-aLbm3-2y{UO5ZPvGNv=Wol<35Uf zy0{{xRweZ#LAqq7xNKbXF$QE9Bl(vad~o%P z4jhSDS6Qi4>!eBk+z6lph>Ra;Ksw`B8W0f%1_po#NN_xS_z-XavZJS`2Pu!Pt}aj> z?d|PaAex$*K!PAl03dGMxB-l~e*HSAkgBRGU<62zix)4RKYzZgtW4V<00@vBz=#tk zP9WIZ&S0Ir1h>3~$(Fi0#024nZ z$m-RrL4t&ahJp(DDM3Jg`1$z(CXfmNQh*Ba^z_t*h_*t27g{N_2|}mS0TZA?v`l~y z0aAbvnKNe&!UPBr1PEheV;l~rtE>AL{gMCoZGaIbEFoKi`d$&%f_{Ni2cu8(FBB8l zD6|fl&x6@fG9w*`N9ZX{mqW&O6;oA>{#K4s6-;KUB%Nr_I;X~i+zg5n6VjusrDecA zq6c-VKjN}x*#?&Kqzx?^szbgVQx?#UiE8{IaHoxZ+c{O38(78lY zFU8KD$G!8tiyd|q4MSlA9Yj-QwoqMM9ze99);62NVOaA;+Y1wOOro(AQ-6BmBSN&1 z2_ry~cf%7KaG2o{A{2_SXsIFW-Y2SV;^58Z#Hd(FQMQl^Hxq6MDP2Jz(!_}ZC8Tqf XliF7@K%9}GH#^*ygAtFyFy!_h${@wV literal 0 HcmV?d00001 diff --git a/assets/tw/template/TEMPLATE_SIREN_AlfredoOriani.gif b/assets/tw/template/TEMPLATE_SIREN_AlfredoOriani.gif new file mode 100644 index 0000000000000000000000000000000000000000..ea60a90c966df8e7f47fd2560bd450a151ea2ca8 GIT binary patch literal 2152 zcmeIy=}*%K7zglQ4@xO5^Z>>sv_M(KibaXG(3%!|AV*V<1a!<$so0@lxfD;@+S-=G z6y_$^Oo!mcf`%)G7(*;&Ajm0*puFH7cuYV1J9FhzRyQ+kWXib z?EyTX0RXevyt%pg@#Dvhjg9s7^|iIN)z#Ja@87Sith{^oZh3imX=!O;Vd2f2H?Lp6 ze)a0r%a<=-ym&D?J3BKoGd(>$K0ZD+Hun7a^U=}Kr%#^_4-XFx4nBVTcwk_lzrVk) zudlbax2LD)(W6J5ot^FN?QLysCX=b9rRCned(F+wckkY9YHDg|XsEBRzkU05ZEbB$ zO-)r*Re5=NX=!OmNy+u=*RNf>R$N@1pP!$Xm#5R|v|8FMF&;qLD4>gwv` z8BKD`NmGy}Whe`j-AVQ4!9C4nwim^8LntcU{0|J2w!Qc93)DEb?U zYvNFRB3q84aI@wN_0>=q8J0*ficQu~M8<3a<-r3R0+F31#}efL1P##=7a?t>p59z0 zC1psWv?uMaY7R*5Be?m|1LTb8lP zo>FUm!F{jC9$J!w7VqLS=aA9Lusp}e+Snf&ZfejT5oO$o-8V1eS-I;&Q3v=DrCu(# zLW?HgA_F6JtodxFHP#-W%BSnLY3<7cr6a59jcSCvSMhf@qXkh|!4fxVjW|U94QBdC z3A#oo5MnUp&nwpxN zoSc}Lu%Pnn*)x!nCr_RX4Gn>)d?8AAcXwA;S4T(3XH;5STa8BJ{rmSpS}d+KHa6b5 za|cAFy1Lq6FjQ1jl$Di%sDPXl6&30A`ohA(f`S5&7K*vFHL?a`^D!xVSiwlbD#8=;&w*Dng+!A|e7r1>^*@ zgu~&m*=!bz6&xHK6cof@FhE!K?b`>^5*QdrqtQTGcJACsrBc73#oOB()a5fRZfj@;8Oa?!ziNy2BU?o>L(Qkf3$UQM(RiiTLRhXVZsA81gZxl{Qx9H zr7T6J`NPL2t&n&hI4vQF&4yw~dcMmd4u}E#L|URE$<~ETCI-@o1}b3h9hAdKn(3*e z$4RBl2BT7ia;eLKG|qIna*ofEQC%JU9>g4IwA7 rZ&6Xxe_eHFWTSfKy^;lX+jtIZxY0qru!5mp=_XdrcK%ET8yEZ!&Ovgh literal 0 HcmV?d00001 diff --git a/assets/tw/template/TEMPLATE_SIREN_Dido.gif b/assets/tw/template/TEMPLATE_SIREN_Dido.gif new file mode 100644 index 0000000000000000000000000000000000000000..07f0134f539313dffacf086a611b00aa772c1f01 GIT binary patch literal 2137 zcmeIyTTGK@7zgn8J3y;cC`=s;h@{;-G-{w4jEIbYK`jW_sZBUJ!6V2a9uevZJaw-j;bu3x+>-t8p3CQ-=lMm; zr2=7!6L0|)0N8A{j~_pN`0(NV`}gnOy?gui?c(C%!otFvH*a3Qe*NmztCufdTCLW( zxw+@hpU=+DK701;>C>l=A3uKh@ZrqN%=Gm1)YR0(#KgUO_io?5ee2e((b3VHH*b!N zj0_D84GaueESBrnuV1-xrLV8Ax3~AwrArqsTNJ-55V-8Vo*MQ02mAr(JNROvxROwM!D*NV~JpJ(oE5?V+RrfFgCd&@JQ5I z;xkPxKeCKTctl-w;HZ)W-l)X#t8d~^_8+Mk3^(4kj^v%x%u3fyJFGvVUfUog?+lT1wl#6F-)K}bRqy<9l{ZO zok*g%-rvjz$Ys=GlGsPFwRc1m!(juC;D2wW z;-Ap*;>8Q7$Nc>KG9XZoCr_S0Kps7Mv<%4Pm>0(`vO#m}oSb^73-ES`9IwD5|uy6p~U>Qc_%8TvSxFtV&*9UT$t~ zPEOA5-MgVJ5SEOLjP&$$XiG{;N^){?Vq#)KLV{AMTowlMBA3e{FVWG_5SHNJUB@h)J(@ip+#n+yuPveH(}179sc?K2afnE zniXezI${obe`Db1S*JuO&6)GnXxE|$!R&B63-C#D7(3#{<(hU<&O(~mkvR@<6m!Jc z1EtvP7{->-xquo*Jvf>M%b`MhybX~RiMoUU`68KZP^sEM6D~HaI?MC?ecLRuj-w-4 HED-z`qIgdU literal 0 HcmV?d00001 diff --git a/assets/tw/template/TEMPLATE_SIREN_Leipzig_g.gif b/assets/tw/template/TEMPLATE_SIREN_Leipzig_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..20f184edcbcf35d74c426f6fe971e568cefa22b8 GIT binary patch literal 3197 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 B|Be6v literal 0 HcmV?d00001 diff --git a/assets/tw/template/TEMPLATE_SIREN_Sirius.gif b/assets/tw/template/TEMPLATE_SIREN_Sirius.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e7d635f243d43ffa8c96f3b43df020ed81a6666 GIT binary patch literal 3117 zcmeI!Sxl2z6u|M@uPqcoD3;xVEsGQ_SQKgjhqOqU>ZmM(8y0O51V$u)1QCsvvWf@= z(J-=zjAG+P0dc{tvRMcsC<-cShec5o_XRiRm?mVR@nK#jKKQ+TZ#O6R{OqQdG7uUY8~gnE^QTXrK7Rc8;lqdb@86G(j=p>M?(N&RZ{ECl_3G7&7cZVafBx** zvyqXJCr_R{e*AcNc=*wyM?*tH4<0CW*ZQZ(c)22-c2?^`huaAq1i;0Pej*ect zc5P&2WO#UZXlUr_)vHA!(W+Ief`fyFLgCV-O9KM~eSLiwE?l@^!Giho=L-Y^KA-RH z?d|F5$!4=%U0t1>ogEz=9UL5NY;3Hpt*xxAEG;dmRH}uAg_)U|si~=vk&&UHp`M-| ziA2)Y*4EO}A`*#sJRbf0e$)xj$8;C*1Am{(^W)ESV&QRPV~9RVAkpEx%r9sDvL=W$ zf{NlfvJiocfJ~y4ED2YaA>^=R{-N`dF|wBsv6Os@oBz~8R=%B-Fo`}jP$5g%W;kW} z5!&hM1|8BgfA4}GUNbJ$)Fs-;&6$QtwSK8feHhZOt2@IUl6?In% zwIzc|T;(!EY}aC`Uo(Shf}83F85^7sHi7LOF`Te2#nx&9RjtdGrkjP8X20X&l=v#l z0;&AUw;8ckmI{^|;?l^*)J(t8zKZrDc4S*-i&U%KY)1S@+crvrPLWIajVSuuOF4Ti zSf@mEvUt9^BOO!7<3bw|k;Wttz8yiv_nZJEUcY_~PP}~i@_SS~eE1Nw7#tiNcLI>O zbLS2?ar^ddprX6G`^JqMSFc{}?Ck95=(uv_3Yei%shXRc>+9<;Uc3lqoI7{!?Afzt z&YY>MtJBC)TU)Eaqq@4fs;Ww%P-ql6di3ZxByzdDqM`yUDJdz@-~nLd<>i4H07hnJ zCQy-@nktn_larITY}vAT^X8|i^bqXP*6}nK!Cr$ zKOo`b<1=T@91jlJ~YO3}3zD&y(>gWD;T-%hahRfXx9&sx`2>_AChV8Mx2 z74vF*a*DAz9dhx@!Sn)7+_7bu%D?z{%=6GIBvIqG8<^^6lk=@<|3LvwEqn{X)2C0z zvjLhx1A!(RfCDIpe`kZnf}1yQLOFDGb%6zt4Q*|0t*xyP55PfFQBZ1AeF6{y zxxEa(kd|BwS;z!=Ws-@Go=X@V^w4=2BD=dl2&C^~dr%wws);9Ov_{urX`2(3``=ImtV_7_@7Ku`y zwTmY9BBF>H<-xjYwBk6PTs@JdcI?F^bY3kfTFR9W^Yc(sh aSLs|<-bQs@EaZw;`?TUQ_myzvJpKU@lZY<> literal 0 HcmV?d00001 diff --git a/assets/tw/template/TEMPLATE_SIREN_Z23_g.gif b/assets/tw/template/TEMPLATE_SIREN_Z23_g.gif new file mode 100644 index 0000000000000000000000000000000000000000..c853ba694140c147b8c0188a686881a821b346f6 GIT binary patch literal 3200 zcmeI!`BT!}9>DQ00=bbOl46Q|5i1umF)U0oR1`FFFKxZJC6$(_6|DlHl3HO}T3Lbm zsJE;%t*orva_LboYSYwYr9QSyIi_AKZS$VyJHs^f%-mm|xxd`!`~l~j`OG=5d2{`I zS+0?IhzMms(Dd~5w{PFReEIVE^XE^WK21(ee*F0H!-o&=-@l)jm>3%y8yy`T9v*)4 z=FO{DuU@`<+27ya*Vp&>@#Eg!-tO*hjYiYi+1b(2(c0SD+}zyI&~WF@o!hr>*VWbC zx^=6zw)Wb!Yc(}Bm6es3FJHcN=~6{S#f1wO%FD}3OG`^iN{Wk%i;9ZWYW2Bu=gyuz zd*;lU!otE+r%vVP=N~_QJU2HtCnra#R31Hg^vIDT+1c4ySy`ExnTHM?QYaMZ>FH@{ zX>z$-CX-2}(gOz$?Afzt_wL<0cI-$_PEJZn+P-~ze0=R5C}GI+_+)GhIQ-KtzElz&6+h45fS0x;j31y3JVJh2?+@f4h{+m3JeSk z2nblYawVV7U$J6^zrR11%k}m3_4fAm^73M{*~^zN_wevowrrWZyStm4+tQ^=SuEC) zB}-gfT$oHIgTZiea$2}>;erJV?CtIC?Cj>xpFeNjJR2JuYinyNl?ub~?Af!;%*;$p zO^HOJfq?-AgMt2D|Emd5rws#q!a}`0gM6GFozST1X^1+7(W8R%>EG}0{hL5~(~yM| zxlQ0HJ*a1F6{LbuGC9GJz_wMf?kvT|~etH{!)mO~JH@8qI{cnnJ@C`P-4-zmvC zhz>NvL-ZMXkP17~JypY(GFtd)y7+nVu<_90a~^RML!&0IeDUrHb(6u)Wq7r#JP$XR zOKZ(0Eev6|TW|Uub`xMw$9i7|sto1jEcKP5Be^dBxY=jd!wERfQd>0rFm`txRX56;CIlGi6PN3E#vuM^5Z z82S43t5%Y!sVOZcK$7wCaUjXNckf0>C-@!lP6CW6cps;X3c(dr?QNK#W%fg$_%@87p?A0o-F zUAuPf+zC8MNl8IS5s5^JiHQJ~goFfO%hs)1f8r%NI$FyMux0)F_5YQZA8q0Bc)%7P zA0I6*z!pRmS65d6%i_h05m=m^owc?&IywSg92^{gFt)a~h%hu7O)CtALb0^8)H*{V zkpMN{U-#eZFPZ=lg65vs4>tlKOuPszCv_9rfJ~2I3nbDET>}zJ2}#mSj4(DlA7M{(vkwq>4vb1ETw3(wIHe?<@JS-aLbm3-2y{UO5ZPvGNv=Wol<35Uf zy0{{xRweZ#LAqq7xNKbXF$QE9Bl(vad~o%P z4jhSDS6Qi4>!eBk+z6lph>Ra;Ksw`B8W0f%1_po#NN_xS_z-XavZJS`2Pu!Pt}aj> z?d|PaAex$*K!PAl03dGMxB-l~e*HSAkgBRGU<62zix)4RKYzZgtW4V<00@vBz=#tk zP9WIZ&S0Ir1h>3~$(Fi0#024nZ z$m-RrL4t&ahJp(DDM3Jg`1$z(CXfmNQh*Ba^z_t*h_*t27g{N_2|}mS0TZA?v`l~y z0aAbvnKNe&!UPBr1PEheV;l~rtE>AL{gMCoZGaIbEFoKi`d$&%f_{Ni2cu8(FBB8l zD6|fl&x6@fG9w*`N9ZX{mqW&O6;oA>{#K4s6-;KUB%Nr_I;X~i+zg5n6VjusrDecA zq6c-VKjN}x*#?&Kqzx?^szbgVQx?#UiE8{IaHoxZ+c{O38(78lY zFU8KD$G!8tiyd|q4MSlA9Yj-QwoqMM9ze99);62NVOaA;+Y1wOOro(AQ-6BmBSN&1 z2_ry~cf%7KaG2o{A{2_SXsIFW-Y2SV;^58Z#Hd(FQMQl^Hxq6MDP2Jz(!_}ZC8Tqf XliF7@K%9}GH#^*ygAtFyFy!_h${@wV literal 0 HcmV?d00001 diff --git a/campaign/event_20240725_cn/t1.py b/campaign/event_20240725_cn/t1.py new file mode 100644 index 0000000000..5d9fc56ff4 --- /dev/null +++ b/campaign/event_20240725_cn/t1.py @@ -0,0 +1,76 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('T1') +MAP.shape = 'I7' +MAP.camera_data = ['D2', 'D5', 'F2', 'F5'] +MAP.camera_data_spawn_point = ['F2', 'D2'] +MAP.map_data = """ + ++ ++ -- -- SP -- SP -- -- + MB ++ ME -- -- -- -- -- ++ + -- ME -- -- -- MS -- -- ME + -- -- __ -- Me ++ Me -- -- + -- ME -- -- -- ++ -- -- ME + ++ ++ -- Me -- MS -- Me -- + ++ ++ ME -- ME -- ME -- ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Sirius', 'Dido'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + STAGE_ENTRANCE = ['normal', '20240725'] + MAP_SWIPE_MULTIPLY = (1.236, 1.259) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.195, 1.217) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.160, 1.181) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/campaign/event_20240725_cn/t2.py b/campaign/event_20240725_cn/t2.py new file mode 100644 index 0000000000..4798088fd5 --- /dev/null +++ b/campaign/event_20240725_cn/t2.py @@ -0,0 +1,77 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .t1 import Config as ConfigBase + +MAP = CampaignMap('T2') +MAP.shape = 'I7' +MAP.camera_data = ['D2', 'D5', 'F2', 'F5'] +MAP.camera_data_spawn_point = ['F2'] +MAP.map_data = """ + -- ++ -- Me -- MS ++ ++ ++ + ME ++ Me -- -- -- -- SP -- + -- -- -- -- -- MS -- -- SP + ME -- ME -- Me ++ ++ -- -- + -- ++ -- __ -- ++ ++ MS Me + ++ ME -- ME -- ME ME -- ++ + MB -- -- ++ Me -- -- ME -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 1}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Z23_g', 'Leipzig_g'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + STAGE_ENTRANCE = ['normal', '20240725'] + MAP_SWIPE_MULTIPLY = (1.189, 1.211) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.150, 1.171) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.116, 1.137) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/campaign/event_20240725_cn/t3.py b/campaign/event_20240725_cn/t3.py new file mode 100644 index 0000000000..1257a531ce --- /dev/null +++ b/campaign/event_20240725_cn/t3.py @@ -0,0 +1,81 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .t1 import Config as ConfigBase + +MAP = CampaignMap('T3') +MAP.shape = 'I8' +MAP.camera_data = ['D2', 'D6', 'F2', 'F6'] +MAP.camera_data_spawn_point = ['D6'] +MAP.map_data = """ + ++ ++ Me -- MB -- ++ -- -- + ++ ++ -- ME -- ME ++ Me ME + -- ME -- -- __ -- Me -- -- + -- ME -- ++ -- -- -- -- ME + -- ++ MS ++ MS ++ -- -- ME + -- Me -- -- -- MS -- Me -- + ME -- -- -- -- -- -- ++ ++ + ++ ++ ++ SP SP ++ ME -- ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 2}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4}, + {'battle': 5, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['PompeoMagno', 'AlfredoOriani'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + STAGE_ENTRANCE = ['normal', '20240725'] + MAP_SWIPE_MULTIPLY = (1.089, 1.109) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.053, 1.072) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.022, 1.041) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/module/template/assets.py b/module/template/assets.py index b973660f10..862ba08ed7 100644 --- a/module/template/assets.py +++ b/module/template/assets.py @@ -42,6 +42,7 @@ TEMPLATE_SIREN_Akagi = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Akagi.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Akagi.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Akagi.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Akagi.gif'}) TEMPLATE_SIREN_Akashi = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Akashi.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Akashi.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Akashi.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Akashi.gif'}) TEMPLATE_SIREN_AlbacoreIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_AlbacoreIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_AlbacoreIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_AlbacoreIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_AlbacoreIdol.gif'}) +TEMPLATE_SIREN_AlfredoOriani = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_AlfredoOriani.gif', 'en': './assets/en/template/TEMPLATE_SIREN_AlfredoOriani.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_AlfredoOriani.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_AlfredoOriani.gif'}) TEMPLATE_SIREN_Algerie = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Algerie.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Algerie.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Algerie.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Algerie.gif'}) TEMPLATE_SIREN_Amazon = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Amazon.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Amazon.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Amazon.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Amazon.gif'}) TEMPLATE_SIREN_Arethusa = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Arethusa.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Arethusa.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Arethusa.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Arethusa.gif'}) @@ -85,6 +86,7 @@ TEMPLATE_SIREN_Dace = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dace.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dace.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dace.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dace.gif'}) TEMPLATE_SIREN_Deutschland = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Deutschland.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Deutschland.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Deutschland.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Deutschland.gif'}) TEMPLATE_SIREN_Dewey = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dewey.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dewey.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dewey.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dewey.gif'}) +TEMPLATE_SIREN_Dido = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dido.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dido.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dido.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dido.gif'}) TEMPLATE_SIREN_DidoIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DidoIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DidoIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DidoIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DidoIdol.gif'}) TEMPLATE_SIREN_DidoIdol2 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DidoIdol2.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DidoIdol2.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DidoIdol2.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DidoIdol2.gif'}) TEMPLATE_SIREN_Dilloy = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dilloy.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dilloy.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dilloy.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dilloy.gif'}) @@ -138,6 +140,7 @@ TEMPLATE_SIREN_LeMars = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LeMars.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LeMars.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LeMars.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LeMars.gif'}) TEMPLATE_SIREN_LeMars_ghost = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LeMars_ghost.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LeMars_ghost.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LeMars_ghost.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LeMars_ghost.gif'}) TEMPLATE_SIREN_Leipzig = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Leipzig.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Leipzig.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Leipzig.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Leipzig.gif'}) +TEMPLATE_SIREN_Leipzig_g = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Leipzig_g.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Leipzig_g.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Leipzig_g.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Leipzig_g.gif'}) TEMPLATE_SIREN_Lexington = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Lexington.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Lexington.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Lexington.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Lexington.gif'}) TEMPLATE_SIREN_Littorio = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Littorio.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Littorio.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Littorio.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Littorio.gif'}) TEMPLATE_SIREN_Lover = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Lover.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Lover.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Lover.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Lover.gif'}) @@ -157,6 +160,7 @@ TEMPLATE_SIREN_NyotenguDOA = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_NyotenguDOA.gif', 'en': './assets/en/template/TEMPLATE_SIREN_NyotenguDOA.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_NyotenguDOA.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_NyotenguDOA.gif'}) TEMPLATE_SIREN_Odin = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Odin.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Odin.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Odin.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Odin.gif'}) TEMPLATE_SIREN_PeterStrasser = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_PeterStrasser.gif', 'en': './assets/en/template/TEMPLATE_SIREN_PeterStrasser.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_PeterStrasser.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_PeterStrasser.gif'}) +TEMPLATE_SIREN_PompeoMagno = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_PompeoMagno.gif', 'en': './assets/en/template/TEMPLATE_SIREN_PompeoMagno.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_PompeoMagno.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_PompeoMagno.gif'}) TEMPLATE_SIREN_PrinceOfWales = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_PrinceOfWales.gif', 'en': './assets/en/template/TEMPLATE_SIREN_PrinceOfWales.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_PrinceOfWales.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_PrinceOfWales.gif'}) TEMPLATE_SIREN_PrinzAdalbert = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_PrinzAdalbert.gif', 'en': './assets/en/template/TEMPLATE_SIREN_PrinzAdalbert.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_PrinzAdalbert.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_PrinzAdalbert.gif'}) TEMPLATE_SIREN_PrinzEugen = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_PrinzEugen.gif', 'en': './assets/en/template/TEMPLATE_SIREN_PrinzEugen.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_PrinzEugen.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_PrinzEugen.gif'}) @@ -186,6 +190,7 @@ TEMPLATE_SIREN_SirenBoss182 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss182.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss182.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss182.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss182.gif'}) TEMPLATE_SIREN_SirenBoss19 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SirenBoss19.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SirenBoss19.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SirenBoss19.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SirenBoss19.gif'}) TEMPLATE_SIREN_Sirenboss10 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sirenboss10.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sirenboss10.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sirenboss10.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sirenboss10.gif'}) +TEMPLATE_SIREN_Sirius = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Sirius.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Sirius.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Sirius.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Sirius.gif'}) TEMPLATE_SIREN_Soobrazitelny = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Soobrazitelny.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Soobrazitelny.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Soobrazitelny.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Soobrazitelny.gif'}) TEMPLATE_SIREN_Spee = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Spee.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Spee.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Spee.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Spee.gif'}) TEMPLATE_SIREN_SpeeIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_SpeeIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_SpeeIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_SpeeIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_SpeeIdol.gif'}) @@ -214,6 +219,7 @@ TEMPLATE_SIREN_Z19 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z19.png', 'en': './assets/en/template/TEMPLATE_SIREN_Z19.png', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z19.png', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z19.png'}) TEMPLATE_SIREN_Z2 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z2.png', 'en': './assets/en/template/TEMPLATE_SIREN_Z2.png', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z2.png', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z2.png'}) TEMPLATE_SIREN_Z23_5 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z23_5.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Z23_5.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z23_5.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z23_5.gif'}) +TEMPLATE_SIREN_Z23_g = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z23_g.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Z23_g.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z23_g.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z23_g.gif'}) TEMPLATE_SIREN_Z24 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z24.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Z24.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z24.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z24.gif'}) TEMPLATE_SIREN_Z46 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Z46.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Z46.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Z46.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Z46.gif'}) TEMPLATE_SIREN_Zuiho = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Zuiho.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Zuiho.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Zuiho.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Zuiho.gif'}) From 5e05db05d074aac425212b6cc46de4bed099110f Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 02:09:55 +0800 Subject: [PATCH 151/161] Add: Switch mode in map preparation --- assets/cn/map/MAP_MODE_SWITCH_HARD.png | Bin 0 -> 7510 bytes assets/cn/map/MAP_MODE_SWITCH_NORMAL.png | Bin 0 -> 7510 bytes assets/en/map/MAP_MODE_SWITCH_HARD.png | Bin 0 -> 7510 bytes assets/en/map/MAP_MODE_SWITCH_NORMAL.png | Bin 0 -> 7510 bytes assets/jp/map/MAP_MODE_SWITCH_HARD.png | Bin 0 -> 7510 bytes assets/jp/map/MAP_MODE_SWITCH_NORMAL.png | Bin 0 -> 7510 bytes assets/tw/map/MAP_MODE_SWITCH_HARD.png | Bin 0 -> 7510 bytes assets/tw/map/MAP_MODE_SWITCH_NORMAL.png | Bin 0 -> 7510 bytes campaign/event_20240725_cn/campaign_base.py | 3 ++ module/campaign/campaign_ui.py | 38 ++++++++++++++-- module/config/config_manual.py | 1 + module/map/assets.py | 2 + module/map/map_operation.py | 46 +++++++++++++++++++- 13 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 assets/cn/map/MAP_MODE_SWITCH_HARD.png create mode 100644 assets/cn/map/MAP_MODE_SWITCH_NORMAL.png create mode 100644 assets/en/map/MAP_MODE_SWITCH_HARD.png create mode 100644 assets/en/map/MAP_MODE_SWITCH_NORMAL.png create mode 100644 assets/jp/map/MAP_MODE_SWITCH_HARD.png create mode 100644 assets/jp/map/MAP_MODE_SWITCH_NORMAL.png create mode 100644 assets/tw/map/MAP_MODE_SWITCH_HARD.png create mode 100644 assets/tw/map/MAP_MODE_SWITCH_NORMAL.png diff --git a/assets/cn/map/MAP_MODE_SWITCH_HARD.png b/assets/cn/map/MAP_MODE_SWITCH_HARD.png new file mode 100644 index 0000000000000000000000000000000000000000..fcff54fe99b7951c8798f378f5dc9bbd6544f58a GIT binary patch literal 7510 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 PpAR%tbyUh8+PwG=$Q!kP literal 0 HcmV?d00001 diff --git a/assets/cn/map/MAP_MODE_SWITCH_NORMAL.png b/assets/cn/map/MAP_MODE_SWITCH_NORMAL.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b75976efdce73f815b2be5342da4209016f8df GIT binary patch literal 7510 zcmeI1_cPqz_s3sag6JVy5QKE1hE=0Pi7r}%RiZD8=sl6J2~nbKSS@O>h!VXfYF3Hf ztzZd@Rd=&kKHl&5|L~pf4>>b;&Yih4cg{TS^UOWx-nRyN8uYYWv;Y9mYiX((0RYwc zl>8?(`MFc((v9d~-!#LxXTzTgQ{f&1)HJCxd}Qj1kN%!ilT7 zu7Ir38+p(aU%b9%XJ1PMIh*DpnuIL z&LSHPgw23!iNK-&%i1Nf1S6nV%+&CBURNgqs0lFcTD>J!D^e+uVvRff-Z48BMg3n5D^iO8Ya27!xRSqRgB1 z$)H!B=PK=`OIt#ApIYuO$0(jj{DLmVgyGLuPiKR60IziAw6gZRNRpSWhhMo-+!~5f zMa+Y+CV1NOCY%&^Yj{br^4VRscf00q>kR~SdjcDb64m(oQkGcO!^L;`Qb{yP{`3Gc zO21vN_TpC*q&;k#j684%@#M%XKLcU~-Za4vU5CfLhMW+g~1Dr0ZZswlidO@HDrxliOZUQ>MG#F3+TLVI|@!?I(`Zhn!E^ zK~t1pzff@HFsJ`ws_77upu3qna@?sU&L@Q0fy?frQvhSZMA0^E8UK4+J@~&A!Ecs=X zA;lQEuX4j@KCXA&IWLBNBpIzDQghvgxoUV7B5HGsh9Cd-^#MZP#o0#;i!nIHPTM|< zjaIjLYtALybHyHGE@c`oWzS>&z>kf)J)+H@B%~{QR`NsK*BTOzCI>Oil_r8S{>Z%CjHF#oO0%L;yhvUBQD|M`(F4&R| zP40E0{A3nwDF-DCWkI&MN?J~d;XrZL7Jo8J;%>L)Hu5MvZQai&;a6!Dk}e8 z-uTaS7bh2jv*@}{Bh7pyuR8oEe8lbd#r3uI$OC^m`Y8Iy9!r-0!lGW2}@Zkd1CUyL}@&*VO9ikiIwGN0?U0fS4s&kuwstP!v-zF* zT`R4(29n*<9UF&hMt%DGd+*L6r;vS{akAqXZdtai)6KId@81-tXLM=DNXPV~&ZLH* zs8V%P3CNG8il%|4ho+NAt%+yYyIA)LGl&7V8G;T0LZE6tmm$hJ{SfYG)+N^XTO+p) zjXh?>HcvKbH`6ySY{}wn@j`fa{18w7RTiEJIUl(YZy(=&KZs{DHvgC8_u|gt@9Snx z+)kffWbR9`7d}ovvB3S{?8fdz?}|LKgHS0LuIbnB&;lU2Ue95bSIkN~+^ld+3P1M@Y+QRy{g;&2iLpW>VzI*R%r{!V`D%b|3 zsNirkxK5$itw^37lpWn#*eZZlXcY{b2ulmQc#?H2cZ4Fv|7rUbxOZh&kIIIugA7B~ z@{;RiHH8}mA7vV4!9^*m_f$<6MClaSROo2vE%>~XpzZ8?+(dhcIj6hn!)!mOGufVU zAKmX4*5RFCj*A+iMl*#$3yC`G{k7DvMGXk7p3j9djhz_Edh51$iO7awjj@lH58>_v za>e4bVzBsl)Zq~Kd+s_H<3NeD%ZzAd=>PzZh21Nf+--WaIT)s}T^>(Fms#D?Z z@(c1cNV-JEO8S#jANz95Ty6vV>iQrm%U|9?$|4~|=FLYIc$E?+@|uc)D2G0#$U~Rs z^4@F28-4ZG-1oVLm38&n@*h?+O;hkP^Kzl8N6u0Dk9@l&X!MQl1SWERXb{}jHC3Bf zKn_iNP2ZDE&PdM4Gop7%t+_LKzwTz8zT=%cN%Sj$tL{IzlcIkRRs@MJ_Gj53Z*+$9 zYK+Pa7_R7ue!df6$zd5#ui2vcbW8&6c)CFpy!7fyNK3*OgZeMkQ}qEByNd#C>P=S! zV~pATMZ~O(8lMNa?2Lq>oKaP&B!eY$Ba=PXz`4CRqsWnh0@XXO%p_BallxE}wauuHL>|v4s2%z;?%nqoZm_%dg3qTQ*TcAhnt>k!6jHWQTbBK`$l1M5(s}rx%y27| zDXFZqu60^?W}&*i0Qb2E({4? zHu(K%@cZ2N0d!h;-61trGw{v`sLdC7FV8(k;ZqClcWhoJk2Q}*Yu#BGPHfrUw?``8 zEGsATrot|9&hk+-UfbYy0?%$v=}KR@s3q zxb98vIuGIbtMx*ShkyL=TkQU{+d6YrLGb|^05$Hz_2)w1_|FzHuORng_4A7Q3vf!G(NYiQEa z@#7)$lc}9c#F(MB^1$MKZ(M0{1*k``hp%}sP+_<5q-=0D6jzUHMEA7trQJMHJ~h}M zKC6Aui0xF=>kBI5CB9(0s!&s+^++G?|lH+umJ$j zM*v`legJnp0sxjsEwzVF0%kVnhhnZe1zp_fL3r;;*5q(?8_?SuR7-!3)bV>wk*LLR zXQzV!G!>-`!C&{L#@~{4uPQpB$L2B0UVlls_N9BT<9_gcO)^m~nC{G%S1iss=>aP2 zkfgAV6rpGjtQD=p9c0hu)J1?wAo@q&5mv!0Vrpv170A@x z#UMrVEwi%&MEK$H1|tB($7$2Z1QSMZzZb&#KV1!h;^!nP>DviV`OnzM{lmTd=HP1Z zoChj0q3Jj;WPN|arj6qKVUP@6MKU)No6T;;s~E?(;j;?++(f0ey!PX9b@N(hzM+fm zym}^706=Ne>Yffl*Mr>Bp{!4xj=i=vkaD-6t<_V90d4h}C^Ly28x#f5o`;pwZlC5d z5pyqOX3oZNi-wFIG+pWs_VVNpKY{N@76}_V&~d6xq>Z-3-V{sb5MN&CxhYgdXAytH z;?2T&QApkHS(4u3;yt_Lsd5`k)^lODuOO4Z4}#s3w`?35Ci|g9vB+TslJm!Kz~WuW_x( zu-JzeE&u=-hu-jNwbi8mh>l0dm_ol1Ty&yBUdbJ=tu~f+xR7s2Fg)TAh|1V@xK)s> zD5(8>Rc)6HPzIumrQUs<@I5H)^0LU1A2ZU;R62?whJ>ddK4(E_Sve{GCM_rtpRGBI zFMO5<0C}U>P7IR1Aj=Wm<9IpGum&}q$ zio%t_dI6&%DZKwsmr+mBnkQ6FZ)VN5e03HDXDF?tr|oWxukN6auI4Vp2z$3Gf${tP zP<)J>m1z>@Enf6rhK<_E1FIcjn{iEf`h^$SH-^xoBB(=z6`dExMl!sF7eQP>-_|41nVZAd;iaWd_;i{cNHjSZ{#vD>iS7G?Bkh}7}tls;y8 zkASPip7EYKeP%cdozIjHTW=!1&$2$#d(w7{3Ez#CSVi-g<@Ge4Z9-cKg-8F$;ycsb zR;m&XUGbiVjYgRH)ru>q`I-`Owcw&Q{I^a0GtZ-=$JH)88g;Yy6AXN6~x9O8a&a_06Ry@fie9toA4X)$Olc0a~a1cAF z{s_74S2?Qo3#YToU_;OWc*S>Cc->oT*Ab0wUBIbU^gmi7$$ z5)?CH))#7UX4U)q>tV!b|0|;;7jzHWx))}S7Yh|Q)<2F#t6coEVnlbjf5&S*Y@6?m!I8CtQbug|xq0S^r)6-tYr z>*A?Tgsj_l4{p7pCR5M2>Jz&c`$?D%uE;%)8B~WNi%sgF#AYhF5Vg;SCt< z(kxZFm84E)Cp^dHTZ-+8Jm@LNgywo~p`I7N|lknU>=dB^|eZ82FUe)Zi z)wL#K1xW`9Ed3zZB@?%Uw+37#!X2fHvAwY@F>*rDAJRri{S_JD!Ch%u^x2YGw&(Wa z_0*C7w7%h}+l-HdV@12iBQG5yWo1s*4V%m-^q<$=9c{^wVm9ech>@$@IEHRPHj#N`#{fOrNcJLpp?EoPfOqM0nge*ivD-COg&0 zPCQF;G-w#VwOfLGe5*mX%{SEnyl{kg{O^Ura4zjBpARSpq-m&{k4X)xRiQkuX5Yzi z$m*k1Ft|Q2_(MZE76#j|4#0Pq6(mSA`?WcpW9n!{iU7n zr`;i1#XRS0HCb1=TD++$o@TE5AnW!XB>W(oz;PD^agg@qfKGt zWI5bJ^q%9Sb#LS8%vhtm2sC_3G|p@`3J0m!o^;qJE1?E#+=Ax3M`RB)EiTcs>pnIjADyS8fQW(P-G8o}`KMo6`f wqvNCSvy~TY0}E^4&a>>V{l6fvBX#Nztln0B9%=H>;h%`K)b-TLRcv1U4}Brb)Bpeg literal 0 HcmV?d00001 diff --git a/assets/en/map/MAP_MODE_SWITCH_HARD.png b/assets/en/map/MAP_MODE_SWITCH_HARD.png new file mode 100644 index 0000000000000000000000000000000000000000..fcff54fe99b7951c8798f378f5dc9bbd6544f58a GIT binary patch literal 7510 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 PpAR%tbyUh8+PwG=$Q!kP literal 0 HcmV?d00001 diff --git a/assets/en/map/MAP_MODE_SWITCH_NORMAL.png b/assets/en/map/MAP_MODE_SWITCH_NORMAL.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b75976efdce73f815b2be5342da4209016f8df GIT binary patch literal 7510 zcmeI1_cPqz_s3sag6JVy5QKE1hE=0Pi7r}%RiZD8=sl6J2~nbKSS@O>h!VXfYF3Hf ztzZd@Rd=&kKHl&5|L~pf4>>b;&Yih4cg{TS^UOWx-nRyN8uYYWv;Y9mYiX((0RYwc zl>8?(`MFc((v9d~-!#LxXTzTgQ{f&1)HJCxd}Qj1kN%!ilT7 zu7Ir38+p(aU%b9%XJ1PMIh*DpnuIL z&LSHPgw23!iNK-&%i1Nf1S6nV%+&CBURNgqs0lFcTD>J!D^e+uVvRff-Z48BMg3n5D^iO8Ya27!xRSqRgB1 z$)H!B=PK=`OIt#ApIYuO$0(jj{DLmVgyGLuPiKR60IziAw6gZRNRpSWhhMo-+!~5f zMa+Y+CV1NOCY%&^Yj{br^4VRscf00q>kR~SdjcDb64m(oQkGcO!^L;`Qb{yP{`3Gc zO21vN_TpC*q&;k#j684%@#M%XKLcU~-Za4vU5CfLhMW+g~1Dr0ZZswlidO@HDrxliOZUQ>MG#F3+TLVI|@!?I(`Zhn!E^ zK~t1pzff@HFsJ`ws_77upu3qna@?sU&L@Q0fy?frQvhSZMA0^E8UK4+J@~&A!Ecs=X zA;lQEuX4j@KCXA&IWLBNBpIzDQghvgxoUV7B5HGsh9Cd-^#MZP#o0#;i!nIHPTM|< zjaIjLYtALybHyHGE@c`oWzS>&z>kf)J)+H@B%~{QR`NsK*BTOzCI>Oil_r8S{>Z%CjHF#oO0%L;yhvUBQD|M`(F4&R| zP40E0{A3nwDF-DCWkI&MN?J~d;XrZL7Jo8J;%>L)Hu5MvZQai&;a6!Dk}e8 z-uTaS7bh2jv*@}{Bh7pyuR8oEe8lbd#r3uI$OC^m`Y8Iy9!r-0!lGW2}@Zkd1CUyL}@&*VO9ikiIwGN0?U0fS4s&kuwstP!v-zF* zT`R4(29n*<9UF&hMt%DGd+*L6r;vS{akAqXZdtai)6KId@81-tXLM=DNXPV~&ZLH* zs8V%P3CNG8il%|4ho+NAt%+yYyIA)LGl&7V8G;T0LZE6tmm$hJ{SfYG)+N^XTO+p) zjXh?>HcvKbH`6ySY{}wn@j`fa{18w7RTiEJIUl(YZy(=&KZs{DHvgC8_u|gt@9Snx z+)kffWbR9`7d}ovvB3S{?8fdz?}|LKgHS0LuIbnB&;lU2Ue95bSIkN~+^ld+3P1M@Y+QRy{g;&2iLpW>VzI*R%r{!V`D%b|3 zsNirkxK5$itw^37lpWn#*eZZlXcY{b2ulmQc#?H2cZ4Fv|7rUbxOZh&kIIIugA7B~ z@{;RiHH8}mA7vV4!9^*m_f$<6MClaSROo2vE%>~XpzZ8?+(dhcIj6hn!)!mOGufVU zAKmX4*5RFCj*A+iMl*#$3yC`G{k7DvMGXk7p3j9djhz_Edh51$iO7awjj@lH58>_v za>e4bVzBsl)Zq~Kd+s_H<3NeD%ZzAd=>PzZh21Nf+--WaIT)s}T^>(Fms#D?Z z@(c1cNV-JEO8S#jANz95Ty6vV>iQrm%U|9?$|4~|=FLYIc$E?+@|uc)D2G0#$U~Rs z^4@F28-4ZG-1oVLm38&n@*h?+O;hkP^Kzl8N6u0Dk9@l&X!MQl1SWERXb{}jHC3Bf zKn_iNP2ZDE&PdM4Gop7%t+_LKzwTz8zT=%cN%Sj$tL{IzlcIkRRs@MJ_Gj53Z*+$9 zYK+Pa7_R7ue!df6$zd5#ui2vcbW8&6c)CFpy!7fyNK3*OgZeMkQ}qEByNd#C>P=S! zV~pATMZ~O(8lMNa?2Lq>oKaP&B!eY$Ba=PXz`4CRqsWnh0@XXO%p_BallxE}wauuHL>|v4s2%z;?%nqoZm_%dg3qTQ*TcAhnt>k!6jHWQTbBK`$l1M5(s}rx%y27| zDXFZqu60^?W}&*i0Qb2E({4? zHu(K%@cZ2N0d!h;-61trGw{v`sLdC7FV8(k;ZqClcWhoJk2Q}*Yu#BGPHfrUw?``8 zEGsATrot|9&hk+-UfbYy0?%$v=}KR@s3q zxb98vIuGIbtMx*ShkyL=TkQU{+d6YrLGb|^05$Hz_2)w1_|FzHuORng_4A7Q3vf!G(NYiQEa z@#7)$lc}9c#F(MB^1$MKZ(M0{1*k``hp%}sP+_<5q-=0D6jzUHMEA7trQJMHJ~h}M zKC6Aui0xF=>kBI5CB9(0s!&s+^++G?|lH+umJ$j zM*v`legJnp0sxjsEwzVF0%kVnhhnZe1zp_fL3r;;*5q(?8_?SuR7-!3)bV>wk*LLR zXQzV!G!>-`!C&{L#@~{4uPQpB$L2B0UVlls_N9BT<9_gcO)^m~nC{G%S1iss=>aP2 zkfgAV6rpGjtQD=p9c0hu)J1?wAo@q&5mv!0Vrpv170A@x z#UMrVEwi%&MEK$H1|tB($7$2Z1QSMZzZb&#KV1!h;^!nP>DviV`OnzM{lmTd=HP1Z zoChj0q3Jj;WPN|arj6qKVUP@6MKU)No6T;;s~E?(;j;?++(f0ey!PX9b@N(hzM+fm zym}^706=Ne>Yffl*Mr>Bp{!4xj=i=vkaD-6t<_V90d4h}C^Ly28x#f5o`;pwZlC5d z5pyqOX3oZNi-wFIG+pWs_VVNpKY{N@76}_V&~d6xq>Z-3-V{sb5MN&CxhYgdXAytH z;?2T&QApkHS(4u3;yt_Lsd5`k)^lODuOO4Z4}#s3w`?35Ci|g9vB+TslJm!Kz~WuW_x( zu-JzeE&u=-hu-jNwbi8mh>l0dm_ol1Ty&yBUdbJ=tu~f+xR7s2Fg)TAh|1V@xK)s> zD5(8>Rc)6HPzIumrQUs<@I5H)^0LU1A2ZU;R62?whJ>ddK4(E_Sve{GCM_rtpRGBI zFMO5<0C}U>P7IR1Aj=Wm<9IpGum&}q$ zio%t_dI6&%DZKwsmr+mBnkQ6FZ)VN5e03HDXDF?tr|oWxukN6auI4Vp2z$3Gf${tP zP<)J>m1z>@Enf6rhK<_E1FIcjn{iEf`h^$SH-^xoBB(=z6`dExMl!sF7eQP>-_|41nVZAd;iaWd_;i{cNHjSZ{#vD>iS7G?Bkh}7}tls;y8 zkASPip7EYKeP%cdozIjHTW=!1&$2$#d(w7{3Ez#CSVi-g<@Ge4Z9-cKg-8F$;ycsb zR;m&XUGbiVjYgRH)ru>q`I-`Owcw&Q{I^a0GtZ-=$JH)88g;Yy6AXN6~x9O8a&a_06Ry@fie9toA4X)$Olc0a~a1cAF z{s_74S2?Qo3#YToU_;OWc*S>Cc->oT*Ab0wUBIbU^gmi7$$ z5)?CH))#7UX4U)q>tV!b|0|;;7jzHWx))}S7Yh|Q)<2F#t6coEVnlbjf5&S*Y@6?m!I8CtQbug|xq0S^r)6-tYr z>*A?Tgsj_l4{p7pCR5M2>Jz&c`$?D%uE;%)8B~WNi%sgF#AYhF5Vg;SCt< z(kxZFm84E)Cp^dHTZ-+8Jm@LNgywo~p`I7N|lknU>=dB^|eZ82FUe)Zi z)wL#K1xW`9Ed3zZB@?%Uw+37#!X2fHvAwY@F>*rDAJRri{S_JD!Ch%u^x2YGw&(Wa z_0*C7w7%h}+l-HdV@12iBQG5yWo1s*4V%m-^q<$=9c{^wVm9ech>@$@IEHRPHj#N`#{fOrNcJLpp?EoPfOqM0nge*ivD-COg&0 zPCQF;G-w#VwOfLGe5*mX%{SEnyl{kg{O^Ura4zjBpARSpq-m&{k4X)xRiQkuX5Yzi z$m*k1Ft|Q2_(MZE76#j|4#0Pq6(mSA`?WcpW9n!{iU7n zr`;i1#XRS0HCb1=TD++$o@TE5AnW!XB>W(oz;PD^agg@qfKGt zWI5bJ^q%9Sb#LS8%vhtm2sC_3G|p@`3J0m!o^;qJE1?E#+=Ax3M`RB)EiTcs>pnIjADyS8fQW(P-G8o}`KMo6`f wqvNCSvy~TY0}E^4&a>>V{l6fvBX#Nztln0B9%=H>;h%`K)b-TLRcv1U4}Brb)Bpeg literal 0 HcmV?d00001 diff --git a/assets/jp/map/MAP_MODE_SWITCH_HARD.png b/assets/jp/map/MAP_MODE_SWITCH_HARD.png new file mode 100644 index 0000000000000000000000000000000000000000..fcff54fe99b7951c8798f378f5dc9bbd6544f58a GIT binary patch literal 7510 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 PpAR%tbyUh8+PwG=$Q!kP literal 0 HcmV?d00001 diff --git a/assets/jp/map/MAP_MODE_SWITCH_NORMAL.png b/assets/jp/map/MAP_MODE_SWITCH_NORMAL.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b75976efdce73f815b2be5342da4209016f8df GIT binary patch literal 7510 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 literal 0 HcmV?d00001 diff --git a/assets/tw/map/MAP_MODE_SWITCH_HARD.png b/assets/tw/map/MAP_MODE_SWITCH_HARD.png new file mode 100644 index 0000000000000000000000000000000000000000..fcff54fe99b7951c8798f378f5dc9bbd6544f58a GIT binary patch literal 7510 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 PpAR%tbyUh8+PwG=$Q!kP literal 0 HcmV?d00001 diff --git a/assets/tw/map/MAP_MODE_SWITCH_NORMAL.png b/assets/tw/map/MAP_MODE_SWITCH_NORMAL.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b75976efdce73f815b2be5342da4209016f8df GIT binary patch literal 7510 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 literal 0 HcmV?d00001 diff --git a/campaign/event_20240725_cn/campaign_base.py b/campaign/event_20240725_cn/campaign_base.py index 5f90681029..6da69fc052 100644 --- a/campaign/event_20240725_cn/campaign_base.py +++ b/campaign/event_20240725_cn/campaign_base.py @@ -17,6 +17,9 @@ def campaign_ensure_mode(self, mode='normal'): Returns: bool: If mode changed. """ + if mode == 'hard': + self.config.override(Campaign_Mode='hard') + if mode in ['normal', 'hard', 'ex']: MODE_SWITCH_20240725.set('combat', main=self) elif mode in ['story']: diff --git a/module/campaign/campaign_ui.py b/module/campaign/campaign_ui.py index f6fd2cacd1..7c0137f604 100644 --- a/module/campaign/campaign_ui.py +++ b/module/campaign/campaign_ui.py @@ -77,6 +77,9 @@ def campaign_ensure_mode(self, mode='normal'): Returns: bool: If mode changed. """ + if mode == 'hard': + self.config.override(Campaign_Mode='hard') + switch_2 = MODE_SWITCH_2.get(main=self) if switch_2 == 'unknown': @@ -100,6 +103,29 @@ def campaign_ensure_mode(self, mode='normal'): else: logger.warning(f'Unknown campaign mode: {mode}') + def campaign_get_mode_names(self, name): + """ + Get stage names in both 'normal' and 'hard' + t1 -> [t1, ht1] + ht1 -> [t1, ht1] + a1 -> [a1, c1] + + Args: + name (str): + + Returns: + list[str]: + """ + if name.startswith('t'): + return [f't{name[1:]}', f'ht{name[1:]}'] + if name.startswith('ht'): + return [f't{name[2:]}', f'ht{name[2:]}'] + if name.startswith('a') or name.startswith('c'): + return [f'a{name[1:]}', f'c{name[1:]}'] + if name.startswith('b') or name.startswith('d'): + return [f'b{name[1:]}', f'd{name[1:]}'] + return [name] + def campaign_get_entrance(self, name): """ Args: @@ -108,12 +134,18 @@ def campaign_get_entrance(self, name): Returns: Button: """ + entrance_name = name + if self.config.MAP_HAS_MODE_SWITCH: + for mode_name in self.campaign_get_mode_names(name): + if mode_name in self.stage_entrance: + name = mode_name + if name not in self.stage_entrance: logger.warning(f'Stage not found: {name}') raise CampaignNameError entrance = self.stage_entrance[name] - entrance.name = name + entrance.name = entrance_name return entrance def campaign_set_chapter_main(self, chapter, mode='normal'): @@ -132,11 +164,11 @@ def campaign_set_chapter_main(self, chapter, mode='normal'): return False def campaign_set_chapter_event(self, chapter, mode='normal'): - if chapter in ['a', 'b', 'c', 'd', 'ex_sp', 'as', 'bs', 'cs', 'ds', 't', 'ts', 'tss', 'hts']: + if chapter in ['a', 'b', 'c', 'd', 'ex_sp', 'as', 'bs', 'cs', 'ds', 't', 'ts', 'tss', 'ht', 'hts']: self.ui_goto_event() if chapter in ['a', 'b', 'as', 'bs', 't', 'ts', 'tss']: self.campaign_ensure_mode('normal') - elif chapter in ['c', 'd', 'cs', 'ds', 'hts']: + elif chapter in ['c', 'd', 'cs', 'ds', 'ht', 'hts']: self.campaign_ensure_mode('hard') elif chapter == 'ex_sp': self.campaign_ensure_mode('ex') diff --git a/module/config/config_manual.py b/module/config/config_manual.py index cd795c7e86..7165534495 100644 --- a/module/config/config_manual.py +++ b/module/config/config_manual.py @@ -113,6 +113,7 @@ def SERVER(self): """ module.map.fleet """ + MAP_HAS_MODE_SWITCH = False # event_20240725_cn has mode switch in map preparation MAP_HAS_CLEAR_PERCENTAGE = True MAP_HAS_WALK_SPEEDUP = False MAP_HAS_AMBUSH = True diff --git a/module/map/assets.py b/module/map/assets.py index 84842cf990..a0c937034d 100644 --- a/module/map/assets.py +++ b/module/map/assets.py @@ -22,6 +22,8 @@ FLEET_PREPARATION_CHECK = Button(area={'cn': (1146, 107, 1174, 136), 'en': (1129, 111, 1158, 140), 'jp': (1146, 107, 1174, 136), 'tw': (1145, 106, 1175, 136)}, color={'cn': (180, 98, 111), 'en': (189, 105, 109), 'jp': (180, 98, 111), 'tw': (180, 90, 92)}, button={'cn': (1146, 107, 1174, 136), 'en': (1129, 111, 1158, 140), 'jp': (1146, 107, 1174, 136), 'tw': (1145, 106, 1175, 136)}, file={'cn': './assets/cn/map/FLEET_PREPARATION_CHECK.png', 'en': './assets/en/map/FLEET_PREPARATION_CHECK.png', 'jp': './assets/jp/map/FLEET_PREPARATION_CHECK.png', 'tw': './assets/tw/map/FLEET_PREPARATION_CHECK.png'}) MAP_CAT_ATTACK = Button(area={'cn': (1237, 103, 1252, 153), 'en': (1237, 103, 1252, 153), 'jp': (1237, 103, 1252, 153), 'tw': (1237, 103, 1252, 153)}, color={'cn': (43, 45, 52), 'en': (43, 45, 52), 'jp': (43, 45, 52), 'tw': (43, 45, 52)}, button={'cn': (1148, 653, 1262, 705), 'en': (1147, 651, 1263, 701), 'jp': (1149, 653, 1261, 704), 'tw': (1148, 653, 1262, 705)}, file={'cn': './assets/cn/map/MAP_CAT_ATTACK.png', 'en': './assets/en/map/MAP_CAT_ATTACK.png', 'jp': './assets/jp/map/MAP_CAT_ATTACK.png', 'tw': './assets/tw/map/MAP_CAT_ATTACK.png'}) MAP_CAT_ATTACK_MIRROR = Button(area={'cn': (147, 145, 187, 157), 'en': (147, 145, 187, 157), 'jp': (147, 145, 187, 157), 'tw': (147, 145, 187, 157)}, color={'cn': (214, 191, 99), 'en': (214, 191, 99), 'jp': (214, 191, 99), 'tw': (214, 191, 99)}, button={'cn': (147, 145, 187, 157), 'en': (147, 145, 187, 157), 'jp': (147, 145, 187, 157), 'tw': (147, 145, 187, 157)}, file={'cn': './assets/cn/map/MAP_CAT_ATTACK_MIRROR.png', 'en': './assets/en/map/MAP_CAT_ATTACK_MIRROR.png', 'jp': './assets/jp/map/MAP_CAT_ATTACK_MIRROR.png', 'tw': './assets/tw/map/MAP_CAT_ATTACK_MIRROR.png'}) +MAP_MODE_SWITCH_HARD = Button(area={'cn': (341, 580, 374, 617), 'en': (341, 580, 374, 617), 'jp': (341, 580, 374, 617), 'tw': (341, 580, 374, 617)}, color={'cn': (234, 179, 179), 'en': (234, 179, 179), 'jp': (234, 179, 179), 'tw': (234, 179, 179)}, button={'cn': (341, 580, 374, 617), 'en': (341, 580, 374, 617), 'jp': (341, 580, 374, 617), 'tw': (341, 580, 374, 617)}, file={'cn': './assets/cn/map/MAP_MODE_SWITCH_HARD.png', 'en': './assets/en/map/MAP_MODE_SWITCH_HARD.png', 'jp': './assets/jp/map/MAP_MODE_SWITCH_HARD.png', 'tw': './assets/tw/map/MAP_MODE_SWITCH_HARD.png'}) +MAP_MODE_SWITCH_NORMAL = Button(area={'cn': (214, 584, 255, 615), 'en': (214, 584, 255, 615), 'jp': (214, 584, 255, 615), 'tw': (214, 584, 255, 615)}, color={'cn': (185, 201, 236), 'en': (185, 201, 236), 'jp': (185, 201, 236), 'tw': (185, 201, 236)}, button={'cn': (214, 584, 255, 615), 'en': (214, 584, 255, 615), 'jp': (214, 584, 255, 615), 'tw': (214, 584, 255, 615)}, file={'cn': './assets/cn/map/MAP_MODE_SWITCH_NORMAL.png', 'en': './assets/en/map/MAP_MODE_SWITCH_NORMAL.png', 'jp': './assets/jp/map/MAP_MODE_SWITCH_NORMAL.png', 'tw': './assets/tw/map/MAP_MODE_SWITCH_NORMAL.png'}) MAP_OFFENSIVE = Button(area={'cn': (1148, 653, 1262, 705), 'en': (1147, 652, 1263, 701), 'jp': (1147, 652, 1263, 706), 'tw': (1148, 653, 1262, 705)}, color={'cn': (234, 180, 108), 'en': (234, 183, 108), 'jp': (233, 184, 105), 'tw': (243, 199, 104)}, button={'cn': (1148, 653, 1262, 705), 'en': (1147, 652, 1263, 701), 'jp': (1147, 652, 1263, 706), 'tw': (1148, 653, 1262, 705)}, file={'cn': './assets/cn/map/MAP_OFFENSIVE.png', 'en': './assets/en/map/MAP_OFFENSIVE.png', 'jp': './assets/jp/map/MAP_OFFENSIVE.png', 'tw': './assets/tw/map/MAP_OFFENSIVE.png'}) MAP_PREPARATION = Button(area={'cn': (854, 488, 1052, 548), 'en': (852, 489, 1054, 553), 'jp': (850, 485, 1051, 548), 'tw': (854, 488, 1052, 548)}, color={'cn': (236, 186, 115), 'en': (234, 179, 93), 'jp': (232, 181, 101), 'tw': (236, 186, 115)}, button={'cn': (854, 488, 1052, 548), 'en': (852, 489, 1054, 553), 'jp': (850, 485, 1051, 548), 'tw': (854, 488, 1052, 548)}, file={'cn': './assets/cn/map/MAP_PREPARATION.png', 'en': './assets/en/map/MAP_PREPARATION.png', 'jp': './assets/jp/map/MAP_PREPARATION.png', 'tw': './assets/tw/map/MAP_PREPARATION.png'}) MAP_PREPARATION_CANCEL = Button(area={'cn': (234, 12, 278, 47), 'en': (234, 12, 278, 47), 'jp': (234, 12, 278, 47), 'tw': (234, 12, 278, 47)}, color={'cn': (45, 46, 69), 'en': (45, 46, 69), 'jp': (45, 46, 69), 'tw': (45, 46, 69)}, button={'cn': (234, 12, 278, 47), 'en': (234, 12, 278, 47), 'jp': (234, 12, 278, 47), 'tw': (234, 12, 278, 47)}, file={'cn': './assets/cn/map/MAP_PREPARATION_CANCEL.png', 'en': './assets/en/map/MAP_PREPARATION_CANCEL.png', 'jp': './assets/jp/map/MAP_PREPARATION_CANCEL.png', 'tw': './assets/tw/map/MAP_PREPARATION_CANCEL.png'}) diff --git a/module/map/map_operation.py b/module/map/map_operation.py index f42abc9bb3..ef857fc995 100644 --- a/module/map/map_operation.py +++ b/module/map/map_operation.py @@ -103,7 +103,7 @@ def enter_map(self, button, mode='normal', skip_first_screenshot=True): Args: button: Campaign to enter. - mode (str): 'normal' or 'hard' or 'cd' + mode (str): 'normal' or 'hard' skip_first_screenshot (bool): """ logger.hr('Enter map') @@ -115,6 +115,8 @@ def enter_map(self, button, mode='normal', skip_first_screenshot=True): fleet_click = 0 checked_in_map = False self.stage_entrance = button + self.map_clear_percentage_prev = -1 + self.map_clear_percentage_timer.reset() with self.stat.new( genre=self.config.campaign_name, method=self.config.DropRecord_CombatRecord @@ -153,7 +155,7 @@ def enter_map(self, button, mode='normal', skip_first_screenshot=True): continue # Map preparation - if map_timer.reached() and self.handle_map_preparation(): + if map_timer.reached() and self.handle_map_mode_switch(mode) and self.handle_map_preparation(): self.map_get_info() self.handle_map_walk_speedup() self.handle_fast_forward() @@ -254,6 +256,46 @@ def enter_map_cancel(self, skip_first_screenshot=True): return True + def handle_map_mode_switch(self, mode): + """ + Args: + mode (str): 'normal' or 'hard' + + Returns: + bool: If map mode satisfied + Always True if map doesn't have mode switch in map preparation + """ + if not self.config.MAP_HAS_MODE_SWITCH: + return True + + if mode == 'normal': + if self.appear(MAP_MODE_SWITCH_NORMAL, offset=(20, 20)) \ + and MAP_MODE_SWITCH_NORMAL.match_appear_on(self.device.image): + logger.attr('MAP_MODE_SWITCH', 'normal') + return True + elif self.appear(MAP_MODE_SWITCH_HARD, offset=(20, 20), interval=2): + logger.attr('MAP_MODE_SWITCH', 'hard') + MAP_MODE_SWITCH_NORMAL.clear_offset() + self.device.click(MAP_MODE_SWITCH_NORMAL) + return False + else: + return False + elif mode == 'hard': + if self.appear(MAP_MODE_SWITCH_HARD, offset=(20, 20)) \ + and MAP_MODE_SWITCH_HARD.match_appear_on(self.device.image): + logger.attr('MAP_MODE_SWITCH', 'hard') + return True + if self.appear(MAP_MODE_SWITCH_NORMAL, offset=(20, 20), interval=2): + logger.attr('MAP_MODE_SWITCH', 'normal') + MAP_MODE_SWITCH_HARD.clear_offset() + self.device.click(MAP_MODE_SWITCH_HARD) + return False + else: + return False + else: + logger.error(f'handle_map_mode_switch: Unknown mode={mode}') + return False + def handle_map_preparation(self): """ Returns: From 5a1bf7f26534fa1d7723247c98e6f786c9880815 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 02:24:41 +0800 Subject: [PATCH 152/161] Add: Chapter HT --- campaign/event_20240725_cn/ht1.py | 78 ++++++++++++++++++++++++++ campaign/event_20240725_cn/ht2.py | 88 ++++++++++++++++++++++++++++++ campaign/event_20240725_cn/ht3.py | 91 +++++++++++++++++++++++++++++++ campaign/event_20240725_cn/t1.py | 1 + campaign/event_20240725_cn/t2.py | 1 + campaign/event_20240725_cn/t3.py | 1 + 6 files changed, 260 insertions(+) create mode 100644 campaign/event_20240725_cn/ht1.py create mode 100644 campaign/event_20240725_cn/ht2.py create mode 100644 campaign/event_20240725_cn/ht3.py diff --git a/campaign/event_20240725_cn/ht1.py b/campaign/event_20240725_cn/ht1.py new file mode 100644 index 0000000000..1ef05e4c86 --- /dev/null +++ b/campaign/event_20240725_cn/ht1.py @@ -0,0 +1,78 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('HT1') +MAP.shape = 'I7' +MAP.camera_data = ['D2', 'D5', 'F2', 'F5'] +MAP.camera_data_spawn_point = ['F2', 'D2'] +MAP.map_data = """ + ++ ++ -- -- SP -- SP -- -- + MB ++ ME -- -- -- -- -- ++ + -- ME -- -- -- MS -- -- ME + -- -- __ -- Me ++ Me -- -- + -- ME -- -- -- ++ -- -- ME + ++ ++ -- Me -- MS -- Me -- + ++ ++ ME -- ME -- ME -- ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 2}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Sirius', 'Dido'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + STAGE_ENTRANCE = ['normal', '20240725'] + MAP_HAS_MODE_SWITCH = True + MAP_SWIPE_MULTIPLY = (1.236, 1.259) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.195, 1.217) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.160, 1.181) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/campaign/event_20240725_cn/ht2.py b/campaign/event_20240725_cn/ht2.py new file mode 100644 index 0000000000..5249574768 --- /dev/null +++ b/campaign/event_20240725_cn/ht2.py @@ -0,0 +1,88 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .ht1 import Config as ConfigBase + +MAP = CampaignMap('HT2') +MAP.shape = 'I7' +MAP.camera_data = ['D2', 'D5', 'F2', 'F5'] +MAP.camera_data_spawn_point = ['F2'] +MAP.map_data = """ + -- ++ -- Me -- MS ++ ++ ++ + ME ++ Me -- -- -- -- SP -- + -- -- -- -- -- MS -- -- SP + ME -- ME -- Me ++ ++ -- -- + -- ++ -- __ -- ++ ++ MS Me + ++ ME -- ME -- ME ME -- ++ + MB -- -- ++ Me -- -- ME -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 2}, + {'battle': 1, 'enemy': 2, 'siren': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5}, + {'battle': 6, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Z23_g', 'Leipzig_g'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + STAGE_ENTRANCE = ['normal', '20240725'] + MAP_HAS_MODE_SWITCH = True + MAP_SWIPE_MULTIPLY = (1.189, 1.211) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.150, 1.171) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.116, 1.137) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_boss.clear_boss() diff --git a/campaign/event_20240725_cn/ht3.py b/campaign/event_20240725_cn/ht3.py new file mode 100644 index 0000000000..4c2ab18a0a --- /dev/null +++ b/campaign/event_20240725_cn/ht3.py @@ -0,0 +1,91 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .ht1 import Config as ConfigBase + +MAP = CampaignMap('HT3') +MAP.shape = 'I8' +MAP.camera_data = ['D2', 'D6', 'F2', 'F6'] +MAP.camera_data_spawn_point = ['D6'] +MAP.map_data = """ + ++ ++ Me -- MB -- ++ -- -- + ++ ++ -- ME -- ME ++ Me ME + -- ME -- -- __ -- Me -- -- + -- ME -- ++ -- -- -- -- ME + -- ++ MS ++ MS ++ -- -- ME + -- Me -- -- -- MS -- Me -- + ME -- -- -- -- -- -- ++ ++ + ++ ++ ++ SP SP ++ ME -- ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3, 'siren': 2}, + {'battle': 1, 'enemy': 2, 'siren': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5}, + {'battle': 6, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['PompeoMagno', 'AlfredoOriani'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + STAGE_ENTRANCE = ['normal', '20240725'] + MAP_HAS_MODE_SWITCH = True + MAP_SWIPE_MULTIPLY = (1.089, 1.109) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.053, 1.072) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.022, 1.041) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_boss.clear_boss() diff --git a/campaign/event_20240725_cn/t1.py b/campaign/event_20240725_cn/t1.py index 5d9fc56ff4..bab8993b3e 100644 --- a/campaign/event_20240725_cn/t1.py +++ b/campaign/event_20240725_cn/t1.py @@ -55,6 +55,7 @@ class Config: # ===== End of generated config ===== STAGE_ENTRANCE = ['normal', '20240725'] + MAP_HAS_MODE_SWITCH = True MAP_SWIPE_MULTIPLY = (1.236, 1.259) MAP_SWIPE_MULTIPLY_MINITOUCH = (1.195, 1.217) MAP_SWIPE_MULTIPLY_MAATOUCH = (1.160, 1.181) diff --git a/campaign/event_20240725_cn/t2.py b/campaign/event_20240725_cn/t2.py index 4798088fd5..e315675aa0 100644 --- a/campaign/event_20240725_cn/t2.py +++ b/campaign/event_20240725_cn/t2.py @@ -56,6 +56,7 @@ class Config(ConfigBase): # ===== End of generated config ===== STAGE_ENTRANCE = ['normal', '20240725'] + MAP_HAS_MODE_SWITCH = True MAP_SWIPE_MULTIPLY = (1.189, 1.211) MAP_SWIPE_MULTIPLY_MINITOUCH = (1.150, 1.171) MAP_SWIPE_MULTIPLY_MAATOUCH = (1.116, 1.137) diff --git a/campaign/event_20240725_cn/t3.py b/campaign/event_20240725_cn/t3.py index 1257a531ce..a2d9f8bbdc 100644 --- a/campaign/event_20240725_cn/t3.py +++ b/campaign/event_20240725_cn/t3.py @@ -60,6 +60,7 @@ class Config(ConfigBase): # ===== End of generated config ===== STAGE_ENTRANCE = ['normal', '20240725'] + MAP_HAS_MODE_SWITCH = True MAP_SWIPE_MULTIPLY = (1.089, 1.109) MAP_SWIPE_MULTIPLY_MINITOUCH = (1.053, 1.072) MAP_SWIPE_MULTIPLY_MAATOUCH = (1.022, 1.041) From 19ba250bd44813ac63d35dcb091607bdcf5c9980 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 02:46:17 +0800 Subject: [PATCH 153/161] Add: Chapter SP --- assets/cn/template/TEMPLATE_SIREN_Dupleix.gif | Bin 0 -> 2153 bytes .../cn/template/TEMPLATE_SIREN_LAudacieux.gif | Bin 0 -> 4202 bytes assets/en/template/TEMPLATE_SIREN_Dupleix.gif | Bin 0 -> 2153 bytes .../en/template/TEMPLATE_SIREN_LAudacieux.gif | Bin 0 -> 4202 bytes assets/jp/template/TEMPLATE_SIREN_Dupleix.gif | Bin 0 -> 2153 bytes .../jp/template/TEMPLATE_SIREN_LAudacieux.gif | Bin 0 -> 4202 bytes assets/tw/template/TEMPLATE_SIREN_Dupleix.gif | Bin 0 -> 2153 bytes .../tw/template/TEMPLATE_SIREN_LAudacieux.gif | Bin 0 -> 4202 bytes campaign/event_20240725_cn/sp.py | 97 ++++++++++++++++++ module/template/assets.py | 2 + 10 files changed, 99 insertions(+) create mode 100644 assets/cn/template/TEMPLATE_SIREN_Dupleix.gif create mode 100644 assets/cn/template/TEMPLATE_SIREN_LAudacieux.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_Dupleix.gif create mode 100644 assets/en/template/TEMPLATE_SIREN_LAudacieux.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_Dupleix.gif create mode 100644 assets/jp/template/TEMPLATE_SIREN_LAudacieux.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_Dupleix.gif create mode 100644 assets/tw/template/TEMPLATE_SIREN_LAudacieux.gif create mode 100644 campaign/event_20240725_cn/sp.py diff --git a/assets/cn/template/TEMPLATE_SIREN_Dupleix.gif b/assets/cn/template/TEMPLATE_SIREN_Dupleix.gif new file mode 100644 index 0000000000000000000000000000000000000000..df14c0afa7be83c94c6ef536d7d15632aca723a5 GIT binary patch literal 2153 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 O6<^Ls*C3Gy`TPe10=WMG literal 0 HcmV?d00001 diff --git a/assets/en/template/TEMPLATE_SIREN_Dupleix.gif b/assets/en/template/TEMPLATE_SIREN_Dupleix.gif new file mode 100644 index 0000000000000000000000000000000000000000..df14c0afa7be83c94c6ef536d7d15632aca723a5 GIT binary patch literal 2153 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 O6<^Ls*C3Gy`TPe10=WMG literal 0 HcmV?d00001 diff --git a/assets/jp/template/TEMPLATE_SIREN_Dupleix.gif b/assets/jp/template/TEMPLATE_SIREN_Dupleix.gif new file mode 100644 index 0000000000000000000000000000000000000000..df14c0afa7be83c94c6ef536d7d15632aca723a5 GIT binary patch literal 2153 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 O6<^Ls*C3Gy`TPe10=WMG literal 0 HcmV?d00001 diff --git a/assets/tw/template/TEMPLATE_SIREN_Dupleix.gif b/assets/tw/template/TEMPLATE_SIREN_Dupleix.gif new file mode 100644 index 0000000000000000000000000000000000000000..df14c0afa7be83c94c6ef536d7d15632aca723a5 GIT binary patch literal 2153 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 O6<^Ls*C3Gy`TPe10=WMG literal 0 HcmV?d00001 diff --git a/campaign/event_20240725_cn/sp.py b/campaign/event_20240725_cn/sp.py new file mode 100644 index 0000000000..6399a79997 --- /dev/null +++ b/campaign/event_20240725_cn/sp.py @@ -0,0 +1,97 @@ +from .campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('SP') +MAP.shape = 'I9' +MAP.camera_data = ['E5', 'E7'] +MAP.camera_data_spawn_point = ['E5'] +MAP.map_data = """ + ++ ++ ++ ++ ++ ++ ++ ++ ++ + -- -- -- ++ ++ ++ -- -- -- + -- -- -- ++ ++ ++ -- -- -- + ++ -- -- SP -- SP -- -- ++ + -- -- ME -- MS -- ME -- -- + -- ME -- MS __ MS -- ME -- + -- -- ME -- MB -- ME -- -- + ++ ++ -- ME -- ME -- ++ ++ + ++ ++ -- -- ME -- -- ++ ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 9, 'siren': 3}, + {'battle': 1}, + {'battle': 2}, + {'battle': 3}, + {'battle': 4}, + {'battle': 5}, + {'battle': 6}, + {'battle': 7, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ +A9, B9, C9, D9, E9, F9, G9, H9, I9, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['LAudacieux', 'Dupleix'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = False + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = False + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + STAGE_ENTRANCE = ['normal', '20240725'] + MAP_IS_ONE_TIME_STAGE = True + MAP_SWIPE_MULTIPLY = (1.073, 1.093) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.038, 1.057) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.008, 1.026) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=2): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_7(self): + return self.fleet_boss.clear_boss() diff --git a/module/template/assets.py b/module/template/assets.py index 862ba08ed7..d41d2d6951 100644 --- a/module/template/assets.py +++ b/module/template/assets.py @@ -93,6 +93,7 @@ TEMPLATE_SIREN_DogPink = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DogPink.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DogPink.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DogPink.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DogPink.gif'}) TEMPLATE_SIREN_Dorsetshire = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dorsetshire.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dorsetshire.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dorsetshire.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dorsetshire.gif'}) TEMPLATE_SIREN_DukeOfYork = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_DukeOfYork.gif', 'en': './assets/en/template/TEMPLATE_SIREN_DukeOfYork.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_DukeOfYork.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_DukeOfYork.gif'}) +TEMPLATE_SIREN_Dupleix = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Dupleix.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Dupleix.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Dupleix.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Dupleix.gif'}) TEMPLATE_SIREN_ELpurple = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_ELpurple.gif', 'en': './assets/en/template/TEMPLATE_SIREN_ELpurple.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_ELpurple.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_ELpurple.gif'}) TEMPLATE_SIREN_Elizabeth3 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Elizabeth3.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Elizabeth3.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Elizabeth3.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Elizabeth3.gif'}) TEMPLATE_SIREN_Formidable = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Formidable.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Formidable.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Formidable.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Formidable.gif'}) @@ -134,6 +135,7 @@ TEMPLATE_SIREN_Kinu = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Kinu.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Kinu.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Kinu.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Kinu.gif'}) TEMPLATE_SIREN_Kirishima = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Kirishima.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Kirishima.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Kirishima.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Kirishima.gif'}) TEMPLATE_SIREN_Kongo = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Kongo.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Kongo.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Kongo.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Kongo.gif'}) +TEMPLATE_SIREN_LAudacieux = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LAudacieux.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LAudacieux.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LAudacieux.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LAudacieux.gif'}) TEMPLATE_SIREN_LaGalissonniere = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LaGalissonniere.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LaGalissonniere.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LaGalissonniere.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LaGalissonniere.gif'}) TEMPLATE_SIREN_Laffey6 = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_Laffey6.gif', 'en': './assets/en/template/TEMPLATE_SIREN_Laffey6.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_Laffey6.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_Laffey6.gif'}) TEMPLATE_SIREN_LeMalinIdol = Template(file={'cn': './assets/cn/template/TEMPLATE_SIREN_LeMalinIdol.gif', 'en': './assets/en/template/TEMPLATE_SIREN_LeMalinIdol.gif', 'jp': './assets/jp/template/TEMPLATE_SIREN_LeMalinIdol.gif', 'tw': './assets/tw/template/TEMPLATE_SIREN_LeMalinIdol.gif'}) From 7644a4b7ef58e78e6d9ae2d958b1096c32e9a518 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 02:47:01 +0800 Subject: [PATCH 154/161] Opt: Skip waiting info_bar after _retirement_confirm() for faster --- module/retire/retirement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/retire/retirement.py b/module/retire/retirement.py index e1efed738c..0146854b55 100644 --- a/module/retire/retirement.py +++ b/module/retire/retirement.py @@ -105,7 +105,6 @@ def _retirement_confirm(self, skip_first_screenshot=True): break if self.appear(IN_RETIREMENT_CHECK, offset=(20, 20)): if executed: - self.handle_info_bar() break else: timeout.reset() @@ -309,6 +308,7 @@ def retire_gems_farming_flagships(self, keep_one=True) -> int: else: self.device.screenshot() + self.handle_info_bar() ships = scanner.scan(self.device.image) if not ships: # exit if nothing can be retired From 4a000745cc3ededef7d572c7167b717e410bb39c Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 26 Jul 2024 02:53:15 +0800 Subject: [PATCH 155/161] Upd: [JP] POPUP_CANCEL_WHITE and POPUP_CONFIRM_WHITE --- assets/jp/ui_white/POPUP_CANCEL_WHITE.png | Bin 0 -> 6680 bytes assets/jp/ui_white/POPUP_CONFIRM_WHITE.png | Bin 0 -> 6906 bytes module/ui_white/assets.py | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 assets/jp/ui_white/POPUP_CANCEL_WHITE.png create mode 100644 assets/jp/ui_white/POPUP_CONFIRM_WHITE.png diff --git a/assets/jp/ui_white/POPUP_CANCEL_WHITE.png b/assets/jp/ui_white/POPUP_CANCEL_WHITE.png new file mode 100644 index 0000000000000000000000000000000000000000..198a3b348033463e918d8f436cb8ec46c1a9fb39 GIT binary patch literal 6680 zcmeI1`8!na|Hp4zAzR9leTg;_(%X_Hl&OS}CD}s^#?FkjB+4>&jXh+YAz70_Sw_YZ zvSgQK(qJ%+vHN=W{_XQGe7mpfT(5KP`?|05I*;dh-sg3l`=x=N4l6SsGXQ|~HbmPH z0EVMe+OLeXM?pjQ4$VBSEnCEdM0^8>toEq&BQOqW% zxlU$69<)6hVuoZz{m4CE?{wo;)Whfpj}%vhCyHu<5u}xs-I3i!LMCCa7v9Gnc9K&v zVKL7IC>cgvR~{8c>OG3Q&4Z_gQnW3fK`vTD>@s~zIZc>(t(*>R7?uI}#hJyk zYuJD=l%`ZS5K*KBT0*CK=tOksa<2?D&y-kuMASoQKnp&lC++Tl{OS6XuYCR+YcoGY ze|&bW)U@Tfz~RU=FP52>A$|%%pwsPT0f2A2|I>k@dOTrqes(nRxnUduPp>KK zP9=yR;37iz<1Y07xLWy0itE+7*~@wZv3of92g3wy(Jv`KIF~}@)`e5|nUefj0s4i0 zi(c*V8oX;u$SMtOG@IFgAJ7OPOULk?NB|s8SLNVT0Fs)wks6n1Ku=o18~}|!c^?%g z^XPUk0-&Ay^y0f4$5}p|Mz^qKeiCeK;i0uXrmewpw&gm9=CS!O0p>TnsW;9Tw5^ax5#NyZI;|A@NMk2{yi-K()!wT{9ogik@9)!n zqxIGJP%O;%sw3CC-{&g3Wmsw+w4bk9NHwfBuc-g-PhTGp{3^~qtY4PRG3LANqnOB> zHZRTiByrsK3uX!?NCkUAvw!@!_*=4VNsMP{qr*-uGg>|8`0V;DK3>O6*G#KeVp+VJ zvw_i+Dg3$Y=W}97#ro9}2+r-c06XQ%c(Yq*$yHGy(QFB;b8M%qlOSi=z9i1;F^fvX z%iTV48wweRNEa9ts10y9XX;84nHAhX`Qb-t=t~pITF zigI_zIjoy$4ttr<5v8E7@7Y&a5eY z&%Dzs+RNG-C`~7=E?qh7f^o!zVE!3Hj5Uqft$3|)uMo#bz8+Z`SyEXPWjAH2FH^&{ zhWiZ>4dcFjztjfpiSQ*RN*9G~X|i93;`TeA!t;wXOzIc@=gL*aYw>4XWfkq$y5sGC zLSH-A_pd#lEk22V(vZ;hB)|W5{<(U|)kvo~g#aEkM>I^p4t;gXVFG&H*3MaOyr8Y1 zeZJ{JPck&!v7x_u(5Jh*Q)&V|ivF@3r!)W1UEW(}UQS;=wxYCYyD72hw%I4xEx;)_tm2~*XMD_mIlh;@tFnXNM{YIG!Z*0r^%!huKG7`D zV4w0mt)hd{3DcwK-yTm-p|#=n8o#w zf9FcKq^{8L$+#DNjME$;u)=NKrS4kBs2LsCsCr@NGboci?w>z@WhZrxl@dQHAT7ql3eCB+*ajK7f`NBlr2cE^HUVOIyb#n#t_#nlI*PLnZ)fd7=G!0~U^%siV z?+KpTc%BiVf3qnsHt&9AUHzTwiHlh#DVvIuD#2PiPhRNX^6ijk(l@*WPdJnKL7aTg zM0Q>YlD1It5#W6^rg#^!Ub`Yx$dq ztWZn5$v&p6uIanv*l_cte;}DHT#!p;ij=(ESaT`B6pFnWOmQDreKv{Yv*T}QUJRM6 z*YVeH&qfBm4LrVilSFj!fw@jpG&jFzax=UZD+E#YSjY&2jb*H)``36pyvX+X#=@s_ z(}h8Sa|S=(^tMj4_DrKf>nMyw2>jB3a@mowWM0)p>kYb?dCDU&vDg zq~%(p$8RyJ-MB>EU5skEs!oW<{w$=z0!DP8;#W+sm;3y_M^x!24TkQxviyD z%z9-3@g4SGq16YK4lQmoTQ)5= zeXZu~BsQkV?1SXP!N}PGoo<~W@mu1t$Sh%DepWv0#^c_k*`oJFr~0}=Ob+2h?=r&l z{*T?eeP(;3YbUm&`)j4e<0yG%g1kIhtt)9QSE%ITx=Ot!7!J$a(4A~9O9_!z(O z%rP$X<;}b!1Nvyb$Wp9Akv;CGeNPUjly#{3Y-|1h!PR zBSpa$eurY+e2{Y3Xnn99JDt~Hds^aXZfLDmTzQfLhg;->a=rEbj$(|iBwt&Xm z>yJ@)wY5Xl@~y9_r)k0Y!f*oOd8*ZAG>V%~phPTANENY1Shr)5l_C4Q96473!L?a227Bz8-2YiMY>qazD|N8Wq+3$URjShp^VCRA-NDMyVJou75*{?16wfYdZVqq+>fvrsmY13{gYInV$C3ey2r8>z)Nsg z#g2U3LA#TA8nztO3a7gpCpJg91EM^6i)_=8Tfls^6jQ^ zl8lfN6>&z(_e0B&lZjJ=UojgEQn-ap!g$QyBY9f@Xn4b5*Sy&iwi_8u;Fgz7JN61l z{UH)p)KeQTB6~tc$mgJqgwe6#I<@I;eBa&ibindQqS8dmy^YDI#^DfQaJEo*Co7slj37+`5RyynJUNx{Id zBvon_4&8bYagelQVhE)t0Ol>o2&){Lz~4B7x35n zuv8O^<@<$lffq#syS=y%kH2We zDGeK8>Fxg$6nb&c0p(A$aMW|SXoO-j(*9-?EbJrWn_-XqSDXuPtA}^AngYMo z#$Ah*Xk)b85xch>zjuk4n$54ZWXXNnKL&Y>wbYe1xXy-A#+G#ObJ%(*SxKV5LFYm;JPQ zAG%*%)t^$ajLiwm6qm0c%$M~Y|MS8VpU=V&>YxQlIH&bLE8XAAzXblj5D3*jJlg6x bJfs7ag~e8I3`5JmPk#HRo_4vW&9nalA*}{1 literal 0 HcmV?d00001 diff --git a/assets/jp/ui_white/POPUP_CONFIRM_WHITE.png b/assets/jp/ui_white/POPUP_CONFIRM_WHITE.png new file mode 100644 index 0000000000000000000000000000000000000000..96f085ef8e0675d64c21a5e6cfbd3e44568e9c89 GIT binary patch literal 6906 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 Date: Fri, 26 Jul 2024 03:01:04 +0800 Subject: [PATCH 156/161] Upd: [JP] Mail assets --- assets/jp/freebies/MAIL_BATCH_CLAIM.png | Bin 0 -> 7439 bytes assets/jp/freebies/MAIL_BATCH_DELETE.png | Bin 0 -> 7701 bytes assets/jp/freebies/MAIL_MANAGE.png | Bin 0 -> 7047 bytes assets/jp/freebies/MAIL_SELECT_COINS.png | Bin 5773 -> 5779 bytes assets/jp/freebies/MAIL_SELECT_CUBE.png | Bin 5771 -> 5781 bytes assets/jp/freebies/MAIL_SELECT_GEMS.png | Bin 5772 -> 5774 bytes assets/jp/freebies/MAIL_SELECT_MERIT.png | Bin 6176 -> 5782 bytes assets/jp/freebies/MAIL_SELECT_OIL.png | Bin 5773 -> 5780 bytes module/freebies/assets.py | 16 ++++++++-------- module/freebies/mail_white.py | 2 +- 10 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 assets/jp/freebies/MAIL_BATCH_CLAIM.png create mode 100644 assets/jp/freebies/MAIL_BATCH_DELETE.png create mode 100644 assets/jp/freebies/MAIL_MANAGE.png diff --git a/assets/jp/freebies/MAIL_BATCH_CLAIM.png b/assets/jp/freebies/MAIL_BATCH_CLAIM.png new file mode 100644 index 0000000000000000000000000000000000000000..9fd74d8db39db9643eb25e8ee2eee398c8f9a1d0 GIT binary patch literal 7439 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 MS_b#Z?$|&3AL&Sa0RR91 literal 0 HcmV?d00001 diff --git a/assets/jp/freebies/MAIL_BATCH_DELETE.png b/assets/jp/freebies/MAIL_BATCH_DELETE.png new file mode 100644 index 0000000000000000000000000000000000000000..961b35f589a1623366738632f4a5e8df0ec298ff GIT binary patch literal 7701 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 Fe*o;UF%|#- literal 0 HcmV?d00001 diff --git a/assets/jp/freebies/MAIL_MANAGE.png b/assets/jp/freebies/MAIL_MANAGE.png new file mode 100644 index 0000000000000000000000000000000000000000..295acf6f2a8ba6e7b48af30476b478118669f69f GIT binary patch literal 7047 zcmeI0`8(9#`^R6FvSdryiL50dYluM!WlPN<`yOVD##qKKmEKWy8pBXT7)#d4Ub2oQ zgzQAx}4pNOvL9l8@?@iVgV2Y@r@{_S)?W)?pHu$p?^ylG+a2#$pNKZ5&-8Q;7q z=7)fLctPC(Ab2ACevoy_F0an)t~)10|HsD13}`eUE`ek2;<9J=*O7m++o?FJC;21KV9+7o94s2@j#Ht?iHPHxjdm2mPo4&Zx88 z(n-tjya5f%XE!u|h$fgDeeG_FqUSOC$tcFPO!(q?svQ6}7*N3x$O_N_T{#?}2mD6l zCAmB2j$`UAq^$t4B;cnwH}MQzq9xEJ|DfwU(8Ui_+^=$D1yTV3iVD*g2a4H&)!)WC zr+^?gdtaPsO4voqu(>U5gpJzGB6V7jED>-{cHdrNE`fobRCpPTwa`8_8>wjzuVs( z`E0<&buMns9yP|(GXh*@1C$BJrKM2@zfQV}fEJ%3TVMhLJesB1D^IfIK4kqCLW@IP zKhV8^eQVKmLqwSM%$YSQm$yxd3voKf%5(6aaS=NwvqzI58^GgCU36({Ud-WRXWzgw z%hyB#qy9DDU82NM=d;Me*Ov$9RX*Cu@x0u${s(U%ZrX|ZWSMm9Vt3j+Hz`tSQ#AdM zB_)U*z&$r_HLGQ+AwXLr*6HZuIIM>P0o@4d)p)@(NdT9piV|uU08(0L3A$J4fWD0U z1psLL#c%f^mCvMu831nOM$1;{F|mCJAljCx(n=bSHUzBudh z`iFQwGavdjqqwt?N^(@b80p3Nd*ba~h~!}MC%eRyULL$` zG`yA?U3rh;JAHuerw^inFFSJm2azhA_EG8YdAzAGHO`zrtmgui_48Z~&0 z!y|s&*)BfT!08`rLFrCj*ClIJNP?=Xh_!kkk5DVtnZhhd9~X6QmD%Ab*B9uM#6&}D z6YHBFq*f)Wx$Bv&SYn=D`ywNr^1-}XDukQ%HP}V-W1{tKob>v|3m36c4l*3)98-)W zIl7Y<%~&r=B`O)8G4?T^qBjKYa!B-JHyCYmSjcegFp zo!4xCF>k{1PSP?zmHUpWJ7|gVeU6eoI=A>{pmR01-k z`Y^mV3Hev^M!scwd3x~=pnx_hI5nJs?s}j&rX+;+e8c$$4z`xvkqzTX>fuh)OoOCFq!EiBfc?Rs5mfP+ z2PI(65(h|l!EJDr#X=FwcixhbA`x&yky=^5Wsqfp#lStsyk^J4Cws2!mCv8u&V6o_ z_rSHTL*Ug#-VP=AO>6y81cZSs=kvzY#kP{RY7%&AUno!^)a3q6|6&iEN0LYO^5Z@_ zj*gE%H#Ekx?(|>mXYUWa%5W8Q_2Z~_sYhuZLCKN&EvqBHDDMv+*yd$u`9(}RZFu-|#xd@Gbu%Alyaq6)KGq5$;T zb=QsFMAu(FZ@ln>8&BsxoFzP}Px|^OZ_qeT1~0uH>$#vB%%|glgA2RhRA$_#d~P_q zcqvWhf6Z@SY`)x=>XYeFKUh7C?CtH6o5KCTb+5kG7|HU%Iya9uP9D5^_TC_?{Z5=( zTxa@3dKlqUx@r0@?hQl-f`V*A#&E`?c4Rp+Y}5*BA!LPN!)Rg%wI50`rC$Rvu-WtT z=MyduUEaR$J8^CGV3l<>bM^F^#*Xuj)DCQCK%`fgTVzxVsTGDm2J{9({Tj*nb1H2` zUyIsER-QthZ~e2Ws(b}^6A9cIff;=FVTCUXeRDzxX-k6Z^+Pqo+j*D<%v9+(?E7@D zc6)fqzNtJ$sN23xo1*u7W7m5AaqmKQA*S#PHIb;`5a?jzu-|mSyedl?JE{oln*3_} zvxyMuq^YCrzSmD;(CN^j&k4ziZ7ygQpVe-bh!~APM=%{=_qFy2hY5dL=1^O_n`Wn+ z=swde(KSU2MprTTFo-gu8Q(Lho_ckv;j{vq4v#(?3%iXdA_d;cw&=9s_)l7s-nEE8PnW<6lHDu4`pTGM-K9Enk1JPF*cVaDupXh5*q+@*37S83 zk_YR8SPk6(c5-Tk56;(g*!8V&e z#aj#-cqQWQ^99LYv$w2w3-;O=3MV`wRHh$V%v)Q6x4cnPTdyr+hTgxwdF6?fN?K8B zH^H~;_rp)FJkDuuV2XFfh%F>imM_f_=ZE`H_NU@DetF)?%yA@a8wHz~n7LEevqm-| z=aYSLyEZN5cbdHQM!r4YU@sNge6=Lx2)i?bd>i7u{kpHZucwbe)me4Twzn2Hx%E~p zZzm)>(w+c0EG?;P9+#dNZJ7=VrEOI=vv2aeIe-Q6A=gdn9Cy2^ zGsFb@jYhxU;=3-BQgwGrwac^(Bm55MjLL1{WcOXdn$?Xm?$GH+&ee}`2754Lw zQ>)WJn++$0gC!PwlzKcIJ2zz5Yd9itTjEthwy3BOyI|9nTYt)2VP)aDfu0D+F^cS8 zLYzIE-@iLxeeh%B3@vV;r3_U>MNmqL$~8MBIz=1%QQDgY2c`X!;S@Zjezvno6n*JH z_sD`exaZng_|1G*hZuHrps@Rtx-T`9qkQt(6mvDQGzEYVDFBFy0RY~Ej|*YL#B(Mjc0=y--$nC3l(bu>u@;4gu{1pX5EOW-eozXbjg_+JQIcvuXax=Fiu%bC-K3g^qW+G8#uO>j-f=59jhJUwW1~Xi5lBDT3&Zv@e&0rI7oiFOIa@xW zbQ|vlcc}<1g*qy71MREUlpLNHHa1xkSaZ04xpmD6vx=FHw?JwsjwO8ydj>1~UN6 zt*r}cwBoqjxMhEbS~&zhJI496L9$8lD!3>F^H5qLP-&gGZPRQo*`1%$?%rR9s7PDT zoR!-OL_bE~L#Kdeu2$I*8@nf|!sW^NzSNzA<)g_gJGVfzWQnv4AG$oO8vt6OE`Mvy zaH`AE+w_y(sJ@e2&_`;yc&Qe{9h`Pp+Nd=!82jDSOx}{$c&p)_G}z%-BQBsvyfc88 zY**to@qNu)yP^=BvBG3f0Pb*OitSeUpSBYlp=Yob-Xwd(v3`f3^FcO z8jkKBI4MCZZv2rOMURZn_6*B*@@ecPHWg#u9ZYZe2aO&rIwxcHz|?@`RZi-9BW5xT z76D2eGwnDOW~+iG=gT1!SH8f&i+(b31bO%X#4Gu*^t-7?iHf89Y!D|P1~4S_pi+V; zRruaHGlx}=p@9!Ml2w{j2`eB@Lj1n*zH&+|LDGPioY14i2jU4Yw=Wz`sn>!|#P1n5 zIDf-uv zUBW{@H*gr9xMUOkSYD@0^2BE^=CC#et-++aH`|w2!|~Z*%$U%=ch#RqXE)|$ z$84E1+LteB$tlY=yl)Az5^+?xCl8BZ{Lh(v6}^OPVK)DeB=|{~S6LU{aab>~%BM{d zN$wBbhXyLU2e)SYkq_Oc2C7OWX~T}7kDHc*uP%TTFKs~+;WM*XFt5ONz52Fy7EKe3 zc0i<+X(Dah_9Tr%3?O~5nyUc7==Af|osxBFGeX%=n52<8`mo z>i2U&NcN|W|M5OAeQ1*7R(>a0Jx+L}h;nPVzPm2Yt_E`kawy(#*f`bgiyi|_74?QK z$W9-%$?$<{F6}Ci`#%5q-1f)sjkHE{*RCkyVWBTv9ab;@8eC^@gR8_Jviut@Vcib# zDT~$p&=L19Ov~GzzuwDkZC{TOpOr@-LG##Abh)zQk^{cZRhu6cOw$X?9?{So z2+JOqr3M7R*_~pL7*yaTz1B(HJwQ?#W z(0B$01||t00Tf{XX+vfKwJ3MwP-u7$EAft9)tbOHmY1X;rpZA=Z{{P0FWtB6}FE5S?o^B0twUjVJLGb;h z|1RuL&z*gnd;Ut(t1I4zUz+RoL>0ANLB0s!vH^mRHOkA2^dkK_7hs+5v)$~l*F8J|Ye0RRB7 z87ZabPtWr_|Ge7reR=u(lD;MD0RZdXnE-%03gDc^k7Ls`*I$V-_I;n;1TX*~fC1Q! zd7j&@y%X(D722*Xr(!(-KmY^q?^}K>Mw6cr9)AD;KmY>(0Du4n000013;+NC0vLd8 zXm@Qnm0NF>Q)%ngdH{d`24FLqu9*%~INx&T9Hzt6bdB`@009iZW(?gBtGIvNU-=)n zoXY-nAFDWYgY^J_bq^*0;C?L2Qc9V=Or?|s@U?06+i(u$ch9 zJQJjxa?XEV|EjL*zUznn_rG5O46^|XS_B?<;1>V@0RR6305blY+G*g}`Tzg`07*qo IM6N<$f;k+ubN~PV diff --git a/assets/jp/freebies/MAIL_SELECT_CUBE.png b/assets/jp/freebies/MAIL_SELECT_CUBE.png index 7eb836fbfd505af57dfd3467a65cc5c4e3ca63fe..2776047ee30cf1771dc2fc53559ab014ab391795 100644 GIT binary patch delta 434 zcmeCyovOQGAD7c1PZ!6Kid%1P8)n~jkU91+d*VYD$&!R-zFjI?lG|(k^G`h3-0rkR z#3aRnpYbm{uOydj@^WY4iO*-gpH?pLXM?`#W=8JItdj+}mFgdS{*lDgkZtp?%13kV z?aEHoYdb3J%6Fds8_+i8(%pV*W+sL`yU(#OgwLA)Y|FB!?7v~f=hn~Z>G=8G=KSYR zTdp{p$}%)Cu-)j1P*QqVe{Xv2&I*g?HNXFsNwl>;cGgL>WoYPsu$q%$-Nkv)vae0Q z{tmx6-Rj!6J8Re(6dD-K43w3Q9sY4G`f*~<2W|tfNfW-#kL#_-U;@j#onm6(NMK-K z5I6uNfI24E3X0b=@*G%w?SBy~0|O(^hrXie$-Ud}b(t1TUkfpG@&1>~GrKCBnca_= z`2O|Hw*{%&An7c3X_nvdVC(It>vQ6O)a8#;u9-YJSjE5~@0iZiu=-8V&FS`$x1awz z^t{;DTW6(rYW$^hWef~V5(&Yd^sYPiszz-Q5w}~H%Br#R`!8`|7+SE%HTb^(I^+L; aW(N7!nXh;zR{8?n$>8bg=d#Wzp$PyH61)ij delta 437 zcmbQL+pW7{AD7b(PZ!6Kid%1PZp^yvz;OJbcj3c44vz(nO(ge9BelTbe2=ZcMVfgX945%XWm!WL`@qO=)w}1TgeDk$g-g--~L>)hVv4nwv zA;<09&zG+++nnDY9vwIRdH#9J^w*3Gd)VjuFl^9j@Rr-(<9m6{exc+yzwN{sc^DKL z7`G+Qd%i6@fA{8k?%eCwg15>sG%&FJ_^-QPOqqd!BLQfpzyTluRLQ{Lz`)D^l164R z0F7Z_V3Gh5ECvV6wtUaEy&9c=H8}TuG|*;_god*jyV9(D9@dgTe~DWM4fCsVqE diff --git a/assets/jp/freebies/MAIL_SELECT_GEMS.png b/assets/jp/freebies/MAIL_SELECT_GEMS.png index 7d7270022f903e8d0aa5fc53e437b5db4c86f15a..45e1ba5afd78a56b5f860e04f2363a23a42277e9 100644 GIT binary patch delta 430 zcmV;f0a5;pEsia)z6wCQNklf55-Sm5CGuYbtV9~QCnL5Q`3_A=u=$0bV+rt>6G$IjxJ0G03d(? zp2w1EseSb8-8x6U^7FZ*+UjwCtffxVWIO#lob3|AV z06+i(JdHf$Qc8WVw;!qRwUm;FY&`&gN6?u7;1;opXtCe#^OQI1O>~!@>$asGj)z)m zM8wJyU;qFC4DeK@muaqZEw%hE*Mqr>e(1+M+6@2z000000J8`ScLN@Phw=#k0RR63 Y0QhWVo&8_D*Z=?k07*qoM6N<$f)kO#&Hw-a delta 400 zcmV;B0dM|}EsQO&z6wIQNklani$gb94*=jJ zxHAF38q2cOTBq+*tu@9Nc30m%hs$f@;}~O1DH#v|KmY@5CV(%8TyiPp?eVWtN`2Q4 u55wD|0RaKC0Sm8;pM>whW)Y0ObiXZKKj8?L7GczG%TN+`?($YQaWo{ zX5GxwD`)UIFfcQ`aXTX@*gs$Xck=!!i{~}}Kkj?IYx(EkGl{khx_itS84ifO%GP!M zIrG=wh4$Z4HNI|*;s$Zg%AYHiZU8hBCKP zOdJUe3=9GXfCNZ{0i=xqq!5Y4z{taJVD>coB32OVgZNXm<3ii-b*UCjUds+PX7S$a zg|828x-&t2vrkmMkX}Z$>+sg!NkR=mE`z75pUXO@geCw~e!#&1 delta 862 zcmV-k1EKtuEub*4y$XK~O-V#SRCwC#-9cy^RU8N4f8zvZ#6VUI7%<=-g7j#iffie6 zZq3asJ>^t-DoanzA;=O+*(zvzYrROl6;JgbG#(0j32cQ*R;f_bgCK!|2~KniJH;V* z2rUt9LZMS#{66Nev-{rVcX~hG?7Rp7000000000000000006TP3&jP0{#Oo&005v0 zgZ?1Oxc14l&T1#mizo+sjZU&!Ej@Z<_WZeKL}X)`009(W0D$3;Z{+QZ?e5*~GtD}5 z_-K2%Rhy^@5a78>L;wI#h3;y%+uOhW7LPxqNkkcIy>4f^rQCh_Z<7GD(sci6~=#>-WvxT2Fug3NQe`a7bTCV|)K$WkhV32d57{`r}bqma!ch zzdl_5`6mGaJa;1^005{&xz~?H#8whpNu*iUm~UMF^!nuwFF&3*5FS7k{0A5S;NLTv zEu5M^bNWo0rg@(4-I)PUfB^vh^9!zB&9->_WjdNQPR{ML-#cG_-MRF^B~bxX$|KMK z0M)kNEXxkPeJCP|G9qe45=9a*nw>c{fBsxEP14@iz4rUXnbKOg=f?ch}x;@4MdOdyDx-E+_y67yw|16=kg5U5kh#?;L3@x3a7lAI+Li_I|(V zwU%46ybbJ8!+Z(7y1G?N-9WZf|{YYcLqp zYPFl6-z4lv~qqDJ0 ofU^M$Sp*)yaQYJf0RR630REKf08bE1zW@LL07*qoM6N<$f|=fnU;qFB diff --git a/assets/jp/freebies/MAIL_SELECT_OIL.png b/assets/jp/freebies/MAIL_SELECT_OIL.png index bd02ba74788df4a2ecd32e5b9236817635226c22..ae92e32e1e230149e10295f015179c6b1bd2c8db 100644 GIT binary patch delta 451 zcmeCxoua#8AD7cXPZ!6Kid%1P8)n~jkU91+d*VYD$&!R-zFjI?lG|(k^G`h3-0rkR z#3aRnpYbm{uPl?~;~mc3A3mQcKW+W8y;hTPGb8r}*2x0gs`c9+cXKj4xMuUe%1?9d z?aEHoYdb3J%6Fds8_+i8(&2t~W~L2+bAc+@&gvIO1+U#&x7zYKzn|NYKh^V|S5`*8 zS~!c3fsuzH$4y62u-{((^u8FIImP?_*V*$NzWh;h+6Fm>+h4D;Fcf6p%3huP*XQp( z?cYJiZ^h02${=un;Xqow%aIC+^?Oa_j&bZ~U{+{gY~YRj|4MX9doTk>0z*Rb5*-F6 z2?hoR76Tvw6kz~qV_*O(WMFV$U}j)wU|<9Cfi9@Y_^Tcc7W*Igu4ql++qDtOA~N8Wz^@6huDv!+D^O+U47$=tVKmoIw~yRKczdrO93`v0GqVRLBatC~aonm{gtr>mdKI;Vst03px6Pyhe` delta 440 zcmbQD+pD`_AD8_uPZ!6Kid%1PZp^x4z;HZqX^dkNTh50BXQq$8?VWdtY>Jax@W6@b zQ^A7+k}5A8t(SZ*G~e~Tb~1bBPyc@l1vWEsUtz7^p;@lR&=4Kmr`j^%xsCj@pi@5& zzs&A3G`m0RVIqg(lHVx~3}#vBObk05FRy=@{P)H0x*Cb||M{*&eV+5Ya@}$(yWVBY z3=9dzpQ7vj{oA8+dVcrRqskdJ!`OR-Tc@_f( zCJBZceW{VR>euakcTR6hZ1nXkTLz%`hyU99y9F5p4lpnbae1M(Sp zfCNw@14tn>y*z zl3%p#=>C2EZ|nO5rE7n?U$4D$bfNSb_5)>iXEHG4`C3k^T6OvS%4<@|Ji9k9_kZ~L z#t)NY5|?X@6u@rwVW_WrVtKxO=aiyn1zs!P|62I}`#0?dMqpq@vdCGy)(1N5|9@r% Yn}4%!E!w5?3dm*fboFyt=akR{0KUDy#{d8T diff --git a/module/freebies/assets.py b/module/freebies/assets.py index 6e9e4cd225..9481df6e91 100644 --- a/module/freebies/assets.py +++ b/module/freebies/assets.py @@ -9,8 +9,8 @@ DATA_KEY_COLLECT = Button(area={'cn': (251, 38, 339, 73), 'en': (256, 42, 337, 68), 'jp': (254, 40, 340, 72), 'tw': (251, 38, 339, 73)}, color={'cn': (144, 116, 77), 'en': (145, 109, 72), 'jp': (144, 111, 69), 'tw': (144, 116, 77)}, button={'cn': (251, 38, 339, 73), 'en': (256, 42, 337, 68), 'jp': (254, 40, 340, 72), 'tw': (251, 38, 339, 73)}, file={'cn': './assets/cn/freebies/DATA_KEY_COLLECT.png', 'en': './assets/en/freebies/DATA_KEY_COLLECT.png', 'jp': './assets/jp/freebies/DATA_KEY_COLLECT.png', 'tw': './assets/tw/freebies/DATA_KEY_COLLECT.png'}) DATA_KEY_COLLECTED = Button(area={'cn': (251, 38, 339, 73), 'en': (255, 42, 338, 68), 'jp': (254, 41, 340, 71), 'tw': (251, 38, 339, 73)}, color={'cn': (102, 103, 103), 'en': (113, 113, 115), 'jp': (102, 103, 103), 'tw': (102, 103, 103)}, button={'cn': (251, 38, 339, 73), 'en': (255, 42, 338, 68), 'jp': (254, 41, 340, 71), 'tw': (251, 38, 339, 73)}, file={'cn': './assets/cn/freebies/DATA_KEY_COLLECTED.png', 'en': './assets/en/freebies/DATA_KEY_COLLECTED.png', 'jp': './assets/jp/freebies/DATA_KEY_COLLECTED.png', 'tw': './assets/tw/freebies/DATA_KEY_COLLECTED.png'}) FREE_SUPPLY_PACK = Button(area={'cn': (525, 533, 579, 560), 'en': (523, 533, 582, 553), 'jp': (523, 530, 583, 559), 'tw': (524, 532, 582, 562)}, color={'cn': (144, 154, 164), 'en': (150, 160, 169), 'jp': (123, 137, 148), 'tw': (130, 143, 154)}, button={'cn': (378, 155, 577, 352), 'en': (426, 181, 557, 319), 'jp': (373, 177, 583, 356), 'tw': (388, 194, 554, 352)}, file={'cn': './assets/cn/freebies/FREE_SUPPLY_PACK.png', 'en': './assets/en/freebies/FREE_SUPPLY_PACK.png', 'jp': './assets/jp/freebies/FREE_SUPPLY_PACK.png', 'tw': './assets/tw/freebies/FREE_SUPPLY_PACK.png'}) -MAIL_BATCH_CLAIM = Button(area={'cn': (593, 524, 687, 546), 'en': (643, 525, 704, 543), 'jp': (593, 524, 687, 546), 'tw': (593, 524, 687, 546)}, color={'cn': (114, 209, 255), 'en': (147, 220, 255), 'jp': (114, 209, 255), 'tw': (114, 209, 255)}, button={'cn': (593, 524, 687, 546), 'en': (643, 525, 704, 543), 'jp': (593, 524, 687, 546), 'tw': (593, 524, 687, 546)}, file={'cn': './assets/cn/freebies/MAIL_BATCH_CLAIM.png', 'en': './assets/en/freebies/MAIL_BATCH_CLAIM.png', 'jp': './assets/cn/freebies/MAIL_BATCH_CLAIM.png', 'tw': './assets/cn/freebies/MAIL_BATCH_CLAIM.png'}) -MAIL_BATCH_DELETE = Button(area={'cn': (770, 523, 865, 547), 'en': (817, 526, 887, 544), 'jp': (770, 523, 865, 547), 'tw': (770, 523, 865, 547)}, color={'cn': (112, 209, 255), 'en': (150, 221, 255), 'jp': (112, 209, 255), 'tw': (112, 209, 255)}, button={'cn': (770, 523, 865, 547), 'en': (817, 526, 887, 544), 'jp': (770, 523, 865, 547), 'tw': (770, 523, 865, 547)}, file={'cn': './assets/cn/freebies/MAIL_BATCH_DELETE.png', 'en': './assets/en/freebies/MAIL_BATCH_DELETE.png', 'jp': './assets/cn/freebies/MAIL_BATCH_DELETE.png', 'tw': './assets/cn/freebies/MAIL_BATCH_DELETE.png'}) +MAIL_BATCH_CLAIM = Button(area={'cn': (593, 524, 687, 546), 'en': (643, 525, 704, 543), 'jp': (594, 525, 686, 547), 'tw': (593, 524, 687, 546)}, color={'cn': (114, 209, 255), 'en': (147, 220, 255), 'jp': (128, 213, 255), 'tw': (114, 209, 255)}, button={'cn': (593, 524, 687, 546), 'en': (643, 525, 704, 543), 'jp': (594, 525, 686, 547), 'tw': (593, 524, 687, 546)}, file={'cn': './assets/cn/freebies/MAIL_BATCH_CLAIM.png', 'en': './assets/en/freebies/MAIL_BATCH_CLAIM.png', 'jp': './assets/jp/freebies/MAIL_BATCH_CLAIM.png', 'tw': './assets/cn/freebies/MAIL_BATCH_CLAIM.png'}) +MAIL_BATCH_DELETE = Button(area={'cn': (770, 523, 865, 547), 'en': (817, 526, 887, 544), 'jp': (770, 524, 865, 548), 'tw': (770, 523, 865, 547)}, color={'cn': (112, 209, 255), 'en': (150, 221, 255), 'jp': (126, 213, 255), 'tw': (112, 209, 255)}, button={'cn': (770, 523, 865, 547), 'en': (817, 526, 887, 544), 'jp': (770, 524, 865, 548), 'tw': (770, 523, 865, 547)}, file={'cn': './assets/cn/freebies/MAIL_BATCH_DELETE.png', 'en': './assets/en/freebies/MAIL_BATCH_DELETE.png', 'jp': './assets/jp/freebies/MAIL_BATCH_DELETE.png', 'tw': './assets/cn/freebies/MAIL_BATCH_DELETE.png'}) MAIL_COLLECT = Button(area={'cn': (841, 577, 970, 608), 'en': (865, 583, 947, 601), 'jp': (842, 575, 964, 609), 'tw': (838, 575, 973, 611)}, color={'cn': (155, 184, 219), 'en': (151, 180, 216), 'jp': (116, 154, 203), 'tw': (145, 174, 212)}, button={'cn': (841, 577, 970, 608), 'en': (865, 583, 947, 601), 'jp': (842, 575, 964, 609), 'tw': (838, 575, 973, 611)}, file={'cn': './assets/cn/freebies/MAIL_COLLECT.png', 'en': './assets/en/freebies/MAIL_COLLECT.png', 'jp': './assets/jp/freebies/MAIL_COLLECT.png', 'tw': './assets/tw/freebies/MAIL_COLLECT.png'}) MAIL_COLLECTED = Button(area={'cn': (893, 578, 986, 607), 'en': (835, 578, 975, 606), 'jp': (861, 575, 951, 608), 'tw': (891, 576, 987, 609)}, color={'cn': (55, 61, 70), 'en': (54, 63, 71), 'jp': (48, 57, 65), 'tw': (55, 62, 72)}, button={'cn': (893, 578, 986, 607), 'en': (835, 578, 975, 606), 'jp': (861, 575, 951, 608), 'tw': (891, 576, 987, 609)}, file={'cn': './assets/cn/freebies/MAIL_COLLECTED.png', 'en': './assets/en/freebies/MAIL_COLLECTED.png', 'jp': './assets/jp/freebies/MAIL_COLLECTED.png', 'tw': './assets/tw/freebies/MAIL_COLLECTED.png'}) MAIL_DELETE = Button(area={'cn': (176, 560, 306, 590), 'en': (428, 567, 500, 584), 'jp': (177, 556, 307, 591), 'tw': (175, 559, 308, 592)}, color={'cn': (221, 171, 166), 'en': (216, 173, 169), 'jp': (210, 151, 146), 'tw': (217, 166, 162)}, button={'cn': (176, 560, 306, 590), 'en': (428, 567, 500, 584), 'jp': (177, 556, 307, 591), 'tw': (175, 559, 308, 592)}, file={'cn': './assets/cn/freebies/MAIL_DELETE.png', 'en': './assets/en/freebies/MAIL_DELETE.png', 'jp': './assets/jp/freebies/MAIL_DELETE.png', 'tw': './assets/tw/freebies/MAIL_DELETE.png'}) @@ -18,12 +18,12 @@ MAIL_EMPTY_2 = Button(area={'cn': (507, 364, 596, 391), 'en': (507, 364, 596, 391), 'jp': (507, 364, 596, 391), 'tw': (507, 364, 596, 391)}, color={'cn': (181, 185, 194), 'en': (181, 185, 194), 'jp': (181, 185, 194), 'tw': (181, 185, 194)}, button={'cn': (507, 364, 596, 391), 'en': (507, 364, 596, 391), 'jp': (507, 364, 596, 391), 'tw': (507, 364, 596, 391)}, file={'cn': './assets/cn/freebies/MAIL_EMPTY_2.png', 'en': './assets/en/freebies/MAIL_EMPTY_2.png', 'jp': './assets/jp/freebies/MAIL_EMPTY_2.png', 'tw': './assets/tw/freebies/MAIL_EMPTY_2.png'}) MAIL_ENTER = Button(area={'cn': (1207, 393, 1253, 429), 'en': (1207, 393, 1253, 429), 'jp': (1207, 393, 1253, 429), 'tw': (1207, 393, 1253, 429)}, color={'cn': (109, 107, 95), 'en': (109, 107, 95), 'jp': (109, 107, 95), 'tw': (109, 107, 95)}, button={'cn': (1207, 393, 1253, 429), 'en': (1207, 393, 1253, 429), 'jp': (1207, 393, 1253, 429), 'tw': (1207, 393, 1253, 429)}, file={'cn': './assets/cn/freebies/MAIL_ENTER.png', 'en': './assets/en/freebies/MAIL_ENTER.png', 'jp': './assets/jp/freebies/MAIL_ENTER.png', 'tw': './assets/tw/freebies/MAIL_ENTER.png'}) MAIL_GUILD_MESSAGE = Button(area={'cn': (412, 214, 461, 235), 'en': (412, 214, 461, 235), 'jp': (412, 214, 461, 235), 'tw': (412, 214, 461, 235)}, color={'cn': (123, 124, 126), 'en': (123, 124, 126), 'jp': (123, 124, 126), 'tw': (123, 124, 126)}, button={'cn': (412, 214, 461, 235), 'en': (412, 214, 461, 235), 'jp': (412, 214, 461, 235), 'tw': (412, 214, 461, 235)}, file={'cn': './assets/cn/freebies/MAIL_GUILD_MESSAGE.png', 'en': './assets/en/freebies/MAIL_GUILD_MESSAGE.png', 'jp': './assets/jp/freebies/MAIL_GUILD_MESSAGE.png', 'tw': './assets/tw/freebies/MAIL_GUILD_MESSAGE.png'}) -MAIL_MANAGE = Button(area={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (415, 639, 485, 658), 'tw': (415, 639, 485, 658)}, color={'cn': (116, 210, 255), 'en': (131, 214, 255), 'jp': (116, 210, 255), 'tw': (116, 210, 255)}, button={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (415, 639, 485, 658), 'tw': (415, 639, 485, 658)}, file={'cn': './assets/cn/freebies/MAIL_MANAGE.png', 'en': './assets/en/freebies/MAIL_MANAGE.png', 'jp': './assets/cn/freebies/MAIL_MANAGE.png', 'tw': './assets/cn/freebies/MAIL_MANAGE.png'}) -MAIL_SELECT_COINS = Button(area={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 401, 582, 421), 'tw': (562, 401, 582, 421)}, color={'cn': (241, 240, 241), 'en': (241, 240, 241), 'jp': (241, 240, 241), 'tw': (241, 240, 241)}, button={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 401, 582, 421), 'tw': (562, 401, 582, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_COINS.png', 'en': './assets/en/freebies/MAIL_SELECT_COINS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_COINS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_COINS.png'}) -MAIL_SELECT_CUBE = Button(area={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 401, 462, 421), 'tw': (442, 401, 462, 421)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (241, 241, 241), 'tw': (241, 241, 241)}, button={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 401, 462, 421), 'tw': (442, 401, 462, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_CUBE.png', 'en': './assets/en/freebies/MAIL_SELECT_CUBE.png', 'jp': './assets/jp/freebies/MAIL_SELECT_CUBE.png', 'tw': './assets/tw/freebies/MAIL_SELECT_CUBE.png'}) -MAIL_SELECT_GEMS = Button(area={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 441, 462, 461), 'tw': (442, 441, 462, 461)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (241, 241, 241), 'tw': (241, 241, 241)}, button={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 441, 462, 461), 'tw': (442, 441, 462, 461)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_GEMS.png', 'en': './assets/en/freebies/MAIL_SELECT_GEMS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_GEMS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_GEMS.png'}) -MAIL_SELECT_MERIT = Button(area={'cn': (802, 401, 822, 421), 'en': (802, 401, 822, 421), 'jp': (802, 401, 822, 421), 'tw': (802, 401, 822, 421)}, color={'cn': (87, 87, 88), 'en': (87, 87, 88), 'jp': (87, 87, 88), 'tw': (87, 87, 88)}, button={'cn': (802, 401, 822, 421), 'en': (802, 401, 822, 421), 'jp': (802, 401, 822, 421), 'tw': (802, 401, 822, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_MERIT.png', 'en': './assets/en/freebies/MAIL_SELECT_MERIT.png', 'jp': './assets/jp/freebies/MAIL_SELECT_MERIT.png', 'tw': './assets/tw/freebies/MAIL_SELECT_MERIT.png'}) -MAIL_SELECT_OIL = Button(area={'cn': (682, 401, 702, 421), 'en': (682, 401, 702, 421), 'jp': (682, 401, 702, 421), 'tw': (682, 401, 702, 421)}, color={'cn': (241, 240, 241), 'en': (241, 240, 241), 'jp': (241, 240, 241), 'tw': (241, 240, 241)}, button={'cn': (682, 401, 702, 421), 'en': (682, 401, 702, 421), 'jp': (682, 401, 702, 421), 'tw': (682, 401, 702, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_OIL.png', 'en': './assets/en/freebies/MAIL_SELECT_OIL.png', 'jp': './assets/jp/freebies/MAIL_SELECT_OIL.png', 'tw': './assets/tw/freebies/MAIL_SELECT_OIL.png'}) +MAIL_MANAGE = Button(area={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (407, 641, 495, 658), 'tw': (415, 639, 485, 658)}, color={'cn': (116, 210, 255), 'en': (131, 214, 255), 'jp': (115, 209, 255), 'tw': (116, 210, 255)}, button={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (407, 641, 495, 658), 'tw': (415, 639, 485, 658)}, file={'cn': './assets/cn/freebies/MAIL_MANAGE.png', 'en': './assets/en/freebies/MAIL_MANAGE.png', 'jp': './assets/jp/freebies/MAIL_MANAGE.png', 'tw': './assets/cn/freebies/MAIL_MANAGE.png'}) +MAIL_SELECT_COINS = Button(area={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 410, 582, 430), 'tw': (562, 401, 582, 421)}, color={'cn': (241, 240, 241), 'en': (241, 240, 241), 'jp': (239, 239, 239), 'tw': (241, 240, 241)}, button={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 410, 582, 430), 'tw': (562, 401, 582, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_COINS.png', 'en': './assets/en/freebies/MAIL_SELECT_COINS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_COINS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_COINS.png'}) +MAIL_SELECT_CUBE = Button(area={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 410, 462, 430), 'tw': (442, 401, 462, 421)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (239, 239, 239), 'tw': (241, 241, 241)}, button={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 410, 462, 430), 'tw': (442, 401, 462, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_CUBE.png', 'en': './assets/en/freebies/MAIL_SELECT_CUBE.png', 'jp': './assets/jp/freebies/MAIL_SELECT_CUBE.png', 'tw': './assets/tw/freebies/MAIL_SELECT_CUBE.png'}) +MAIL_SELECT_GEMS = Button(area={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 460, 462, 480), 'tw': (442, 441, 462, 461)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (239, 239, 239), 'tw': (241, 241, 241)}, button={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 460, 462, 480), 'tw': (442, 441, 462, 461)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_GEMS.png', 'en': './assets/en/freebies/MAIL_SELECT_GEMS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_GEMS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_GEMS.png'}) +MAIL_SELECT_MERIT = Button(area={'cn': (802, 401, 822, 421), 'en': (802, 401, 822, 421), 'jp': (802, 410, 822, 430), 'tw': (802, 401, 822, 421)}, color={'cn': (87, 87, 88), 'en': (87, 87, 88), 'jp': (239, 239, 239), 'tw': (87, 87, 88)}, button={'cn': (802, 401, 822, 421), 'en': (802, 401, 822, 421), 'jp': (802, 410, 822, 430), 'tw': (802, 401, 822, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_MERIT.png', 'en': './assets/en/freebies/MAIL_SELECT_MERIT.png', 'jp': './assets/jp/freebies/MAIL_SELECT_MERIT.png', 'tw': './assets/tw/freebies/MAIL_SELECT_MERIT.png'}) +MAIL_SELECT_OIL = Button(area={'cn': (682, 401, 702, 421), 'en': (682, 401, 702, 421), 'jp': (682, 410, 702, 430), 'tw': (682, 401, 702, 421)}, color={'cn': (241, 240, 241), 'en': (241, 240, 241), 'jp': (239, 239, 239), 'tw': (241, 240, 241)}, button={'cn': (682, 401, 702, 421), 'en': (682, 401, 702, 421), 'jp': (682, 410, 702, 430), 'tw': (682, 401, 702, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_OIL.png', 'en': './assets/en/freebies/MAIL_SELECT_OIL.png', 'jp': './assets/jp/freebies/MAIL_SELECT_OIL.png', 'tw': './assets/tw/freebies/MAIL_SELECT_OIL.png'}) OCR_DATA_KEY = Button(area={'cn': (132, 42, 233, 70), 'en': (132, 42, 233, 70), 'jp': (132, 42, 233, 70), 'tw': (132, 42, 233, 70)}, color={'cn': (74, 75, 86), 'en': (74, 75, 86), 'jp': (74, 75, 86), 'tw': (74, 75, 86)}, button={'cn': (132, 42, 233, 70), 'en': (132, 42, 233, 70), 'jp': (132, 42, 233, 70), 'tw': (132, 42, 233, 70)}, file={'cn': './assets/cn/freebies/OCR_DATA_KEY.png', 'en': './assets/en/freebies/OCR_DATA_KEY.png', 'jp': './assets/jp/freebies/OCR_DATA_KEY.png', 'tw': './assets/tw/freebies/OCR_DATA_KEY.png'}) PURCHASE_POPUP = Button(area={'cn': (907, 204, 934, 229), 'en': (907, 204, 934, 229), 'jp': (907, 204, 934, 229), 'tw': (907, 204, 934, 229)}, color={'cn': (176, 130, 110), 'en': (176, 130, 110), 'jp': (176, 130, 110), 'tw': (176, 130, 110)}, button={'cn': (907, 204, 934, 229), 'en': (907, 204, 934, 229), 'jp': (907, 204, 934, 229), 'tw': (907, 204, 934, 229)}, file={'cn': './assets/cn/freebies/PURCHASE_POPUP.png', 'en': './assets/en/freebies/PURCHASE_POPUP.png', 'jp': './assets/jp/freebies/PURCHASE_POPUP.png', 'tw': './assets/tw/freebies/PURCHASE_POPUP.png'}) REWARD_RECEIVE = Button(area={'cn': (1192, 520, 1255, 536), 'en': (1192, 522, 1254, 534), 'jp': (1186, 518, 1259, 536), 'tw': (1192, 520, 1255, 536)}, color={'cn': (191, 178, 163), 'en': (195, 182, 168), 'jp': (208, 197, 183), 'tw': (191, 178, 163)}, button={'cn': (1192, 520, 1255, 536), 'en': (1192, 522, 1254, 534), 'jp': (1186, 518, 1259, 536), 'tw': (1192, 520, 1255, 536)}, file={'cn': './assets/cn/freebies/REWARD_RECEIVE.png', 'en': './assets/en/freebies/REWARD_RECEIVE.png', 'jp': './assets/jp/freebies/REWARD_RECEIVE.png', 'tw': './assets/cn/freebies/REWARD_RECEIVE.png'}) diff --git a/module/freebies/mail_white.py b/module/freebies/mail_white.py index 1907b62c31..24b7c8630d 100644 --- a/module/freebies/mail_white.py +++ b/module/freebies/mail_white.py @@ -236,7 +236,7 @@ def run(self): if not merit and not maintenance and not trade_license: logger.warning('Nothing to claim') return False - if self.config.SERVER not in ['cn', 'en']: + if self.config.SERVER not in ['cn', 'en', 'jp']: logger.warning(f'Mail is not supported in {self.config.SERVER}, please contact server maintainers') return False From 9cc9835e2757682e712c16852c26a9b5ab17934e Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sat, 27 Jul 2024 01:13:54 +0800 Subject: [PATCH 157/161] Fix: Add another frame of TEMPLATE_STAGE_CLEAR_20240725 (#4033) --- .../template/TEMPLATE_STAGE_CLEAR_20240725.gif | Bin 0 -> 2651 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 1426 -> 0 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.gif | Bin 0 -> 2651 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 1426 -> 0 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.gif | Bin 0 -> 2651 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 1426 -> 0 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.gif | Bin 0 -> 2651 bytes .../template/TEMPLATE_STAGE_CLEAR_20240725.png | Bin 1426 -> 0 bytes campaign/event_20240725_cn/ht1.py | 2 +- campaign/event_20240725_cn/ht2.py | 2 +- campaign/event_20240725_cn/ht3.py | 2 +- campaign/event_20240725_cn/sp.py | 2 +- campaign/event_20240725_cn/t1.py | 2 +- campaign/event_20240725_cn/t2.py | 2 +- campaign/event_20240725_cn/t3.py | 2 +- module/template/assets.py | 2 +- 16 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.gif delete mode 100644 assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.png create mode 100644 assets/en/template/TEMPLATE_STAGE_CLEAR_20240725.gif delete mode 100644 assets/en/template/TEMPLATE_STAGE_CLEAR_20240725.png create mode 100644 assets/jp/template/TEMPLATE_STAGE_CLEAR_20240725.gif delete mode 100644 assets/jp/template/TEMPLATE_STAGE_CLEAR_20240725.png create mode 100644 assets/tw/template/TEMPLATE_STAGE_CLEAR_20240725.gif delete mode 100644 assets/tw/template/TEMPLATE_STAGE_CLEAR_20240725.png diff --git a/assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.gif b/assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.gif new file mode 100644 index 0000000000000000000000000000000000000000..6c8a153478672acaf2f40d573d61fa464da44e79 GIT binary patch literal 2651 zcmeIy{ZA7I7zgmX>p`!!2L}`>uHqeoLZQ@xw7eC#zEYrQd6@z-Y_S5O#v&}D5IEWj zMGy-#1|8_(P+>s>i85p{DFqkI{K3STnZ-nW$=r-tFmWbE7j~wJaoNAX_s1vCPv1Px zCof;0o?(ds_&_%R+`fH#VPWCcty?#5-n?<+#>~vj)vH&hrluw*CnqK*E?v4bK0Z#< z^vKA_@bIw5<8iy){r&xYeSN*Xy)KulySv-zbUGXkyWQT=(b3-C-rCxF^yty1rl!Wm z#>0mXA2@KJp`oF^zP_%muC}&z@7}!@i>11{x~i(Gva+(GqGH#sT|0N~EG;c9DJdx~ zF5aka(30b-52T~}7B zQJHipQW*7o-vmI`00*ubT6+lWx4TKZHyY34NLqfda&_Y9pd`r=m>}@H-O?1Vq7s_m*GZ>*^2S2!~r9 z`Z^y&sOzEGM4zyOECe}5(1?4ZY>+?p61SHJqH1@2*2U!>!3HFid}PV*ba4gg4`-f| z4+PtO+hihI4Pk7SbQ#CIU>JvpM1F8<)ei!*g03I>nP#09@dA}HAMOPr<2D}j5^Z2# z_*Y?%UfZUtx_B^uq?vq#lK>#_2q|o7&Jmn_#*t2#sY(unxtGwtP}0Xv`+zNi>=Cye z)$#wP04NpHHnZAf>JFuFzz*kpu(7x?8*LP8RvrSQin$0c2gel@3nERQ5iMMz?0UIj|7B0|Tpfw= z@wXrsHX*)X`I7DT(c-A$uYm|7RgUEpU+R_{QNwlDSU|H2SqP7z}xNd5oD_t@d*>b8>P%XEQA=%`fM+ZQB?*w{G3a$eEa!$gmj~7w0#V zAU=MbiacXG#CZXU7>_(gkRw$!O+cV zitnFWV5DaQKf21ShCpbuyhN%@v08$nGTWt3XkZBnfUNNZxzcstNe)V$fKFFGo_`Kg z4aVFF3D{6MXR^_d2PtY2882#mo!tCpNo37Jr-My)#go$uh@BPecH%3G$gkJ7c%Q~X z&lVg(R$b~Hz^8#O!wLkn!3aA@>RFEelhO`wc?kmIN18K4ph%0kk~9RMoFj27z)}*6 zQxN^|&9s;5++@Ug0{3^&{jhXzfoX+WI2oQ7zWj%ce*ajQc;?NT0K=UK1jS5MyZ5T^ zaP^lHz4XQRondkeGrfLJQXm{-0y(6F@^Vu66p0qum|4JGGeB*A<^6@NZ#EC{3UQT& z7g#M@3N}h+wCMF745B!1OaMsmW*ofS2bhMpm7E?6)xV-B6h&~7WrW2*DIz2s+`{>l z7;d83%V%pO(?U+7kRlJI7$^Xa#s?fj)_E_)6+3NKP-!-i;Oal$kPwvAs73c>-h@12 zntFPI6G|_HMB=LeqFKf?=Ox3(YB#u}RM2q{w~ZsJ5i0!rzUmkIFNi#5ACk?FXQ)NI t-}2=5+*i2guWbRu<05CJ8qJ-X_JYtHZM`yc{G|GV*p;c~5R92w{{n`?C`bSR literal 0 HcmV?d00001 diff --git a/assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.png b/assets/cn/template/TEMPLATE_STAGE_CLEAR_20240725.png deleted file mode 100644 index af27d9a11243757cea08c43e0dd468d0caf8b016..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1426 zcmV;D1#S9?P)KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO51AM#2z{tSB zz;IdD(Z$J?fi%FHTu@ZPz`$^Tfq}s&CAB!2fq~%*0|P^Pc}YPD0|R3W0|SFdQg%TJ z0|R3L0|SFdc1Vyj0|R3V0|OIJNoqw20|NttbACZ(QD%BZiGrb}rKN&nN`6wRLU3hq zNosDff@fZGeo;YwQDRAI3IhWJ)D8v)1_oZ2{1OHC#LPSeLsL}-Dual~C?)grM$+xhxmf|p7B=;2nnnfbQ63e)F`Yd zd{`u1lvi}CSe!Vg_*RJ&Nny#OQWes=(obaO$cD-Z%AJ+(QSedZRlJ}yML9}EN#(Wb zR<%ZTKMh%px0?I3CTgeZSnCSuzS29QKi{CnFv`f%Skm~n$vxA{#r++CO)=?RdfInDbtjt*-0cR=O|sSme3TYk~JdpT)k*{8ss|57-*GH|SXK z`H)+o&%(Y$FhvSRDMcH{xWz`r<;Axo%ud{#bT;{UDpQ(Vx=lt@W>wa#>^(X6@|g0~ z3w#QTi)I%eE_qufQSMSvSUIoiZ1vw-y}J1NNe#yue>WSnq_@s%yWSz#>D|@deYlsQ z&%VEI!oG?BCp%7QoqA$A?~LG?vt~V-qcyi=-o6D~3&R#IUi@*X!?Fp>AFecB)w=rT zTHSR`>u+u}*wnH4!B(qnQ@4NE>AP#y9*(`~`;H$_KiGNb^%1|Ln~#g1s6F}QwD*}U z=VZ^fU-)z>?((Ut7T1>D5WU%Y>+7BLyEpIqJUH;k^zrJaiqB@g5PaG7n)yxL+n?`C zKYaRB@cG@>yl?M*RI+y?e7jKeZ#YO-C0rN>jK~#9!Y|_7P5^)^H@yELlm%DO> zJGm=dCLtlS>;D2Y-M;jYwk| zuu_8vZO?KoXFTl}aHvg9(`S8+?`L1yg+TxayVk~(@{L>=wA(uDC;aV64P&&II?8o; zb8{&f+_ri( zE?}xHNjkSc#3#It=M%1t`_{gD(F-GyiCB@Pf=j0Zq(57n;KhDa`tm!bdi%@5G)j$8 zzgO`Jmy|}3>%Pp;RfU-MxMV4c(N!_|1UOrKaYri_Z^%}^%4x(Pp82>0WZg~BG}?I? zBGlR{;>j|H%0pl{@fMH;8^D8^W9QjCtXpx&={F?m8+OzgekTE=`5aqqO=+@R&cb{o zEx2R+<-l6w_48VH(aE0h^=Z=3adRSUVBM@?AHjHVL+27o#$j@UZIje-R-?p`!!2L}`>uHqeoLZQ@xw7eC#zEYrQd6@z-Y_S5O#v&}D5IEWj zMGy-#1|8_(P+>s>i85p{DFqkI{K3STnZ-nW$=r-tFmWbE7j~wJaoNAX_s1vCPv1Px zCof;0o?(ds_&_%R+`fH#VPWCcty?#5-n?<+#>~vj)vH&hrluw*CnqK*E?v4bK0Z#< z^vKA_@bIw5<8iy){r&xYeSN*Xy)KulySv-zbUGXkyWQT=(b3-C-rCxF^yty1rl!Wm z#>0mXA2@KJp`oF^zP_%muC}&z@7}!@i>11{x~i(Gva+(GqGH#sT|0N~EG;c9DJdx~ zF5aka(30b-52T~}7B zQJHipQW*7o-vmI`00*ubT6+lWx4TKZHyY34NLqfda&_Y9pd`r=m>}@H-O?1Vq7s_m*GZ>*^2S2!~r9 z`Z^y&sOzEGM4zyOECe}5(1?4ZY>+?p61SHJqH1@2*2U!>!3HFid}PV*ba4gg4`-f| z4+PtO+hihI4Pk7SbQ#CIU>JvpM1F8<)ei!*g03I>nP#09@dA}HAMOPr<2D}j5^Z2# z_*Y?%UfZUtx_B^uq?vq#lK>#_2q|o7&Jmn_#*t2#sY(unxtGwtP}0Xv`+zNi>=Cye z)$#wP04NpHHnZAf>JFuFzz*kpu(7x?8*LP8RvrSQin$0c2gel@3nERQ5iMMz?0UIj|7B0|Tpfw= z@wXrsHX*)X`I7DT(c-A$uYm|7RgUEpU+R_{QNwlDSU|H2SqP7z}xNd5oD_t@d*>b8>P%XEQA=%`fM+ZQB?*w{G3a$eEa!$gmj~7w0#V zAU=MbiacXG#CZXU7>_(gkRw$!O+cV zitnFWV5DaQKf21ShCpbuyhN%@v08$nGTWt3XkZBnfUNNZxzcstNe)V$fKFFGo_`Kg z4aVFF3D{6MXR^_d2PtY2882#mo!tCpNo37Jr-My)#go$uh@BPecH%3G$gkJ7c%Q~X z&lVg(R$b~Hz^8#O!wLkn!3aA@>RFEelhO`wc?kmIN18K4ph%0kk~9RMoFj27z)}*6 zQxN^|&9s;5++@Ug0{3^&{jhXzfoX+WI2oQ7zWj%ce*ajQc;?NT0K=UK1jS5MyZ5T^ zaP^lHz4XQRondkeGrfLJQXm{-0y(6F@^Vu66p0qum|4JGGeB*A<^6@NZ#EC{3UQT& z7g#M@3N}h+wCMF745B!1OaMsmW*ofS2bhMpm7E?6)xV-B6h&~7WrW2*DIz2s+`{>l z7;d83%V%pO(?U+7kRlJI7$^Xa#s?fj)_E_)6+3NKP-!-i;Oal$kPwvAs73c>-h@12 zntFPI6G|_HMB=LeqFKf?=Ox3(YB#u}RM2q{w~ZsJ5i0!rzUmkIFNi#5ACk?FXQ)NI t-}2=5+*i2guWbRu<05CJ8qJ-X_JYtHZM`yc{G|GV*p;c~5R92w{{n`?C`bSR literal 0 HcmV?d00001 diff --git a/assets/en/template/TEMPLATE_STAGE_CLEAR_20240725.png b/assets/en/template/TEMPLATE_STAGE_CLEAR_20240725.png deleted file mode 100644 index af27d9a11243757cea08c43e0dd468d0caf8b016..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1426 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 literal 0 HcmV?d00001 diff --git a/assets/jp/template/TEMPLATE_STAGE_CLEAR_20240725.png b/assets/jp/template/TEMPLATE_STAGE_CLEAR_20240725.png deleted file mode 100644 index af27d9a11243757cea08c43e0dd468d0caf8b016..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1426 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 literal 0 HcmV?d00001 diff --git a/assets/tw/template/TEMPLATE_STAGE_CLEAR_20240725.png b/assets/tw/template/TEMPLATE_STAGE_CLEAR_20240725.png deleted file mode 100644 index af27d9a11243757cea08c43e0dd468d0caf8b016..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1426 zcmV;D1#S9?P)KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO51AM#2z{tSB zz;IdD(Z$J?fi%FHTu@ZPz`$^Tfq}s&CAB!2fq~%*0|P^Pc}YPD0|R3W0|SFdQg%TJ z0|R3L0|SFdc1Vyj0|R3V0|OIJNoqw20|NttbACZ(QD%BZiGrb}rKN&nN`6wRLU3hq zNosDff@fZGeo;YwQDRAI3IhWJ)D8v)1_oZ2{1OHC#LPSeLsL}-Dual~C?)grM$+xhxmf|p7B=;2nnnfbQ63e)F`Yd zd{`u1lvi}CSe!Vg_*RJ&Nny#OQWes=(obaO$cD-Z%AJ+(QSedZRlJ}yML9}EN#(Wb zR<%ZTKMh%px0?I3CTgeZSnCSuzS29QKi{CnFv`f%Skm~n$vxA{#r++CO)=?RdfInDbtjt*-0cR=O|sSme3TYk~JdpT)k*{8ss|57-*GH|SXK z`H)+o&%(Y$FhvSRDMcH{xWz`r<;Axo%ud{#bT;{UDpQ(Vx=lt@W>wa#>^(X6@|g0~ z3w#QTi)I%eE_qufQSMSvSUIoiZ1vw-y}J1NNe#yue>WSnq_@s%yWSz#>D|@deYlsQ z&%VEI!oG?BCp%7QoqA$A?~LG?vt~V-qcyi=-o6D~3&R#IUi@*X!?Fp>AFecB)w=rT zTHSR`>u+u}*wnH4!B(qnQ@4NE>AP#y9*(`~`;H$_KiGNb^%1|Ln~#g1s6F}QwD*}U z=VZ^fU-)z>?((Ut7T1>D5WU%Y>+7BLyEpIqJUH;k^zrJaiqB@g5PaG7n)yxL+n?`C zKYaRB@cG@>yl?M*RI+y?e7jKeZ#YO-C0rN>jK~#9!Y|_7P5^)^H@yELlm%DO> zJGm=dCLtlS>;D2Y-M;jYwk| zuu_8vZO?KoXFTl}aHvg9(`S8+?`L1yg+TxayVk~(@{L>=wA(uDC;aV64P&&II?8o; zb8{&f+_ri( zE?}xHNjkSc#3#It=M%1t`_{gD(F-GyiCB@Pf=j0Zq(57n;KhDa`tm!bdi%@5G)j$8 zzgO`Jmy|}3>%Pp;RfU-MxMV4c(N!_|1UOrKaYri_Z^%}^%4x(Pp82>0WZg~BG}?I? zBGlR{;>j|H%0pl{@fMH;8^D8^W9QjCtXpx&={F?m8+OzgekTE=`5aqqO=+@R&cb{o zEx2R+<-l6w_48VH(aE0h^=Z=3adRSUVBM@?AHjHVL+27o#$j@UZIje-R-? Date: Sat, 27 Jul 2024 01:35:13 +0800 Subject: [PATCH 158/161] Fix: Select all mails to delete --- assets/cn/freebies/MAIL_SELECT_ALL.png | Bin 0 -> 6188 bytes assets/en/freebies/MAIL_SELECT_ALL.png | Bin 0 -> 6188 bytes assets/jp/freebies/MAIL_SELECT_ALL.png | Bin 0 -> 6151 bytes assets/tw/freebies/MAIL_SELECT_ALL.png | Bin 0 -> 6188 bytes module/freebies/assets.py | 1 + module/freebies/mail_white.py | 13 +++++++++++++ 6 files changed, 14 insertions(+) create mode 100644 assets/cn/freebies/MAIL_SELECT_ALL.png create mode 100644 assets/en/freebies/MAIL_SELECT_ALL.png create mode 100644 assets/jp/freebies/MAIL_SELECT_ALL.png create mode 100644 assets/tw/freebies/MAIL_SELECT_ALL.png diff --git a/assets/cn/freebies/MAIL_SELECT_ALL.png b/assets/cn/freebies/MAIL_SELECT_ALL.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bf046f8fd79fc49367bf02b96eb9b1f06167bf GIT binary patch literal 6188 zcmeI0`8O2M`^T@5EZMS6k$q_)yB4x!$q->=&91Ct$v(D55+(bVT_}dJi%ABRwXqj5 z$ZpbL?6QCLIp6=`^ZB9YoO_;o&U4Rsp8LA@oacS-3;lbV40N1y000Ipm^vH)s*@?C zp9XT`RCu*1P8_W#%*-2rGpv6H1<1%c2LPR}vznT|zLSTKhqsf5C!dy@8lR_^hl4ZX z5dZ;%Y(sz3q#X{W$(=_`#;PN$RcVMYARvgL>0~y`XHEpR&zM=FX2FbF zpAON%vZH45E@2&Szl<`EHnO;}!aH8{J|vL5yuANozmb?lJm^LCF`>?~h$JlJyMUYU zh+Fa_ym5N4)-D_h!Va4{#mBsW|LjQB2EZyMG9c8aQu%pG3oiq*wBl#E5 zG$4ioq+ik$f~b51Hb!A^F-Xg2kcUwb5I@yYPJuG3lh%Zwa8$rA)-;|;#Rj0#RmHji z-wg;*<6-Tg>A$`@HN!vi8BwKb3%$(j zKszUpm6j>U3L{eL^wI;sxjXRb=!SAUaba$H!GVY%Z?@$hxvqK$DDkgN9yPv#oC3`E z`p5+9`T1c=&kl-8-)2|z13)kaPLsQqGGnxP=5+mmyV1xS2P!-{Z}dBFabKW2b7ooC z?hQ_IHd^UeX4+#aI&}LadNdZe3La;uq?ff6gdaY(b@wZW#}e_URHfW^h=NBQ5l;?d zFAvPfR#}L#zg#nYfz=n#?LdBjC#dsxrOdD_K9OGIO+BPd@@D{;=Xx#o>Zsr25iOxB z6p&~}(?LI=61s8q73Y}*!0ae1jobkssd+a}MU(>cq!rEr(D>_|MR77zr=11>^}Mhv zmA9$sKe1t27_&ZcH?}|_wx`uq=!IHtF{_@QLtUVIb1wDv`TH&MBd6Y1QgY^<&6sAc z{UmjRfjGrt5k~Tap0+uc^Z8ukbJ|n4J84*L`Q@X|+Qp6t^wL1nV^0a#!ekRsg?x)q zYz6{tonD0yw5m&jB}{Prsu$bM7z>rX__0nt>6ZsD8#Sep!>SD^ze9XgJ{0qEzHHC) z9`KQ6vP7lkyAE*H@+hL}@{0!U{_^z!AunU?Q3Fzp4zEUSEnY=x*t{_16zSlwpEs2= zj+3+JHofl0&ef7*OQsQmM59=jXsn(we?~lwkJmKSF;y!TUJ|TjX`nHo4Sy!}`JzBl zv0klkAj@uRfSr6*yy+c`$O=CXe~z%#MMhTZB$yCmSK{0~I)34JX{|F_uCOuK)k4EU zr9o!rEFBS2yj$1LIhtw|hA%ClKAtIFFLA%?%UnI1d|T9v4sE^=yfB$XTketa{HeFO z(yHlsB?dj{obNOxQS!OwT5frM`VxkbXjWO@4B?(k4mEt?)qQQY+(-cXy0x^k>nu-#JQ$ z(A-8J(bvj*;r{SA{XPThf+p+3r+fAc)z2gDdnzC?Oy&~hTfp7lvkCn zRt>w9Ih2K#T_*$*nh18wUdtTIBm&vjGg~EFEPG4dO@7Chw&7ZXK|@5tm~Y>2rEm5m zh zB(ype3}_Wx#EPs$I?l=kK$RRY9vAE|vOgb^ zZ*JRe3vat^_i=Y$VBsEC@KFf%^6~BVLwGil3a4eipj**j7EK(v9N&0nZ^%L4-Hpey zr1_;m4c&@f7P;pJ;#1~1R~iQ2f7>d^{FpgjHtP0$qFb>oq;y|bJd>--@{1z*_U7u2 z>CEHq+3cdsqR$)gLJKo9FJa2mPpR9q+91$>w_sAKFCI1N%7HM?;@pAyAmWf zFgLR4U6a71Vv}I#aAi;|VyHfzmY`Q+SEZ+AFyr+~@@RpsbM4y8j5~^D46uKt$!0g_+LP=S(cu|B8~ePE zW|BG7#Vs{-))RZP73d`$!J{`In!gl$Jbcy(HgIYtM$38n^!zoCTAsQ z6~GysQ)@+kNY-Df*K-gRO=6fsF1UT=N{altGbgy~y)nj)h|n1*sD+p7Gjixi6pIEt zIQJj`3&X+8zspQI9R1!EJoA(z7#ClukFBg3!3LPEO$jt>eB=;}HiY_%OIgAj9tJqC z4u;^J@YSh@`ZK0*qjeYL_@;{Y#unRLL_pFD4w~%fG!cXn`ugfGDsgUH3*)eOb(8jsk9_oL-%239& z7BkOf?WXi>Gv`(^w~&t&?O z`Yjp~3@JJ&-|UOITHuza_y$M*^{OD7+lpJWss1>WEH!KI+aVWcl9QKxNpWp=?rG9& z(Ziyg?0d?J-_ysDJ30sHu9gOtI7hX<9A&7_icY=tPS?gyVw~k_qvxi;j@_7W{oOLf zaz)Kh&%aouNus3PCsJe)??`x>>F_Pq6N}N2L*}vOodN8tjW;eR8 zxg3e!@FJI@E95%_J9ry=k&0{Y4$6ARLdaNh!(<1JH~rFq%8~xYz@B|aQNP}f5;6Gb zKw{_F#=h`iuFQ$rvhck?y=pxh-FRJnrI3n}qmdtWhMQhizh9L}x;7p0Ywo^u%G4achUza* zqhhq-ZK9cw=ivQU^0{;m7Bk(^;02OjW2x8;88Hl@Nzq>I%j2muuxE_cLEv-Q`!qIv^ zh~m8(>Zy-T-is;}r=P3k5bx%SyT|^k|$cHZDC0<;9&n3&QQ+)(kfM_~~u7 zw)s*>Am6tcw{D{m2*BpfQySDKsqMj$_C1dvaZ$m1F}sgCi9&Y-j>JC3y`-M3qoM&Q z2t9q|TwB$M_>K{(9&gXu!E?NY6~m9j#sqsG{*a@9fc{9$ys+EwZH29ps9z!jx&l4b zqZ>8&+^Z6n4+7U89M4t*Fx7QT1kXF2wtFKsyrINF*`AGC+Ajde1C(kZ-h?4UbdmMD zg3+Z>Z-wRhkmiuBg3^2lu*tmzVW79$@2N(t$8<$q`WjWtp~a@v>U7P2`XGqhTckzU zE&p~6=Ik;cgY{}0MJK;@f#;s zZCFhjdgPd!T69+DOK0>bkL@2e)l_0_$k3B!wazJIgSrs1<)ohc=wN;0w?orSU$2sF z^W{#wW_x?}?%fUN3_oM}7<^1wFln-eL?ZqFDU^UX_2l{AdZiP}4MmgkifenV-@DM+ zh85+q6$g&{MktEE`KLu&wA2%Yx?t<8k&z1fEzTKrD|O-$7UDeYeVKpP{OWU!M^Svd zm~#9lV83(>XH{Wd_H%FD*nrJBQOQw~mFGV@m37f{J+Y7O2F_<66Wv9>+Qoe$h%S4~ zQ3Dm_dts+f!$KOG_U-qUvud!e*I{IdRgc@aYX>wY;2tF zGn!;X_sm`t*-`d7Xnp8iWp>jd;Uv=`Hw%U2jX<20I-AC0B1z$TGd7)w%x#;UohA0> zWQGJ?)BP6JDNVHb`@{6l{t@^`;2(j11pd1OR-1RZ060FT1Y>IC>ZsEz9)Ii6(zvHy Ju4?o2{{V&$-yi@0 literal 0 HcmV?d00001 diff --git a/assets/en/freebies/MAIL_SELECT_ALL.png b/assets/en/freebies/MAIL_SELECT_ALL.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bf046f8fd79fc49367bf02b96eb9b1f06167bf GIT binary patch literal 6188 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 Ju4?o2{{V&$-yi@0 literal 0 HcmV?d00001 diff --git a/assets/jp/freebies/MAIL_SELECT_ALL.png b/assets/jp/freebies/MAIL_SELECT_ALL.png new file mode 100644 index 0000000000000000000000000000000000000000..5152ddf988b39e714c2f71cb628f586720f72a52 GIT binary patch literal 6151 zcmeI0`8O0`7ssC&WbCqyCE2x*Eh076Fl5h~-IQ%GvX5O7CHuY$B^oLz69$!iEFnww zU9yZNWPkPk5$`$gIo)%fbDwkWbDsNsKKK4`&+|lISA&k4lNta(r-{5}2mrM%$dNv1X-1KmO z8krkCQ*aUQsQfhgZj7;|+#1hV$%o(o((3BL&_Uf|&f;M=wud3?ER%5JLZJ(|ZWwt} zewZg-582dC2!paBr^tC37l>aRDVhOTC&T)Oc)wLVguKB3DDW5%6J%(V7N4S3=W11&y~VJ zECgg;)DVO!)dE}Na6?gO;}=kXQ{opRZ>)fXnSYehfQAt$fNz{x0)vt*2+LFv?F77X zP@sCAxr>Zfo2)>huVJdx#v>Asgn&k3YFGO0zN;+w)Nh=Ao9k0Ed^2BM->KNaFEKh$ z&GF}?XA3YR7s<4{X#wEe>-~Hzr}#jFv`k zIYDAJM&ab@G-fI$Wal(`JQ}bL9%LzHmNynf96hje_pLCDTO^XJNV@MX3LLjah91RT z>Yb5(XDQ11bi?ckUY}p575m9B@fKfu>I~CTsMH2e+7VTu+9 zDpS&aX2CVm=X~a_YlK7X&fHR>6>Pl8sB&g5>>TxT_B3SZA1O6%XgrmIAL{ zZ)paru(U#SFK5+xMA*l|lHS`teY`>N(>PREuOz+0vmbVr&!W_9pO|q9x3Zz;&16jC zWl-E^vc9Zbjd^w?m>@JJjCloS{fP04>%)Wu4Kr;s)mK6*0#!^kFjK0CN0MJI@F&00 zs}c%e+H3N+mw%UFrj8R{<2%onCuDtrp4lcDDM;U*G^b0=CzK$iNu!BIjv}uV-ziq; zV|32Z7G6$pYoC~dRfges(-W%`7!vf74%)xYRkO%9N6%1Je@q7K*QmXH4uLnxG zjcZG{mTk*;bcFNIo99O1YxB+wyMmyFOGk*J=!EB5Ou{dVVSNqn5Et zRNEcf^h@Kt8ryEai?-pvNQ0za9$5)#*tG->S4lb4dS?RaCpyC!-@E>3`qf#YQ%z!% zQ&F#G(FMHlT9o6gj6Yn#0f#wfkCUFbKZd?(XYVXETHI9JJXe3ID+Qh9P}5u0@7>wi zCOU>2#d58z+$(oW2LAGP0+A`* zHywV*t>kHmdwu{hb)Ivrrtd@lc2Rb1_E`B3x9{Vfi00te2RdTeT$_$% z4?1UaOR`J8Y$Ys;Tl-p@TOSb4>%Gqr&Ks0)Ya4B{m?97ZZRHgZ_xHP($P`)>p!os$ zQS~qD`6m(e0wIGTnIV*ic?UQ4iAV8&8m6(EY#X{1wvf+|c?cn#GyFXnnv91$ll%px z3`HzO?HO@e1y&VWDmrr>&tyy^e3NSrb#=^9G^>}j1D4Brmup|5Q&{`_;MusxJ+Mi} z5X{Rx?WN9-u;?ib*JwPCGe;(TFPP=hWvMc;Uj|ioydQY)iVouD%nuw1aU1ca&pzE4 zb@}M&>xgr4HFQO-WaGo|W%zLas6T2O=}g|tf>f(d=9-3^5Km^kpo^|qS99jFjMBVO z74u_-pWq8i-NZb4sYd4S7yTvt_BM;Doloy@95=SSS==JRVsI{~?&Rz#Dd^4Hb%KZHjH8^-+t zx9B6dS#L~`Qda6di1?eL32MRH?tNtn{h!afzm9$Fn#>HX z-i9qBu_Ar)4L-OlMQ#O%=LFKvXGOW(*4!HP)h8h&$yt<7t4zFUUP10f#Kzv-!{phL zdnI|fx{8SJnPb>p?ZZs8m4Ow(QMD&e5$?UFU2U`5zBREJZ?#_M@tc3wepIOXb~&N~ zp%LP7G>v>?fmy!4OI$U*S>gR#XZc1isXuhzrE$P(=+BUtRX&zLYFXi`b{8H$hZm~Z z{^Pr|3iqR4(;l-5i1&YUSU+;~i*ldgS7_DoyZeo9Q-5q5ZF|0&Gmz-1qVkSYPWq#! z`!qT=1_aavV&ijpc(~{|37hx2lc!56OPG5)LQGDu%U-V+Cy!5gRWL%ezN|NqADtWGjIu^Wve>vHn)?KB~3ko8GR%V$kuS z`0k^v1EIeBtEbZ@FA8a>13-Wf0AUdT?46wYB>;RR0QhAKfP4l3a7>a-v-;`8Dy?}- z*~ounWn39;Y0Q3Rsz0NduGq5{CMfrry?4oTC=VXl;bDOfc8dxO1#cyX zfiNg7tqx>jZ=<9OBU_u>&7sjTzZuJbk^V(T!4ATLicasD{W;}y)E(eEhB!c$^qZdq zyf-XEGMokl{wDEh@Tu4dynJKr<63nwB~YqQzXe#3Nln%yiUs<(lf$DS#2y4Be7*2Z zDg55}`-)KFi_OD~Xud=WKRtX`d0y?!=?P zuSv z)7UY}=jwLbX1(kq1HAdCfOtPtqfZbkzO{R+vXAtG-wPuEV6nAzvTPXCZb>dtL_K>4 zM~N@!3C*@|qji=bpy0F+sE^l1>jKs;kJk0&ly_a1JCuG1(b(M8S S3?cAWfTo)6tqK*}hyMXT7OZ&y literal 0 HcmV?d00001 diff --git a/assets/tw/freebies/MAIL_SELECT_ALL.png b/assets/tw/freebies/MAIL_SELECT_ALL.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bf046f8fd79fc49367bf02b96eb9b1f06167bf GIT binary patch literal 6188 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 Ju4?o2{{V&$-yi@0 literal 0 HcmV?d00001 diff --git a/module/freebies/assets.py b/module/freebies/assets.py index 9481df6e91..a0f1ac059f 100644 --- a/module/freebies/assets.py +++ b/module/freebies/assets.py @@ -19,6 +19,7 @@ MAIL_ENTER = Button(area={'cn': (1207, 393, 1253, 429), 'en': (1207, 393, 1253, 429), 'jp': (1207, 393, 1253, 429), 'tw': (1207, 393, 1253, 429)}, color={'cn': (109, 107, 95), 'en': (109, 107, 95), 'jp': (109, 107, 95), 'tw': (109, 107, 95)}, button={'cn': (1207, 393, 1253, 429), 'en': (1207, 393, 1253, 429), 'jp': (1207, 393, 1253, 429), 'tw': (1207, 393, 1253, 429)}, file={'cn': './assets/cn/freebies/MAIL_ENTER.png', 'en': './assets/en/freebies/MAIL_ENTER.png', 'jp': './assets/jp/freebies/MAIL_ENTER.png', 'tw': './assets/tw/freebies/MAIL_ENTER.png'}) MAIL_GUILD_MESSAGE = Button(area={'cn': (412, 214, 461, 235), 'en': (412, 214, 461, 235), 'jp': (412, 214, 461, 235), 'tw': (412, 214, 461, 235)}, color={'cn': (123, 124, 126), 'en': (123, 124, 126), 'jp': (123, 124, 126), 'tw': (123, 124, 126)}, button={'cn': (412, 214, 461, 235), 'en': (412, 214, 461, 235), 'jp': (412, 214, 461, 235), 'tw': (412, 214, 461, 235)}, file={'cn': './assets/cn/freebies/MAIL_GUILD_MESSAGE.png', 'en': './assets/en/freebies/MAIL_GUILD_MESSAGE.png', 'jp': './assets/jp/freebies/MAIL_GUILD_MESSAGE.png', 'tw': './assets/tw/freebies/MAIL_GUILD_MESSAGE.png'}) MAIL_MANAGE = Button(area={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (407, 641, 495, 658), 'tw': (415, 639, 485, 658)}, color={'cn': (116, 210, 255), 'en': (131, 214, 255), 'jp': (115, 209, 255), 'tw': (116, 210, 255)}, button={'cn': (415, 639, 485, 658), 'en': (393, 641, 463, 660), 'jp': (407, 641, 495, 658), 'tw': (415, 639, 485, 658)}, file={'cn': './assets/cn/freebies/MAIL_MANAGE.png', 'en': './assets/en/freebies/MAIL_MANAGE.png', 'jp': './assets/jp/freebies/MAIL_MANAGE.png', 'tw': './assets/cn/freebies/MAIL_MANAGE.png'}) +MAIL_SELECT_ALL = Button(area={'cn': (390, 319, 410, 339), 'en': (390, 319, 410, 339), 'jp': (390, 323, 410, 343), 'tw': (390, 319, 410, 339)}, color={'cn': (87, 88, 88), 'en': (87, 88, 88), 'jp': (91, 90, 91), 'tw': (87, 88, 88)}, button={'cn': (390, 319, 410, 339), 'en': (390, 319, 410, 339), 'jp': (390, 323, 410, 343), 'tw': (390, 319, 410, 339)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_ALL.png', 'en': './assets/en/freebies/MAIL_SELECT_ALL.png', 'jp': './assets/jp/freebies/MAIL_SELECT_ALL.png', 'tw': './assets/tw/freebies/MAIL_SELECT_ALL.png'}) MAIL_SELECT_COINS = Button(area={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 410, 582, 430), 'tw': (562, 401, 582, 421)}, color={'cn': (241, 240, 241), 'en': (241, 240, 241), 'jp': (239, 239, 239), 'tw': (241, 240, 241)}, button={'cn': (562, 401, 582, 421), 'en': (562, 401, 582, 421), 'jp': (562, 410, 582, 430), 'tw': (562, 401, 582, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_COINS.png', 'en': './assets/en/freebies/MAIL_SELECT_COINS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_COINS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_COINS.png'}) MAIL_SELECT_CUBE = Button(area={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 410, 462, 430), 'tw': (442, 401, 462, 421)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (239, 239, 239), 'tw': (241, 241, 241)}, button={'cn': (442, 401, 462, 421), 'en': (442, 401, 462, 421), 'jp': (442, 410, 462, 430), 'tw': (442, 401, 462, 421)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_CUBE.png', 'en': './assets/en/freebies/MAIL_SELECT_CUBE.png', 'jp': './assets/jp/freebies/MAIL_SELECT_CUBE.png', 'tw': './assets/tw/freebies/MAIL_SELECT_CUBE.png'}) MAIL_SELECT_GEMS = Button(area={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 460, 462, 480), 'tw': (442, 441, 462, 461)}, color={'cn': (241, 241, 241), 'en': (241, 241, 241), 'jp': (239, 239, 239), 'tw': (241, 241, 241)}, button={'cn': (442, 441, 462, 461), 'en': (442, 441, 462, 461), 'jp': (442, 460, 462, 480), 'tw': (442, 441, 462, 461)}, file={'cn': './assets/cn/freebies/MAIL_SELECT_GEMS.png', 'en': './assets/en/freebies/MAIL_SELECT_GEMS.png', 'jp': './assets/jp/freebies/MAIL_SELECT_GEMS.png', 'tw': './assets/tw/freebies/MAIL_SELECT_GEMS.png'}) diff --git a/module/freebies/mail_white.py b/module/freebies/mail_white.py index 24b7c8630d..d8c7dc4624 100644 --- a/module/freebies/mail_white.py +++ b/module/freebies/mail_white.py @@ -27,6 +27,18 @@ def mail_select_setting(self): ) return setting + @cached_property + def mail_select_all_setting(self): + setting = MailSelectSetting('MailAll', main=self) + setting.reset_first = False + setting.add_setting( + setting='all', + option_buttons=[MAIL_SELECT_ALL], + option_names=['all'], + option_default='all' + ) + return setting + def _mail_enter(self, skip_first_screenshot=True): """ Returns: @@ -222,6 +234,7 @@ def mail_claim( if delete: logger.hr('Mail delete', level=2) self._mail_enter() + self.mail_select_all_setting.set(contains=['all']) self._mail_delete() self._mail_quit() From a92fd8a305640d4e5e7310b955b63b61a906e702 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sat, 27 Jul 2024 01:47:27 +0800 Subject: [PATCH 159/161] Del: Remove outdated map files --- .../campaign_main/campaign_12_2_leveling.py | 80 ----------- .../campaign_main/campaign_12_4_leveling.py | 93 ------------- .../campaign_12_4_timeout_leveling.py | 124 ------------------ .../campaign_1_1_affinity_farming.py | 37 ------ .../campaign_7_2_mystery_farming.py | 86 ------------ .../campaign_main/campaign_8_4_leveling.py | 38 ------ 6 files changed, 458 deletions(-) delete mode 100644 campaign/campaign_main/campaign_12_2_leveling.py delete mode 100644 campaign/campaign_main/campaign_12_4_leveling.py delete mode 100644 campaign/campaign_main/campaign_12_4_timeout_leveling.py delete mode 100644 campaign/campaign_main/campaign_1_1_affinity_farming.py delete mode 100644 campaign/campaign_main/campaign_7_2_mystery_farming.py delete mode 100644 campaign/campaign_main/campaign_8_4_leveling.py diff --git a/campaign/campaign_main/campaign_12_2_leveling.py b/campaign/campaign_main/campaign_12_2_leveling.py deleted file mode 100644 index acc7444f64..0000000000 --- a/campaign/campaign_main/campaign_12_2_leveling.py +++ /dev/null @@ -1,80 +0,0 @@ -from campaign.campaign_main.campaign_12_1 import Config as ConfigBase -from module.campaign.campaign_base import CampaignBase -from module.logger import logger -from module.map.map_base import CampaignMap -from module.map.map_grids import RoadGrids, SelectedGrids - -MAP = CampaignMap() -MAP.shape = 'I7' -MAP.camera_data = ['D2', 'D5', 'F2', 'F5'] -MAP.camera_data_spawn_point = ['D2', 'D5'] -MAP.map_data = """ - ++ MB ME ME ++ -- ME Me -- - ++ -- Me -- Me -- Me -- ++ - MB ME ++ ME SP ME -- ME ++ - MB __ ME -- SP ++ ++ __ Me - ++ -- -- Me ME -- ME -- ME - -- ME ME ++ -- -- Me ME -- - ME -- Me -- ME ME -- ++ ++ -""" -MAP.weight_data = """ - 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 -""" -MAP.spawn_data = [ - {'battle': 0, 'enemy': 3}, - {'battle': 1, 'enemy': 2}, - {'battle': 2, 'enemy': 1}, - {'battle': 3, 'enemy': 1}, - {'battle': 4, 'enemy': 1}, - {'battle': 5}, - {'battle': 6, 'boss': 1}, -] -A1, B1, C1, D1, E1, F1, G1, H1, I1, \ -A2, B2, C2, D2, E2, F2, G2, H2, I2, \ -A3, B3, C3, D3, E3, F3, G3, H3, I3, \ -A4, B4, C4, D4, E4, F4, G4, H4, I4, \ -A5, B5, C5, D5, E5, F5, G5, H5, I5, \ -A6, B6, C6, D6, E6, F6, G6, H6, I6, \ -A7, B7, C7, D7, E7, F7, G7, H7, I7, \ - = MAP.flatten() - - -class Config(ConfigBase): - ENABLE_AUTO_SEARCH = False - - -class Campaign(CampaignBase): - MAP = MAP - s3_enemy_count = 0 - - def check_s3_enemy(self): - if self.battle_count == 0: - self.s3_enemy_count = 0 - elif self.battle_count >= 5: - self.withdraw() - - current = self.map.select(is_enemy=True, enemy_scale=2) \ - .add(self.map.select(is_enemy=True, enemy_scale=1)) \ - .count - logger.attr('S2_enemy', current) - - if self.s3_enemy_count >= self.config.C122MediumLeveling_LargeEnemyTolerance and current == 0: - self.withdraw() - - def battle_0(self): - self.check_s3_enemy() - if self.clear_enemy(scale=(2,), genre=['light', 'main', 'treasure', 'enemy', 'carrier']): - return True - if self.clear_enemy(scale=(1,)): - return True - if self.clear_enemy(scale=(3,), genre=['light', 'carrier', 'enemy', 'treasure', 'main']): - self.s3_enemy_count += 1 - return True - - return self.battle_default() diff --git a/campaign/campaign_main/campaign_12_4_leveling.py b/campaign/campaign_main/campaign_12_4_leveling.py deleted file mode 100644 index f9120e0f2b..0000000000 --- a/campaign/campaign_main/campaign_12_4_leveling.py +++ /dev/null @@ -1,93 +0,0 @@ -from campaign.campaign_main.campaign_12_1 import Config as ConfigBase -from module.campaign.campaign_base import CampaignBase -from module.logger import logger -from module.map.map_base import CampaignMap -from module.map.map_grids import RoadGrids, SelectedGrids - -MAP = CampaignMap('12-4') -MAP.shape = 'K8' -MAP.camera_data = ['D2', 'D6', 'H2', 'H6'] -MAP.camera_data_spawn_point = ['D6'] -MAP.map_data = """ - MB MB ME -- ME ++ ++ ++ MB MB ++ - ME ++ -- ME -- MA ++ ++ ME Me ++ - -- ME __ Me Me -- Me Me -- Me -- - ++ -- ME ++ ++ Me ME __ ++ ++ ME - ++ ME ME -- ME ME -- ME -- ++ -- - ++ __ Me Me -- Me ME ++ __ -- ME - ME -- Me -- Me -- Me -- -- ME -- - -- -- -- ME SP SP ++ ++ ++ ME -- -""" -MAP.weight_data = """ - 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 50 -""" -MAP.spawn_data = [ - {'battle': 0, 'enemy': 2}, - {'battle': 1, 'enemy': 2}, - {'battle': 2, 'enemy': 2}, - {'battle': 3, 'enemy': 1}, - {'battle': 4, 'enemy': 1}, - {'battle': 5}, - {'battle': 6, 'boss': 1}, -] -A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, \ -A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, \ -A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, \ -A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, \ -A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, K5, \ -A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, K6, \ -A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, K7, \ -A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, K8, \ - = MAP.flatten() - - -class Config(ConfigBase): - ENABLE_AUTO_SEARCH = False - - -class Campaign(CampaignBase): - MAP = MAP - s3_enemy_count = 0 - non_s3_enemy_count = 0 - - def check_s3_enemy(self): - if self.battle_count == 0: - self.s3_enemy_count = 0 - self.non_s3_enemy_count = 0 - - current = self.map.select(is_enemy=True, enemy_scale=3).count - logger.attr('S3_enemy', current) - - if self.battle_count == self.config.C124LargeLeveling_NonLargeEnterTolerance \ - and self.config.C124LargeLeveling_NonLargeRetreatTolerance < 10: - if self.s3_enemy_count + current == 0: - self.withdraw() - elif self.battle_count > self.config.C124LargeLeveling_NonLargeEnterTolerance: - if self.non_s3_enemy_count >= self.config.C124LargeLeveling_NonLargeRetreatTolerance and current == 0: - self.withdraw() - - def battle_0(self): - self.check_s3_enemy() - - if self.battle_count >= self.config.C124LargeLeveling_PickupAmmo: - self.pick_up_ammo() - - if self.clear_enemy(scale=(3,), genre=['light', 'carrier', 'enemy', 'treasure', 'main']): - self.s3_enemy_count += 1 - self.non_s3_enemy_count = 0 - return True - if self.clear_enemy(scale=[2, 1]): - self.non_s3_enemy_count += 1 - return True - if not self.map.select(is_enemy=True, may_boss=False): - logger.info('No more enemies.') - self.withdraw() - - return self.battle_default() diff --git a/campaign/campaign_main/campaign_12_4_timeout_leveling.py b/campaign/campaign_main/campaign_12_4_timeout_leveling.py deleted file mode 100644 index 153decea0e..0000000000 --- a/campaign/campaign_main/campaign_12_4_timeout_leveling.py +++ /dev/null @@ -1,124 +0,0 @@ -from module.campaign.campaign_base import CampaignBase -from module.combat.assets import * -from module.logger import logger -from module.map.map_base import CampaignMap -from module.map.map_grids import RoadGrids, SelectedGrids - - -class Campaign(CampaignBase): - # MAP = MAP - - def battle_default(self): - if not self.map.select(enemy_scale=3, enemy_genre='Light'): - self.withdraw() - - def handle_combat_weapon_release(self): - if self.appear_then_click(READY_AIR_RAID, interval=5): - return True - - return False - - def handle_battle_status(self, save_get_items=False): - """ - Args: - save_get_items (bool): - - Returns: - bool: - """ - if self.is_combat_executing(): - return False - if self.appear_then_click(BATTLE_STATUS_S, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): - if not save_get_items: - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(BATTLE_STATUS_A, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): - logger.warning('Battle status: A') - if not save_get_items: - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(BATTLE_STATUS_B, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): - logger.warning('Battle Status B') - if not save_get_items: - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(BATTLE_STATUS_C, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): - if not save_get_items: - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(BATTLE_STATUS_D, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): - logger.warning('Battle Status D') - if not save_get_items: - self.device.sleep((0.25, 0.5)) - return True - - return False - - def handle_get_items(self, save_get_items=False): - """ - Args: - save_get_items (bool): - - Returns: - bool: - """ - if self.appear_then_click(GET_ITEMS_1, screenshot=save_get_items, genre='get_items', offset=5, - interval=self.battle_status_click_interval): - self.interval_reset(BATTLE_STATUS_S) - self.interval_reset(BATTLE_STATUS_A) - self.interval_reset(BATTLE_STATUS_B) - self.interval_reset(BATTLE_STATUS_C) - self.interval_reset(BATTLE_STATUS_D) - return True - if self.appear_then_click(GET_ITEMS_2, screenshot=save_get_items, genre='get_items', offset=5, - interval=self.battle_status_click_interval): - self.interval_reset(BATTLE_STATUS_S) - self.interval_reset(BATTLE_STATUS_A) - self.interval_reset(BATTLE_STATUS_B) - self.interval_reset(BATTLE_STATUS_C) - self.interval_reset(BATTLE_STATUS_D) - return True - - return False - - def handle_exp_info(self): - """ - Returns: - bool: - """ - if self.is_combat_executing(): - return False - if self.appear_then_click(EXP_INFO_S): - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(EXP_INFO_A): - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(EXP_INFO_B): - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(EXP_INFO_C): - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(EXP_INFO_D): - self.device.sleep((0.25, 0.5)) - return True - if self.appear_then_click(OPTS_INFO_D, offset=(20, 20)): - self.device.sleep((0.25, 0.5)) - return True - - return False - - def combat(self, balance_hp=None, emotion_reduce=None, func=None, call_submarine_at_boss=None, save_get_items=None, - expected_end=None, fleet_index=1): - self.battle_status_click_interval = 7 if save_get_items else 0 - super().combat(balance_hp=False, expected_end='no_searching', auto_mode='hide_in_bottom_left', save_get_items=False) - - -from module.config.config import AzurLaneConfig - -az = Campaign(AzurLaneConfig('alas')) -for n in range(10000): - logger.hr(f'count: {n}') - az.map_offensive() - az.combat() diff --git a/campaign/campaign_main/campaign_1_1_affinity_farming.py b/campaign/campaign_main/campaign_1_1_affinity_farming.py deleted file mode 100644 index 9c774a1eea..0000000000 --- a/campaign/campaign_main/campaign_1_1_affinity_farming.py +++ /dev/null @@ -1,37 +0,0 @@ -import numpy as np - -from module.campaign.campaign_base import CampaignBase -from module.exception import CampaignEnd, ScriptEnd -from module.logger import logger -from module.map.map_base import CampaignMap -from module.map.map_grids import RoadGrids, SelectedGrids - -from .campaign_1_1 import MAP -from .campaign_1_1 import Config as ConfigBase - -A1, B1, C1, D1, E1, F1, G1, \ - = MAP.flatten() - - -class Config(ConfigBase): - ENABLE_FAST_FORWARD = False - ENABLE_AUTO_SEARCH = False - AMBUSH_EVADE = False - - -class Campaign(CampaignBase): - MAP = MAP - affinity_battle = 0 - - def battle_default(self): - while self.affinity_battle < self.config.C11AffinityFarming_RunCount: - logger.attr('Affinity_battle', f'{self.affinity_battle}/{self.config.C11AffinityFarming_RunCount}') - self.goto(C1) - self.affinity_battle += 1 - self.goto(D1 if np.random.uniform() < 0.7 else B1) - - # End - try: - self.withdraw() - except CampaignEnd: - raise ScriptEnd('Reach condition: Affinity farming battle count') diff --git a/campaign/campaign_main/campaign_7_2_mystery_farming.py b/campaign/campaign_main/campaign_7_2_mystery_farming.py deleted file mode 100644 index 8571ba5c35..0000000000 --- a/campaign/campaign_main/campaign_7_2_mystery_farming.py +++ /dev/null @@ -1,86 +0,0 @@ -from campaign.campaign_main.campaign_7_2 import MAP -from campaign.campaign_main.campaign_7_2 import Config as ConfigBase -from module.campaign.campaign_base import CampaignBase -from module.logger import logger -from module.map.map_base import CampaignMap -from module.map.map_grids import RoadGrids, SelectedGrids - -# MAP.in_map_swipe_preset_data = (-1, 0) - -A1, B1, C1, D1, E1, F1, G1, H1, \ -A2, B2, C2, D2, E2, F2, G2, H2, \ -A3, B3, C3, D3, E3, F3, G3, H3, \ -A4, B4, C4, D4, E4, F4, G4, H4, \ -A5, B5, C5, D5, E5, F5, G5, H5 = MAP.flatten() - -ROAD_MAIN = RoadGrids([A3, [C3, B4, C5], [F1, G2, G3]]) -GRIDS_FOR_FASTER = SelectedGrids([A3, C3, E3, G3]) -FLEET_2_STEP_ON = SelectedGrids([A3, G3, C3, E3]) - - -class Config(ConfigBase): - ENABLE_AUTO_SEARCH = False - - -class Campaign(CampaignBase): - MAP = MAP - - def battle_0(self): - if self.config.C72MysteryFarming_StepOnA3: - if self.fleet_2_step_on(FLEET_2_STEP_ON, roadblocks=[ROAD_MAIN]): - return True - - ignore = None - if self.fleet_at(A3, fleet=2) and A1.enemy_scale != 3 and not self.fleet_at(A1, fleet=1): - ignore = SelectedGrids([A2]) - if self.fleet_at(G3, fleet=2): - ignore = SelectedGrids([H3]) - - self.clear_all_mystery(nearby=False, ignore=ignore) - else: - self.clear_all_mystery(nearby=False) - - if self.clear_roadblocks([ROAD_MAIN], strongest=True): - return True - if self.clear_potential_roadblocks([ROAD_MAIN], strongest=True): - return True - - if self.clear_enemy(scale=(3,)): - return True - - if self.clear_grids_for_faster(GRIDS_FOR_FASTER, scale=(2,)): - return True - if self.clear_enemy(scale=(2,)): - return True - if self.clear_grids_for_faster(GRIDS_FOR_FASTER): - return True - - return self.battle_default() - - def battle_3(self): - if self.config.C72MysteryFarming_StepOnA3: - ignore = None - if self.fleet_at(A3, fleet=2): - ignore = SelectedGrids([A2]) - if self.fleet_at(G3, fleet=2): - ignore = SelectedGrids([H3]) - self.clear_all_mystery(nearby=False, ignore=ignore) - - if self.fleet_at(A3, fleet=2) and A2.is_mystery: - self.fleet_2.clear_chosen_mystery(A2) - if self.fleet_at(G3, fleet=2) and H3.is_mystery: - self.fleet_2.clear_chosen_mystery(H3) - else: - self.clear_all_mystery(nearby=False) - - if self.map.select(is_mystery=True, is_accessible=False): - logger.info('Roadblock blocks mystery.') - if self.fleet_1.clear_roadblocks([ROAD_MAIN]): - return True - - if not self.map.select(is_mystery=True): - self.withdraw() - - @property - def _map_battle(self): - return 3 diff --git a/campaign/campaign_main/campaign_8_4_leveling.py b/campaign/campaign_main/campaign_8_4_leveling.py deleted file mode 100644 index d55e39ae65..0000000000 --- a/campaign/campaign_main/campaign_8_4_leveling.py +++ /dev/null @@ -1,38 +0,0 @@ -from .campaign_8_4 import Campaign as CampaignBase -from .campaign_8_4 import Config as ConfigBase -from .campaign_8_4 import * - - -class Config(ConfigBase): - ENABLE_AUTO_SEARCH = False - - -class Campaign(CampaignBase): - def battle_0(self): - self.fleet_2_push_forward() - - self.clear_all_mystery() - if self.map.select(is_mystery=True, is_accessible_1=False, is_accessible_2=True): - self.fleet_2.clear_all_mystery() - self.fleet_2_push_forward() - - if self.clear_roadblocks([road_D7, road_F3, road_main], strongest=True): - return True - if self.clear_potential_roadblocks([road_D7, road_F3, road_main], scale=(3,)): - return True - if self.clear_enemy(scale=(3,)): - return True - if self.clear_potential_roadblocks([road_D7, road_F3, road_main], strongest=True): - return True - if self.clear_first_roadblocks([road_D7, road_F3, road_main]): - return True - - return self.battle_default() - - def battle_4(self): - self.clear_all_mystery() - if self.map.select(is_mystery=True, is_accessible_1=False, is_accessible_2=True): - self.fleet_2.clear_all_mystery() - self.fleet_2_push_forward() - - return self.brute_clear_boss() From 7d4b21b4fc091e97ef7a32dda2b9c5a8cf3e5a76 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sat, 27 Jul 2024 01:54:57 +0800 Subject: [PATCH 160/161] =?UTF-8?q?Upd:=20[CN]=20New=20server=20=E6=9A=B4?= =?UTF-8?q?=E9=9B=A8=E8=A1=8C=E5=8A=A8=20(#4025)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/config/argument/args.json | 1 + module/config/config_generated.py | 2 +- module/config/i18n/en-US.json | 1 + module/config/i18n/ja-JP.json | 1 + module/config/i18n/zh-CN.json | 1 + module/config/i18n/zh-TW.json | 1 + module/config/server.py | 2 +- 7 files changed, 7 insertions(+), 2 deletions(-) diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 5eab0a05e3..5279f0a6c8 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -62,6 +62,7 @@ "cn_android-21", "cn_android-22", "cn_android-23", + "cn_android-24", "cn_ios-0", "cn_ios-1", "cn_ios-2", diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 2deaa1b1f0..5d811bdcb2 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -20,7 +20,7 @@ class GeneratedConfig: # Group `Emulator` 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_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_android-24, cn_ios-0, cn_ios-1, cn_ios-2, cn_ios-3, cn_ios-4, cn_ios-5, cn_ios-6, cn_ios-7, cn_ios-8, cn_ios-9, cn_ios-10, cn_channel-0, cn_channel-1, cn_channel-2, cn_channel-3, cn_channel-4, en-0, en-1, en-2, en-3, en-4, en-5, jp-0, jp-1, jp-2, jp-3, jp-4, jp-5, jp-6, jp-7, jp-8, jp-9, jp-10, jp-11, jp-12, jp-13, jp-14, jp-15, jp-16, jp-17 Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy, nemu_ipc, ldopengl Emulator_ControlMethod = 'MaaTouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 9faab78629..8cbc6320d8 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -357,6 +357,7 @@ "cn_android-21": "[国服] 莱茵河卫兵", "cn_android-22": "[国服] 北极光计划", "cn_android-23": "[国服] 长戟计划", + "cn_android-24": "[国服] 暴雨行动", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 4dfbabfd62..4e2b30f6ac 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -357,6 +357,7 @@ "cn_android-21": "[国服] 莱茵河卫兵", "cn_android-22": "[国服] 北极光计划", "cn_android-23": "[国服] 长戟计划", + "cn_android-24": "[国服] 暴雨行动", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 4926eadb51..30a6473498 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -357,6 +357,7 @@ "cn_android-21": "[国服] 莱茵河卫兵", "cn_android-22": "[国服] 北极光计划", "cn_android-23": "[国服] 长戟计划", + "cn_android-24": "[国服] 暴雨行动", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 13c1735a6f..9dea145215 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -357,6 +357,7 @@ "cn_android-21": "[国服] 莱茵河卫兵", "cn_android-22": "[国服] 北极光计划", "cn_android-23": "[国服] 长戟计划", + "cn_android-24": "[国服] 暴雨行动", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", diff --git a/module/config/server.py b/module/config/server.py index e26060a18c..6a9ecfd3d9 100644 --- a/module/config/server.py +++ b/module/config/server.py @@ -43,7 +43,7 @@ '杜立特空袭', '地狱犬行动', '开罗宣言', '奥林匹克行动', '小王冠行动', '波茨坦公告', '白色方案', '瓦尔基里行动', '曼哈顿计划', '八月风暴', '秋季旅行', '水星行动', '莱茵河卫兵', - '北极光计划', '长戟计划' + '北极光计划', '长戟计划', '暴雨行动' ], 'cn_ios': [ '夏威夷', '珊瑚海', '中途岛', '铁底湾', '所罗门', '马里亚纳', From 85f19a2f9f0c75fab5dd52c21cbb015846c5334a Mon Sep 17 00:00:00 2001 From: PPPlatelet Date: Sun, 28 Jul 2024 14:57:45 +0800 Subject: [PATCH 161/161] Upd: fix. --- module/device/platform/api_windows.py | 21 +++++++------------ module/device/platform/platform_windows.py | 8 +++---- .../device/platform/winapi/const_windows.py | 8 +++++++ .../platform/winapi/functions_windows.py | 11 ++++------ .../platform/winapi/structures_windows.py | 8 +++---- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/module/device/platform/api_windows.py b/module/device/platform/api_windows.py index 120cb8d004..de1d05191f 100644 --- a/module/device/platform/api_windows.py +++ b/module/device/platform/api_windows.py @@ -223,7 +223,7 @@ def execute(command: str, silentstart: bool, start: bool) -> tuple: '"E:\\Program Files\\Netease\\MuMu Player 12\\shell\\MuMuPlayer.exe" -v 1' Returns: - process: tuple(processhandle, threadhandle, processid, mainthreadid), + process: PROCESS_INFORMATION, focusedwindow: tuple(hwnd, WINDOWPLACEMENT) Raises: @@ -279,7 +279,7 @@ def execute(command: str, silentstart: bool, start: bool) -> tuple: return lpProcessInformation, focusedwindow else: closehandle(lpProcessInformation.hProcess, lpProcessInformation.hThread) - return (), focusedwindow + return None, focusedwindow def terminate_process(pid: int) -> bool: @@ -492,7 +492,7 @@ def get_thread(pid: int) -> int: return mainthreadid -def _get_process(pid: int) -> tuple: +def _get_process(pid: int) -> PROCESS_INFORMATION: """ Get emulator's handle. @@ -514,12 +514,12 @@ def _get_process(pid: int) -> tuple: CloseHandle(hProcess) report("OpenThread failed.", level=30) - return hProcess, hThread, pid, tid + return PROCESS_INFORMATION(hProcess, hThread, pid, tid) except Exception as e: logger.warning(f"Failed to get process and thread handles: {e}") - return None, None, pid, tid + return PROCESS_INFORMATION(None, None, pid, tid) -def get_process(instance: EmulatorInstance) -> tuple: +def get_process(instance: EmulatorInstance) -> PROCESS_INFORMATION: """ Get emulator's process. @@ -566,11 +566,7 @@ def switch_window(hwnds: list, arg: int = SW_SHOWNORMAL) -> bool: bool: """ for hwnd in hwnds: - if GetParent(hwnd) is not None: - continue - rect = RECT() - GetWindowRect(hwnd, byref(rect)) - if {rect.left, rect.top, rect.right, rect.bottom} == {0}: + if not GetWindow(hwnd, GW_CHILD): continue ShowWindow(hwnd, arg) return True @@ -648,8 +644,7 @@ async def grab_pids(self): if not IsUserAnAdmin(): report("Currently not running in administrator mode", statuscode=GetLastError()) with evt_query() as hevent: - events = _enum_events(hevent) - for content in events: + for content in _enum_events(hevent): data = self.evttree.parse_event(content) with self.lock: self.datas.append(data) diff --git a/module/device/platform/platform_windows.py b/module/device/platform/platform_windows.py index 52515996c3..469c3bf8a0 100644 --- a/module/device/platform/platform_windows.py +++ b/module/device/platform/platform_windows.py @@ -13,7 +13,7 @@ class EmulatorUnknown(Exception): class PlatformWindows(PlatformBase, EmulatorManager): # Quadruple, contains the kernel process object, kernel thread object, process ID and thread ID. - process: tuple = () + process = None # Window handles of the target process. hwnds: list = [] # Pair, contains the hwnd of the focused window and a WINDOWPLACEMENT object. @@ -31,7 +31,7 @@ def __execute(self, command: str, start: bool) -> bool: if self.process: if not all(self.process[:2]): api_windows.closehandle(*self.process[:2]) - self.process = () + self.process = None if self.hwnds: self.hwnds = [] @@ -62,7 +62,7 @@ def get_hwnds(pid: int) -> list: return api_windows.get_hwnds(pid) @staticmethod - def get_process(instance: api_windows.t.Optional[EmulatorInstance]) -> tuple: + def get_process(instance: api_windows.t.Optional[EmulatorInstance]) -> api_windows.PROCESS_INFORMATION: return api_windows.get_process(instance) @staticmethod @@ -340,7 +340,7 @@ def emulator_check(self) -> bool: else: if not all(self.process[:2]): api_windows.closehandle(*self.process[:2]) - self.process = () + self.process = None raise ProcessLookupError except api_windows.IterationFinished: return False diff --git a/module/device/platform/winapi/const_windows.py b/module/device/platform/winapi/const_windows.py index 4f9a9de5a2..1cd5b53162 100644 --- a/module/device/platform/winapi/const_windows.py +++ b/module/device/platform/winapi/const_windows.py @@ -113,6 +113,14 @@ SW_FORCEMINIMIZE = 11 SW_MAX = 11 +GW_HWNDFIRST = 0 +GW_HWNDLAST = 1 +GW_HWNDNEXT = 2 +GW_HWNDPREV = 3 +GW_OWNER = 4 +GW_CHILD = 5 +GW_ENABLEDPOPUP = 6 + # winbase.h line 377 DEBUG_PROCESS = 0x00000001 DEBUG_ONLY_THIS_PROCESS = 0x00000002 diff --git a/module/device/platform/winapi/functions_windows.py b/module/device/platform/winapi/functions_windows.py index 99335e5ece..9a559b41c5 100644 --- a/module/device/platform/winapi/functions_windows.py +++ b/module/device/platform/winapi/functions_windows.py @@ -16,7 +16,7 @@ from module.device.platform.winapi.structures_windows import \ SECURITY_ATTRIBUTES, STARTUPINFOW, WINDOWPLACEMENT, \ PROCESS_INFORMATION, PROCESSENTRY32W, THREADENTRY32, \ - FILETIME, RECT + FILETIME from module.logger import logger @@ -105,12 +105,9 @@ ShowWindow.argtypes = [HWND, INT] ShowWindow.restype = BOOL -GetParent = user32.GetParent -GetParent.argtypes = [HWND] -GetParent.restype = HWND -GetWindowRect = user32.GetWindowRect -GetWindowRect.argtypes = [HWND, POINTER(RECT)] -GetWindowRect.restype = BOOL +GetWindow = user32.GetWindow +GetWindow.argtypes = [HWND, UINT] +GetWindow.restype = HWND EnumWindowsProc = WINFUNCTYPE(BOOL, HWND, LPARAM, use_last_error=True) EnumWindows = user32.EnumWindows diff --git a/module/device/platform/winapi/structures_windows.py b/module/device/platform/winapi/structures_windows.py index cb05b8a177..206617b2ac 100644 --- a/module/device/platform/winapi/structures_windows.py +++ b/module/device/platform/winapi/structures_windows.py @@ -17,10 +17,7 @@ def __init_subclass__(cls, **kwargs): def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented - for name in self.field_name: - if getattr(self, name) != getattr(other, name): - return False - return True + return all(getattr(self, name) == getattr(other, name) for name in self.field_name) def __repr__(self): field_values = ', '.join(f"{name}={getattr(self, name)!r}" for name in self.field_name) @@ -130,6 +127,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): ... def __bytes__(self): return bytes(str(self), 'utf-8') + def __bool__(self): + return any(getattr(self, name) for name in self.field_name) + # processthreadsapi.h line 28 class PROCESS_INFORMATION(Structure): _fields_ = [