Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ oas-backend.bat
.eslintrc.js
.prettierrc.js



#CodeBuddy
*.code-workspace
.codebuddy



Expand Down
21 changes: 21 additions & 0 deletions module/device/platform2/emulator_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,27 @@ def iter_running_emulator():
if Emulator.is_emulator(exe):
yield exe

@staticmethod
def iter_running_mumu_device():
"""
Yields:
str: Path to MuMuNxDevice.exe executables, each corresponds to one emulator instance
"""
try:
import psutil
except ModuleNotFoundError:
return
for pid in psutil.pids():
proc = psutil._psplatform.Process(pid)
try:
exe = proc.cmdline()
exe = exe[0].replace(r'\\', '/').replace('\\', '/')
except (psutil.AccessDenied, psutil.NoSuchProcess, IndexError, OSError):
continue

if 'mumunxdevice.exe' in exe.lower():
yield exe

@cached_property
def all_emulators(self) -> t.List[Emulator]:
"""
Expand Down
36 changes: 36 additions & 0 deletions module/device/platform2/platform_windows.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ctypes
import re
import subprocess
import time
import psutil
from adbutils import AdbDevice, AdbClient

Expand Down Expand Up @@ -415,8 +416,43 @@ def show_package(m):
return True


def _check_emulator_availability_before_start(self) -> bool:
"""
仅在真正拉起模拟器前检查一次可用性
"""
config = self.config.script.optimization
if config.max_running_emulators >= 99:
return True
try:
running_emulators = list(EmulatorManager.iter_running_mumu_device())
running_count = len(running_emulators)
logger.info(f'检测到运行中的模拟器进程: {running_count}')
for exe in running_emulators:
logger.info(f' - {exe}')
if running_count >= config.max_running_emulators:
logger.info(f'运行中的模拟器数量 ({running_count}) 已达到最大限制 ({config.max_running_emulators})')
return False
except Exception as e:
logger.warning(f'检查运行中的模拟器失败: {e}')
return True

def _wait_for_emulator_availability_before_start(self) -> None:
config = self.config.script.optimization
if config.max_running_emulators >= 99:
return
check_interval = 60
start_time = time.time()
logger.info(f'启动前检查模拟器可用性,最大允许运行的模拟器数量: {config.max_running_emulators}')
while not self._check_emulator_availability_before_start():
elapsed_minutes = (time.time() - start_time) / 60
logger.info(f'模拟器不可用,等待 {elapsed_minutes:.1f} 分钟...')
time.sleep(check_interval)
logger.info('模拟器现在可用,继续启动')

def emulator_start(self):
logger.hr('Emulator start', level=1)
# 仅在这里做一次并发启动检查,确保是“拉起前检查”
self._wait_for_emulator_availability_before_start()
for i in range(3):
# Stop
if not self._emulator_function_wrapper(self._emulator_stop):
Expand Down
58 changes: 57 additions & 1 deletion script.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
from module.device.device import Device
from module.device.env import IS_WINDOWS
from module.base.utils import load_module
from module.base.decorator import del_cached_property
from module.base.decorator import del_cached_property, has_cached_property
from module.base.timer import Timer
from module.logger import logger
from module.exception import *
from module.server.i18n import I18n
Expand Down Expand Up @@ -328,6 +329,8 @@ def _handle_wait_during_idle(self, next_run: datetime) -> bool:
strategy_map = {
"close_game": self._wait_close_game,
"goto_main": self._wait_goto_main,
"close_emulator": self._wait_close_emulator,
"auto": self._wait_auto,
}
func = strategy_map.get(method)
if not func:
Expand All @@ -350,11 +353,60 @@ def _wait_goto_main(self, next_run: datetime) -> bool:
self.device.release_during_wait()
return self.wait_until(next_run)

def _wait_close_emulator(self, next_run: datetime) -> bool:
logger.info("Close emulator during wait")
self.device.app_stop()
self.device.release_during_wait()
time.sleep(15)
self.device.emulator_stop()
if not self.wait_until(next_run):
return False
getattr(self.device, 'emulator_start')()
time.sleep(15)
self.run("Restart")
return True

def _wait_auto(self, next_run: datetime) -> bool:
wait_minutes = (next_run - datetime.now()).total_seconds() / 60
config = self.config.script.optimization

logger.info(f"Auto mode: wait {wait_minutes:.1f} minutes")

if wait_minutes > config.close_emulator_threshold:
return self._wait_close_emulator(next_run)
elif wait_minutes > config.close_game_threshold:
return self._wait_close_game(next_run)
else:
return self._wait_goto_main(next_run)

def _wait_stay_there(self, next_run: datetime) -> bool:
logger.info("Stay_there (no action) during wait")
self.device.release_during_wait()
return self.wait_until(next_run)

def _is_cached_device_online(self) -> bool:
"""
检查已缓存device是否仍在线。
仅使用缓存对象,不触发self.device重建。
"""
if not has_cached_property(self, 'device'):
return False

try:
cached_device = self.__dict__.get('device')
if cached_device is None:
return False

serial = str(getattr(cached_device, 'serial', ''))
if not serial:
return False

devices = cached_device.list_device().select(status='device')
return any(dev.serial == serial for dev in devices)
except Exception as e:
logger.warning(f'检查缓存device在线状态失败: {e}')
return False

def exception_handler(self, e: Exception, command: str) -> None:
# 处理御魂溢出
from tasks.Utils.post_diagnotor import PostDiagnotor, AnalyzeType
Expand Down Expand Up @@ -483,6 +535,10 @@ def loop(self):

# Get task
task = self.get_next_task()
# 若缓存device对应的模拟器已离线,先清缓存,再让device重建(重建时会走emulator_start前检查)
if has_cached_property(self, 'device') and not self._is_cached_device_online():
logger.warning('检测到缓存device离线,清理缓存并重建设备')
del_cached_property(self, 'device')
_ = self.device
# Skip first restart
if self.is_first_task and task == 'Restart':
Expand Down
Binary file modified tasks/GameUi/page/page_main_goto_daily.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tasks/KekkaiActivation/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class KekkaiActivationAssets:
# description
C_A_SELECT_AUTO = RuleClick(roi_front=(173,160,354,127), roi_back=(173,160,354,127), name="a_select_auto")

# 切换排序(升序/降序)
C_A_SORT_TOGGLE = RuleClick(roi_front=(220,105,110,35), roi_back=(220,105,110,35), name="a_sort_toggle")


# Ocr Rule Assets
# 这张卡一共有多少小时
Expand Down
7 changes: 7 additions & 0 deletions tasks/KekkaiActivation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class CardType(str, Enum):
TAIKO = '太鼓'


class CardPriority(str, Enum):
HIGH = 'high' # 优先挂高值卡
LOW = 'low' # 优先挂低值卡


class ActivationScheduler(Scheduler):
priority: int = Field(default=2, description='priority_help')
success_interval: TimeDelta = Field(default=TimeDelta(days=1), description='success_interval_help')
Expand All @@ -21,8 +26,10 @@ class ActivationScheduler(Scheduler):

class ActivationConfig(BaseModel):
card_type: CardType = Field(default=CardType.TAIKO, description='card_rule_help')
card_priority: CardPriority = Field(default=CardPriority.HIGH, description='挂卡优先级:high=优先高值卡,low=优先低值卡')
min_taiko_num: int = Field(default=8, description='挂卡太鼓每小时最少收益,低于则不挂卡')
min_fish_num: int = Field(default=16, description='挂卡斗鱼每小时最少收益,低于则不挂卡')
activation_weekdays: str = Field(default="", description='挂卡星期过滤:使用逗号或顿号分隔的星期列表,支持中文(周一、周二...)或数字(1-7,1=周一,7=周日)。例如:周一、周三、周五 或 1,3,5。留空则每天都可以挂卡。适合多个小号轮流挂卡给大号吃')
exchange_before: bool = Field(default=True, description='exchange_before_help')
exchange_max: bool = Field(default=True, description='exchange_max_help')
shikigami_class: ShikigamiClass = Field(default=ShikigamiClass.N, description='shikigami_class_help')
Expand Down
80 changes: 73 additions & 7 deletions tasks/KekkaiActivation/script_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from tasks.KekkaiUtilize.utils import CardClass
from tasks.KekkaiActivation.assets import KekkaiActivationAssets
from tasks.KekkaiActivation.utils import parse_rule
from tasks.KekkaiActivation.config import ActivationConfig
from tasks.KekkaiActivation.config import ActivationConfig, CardPriority
from tasks.Utils.config_enum import ShikigamiClass
from tasks.GameUi.page import page_main, page_guild
from tasks.KekkaiActivation.config import CardType
Expand All @@ -30,6 +30,19 @@ class ScriptTask(KU, KekkaiActivationAssets):

def run(self):
con = self.config.kekkai_activation.activation_config

# 星期过滤检查(适合多个小号轮流挂卡给大号吃)
if con.activation_weekdays:
weekday = datetime.now().weekday() # 0=周一, 6=周日
allowed_weekdays = self.parse_weekdays(con.activation_weekdays)
if weekday not in allowed_weekdays:
logger.info(f"今日星期{weekday + 1}不在挂卡星期列表{allowed_weekdays}中,跳过挂卡任务")
# 设置下次执行时间为明天0点
next_run = (datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+ timedelta(days=1))
self.set_next_run("KekkaiActivation", target=next_run)
raise TaskEnd('KekkaiActivation')

self.ui_get_current_page()
self.ui_goto(page_guild)

Expand Down Expand Up @@ -150,6 +163,41 @@ def run_activation(self, _config: ActivationConfig) -> bool:
logger.info('Card is not selected also not using')
self.screening_card(_config.card_type)

def parse_weekdays(self, weekdays_str: str) -> list[int]:
"""
解析星期配置,支持多种格式:
- 中文:周一、周二、周三、周四、周五、周六、周日
- 数字:1-7(1=周一,7=周日)
- 分隔符:支持英文逗号(,)、中文逗号(,)、顿号(、)
返回:datetime.weekday() 格式(0=周一,6=周日)
"""
weekday_map = {
# 中文 -> weekday (0=周一)
'周一': 0, '周二': 1, '周三': 2, '周四': 3, '周五': 4, '周六': 5, '周日': 6,
}

# 统一分隔符:将中文逗号和顿号都替换为英文逗号
normalized_str = weekdays_str.replace(',', ',').replace('、', ',')

allowed_weekdays = []
for item in normalized_str.split(','):
item = item.strip()
if not item:
continue

# 数字(1-7,1=周一)
if item.isdigit():
weekday = int(item)
if 1 <= weekday <= 7:
allowed_weekdays.append(weekday - 1) # 转换为 weekday 格式
# 中文
elif item in weekday_map:
allowed_weekdays.append(weekday_map[item])
else:
logger.warning(f"无法识别的星期配置:{item}")

return allowed_weekdays

def goto_cards(self):
"""
寮结界,前往挂卡界面
Expand Down Expand Up @@ -247,6 +295,14 @@ def screening_card(self, rule: str):
continue
logger.info('Selected card class: {}'.format(card_class))

