From 3b6fdbf55405131c3791a3453a69f12ba341c615 Mon Sep 17 00:00:00 2001 From: Zebartin <16185081+Zebartin@users.noreply.github.com> Date: Mon, 20 May 2024 09:22:45 +0800 Subject: [PATCH 01/13] Fix: Wrong insight in _check_event --- tasks/assignment/assignment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/assignment/assignment.py b/tasks/assignment/assignment.py index 015247cfd..a90d07675 100644 --- a/tasks/assignment/assignment.py +++ b/tasks/assignment/assignment.py @@ -205,8 +205,8 @@ class Assignment(AssignmentClaim, SynthesizeUI): continue logger.hr('Assignment event', level=2) logger.info(f'Check assignment event: {assignment}') - # Order of entries does not change during iteration - self.goto_entry(assignment, insight=False) + # Order of entries changes if claimed + self.goto_entry(assignment, insight=claimed) status = self._check_assignment_status() if status == AssignmentStatus.LOCKED: continue From 8e04b0da5211f0c5cab1d8d5e2c11654395c4ef8 Mon Sep 17 00:00:00 2001 From: Zebartin <16185081+Zebartin@users.noreply.github.com> Date: Mon, 20 May 2024 09:50:01 +0800 Subject: [PATCH 02/13] Fix: Color count for CHARACTER_x_SELECTED --- tasks/assignment/dispatch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tasks/assignment/dispatch.py b/tasks/assignment/dispatch.py index 37acd7e12..fb818c569 100644 --- a/tasks/assignment/dispatch.py +++ b/tasks/assignment/dispatch.py @@ -105,11 +105,11 @@ class AssignmentDispatch(AssignmentUI): continue # Select if self.interval_is_reached(CHARACTER_1_SELECTED, interval=2): - if not self.image_color_count(CHARACTER_1_SELECTED, (240, 240, 240)): + if not self.image_color_count(CHARACTER_1_SELECTED, (240, 240, 240), threshold=221, count=160): self.device.click(CHARACTER_1) self.interval_reset(CHARACTER_1_SELECTED, interval=2) if self.interval_is_reached(CHARACTER_2_SELECTED, interval=2): - if not self.image_color_count(CHARACTER_2_SELECTED, (240, 240, 240)): + if not self.image_color_count(CHARACTER_2_SELECTED, (240, 240, 240), threshold=221, count=160): self.device.click(CHARACTER_2) self.interval_reset(CHARACTER_2_SELECTED, interval=2) @@ -126,7 +126,8 @@ class AssignmentDispatch(AssignmentUI): # End if self.appear(CONFIRM_ASSIGNMENT): if self.image_color_count(CONFIRM_ASSIGNMENT.button, color=(227, 227, 227), count=1000): - logger.info('Characters are all selected (light button)') + logger.info( + 'Characters are all selected (light button)') break if self.appear(CHARACTER_LIST, interval=2): # EMPTY_SLOT appeared above From edb55c6f648add42e6cf5b523762539cb4e3fa5c Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 15:19:34 +0800 Subject: [PATCH 03/13] Fix: Close weekly reward popup in _rogue_theme_set() --- tasks/rogue/entry/entry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tasks/rogue/entry/entry.py b/tasks/rogue/entry/entry.py index 448e33888..acba8caf1 100644 --- a/tasks/rogue/entry/entry.py +++ b/tasks/rogue/entry/entry.py @@ -164,6 +164,11 @@ class RogueEntry(RouteBase, RogueRewardHandler, RoguePathHandler, DungeonUI): if interval.reached() and self.is_page_rogue_main(): self.device.click(THEME_SWITCH) interval.reset() + # Weekly refresh popup + if self.appear_then_click(REWARD_CLOSE, interval=2): + continue + if self.handle_reward(): + continue def _rogue_world_set(self, world: int | DungeonList, skip_first_screenshot=True): """ From 3d5258b59145002b312ce7a65a0140084223ab8b Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 15:19:52 +0800 Subject: [PATCH 04/13] Upd: Add Robin as ranged character --- tasks/character/keywords/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks/character/keywords/__init__.py b/tasks/character/keywords/__init__.py index 346ed08ad..812f10b3e 100644 --- a/tasks/character/keywords/__init__.py +++ b/tasks/character/keywords/__init__.py @@ -11,6 +11,8 @@ DICT_SORTED_RANGES = { Welt, Aventurine, FuXuan, + # Slow bullet + Robin, # Longer precast BlackSwan, ], From 272f803d8cdbd349e0ccf4db7f3e116457694216 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 15:24:00 +0800 Subject: [PATCH 05/13] Fix: unexpected_confirm has no effect if arrive waypoint at the every first screenshot --- tasks/map/control/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks/map/control/control.py b/tasks/map/control/control.py index bf99c8302..799d1ef13 100644 --- a/tasks/map/control/control.py +++ b/tasks/map/control/control.py @@ -135,6 +135,7 @@ class MapControl(Combat, AimDetectorMixin): logger.info(f'Goto {waypoint}') self.screenshot_tracking_add() self.waypoint = waypoint + waypoint.unexpected_confirm.reset() self.device.stuck_record_clear() self.device.click_record_clear() From 687e06b639cd8397cc3636101de41a8aa434b63b Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 15:26:40 +0800 Subject: [PATCH 06/13] Fix: Item wasn't added as walk result if item drops blessing, the curio effect --- tasks/map/control/control.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tasks/map/control/control.py b/tasks/map/control/control.py index 799d1ef13..844bc8788 100644 --- a/tasks/map/control/control.py +++ b/tasks/map/control/control.py @@ -177,6 +177,12 @@ class MapControl(Combat, AimDetectorMixin): if waypoint.early_stop: return result if self.walk_additional(): + # Clearing items may trigger additional popups + if attacked_item.started() and attacked_item.reached(): + logger.info('Walk result add: item') + result.append('item') + if 'item' in waypoint.expected_end and waypoint.early_stop: + return result attacked_enemy.clear() attacked_item.clear() continue @@ -228,7 +234,7 @@ class MapControl(Combat, AimDetectorMixin): if attacked_item.started() and attacked_item.reached(): logger.info('Walk result add: item') result.append('item') - if waypoint.early_stop: + if 'item' in waypoint.expected_end and waypoint.early_stop: return result if waypoint.interact_radius > 0: if diff < waypoint.interact_radius: From 43fddd445c253ed7bf76f4c4c3a499662ef59f0b Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 15:29:54 +0800 Subject: [PATCH 07/13] Fix: Handle curio popup after combat, the curio effect --- tasks/rogue/route/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tasks/rogue/route/base.py b/tasks/rogue/route/base.py index 859c6e7d8..548d1deec 100644 --- a/tasks/rogue/route/base.py +++ b/tasks/rogue/route/base.py @@ -20,6 +20,10 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): enroute_add_item = True def combat_expected_end(self): + # Curio effect, that drops curio after combat + if self.handle_blessing_popup(): + return False + # Blessings after combat if self.is_page_choose_blessing(): logger.info('Combat ended at is_page_choose_blessing()') return True From a1a77f921e1e4855da13ff983bafbeed086920c8 Mon Sep 17 00:00:00 2001 From: Zebartin <16185081+Zebartin@users.noreply.github.com> Date: Mon, 20 May 2024 18:07:39 +0800 Subject: [PATCH 08/13] Fix: Ensure assignment entry loaded --- tasks/assignment/assignment.py | 6 ++++-- tasks/assignment/ui.py | 27 +++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/tasks/assignment/assignment.py b/tasks/assignment/assignment.py index a90d07675..3019181dd 100644 --- a/tasks/assignment/assignment.py +++ b/tasks/assignment/assignment.py @@ -35,7 +35,6 @@ class Assignment(AssignmentClaim, SynthesizeUI): self.has_new_dispatch = False self.ensure_scroll_top(page_menu) self.ui_ensure(page_assignment) - self._wait_until_group_loaded() event_ongoing = next(( g for g in self._iter_groups() if isinstance(g, AssignmentEventGroup) @@ -91,12 +90,14 @@ class Assignment(AssignmentClaim, SynthesizeUI): logger.info( f'User specified assignments: {", ".join([x.name for x in assignments])}') remain = None + insight = False for assignment in assignments: if assignment in self.dispatched: continue logger.hr('Assignment inlist', level=2) logger.info(f'Check assignment inlist: {assignment}') - self.goto_entry(assignment) + self.goto_entry(assignment, insight=insight) + insight = True if remain is None: _, remain, _ = self._limit_status status = self._check_assignment_status() @@ -106,6 +107,7 @@ class Assignment(AssignmentClaim, SynthesizeUI): if status == AssignmentStatus.DISPATCHED: self.dispatched[assignment] = datetime.now() + \ self._get_assignment_time() + insight = False continue # General assignments must be dispatchable here if remain <= 0: diff --git a/tasks/assignment/ui.py b/tasks/assignment/ui.py index b083b6897..0062be865 100644 --- a/tasks/assignment/ui.py +++ b/tasks/assignment/ui.py @@ -40,7 +40,8 @@ class AssignmentOcr(Ocr): (KEYWORDS_ASSIGNMENT_ENTRY.Legend_of_the_Puppet_Master, '^师传说'), (KEYWORDS_ASSIGNMENT_ENTRY.The_Wages_of_Humanity, '[赠]养人类'), (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Car_Thief, '.*的偷车贼.*'), - (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Synesthesia_Beacon_Function_Iteration, '联觉信标功能[送]代'), + (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Synesthesia_Beacon_Function_Iteration, + '联觉信标功能[送]代'), ], 'en': [ # (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Food_Improvement_Plan.name, @@ -160,6 +161,7 @@ class AssignmentUI(UI): logger.hr('Assignment group goto', level=3) if ASSIGNMENT_GROUP_SWITCH.set(group, self): self._wait_until_entry_loaded() + self._wait_until_correct_entry_loaded(group) def goto_entry(self, entry: AssignmentEntry, insight: bool = True): """ @@ -218,6 +220,27 @@ class AssignmentUI(UI): logger.info('Entry loaded') break + def _wait_until_correct_entry_loaded(self, group: AssignmentGroup): + skip_first_screenshot = True + timeout = Timer(3, count=3).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + logger.warning('Wait correct entry loaded timeout') + break + + ASSIGNMENT_ENTRY_LIST.load_rows(self) + if all( + x.matched_keyword.group == group + for x in ASSIGNMENT_ENTRY_LIST.cur_buttons + ): + logger.info('Correct entry loaded') + break + @property def _limit_status(self) -> tuple[int, int, int]: self.device.screenshot() @@ -279,7 +302,7 @@ class AssignmentUI(UI): """ Iterate entries from top to bottom """ - ASSIGNMENT_ENTRY_LIST.load_rows(main=self) + # load_rows is done in _wait_until_correct_entry_loaded already # Freeze ocr results here yield from [ button.matched_keyword From 191f2f177f36bb7a0db5bccfa99be7d1b1ddf459 Mon Sep 17 00:00:00 2001 From: Zebartin <16185081+Zebartin@users.noreply.github.com> Date: Mon, 20 May 2024 21:27:55 +0800 Subject: [PATCH 09/13] Fix: Ensure necessary load_rows in goto_group --- tasks/assignment/ui.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tasks/assignment/ui.py b/tasks/assignment/ui.py index 0062be865..494762220 100644 --- a/tasks/assignment/ui.py +++ b/tasks/assignment/ui.py @@ -157,6 +157,8 @@ class AssignmentUI(UI): self.goto_group(KEYWORDS_ASSIGNMENT_GROUP.Character_Materials) """ if ASSIGNMENT_GROUP_SWITCH.get(self) == group: + if not ASSIGNMENT_ENTRY_LIST.cur_buttons: + ASSIGNMENT_ENTRY_LIST.load_rows(self) return logger.hr('Assignment group goto', level=3) if ASSIGNMENT_GROUP_SWITCH.set(group, self): @@ -302,7 +304,7 @@ class AssignmentUI(UI): """ Iterate entries from top to bottom """ - # load_rows is done in _wait_until_correct_entry_loaded already + # load_rows is done in goto_group already # Freeze ocr results here yield from [ button.matched_keyword From 44458596716e06930a91604a8a934e9dde6e816f Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 19:11:27 +0800 Subject: [PATCH 10/13] Chore: [ALAS] Abstract save_error_log for reusing --- module/alas.py | 32 ++-------------------------- module/logger/__init__.py | 5 ++--- module/logger/error.py | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 module/logger/error.py diff --git a/module/alas.py b/module/alas.py index 34d1d7b53..7663fc119 100644 --- a/module/alas.py +++ b/module/alas.py @@ -1,5 +1,3 @@ -import os -import re import threading import time from datetime import datetime, timedelta @@ -11,7 +9,7 @@ from module.base.decorator import del_cached_property from module.config.config import AzurLaneConfig, TaskEnd from module.config.utils import deep_get, deep_set from module.exception import * -from module.logger import logger +from module.logger import logger, save_error_log from module.notify import handle_notify @@ -149,33 +147,7 @@ class AzurLaneAutoScript: Save last 60 screenshots in ./log/error/ Save logs to ./log/error//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: - folder = f'./log/error/{int(time.time() * 1000)}' - logger.warning(f'Saving error: {folder}') - os.makedirs(folder, exist_ok=True) - 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') - if self.device.screenshot_tracking: - os.makedirs(f'{folder}/tracking', exist_ok=True) - for data in self.device.screenshot_tracking: - image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') - with open(f'{folder}/tracking/{image_time}.png', 'wb') as f: - f.write(data['image'].getvalue()) - 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) + save_error_log(config=self.config, device=self.device) def wait_until(self, future): """ diff --git a/module/logger/__init__.py b/module/logger/__init__.py index b584b3c1f..a4d045ddc 100644 --- a/module/logger/__init__.py +++ b/module/logger/__init__.py @@ -1,3 +1,2 @@ -from .logger import logger -from .logger import set_file_logger, set_func_logger -from .logger import WEB_THEME, Highlighter, HTMLConsole +from .error import save_error_log +from .logger import HTMLConsole, Highlighter, WEB_THEME, logger, set_file_logger, set_func_logger diff --git a/module/logger/error.py b/module/logger/error.py new file mode 100644 index 000000000..b2236d5ad --- /dev/null +++ b/module/logger/error.py @@ -0,0 +1,44 @@ +import os +import re +import time +from datetime import datetime + +from module.logger.logger import logger + + +def save_error_log(config, device): + """ + Save last 60 screenshots in ./log/error/ + Save logs to ./log/error//log.txt + + Args: + config: AzurLaneConfig object + device: Device object + """ + from module.base.utils import save_image + from module.handler.sensitive_info import (handle_sensitive_image, handle_sensitive_logs) + if config.Error_SaveError: + folder = f'./log/error/{int(time.time() * 1000)}' + logger.warning(f'Saving error: {folder}') + os.makedirs(folder, exist_ok=True) + for data in 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') + if device.screenshot_tracking: + os.makedirs(f'{folder}/tracking', exist_ok=True) + for data in device.screenshot_tracking: + image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') + with open(f'{folder}/tracking/{image_time}.png', 'wb') as f: + f.write(data['image'].getvalue()) + 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) From 80802663be6372337c00bff6af434bed61c666f0 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 19:22:43 +0800 Subject: [PATCH 11/13] Fix: Leave rogue on error instead of restarting --- module/alas.py | 3 ++ module/exception.py | 6 ++++ tasks/rogue/route/loader.py | 56 +++++++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/module/alas.py b/module/alas.py index 7663fc119..5c185d170 100644 --- a/module/alas.py +++ b/module/alas.py @@ -115,6 +115,9 @@ class AzurLaneAutoScript: else: self.checker.wait_until_available() return False + except HandledError as e: + logger.error(e) + return False except ScriptError as e: logger.critical(e) logger.critical('This is likely to be a mistake of developers, but sometimes just random issues') diff --git a/module/exception.py b/module/exception.py index 2d027bb99..a0e96e460 100644 --- a/module/exception.py +++ b/module/exception.py @@ -17,6 +17,12 @@ class GameTooManyClickError(Exception): pass +class HandledError(Exception): + # Error handled before raising + # No extra handling required, just retry + pass + + class EmulatorNotRunningError(Exception): pass diff --git a/tasks/rogue/route/loader.py b/tasks/rogue/route/loader.py index ecf78455c..927000031 100644 --- a/tasks/rogue/route/loader.py +++ b/tasks/rogue/route/loader.py @@ -4,7 +4,10 @@ import numpy as np from module.base.decorator import cached_property from module.base.timer import Timer -from module.logger import logger +from module.exception import GameStuckError, HandledError +from module.logger import logger, save_error_log +from tasks.base.assets.assets_base_main_page import ROGUE_LEAVE_FOR_NOW +from tasks.base.assets.assets_base_page import MAP_EXIT from tasks.character.switch import CharacterSwitch from tasks.map.keywords import MapPlane from tasks.map.keywords.plane import ( @@ -17,6 +20,8 @@ from tasks.map.keywords.plane import ( from tasks.map.minimap.minimap import Minimap from tasks.map.resource.resource import SPECIAL_PLANES from tasks.map.route.loader import RouteLoader as RouteLoader_ +from tasks.rogue.assets.assets_rogue_ui import BLESSING_CONFIRM +from tasks.rogue.assets.assets_rogue_weekly import ROGUE_REPORT from tasks.rogue.blessing.ui import RogueUI from tasks.rogue.route.base import RouteBase from tasks.rogue.route.model import RogueRouteListModel, RogueRouteModel @@ -75,7 +80,7 @@ class MinimapWrapper: return self.all_minimap[route.plane_floor] -class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): +class RouteLoader(RouteBase, MinimapWrapper, RouteLoader_, CharacterSwitch): def position_find_known(self, image, force_return=False) -> Optional[RogueRouteModel]: """ Try to find from known route spawn point @@ -271,6 +276,43 @@ class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): if route is not None: return route + def rogue_leave(self, skip_first_screenshot=True): + logger.hr('Rogue leave', level=1) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.is_page_rogue_main(): + logger.info('Rogue left') + break + + # Re-enter + if self.handle_combat_interact(): + continue + # From ui_leave_special + if self.is_in_map_exit(interval=2): + self.device.click(MAP_EXIT) + continue + if self.handle_popup_confirm(): + continue + if self.appear_then_click(ROGUE_LEAVE_FOR_NOW, interval=2): + continue + # Blessing + if self.handle_blessing(): + continue + # _domain_exit_wait_next() + if self.match_template_color(ROGUE_REPORT, interval=2): + logger.info(f'{ROGUE_REPORT} -> {BLESSING_CONFIRM}') + self.device.click(BLESSING_CONFIRM) + continue + if self.handle_reward(): + continue + if self.handle_get_character(): + continue + def route_run(self, route=None): """ Run a rogue domain @@ -286,7 +328,15 @@ class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): # To have a newer image, since previous loadings took some time route = self.position_find(skip_first_screenshot=False) self.screenshot_tracking_add() - super().route_run(route) + + try: + super().route_run(route) + return True + except GameStuckError as e: + logger.error(e) + save_error_log(config=self.config, device=self.device) + self.rogue_leave() + raise HandledError('Rogue run failed') def rogue_run(self, skip_first_screenshot=True): """ From de536125308e6af01c87227d5111e551286d30a0 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 20 May 2024 23:50:13 +0800 Subject: [PATCH 12/13] Fix: Reset combat_wave_cost before every combat --- tasks/combat/combat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index c460b1f77..2aca59b2d 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -86,6 +86,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo support_set = False else: support_set = True + # Reset combat_wave_cost, so handle_combat_interact() won't activate before handle_combat_prepare() + self.combat_wave_cost = 10 logger.info([support_character, support_set]) trial = 0 while 1: From c43a0837c460516542bb599fc11f90ee55db7339 Mon Sep 17 00:00:00 2001 From: Zebartin <16185081+Zebartin@users.noreply.github.com> Date: Mon, 20 May 2024 23:49:36 +0800 Subject: [PATCH 13/13] Fix: Check EVENT_FINISHED TODO: [EN] Add EVENT_FINISHED --- assets/cn/assignment/ui/EVENT_FINISHED.png | Bin 0 -> 8123 bytes tasks/assignment/assets/assets_assignment_ui.py | 11 +++++++++++ tasks/assignment/claim.py | 4 ++++ tasks/assignment/ui.py | 6 ++++++ 4 files changed, 21 insertions(+) create mode 100644 assets/cn/assignment/ui/EVENT_FINISHED.png diff --git a/assets/cn/assignment/ui/EVENT_FINISHED.png b/assets/cn/assignment/ui/EVENT_FINISHED.png new file mode 100644 index 0000000000000000000000000000000000000000..b0dedb535204cdf31d1204bb0962d4069d012341 GIT binary patch literal 8123 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