diff --git a/requirements.txt b/requirements.txt index 77c4e83..90e40a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ outcome==1.2.0 packaging==23.0 pathvalidate==3.0.0 pefile==2023.2.7 -Pillow>=10.0.1 +Pillow==10.1.0 protobuf==4.22.1 psutil==5.9.5 pycparser==2.21 @@ -47,14 +47,14 @@ pywin32==306 pywin32-ctypes==0.2.2 PyYAML==6.0 requests==2.31.0 -selenium==4.8.2 +selenium==4.15.2 six==1.16.0 sniffio==1.3.0 sortedcontainers==2.4.0 soupsieve==2.4 trio==0.22.0 trio-websocket==0.9.2 -urllib3>=1.26.18 -Werkzeug>=3.0.1 +urllib3==1.26.18 +Werkzeug==3.0.1 win32-setctime==1.1.0 wsproto==1.2.0 diff --git a/umalauncher/carrotjuicer.py b/umalauncher/carrotjuicer.py index 40d7842..b00c815 100644 --- a/umalauncher/carrotjuicer.py +++ b/umalauncher/carrotjuicer.py @@ -638,7 +638,11 @@ def run(self): while not self.should_stop: time.sleep(0.25) - if not self.threader.settings["s_enable_carrotjuicer"]: + if not self.threader.settings["s_enable_carrotjuicer"] or not self.threader.settings['s_enable_browser']: + if self.browser and self.browser.alive(): + self.browser.quit() + if self.skill_browser and self.skill_browser.alive(): + self.skill_browser.quit() continue if self.browser and self.browser.alive(): diff --git a/umalauncher/constants.py b/umalauncher/constants.py index e671d71..d5410f2 100644 --- a/umalauncher/constants.py +++ b/umalauncher/constants.py @@ -143,35 +143,35 @@ 30000: "Platinum 4" } -SCOUTING_SCORE_TO_RANK_DICT = { - 0: "No rank", - 60000: "E", - 63000: "E1", - 66000: "E2", - 69000: "E3", - 72000: "D", - 76000: "D1", - 80000: "D2", - 85000: "D3", - 90000: "C", - 95000: "C1", - 100000: "C2", - 105000: "C3", - 110000: "B", - 115000: "B1", - 120000: "B2", - 125000: "B3", - 130000: "A", - 135000: "A1", - 140000: "A2", - 145000: "A3", - 150000: "A4", - 155000: "A5", - 160000: "S", - 165000: "S1", - 170000: "S2", - 180000: "S3", - 190000: "S4", - 200000: "S5", - 210000: "SS" -} \ No newline at end of file +SCOUTING_RANK_LIST = [ + "No rank", + "E", + "E1", + "E2", + "E3", + "D", + "D1", + "D2", + "D3", + "C", + "C1", + "C2", + "C3", + "B", + "B1", + "B2", + "B3", + "A", + "A1", + "A2", + "A3", + "A4", + "A5", + "S", + "S1", + "S2", + "S3", + "S4", + "S5", + "SS" +] \ No newline at end of file diff --git a/umalauncher/gui.py b/umalauncher/gui.py index 166217f..efd9d89 100644 --- a/umalauncher/gui.py +++ b/umalauncher/gui.py @@ -586,6 +586,9 @@ def init_ui(self, settings_var, tab=" General", window_title="Change options", * self.load_settings() + self.verticalSpacer = qtw.QSpacerItem(0, 0, qtw.QSizePolicy.Minimum, qtw.QSizePolicy.Expanding) + self.verticalLayout.addItem(self.verticalSpacer) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) self.btn_restore = qtw.QPushButton(self) @@ -662,6 +665,18 @@ def save_settings(self): return True def add_group_box(self, setting): + # If the setting is a divider, add a horizontal line. + if setting.type == se.SettingType.DIVIDER: + line = qtw.QFrame(self.scrollAreaWidgetContents) + line.setObjectName(f"line_{setting.name}") + line.setMinimumHeight(16) + line.setLineWidth(0) + line.setMidLineWidth(2) + line.setFrameShape(qtw.QFrame.HLine) + line.setFrameShadow(qtw.QFrame.Sunken) + return line, None + + grp_setting = qtw.QGroupBox(self.scrollAreaWidgetContents) grp_setting.setObjectName(f"grp_setting_{setting.name}") sizePolicy = qtw.QSizePolicy(qtw.QSizePolicy.Preferred, qtw.QSizePolicy.Fixed) @@ -691,30 +706,30 @@ def add_group_box(self, setting): horizontalLayout.addWidget(lbl_setting_description) - input_widgets = [] + input_widgets = [None] value_func = None - if setting.type == se.SettingType.MESSAGE: - input_widgets = [None] - elif setting.type == se.SettingType.BOOL: - input_widgets, value_func = self.add_checkbox(setting, grp_setting) - elif setting.type == se.SettingType.INT: - input_widgets, value_func = self.add_spinbox(setting, grp_setting) - elif setting.type == se.SettingType.STRING: - input_widgets, value_func = self.add_lineedit(setting, grp_setting) - elif setting.type == se.SettingType.COMBOBOX: - input_widgets, value_func = self.add_combobox(setting, grp_setting) - elif setting.type == se.SettingType.COLOR: - input_widgets, value_func = self.add_colorpicker(setting, grp_setting) - elif setting.type == se.SettingType.RADIOBUTTONS: - input_widgets, value_func = self.add_radiobuttons(setting, grp_setting) - elif setting.type == se.SettingType.FILEDIALOG: - input_widgets, value_func = self.add_filedialog(setting, grp_setting) - elif setting.type == se.SettingType.FOLDERDIALOG: - input_widgets, value_func = self.add_folderdialog(setting, grp_setting) - elif setting.type == se.SettingType.XYWHSPINBOXES: - input_widgets, value_func = self.add_multi_spinboxes(setting, grp_setting, ['Left', 'Top', 'Width', 'Height']) - elif setting.type == se.SettingType.LRTBSPINBOXES: - input_widgets, value_func = self.add_multi_spinboxes(setting, grp_setting, ['Left', 'Right', 'Top', 'Bottom']) + + match setting.type: + case se.SettingType.BOOL: + input_widgets, value_func = self.add_checkbox(setting, grp_setting) + case se.SettingType.INT: + input_widgets, value_func = self.add_spinbox(setting, grp_setting) + case se.SettingType.STRING: + input_widgets, value_func = self.add_lineedit(setting, grp_setting) + case se.SettingType.COMBOBOX: + input_widgets, value_func = self.add_combobox(setting, grp_setting) + case se.SettingType.COLOR: + input_widgets, value_func = self.add_colorpicker(setting, grp_setting) + case se.SettingType.RADIOBUTTONS: + input_widgets, value_func = self.add_radiobuttons(setting, grp_setting) + case se.SettingType.FILEDIALOG: + input_widgets, value_func = self.add_filedialog(setting, grp_setting) + case se.SettingType.FOLDERDIALOG: + input_widgets, value_func = self.add_folderdialog(setting, grp_setting) + case se.SettingType.XYWHSPINBOXES: + input_widgets, value_func = self.add_multi_spinboxes(setting, grp_setting, ['Left', 'Top', 'Width', 'Height']) + case se.SettingType.LRTBSPINBOXES: + input_widgets, value_func = self.add_multi_spinboxes(setting, grp_setting, ['Left', 'Right', 'Top', 'Bottom']) if not input_widgets: logger.debug(f"{setting.type} not implemented for {setting.name}") diff --git a/umalauncher/horsium.py b/umalauncher/horsium.py index 0490efd..685e237 100644 --- a/umalauncher/horsium.py +++ b/umalauncher/horsium.py @@ -21,41 +21,86 @@ OLD_DRIVERS = [] -def firefox_setup(helper_url): - firefox_service = FirefoxService() +def firefox_setup(helper_url, settings): + driver_path = None + if settings['s_enable_browser_override']: + new_path = settings['s_browser_custom_driver'] + if new_path: + driver_path = new_path + + firefox_service = FirefoxService(executable_path=driver_path) firefox_service.creation_flags = CREATE_NO_WINDOW profile = webdriver.FirefoxProfile(util.get_asset("ff_profile")) options = webdriver.FirefoxOptions() - browser = webdriver.Firefox(service=firefox_service, firefox_profile=profile, options=options) + options.profile = profile + + binary_path = None + + if settings['s_enable_browser_override']: + binary_path = settings['s_browser_custom_binary'] + if binary_path: + options.binary_location = binary_path + + logger.debug(f"Firefox driver path: {driver_path}") + logger.debug(f"Firefox binary path: {binary_path}") + + browser = webdriver.Firefox(service=firefox_service, options=options) browser.get(helper_url) return browser -def chromium_setup(service, options_class, driver_class, profile, helper_url): +def chromium_setup(service, options_class, driver_class, profile, helper_url, settings, binary_path=None): service.creation_flags = CREATE_NO_WINDOW options = options_class() + + if binary_path: + options.binary_location = binary_path + options.add_argument("--user-data-dir=" + str(util.get_asset(profile))) - options.add_argument("--app=" + helper_url) options.add_argument("--remote-debugging-port=9222") options.add_argument("--new-window") + + if not settings['s_enable_browser_override']: + options.add_argument("--app=" + helper_url) + browser = driver_class(service=service, options=options) + + if settings['s_enable_browser_override']: + browser.get(helper_url) + return browser -def chrome_setup(helper_url): +def chrome_setup(helper_url, settings): + driver_path = None + if settings['s_enable_browser_override']: + new_path = settings['s_browser_custom_driver'] + if new_path: + driver_path = new_path + + binary_path = None + if settings['s_enable_browser_override']: + binary_path = settings['s_browser_custom_binary'] + + logger.debug(f"Chrome driver path: {driver_path}") + logger.debug(f"Chrome binary path: {binary_path}") + return chromium_setup( - service=ChromeService(), + service=ChromeService(executable_path=driver_path) if driver_path else ChromeService(), options_class=webdriver.ChromeOptions, driver_class=webdriver.Chrome, profile="chr_profile", - helper_url=helper_url + helper_url=helper_url, + settings=settings, + binary_path=binary_path ) -def edge_setup(helper_url): +def edge_setup(helper_url, settings): return chromium_setup( service=EdgeService(), options_class=webdriver.EdgeOptions, driver_class=webdriver.Edge, profile="edg_profile", - helper_url=helper_url + helper_url=helper_url, + settings=settings ) BROWSER_LIST = { @@ -77,24 +122,34 @@ class BrowserWindow: def __init__(self, url, threader, rect=None, run_at_launch=None): self.url = url self.threader = threader + self.settings = threader.settings self.driver: RemoteWebDriver = None self.active_tab_handle = None self.last_window_rect = {'x': rect[0], 'y': rect[1], 'width': rect[2], 'height': rect[3]} if rect else None self.run_at_launch = run_at_launch self.browser_name = "Auto" + self.latest_error = "" self.ensure_tab_open() def init_browser(self) -> RemoteWebDriver: driver = None + if self.settings['s_enable_browser_override']: + selection = self.settings['s_custom_browser_type'] + else: + selection = self.settings['s_selected_browser'] browser_name = [ browser - for browser, selected in self.threader.settings['s_selected_browser'].items() + for browser, selected in selection.items() if selected ][0] self.browser_name = browser_name + # Hack to convert override Chromium to Chrome + if browser_name == 'Other (Chromium)': + browser_name = 'Chrome' + browser_list = [] if browser_name == "Auto": browser_list = BROWSER_LIST.items() @@ -105,14 +160,15 @@ def init_browser(self) -> RemoteWebDriver: browser_name, browser_setup = browser_data try: logger.info("Attempting " + str(browser_setup.__name__)) - driver = browser_setup(self.url) + driver = browser_setup(self.url, self.settings) self.browser_name = browser_name break - except Exception: + except Exception as e: logger.error("Failed to start browser") logger.error(traceback.format_exc()) - if not driver: - util.show_warning_box("Uma Launcher: Unable to start browser.", "Selected webbrowser cannot be started.") + self.latest_error = traceback.format_exception_only(type(e), e)[-1] + # if not driver: + # util.show_warning_box("Uma Launcher: Unable to start browser.", "Selected webbrowser cannot be started.") return driver def alive(self): @@ -189,7 +245,7 @@ def wrapper(self, *args, **kwargs): if self.driver: return func(self, *args, **kwargs) - util.show_warning_box("Uma Launcher: Unable to reach browser.", "Webbrowser is unable to open.