# 根据配置切换排序(游戏默认降序,优先低值卡需要点击一次切换为升序)
priority = self.config.kekkai_activation.activation_config.card_priority
if priority == CardPriority.LOW:
logger.info('优先挂低值卡,点击切换为升序')
self.click(self.C_A_SORT_TOGGLE, interval=0.5)
else:
logger.info('优先挂高值卡,保持默认降序')

# 找最优卡
while 1:
self.screenshot()
Expand Down Expand Up @@ -298,19 +354,29 @@ def check_card_num(self):
numeric_results.append((numbers[0], result)) # 按第一个数字排序

if numeric_results:
# 按数字大到小排序
sorted_results = [result for _, result in sorted(numeric_results, key=lambda x: x[0], reverse=True)]
max_result = sorted_results[0] # 获取数字最大的结果对象

box = max_result.box # 获取边界框坐标
# 根据配置决定排序方向
priority = self.config.kekkai_activation.activation_config.card_priority
if priority == CardPriority.HIGH:
# 按数字大到小排序(优先高值卡)
sorted_results = [result for _, result in sorted(numeric_results, key=lambda x: x[0], reverse=True)]
logger.info("优先选择高值卡")
else:
# 按数字从小到大排序(优先低值卡)
sorted_results = [result for _, result in sorted(numeric_results, key=lambda x: x[0], reverse=False)]
logger.info("优先选择低值卡")

