diff --git a/Rose.spec b/Rose.spec index 46fd0407..adbbbf41 100644 --- a/Rose.spec +++ b/Rose.spec @@ -92,7 +92,7 @@ if Path('utils/crypto/skin_config.py').exists(): else: print("[WARNING] utils/crypto/skin_config.py not found - skin decryption will not work") -for _crypto_mod in ['client_secrets', 'integrity']: +for _crypto_mod in ['client_secrets', 'integrity', 'key_provider']: _py = Path(f'utils/crypto/{_crypto_mod}.py') _pyd = list(Path('utils/crypto').glob(f'{_crypto_mod}*.pyd')) if _pyd: @@ -153,6 +153,7 @@ hiddenimports = [ 'injection.config.threshold_manager', 'injection.mods', 'injection.mods.mod_manager', + 'injection.mods.loading_name_stringtable', 'injection.mods.zip_resolver', 'injection.overlay', 'injection.overlay.overlay_manager', diff --git a/injection/core/injector.py b/injection/core/injector.py index 7e5dbc23..113c8aa7 100644 --- a/injection/core/injector.py +++ b/injection/core/injector.py @@ -146,6 +146,7 @@ def inject_skin( champion_name: str = None, champion_id: int = None, extra_mods_callback: Optional[Callable[["SkinInjector"], List[str]]] = None, + loading_label: str = None, ) -> bool: """Inject a single skin (with optional chroma and party mods) @@ -199,11 +200,48 @@ def inject_skin( extract_start = time.time() mod_folder = self._extract_zip_to_mod(zp) + mod_names = [mod_folder.name] + + # Chroma packages can be tiny and may not contain the loading-screen art. + # When the selected package is a chroma, also extract its parent base skin + # so we can patch/inject the loadscreen from the base skin WAD. + base_mod_folder = None + try: + selected_id = int(str(zp.stem)) + parent_dir = zp.parent.parent if zp.parent.name == str(selected_id) else None + if parent_dir and parent_dir.name.isdigit(): + base_id = int(parent_dir.name) + if base_id != selected_id: + from injection.mods.zip_resolver import _find_by_extensions + base_zp = _find_by_extensions(parent_dir, str(base_id)) + if base_zp and base_zp != zp: + log.info("[LoadingLabel] Chroma package detected; including base skin package %s for loadscreen", base_zp.name) + base_mod_folder = self._extract_zip_to_mod(base_zp) + mod_names.insert(0, base_mod_folder.name) + except Exception as e: + log.debug("[LoadingLabel] Base skin package detection skipped: %s", e) + + if loading_label: + try: + from injection.mods.loading_name_stringtable import create_loading_name_stringtable_mod + rst_mod = create_loading_name_stringtable_mod( + self.mods_dir, + self.tools_dir, + self.game_dir, + champion_name, + loading_label, + ) + if rst_mod: + mod_names.append(rst_mod) + log.info("[LoadingNameRST] Including stringtable override mod: %s", rst_mod) + except Exception as e: + log.warning("[LoadingNameRST] Failed to prepare loading name override: %s", e) + else: + log.info("[LoadingNameRST] No loading label provided for %s", mod_folder.name) extract_duration = time.time() - extract_start log.debug(f"[INJECT] ZIP extraction took {extract_duration:.2f}s") # Create list of mods to inject (our skin + optional party/extra mods) - mod_names = [mod_folder.name] if extra_mods_callback: try: extra = extra_mods_callback(self) @@ -318,4 +356,4 @@ def kill_all_runoverlay_processes(self): def kill_all_modtools_processes(self): """Kill all mod-tools.exe processes (for application shutdown)""" - self.process_manager.kill_all_modtools_processes() \ No newline at end of file + self.process_manager.kill_all_modtools_processes() diff --git a/injection/core/manager.py b/injection/core/manager.py index 2ed8ea8f..16ff0705 100644 --- a/injection/core/manager.py +++ b/injection/core/manager.py @@ -201,7 +201,7 @@ def on_loadout_countdown(self, seconds_remaining: int): # This prevents unnecessary suspension for base skins and owned skins pass - def inject_skin_immediately(self, skin_name: str, stop_callback=None, chroma_id: int = None, champion_name: str = None, champion_id: int = None) -> bool: + def inject_skin_immediately(self, skin_name: str, stop_callback=None, chroma_id: int = None, champion_name: str = None, champion_id: int = None, loading_label: str = None) -> bool: """Immediately inject a specific skin (with optional chroma) Args: @@ -328,6 +328,7 @@ def inject_skin_immediately(self, skin_name: str, stop_callback=None, chroma_id: champion_name=champion_name, champion_id=champion_id, extra_mods_callback=extra_mods_callback, + loading_label=loading_label, ) if success: diff --git a/injection/mods/loading_name_stringtable.py b/injection/mods/loading_name_stringtable.py new file mode 100644 index 00000000..ea1d6722 --- /dev/null +++ b/injection/mods/loading_name_stringtable.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Build a tiny CSLOL mod that overrides the localized champion display string +used by the loading screen card. +""" + +import json +import hashlib +import shutil +import struct +import subprocess +import tempfile +from pathlib import Path +from typing import Iterable + +from utils.core.logging import get_logger +from utils.core.paths import get_user_data_dir + +log = get_logger() + + +RST_MAGIC = b"RST" +RST_VERSION = 5 +RST_HASH_BITS = 38 +CACHE_VERSION = "exact-display-v2" + + +def create_loading_name_stringtable_mod( + mods_dir: Path, + tools_dir: Path, + game_dir: Path | None, + champion_name: str | None, + skin_label: str | None, +) -> str | None: + """Create a temporary mod folder that patches the global string table.""" + champion_name = (champion_name or "").strip() + skin_label = (skin_label or "").strip() + if not champion_name or not skin_label or champion_name == skin_label: + return None + + source_wad = _find_global_stringtable_wad(game_dir) + if not source_wad: + log.info("[LoadingNameRST] Global stringtable WAD not found; skipping") + return None + + wad_extract = Path(tools_dir) / "wad-extract.exe" + wad_make = Path(tools_dir) / "wad-make.exe" + hashdict = Path(tools_dir) / "hashes.game.txt" + if not wad_extract.exists() or not wad_make.exists(): + log.info("[LoadingNameRST] WAD tools missing; skipping") + return None + + mod_dir = Path(mods_dir) / "_rose_loading_name_text" + if mod_dir.exists(): + shutil.rmtree(mod_dir, ignore_errors=True) + (mod_dir / "META").mkdir(parents=True, exist_ok=True) + (mod_dir / "WAD").mkdir(parents=True, exist_ok=True) + + try: + cached_wad = _get_cached_wad(source_wad, champion_name, skin_label) + out_wad = mod_dir / "WAD" / source_wad.name + if cached_wad.exists(): + shutil.copy2(cached_wad, out_wad) + _write_info(mod_dir, champion_name, skin_label) + log.info("[LoadingNameRST] Reused cached stringtable override: %s", cached_wad.name) + return mod_dir.name + + with tempfile.TemporaryDirectory(prefix="rose_loading_rst_") as tmp: + tmp_dir = Path(tmp) + extract_dir = tmp_dir / "extract" + extract_dir.mkdir(parents=True, exist_ok=True) + + extract_cmd = [str(wad_extract), str(source_wad), str(extract_dir)] + if hashdict.exists(): + extract_cmd.append(str(hashdict)) + if _run_tool(extract_cmd) != 0: + return None + + patched = 0 + for rst_path in extract_dir.rglob("*.stringtable"): + patched += _patch_rst_file(rst_path, champion_name, skin_label) + + if patched <= 0: + shutil.rmtree(mod_dir, ignore_errors=True) + log.info("[LoadingNameRST] No stringtable entries patched for %s", champion_name) + return None + + if _run_tool([str(wad_make), str(extract_dir), str(out_wad)]) != 0 or not out_wad.exists(): + shutil.rmtree(mod_dir, ignore_errors=True) + return None + + cached_wad.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(out_wad, cached_wad) + _write_info(mod_dir, champion_name, skin_label) + log.info("[LoadingNameRST] Created stringtable mod with %s patched entrie(s)", patched) + return mod_dir.name + except Exception as exc: + shutil.rmtree(mod_dir, ignore_errors=True) + log.warning("[LoadingNameRST] Failed to create stringtable mod: %s", exc) + return None + + +def _find_global_stringtable_wad(game_dir: Path | None) -> Path | None: + if not game_dir: + return None + + localized_dir = Path(game_dir) / "DATA" / "FINAL" / "Localized" + preferred = localized_dir / "Global.pt_BR.wad.client" + if preferred.exists(): + return preferred + + candidates = sorted(localized_dir.glob("Global.*.wad.client")) if localized_dir.exists() else [] + return candidates[0] if candidates else None + + +def _patch_rst_file(rst_path: Path, champion_name: str, skin_label: str) -> int: + entries = _read_rst(rst_path) + patched = 0 + for key, value in list(entries.items()): + if _should_replace(key, value, champion_name): + entries[key] = skin_label + patched += 1 + + if patched: + rst_path.write_bytes(_write_rst(entries)) + log.info("[LoadingNameRST] Patched %s entrie(s) in %s", patched, rst_path.name) + return patched + + +def _should_replace(key: int, value: str, champion_name: str) -> bool: + return value in {champion_name, f"game_character_displayname_{champion_name}"} + + +def _get_cached_wad(source_wad: Path, champion_name: str, skin_label: str) -> Path: + stat = source_wad.stat() + raw = "|".join( + [ + CACHE_VERSION, + source_wad.name, + str(stat.st_size), + str(stat.st_mtime_ns), + champion_name, + skin_label, + ] + ) + digest = hashlib.sha1(raw.encode("utf-8")).hexdigest() + return get_user_data_dir() / "cache" / "loading-name-rst" / f"{digest}.wad.client" + + +def _write_info(mod_dir: Path, champion_name: str, skin_label: str) -> None: + info = { + "Name": "Rose Loading Name Text", + "Author": "Rose", + "Version": "1.0", + "Description": f"Overrides {champion_name} loading name with {skin_label}.", + } + (mod_dir / "META" / "info.json").write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding="utf-8") + + +def _read_rst(path: Path) -> dict[int, str]: + data = path.read_bytes() + if data[:3] != RST_MAGIC or data[3] != RST_VERSION: + raise ValueError(f"Unsupported RST file: {path}") + + count = struct.unpack_from("> RST_HASH_BITS + key_hash = packed & mask + end = strings.find(b"\x00", offset) + if end < 0: + end = len(strings) + entries[key_hash] = strings[offset:end].decode("utf-8", errors="replace") + + return entries + + +def _write_rst(entries: dict[int, str]) -> bytes: + data_block = bytearray() + rows: list[tuple[int, int]] = [] + mask = (1 << RST_HASH_BITS) - 1 + + for key_hash, text in entries.items(): + offset = len(data_block) + data_block.extend(text.encode("utf-8")) + data_block.append(0) + rows.append((key_hash & mask, offset)) + + out = bytearray() + out.extend(RST_MAGIC) + out.append(RST_VERSION) + out.extend(struct.pack(" int: + try: + flags = 0 + if hasattr(subprocess, "CREATE_NO_WINDOW"): + flags = subprocess.CREATE_NO_WINDOW + proc = subprocess.run( + list(cmd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + creationflags=flags, + timeout=60, + ) + if proc.returncode != 0: + log.debug("[LoadingNameRST] Tool failed (%s): %s", proc.returncode, " ".join(cmd)) + if proc.stderr: + log.debug("[LoadingNameRST] stderr: %s", proc.stderr[:500]) + return proc.returncode + except Exception as exc: + log.debug("[LoadingNameRST] Tool execution failed: %s", exc) + return 1 diff --git a/main/core/lcu_handler.py b/main/core/lcu_handler.py index 332f5468..dbb0605c 100644 --- a/main/core/lcu_handler.py +++ b/main/core/lcu_handler.py @@ -30,6 +30,7 @@ def on_lcu_disconnected(): state.loadout_left0_ms = 0 state.last_remain_ms = 0 state.last_hover_written = False + state.loading_skin_restore_done = False state.selected_skin_id = None state.selected_chroma_id = None state.selected_form_path = None @@ -52,6 +53,7 @@ def on_lcu_disconnected(): state.last_hovered_skin_key = None state.last_hovered_skin_id = None state.last_hovered_skin_slug = None + state.selected_skin_display_name = None state.champion_exchange_triggered = False state.injection_completed = False @@ -118,4 +120,3 @@ def on_lcu_disconnected(): log.debug(f"[Main] Failed to update app status after disconnection: {e}") return on_lcu_disconnected - diff --git a/pengu/processing/skin_processor.py b/pengu/processing/skin_processor.py index 3525b654..a3c597c9 100644 --- a/pengu/processing/skin_processor.py +++ b/pengu/processing/skin_processor.py @@ -78,6 +78,7 @@ def _process_swiftplay_skin_name(self, skin_name: str, broadcaster=None) -> None tracking_snapshot = dict(self.shared_state.swiftplay_skin_tracking) self.shared_state.ui_skin_id = skin_id self.shared_state.last_hovered_skin_id = skin_id + self.shared_state.selected_skin_display_name = skin_name # Mark this champion as explicitly changed so the restore logic # won't override the user's choice on re-queue @@ -131,6 +132,7 @@ def _process_regular_skin_name(self, skin_name: str, broadcaster=None) -> None: # Use the matched name from the matcher instead of the input self.shared_state.last_hovered_skin_key = matched_name + self.shared_state.selected_skin_display_name = matched_name log.info( "[SkinMonitor] Skin '%s' mapped to ID %s (key=%s)", skin_name, @@ -189,4 +191,4 @@ def clear_cache(self) -> None: self.last_skin_name = None self.shared_state.ui_skin_id = None self.shared_state.ui_last_text = None - + self.shared_state.selected_skin_display_name = None diff --git a/state/core/shared_state.py b/state/core/shared_state.py index a7727535..d944c90b 100644 --- a/state/core/shared_state.py +++ b/state/core/shared_state.py @@ -20,6 +20,7 @@ class SharedState: last_hovered_skin_key: Optional[str] = None last_hovered_skin_id: Optional[int] = None last_hovered_skin_slug: Optional[str] = None + selected_skin_display_name: Optional[str] = None # Human-readable selected skin name for loading/injection logs selected_skin_id: Optional[int] = None # Skin ID selected in LCU (owned skin) owned_skin_ids: set = field(default_factory=set) # All owned skin IDs from LCU inventory processed_action_ids: set = field(default_factory=set) @@ -35,6 +36,7 @@ class SharedState: loadout_left0_ms: int = 0 last_remain_ms: int = 0 # Remaining time in milliseconds last_hover_written: bool = False + loading_skin_restore_done: bool = False timer_lock: threading.Lock = field(default_factory=threading.Lock) ticker_seq: int = 0 current_ticker: int = 0 diff --git a/threads/handlers/champ_thread.py b/threads/handlers/champ_thread.py index 3c37ef13..35f7721b 100644 --- a/threads/handlers/champ_thread.py +++ b/threads/handlers/champ_thread.py @@ -43,10 +43,12 @@ def _handle_champion_exchange(self, old_champ_id: int, new_champ_id: int, new_ch self.state.last_hovered_skin_key = None self.state.last_hovered_skin_id = None self.state.last_hovered_skin_slug = None + self.state.selected_skin_display_name = None # Reset injection state self.state.injection_completed = False self.state.last_hover_written = False + self.state.loading_skin_restore_done = False # Reset locked champion state self.state.locked_champ_id = new_champ_id diff --git a/threads/handlers/champion_lock_handler.py b/threads/handlers/champion_lock_handler.py index b757cadf..0cbd9c8f 100644 --- a/threads/handlers/champion_lock_handler.py +++ b/threads/handlers/champion_lock_handler.py @@ -123,6 +123,7 @@ def handle_champion_exchange(self, old_champ_id: int, new_champ_id: int, new_cha self.state.last_hovered_skin_key = None self.state.last_hovered_skin_id = None self.state.last_hovered_skin_slug = None + self.state.selected_skin_display_name = None # Reset chroma selection (prevents stale chromas from previous champion) self.state.selected_chroma_id = None @@ -131,6 +132,7 @@ def handle_champion_exchange(self, old_champ_id: int, new_champ_id: int, new_cha # Reset injection state self.state.injection_completed = False self.state.last_hover_written = False + self.state.loading_skin_restore_done = False # Reset locked champion state self.state.locked_champ_id = new_champ_id @@ -264,4 +266,3 @@ def on_own_champion_locked(self, champion_id: int, champion_label: str, old_cham self.state.ui_skin_thread._broadcast_champion_locked(True) except Exception as e: log.debug(f"[lock:champ] Failed to broadcast champion lock state: {e}") - diff --git a/threads/handlers/injection_trigger.py b/threads/handlers/injection_trigger.py index 66d3be5e..1a72377f 100644 --- a/threads/handlers/injection_trigger.py +++ b/threads/handlers/injection_trigger.py @@ -45,7 +45,7 @@ def __init__( self.injection_manager = injection_manager self.skin_scraper = skin_scraper - def trigger_injection(self, name: str, ticker_id: int, cname: str = ""): + def trigger_injection(self, name: str, ticker_id: int, cname: str = "", display_label: Optional[str] = None): """Trigger injection for a skin/chroma Args: @@ -80,13 +80,17 @@ def trigger_injection(self, name: str, ticker_id: int, cname: str = ""): if selected_custom_mod: mod_name = selected_custom_mod.get("mod_name") or selected_custom_mod.get("mod_folder_name") - # Collect all selected mods for log message - mod_labels = [] + # Collect all selected mods for log message. Prefer the human-readable + # selected skin name so loading/injection output does not show skin_123. + selected_skin_label = ( + display_label + or getattr(self.state, "selected_skin_display_name", None) + or getattr(self.state, "last_hovered_skin_key", None) + or name.upper() + ) + mod_labels = [selected_skin_label] if mod_name: - mod_target_skin = selected_custom_mod.get("skin_id", ui_skin_id) if selected_custom_mod else ui_skin_id - mod_labels.append(f"{mod_name} (SKIN_{mod_target_skin})") - else: - mod_labels.append(name.upper()) + mod_labels.append(f"MOD: {mod_name}") # Add map/font/announcer/other mods if selected selected_map_mod = getattr(self.state, 'selected_map_mod', None) @@ -122,7 +126,7 @@ def trigger_injection(self, name: str, ticker_id: int, cname: str = ""): log.info(f"PREPARING INJECTION >>> {injection_label} <<<") log.info(f" Loadout Timer: #{ticker_id}") log.info("=" * LOG_SEPARATOR_WIDTH) - + try: lcu_skin_id = self.state.selected_skin_id owned_skin_ids = self.state.owned_skin_ids @@ -527,6 +531,7 @@ def auto_select_historic_mod(mod_type: str, category_attr: str): name, champion_name=cname, champion_id=self.state.locked_champ_id or self.state.hovered_champ_id, + loading_label=selected_skin_label, ) # Also check if base skin is owned but chroma is selected (for owned chromas) @@ -539,6 +544,7 @@ def auto_select_historic_mod(mod_type: str, category_attr: str): name, champion_name=cname, champion_id=self.state.locked_champ_id or self.state.hovered_champ_id, + loading_label=selected_skin_label, ) # Inject if user doesn't own the hovered skin @@ -547,6 +553,65 @@ def auto_select_historic_mod(mod_type: str, category_attr: str): except Exception as e: log.warning(f"[loadout #{ticker_id}] injection setup failed: {e}") + + def _get_injection_skin_id(self, name: Optional[str]) -> Optional[int]: + """Extract the numeric skin/chroma ID from an injection name.""" + if not isinstance(name, str) or "_" not in name: + return None + try: + kind, raw_id = name.split("_", 1) + if kind not in {"skin", "chroma"} or not raw_id.isdigit(): + return None + return int(raw_id) + except Exception: + return None + + def _restore_loading_screen_skin(self, name: Optional[str]) -> None: + """Best-effort restore of selectedSkinId so the loading screen shows the chosen skin. + + Unowned skin injection needs the base skin briefly selected for the overlay build. + After the base skin is confirmed, try to put the chosen skin/chroma ID back into + champ-select state before GameStart so League's loading screen can display it. + """ + target_skin_id = self._get_injection_skin_id(name) or getattr(self.state, "last_hovered_skin_id", None) + champ_id = self.state.locked_champ_id or self.state.hovered_champ_id + if not target_skin_id or not champ_id: + return + + base_skin_id = int(champ_id) * 1000 + if int(target_skin_id) == base_skin_id: + return + + skin_label = getattr(self.state, "selected_skin_display_name", None) or getattr(self.state, "last_hovered_skin_key", None) or name + log.info( + "[INJECT] Restoring selected skin for loading screen: %s (skinId=%s)", + skin_label, + target_skin_id, + ) + + try: + if self.lcu.set_my_selection_skin(int(target_skin_id)): + self.state.selected_skin_id = int(target_skin_id) + log.info("[INJECT] Loading screen skin restored via my-selection") + return + + # Fallback for the rare case where the pick action is still editable. + sess = self.lcu.session or {} + actions = sess.get("actions") or [] + my_cell = self.state.local_cell_id + for rnd in actions: + for act in rnd: + if act.get("actorCellId") == my_cell and act.get("type") == "pick": + action_id = act.get("id") + if action_id is not None and self.lcu.set_selected_skin(action_id, int(target_skin_id)): + self.state.selected_skin_id = int(target_skin_id) + log.info("[INJECT] Loading screen skin restored via action") + return + break + + log.warning("[INJECT] Could not restore loading screen skin; LCU rejected skinId=%s", target_skin_id) + except Exception as e: + log.warning("[INJECT] Failed to restore loading screen skin: %s", e) def _force_owned_skin(self, skin_id: int): """Force owned skin/chroma selection via LCU""" @@ -647,6 +712,10 @@ def _inject_unowned_skin(self, name: str, cname: str): # Only force base skin if current selection is not already base skin if actual_lcu_skin_id is None or actual_lcu_skin_id != base_skin_id: self._force_base_skin(base_skin_id) + + # Even when LCU is already on the base skin, try to restore the + # selected skin ID for the official loading-screen label. + self._restore_loading_screen_skin(name) # Create callback to check if game ended has_been_in_progress = False @@ -676,7 +745,8 @@ def run_injection(): name, stop_callback=game_ended_callback, champion_name=cname, - champion_id=self.state.locked_champ_id + champion_id=self.state.locked_champ_id, + loading_label=getattr(self.state, "selected_skin_display_name", None) or getattr(self.state, "last_hovered_skin_key", None) or name, ) # Clear random state after injection @@ -1176,6 +1246,7 @@ def re_extract_mod(mod_dict, mod_type_name): # Injecting base skin ZIP for unowned skin - force base skin base_skin_id = champion_id * 1000 self._force_base_skin(base_skin_id) + self._restore_loading_screen_skin(base_skin_name) # Create callback to check if game ended has_been_in_progress = False @@ -1348,4 +1419,3 @@ def normalize_path(p): log.error(f"[INJECT] Error injecting custom mod: {e}") import traceback log.error(f"[INJECT] Traceback: {traceback.format_exc()}") - diff --git a/threads/handlers/swiftplay_handler.py b/threads/handlers/swiftplay_handler.py index cd735f39..efef12ce 100644 --- a/threads/handlers/swiftplay_handler.py +++ b/threads/handlers/swiftplay_handler.py @@ -362,6 +362,7 @@ def cleanup_swiftplay_exit(self): self.state.ui_last_text = None self.state.last_hovered_skin_id = None self.state.last_hovered_skin_key = None + self.state.selected_skin_display_name = None # Reset champion lock state self.state.own_champion_locked = False @@ -605,4 +606,3 @@ def run_swiftplay_overlay(self): log.warning(f"[phase] Error running Swiftplay overlay: {e}") import traceback log.debug(f"[phase] Traceback: {traceback.format_exc()}") - diff --git a/threads/utilities/loadout_ticker.py b/threads/utilities/loadout_ticker.py index e0153408..9dc5c52d 100644 --- a/threads/utilities/loadout_ticker.py +++ b/threads/utilities/loadout_ticker.py @@ -113,6 +113,15 @@ def run(self): # Write last hovered skin at T<=threshold thresh = int(getattr(self.state, 'skin_write_ms', SKIN_THRESHOLD_MS_DEFAULT) or SKIN_THRESHOLD_MS_DEFAULT) + + # Restore the selected skin ID a little earlier so League has time + # to prepare its official loading-screen label before GameStart. + if remain_ms <= max(3000, thresh + 1000) and not getattr(self.state, 'loading_skin_restore_done', False): + early_name = self.skin_name_resolver.resolve_injection_name() + if early_name: + self.injection_trigger._restore_loading_screen_skin(early_name) + self.state.loading_skin_restore_done = True + if remain_ms <= thresh and not self.state.last_hover_written: # Build skin label final_label = self.skin_name_resolver.build_skin_label() @@ -133,7 +142,7 @@ def run(self): if name: # Trigger injection - self.injection_trigger.trigger_injection(name, self.ticker_id, cname) + self.injection_trigger.trigger_injection(name, self.ticker_id, cname, display_label=final_label) if remain_ms <= 0: break diff --git a/threads/utilities/skin_name_resolver.py b/threads/utilities/skin_name_resolver.py index 67ee71f0..2317ff06 100644 --- a/threads/utilities/skin_name_resolver.py +++ b/threads/utilities/skin_name_resolver.py @@ -165,6 +165,10 @@ def build_skin_label(self) -> Optional[str]: Returns: Clean skin label or None """ + display_name = getattr(self.state, "selected_skin_display_name", None) + if display_name: + return str(display_name).strip() + raw = self.state.last_hovered_skin_key or self.state.last_hovered_skin_slug \ or (str(self.state.last_hovered_skin_id) if self.state.last_hovered_skin_id else None) @@ -208,4 +212,3 @@ def build_skin_label(self) -> Optional[str]: return final_label except Exception: return raw or "" - diff --git a/threads/websocket/websocket_event_handler.py b/threads/websocket/websocket_event_handler.py index 11cf1b72..bb42ec12 100644 --- a/threads/websocket/websocket_event_handler.py +++ b/threads/websocket/websocket_event_handler.py @@ -136,11 +136,12 @@ def _handle_phase_event(self, payload: dict): def _handle_champ_select_entry(self): """Handle entering ChampSelect phase""" log_event(log, "Entering ChampSelect - resetting state for new game", "") - + # Reset skin detection state self.state.last_hovered_skin_key = None self.state.last_hovered_skin_id = None self.state.last_hovered_skin_slug = None + self.state.selected_skin_display_name = None self.state.ui_last_text = None self.state.ui_skin_id = None @@ -148,6 +149,7 @@ def _handle_champ_select_entry(self): self.state.selected_skin_id = None self.state.owned_skin_ids.clear() self.state.last_hover_written = False + self.state.loading_skin_restore_done = False # Reset injection and countdown state self.state.injection_completed = False @@ -220,8 +222,9 @@ def _handle_in_progress_entry(self): """Handle entering InProgress phase""" from utils.core.logging import log_section - if self.state.last_hovered_skin_key: - log_section(log, f"Game Starting - Last Detected Skin: {self.state.last_hovered_skin_key.upper()}", "", { + skin_display_name = self.state.selected_skin_display_name or self.state.last_hovered_skin_key + if skin_display_name: + log_section(log, f"Game Starting - Selected Skin: {skin_display_name.upper()}", "", { "Champion": self.state.last_hovered_skin_slug, "SkinID": self.state.last_hovered_skin_id }) @@ -296,4 +299,3 @@ def _handle_session_event(self, payload: dict): # Timer if self.timer_manager: self.timer_manager.maybe_start_timer(sess) - diff --git a/ui/chroma/selection_handler.py b/ui/chroma/selection_handler.py index 8e8c25f3..0c4db6fd 100644 --- a/ui/chroma/selection_handler.py +++ b/ui/chroma/selection_handler.py @@ -29,6 +29,10 @@ def __init__(self, state: SharedState, skin_scraper=None, panel=None, current_sk self.skin_scraper = skin_scraper self.panel = panel self.current_skin_id = current_skin_id + + def _set_selected_skin_name(self, skin_name: str) -> None: + self.state.last_hovered_skin_key = skin_name + self.state.selected_skin_display_name = skin_name def handle_selection(self, chroma_id: int, chroma_name: str): """Handle chroma selection callback @@ -109,7 +113,7 @@ def _handle_elementalist_form_selection(self, chroma_id: int, chroma_name: str): if hasattr(self.panel, 'current_skin_name') and self.panel.current_skin_name: base_skin_name = self.panel.current_skin_name form_skin_name = f"{base_skin_name} {chroma_name}" - self.state.last_hovered_skin_key = form_skin_name + self._set_selected_skin_name(form_skin_name) log.debug(f"[CHROMA] Form skin name: {form_skin_name}") log.debug(f"[CHROMA] Form path: {form_data['form_path']}") log.debug(f"[CHROMA] Using fake ID {chroma_id} for injection (not owned)") @@ -148,7 +152,7 @@ def _handle_mordekaiser_form_selection(self, chroma_id: int, chroma_name: str): if hasattr(self.panel, 'current_skin_name') and self.panel.current_skin_name: base_skin_name = self.panel.current_skin_name form_skin_name = f"{base_skin_name} {chroma_name}" - self.state.last_hovered_skin_key = form_skin_name + self._set_selected_skin_name(form_skin_name) log.debug(f"[CHROMA] Form skin name: {form_skin_name}") log.debug(f"[CHROMA] Form path: {form_data['form_path']}") log.debug(f"[CHROMA] Using fake ID {chroma_id} for injection (not owned)") @@ -187,7 +191,7 @@ def _handle_morgana_form_selection(self, chroma_id: int, chroma_name: str): if hasattr(self.panel, 'current_skin_name') and self.panel.current_skin_name: base_skin_name = self.panel.current_skin_name form_skin_name = f"{base_skin_name} {chroma_name}" - self.state.last_hovered_skin_key = form_skin_name + self._set_selected_skin_name(form_skin_name) log.debug(f"[CHROMA] Form skin name: {form_skin_name}") log.debug(f"[CHROMA] Form path: {form_data['form_path']}") log.debug(f"[CHROMA] Using fake ID {chroma_id} for injection (not owned)") @@ -226,7 +230,7 @@ def _handle_sett_form_selection(self, chroma_id: int, chroma_name: str): if hasattr(self.panel, 'current_skin_name') and self.panel.current_skin_name: base_skin_name = self.panel.current_skin_name form_skin_name = f"{base_skin_name} {chroma_name}" - self.state.last_hovered_skin_key = form_skin_name + self._set_selected_skin_name(form_skin_name) log.debug(f"[CHROMA] Form skin name: {form_skin_name}") log.debug(f"[CHROMA] Form path: {form_data['form_path']}") log.debug(f"[CHROMA] Using real ID {chroma_id} for injection (not owned)") @@ -265,7 +269,7 @@ def _handle_seraphine_form_selection(self, chroma_id: int, chroma_name: str): if hasattr(self.panel, 'current_skin_name') and self.panel.current_skin_name: base_skin_name = self.panel.current_skin_name form_skin_name = f"{base_skin_name} {chroma_name}" - self.state.last_hovered_skin_key = form_skin_name + self._set_selected_skin_name(form_skin_name) log.debug(f"[CHROMA] Form skin name: {form_skin_name}") log.debug(f"[CHROMA] Form path: {form_data['form_path']}") log.debug(f"[CHROMA] Using real ID {chroma_id} for injection (not owned)") @@ -304,7 +308,7 @@ def _handle_viego_form_selection(self, chroma_id: int, chroma_name: str): if hasattr(self.panel, 'current_skin_name') and self.panel.current_skin_name: base_skin_name = self.panel.current_skin_name form_skin_name = f"{base_skin_name} {chroma_name}" - self.state.last_hovered_skin_key = form_skin_name + self._set_selected_skin_name(form_skin_name) log.debug(f"[CHROMA] Form skin name: {form_skin_name}") log.debug(f"[CHROMA] Form path: {form_data['form_path']}") log.debug(f"[CHROMA] Using real ID {chroma_id} for injection (not owned)") @@ -341,7 +345,7 @@ def _handle_hol_chroma_selection(self, chroma_id: int, chroma_name: str): if hasattr(self.panel, 'current_skin_name') and self.panel.current_skin_name: base_skin_name = self.panel.current_skin_name hol_skin_name = f"{base_skin_name} {chroma_name}" - self.state.last_hovered_skin_key = hol_skin_name + self._set_selected_skin_name(hol_skin_name) log.debug(f"[CHROMA] HOL skin name: {hol_skin_name}") log.debug(f"[CHROMA] HOL skin ID: {target_skin_id}") log.debug(f"[CHROMA] Using real ID {chroma_id} for injection (not owned)") @@ -361,7 +365,7 @@ def _handle_base_skin_selection(self): english_skin_name = skin_data.get('skinName', '') # For base skins, use just the skin name (no chroma ID) - self.state.last_hovered_skin_key = english_skin_name + self._set_selected_skin_name(english_skin_name) log.debug(f"[CHROMA] Reset last_hovered_skin_key to: {self.state.last_hovered_skin_key}") # Update Swiftplay tracking dictionary if in Swiftplay mode @@ -422,7 +426,8 @@ def _handle_regular_chroma_selection(self, chroma_id: int, chroma_name: str): english_skin_name = skin_data.get('skinName', '') # For chromas, append the chroma ID to the clean base skin name - self.state.last_hovered_skin_key = f"{english_skin_name} {chroma_id}" + chroma_display_name = chroma_name or f"{english_skin_name} {chroma_id}" + self._set_selected_skin_name(chroma_display_name) log.debug(f"[CHROMA] Updated last_hovered_skin_key to: {self.state.last_hovered_skin_key}") log.info(f"[CHROMA] Updated last_hovered_skin_id from {self.current_skin_id} to {chroma_id}") @@ -461,4 +466,3 @@ def _safety_check_historic_mode(self): log.debug(f"[CHROMA] Failed to broadcast historic state in safety check: {e}") except Exception as e: log.debug(f"[CHROMA] Error disabling historic mode: {e}") -