641 lines
29 KiB
Python

import copy
import importlib
import os
import random
from datetime import datetime
from module.campaign.campaign_base import CampaignBase
from module.campaign.campaign_event import CampaignEvent
from module.shop.shop_status import ShopStatus
from module.campaign.campaign_ui import MODE_SWITCH_1
from module.config.config import AzurLaneConfig
from module.exception import CampaignEnd, RequestHumanTakeover, ScriptEnd
from module.handler.fast_forward import map_files, to_map_file_name
from module.logger import logger
from module.notify import handle_notify
from module.ui.page import page_campaign
from module.exception import GameStuckError,GamePageUnknownError
from module.handler.assets import LOW_EMOTION_LEFT
from module.base.button import Button
from module.ocr.ocr import Ocr
class CampaignRun(CampaignEvent, ShopStatus):
folder: str
name: str
stage: str
module = None
config: AzurLaneConfig
campaign: CampaignBase
run_count: int
run_limit: int
is_stage_loop = False
def load_campaign(self, name, folder='campaign_main'):
"""
Args:
name (str): Name of .py file under module.campaign.
folder (str): Name of the file folder under campaign.
Returns:
bool: If load.
"""
if hasattr(self, 'name') and name == self.name:
return False
self.name = name
self.folder = folder
if folder.startswith('campaign_'):
self.stage = '-'.join(name.split('_')[1:3])
if folder.startswith('event') or folder.startswith('war_archives'):
self.stage = name
try:
self.module = importlib.import_module('.' + name, f'campaign.{folder}')
except ModuleNotFoundError:
logger.warning(f'Map file not found: campaign.{folder}.{name}')
if not os.path.exists(f'./campaign/{folder}'):
logger.warning(f'Folder not exists: ./campaign/{folder}')
else:
files = map_files(folder)
logger.warning(f'Existing files: {files}')
logger.critical(f'Possible reason #1: This event ({folder}) does not have {name}')
logger.critical(f'Possible reason #2: You are using an old Alas, '
'please check for update, or make map files yourself using dev_tools/map_extractor.py')
raise RequestHumanTakeover
config = copy.deepcopy(self.config).merge(self.module.Config())
device = self.device
self.campaign = self.module.Campaign(config=config, device=device)
return True
def triggered_stop_condition(self, oil_check=True):
"""
Returns:
bool: If triggered a stop condition.
"""
# Run count limit
if self.run_limit and self.config.StopCondition_RunCount <= 0:
logger.hr('Triggered stop condition: Run count')
self.config.StopCondition_RunCount = 0
self.config.Scheduler_Enable = False
handle_notify(
self.config.Error_OnePushConfig,
title=f"Alas <{self.config.config_name}> campaign finished",
content=f"<{self.config.config_name}> {self.name} reached run count limit"
)
return True
# Lv120 limit
if self.config.StopCondition_ReachLevel and self.campaign.config.LV_TRIGGERED:
logger.hr(f'Triggered stop condition: Reach level {self.config.StopCondition_ReachLevel}')
self.config.Scheduler_Enable = False
handle_notify(
self.config.Error_OnePushConfig,
title=f"Alas <{self.config.config_name}> campaign finished",
content=f"<{self.config.config_name}> {self.name} reached level limit"
)
return True
# Oil limit
if oil_check:
self.status_get_gems()
self.get_coin()
_oil = self.get_oil()
if _oil < max(500, self.config.StopCondition_OilLimit):
logger.hr('Triggered stop condition: Oil limit')
self.config.task_delay(minute=(120, 240))
return True
# Auto search oil limit
if self.campaign.auto_search_oil_limit_triggered:
logger.hr('Triggered stop condition: Auto search oil limit')
self.config.task_delay(minute=(120, 240))
return True
# If Get a New Ship
if self.config.StopCondition_GetNewShip and self.campaign.config.GET_SHIP_TRIGGERED:
logger.hr('Triggered stop condition: Get new ship')
self.config.Scheduler_Enable = False
handle_notify(
self.config.Error_OnePushConfig,
title=f"Alas <{self.config.config_name}> campaign finished",
content=f"<{self.config.config_name}> {self.name} got new ship"
)
return True
# Event limit
if oil_check and self.campaign.event_pt_limit_triggered():
logger.hr('Triggered stop condition: Event PT limit')
return True
# Auto search TaskBalancer
if self.config.TaskBalancer_Enable and self.campaign.auto_search_coin_limit_triggered:
logger.hr('Triggered stop condition: Auto search coin limit')
self.handle_task_balancer()
return True
# TaskBalancer
if oil_check and self.run_count >= 1:
if self.config.TaskBalancer_Enable and self.triggered_task_balancer():
logger.hr('Triggered stop condition: Coin limit')
self.handle_task_balancer()
return True
return False
def _triggered_app_restart(self):
"""
Returns:
bool: If triggered a restart condition.
"""
if not self.campaign.emotion.is_ignore:
if self.campaign.emotion.triggered_bug():
logger.info('Triggered restart avoid emotion bug')
return True
return False
def handle_app_restart(self):
if self._triggered_app_restart():
self.config.task_call('Restart')
return True
return False
def handle_stage_name(self, name, folder, mode='normal'):
"""
Handle wrong stage names.
In some events, the name of SP may be different, such as 'vsp', muse sp.
To call them easier, their map files should named 'sp.py'.
Args:
name (str): Name of .py file.
folder (str): Name of the file folder under campaign.
Returns:
str, str: name, folder
"""
name = to_map_file_name(name)
# For GemsFarming, auto choose events or main chapters
if self.config.task.command == 'GemsFarming':
if self.stage_is_main(name):
logger.info(f'Stage name {name} is from campaign_main')
folder = 'campaign_main'
else:
folder = self.config.cross_get('Event.Campaign.Event')
if folder is not None:
logger.info(f'Stage name {name} is from event {folder}')
else:
logger.warning(f'Cannot get the latest event, fallback to campaign_main')
folder = 'campaign_main'
# Handle special names SP maps
if folder == 'event_20201126_cn' and name == 'vsp':
name = 'sp'
if folder == 'event_20210723_cn' and name == 'vsp':
name = 'sp'
if folder == 'event_20220324_cn' and name == 'esp':
name = 'sp'
if folder == 'event_20220818_cn' and name == 'esp':
name = 'sp'
if folder == 'event_20221124_cn' and name in ['asp', 'a.sp']:
name = 'sp'
if folder == 'event_20240425_cn':
if name in ['μsp', 'usp', 'iisp']:
name = 'sp'
name = name.replace('lsp', 'isp').replace('1sp', 'isp')
if name == 'isp':
name = 'isp1'
if folder == 'event_20240724_cn':
if name in ['ysp', 'y.sp']:
name = 'sp'
# Convert to chapter T
convert = {
'a1': 't1',
'a2': 't2',
'a3': 't3',
'a4': 't4',
'a5': 't5',
'a6': 't6',
'sp1': 't1',
'sp2': 't2',
'sp3': 't3',
'sp4': 't4',
'sp5': 't5',
'sp6': 't6',
}
if folder in [
'event_20211125_cn',
'event_20231026_cn',
'event_20241024_cn',
'event_20250424_cn',
'event_20250724_cn',
'event_20250814_cn',
'event_20251023_cn',
'event_20260326_cn',
'war_archives_20231026_cn',
]:
name = convert.get(name, name)
# Convert between A/B/C/D and T/HT
convert = {
'a1': 't1',
'a2': 't2',
'a3': 't3',
'b1': 't4',
'b2': 't5',
'b3': 't6',
'c1': 'ht1',
'c2': 'ht2',
'c3': 'ht3',
'd1': 'ht4',
'd2': 'ht5',
'd3': 'ht6',
}
if folder in [
'event_20200917_cn',
'event_20221124_cn',
'event_20230525_cn',
'war_archives_20200917_cn',
# chapter T
'event_20211125_cn',
'event_20231026_cn',
'event_20231123_cn',
'event_20240725_cn',
'event_20240829_cn',
'event_20241024_cn',
'event_20241121_cn',
'event_20250424_cn',
'event_20250724_cn',
'event_20250814_cn',
'event_20251023_cn',
'event_20260326_cn',
'war_archives_20231026_cn',
]:
name = convert.get(name, name)
else:
reverse = {v: k for k, v in convert.items()}
name = reverse.get(name, name)
# The Alchemist and the Archipelago of Secrets
# Handle typo
if folder == 'event_20221124_cn':
name = name.replace('ht', 'th')
# Chapter TH has no map_percentage and no 3_stars
if folder == 'event_20221124_cn' and name.startswith('th'):
if self.config.StopCondition_MapAchievement != 'non_stop':
logger.info(f'When running chapter TH of event_20221124_cn, '
f'StopCondition.MapAchievement is forced set to threat_safe')
self.config.override(StopCondition_MapAchievement='threat_safe')
if folder == 'event_20250724_cn' and name.startswith('ts'):
if self.config.StopCondition_MapAchievement != 'non_stop':
logger.info(f'When running chapter TS of event_20250724_cn, '
f'StopCondition.MapAchievement is forced set to threat_safe')
self.config.override(StopCondition_MapAchievement='threat_safe')
# event_20211125_cn, TSS maps are on time maps
if folder == 'event_20211125_cn' and 'tss' in name:
self.config.override(
StopCondition_OilLimit=0, # No oil cost
StopCondition_MapAchievement='100_percent_clear',
StopCondition_StageIncrease=True,
Emotion_Mode='ignore', # No emotion cost
Fleet_Fleet2=0, # Has only one fleet
Submarine_Fleet=0, # No submarine
)
# event_20230817_cn story states
if folder == 'event_20230817_cn':
if name.startswith('e0'):
name = 'a1'
# event_20240829_cn, TP -> SP
if folder == 'event_20240829_cn':
if name == 'tp':
name = 'sp'
# Stage loop
for alias, stages in self.config.STAGE_LOOP_ALIAS.items():
alias_folder, alias = alias
if folder == alias_folder and name == alias.lower():
stages = [i.strip(' \t\r\n') for i in stages.split('>')]
cycle = len(stages)
count = int(self.config.StopCondition_RunCount)
if count == 0:
stage = random.choice(stages)
logger.info(f'Loop stages in {name.upper()}, run random stage: {stage}')
else:
index = count % cycle
index = 0 if index == 0 else cycle - index
stage = stages[index]
logger.info(f'Loop stages in {name.upper()} with remain run_count={count}, '
f'run ordered stage: {stage}')
name = stage.lower()
self.is_stage_loop = True
# disable continuous clear
logger.info('disable continuous clear')
self.config.override(StopCondition_MapAchievement='non_stop')
self.config.override(StopCondition_StageIncrease=False)
# Convert campaign_main to campaign hard if mode is hard and file exists
if mode == 'hard' and folder == 'campaign_main' and name in map_files('campaign_hard'):
folder = 'campaign_hard'
# event_20240912_cn does not have "Threat: Safe" indicator, fallback MapAchievement
if folder == 'event_20240912_cn':
if self.config.StopCondition_MapAchievement == 'threat_safe':
logger.info(
'In event_20240912_cn, MapAchievement=threat_safe fallback to map_3_stars')
self.config.override(StopCondition_MapAchievement='map_3_stars')
if self.config.StopCondition_MapAchievement == 'threat_safe_without_3_stars':
logger.info(
'In event_20240912_cn, MapAchievement=threat_safe_without_3_stars fallback to 100_percent_clear')
self.config.override(StopCondition_MapAchievement='100_percent_clear')
return name, folder
def can_use_auto_search_continue(self):
# Cannot update map info in auto search menu
# Close it if map achievement is set
if self.config.StopCondition_MapAchievement != 'non_stop':
return False
return self.run_count > 0 and self.campaign.map_is_auto_search
def handle_commission_notice(self):
"""
Check commission notice.
If found, stop current task and call commission.
Raises:
TaskEnd: If found commission notice.
Pages:
in: page_campaign
"""
if self.campaign.commission_notice_show_at_campaign():
logger.info('Commission notice found')
self.config.task_call('Commission', force_call=True)
self.config.task_stop('Commission notice found')
def sync_emotion(self):
KEYS = ['.Emotion.Fleet1Value','.Emotion.Fleet1Record','.Emotion.Fleet1Recover','.Emotion.Fleet2Value','.Emotion.Fleet2Record','.Emotion.Fleet2Recover']
DATA =[self.config.Emotion_Fleet1Value,self.config.Emotion_Fleet1Record,self.config.Emotion_Fleet1Recover,self.config.Emotion_Fleet2Value,self.config.Emotion_Fleet2Record,self.config.Emotion_Fleet2Recover]
for i, key in enumerate(KEYS):
data = DATA[i]
self.config.cross_set(keys=f'Event2{key}', value=f'{data}')
logger.hr('detect emotion delay,sync emotion to event2')
def solve_emotion_error(self,name):
method = self.config.Fleet_FleetOrder
if method == 'fleet1_mob_fleet2_boss':
fleet = 'fleet_1'
elif method == 'fleet1_boss_fleet2_mob':
fleet = 'fleet_2'
elif method == 'fleet1_all_fleet2_standby':
fleet = 'fleet_1'
elif method == 'fleet1_standby_fleet2_all':
fleet = 'fleet_2'
logger.info(f"now combat is {method}")
logger.warning(f"{name} recorded {fleet} is :{getattr(self.campaign.emotion, fleet).current}")
if getattr(self.campaign.emotion, fleet).current > 75:
handle_notify(
self.config.Error_OnePushConfig,
title=f"Alas <{self.config.config_name}> {name} Emotion calculate error ",
content=f"<{self.config.config_name}> {fleet} recorded is {getattr(self.campaign.emotion, fleet).current},Emotion calculate error"
)
setattr(getattr(self.campaign.emotion, fleet), 'current', 0)
self.campaign.emotion.record()
self.campaign.emotion.show()
try:
self.campaign.emotion.check_reduce(self.campaign._map_battle)
except ScriptEnd as e:
logger.hr('Script end')
logger.info(str(e))
if self.appear_then_click(LOW_EMOTION_LEFT, offset=(30, 30), interval=3):
return True
else:
raise GamePageUnknownError(f'LOW EMOTION TIP FOUND, BUT NO LEFT button')
def detect_low_emotion(self,name):
EMOTION_TIP_L1=Button(area=(352, 311, 929, 348), color=(), button=(352, 311, 929, 348))
EMOTION_TIP_L2=Button(area=(352, 350, 929, 387), color=(), button=(352, 350, 929, 387))
EMOTION_TIP_L3=Button(area=(352, 390, 929, 427), color=(), button=(352, 390, 929, 427))
# 获取识别结果
result = Ocr(EMOTION_TIP_L1, lang= 'cnocr').ocr(self.device.image)
result += Ocr(EMOTION_TIP_L2, lang= 'cnocr').ocr(self.device.image)
result += Ocr(EMOTION_TIP_L3, lang= 'cnocr').ocr(self.device.image)
logger.info(result)
if "低心情" in result or "降低好感" in result:
logger.warning("舰队心情低")
self.solve_emotion_error(name)
else:
logger.info("开始第二轮心情OCR识别")
EMOTION_TIP_L4=Button(area=(352, 290, 929, 325), color=(), button=(352, 290, 929, 325))
EMOTION_TIP_L5=Button(area=(352, 325, 929, 360), color=(), button=(352, 325, 929, 360))
EMOTION_TIP_L6=Button(area=(352, 360, 929, 395), color=(), button=(352, 360, 929, 395))
result2 = Ocr(EMOTION_TIP_L4, lang= 'cnocr').ocr(self.device.image)
result2 += Ocr(EMOTION_TIP_L5, lang= 'cnocr').ocr(self.device.image)
result2 += Ocr(EMOTION_TIP_L6, lang= 'cnocr').ocr(self.device.image)
if "低心情" in result2 or "降低好感" in result2:
logger.warning("舰队心情低")
self.solve_emotion_error(name)
else:
logger.warning("Game stuck, but not emotion error")
raise GameStuckError(f'Wait too long but not emotion error')
def run(self, name, folder='campaign_main', mode='normal', total=0,from_eventDaily=False):
"""
Args:
name (str): Name of .py file.
folder (str): Name of the file folder under campaign.
mode (str): `normal` or `hard`
total (int):
"""
# Check if need to reset stage due to long time no run (for Event task)
if self.config.task.command == 'Event':
reset_hours = self.config.EventPt_EventResetStageAfterHours
if reset_hours > 0:
last_record = self.config.cross_get(keys=['Event', 'EventPt', 'EventResetStageRecord'], default=None)
if isinstance(last_record, datetime):
hours_since_last = (datetime.now() - last_record).total_seconds() / 3600
if hours_since_last > reset_hours:
reset_name = self.config.EventPt_EventResetStageName
logger.info(f'Event task has not run for {hours_since_last:.1f} hours (> {reset_hours}h), '
f'resetting stage from {name} to {reset_name}')
name = reset_name
# Save the reset stage name to config
self.config.cross_set(keys='Event.Campaign.Name', value=reset_name)
# Update the record time
self.config.cross_set(keys='Event.EventPt.EventResetStageRecord', value=datetime.now().replace(microsecond=0))
name, folder = self.handle_stage_name(name, folder, mode=mode)
self.config.override(Campaign_Name=name, Campaign_Event=folder)
self.load_campaign(name, folder=folder)
self.run_count = 0
self.run_limit = self.config.StopCondition_RunCount
while 1:
# End
if total and self.run_count >= total:
break
if self.campaign.event_time_limit_triggered():
self.config.task_stop()
# Log
logger.hr(name, level=1)
if self.config.StopCondition_RunCount > 0:
logger.info(f'Count remain: {self.config.StopCondition_RunCount}')
else:
logger.info(f'Count: {self.run_count}')
# UI ensure
self.device.stuck_record_clear()
self.device.click_record_clear()
if not self.device.has_cached_image:
self.device.screenshot()
self.campaign.device.image = self.device.image
if self.campaign.is_in_map():
logger.info('Already in map, retreating.')
try:
self.campaign.withdraw()
except CampaignEnd:
pass
ensure_campaign_ui_result = self.campaign.ensure_campaign_ui(name=self.stage, mode=mode)
elif self.campaign.is_in_auto_search_menu():
if self.can_use_auto_search_continue():
logger.info('In auto search menu, skip ensure_campaign_ui.')
else:
logger.info('In auto search menu, closing.')
# Because event_20240725 task balancer delete self.campaign.ensure_auto_search_exit()
ensure_campaign_ui_result = self.campaign.ensure_campaign_ui(name=self.stage, mode=mode)
else:
ensure_campaign_ui_result = self.campaign.ensure_campaign_ui(name=self.stage, mode=mode)
if ensure_campaign_ui_result is False:
logger.info('Maybe Already pass the stage, goto next.')
self.campaign.handle_map_stop()
break
self.disable_raid_on_event()
self.handle_commission_notice()
# if in hard mode, check remain times
if self.ui_page_appear(page_campaign) and MODE_SWITCH_1.get(main=self) == 'normal':
from module.hard.hard import OCR_HARD_REMAIN
remain = OCR_HARD_REMAIN.ocr(self.device.image)
if not remain:
logger.info('Remaining number of times of hard mode campaign_main is 0, delay task to next day')
self.config.task_delay(server_update=True)
break
# End
if self.triggered_stop_condition(oil_check=not self.campaign.is_in_auto_search_menu()):
break
# Update config
if len(self.config.modified):
logger.info('Updating config for dashboard')
self.config.update()
# Run
self.device.stuck_record_clear()
self.device.click_record_clear()
try:
self.campaign.run()
if self.config.task.command in ['Main2']:
CurrentTimes = self.config.RegularInspections_CurrentCampaignTimes + 1
CheckInterval = self.config.RegularInspections_CheckInterval
self.config.modified["Main2.RegularInspections.CurrentCampaignTimes"] = CurrentTimes
if self.config.RegularInspections_IsResearchInspect:
# logger.info(f"Main2:CurrentTimes: {CurrentTimes}, CheckInterval: {CheckInterval}")
if CurrentTimes % CheckInterval == 0:
from module.regular_inspect.research_inspect import ResearchInspect
ResearchInspect(config=self.config, device=self.device).CheckResearchShipExperience()
self.config.update()
if self.config.RegularInspections_IsFleetInspect:
if CurrentTimes % CheckInterval == 0:
from module.regular_inspect.fleet_inspect import FleetInfoCheck
fleet_index = self.config.RegularInspections_FleetInspectIndex
if fleet_index == 0:
method = self.config.Fleet_FleetOrder
if method == 'fleet1_mob_fleet2_boss':
fleet = 'Fleet1'
elif method == 'fleet1_boss_fleet2_mob':
fleet = 'Fleet2'
elif method == 'fleet1_all_fleet2_standby':
fleet = 'Fleet1'
elif method == 'fleet1_standby_fleet2_all':
fleet = 'Fleet2'
fleet_index = getattr(self.config, f'Fleet_{fleet}')
logger.info(f"now combat is {method}, fleet_index: {fleet_index}")
FleetInfoCheck(config=self.config, device=self.device).get_fleet_info(fleet_index)
self.config.update()
except ScriptEnd as e:
logger.hr('Script end')
logger.info(str(e))
if from_eventDaily == True:
if str(e) == 'Emotion control':
self.sync_emotion()
break
except GameStuckError as e:
if self.detect_low_emotion(name):
break
# Update config
if len(self.campaign.config.modified):
logger.info('Updating config for dashboard')
self.campaign.config.update()
# After run
self.run_count += 1
if self.config.StopCondition_RunCount:
self.config.StopCondition_RunCount -= 1
# End
if self.triggered_stop_condition(oil_check=False):
break
# One-time stage limit
if self.campaign.config.MAP_IS_ONE_TIME_STAGE:
if self.run_count >= 1:
logger.hr('Triggered one-time stage limit')
self.campaign.handle_map_stop()
break
# Loop stages
if self.is_stage_loop:
if self.run_count >= 1:
logger.hr('Triggered loop stage switch')
break
# Scheduler
if self.config.task_switched():
self.campaign.ensure_auto_search_exit()
self.config.task_stop()
self.campaign.ensure_auto_search_exit()
if __name__ == '__main__':
import numpy as np
from PIL import Image
# 初始化logger
logger.hr('测试低心情检测')
def detect_low_emotion_TEST(image: np.ndarray):
EMOTION_TIP_L1=Button(area=(352, 311, 929, 348), color=(), button=(352, 311, 929, 348))
EMOTION_TIP_L2=Button(area=(352, 350, 929, 387), color=(), button=(352, 350, 929, 387))
EMOTION_TIP_L3=Button(area=(352, 390, 929, 427), color=(), button=(352, 390, 929, 427))
logger.info("开始OCR识别")
# 获取识别结果
result = Ocr(EMOTION_TIP_L1, lang= 'cnocr').ocr(image)
result += Ocr(EMOTION_TIP_L2, lang= 'cnocr').ocr(image)
result += Ocr(EMOTION_TIP_L3, lang= 'cnocr').ocr(image)
logger.info(f"OCR识别结果: {result}")
if "低心情" in result or "降低好感" in result:
logger.warning("舰队心情低")
else:
logger.info("开始第二轮OCR识别")
EMOTION_TIP_L4=Button(area=(352, 290, 929, 325), color=(), button=(352, 290, 929, 325))
EMOTION_TIP_L5=Button(area=(352, 325, 929, 360), color=(), button=(352, 325, 929, 360))
EMOTION_TIP_L6=Button(area=(352, 360, 929, 395), color=(), button=(352, 360, 929, 395))
result2 = Ocr(EMOTION_TIP_L4, lang= 'cnocr').ocr(image)
result2 += Ocr(EMOTION_TIP_L5, lang= 'cnocr').ocr(image)
result2 += Ocr(EMOTION_TIP_L6, lang= 'cnocr').ocr(image)
if "低心情" in result2 or "降低好感" in result2:
logger.warning("舰队心情低")
else:
logger.warning("Game stuck, but not emotion error")
raise GameStuckError(f'Wait too long but not emotion error')
# 测试用例
logger.info("正在读取测试图片")
try:
image = np.array(Image.open(r'C:\Users\W1NDe\Documents\GitHub\M-AzurLaneAutoScript\module\campaign\test.png').convert('RGB'))
logger.info(f"成功读取图片,尺寸: {image.shape}")
detect_low_emotion_TEST(image)
except Exception as e:
logger.warning(f"读取图片失败: {str(e)}")