selected_result = sorted_results[0] # 获取排第一的结果对象

box = selected_result.box # 获取边界框坐标
x_min = self.O_CHECK_CARD_NUMBER.roi[0] + box[0][0]
y_min = self.O_CHECK_CARD_NUMBER.roi[1] + box[0][1]
width = box[1][0] - box[0][0]
height = box[2][1] - box[1][1]
roi = int(x_min), int(y_min), int(width), int(height)

target = RuleClick(roi_front=roi, roi_back=roi, name="tmpclick")
logger.info(f"选择挂卡: [{max_result.ocr_text}] {roi}")
card_value = numeric_results[0][0]
logger.info(f"选择挂卡: [{selected_result.ocr_text}] 数值:{card_value} {roi}")

return target
else:
Expand Down
2 changes: 1 addition & 1 deletion tasks/KekkaiUtilize/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class KekkaiUtilizeAssets:
# 这个寄养的剩余时间
O_UTILIZE_RES_TIME = RuleOcr(roi=(1140,117,100,30), area=(1140,117,100,30), mode="Duration", method="Default", keyword="", name="utilize_res_time")
# 今日已领取经验
O_BOX_EXP = RuleOcr(roi=(654,538,179,39), area=(654,538,179,39), mode="DigitCounter", method="Default", keyword="", name="box_exp")
O_BOX_EXP = RuleOcr(roi=(430,540,420,40), area=(430,540,420,40), mode="DigitCounter", method="Default", keyword="", name="box_exp")
# 斗鱼或太古寄养数目
O_CARD_NUM = RuleOcr(roi=(800,421,150,33), area=(800,421,150,33), mode="Single", method="Default", keyword="", name="card_num")

