diff --git a/.gitignore b/.gitignore index 007642a..f248c67 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ venv/* _dump/* .vscode/* *__pycache__* -*.log \ No newline at end of file +*.log* \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index beb080e..08182c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,6 @@ charset-normalizer==3.0.1 colorama==0.4.6 contourpy==1.0.7 cycler==0.11.0 -elevate==0.1.3 exceptiongroup==1.1.0 fonttools==4.38.0 h11==0.14.0 diff --git a/umalauncher/carrotjuicer.py b/umalauncher/carrotjuicer.py index 0bbe5a4..7e07010 100644 --- a/umalauncher/carrotjuicer.py +++ b/umalauncher/carrotjuicer.py @@ -13,8 +13,9 @@ from selenium.webdriver.edge.service import Service as EdgeService from selenium.webdriver.chrome.service import Service as ChromeService from selenium.common.exceptions import NoSuchWindowException -from screenstate import ScreenState, Location +import screenstate_utils import util +import constants import mdb import helper_table import training_tracker @@ -34,6 +35,8 @@ class CarrotJuicer(): training_tracker = None previous_request = None last_helper_data = None + previous_race_program_id = None + last_data = None _browser_list = None @@ -67,7 +70,12 @@ def restart_time(self): def load_request(self, msg_path): try: with open(msg_path, "rb") as in_file: - return msgpack.unpackb(in_file.read()[170:], strict_map_key=False) + unpacked = msgpack.unpackb(in_file.read()[170:], strict_map_key=False) + # Remove keys that are not needed + for key in constants.REQUEST_KEYS_TO_BE_REMOVED: + if key in unpacked: + del unpacked[key] + return unpacked except PermissionError: logger.warning("Could not load request because it is already in use!") time.sleep(0.1) @@ -94,7 +102,7 @@ def create_gametora_helper_url_from_start(self, packet_data): def to_json(self, packet, out_name="packet.json"): - with open(out_name, 'w', encoding='utf-8') as f: + with open(util.get_relative(out_name), 'w', encoding='utf-8') as f: f.write(json.dumps(packet, indent=4, ensure_ascii=False)) # def to_python_dict_file(self, packet, out_name="packet.py"): @@ -346,6 +354,33 @@ def add_response_to_tracker(self, data): if should_track: self.training_tracker.add_response(data) + + EVENT_ID_TO_POS_STRING = { + 7005: '(1st)', + 7006: '(2nd-5th)', + 7007: '(6th or worse)' + } + + def get_after_race_event_title(self, event_id): + if not self.previous_race_program_id: + return "PREVIOUS RACE UNKNOWN" + + race_grade = mdb.get_program_id_grade(self.previous_race_program_id) + + if not race_grade: + logger.error(f"Race grade not found for program id {self.previous_race_program_id}") + return "RACE GRADE NOT FOUND" + + grade_text = "" + if race_grade > 300: + grade_text = "OP/Pre-OP" + elif race_grade > 100: + grade_text = "G2/G3" + else: + grade_text = "G1" + + return f"{grade_text} {self.EVENT_ID_TO_POS_STRING[event_id]}" + def handle_response(self, message): data = self.load_response(message) @@ -377,8 +412,8 @@ def handle_response(self, message): # Concert Theater if "live_theater_save_info_array" in data: if self.screen_state_handler: - new_state = ScreenState(self.threader.screenstate) - new_state.location = Location.THEATER + new_state = screenstate_utils.ss.ScreenState(self.threader.screenstate) + new_state.location = screenstate_utils.ss.Location.THEATER new_state.main = "Concert Theater" new_state.sub = "Vibing" @@ -386,13 +421,19 @@ def handle_response(self, message): return # Race starts. - if 'race_scenario' in data and 'race_start_info' in data and data['race_scenario']: + if self.training_tracker and 'race_scenario' in data and 'race_start_info' in data and data['race_scenario']: + self.previous_race_program_id = data['race_start_info']['program_id'] # Currently starting a race. Add packet to training tracker. logger.debug("Race packet received.") self.add_response_to_tracker(data) return + # Update history + if 'race_history' in data and data['race_history']: + self.previous_race_program_id = data['race_history'][-1]['program_id'] + + # Gametora if 'chara_info' in data: # Inside training run. @@ -406,27 +447,15 @@ def handle_response(self, message): # Training info outfit_id = data['chara_info']['card_id'] - chara_id = int(str(outfit_id)[:-2]) supports = [card_data['support_card_id'] for card_data in data['chara_info']['support_card_array']] scenario_id = data['chara_info']['scenario_id'] # Training stats if self.screen_state_handler: - new_state = ScreenState(self.threader.screenstate) - - new_state.location = Location.TRAINING - - new_state.main = f"Training - {util.turn_to_string(data['chara_info']['turn'])}" - new_state.sub = f"{data['chara_info']['speed']} {data['chara_info']['stamina']} {data['chara_info']['power']} {data['chara_info']['guts']} {data['chara_info']['wiz']} | {data['chara_info']['skill_point']}" - - scenario_id = data['chara_info']['scenario_id'] - scenario_name = util.SCENARIO_DICT.get(scenario_id, None) - if not scenario_name: - logger.error(f"Scenario ID not found in scenario dict: {scenario_id}") - scenario_name = "You are now breathing manually." - new_state.set_chara(chara_id, outfit_id=outfit_id, small_text=scenario_name) - - self.screen_state_handler.carrotjuicer_state = new_state + if data.get('race_start_info', None): + self.screen_state_handler.carrotjuicer_state = screenstate_utils.make_training_race_state(data, self.threader.screenstate) + else: + self.screen_state_handler.carrotjuicer_state = screenstate_utils.make_training_state(data, self.threader.screenstate) if not self.browser or not self.browser.current_url.startswith("https://gametora.com/umamusume/training-event-helper"): logger.info("GT tab not open, opening tab") @@ -467,6 +496,11 @@ def handle_response(self, message): else: logger.debug("Trained character or support card detected") + # Check for after-race event. + if event_data['event_id'] in (7005, 7006, 7007): + logger.debug("After-race event detected.") + event_title = self.get_after_race_event_title(event_data['event_id']) + # Activate and scroll to the outcome. self.previous_element = self.browser.execute_script( """a = document.querySelectorAll("[class^='compatibility_viewer_item_']"); @@ -485,7 +519,7 @@ def handle_response(self, message): event_title ) if not self.previous_element: - logger.debug("Could not find event on GT page.") + logger.debug(f"Could not find event on GT page: {event_title} {event_data['story_id']}") self.browser.execute_script(""" if (arguments[0]) { // document.querySelector(".tippy-box").scrollIntoView({behavior:"smooth", block:"center"}); @@ -495,11 +529,14 @@ def handle_response(self, message): """, self.previous_element ) + + self.last_data = data except Exception: logger.error("ERROR IN HANDLING RESPONSE MSGPACK") logger.error(data) - logger.error(traceback.format_exc()) - util.show_warning_box("Uma Launcher: Error in response msgpack.", "This should not happen. You may contact the developer about this issue.") + exception_string = traceback.format_exc() + logger.error(exception_string) + util.show_warning_box("Uma Launcher: Error in response msgpack.", f"This should not happen. You may contact the developer about this issue.\n\n{exception_string}") # self.close_browser() def check_browser(self): @@ -517,10 +554,7 @@ def check_browser(self): def start_concert(self, music_id): logger.debug("Starting concert") - new_state = ScreenState(self.threader.screenstate) - new_state.location = Location.THEATER - new_state.set_music(music_id) - self.screen_state_handler.carrotjuicer_state = new_state + self.screen_state_handler.carrotjuicer_state = screenstate_utils.make_concert_state(music_id, self.threader.screenstate) return def handle_request(self, message): @@ -559,8 +593,9 @@ def handle_request(self, message): except Exception: logger.error("ERROR IN HANDLING REQUEST MSGPACK") logger.error(data) - logger.error(traceback.format_exc()) - util.show_warning_box("Uma Launcher: Error in request msgpack.", "This should not happen. You may contact the developer about this issue.") + exception_string = traceback.format_exc() + logger.error(exception_string) + util.show_warning_box("Uma Launcher: Error in request msgpack.", f"This should not happen. You may contact the developer about this issue.\n\n{exception_string}") # self.close_browser() diff --git a/umalauncher/constants.py b/umalauncher/constants.py new file mode 100644 index 0000000..b6dcb22 --- /dev/null +++ b/umalauncher/constants.py @@ -0,0 +1,118 @@ +SCENARIO_DICT = { + 1: "URA Finals", + 2: "Aoharu Cup", + 3: "Grand Live", + 4: "Make a New Track", + 5: "Grand Masters", +} + +MOTIVATION_DICT = { + 5: "Very High", + 4: "High", + 3: "Normal", + 2: "Low", + 1: "Very Low" +} + +SUPPORT_CARD_RARITY_DICT = { + 1: "R", + 2: "SR", + 3: "SSR" +} + +SUPPORT_CARD_TYPE_DICT = { + (101, 1): "speed", + (105, 1): "stamina", + (102, 1): "power", + (103, 1): "guts", + (106, 1): "wiz", + (0, 2): "friend", + (0, 3): "group" +} + +SUPPORT_CARD_TYPE_DISPLAY_DICT = { + "speed": "Speed", + "stamina": "Stamina", + "power": "Power", + "guts": "Guts", + "wiz": "Wisdom", + "friend": "Friend", + "group": "Group" +} + +SUPPORT_TYPE_TO_COMMAND_IDS = { + "speed": [101, 601], + "stamina": [105, 602], + "power": [102, 603], + "guts": [103, 604], + "wiz": [106, 605], + "friend": [], + "group": [] +} + +COMMAND_ID_TO_KEY = { + 101: "speed", + 105: "stamina", + 102: "power", + 103: "guts", + 106: "wiz", + 601: "speed", + 602: "stamina", + 603: "power", + 604: "guts", + 605: "wiz" +} + +TARGET_TYPE_TO_KEY = { + 1: "speed", + 2: "stamina", + 3: "power", + 4: "guts", + 5: "wiz" +} + +MONTH_DICT = { + 1: 'January', + 2: 'February', + 3: 'March', + 4: 'April', + 5: 'May', + 6: 'June', + 7: 'July', + 8: 'August', + 9: 'September', + 10: 'October', + 11: 'November', + 12: 'December' +} + +GL_TOKEN_LIST = [ + 'dance', + 'passion', + 'vocal', + 'visual', + 'mental' +] + +ORIENTATION_DICT = { + True: 'portrait', + False: 'landscape', + 'portrait': True, + 'landscape': False, +} + +# Request packets contain keys that should not be kept for privacy reasons. +REQUEST_KEYS_TO_BE_REMOVED = [ + "device", + "device_id", + "device_name", + "graphics_device_name", + "ip_address", + "platform_os_version", + "carrier", + "keychain", + "locale", + "button_info", + "dmm_viewer_id", + "dmm_onetime_token", +] \ No newline at end of file diff --git a/umalauncher/helper_table.py b/umalauncher/helper_table.py index b9f323a..67c186a 100644 --- a/umalauncher/helper_table.py +++ b/umalauncher/helper_table.py @@ -2,6 +2,7 @@ from loguru import logger import mdb import util +import constants class TrainingPartner(): @@ -72,7 +73,7 @@ def create_helper_elements(self, data) -> str: all_commands[command['command_id']]['performance_inc_dec_info_array'] = command['performance_inc_dec_info_array'] for command in all_commands.values(): - if command['command_id'] not in util.COMMAND_ID_TO_KEY: + if command['command_id'] not in constants.COMMAND_ID_TO_KEY: continue eval_dict = { @@ -81,7 +82,7 @@ def create_helper_elements(self, data) -> str: } level = command['level'] failure_rate = command['failure_rate'] - gained_stats = {stat_type: 0 for stat_type in set(util.COMMAND_ID_TO_KEY.values())} + gained_stats = {stat_type: 0 for stat_type in set(constants.COMMAND_ID_TO_KEY.values())} skillpt = 0 total_bond = 0 useful_bond = 0 @@ -90,7 +91,7 @@ def create_helper_elements(self, data) -> str: for param in command['params_inc_dec_info_array']: if param['target_type'] < 6: - gained_stats[util.TARGET_TYPE_TO_KEY[param['target_type']]] += param['value'] + gained_stats[constants.TARGET_TYPE_TO_KEY[param['target_type']]] += param['value'] elif param['target_type'] == 30: skillpt += param['value'] elif param['target_type'] == 10: @@ -135,7 +136,7 @@ def calc_bond_gain(partner_id, amount): if partner_id <= 6: support_card_id = data['chara_info']['support_card_array'][partner_id - 1]['support_card_id'] support_card_data = mdb.get_support_card_dict()[support_card_id] - support_card_type = util.SUPPORT_CARD_TYPE_DICT[(support_card_data[1], support_card_data[2])] + support_card_type = constants.SUPPORT_CARD_TYPE_DICT[(support_card_data[1], support_card_data[2])] if support_card_type in ("group", "friend"): return 0 @@ -163,7 +164,7 @@ def calc_bond_gain(partner_id, amount): support_id = data['chara_info']['support_card_array'][training_partner_id - 1]['support_card_id'] support_data = mdb.get_support_card_dict()[support_id] support_card_type = mdb.get_support_card_type(support_data) - if support_card_type not in ("group", "friend") and training_partner.starting_bond >= 80 and command['command_id'] in util.SUPPORT_TYPE_TO_COMMAND_IDS[support_card_type]: + if support_card_type not in ("group", "friend") and training_partner.starting_bond >= 80 and command['command_id'] in constants.SUPPORT_TYPE_TO_COMMAND_IDS[support_card_type]: rainbow_count += 1 elif support_card_type == "group" and util.get_group_support_id_to_passion_zone_effect_id_dict()[support_id] in data['chara_info']['chara_effect_id_array']: rainbow_count += 1 @@ -199,13 +200,13 @@ def calc_bond_gain(partner_id, amount): total_bond += sum(tip_gains_total) useful_bond += sum(tip_gains_useful) - current_stats = data['chara_info'][util.COMMAND_ID_TO_KEY[command['command_id']]] + current_stats = data['chara_info'][constants.COMMAND_ID_TO_KEY[command['command_id']]] - gl_tokens = {token_type: 0 for token_type in util.gl_token_list} + gl_tokens = {token_type: 0 for token_type in constants.GL_TOKEN_LIST} # Grand Live tokens if 'live_data_set' in data: for token_data in command['performance_inc_dec_info_array']: - gl_tokens[util.gl_token_list[token_data['performance_type']-1]] += token_data['value'] + gl_tokens[constants.GL_TOKEN_LIST[token_data['performance_type']-1]] += token_data['value'] command_info[command['command_id']] = { 'scenario_id': data['chara_info']['scenario_id'], @@ -226,9 +227,9 @@ def calc_bond_gain(partner_id, amount): # Simplify everything down to a dict with only the keys we care about. # No distinction between normal and summer training. command_info = { - util.COMMAND_ID_TO_KEY[command_id]: command_info[command_id] + constants.COMMAND_ID_TO_KEY[command_id]: command_info[command_id] for command_id in command_info - if command_id in util.COMMAND_ID_TO_KEY + if command_id in constants.COMMAND_ID_TO_KEY } # Grand Masters Fragments diff --git a/umalauncher/helper_table_elements.py b/umalauncher/helper_table_elements.py index 43065cc..5fc2905 100644 --- a/umalauncher/helper_table_elements.py +++ b/umalauncher/helper_table_elements.py @@ -2,6 +2,7 @@ from loguru import logger import gui import util +import constants TABLE_HEADERS = [ "Facility", @@ -239,7 +240,7 @@ def generate_gl_table(self, main_info): top_row = [] bottom_row = [] - for token_type in util.gl_token_list: + for token_type in constants.GL_TOKEN_LIST: top_row.append(f"") bottom_row.append(f"{main_info['gl_stats'][token_type]}") diff --git a/umalauncher/mdb.py b/umalauncher/mdb.py index 1df9d59..a3bff00 100644 --- a/umalauncher/mdb.py +++ b/umalauncher/mdb.py @@ -2,6 +2,7 @@ import os from loguru import logger import util +import constants DB_PATH = os.path.expandvars("%userprofile%\\appdata\\locallow\\Cygames\\umamusume\\master\\master.mdb") SUPPORT_CARD_DICT = {} @@ -15,7 +16,7 @@ def __exit__(self, type, value, traceback): self.conn.close() def create_support_card_string(rarity, command_id, support_card_type, chara_id): - return f"{util.SUPPORT_CARD_RARITY_DICT[rarity]} {util.SUPPORT_CARD_TYPE_DISPLAY_DICT[util.SUPPORT_CARD_TYPE_DICT[(command_id, support_card_type)]]} {util.get_character_name_dict()[chara_id]}" + return f"{constants.SUPPORT_CARD_RARITY_DICT[rarity]} {constants.SUPPORT_CARD_TYPE_DISPLAY_DICT[constants.SUPPORT_CARD_TYPE_DICT[(command_id, support_card_type)]]} {util.get_character_name_dict()[chara_id]}" def get_event_title(story_id): with Connection() as (_, cursor): @@ -115,15 +116,17 @@ def get_event_title_dict(): out[row[1]] = row[2] return out - +RACE_PROGRAM_NAME_DICT = None def get_race_program_name_dict(): - with Connection() as (_, cursor): - cursor.execute( - """SELECT s.id, t.text FROM single_mode_program s INNER JOIN text_data t ON s.race_instance_id = t."index" AND t.category = 28""" - ) - rows = cursor.fetchall() - - return {row[0]: row[1] for row in rows} + global RACE_PROGRAM_NAME_DICT + if not RACE_PROGRAM_NAME_DICT: + with Connection() as (_, cursor): + cursor.execute( + """SELECT s.id, t.text FROM single_mode_program s INNER JOIN text_data t ON s.race_instance_id = t."index" AND t.category = 28""" + ) + rows = cursor.fetchall() + RACE_PROGRAM_NAME_DICT = {row[0]: row[1] for row in rows} + return RACE_PROGRAM_NAME_DICT def get_skill_name_dict(): with Connection() as (_, cursor): @@ -173,7 +176,7 @@ def get_support_card_dict(): return SUPPORT_CARD_DICT def get_support_card_type(support_data): - return util.SUPPORT_CARD_TYPE_DICT[(support_data[1], support_data[2])] + return constants.SUPPORT_CARD_TYPE_DICT[(support_data[1], support_data[2])] def get_support_card_string_dict(): support_card_dict = get_support_card_dict() @@ -216,4 +219,17 @@ def get_group_card_effect_ids(): if not rows: return [] - return rows \ No newline at end of file + return rows + +def get_program_id_grade(program_id): + with Connection() as (_, cursor): + cursor.execute( + """SELECT r.grade FROM single_mode_program smp JOIN race_instance ri on smp.race_instance_id = ri.id JOIN race r on ri.race_id = r.id WHERE smp.id = ?;""", + (program_id,) + ) + row = cursor.fetchone() + + if not row: + return None + + return row[0] \ No newline at end of file diff --git a/umalauncher/screenstate.py b/umalauncher/screenstate.py index f85f4f9..e95fd9b 100644 --- a/umalauncher/screenstate.py +++ b/umalauncher/screenstate.py @@ -23,7 +23,6 @@ class Location(Enum): THEATER = 2 TRAINING = 3 - class ScreenState: location = Location.MAIN_MENU main = "Launching game..." @@ -173,7 +172,7 @@ def get_screenshot(self): image = ImageGrab.grab(bbox=(x, y, x+x1, y+y1), all_screens=True) if util.is_debug: - image.save("screenshot.png", "PNG") + image.save(util.get_relative("screenshot.png"), "PNG") return image except Exception: logger.error("Couldn't get screenshot.") @@ -238,7 +237,9 @@ def run(self): carrotjuicer_handle = util.get_window_handle("Umapyoi", type=util.EXACT) if carrotjuicer_handle: logger.info("Attempting to minimize CarrotJuicer.") - success = util.show_window(carrotjuicer_handle, win32con.SW_MINIMIZE) + success1 = util.show_window(carrotjuicer_handle, win32con.SW_MINIMIZE) + success2 = util.hide_window_from_taskbar(carrotjuicer_handle) + success = success1 and success2 if not success: logger.error("Failed to minimize CarrotJuicer") else: diff --git a/umalauncher/screenstate_utils.py b/umalauncher/screenstate_utils.py new file mode 100644 index 0000000..105995e --- /dev/null +++ b/umalauncher/screenstate_utils.py @@ -0,0 +1,38 @@ +from loguru import logger +import screenstate as ss +import util +import constants +import mdb + +def _make_default_training_state(data, handler) -> ss.ScreenState: + new_state = ss.ScreenState(handler) + + new_state.location = ss.Location.TRAINING + + new_state.main = f"Training - {util.turn_to_string(data['chara_info']['turn'])}" + + outfit_id = data['chara_info']['card_id'] + chara_id = int(str(outfit_id)[:-2]) + scenario_id = data['chara_info']['scenario_id'] + scenario_name = constants.SCENARIO_DICT.get(scenario_id, None) + if not scenario_name: + logger.error(f"Scenario ID not found in scenario dict: {scenario_id}") + scenario_name = "You are now breathing manually." + new_state.set_chara(chara_id, outfit_id=outfit_id, small_text=scenario_name) + return new_state + +def make_training_state(data, handler) -> ss.ScreenState: + new_state = _make_default_training_state(data, handler) + new_state.sub = f"{data['chara_info']['speed']} {data['chara_info']['stamina']} {data['chara_info']['power']} {data['chara_info']['guts']} {data['chara_info']['wiz']} | {data['chara_info']['skill_point']}" + return new_state + +def make_training_race_state(data, handler) -> ss.ScreenState: + new_state = _make_default_training_state(data, handler) + new_state.sub = f"In race: {util.get_race_name_dict()[data['race_start_info']['program_id']]}" + return new_state + +def make_concert_state(music_id, handler) -> ss.ScreenState: + new_state = ss.ScreenState(handler) + new_state.location = ss.Location.THEATER + new_state.set_music(music_id) + return new_state \ No newline at end of file diff --git a/umalauncher/settings.py b/umalauncher/settings.py index 9edb173..f0bc48e 100644 --- a/umalauncher/settings.py +++ b/umalauncher/settings.py @@ -6,18 +6,12 @@ import traceback from loguru import logger import util +import constants import version import gui import helper_table_defaults as htd import helper_table_elements as hte -ORIENTATION_DICT = { - True: 'portrait', - False: 'landscape', - 'portrait': True, - 'landscape': False, -} - class Settings(): settings_file = "umasettings.json" @@ -54,7 +48,7 @@ class Settings(): def __init__(self, threader): self.threader = threader # Load settings on import. - if not os.path.exists(self.settings_file): + if not os.path.exists(util.get_relative(self.settings_file)): logger.warning("Settings file not found. Starting with default settings.") self.loaded_settings = self.default_settings self.save_settings() @@ -84,13 +78,13 @@ def make_user_choose_folder(self, setting, file_to_verify, title, error): sys.exit() def save_settings(self): - with open(self.settings_file, 'w', encoding='utf-8') as f: + with open(util.get_relative(self.settings_file), 'w', encoding='utf-8') as f: json.dump(self.loaded_settings, f, ensure_ascii=False, indent=2) def load_settings(self): logger.info("Loading settings file.") - with open(self.settings_file, 'r', encoding='utf-8') as f: + with open(util.get_relative(self.settings_file), 'r', encoding='utf-8') as f: try: self.loaded_settings = json.load(f) @@ -182,20 +176,20 @@ def set(self, key: str, value): def save_game_position(self, pos, portrait): if util.is_minimized(self.threader.screenstate.game_handle): - logger.warning(f"Game minimized, cannot save {ORIENTATION_DICT[portrait]} position: {pos}") + logger.warning(f"Game minimized, cannot save {constants.ORIENTATION_DICT[portrait]} position: {pos}") return if (pos[0] == -32000 and pos[1] == -32000): - logger.warning(f"Game minimized, cannot save {ORIENTATION_DICT[portrait]} position: {pos}") + logger.warning(f"Game minimized, cannot save {constants.ORIENTATION_DICT[portrait]} position: {pos}") return - orientation_key = ORIENTATION_DICT[portrait] + orientation_key = constants.ORIENTATION_DICT[portrait] self.loaded_settings['game_position'][orientation_key] = pos logger.info(f"Saving {orientation_key} position: {pos}") self.save_settings() def load_game_position(self, portrait): - orientation_key = ORIENTATION_DICT[portrait] + orientation_key = constants.ORIENTATION_DICT[portrait] return self.loaded_settings['game_position'][orientation_key] def get_browsers(self): diff --git a/umalauncher/threader.py b/umalauncher/threader.py index 529c96c..d2da040 100644 --- a/umalauncher/threader.py +++ b/umalauncher/threader.py @@ -8,10 +8,7 @@ training_tracker.training_csv_dialog(gzips) sys.exit() -from elevate import elevate -try: - elevate() -except OSError: +if not util.elevate(): util.show_error_box("Launch Error", "Uma Launcher needs administrator privileges to start.") sys.exit() diff --git a/umalauncher/training_tracker.py b/umalauncher/training_tracker.py index 7ba227c..7ef86f3 100644 --- a/umalauncher/training_tracker.py +++ b/umalauncher/training_tracker.py @@ -19,29 +19,16 @@ import gui import mdb import util +import constants from external import race_data_parser class TrainingTracker(): - request_remove_keys = [ - "viewer_id", - "device", - "device_id", - "device_name", - "graphics_device_name", - "ip_address", - "platform_os_version", - "carrier", - "keychain", - "locale", - "button_info", - "dmm_viewer_id", - "dmm_onetime_token", - ] - def __init__(self, training_id: str, card_id: int=None, training_log_folder: str="training_logs", full_path: str=None): self.full_path=full_path + if not training_log_folder: + training_log_folder = util.get_relative("training_logs") self.training_log_folder = training_log_folder self.card_id = card_id @@ -74,7 +61,7 @@ def add_request(self, request: dict): request['_direction'] = 0 # Remove keys that should not be saved - for key in self.request_remove_keys: + for key in constants.REQUEST_KEYS_TO_BE_REMOVED: if key in request: del request[key] @@ -389,7 +376,7 @@ def to_csv_list(self): prev_resp = resp # Write to CSV - scenario_str = util.SCENARIO_DICT.get(self.scenario_id, 'Unknown') + scenario_str = constants.SCENARIO_DICT.get(self.scenario_id, 'Unknown') chara_str = f"{self.chara_names_dict.get(self.chara_id, 'Unknown')} {self.outfit_name_dict[self.card_id]}" support_1_str = f"{self.support_cards[0]['support_card_id']} - {self.support_card_string_dict[self.support_cards[0]['support_card_id']]}" support_2_str = f"{self.support_cards[1]['support_card_id']} - {self.support_card_string_dict[self.support_cards[1]['support_card_id']]}" @@ -418,7 +405,7 @@ def to_csv_list(self): ("INT", lambda x: x.wisdom), ("SKLPT", lambda x: x.skill_pt), ("ERG", lambda x: x.energy), - ("MOT", lambda x: util.MOTIVATION_DICT.get(x.motivation, "Unknown")), + ("MOT", lambda x: constants.MOTIVATION_DICT.get(x.motivation, "Unknown")), ("FAN", lambda x: x.fans), ("ΔSPD", lambda x: x.dspeed), @@ -780,11 +767,12 @@ def training_csv_dialog(training_paths=None): if training_paths is None: try: training_paths, _, _ = win32gui.GetOpenFileNameW( - InitialDir="training_logs", + InitialDir=util.get_relative("training_logs"), Title="Select training log(s)", Flags=win32con.OFN_ALLOWMULTISELECT | win32con.OFN_FILEMUSTEXIST | win32con.OFN_EXPLORER | win32con.OFN_NOCHANGEDIR, DefExt="gz", - Filter="Training logs (*.gz)\0*.gz\0\0" + Filter="Training logs (*.gz)\0*.gz\0\0", + MaxFile=2147483647 ) # os.chdir(cwd_before) @@ -793,7 +781,11 @@ def training_csv_dialog(training_paths=None): dir_path = training_paths[0] training_paths = [os.path.join(dir_path, training_path) for training_path in training_paths[1:]] - except util.pywinerror: + except util.pywinerror as e: + if e.winerror == 12291: + # Ran out of buffer space + util.show_error_box("Error", "Too many files selected. / File names too long.") + return # os.chdir(cwd_before) util.show_error_box("Error", "No file(s) selected.") return @@ -807,7 +799,7 @@ def training_csv_dialog(training_paths=None): try: output_file_path, _, _ = win32gui.GetSaveFileNameW( - InitialDir="training_logs", + InitialDir=util.get_relative("training_logs"), Title="Select output file", Flags=win32con.OFN_EXPLORER | win32con.OFN_OVERWRITEPROMPT | win32con.OFN_PATHMUSTEXIST | win32con.OFN_NOCHANGEDIR, File="training", diff --git a/umalauncher/ui/new_preset_dialog.ui b/umalauncher/ui/new_preset_dialog.ui index 9c126ce..0058ee3 100644 --- a/umalauncher/ui/new_preset_dialog.ui +++ b/umalauncher/ui/new_preset_dialog.ui @@ -7,7 +7,7 @@ 0 0 321 - 91 + 121 @@ -43,7 +43,7 @@ 230 - 60 + 90 81 23 @@ -56,7 +56,7 @@ 140 - 60 + 90 81 23 @@ -65,6 +65,40 @@ OK + + + + 10 + 60 + 121 + 21 + + + + Copy existing: + + + + + false + + + + 108 + 60 + 201 + 22 + + + + Default + + + + Default + + + diff --git a/umalauncher/util.py b/umalauncher/util.py index bdc5278..f38cedc 100644 --- a/umalauncher/util.py +++ b/umalauncher/util.py @@ -2,17 +2,62 @@ import sys import base64 import io +import ctypes +import win32event +from win32com.shell.shell import ShellExecuteEx +from win32com.shell import shellcon +import win32con from PIL import Image from loguru import logger +import constants -unpack_dir = os.getcwd() +relative_dir = os.path.abspath(os.getcwd()) +unpack_dir = relative_dir is_script = True if hasattr(sys, "_MEIPASS"): unpack_dir = sys._MEIPASS is_script = False - os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) + relative_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + os.chdir(relative_dir) is_debug = is_script +def get_relative(relative_path): + return os.path.join(relative_dir, relative_path) + +def get_asset(asset_path): + return os.path.join(unpack_dir, asset_path) + +def elevate(): + """Elevate the script if it's not already running as admin. + Based on PyUAC https://github.com/Preston-Landers/pyuac + """ + + if ctypes.windll.shell32.IsUserAnAdmin(): + return True + + # Elevate the script. + proc_info = None + executable = sys.executable + params = " ".join(sys.argv if is_script else sys.argv[1:]) # Add the script path if it's a script. + try: + proc_info = ShellExecuteEx( + nShow=win32con.SW_SHOWNORMAL, + fMask=shellcon.SEE_MASK_NOCLOSEPROCESS | shellcon.SEE_MASK_NO_CONSOLE, + lpVerb="runas", + lpFile=executable, + lpParameters=params, + ) + except Exception as e: + return False + + if not proc_info: + return False + + handle = proc_info["hProcess"] + _ = win32event.WaitForSingleObject(handle, win32event.INFINITE) + sys.exit(1) + + def log_reset(): logger.remove() if is_script: @@ -21,12 +66,12 @@ def log_reset(): def log_set_info(): log_reset() - logger.add("log.log", rotation="1 week", compression="zip", retention="1 month", encoding='utf-8', level="INFO") + logger.add(get_relative("log.log"), rotation="1 week", compression="zip", retention="1 month", encoding='utf-8', level="INFO") return def log_set_trace(): log_reset() - logger.add("log.log", rotation="1 week", compression="zip", retention="1 month", encoding='utf-8', level="TRACE") + logger.add(get_relative("log.log"), rotation="1 week", compression="zip", retention="1 month", encoding='utf-8', level="TRACE") return if is_script: @@ -48,87 +93,13 @@ def log_set_trace(): window_handle = None -SCENARIO_DICT = { - 1: "URA Finals", - 2: "Aoharu Cup", - 3: "Grand Live", - 4: "Make a New Track", - 5: "Grand Masters", -} - -MOTIVATION_DICT = { - 5: "Very High", - 4: "High", - 3: "Normal", - 2: "Low", - 1: "Very Low" -} - -SUPPORT_CARD_RARITY_DICT = { - 1: "R", - 2: "SR", - 3: "SSR" -} - -SUPPORT_CARD_TYPE_DICT = { - (101, 1): "speed", - (105, 1): "stamina", - (102, 1): "power", - (103, 1): "guts", - (106, 1): "wiz", - (0, 2): "friend", - (0, 3): "group" -} - -SUPPORT_CARD_TYPE_DISPLAY_DICT = { - "speed": "Speed", - "stamina": "Stamina", - "power": "Power", - "guts": "Guts", - "wiz": "Wisdom", - "friend": "Friend", - "group": "Group" -} - -SUPPORT_TYPE_TO_COMMAND_IDS = { - "speed": [101, 601], - "stamina": [105, 602], - "power": [102, 603], - "guts": [103, 604], - "wiz": [106, 605], - "friend": [], - "group": [] -} - -COMMAND_ID_TO_KEY = { - 101: "speed", - 105: "stamina", - 102: "power", - 103: "guts", - 106: "wiz", - 601: "speed", - 602: "stamina", - 603: "power", - 604: "guts", - 605: "wiz" -} - -TARGET_TYPE_TO_KEY = { - 1: "speed", - 2: "stamina", - 3: "power", - 4: "guts", - 5: "wiz" -} - -def get_asset(asset_path): - return os.path.join(unpack_dir, asset_path) def get_width_from_height(height, portrait): if portrait: return math.ceil((height * 0.5626065430) - 6.2123937177) return math.ceil((height * 1.7770777107) - 52.7501897551) + def _show_alert_box(error, message, icon): app = gui.UmaApp() app.run(gui.UmaInfoPopup(error, message, icon)) @@ -205,21 +176,6 @@ def similar_color(col1: tuple[int,int,int], col2: tuple[int,int,int], threshold: total_diff += abs(col1[i] - col2[i]) return total_diff < threshold -MONTH_DICT = { - 1: 'January', - 2: 'February', - 3: 'March', - 4: 'April', - 5: 'May', - 6: 'June', - 7: 'July', - 8: 'August', - 9: 'September', - 10: 'October', - 11: 'November', - 12: 'December' -} - def turn_to_string(turn): turn = turn - 1 @@ -230,7 +186,7 @@ def turn_to_string(turn): month = int(turn) % 12 + 1 year = math.floor(turn / 12) + 1 - return f"Y{year}, {'Late' if second_half else 'Early'} {MONTH_DICT[month]}" + return f"Y{year}, {'Late' if second_half else 'Early'} {constants.MONTH_DICT[month]}" def get_window_rect(*args, **kwargs): try: @@ -264,6 +220,17 @@ def show_window(*args, **kwargs): except pywinerror: return False +def hide_window_from_taskbar(window_handle): + try: + style = win32gui.GetWindowLong(window_handle, win32con.GWL_EXSTYLE) + style |= win32con.WS_EX_TOOLWINDOW + win32gui.ShowWindow(window_handle, win32con.SW_HIDE) + win32gui.SetWindowLong(window_handle, win32con.GWL_EXSTYLE, style) + return True + except pywinerror: + return False + + def is_minimized(handle): try: tup = win32gui.GetWindowPlacement(handle) @@ -275,7 +242,6 @@ def is_minimized(handle): return True downloaded_chara_dict = None - def get_character_name_dict(): global downloaded_chara_dict @@ -311,6 +277,24 @@ def get_outfit_name_dict(): downloaded_outfit_dict = outfit_dict return downloaded_outfit_dict +downloaded_race_name_dict = None +def get_race_name_dict(): + global downloaded_race_name_dict + + if not downloaded_race_name_dict: + race_name_dict = mdb.get_race_program_name_dict() + logger.info("Requesting race names from umapyoi.net") + response = requests.get("https://umapyoi.net/api/v1/race_program") + if not response.ok: + show_warning_box("Uma Launcher: Internet error.", "Cannot download the race names from umapyoi.net for the Discord Rich Presence. Please check your internet connection.") + return race_name_dict + + for race_program in response.json(): + race_name_dict[race_program['id']] = race_program['name'] + + downloaded_race_name_dict = race_name_dict + return downloaded_race_name_dict + def create_gametora_helper_url(card_id, scenario_id, support_ids): support_ids = list(map(str, support_ids)) return f"https://gametora.com/umamusume/training-event-helper?deck={np.base_repr(int(str(card_id) + str(scenario_id)), 36)}-{np.base_repr(int(support_ids[0] + support_ids[1] + support_ids[2]), 36)}-{np.base_repr(int(support_ids[3] + support_ids[4] + support_ids[5]), 36)}".lower() @@ -345,14 +329,6 @@ def get_gm_fragment_dict(): return gm_fragment_dict -gl_token_list = [ - 'dance', - 'passion', - 'vocal', - 'visual', - 'mental' -] - gl_token_dict = None def get_gl_token_dict(): global gl_token_dict diff --git a/umalauncher/version.py b/umalauncher/version.py index a09ee22..162d496 100644 --- a/umalauncher/version.py +++ b/umalauncher/version.py @@ -11,7 +11,7 @@ import util import gui -VERSION = "1.4.3" +VERSION = "1.4.4" def parse_version(version_string: str): """Convert version string to tuple.""" @@ -145,8 +145,8 @@ def auto_update(umasettings, script_version, skip_version): app.run(gui.UmaUpdatePopup(app, update_object)) logger.debug("Update window closed: Update failed.") - if os.path.exists("update.tmp"): - os.remove("update.tmp") + if os.path.exists(util.get_relative("update.tmp")): + os.remove(util.get_relative("update.tmp")) util.show_error_box("Update failed.", "Could not update. Please check your internet connection.
Uma Launcher will now close.") return False @@ -174,7 +174,7 @@ def run(self): urllib.request.urlretrieve(download_url, "UmaLauncher.exe_") # Start a process that starts the new exe. logger.info("Download complete, now trying to open the new launcher.") - open("update.tmp", "wb").close() + open(util.get_relative("update.tmp"), "wb").close() sub = subprocess.Popen("taskkill /F /IM UmaLauncher.exe && move /y .\\UmaLauncher.exe .\\UmaLauncher.old && move /y .\\UmaLauncher.exe_ .\\UmaLauncher.exe && .\\UmaLauncher.exe", shell=True) while True: # Check if subprocess is still running