forked from LmeSzinc/AzurLaneAutoScript
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalas.py
More file actions
778 lines (676 loc) · 32.8 KB
/
alas.py
File metadata and controls
778 lines (676 loc) · 32.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
# rev: auto_restart
# 基于原版 alas.py 增加了自动尝试重启调度器的功能
# 用于解决 Unknown ui page 、短暂网络不良等 无需人工修复的偶发意外情形,避免调度器直接终止
# Modified: run, loop
# Last Updated: 2025-09-01 00:03
import os
import re
import threading
import time
from datetime import datetime, timedelta
import inflection
from cached_property import cached_property
from module.base.decorator import del_cached_property
from module.base.api_client import ApiClient
from module.config.config import AzurLaneConfig, TaskEnd
from module.config.deep import deep_get, deep_set
from module.exception import *
from module.logger import logger
from module.notify import handle_notify
RESTART_SENSITIVE_TASKS = ['OpsiObscure', 'OpsiAbyssal', 'OpsiCrossMonth']
class AzurLaneAutoScript:
stop_event: threading.Event = None
def __init__(self, config_name='alas'):
logger.hr('Start', level=0)
self.config_name = config_name
# Skip first restart
self.is_first_task = True
# Failure count of tasks
# Key: str, task name, value: int, failure count
self.failure_record = {}
@cached_property
def config(self):
try:
config = AzurLaneConfig(config_name=self.config_name)
return config
except RequestHumanTakeover:
logger.critical('Request human takeover')
exit(1)
except Exception as e:
logger.exception(e)
exit(1)
@cached_property
def device(self):
try:
from module.device.device import Device
device = Device(config=self.config)
return device
except RequestHumanTakeover:
logger.critical('Request human takeover')
exit(1)
except EmulatorNotRunningError:
logger.critical('EmulatorNotRunningError')
exit(1)
except Exception as e:
logger.exception(e)
exit(1)
@cached_property
def checker(self):
try:
from module.server_checker import ServerChecker
checker = ServerChecker(server=self.config.Emulator_ServerName)
return checker
except Exception as e:
logger.exception(e)
exit(1)
# def run(self, command, skip_first_screenshot=False):
# try:
# if not skip_first_screenshot:
# self.device.screenshot()
# self.__getattribute__(command)()
# return True
# except TaskEnd:
# return True
# except GameNotRunningError as e:
# logger.warning(e)
# self.config.task_call('Restart')
# return False
# except (GameStuckError, GameTooManyClickError) as e:
# logger.error(e)
# self.save_error_log()
# logger.warning(f'Game stuck, {self.device.package} will be restarted in 10 seconds')
# logger.warning('If you are playing by hand, please stop Alas')
# self.config.task_call('Restart')
# self.device.sleep(10)
# return False
# except GameBugError as e:
# logger.warning(e)
# self.save_error_log()
# logger.warning('An error has occurred in Azur Lane game client, Alas is unable to handle')
# logger.warning(f'Restarting {self.device.package} to fix it')
# self.config.task_call('Restart')
# self.device.sleep(10)
# return False
# except GamePageUnknownError:
# logger.info('Game server may be under maintenance or network may be broken, check server status now')
# self.checker.check_now()
# if self.checker.is_available():
# logger.critical('Game page unknown')
# self.save_error_log()
# handle_notify(
# self.config.Error_OnePushConfig,
# title=f"Alas <{self.config_name}> crashed",
# content=f"<{self.config_name}> GamePageUnknownError",
# )
# exit(1)
# else:
# self.checker.wait_until_available()
# return False
# except ScriptError as 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,
# title=f"Alas <{self.config_name}> crashed",
# content=f"<{self.config_name}> ScriptError",
# )
# exit(1)
# except RequestHumanTakeover:
# logger.critical('Request human takeover')
# handle_notify(
# self.config.Error_OnePushConfig,
# title=f"Alas <{self.config_name}> crashed",
# content=f"<{self.config_name}> RequestHumanTakeover",
# )
# exit(1)
# except Exception as e:
# logger.exception(e)
# self.save_error_log()
# handle_notify(
# self.config.Error_OnePushConfig,
# title=f"Alas <{self.config_name}> crashed",
# content=f"<{self.config_name}> Exception occured",
# )
# exit(1)
def run(self, command, skip_first_screenshot=False):
"""
Run a task command.
Returns:
True: Task completed successfully
False: Task failed with unrecoverable error (counts toward failure limit)
'recoverable': Task failed with recoverable error (does NOT count toward failure limit)
"""
try:
if not skip_first_screenshot:
self.device.screenshot()
self.__getattribute__(command)()
return True
except TaskEnd:
return True
except GameNotRunningError as e:
# 可恢复错误:游戏未运行,重启即可
logger.warning(e)
self.config.task_call('Restart')
return 'recoverable'
except (GameStuckError, GameTooManyClickError) as e:
# 可恢复错误:游戏卡住或点击过多,重启即可
logger.error(e)
self.save_error_log()
logger.warning(f'Game stuck, {self.device.package} will be restarted in 10 seconds')
logger.warning('If you are playing by hand, please stop Alas')
self.config.task_call('Restart')
self.device.sleep(10)
return 'recoverable'
except GameBugError as e:
# 可恢复错误:游戏客户端 bug,重启即可
logger.warning(e)
self.save_error_log()
logger.warning('An error has occurred in Azur Lane game client, Alas is unable to handle')
logger.warning(f'Restarting {self.device.package} to fix it')
self.config.task_call('Restart')
self.device.sleep(10)
return 'recoverable'
except GamePageUnknownError:
logger.info('Game server may be under maintenance or network may be broken, check server status now')
self.checker.check_now()
if self.checker.is_available():
logger.critical('Game page unknown')
self.save_error_log()
logger.warning('Restart to reset Game page in 10 seconds')
self.config.task_call('Restart')
self.device.sleep(10)
return False
else:
self.checker.wait_until_available()
return False
except ScriptError as 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,
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> ScriptError",
)
# exit(1)
raise
except RequestHumanTakeover:
logger.critical('Request human takeover')
handle_notify(
self.config.Error_OnePushConfig,
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> RequestHumanTakeover",
)
exit(1)
except AutoSearchSetError:
logger.critical('Auto search could not be set correctly. Maybe your ships in hard mode are changed.')
logger.critical('Request human takeover.')
exit(1)
except Exception as e:
logger.exception(e)
self.save_error_log()
handle_notify(
self.config.Error_OnePushConfig,
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> Exception occured",
)
# exit(1)
raise
def save_error_log(self):
"""
Save last 60 screenshots in ./log/error/<timestamp>
Save logs to ./log/error/<timestamp>/log.txt
"""
from module.base.utils import save_image
from module.handler.sensitive_info import (handle_sensitive_image,
handle_sensitive_logs)
if self.config.Error_SaveError:
if not os.path.exists('./log/error'):
os.mkdir('./log/error')
folder = f'./log/error/{int(time.time() * 1000)}'
logger.warning(f'Saving error: {folder}')
os.mkdir(folder)
for data in self.device.screenshot_deque:
image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f')
image = handle_sensitive_image(data['image'])
save_image(image, f'{folder}/{image_time}.png')
with open(logger.log_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
start = 0
for index, line in enumerate(lines):
line = line.strip(' \r\t\n')
if re.match('^═{15,}$', line):
start = index
lines = lines[start - 2:]
lines = handle_sensitive_logs(lines)
with open(f'{folder}/log.txt', 'w', encoding='utf-8') as f:
f.writelines(lines)
def restart(self):
from module.handler.login import LoginHandler
LoginHandler(self.config, device=self.device).app_restart()
self.config.task_delay(server_update=True)
def start(self):
from module.handler.login import LoginHandler
LoginHandler(self.config, device=self.device).app_start()
def goto_main(self):
from module.handler.login import LoginHandler
from module.ui.ui import UI
if self.device.app_is_running():
logger.info('App is already running, goto main page')
UI(self.config, device=self.device).ui_goto_main()
else:
logger.info('App is not running, start app and goto main page')
LoginHandler(self.config, device=self.device).app_start()
UI(self.config, device=self.device).ui_goto_main()
def research(self):
from module.research.research import RewardResearch
RewardResearch(config=self.config, device=self.device).run()
def commission(self):
from module.commission.commission import RewardCommission
RewardCommission(config=self.config, device=self.device).run()
def tactical(self):
from module.tactical.tactical_class import RewardTacticalClass
RewardTacticalClass(config=self.config, device=self.device).run()
def dorm(self):
from module.dorm.dorm import RewardDorm
RewardDorm(config=self.config, device=self.device).run()
def meowfficer(self):
from module.meowfficer.meowfficer import RewardMeowfficer
RewardMeowfficer(config=self.config, device=self.device).run()
def guild(self):
from module.guild.guild_reward import RewardGuild
RewardGuild(config=self.config, device=self.device).run()
def reward(self):
from module.reward.reward import Reward
Reward(config=self.config, device=self.device).run()
def awaken(self):
from module.awaken.awaken import Awaken
Awaken(config=self.config, device=self.device).run()
def shop_frequent(self):
from module.shop.shop_reward import RewardShop
RewardShop(config=self.config, device=self.device).run_frequent()
def shop_once(self):
from module.shop.shop_reward import RewardShop
RewardShop(config=self.config, device=self.device).run_once()
def event_shop(self):
from module.shop_event.shop_event import EventShop
EventShop(config=self.config, device=self.device).run()
def shipyard(self):
from module.shipyard.shipyard_reward import RewardShipyard
RewardShipyard(config=self.config, device=self.device).run()
def gacha(self):
from module.gacha.gacha_reward import RewardGacha
RewardGacha(config=self.config, device=self.device).run()
def freebies(self):
from module.freebies.freebies import Freebies
Freebies(config=self.config, device=self.device).run()
def minigame(self):
from module.minigame.minigame import Minigame
Minigame(config=self.config, device=self.device).run()
def private_quarters(self):
from module.private_quarters.private_quarters import PrivateQuarters
PrivateQuarters(config=self.config, device=self.device).run()
def island(self):
from module.island.island import Island
Island(config=self.config, device=self.device).run()
def daily(self):
from module.daily.daily import Daily
Daily(config=self.config, device=self.device).run()
def hard(self):
from module.hard.hard import CampaignHard
CampaignHard(config=self.config, device=self.device).run()
def exercise(self):
from module.exercise.exercise import Exercise
Exercise(config=self.config, device=self.device).run()
def sos(self):
from module.sos.sos import CampaignSos
CampaignSos(config=self.config, device=self.device).run()
def war_archives(self):
from module.war_archives.war_archives import CampaignWarArchives
CampaignWarArchives(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def raid_daily(self):
from module.raid.daily import RaidDaily
RaidDaily(config=self.config, device=self.device).run()
def event_a(self):
from module.event.campaign_abcd import CampaignABCD
CampaignABCD(config=self.config, device=self.device).run()
def event_b(self):
from module.event.campaign_abcd import CampaignABCD
CampaignABCD(config=self.config, device=self.device).run()
def event_c(self):
from module.event.campaign_abcd import CampaignABCD
CampaignABCD(config=self.config, device=self.device).run()
def event_d(self):
from module.event.campaign_abcd import CampaignABCD
CampaignABCD(config=self.config, device=self.device).run()
def event_sp(self):
from module.event.campaign_sp import CampaignSP
CampaignSP(config=self.config, device=self.device).run()
def maritime_escort(self):
from module.event.maritime_escort import MaritimeEscort
MaritimeEscort(config=self.config, device=self.device).run()
def opsi_ash_assist(self):
from module.os_ash.meta import AshBeaconAssist
AshBeaconAssist(config=self.config, device=self.device).run()
def opsi_ash_beacon(self):
from module.os_ash.meta import OpsiAshBeacon
OpsiAshBeacon(config=self.config, device=self.device).run()
def opsi_explore(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_explore()
def opsi_shop(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_shop()
def opsi_voucher(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_voucher()
def opsi_daily(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_daily()
def opsi_obscure(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_obscure()
def opsi_month_boss(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_month_boss()
def opsi_abyssal(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_abyssal()
def opsi_archive(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_archive()
def opsi_stronghold(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_stronghold()
def opsi_meowfficer_farming(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_meowfficer_farming()
def opsi_hazard1_leveling(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_hazard1_leveling()
def opsi_cross_month(self):
from module.campaign.os_run import OSCampaignRun
OSCampaignRun(config=self.config, device=self.device).opsi_cross_month()
def main(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def main2(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def main3(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def event(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def event2(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def raid(self):
from module.raid.run import RaidRun
RaidRun(config=self.config, device=self.device).run()
def hospital(self):
from module.event_hospital.hospital import Hospital
Hospital(config=self.config, device=self.device).run()
def hospital_event(self):
from module.event_hospital.hospital_event import HospitalEvent
HospitalEvent(config=self.config, device=self.device).run()
def coalition(self):
from module.coalition.coalition import Coalition
Coalition(config=self.config, device=self.device).run()
def coalition_sp(self):
from module.coalition.coalition_sp import CoalitionSP
CoalitionSP(config=self.config, device=self.device).run()
def c72_mystery_farming(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def c122_medium_leveling(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def c124_large_leveling(self):
from module.campaign.run import CampaignRun
CampaignRun(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def gems_farming(self):
from module.campaign.gems_farming import GemsFarming
GemsFarming(config=self.config, device=self.device).run(
name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode)
def island_season_task(self):
from module.island.season_task import IslandSeasonTaskHandler
IslandSeasonTaskHandler(config=self.config, device=self.device).run()
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 event_story(self):
from module.eventstory.eventstory import EventStory
EventStory(config=self.config, device=self.device, task="EventStory").run()
def box_disassemble(self):
from module.storage.box_disassemble import StorageBox
StorageBox(config=self.config, device=self.device, task="BoxDisassemble").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.
Args:
future (datetime):
Returns:
bool: True if wait finished, False if config changed.
"""
future = future + timedelta(seconds=1)
self.config.start_watching()
while 1:
if datetime.now() > future:
return True
if self.stop_event is not None:
if self.stop_event.is_set():
logger.info("Update event detected")
logger.info(f"[{self.config_name}] exited. Reason: Update")
exit(0)
time.sleep(5)
if self.config.should_reload():
return False
def get_next_task(self):
"""
Returns:
str: Name of the next task.
"""
while 1:
task = self.config.get_next()
self.config.task = task
self.config.bind(task)
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():
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.config.task_call('Restart')
del_cached_property(self, 'config')
continue
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
return task.command
def loop(self):
logger.set_file_logger(self.config_name)
logger.info(f'Start scheduler loop: {self.config_name}')
# --- 初始化计数器 ---
consecutive_global_failures = 0
MAX_GLOBAL_FAILURES = 3 # 3 or more,4次及以上会执行长达5分钟的防网络波动等待
RESTART_DELAY = 20 # 重启尝试间隔
LONG_WAIT = 300
while 1:
try:
# Check update event from GUI
if self.stop_event is not None:
if self.stop_event.is_set():
logger.info("Update event detected")
logger.info(f"Alas [{self.config_name}] exited.")
break
# Check game server maintenance
self.checker.wait_until_available()
if self.checker.is_recovered():
# There is an accidental bug hard to reproduce
# Sometimes, config won't be updated due to blocking
# even though it has been changed
# So update it once recovered
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
self.device.config = self.config
# Skip first restart
if task == 'Restart':
if self.is_first_task:
logger.info('Skip task `Restart` at scheduler start')
else:
from module.handler.login import LoginHandler
LoginHandler(self.config, self.device).app_restart()
self.config.task_delay(server_update=True)
del_cached_property(self, 'config')
continue
# Run
logger.info(f'Scheduler: Start task `{task}`')
self.device.stuck_record_clear()
self.device.click_record_clear()
logger.hr(task, level=0)
success = self.run(inflection.underscore(task))
logger.info(f'Scheduler: End task `{task}`')
self.is_first_task = False
# Check failures
# @ 单个任务连续失败三次终止程序
# 注意:可恢复错误 (success == 'recoverable') 不计入失败次数
failed = deep_get(self.failure_record, keys=task, default=0)
if success == True:
failed = 0 # 成功,重置计数
elif success == 'recoverable':
# 可恢复错误(如 GameStuckError),不增加失败计数
# 但也不重置,保持之前的计数
logger.info(f'Task `{task}` encountered a recoverable error, not counting toward failure limit')
else:
failed = failed + 1 # 不可恢复错误,增加计数
deep_set(self.failure_record, keys=task, value=failed)
strict_restart = self.config.Error_StrictRestart and failed >= 1 and task in RESTART_SENSITIVE_TASKS
if failed >= 3 or strict_restart:
logger.critical(f"Task `{task}` failed {failed} or more times.")
logger.critical("Possible reason #1: You haven't used it correctly. "
"Please read the help text of the options.")
logger.critical("Possible reason #2: There is a problem with this task. "
"Please contact developers or try to fix it yourself.")
if strict_restart:
logger.critical("Possible reason #3: This is a restart sensitive task. "
"Please take over the game manually or turn off 'StrictRestart' option.")
logger.critical('Request human takeover')
handle_notify(
self.config.Error_OnePushConfig,
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> RequestHumanTakeover\nTask `{task}` failed {failed} or more times.",
)
logger.warning("任务连续失败次数过多")
# ApiClient.submit_bug_log(f"Alas <{self.config_name}> crashed\nTask `{task}` failed {failed} or more times.")
exit(1)
if success == True:
del_cached_property(self, 'config')
consecutive_global_failures = 0 # Reset global failure counter on successful task
continue
elif success == 'recoverable' or self.config.Error_HandleError:
# 可恢复错误或启用了错误处理,继续循环
# self.config.task_delay(success=False)
del_cached_property(self, 'config')
self.checker.check_now()
continue
else:
break
# --- 新增代码:捕获全局异常并执行重启 ---
except Exception as e:
consecutive_global_failures += 1
self.is_first_task = False
logger.error("An unexpected global exception occurred in the scheduler loop!")
import traceback
logger.error(traceback.format_exc()) # 打印完整的错误堆栈
logger.warning(
f">>> This is consecutive global failure #{consecutive_global_failures} of {MAX_GLOBAL_FAILURES}."
)
# --- 检查是否达到重试上限 ---
if consecutive_global_failures >= MAX_GLOBAL_FAILURES:
logger.critical(
f"Maximum number of consecutive global failures ({MAX_GLOBAL_FAILURES}) reached."
)
logger.critical("The error appears to be fatal and unrecoverable by restarting.")
self.save_error_log()
logger.critical("Scheduler is now terminating. Manual intervention is required.")
logger.warning("遇到无法恢复的致命错误")
# ApiClient.submit_bug_log(f"Alas <{self.config_name}> Scheduler terminating.\nMaximum global failures ({MAX_GLOBAL_FAILURES}) reached.\n{traceback.format_exc()}")
exit(1) # 达到上限,强制终止程序
# --- 尝试重启 ---
logger.warning("Attempting to recover by forcing a RESTART task...")
try:
# 注入 Restart 任务
self.config.task_call('Restart')
# 重新加载配置
del_cached_property(self, 'config')
logger.info("A `Restart` task has been scheduled for the next loop.")
except Exception as restart_e:
logger.error("Failed to even schedule a restart task!")
logger.error(f"Scheduling Error: {restart_e}")
# --- 等待一段时间后开始下一次循环 ---
wait_seconds = RESTART_DELAY if consecutive_global_failures < 4 else LONG_WAIT
logger.info(
f"Scheduler will retry from the beginning in {wait_seconds} seconds."
)
time.sleep(wait_seconds)
if __name__ == '__main__':
alas = AzurLaneAutoScript()
alas.loop()