feat(task): 为 DMM 控制适配 HostProtocol
This commit is contained in:
parent
548ba04071
commit
68bf47d89e
|
@ -9,8 +9,6 @@ from dataclasses import dataclass, field
|
|||
from typing import Any, Literal, Callable, Generic, TypeVar, ParamSpec
|
||||
|
||||
from kotonebot.client import Device
|
||||
from kotonebot.client.host import Mumu12Host, LeidianHost
|
||||
from kotonebot.ui import user
|
||||
from kotonebot.client.host.protocol import Instance
|
||||
from kotonebot.backend.context import init_context, vars
|
||||
from kotonebot.backend.context import task_registry, action_registry, Task, Action
|
||||
|
@ -132,80 +130,34 @@ class KotoneBot:
|
|||
logger.debug(f'Loading sub-module: {name}')
|
||||
try:
|
||||
importlib.import_module(name)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error(f'Failed to load sub-module: {name}')
|
||||
logger.exception(f'Error: ')
|
||||
logger.exception('Error: ')
|
||||
|
||||
logger.info('Tasks and actions initialized.')
|
||||
logger.info(f'{len(task_registry)} task(s) and {len(action_registry)} action(s) loaded.')
|
||||
|
||||
def check_backend(self) -> Device:
|
||||
from kotonebot.client.host import create_custom
|
||||
from kotonebot.config.manager import load_config
|
||||
# HACK: 硬编码
|
||||
config = load_config(self.config_path, type=self.config_type)
|
||||
config = config.user_configs[0]
|
||||
logger.info('Checking backend...')
|
||||
if config.backend.type == 'custom':
|
||||
exe = config.backend.emulator_path
|
||||
if exe is None:
|
||||
user.error('「检查并启动模拟器」已开启但未配置「模拟器 exe 文件路径」。')
|
||||
raise ValueError('Emulator executable path is not set.')
|
||||
if not os.path.exists(exe):
|
||||
user.error('「模拟器 exe 文件路径」对应的文件不存在!请检查路径是否正确。')
|
||||
raise FileNotFoundError(f'Emulator executable not found: {exe}')
|
||||
self.backend_instance = create_custom(
|
||||
adb_ip=config.backend.adb_ip,
|
||||
adb_port=config.backend.adb_port,
|
||||
adb_name=config.backend.adb_emulator_name,
|
||||
exe_path=exe,
|
||||
emulator_args=config.backend.emulator_args
|
||||
)
|
||||
if config.backend.check_emulator:
|
||||
if not self.backend_instance.running():
|
||||
logger.info('Starting custom backend...')
|
||||
self.backend_instance.start()
|
||||
logger.info('Waiting for custom backend to be available...')
|
||||
self.backend_instance.wait_available()
|
||||
else:
|
||||
logger.info('Custom backend "%s" already running.', self.backend_instance)
|
||||
elif config.backend.type == 'mumu12':
|
||||
if config.backend.instance_id is None:
|
||||
raise ValueError('MuMu12 instance ID is not set.')
|
||||
self.backend_instance = Mumu12Host.query(id=config.backend.instance_id)
|
||||
if self.backend_instance is None:
|
||||
raise ValueError(f'MuMu12 instance not found: {config.backend.instance_id}')
|
||||
if not self.backend_instance.running():
|
||||
logger.info('Starting MuMu12 backend...')
|
||||
self.backend_instance.start()
|
||||
logger.info('Waiting for MuMu12 backend to be available...')
|
||||
self.backend_instance.wait_available()
|
||||
else:
|
||||
logger.info('MuMu12 backend "%s" already running.', self.backend_instance)
|
||||
elif config.backend.type == 'leidian':
|
||||
if config.backend.instance_id is None:
|
||||
raise ValueError('Leidian instance ID is not set.')
|
||||
self.backend_instance = LeidianHost.query(id=config.backend.instance_id)
|
||||
if self.backend_instance is None:
|
||||
raise ValueError(f'Leidian instance not found: {config.backend.instance_id}')
|
||||
if not self.backend_instance.running():
|
||||
logger.info('Starting Leidian backend...')
|
||||
self.backend_instance.start()
|
||||
logger.info('Waiting for Leidian backend to be available...')
|
||||
self.backend_instance.wait_available()
|
||||
else:
|
||||
logger.info('Leidian backend "%s" already running.', self.backend_instance)
|
||||
else:
|
||||
raise ValueError(f'Unsupported backend type: {config.backend.type}')
|
||||
assert self.backend_instance is not None, 'Backend instance is not set.'
|
||||
return self.backend_instance.create_device(config.backend.screenshot_impl)
|
||||
def _on_create_device(self) -> Device:
|
||||
"""
|
||||
抽象方法,用于创建 Device 类,在 `run()` 方法执行前会被调用。
|
||||
|
||||
所有子类都需要重写该方法。
|
||||
"""
|
||||
raise NotImplementedError('Implement `_create_device` before using Kotonebot.')
|
||||
|
||||
def _on_after_init_context(self):
|
||||
"""
|
||||
抽象方法,在 init_context() 被调用后立即执行。
|
||||
"""
|
||||
pass
|
||||
|
||||
def run(self, tasks: list[Task], *, by_priority: bool = True):
|
||||
"""
|
||||
按优先级顺序运行所有任务。
|
||||
"""
|
||||
d = self.check_backend()
|
||||
d = self._on_create_device()
|
||||
init_context(config_path=self.config_path, config_type=self.config_type, target_device=d)
|
||||
self._on_after_init_context()
|
||||
vars.interrupted.clear()
|
||||
|
||||
if by_priority:
|
||||
|
|
|
@ -174,11 +174,30 @@ def is_manual_screenshot_mode() -> bool:
|
|||
|
||||
class ContextGlobalVars:
|
||||
def __init__(self):
|
||||
self.auto_collect: bool = False
|
||||
"""遇到未知P饮料/卡片时,是否自动截图并收集"""
|
||||
self.__vars = dict[str, Any]()
|
||||
self.interrupted: Event = Event()
|
||||
"""用户请求中断事件"""
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.__vars[key]
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
self.__vars[key] = value
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self.__vars[key]
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self.__vars
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
return self.__vars.get(key, default)
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
self.__vars[key] = value
|
||||
|
||||
def clear(self):
|
||||
self.__vars.clear()
|
||||
|
||||
class ContextStackVars:
|
||||
stack: list['ContextStackVars'] = []
|
||||
|
|
|
@ -81,4 +81,6 @@ def create_device(
|
|||
remote_impl = RemoteWindowsImpl(device, host, port)
|
||||
device._touch = remote_impl
|
||||
device._screenshot = remote_impl
|
||||
else:
|
||||
raise ValueError(f"Unsupported device implementation: {impl}")
|
||||
return device
|
||||
|
|
|
@ -81,7 +81,7 @@ class UserConfig(ConfigBaseModel, Generic[T]):
|
|||
|
||||
|
||||
class RootConfig(ConfigBaseModel, Generic[T]):
|
||||
version: int = 4
|
||||
version: int = 5
|
||||
"""配置版本。"""
|
||||
user_configs: list[UserConfig[T]] = []
|
||||
"""用户配置。"""
|
||||
|
|
|
@ -523,6 +523,11 @@ def upgrade_config() -> str | None:
|
|||
user_config, msg = upgrade_v3_to_v4(user_config['options'])
|
||||
messages.append(msg)
|
||||
version = 4
|
||||
case 4:
|
||||
logger.info('Upgrading config: v4 -> v5')
|
||||
user_config, msg = upgrade_v4_to_v5(user_config, user_config['options'])
|
||||
messages.append(msg)
|
||||
version = 5
|
||||
case _:
|
||||
logger.info('No config upgrade needed.')
|
||||
return version
|
||||
|
@ -929,5 +934,16 @@ def upgrade_v3_to_v4(options: dict[str, Any]) -> tuple[dict[str, Any], str]:
|
|||
logger.info('Corrected game package name to com.bandainamcoent.idolmaster_gakuen')
|
||||
return options, ''
|
||||
|
||||
def upgrade_v4_to_v5(user_config: dict[str, Any], options: dict[str, Any]) -> tuple[dict[str, Any], str]:
|
||||
"""
|
||||
v4 -> v5 变更:
|
||||
为 windows 和 windows_remote 截图方式的 type 设置为 dmm
|
||||
"""
|
||||
shutil.copy('config.json', 'config.v4.json')
|
||||
if user_config['backend']['screenshot_impl'] in ['windows', 'remote_windows']:
|
||||
logger.info('Set backend type to dmm.')
|
||||
user_config['backend']['type'] = 'dmm'
|
||||
return options, ''
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(PurchaseConfig.model_fields['money_refresh_on'].description)
|
|
@ -0,0 +1,8 @@
|
|||
from kotonebot.backend.context import vars
|
||||
from kotonebot.client.host import Instance
|
||||
|
||||
def _set_instance(new_instance: Instance) -> None:
|
||||
vars.set('instance', new_instance)
|
||||
|
||||
def instance() -> Instance:
|
||||
return vars.get('instance')
|
|
@ -0,0 +1,53 @@
|
|||
from typing_extensions import override
|
||||
|
||||
from kotonebot.client import Device, create_device
|
||||
from kotonebot.client import DeviceImpl, Device
|
||||
from kotonebot.client.host import HostProtocol, Instance
|
||||
|
||||
# TODO: 可能应该把 start_game 和 end_game 里对启停的操作移动到这里来
|
||||
class DmmInstance(Instance):
|
||||
def __init__(self):
|
||||
super().__init__('dmm', 'gakumas')
|
||||
|
||||
@override
|
||||
def refresh(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def start(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def stop(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def running(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def wait_available(self, timeout: float = 180):
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def create_device(self, impl: DeviceImpl, *, timeout: float = 180) -> Device:
|
||||
if impl not in ['windows', 'remote_windows']:
|
||||
raise ValueError(f'Unsupported device implementation: {impl}')
|
||||
return create_device('', impl, timeout=timeout)
|
||||
|
||||
class DmmHost(HostProtocol):
|
||||
instance = DmmInstance()
|
||||
"""DmmInstance 单例。"""
|
||||
|
||||
@staticmethod
|
||||
def installed() -> bool:
|
||||
# TODO: 应该检查 DMM 和 gamkumas 的安装情况
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def list() -> list[Instance]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def query(*, id: str) -> Instance | None:
|
||||
raise NotImplementedError()
|
|
@ -437,7 +437,24 @@ class KotoneBotUI:
|
|||
has_mumu12 = Mumu12Host.installed()
|
||||
has_leidian = LeidianHost.installed()
|
||||
current_tab = 0
|
||||
with gr.Tabs(selected=self.current_config.backend.type):
|
||||
|
||||
def _update_emulator_tab_options(impl_value: str, selected_index: int):
|
||||
nonlocal current_tab
|
||||
current_tab = selected_index
|
||||
|
||||
if selected_index == 3: # DMM
|
||||
choices = ['windows', 'remote_windows']
|
||||
else: # Mumu, Leidian, Custom
|
||||
choices = ['adb', 'adb_raw', 'uiautomator2']
|
||||
|
||||
if not impl_value in choices:
|
||||
new_value = choices[0]
|
||||
else:
|
||||
new_value = impl_value
|
||||
|
||||
return gr.Dropdown(choices=choices, value=new_value)
|
||||
|
||||
with gr.Tabs(selected=self.current_config.backend.type) as emulator_tabs:
|
||||
with gr.Tab("MuMu 12", interactive=has_mumu12, id="mumu12") as tab_mumu12:
|
||||
gr.Markdown("已选中 MuMu 12 模拟器")
|
||||
if has_mumu12:
|
||||
|
@ -512,59 +529,74 @@ class KotoneBotUI:
|
|||
inputs=[check_emulator],
|
||||
outputs=[check_emulator_group]
|
||||
)
|
||||
|
||||
screenshot_impl = gr.Dropdown(
|
||||
choices=['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows'],
|
||||
value=self.current_config.backend.screenshot_impl,
|
||||
label="截图方法",
|
||||
info=BackendConfig.model_fields['screenshot_impl'].description,
|
||||
interactive=True
|
||||
)
|
||||
keep_screenshots = gr.Checkbox(
|
||||
label="保留截图数据",
|
||||
value=self.current_config.keep_screenshots,
|
||||
info=UserConfig.model_fields['keep_screenshots'].description,
|
||||
interactive=True
|
||||
)
|
||||
|
||||
def set_current_tab(value: int) -> None:
|
||||
nonlocal current_tab
|
||||
current_tab = value
|
||||
tab_mumu12.select(fn=partial(set_current_tab, 0))
|
||||
tab_leidian.select(fn=partial(set_current_tab, 1))
|
||||
tab_custom.select(fn=partial(set_current_tab, 2))
|
||||
|
||||
def set_config(_: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||||
if current_tab == 0:
|
||||
self.current_config.backend.type = 'mumu12'
|
||||
self.current_config.backend.instance_id = data['_mumu_index']
|
||||
elif current_tab == 1:
|
||||
self.current_config.backend.type = 'leidian'
|
||||
self.current_config.backend.instance_id = data['_leidian_index']
|
||||
else:
|
||||
self.current_config.backend.type = 'custom'
|
||||
self.current_config.backend.instance_id = None
|
||||
self.current_config.backend.adb_ip = data['adb_ip']
|
||||
self.current_config.backend.adb_port = data['adb_port']
|
||||
self.current_config.backend.adb_emulator_name = data['adb_emulator_name']
|
||||
self.current_config.backend.screenshot_impl = data['screenshot_method']
|
||||
self.current_config.keep_screenshots = data['keep_screenshots']
|
||||
self.current_config.backend.check_emulator = data['check_emulator']
|
||||
self.current_config.backend.emulator_path = data['emulator_path']
|
||||
self.current_config.backend.emulator_args = data['emulator_args']
|
||||
|
||||
return set_config, {
|
||||
'adb_ip': adb_ip,
|
||||
'adb_port': adb_port,
|
||||
'screenshot_method': screenshot_impl,
|
||||
'keep_screenshots': keep_screenshots,
|
||||
'check_emulator': check_emulator,
|
||||
'emulator_path': emulator_path,
|
||||
'adb_emulator_name': adb_emulator_name,
|
||||
'emulator_args': emulator_args,
|
||||
'_mumu_index': mumu_instance,
|
||||
'_leidian_index': leidian_instance
|
||||
}
|
||||
with gr.Tab("DMM", id="dmm") as tab_dmm:
|
||||
gr.Markdown("已选中 DMM")
|
||||
|
||||
type_in_config = self.current_config.backend.type
|
||||
if type_in_config in ['dmm']:
|
||||
choices = ['windows', 'remote_windows']
|
||||
elif type_in_config in ['mumu12', 'leidian', 'custom']:
|
||||
choices = ['adb', 'adb_raw', 'uiautomator2']
|
||||
else:
|
||||
raise ValueError(f'Unsupported backend type: {type_in_config}')
|
||||
screenshot_impl = gr.Dropdown(
|
||||
choices=choices,
|
||||
value=self.current_config.backend.screenshot_impl,
|
||||
label="截图方法",
|
||||
info=BackendConfig.model_fields['screenshot_impl'].description,
|
||||
interactive=True
|
||||
)
|
||||
|
||||
keep_screenshots = gr.Checkbox(
|
||||
label="保留截图数据",
|
||||
value=self.current_config.keep_screenshots,
|
||||
info=UserConfig.model_fields['keep_screenshots'].description,
|
||||
interactive=True
|
||||
)
|
||||
|
||||
tab_mumu12.select(fn=partial(_update_emulator_tab_options, selected_index=0), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||||
tab_leidian.select(fn=partial(_update_emulator_tab_options, selected_index=1), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||||
tab_custom.select(fn=partial(_update_emulator_tab_options, selected_index=2), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||||
tab_dmm.select(fn=partial(_update_emulator_tab_options, selected_index=3), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||||
|
||||
def set_config(_: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||||
# current_tab is updated by _update_emulator_tab_options
|
||||
if current_tab == 0: # Mumu
|
||||
self.current_config.backend.type = 'mumu12'
|
||||
self.current_config.backend.instance_id = data['_mumu_index']
|
||||
elif current_tab == 1: # Leidian
|
||||
self.current_config.backend.type = 'leidian'
|
||||
self.current_config.backend.instance_id = data['_leidian_index']
|
||||
elif current_tab == 2: # Custom
|
||||
self.current_config.backend.type = 'custom'
|
||||
self.current_config.backend.instance_id = None
|
||||
self.current_config.backend.adb_ip = data['adb_ip']
|
||||
self.current_config.backend.adb_port = data['adb_port']
|
||||
self.current_config.backend.adb_emulator_name = data['adb_emulator_name']
|
||||
self.current_config.backend.check_emulator = data['check_emulator']
|
||||
self.current_config.backend.emulator_path = data['emulator_path']
|
||||
self.current_config.backend.emulator_args = data['emulator_args']
|
||||
elif current_tab == 3: # DMM
|
||||
self.current_config.backend.type = 'dmm'
|
||||
self.current_config.backend.instance_id = None # DMM doesn't use instance_id here
|
||||
|
||||
# Common settings for all backend types
|
||||
self.current_config.backend.screenshot_impl = data['screenshot_method']
|
||||
self.current_config.keep_screenshots = data['keep_screenshots'] # This is a UserConfig field
|
||||
|
||||
return set_config, {
|
||||
'adb_ip': adb_ip,
|
||||
'adb_port': adb_port,
|
||||
'screenshot_method': screenshot_impl, # screenshot_impl is the component
|
||||
'keep_screenshots': keep_screenshots,
|
||||
'check_emulator': check_emulator,
|
||||
'emulator_path': emulator_path,
|
||||
'adb_emulator_name': adb_emulator_name,
|
||||
'emulator_args': emulator_args,
|
||||
'_mumu_index': mumu_instance,
|
||||
'_leidian_index': leidian_instance
|
||||
}
|
||||
|
||||
def _create_purchase_settings(self) -> ConfigBuilderReturnValue:
|
||||
with gr.Column():
|
||||
|
|
|
@ -7,9 +7,15 @@ import zipfile
|
|||
from datetime import datetime
|
||||
|
||||
import cv2
|
||||
from typing_extensions import override
|
||||
|
||||
from .dmm_host import DmmHost
|
||||
from ...client import Device
|
||||
from kotonebot.ui import user
|
||||
from kotonebot import KotoneBot
|
||||
from ..kaa_context import _set_instance
|
||||
from ..common import BaseConfig, upgrade_config
|
||||
from kotonebot.client.host import Mumu12Host, LeidianHost
|
||||
|
||||
# 初始化日志
|
||||
log_formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(name)s] %(message)s')
|
||||
|
@ -90,4 +96,74 @@ class Kaa(KotoneBot):
|
|||
return path
|
||||
except Exception as e:
|
||||
logger.exception(f'Failed to save error report:')
|
||||
return ''
|
||||
return ''
|
||||
|
||||
@override
|
||||
def _on_after_init_context(self):
|
||||
if self.backend_instance is None:
|
||||
raise ValueError('Backend instance is not set.')
|
||||
_set_instance(self.backend_instance)
|
||||
|
||||
@override
|
||||
def _on_create_device(self) -> Device:
|
||||
from kotonebot.client.host import create_custom
|
||||
from kotonebot.config.manager import load_config
|
||||
# HACK: 硬编码
|
||||
config = load_config(self.config_path, type=self.config_type)
|
||||
config = config.user_configs[0]
|
||||
logger.info('Checking backend...')
|
||||
if config.backend.type == 'custom':
|
||||
exe = config.backend.emulator_path
|
||||
if exe is None:
|
||||
user.error('「检查并启动模拟器」已开启但未配置「模拟器 exe 文件路径」。')
|
||||
raise ValueError('Emulator executable path is not set.')
|
||||
if not os.path.exists(exe):
|
||||
user.error('「模拟器 exe 文件路径」对应的文件不存在!请检查路径是否正确。')
|
||||
raise FileNotFoundError(f'Emulator executable not found: {exe}')
|
||||
self.backend_instance = create_custom(
|
||||
adb_ip=config.backend.adb_ip,
|
||||
adb_port=config.backend.adb_port,
|
||||
adb_name=config.backend.adb_emulator_name,
|
||||
exe_path=exe,
|
||||
emulator_args=config.backend.emulator_args
|
||||
)
|
||||
if config.backend.check_emulator:
|
||||
if not self.backend_instance.running():
|
||||
logger.info('Starting custom backend...')
|
||||
self.backend_instance.start()
|
||||
logger.info('Waiting for custom backend to be available...')
|
||||
self.backend_instance.wait_available()
|
||||
else:
|
||||
logger.info('Custom backend "%s" already running.', self.backend_instance)
|
||||
elif config.backend.type == 'mumu12':
|
||||
if config.backend.instance_id is None:
|
||||
raise ValueError('MuMu12 instance ID is not set.')
|
||||
self.backend_instance = Mumu12Host.query(id=config.backend.instance_id)
|
||||
if self.backend_instance is None:
|
||||
raise ValueError(f'MuMu12 instance not found: {config.backend.instance_id}')
|
||||
if not self.backend_instance.running():
|
||||
logger.info('Starting MuMu12 backend...')
|
||||
self.backend_instance.start()
|
||||
logger.info('Waiting for MuMu12 backend to be available...')
|
||||
self.backend_instance.wait_available()
|
||||
else:
|
||||
logger.info('MuMu12 backend "%s" already running.', self.backend_instance)
|
||||
elif config.backend.type == 'leidian':
|
||||
if config.backend.instance_id is None:
|
||||
raise ValueError('Leidian instance ID is not set.')
|
||||
self.backend_instance = LeidianHost.query(id=config.backend.instance_id)
|
||||
if self.backend_instance is None:
|
||||
raise ValueError(f'Leidian instance not found: {config.backend.instance_id}')
|
||||
if not self.backend_instance.running():
|
||||
logger.info('Starting Leidian backend...')
|
||||
self.backend_instance.start()
|
||||
logger.info('Waiting for Leidian backend to be available...')
|
||||
self.backend_instance.wait_available()
|
||||
else:
|
||||
logger.info('Leidian backend "%s" already running.', self.backend_instance)
|
||||
elif config.backend.type == 'dmm':
|
||||
self.backend_instance = DmmHost.instance
|
||||
else:
|
||||
raise ValueError(f'Unsupported backend type: {config.backend.type}')
|
||||
assert self.backend_instance is not None, 'Backend instance is not set.'
|
||||
return self.backend_instance.create_device(config.backend.screenshot_impl)
|
|
@ -6,6 +6,7 @@ import _thread
|
|||
import threading
|
||||
|
||||
from kotonebot.ui import user
|
||||
from ..kaa_context import instance
|
||||
from kotonebot.kaa.common import Priority, conf
|
||||
from kotonebot import task, action, config, device
|
||||
|
||||
|
@ -56,15 +57,7 @@ def end_game():
|
|||
|
||||
# 关闭模拟器
|
||||
if conf().end_game.kill_emulator:
|
||||
emulator_path = config.current.backend.emulator_path
|
||||
if emulator_path is None:
|
||||
logger.warning("Emulator path is not set. Skipping")
|
||||
user.info("「关闭模拟器」功能需要配置「模拟器 exe 文件路径」。")
|
||||
else:
|
||||
exe_name = os.path.basename(emulator_path)
|
||||
os.system(f"taskkill /f /im {exe_name}")
|
||||
logger.info("Emulator closed")
|
||||
# TODO: 实现关闭模拟器
|
||||
instance().stop()
|
||||
|
||||
# 关机
|
||||
if conf().end_game.shutdown:
|
||||
|
|
Loading…
Reference in New Issue