Expand Down
6 changes: 6 additions & 0 deletions tasks/KekkaiUtilize/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ class UtilizeRule(str, Enum):
FISH = 'fish' # 斗鱼优先
# AUTO = 'auto' # 自动 兼容代码罢了

class ValueCalculationRule(str, Enum):
DEFAULT = 'default' # 按档位选
TAIKO_PRIORITY = 'taiko_priority' # 启用价值计算,太鼓系数2.2
FISH_PRIORITY = 'fish_priority' # 启用价值计算,斗鱼系数1.8



class UtilizeScheduler(Scheduler):
Expand All @@ -28,6 +33,7 @@ class UtilizeScheduler(Scheduler):

class UtilizeConfig(BaseModel):
utilize_rule: UtilizeRule = Field(default=UtilizeRule.DEFAULT, description='utilize_rule_help')
value_calculation_rule: ValueCalculationRule = Field(default=ValueCalculationRule.DEFAULT, description='价值计算规则,默认按档位选;taiko_priority:太鼓优先(太鼓系数2.2),fish_priority:斗鱼优先(太鼓系数1.8)')
select_friend_list: SelectFriendList = Field(default=SelectFriendList.SAME_SERVER, description='select_friend_list_help')
shikigami_class: ShikigamiClass = Field(default=ShikigamiClass.N, description='shikigami_class_help')
shikigami_order: int = Field(default=4, description='shikigami_order_help')
Expand Down
Loading