If this problem persists, try restarting your computer
or selecting a different browser in the preferences.") + util.show_warning_box("Uma Launcher: Unable to reach browser.", f"Webbrowser is unable to open.

If this problem persists, try restarting your computer
or selecting a different browser in the preferences.

Extra info:
{self.latest_error}") return wrapper @ensure_focus diff --git a/umalauncher/mdb.py b/umalauncher/mdb.py index fdc0e5a..edf09d1 100644 --- a/umalauncher/mdb.py +++ b/umalauncher/mdb.py @@ -111,8 +111,23 @@ def _get_event_titles_default(story_id): return [None] return [row[0]] + +def convert_short_story_id(story_id): + with Connection() as (_, cursor): + cursor.execute( + """SELECT story_id FROM single_mode_story_data WHERE short_story_id = ? LIMIT 1""", + (story_id,) + ) + row = cursor.fetchone() + + if row is None: + return story_id + + return row[0] def get_event_titles(story_id, card_id): + story_id = convert_short_story_id(story_id) + str_event_title = str(story_id) if str_event_title.startswith("40"): @@ -303,6 +318,10 @@ def get_support_card_string_dict(force=False): global SUPPORT_CARD_STRING_DICT if force or not SUPPORT_CARD_STRING_DICT: support_card_dict = get_support_card_dict() + + # Forcefully clear the character name dict cache if needed. + util.get_character_name_dict(force=force) + SUPPORT_CARD_STRING_DICT.update({id: create_support_card_string(*data) for id, data in support_card_dict.items()}) return SUPPORT_CARD_STRING_DICT @@ -414,6 +433,28 @@ def get_skill_id_dict(force=False): return SKILL_ID_DICT +SCOUTING_SCORE_TO_RANK_DICT = {} +def get_scouting_score_to_rank_dict(force=False): + global SCOUTING_SCORE_TO_RANK_DICT + if force or not SCOUTING_SCORE_TO_RANK_DICT: + with Connection() as (_, cursor): + cursor.execute( + """SELECT team_min_value FROM team_building_rank""" + ) + rows = cursor.fetchall() + + tmp_dict = {} + for i, row in enumerate(rows): + min_score = row[0] + try: + rank = constants.SCOUTING_RANK_LIST[i] + except IndexError: + rank = constants.SCOUTING_RANK_LIST[-1] + tmp_dict[min_score] = rank + + SCOUTING_SCORE_TO_RANK_DICT.update(tmp_dict) + + return SCOUTING_SCORE_TO_RANK_DICT def get_card_inherent_skills(card_id, level=99): skills = [] @@ -483,6 +524,7 @@ def get_total_minigame_plushies(force=False): return 3 * (total_plushies + len(total_charas)) UPDATE_FUNCS = [ + get_chara_name_dict, get_event_title_dict, get_race_program_name_dict, get_skill_name_dict, @@ -491,9 +533,9 @@ def get_total_minigame_plushies(force=False): get_outfit_name_dict, get_support_card_dict, get_support_card_string_dict, - get_chara_name_dict, get_mant_item_string_dict, get_gl_lesson_dict, get_group_card_effect_ids, get_skill_id_dict, + get_scouting_score_to_rank_dict ] \ No newline at end of file diff --git a/umalauncher/screenstate.py b/umalauncher/screenstate.py index fa51805..d438320 100644 --- a/umalauncher/screenstate.py +++ b/umalauncher/screenstate.py @@ -112,6 +112,7 @@ class ScreenStateHandler(): game_handle = None carrotjuicer_closed = False + carrotjuicer_handle = None should_stop = False @@ -271,17 +272,30 @@ def run(self): self.vpn = None if not self.carrotjuicer_closed and self.threader.settings["s_hide_carrotjuicer"]: - carrotjuicer_handle = util.get_window_handle("Umapyoi", type=util.EXACT) - if carrotjuicer_handle: + self.carrotjuicer_handle = util.get_window_handle("Umapyoi", type=util.EXACT) + if self.carrotjuicer_handle: logger.info("Attempting to minimize CarrotJuicer.") - success1 = util.show_window(carrotjuicer_handle, win32con.SW_MINIMIZE) - success2 = util.hide_window_from_taskbar(carrotjuicer_handle) + success1 = util.show_window(self.carrotjuicer_handle, win32con.SW_MINIMIZE) + success2 = util.hide_window_from_taskbar(self.carrotjuicer_handle) success = success1 and success2 if not success: logger.error("Failed to minimize CarrotJuicer") else: self.carrotjuicer_closed = True time.sleep(0.25) + + if self.carrotjuicer_closed and not self.threader.settings["s_hide_carrotjuicer"]: + logger.debug(f"CarrotJuicer handle: {self.carrotjuicer_handle}") + if self.carrotjuicer_handle: + logger.info("Attempting to restore CarrotJuicer.") + success1 = util.show_window(self.carrotjuicer_handle, win32con.SW_RESTORE) + success2 = util.unhide_window_from_taskbar(self.carrotjuicer_handle) + success = success1 and success2 + if not success: + logger.error("Failed to restore CarrotJuicer") + else: + self.carrotjuicer_closed = False + time.sleep(0.25) self.sleep_time = 2.0 diff --git a/umalauncher/settings.py b/umalauncher/settings.py index 4e31cd3..329e8ad 100644 --- a/umalauncher/settings.py +++ b/umalauncher/settings.py @@ -145,8 +145,16 @@ def __init__(self): priority=50, tab="Position" ) + self.s_enable_browser = se.Setting( + "Enable browser", + "Enable the Automatic Training Event helper browser.", + True, + se.SettingType.BOOL, + priority=100, + tab="Event Helper" + ) self.s_selected_browser = se.Setting( - "Selected browser", + "Browser type", "Browser to use for the Automatic Training Event Helper.", { "Auto": True, @@ -155,14 +163,67 @@ def __init__(self): "Edge": False }, se.SettingType.RADIOBUTTONS, - priority=98 + priority=98, + tab="Event Helper" ) self.s_gametora_dark_mode = se.Setting( "GameTora dark mode", "Enable dark mode for GameTora.", True, se.SettingType.BOOL, - priority=97 + priority=97, + tab="Event Helper" + ) + self.s_custom_browser_divider = se.Setting( + "Custom browser divider", + None, + None, + se.SettingType.DIVIDER, + priority=96, + tab="Event Helper" + ) + self.s_custom_browser_message = se.Setting( + "Custom browser", + "

The following settings allow overriding of the browser binary and driver given to Selenium to control. You should only enable this if the browser fails to start, or you want to use a different Chromium-based browser.

", + None, + se.SettingType.MESSAGE, + priority=96, + tab="Event Helper" + ) + self.s_enable_browser_override = se.Setting( + "Enable browser override", + "Enable overriding of the browser binary and driver. This also disables app mode for Chromium-based browsers, so you can reach settings in case things don't work.", + False, + se.SettingType.BOOL, + priority=95, + tab="Event Helper" + ) + self.s_custom_browser_type = se.Setting( + "Browser override type", + "Browser to use for the Automatic Training Event Helper. If browser override is enabled, this will override the browser type setting above.", + { + "Firefox": True, + "Other (Chromium)": False + }, + se.SettingType.RADIOBUTTONS, + priority=94, + tab="Event Helper" + ) + self.s_browser_custom_binary = se.Setting( + "Browser custom binary", + "Path to a custom browser executable.
Leave empty to let Selenium decide.", + None, + se.SettingType.FILEDIALOG, + priority=93, + tab="Event Helper" + ) + self.s_browser_custom_driver = se.Setting( + "Browser custom driver", + "Path to a custom browser driver.
Leave empty to let Selenium decide.", + None, + se.SettingType.FILEDIALOG, + priority=92, + tab="Event Helper" ) self.s_training_helper_table_preset = se.Setting( "Training helper table preset", @@ -188,7 +249,7 @@ def __init__(self): self.s_training_helper_table_scenario_presets = se.Setting( "Training helper table scenario presets", "Scenario-specific selected preset.", - {key: "Default" for key in constants.SCENARIO_DICT}, + {str(key): "Default" for key in constants.SCENARIO_DICT}, se.SettingType.DICT, priority=-1 ) diff --git a/umalauncher/settings_elements.py b/umalauncher/settings_elements.py index d7e7f57..6f483f8 100644 --- a/umalauncher/settings_elements.py +++ b/umalauncher/settings_elements.py @@ -1,4 +1,5 @@ import enum +from loguru import logger class SettingType(enum.Enum): UNDEFINED = "undefined" @@ -15,6 +16,7 @@ class SettingType(enum.Enum): MESSAGE = "message" XYWHSPINBOXES = "xywhspinboxes" LRTBSPINBOXES = "lrtbspinboxes" + DIVIDER = "divider" class Settings(): @@ -23,7 +25,7 @@ def get_settings_keys(self): def to_dict(self): settings = self.get_settings_keys() - return {setting: getattr(self, setting).value for setting in settings if getattr(self, setting).type != SettingType.MESSAGE} if settings else {} + return {setting: getattr(self, setting).value for setting in settings if getattr(self, setting).type not in (SettingType.MESSAGE, SettingType.DIVIDER)} if settings else {} def import_dict(self, settings_dict, keep_undefined=False): for key, value in settings_dict.items(): @@ -40,7 +42,20 @@ def import_dict(self, settings_dict, keep_undefined=False): priority=-2 )) continue - getattr(self, key).value = value + + attribute = getattr(self, key) + + if isinstance(attribute.value, dict): + true_keys = set(attribute.value.keys()) + new_keys = set(value.keys()) + + if true_keys != new_keys: + logger.warning(f"Setting {key} has different keys in the new settings dict. Reverting to default.") + logger.warning(f"True keys: {true_keys}") + logger.warning(f"New keys: {new_keys}") + continue + + attribute.value = value def __repr__(self): return str(self.to_dict()) diff --git a/umalauncher/util.py b/umalauncher/util.py index b51b7bf..5563a0f 100644 --- a/umalauncher/util.py +++ b/umalauncher/util.py @@ -116,7 +116,7 @@ def do_get_request(url, error_title=None, error_message=None, ignore_timeout=Fal else: return None logger.debug(f"GET request to {url}") - response = requests.get(url) + response = requests.get(url, timeout=10) response.raise_for_status() return response except: @@ -321,6 +321,16 @@ def hide_window_from_taskbar(window_handle): except pywinerror: return False +def unhide_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_SHOW) + win32gui.SetWindowLong(window_handle, win32con.GWL_EXSTYLE, style) + return True + except pywinerror: + return False + def is_minimized(handle): try: @@ -473,8 +483,8 @@ def heroes_score_to_league_string(score): return current_league def scouting_score_to_rank_string(score): - current_rank = list(constants.SCOUTING_SCORE_TO_RANK_DICT.keys())[0] - for score_threshold, rank in constants.SCOUTING_SCORE_TO_RANK_DICT.items(): + current_rank = list(mdb.get_scouting_score_to_rank_dict().keys())[0] + for score_threshold, rank in mdb.get_scouting_score_to_rank_dict().items(): if score >= score_threshold: current_rank = rank else: diff --git a/umalauncher/version.py b/umalauncher/version.py index 2a6ddfe..98d65fa 100644 --- a/umalauncher/version.py +++ b/umalauncher/version.py @@ -10,7 +10,7 @@ import util import gui -VERSION = "1.9.1" +VERSION = "1.9.2" def parse_version(version_string: str): """Convert version string to tuple."""