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