From c193b536255534a9e36f42231ca4a7bf3c472fee Mon Sep 17 00:00:00 2001 From: rtw1x1 Date: Mon, 5 Jan 2026 18:55:18 +0000 Subject: [PATCH 1/2] ML: Hot reload locale system --- assets/root/intrologin.py | 103 ++----- assets/root/localeinfo.py | 2 + assets/root/uilocalechange.py | 472 ++++++++++++++++++++++++++++++++ assets/root/uilocaleselector.py | 191 +++++++++++++ 4 files changed, 693 insertions(+), 75 deletions(-) create mode 100644 assets/root/uilocalechange.py create mode 100644 assets/root/uilocaleselector.py diff --git a/assets/root/intrologin.py b/assets/root/intrologin.py index 343494c8..77955da2 100644 --- a/assets/root/intrologin.py +++ b/assets/root/intrologin.py @@ -17,6 +17,10 @@ import serverCommandParser import ime import uiScriptLocale +# Multi-language hot-reload system +from uilocaleselector import LocaleSelector +from uilocalechange import LocaleChangeManager + LOGIN_DELAY_SEC = 0.0 SKIP_LOGIN_PHASE = False SKIP_LOGIN_PHASE_SUPPORT_CHANNEL = False @@ -55,6 +59,13 @@ def GetLoginDelay(): app.SetGuildMarkPath("test") +############################################################################### +# Multi-Language Hot-Reload System +# All locale selector and hot-reload logic moved to: +# - uilocaleselector.py (UI component) +# - uilocalechange.py (hot-reload manager) +############################################################################### + class ConnectingDialog(ui.ScriptWindow): def __init__(self): @@ -144,11 +155,9 @@ class LoginWindow(ui.ScriptWindow): self.virtualKeyboardIsUpper = False self.timeOutMsg = False #Fix - self.language_list = [] - self.flag_button_list = [] - self.language_board = None - self.language_popup = None - self.__LoadLocale() + # Multi-language hot-reload system + self.localeSelector = None + self.localeChangeManager = LocaleChangeManager(self) def __del__(self): net.ClearPhaseWindow(net.PHASE_WINDOW_LOGIN, self) @@ -276,10 +285,9 @@ class LoginWindow(ui.ScriptWindow): self.connectingDialog = None self.loadingImage = None - self.language_list = [] - self.flag_button_list = [] - self.language_board = None - self.language_popup = None + if self.localeSelector: + self.localeSelector.Destroy() + self.localeSelector = None self.serverBoard = None self.serverList = None @@ -465,29 +473,11 @@ class LoginWindow(ui.ScriptWindow): self.GetChild("key_at").SetToggleDownEvent(lambda : self.__VirtualKeyboard_SetSymbolMode()) self.GetChild("key_at").SetToggleUpEvent(lambda : self.__VirtualKeyboard_SetAlphabetMode()) - self.language_board = ui.ThinBoard() - self.language_board.SetParent(self) - self.language_board.SetSize(wndMgr.GetScreenWidth(), 35) - self.language_board.SetPosition(0, 20) - self.language_board.Show() - - step = wndMgr.GetScreenWidth() / len(self.language_list) - x = 0 - - for i, lang in enumerate(self.language_list): - img_path = "d:/ymir work/ui/intro/login/server_flag_%s.sub" % lang - btn = ui.Button() - btn.SetParent(self.language_board) - btn.SetPosition(x + 15, 10) - btn.SetUpVisual(img_path) - btn.SetOverVisual(img_path) - btn.SetDownVisual(img_path) - btn.SetToolTipText(lang.upper()) - btn.SetEvent(ui.__mem_func__(self.__ClickLanguage), i) - btn.Show() - - self.flag_button_list.append(btn) - x += step + # Create locale selector (only if it doesn't exist - during hot-reload we keep the old one) + if not self.localeSelector: + self.localeSelector = LocaleSelector() + self.localeSelector.Create(self) + self.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.__OnLocaleChanged)) except: import exception @@ -608,49 +598,12 @@ class LoginWindow(ui.ScriptWindow): def __OnClickExitButton(self): self.stream.SetPhaseWindow(0) - def __LoadLocale(self): - self.language_list = [ - "ae", "en", "cz", "de", "dk", - "es", "fr", "gr", "hu", "it", - "nl", "pl", "pt", "ro", "ru", "tr", - ] - - def __SaveLocale(self, locale): - try: - with open("config/locale.cfg", "wt") as f: - f.write(locale) - except: - import dbg - dbg.LogBox("__SaveLocale error locale.cfg") - app.Abort() - - def __ClickLanguage(self, index): - if index >= len(self.language_list): - return - - self.locale = self.language_list[index] - - if not self.language_popup: - self.language_popup = uiCommon.QuestionDialog() - - self.language_popup.SetText("Change language and restart the client?") - self.language_popup.SetAcceptEvent(ui.__mem_func__(self.__OnAcceptLanguage)) - self.language_popup.SetCancelEvent(ui.__mem_func__(self.__OnCancelLanguage)) - self.language_popup.Open() - - def __OnAcceptLanguage(self): - if self.language_popup: - self.language_popup.Close() - - self.__SaveLocale(self.locale) - - import os - app.Exit() - os.popen('start "" "Metin2_Debug.exe"') - - def __OnCancelLanguage(self): - if self.language_popup: - self.language_popup.Close() + def __OnLocaleChanged(self, newLocaleCode): + """ + Callback when user confirms locale change. + All the heavy lifting is done by LocaleChangeManager - this is just 3 lines! + """ + self.localeChangeManager.ReloadWithNewLocale(newLocaleCode, "uiscript/LoginWindow.py") def __SetServerInfo(self, name): net.SetServerInfo(name.strip()) diff --git a/assets/root/localeinfo.py b/assets/root/localeinfo.py index 2007f9ab..9cae9f65 100644 --- a/assets/root/localeinfo.py +++ b/assets/root/localeinfo.py @@ -13,6 +13,8 @@ BLEND_POTION_NO_INFO = 'BLEND_POTION_NO_INFO' LOGIN_FAILURE_WRONG_SOCIALID = 'LOGIN_FAILURE_WRONG_SOCIALID' LOGIN_FAILURE_SHUTDOWN_TIME = 'LOGIN_FAILURE_SHUTDOWN_TIME' +LOCALE_CHANGE_CONFIRM = 'Change language to %s and reload the game?' + GUILD_MEMBER_COUNT_INFINITY = 'INFINITY' GUILD_MARK_MIN_LEVEL = '3' GUILD_BUILDING_LIST_TXT = '{:s}/GuildBuildingList.txt'.format(APP_GET_LOCALE_PATH) diff --git a/assets/root/uilocalechange.py b/assets/root/uilocalechange.py new file mode 100644 index 00000000..54096089 --- /dev/null +++ b/assets/root/uilocalechange.py @@ -0,0 +1,472 @@ +# -*- coding: utf-8 -*- +""" +Multi-Language Hot-Reload System +================================= + +This module provides a clean, reusable system for hot-reloading locale changes +without restarting the client. It's designed to be window-agnostic and can be +used with any UI window (login, game, etc.). + +Architecture: +- C++ side handles: LoadConfig() → LoadLocaleData() → Clear Python module cache +- Python side handles: Module reimport → UI state save/restore → Window recreation + +Usage: + from uilocalechange import LocaleChangeManager + + # In your window class __init__: + self.localeChangeManager = LocaleChangeManager(self) + + # When locale changes (e.g., from a locale selector): + self.localeChangeManager.ReloadWithNewLocale(newLocaleCode) + +Features: +- Automatically saves and restores UI state (text inputs, selections, etc.) +- Handles board visibility states +- Preserves virtual keyboard state +- Works with any window that follows the standard pattern +""" + +import app +import dbg +import ui +import sys + + +class LocaleChangeManager: + """ + Manages locale hot-reload for a UI window. + + This class handles the complete lifecycle of a locale change: + 1. Save current UI state + 2. Call C++ to reload locale data + 3. Reimport Python locale modules + 4. Recreate UI with new locale + 5. Restore saved state + """ + + def __init__(self, window): + """ + Initialize the locale change manager. + + Args: + window: The UI window instance to manage (e.g., LoginWindow, GameWindow) + """ + self.window = window + self.stateHandlers = {} + self._RegisterDefaultStateHandlers() + + def _RegisterDefaultStateHandlers(self): + """Register default state save/restore handlers for common UI elements.""" + # Text input fields + self.RegisterStateHandler("idEditLine", self._SaveEditLineText, self._RestoreEditLineText) + self.RegisterStateHandler("pwdEditLine", self._SaveEditLineText, self._RestoreEditLineText) + + # List selections + self.RegisterStateHandler("serverList", self._SaveListSelection, self._RestoreListSelection) + self.RegisterStateHandler("channelList", self._SaveListSelection, self._RestoreListSelection) + + # Virtual keyboard + self.RegisterStateHandler("virtualKeyboard", self._SaveVisibility, self._RestoreVisibility) + + # Server info text + self.RegisterStateHandler("serverInfo", self._SaveTextLineText, self._RestoreTextLineText) + + def RegisterStateHandler(self, elementName, saveFunc, restoreFunc): + """ + Register a custom state handler for a UI element. + + Args: + elementName: The attribute name of the UI element (e.g., "idEditLine") + saveFunc: Function(element) -> stateData to save state + restoreFunc: Function(element, stateData) to restore state + """ + self.stateHandlers[elementName] = (saveFunc, restoreFunc) + + def _SaveEditLineText(self, editLine): + """Save text from an edit line.""" + if editLine: + return editLine.GetText() + return "" + + def _RestoreEditLineText(self, editLine, text): + """Restore text to an edit line.""" + if editLine and text: + editLine.SetText(text) + + def _SaveListSelection(self, listBox): + """Save selected item ID from a list box.""" + if listBox: + return listBox.GetSelectedItem() + return None + + def _RestoreListSelection(self, listBox, selectedID): + """Restore selected item in a list box by finding its position.""" + if listBox and selectedID is not None: + # Find position for the saved ID + for position, itemID in listBox.keyDict.items(): + if itemID == selectedID: + listBox.SelectItem(position) + return True + return False + + def _SaveVisibility(self, element): + """Save visibility state of an element.""" + if element: + return element.IsShow() + return False + + def _RestoreVisibility(self, element, wasVisible): + """Restore visibility state of an element.""" + if element: + if wasVisible: + element.Show() + else: + element.Hide() + + def _SaveTextLineText(self, textLine): + """Save text from a text line.""" + if textLine: + return textLine.GetText() + return "" + + def _RestoreTextLineText(self, textLine, text): + """Restore text to a text line.""" + if textLine and text: + textLine.SetText(text) + + def SaveLocaleCode(self, localeCode): + """ + Save locale code to config/locale.cfg file. + + Args: + localeCode: Two-letter locale code (e.g., "en", "ro", "de") + + Returns: + True if saved successfully, False otherwise + """ + try: + import os + if not os.path.exists("config"): + os.makedirs("config") + with open("config/locale.cfg", "w") as f: + f.write(localeCode) + return True + except Exception as e: + dbg.TraceError("Failed to save locale config: %s" % str(e)) + return False + + def ReloadWithNewLocale(self, newLocaleCode, scriptPath="uiscript/LoginWindow.py"): + """ + Hot-reload the UI with a new locale. + + This is the main entry point for locale changes. It handles the complete + reload process: + 1. Save locale code to config file + 2. Call C++ to reload locale data and clear Python module cache + 3. Reimport Python locale modules + 4. Save current UI state + 5. Recreate UI + 6. Restore UI state + + Args: + newLocaleCode: The new locale code to switch to + scriptPath: Path to the UI script file to reload (default: LoginWindow.py) + + Returns: + True if reload succeeded, False otherwise + """ + try: + dbg.TraceError("=== Starting Locale Hot-Reload to '%s' ===" % newLocaleCode) + + # Step 1: Save locale code to file + if not self.SaveLocaleCode(newLocaleCode): + dbg.TraceError("Failed to save locale code") + return False + + # Step 2: Call C++ comprehensive reload + # This does: LoadConfig() → LoadLocaleData() → Clear Python module cache + reloadSuccess = app.ReloadLocale() + if not reloadSuccess: + dbg.TraceError("C++ ReloadLocale() failed") + return False + + # Step 3: Reimport Python locale modules + self._ReimportLocaleModules() + + # Step 4: Save current UI state + savedState = self._SaveWindowState() + + # Step 5: Recreate UI + self._RecreateUI(scriptPath) + + # Step 6: Restore UI state + self._RestoreWindowState(savedState) + + dbg.TraceError("=== Locale Hot-Reload Complete ===") + return True + + except Exception as e: + dbg.TraceError("Error in ReloadWithNewLocale: %s" % str(e)) + import exception + exception.Abort("ReloadWithNewLocale") + return False + + def _ReimportLocaleModules(self): + """Force reimport of locale modules after C++ cleared the cache.""" + dbg.TraceError("Reimporting locale modules...") + + # Import fresh modules - C++ already deleted them from sys.modules + localeInfo = __import__('localeInfo') + uiScriptLocale = __import__('uiScriptLocale') + + # Update sys.modules references + sys.modules['localeInfo'] = localeInfo + sys.modules['uiScriptLocale'] = uiScriptLocale + + # CRITICAL: Update the window module's globals, not ours! + # Get the window's module (e.g., intrologin module) + windowModule = sys.modules.get(self.window.__module__) + if windowModule: + dbg.TraceError("Updating globals in module: %s" % self.window.__module__) + windowModule.localeInfo = localeInfo + windowModule.uiScriptLocale = uiScriptLocale + else: + dbg.TraceError("WARNING: Could not find window module: %s" % self.window.__module__) + + # Also update this module's globals for safety + globals()['localeInfo'] = localeInfo + globals()['uiScriptLocale'] = uiScriptLocale + + dbg.TraceError("Locale modules reimported successfully") + + def _SaveWindowState(self): + """ + Save the current state of all registered UI elements. + + Returns: + Dictionary containing saved state data + """ + state = { + "elements": {}, + "visibleBoard": None, + } + + # Save state of registered elements + for elementName, (saveFunc, _) in self.stateHandlers.items(): + if hasattr(self.window, elementName): + element = getattr(self.window, elementName) + if element: + state["elements"][elementName] = saveFunc(element) + + # Determine which board is currently visible + for boardName in ["loginBoard", "serverBoard", "connectBoard"]: + if hasattr(self.window, boardName): + board = getattr(self.window, boardName) + if board and board.IsShow(): + state["visibleBoard"] = boardName + break + + dbg.TraceError("Saved window state: visibleBoard=%s" % state["visibleBoard"]) + return state + + def _RecreateUI(self, scriptPath): + """ + Recreate the UI by clearing and reloading the script. + + Args: + scriptPath: Path to the UI script file + """ + dbg.TraceError("Recreating UI from script: %s" % scriptPath) + + # Completely destroy the locale selector - we'll recreate it fresh + if hasattr(self.window, 'localeSelector') and self.window.localeSelector: + dbg.TraceError("Destroying old locale selector before UI recreation") + self.window.localeSelector.Destroy() + self.window.localeSelector = None + + # Clear existing UI elements + if hasattr(self.window, 'ClearDictionary'): + self.window.ClearDictionary() + + # Reload the UI script file with new locale strings + # This will create a fresh locale selector through __LoadScript + if hasattr(self.window, '_LoginWindow__LoadScript'): + # Private method name mangling for LoginWindow + self.window._LoginWindow__LoadScript(scriptPath) + elif hasattr(self.window, 'LoadScript'): + self.window.LoadScript(scriptPath) + + # __LoadScript should have created a new locale selector + # Make sure it's visible + if hasattr(self.window, 'localeSelector') and self.window.localeSelector: + dbg.TraceError("Locale selector created by __LoadScript, ensuring visibility") + self.window.localeSelector.Show() + self.window.localeSelector.SetTop() + else: + # If __LoadScript didn't create it, create it manually + dbg.TraceError("Creating locale selector manually") + from uilocaleselector import LocaleSelector + self.window.localeSelector = LocaleSelector() + self.window.localeSelector.Create(self.window) + # Set the event handler to call back to the window's method + if hasattr(self.window, '_LoginWindow__OnLocaleChanged'): + import ui + self.window.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.window._LoginWindow__OnLocaleChanged)) + self.window.localeSelector.Show() + self.window.localeSelector.SetTop() + + # Hide all boards to reset state + for boardName in ["loginBoard", "serverBoard", "connectBoard"]: + if hasattr(self.window, boardName): + board = getattr(self.window, boardName) + if board: + board.Hide() + + # Hide virtual keyboard + if hasattr(self.window, "virtualKeyboard"): + vk = getattr(self.window, "virtualKeyboard") + if vk: + vk.Hide() + + def _RestoreWindowState(self, state): + """ + Restore the saved UI state. + + Args: + state: Dictionary containing saved state data + """ + dbg.TraceError("Restoring window state...") + + # Restore element states + for elementName, savedData in state["elements"].items(): + if elementName in self.stateHandlers: + _, restoreFunc = self.stateHandlers[elementName] + if hasattr(self.window, elementName): + element = getattr(self.window, elementName) + if element: + restoreFunc(element, savedData) + + # Rebuild locale-dependent dictionaries (like loginFailureMsgDict) + self._RebuildLocaleDictionaries() + + # Restore visible board + visibleBoard = state.get("visibleBoard") + if visibleBoard: + self._RestoreBoardVisibility(visibleBoard, state) + + # CRITICAL: Make sure locale selector is visible and on top after everything is restored + if hasattr(self.window, 'localeSelector') and self.window.localeSelector: + dbg.TraceError("Final check: ensuring locale selector is visible") + self.window.localeSelector.Show() + self.window.localeSelector.SetTop() + + def _RebuildLocaleDictionaries(self): + """Rebuild any dictionaries that depend on localeInfo strings.""" + # Check if this is a LoginWindow with loginFailureMsgDict + if hasattr(self.window, 'loginFailureMsgDict'): + import localeInfo + dbg.TraceError("Rebuilding loginFailureMsgDict with new locale strings") + self.window.loginFailureMsgDict = { + "ALREADY" : localeInfo.LOGIN_FAILURE_ALREAY, + "NOID" : localeInfo.LOGIN_FAILURE_NOT_EXIST_ID, + "WRONGPWD" : localeInfo.LOGIN_FAILURE_WRONG_PASSWORD, + "FULL" : localeInfo.LOGIN_FAILURE_TOO_MANY_USER, + "SHUTDOWN" : localeInfo.LOGIN_FAILURE_SHUTDOWN, + "REPAIR" : localeInfo.LOGIN_FAILURE_REPAIR_ID, + "BLOCK" : localeInfo.LOGIN_FAILURE_BLOCK_ID, + "BESAMEKEY" : localeInfo.LOGIN_FAILURE_BE_SAME_KEY, + "NOTAVAIL" : localeInfo.LOGIN_FAILURE_NOT_AVAIL, + "NOBILL" : localeInfo.LOGIN_FAILURE_NOBILL, + "BLKLOGIN" : localeInfo.LOGIN_FAILURE_BLOCK_LOGIN, + "WEBBLK" : localeInfo.LOGIN_FAILURE_WEB_BLOCK, + "BADSCLID" : localeInfo.LOGIN_FAILURE_WRONG_SOCIALID, + "AGELIMIT" : localeInfo.LOGIN_FAILURE_SHUTDOWN_TIME, + } + + if hasattr(self.window, 'loginFailureFuncDict'): + self.window.loginFailureFuncDict = { + "WRONGPWD" : self.window._LoginWindow__DisconnectAndInputPassword if hasattr(self.window, '_LoginWindow__DisconnectAndInputPassword') else None, + "QUIT" : __import__('app').Exit, + } + + def _RestoreBoardVisibility(self, boardName, state): + """ + Restore the visibility of a specific board and its state. + + Args: + boardName: Name of the board to show ("loginBoard", "serverBoard", "connectBoard") + state: Full saved state dictionary + """ + dbg.TraceError("Restoring board visibility: %s" % boardName) + + if boardName == "loginBoard": + self._RestoreLoginBoard() + elif boardName == "serverBoard": + self._RestoreServerBoard(state) + elif boardName == "connectBoard": + self._RestoreConnectBoard() + + def _RestoreLoginBoard(self): + """Show and configure the login board.""" + if hasattr(self.window, "loginBoard"): + self.window.loginBoard.Show() + + def _RestoreServerBoard(self, state): + """Show and configure the server board with saved selections.""" + if not hasattr(self.window, "serverBoard"): + return + + self.window.serverBoard.Show() + + # Refresh server list first + if hasattr(self.window, '_LoginWindow__RefreshServerList'): + self.window._LoginWindow__RefreshServerList() + + # Now restore server selection AFTER the list is refreshed + savedServerID = state["elements"].get("serverList") + if savedServerID is not None and hasattr(self.window, 'serverList') and self.window.serverList: + # Find the position index for the saved server ID + # keyDict maps position -> server ID + for position, serverID in self.window.serverList.keyDict.items(): + if serverID == savedServerID: + # SelectItem expects position index, not server ID + self.window.serverList.SelectItem(position) + + # Refresh channel list for the selected server + if hasattr(self.window, '_LoginWindow__RequestServerStateList'): + self.window._LoginWindow__RequestServerStateList() + if hasattr(self.window, '_LoginWindow__RefreshServerStateList'): + self.window._LoginWindow__RefreshServerStateList() + + # Restore channel selection AFTER channel list is refreshed + savedChannelID = state["elements"].get("channelList") + if savedChannelID is not None and hasattr(self.window, 'channelList') and self.window.channelList: + # Find the position index for the saved channel ID + for channelPos, channelID in self.window.channelList.keyDict.items(): + if channelID == savedChannelID: + self.window.channelList.SelectItem(channelPos) + break + break + + def _RestoreConnectBoard(self): + """Show and configure the connect board.""" + if not hasattr(self.window, "connectBoard"): + return + + # Connect board overlays login board + if hasattr(self.window, "loginBoard"): + self.window.loginBoard.Show() + + self.window.connectBoard.Show() + + # Explicitly show connect board children (they may not auto-show) + if hasattr(self.window, "selectConnectButton"): + btn = getattr(self.window, "selectConnectButton") + if btn: + btn.Show() + + if hasattr(self.window, "serverInfo"): + info = getattr(self.window, "serverInfo") + if info: + info.Show() diff --git a/assets/root/uilocaleselector.py b/assets/root/uilocaleselector.py new file mode 100644 index 00000000..e32a49e2 --- /dev/null +++ b/assets/root/uilocaleselector.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +""" +Locale Selector UI Component +============================= + +A reusable UI component for selecting and changing the client language. +Can be added to any window (login, game settings, etc.). + +Usage: + from uilocaleselector import LocaleSelector + + # In your window class: + self.localeSelector = LocaleSelector() + self.localeSelector.Create(self) + self.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.__OnLocaleChanged)) + + # Implement the callback: + def __OnLocaleChanged(self, newLocaleCode): + # Handle UI recreation here + pass +""" + +import ui +import uiCommon +import localeInfo +import wndMgr + + +# Available locales configuration +AVAILABLE_LOCALES = [ + {"code": "ae", "name": "Arabic", "flag": "ae"}, + {"code": "en", "name": "English", "flag": "en"}, + {"code": "cz", "name": "Čeština", "flag": "cz"}, + {"code": "de", "name": "Deutsch", "flag": "de"}, + {"code": "dk", "name": "Dansk", "flag": "dk"}, + {"code": "es", "name": "Español", "flag": "es"}, + {"code": "fr", "name": "Français", "flag": "fr"}, + {"code": "gr", "name": "Ελληνικά", "flag": "gr"}, + {"code": "hu", "name": "Magyar", "flag": "hu"}, + {"code": "it", "name": "Italiano", "flag": "it"}, + {"code": "nl", "name": "Nederlands", "flag": "nl"}, + {"code": "pl", "name": "Polski", "flag": "pl"}, + {"code": "pt", "name": "Português", "flag": "pt"}, + {"code": "ro", "name": "Română", "flag": "ro"}, + {"code": "ru", "name": "Русский", "flag": "ru"}, + {"code": "tr", "name": "Türkçe", "flag": "tr"}, +] + +# Flag image path template +FLAG_IMAGE_PATH = "d:/ymir work/ui/intro/login/server_flag_%s.sub" + + +class LocaleSelector(ui.Window): + """ + UI component for selecting and changing client language. + + Features: + - Displays flag buttons for all available locales + - Shows confirmation dialog before changing + - Triggers callback when locale is confirmed + - Self-contained and reusable + """ + + def __init__(self): + ui.Window.__init__(self) + self.background = None + self.flagButtons = [] + self.confirmDialog = None + self.selectedLocaleCode = None + self.eventLocaleChanged = None + + def __del__(self): + ui.Window.__del__(self) + + def Destroy(self): + """Clean up resources when destroying the selector.""" + self.eventLocaleChanged = None + self.selectedLocaleCode = None + + if self.confirmDialog: + self.confirmDialog.Close() + self.confirmDialog = None + + for btn in self.flagButtons: + btn.Hide() + btn = None + self.flagButtons = [] + + if self.background: + self.background.Hide() + self.background = None + + def Create(self, parent): + """ + Create and display the locale selector UI. + + Args: + parent: The parent window to attach to + """ + self.SetParent(parent) + self.SetSize(wndMgr.GetScreenWidth(), 35) + self.SetPosition(0, 20) + + # Create background board + self.background = ui.ThinBoard() + self.background.SetParent(self) + self.background.SetSize(wndMgr.GetScreenWidth(), 35) + self.background.SetPosition(0, 0) + self.background.Show() + + # Create flag buttons + self._CreateFlagButtons() + + self.Show() + + def _CreateFlagButtons(self): + """Create flag buttons for all available locales.""" + localeCount = len(AVAILABLE_LOCALES) + if localeCount == 0: + return + + buttonSpacing = wndMgr.GetScreenWidth() / localeCount + xPosition = 0 + + for locale in AVAILABLE_LOCALES: + flagPath = FLAG_IMAGE_PATH % locale["flag"] + + button = ui.Button() + button.SetParent(self.background) + button.SetPosition(xPosition + 15, 10) + button.SetUpVisual(flagPath) + button.SetOverVisual(flagPath) + button.SetDownVisual(flagPath) + button.SetToolTipText(locale["name"]) + button.SetEvent(ui.__mem_func__(self._OnClickFlag), locale["code"]) + button.Show() + + self.flagButtons.append(button) + xPosition += buttonSpacing + + def _OnClickFlag(self, localeCode): + """ + Handle flag button click - show confirmation dialog. + + Args: + localeCode: The locale code that was clicked + """ + self.selectedLocaleCode = localeCode + + # Get locale name for display + localeName = "Unknown" + for locale in AVAILABLE_LOCALES: + if locale["code"] == localeCode: + localeName = locale["name"] + break + + # Show confirmation dialog + if not self.confirmDialog: + self.confirmDialog = uiCommon.QuestionDialog() + + self.confirmDialog.SetText(localeInfo.LOCALE_CHANGE_CONFIRM % localeName) + self.confirmDialog.SetAcceptEvent(ui.__mem_func__(self._OnConfirmLocaleChange)) + self.confirmDialog.SetCancelEvent(ui.__mem_func__(self._OnCancelLocaleChange)) + self.confirmDialog.Open() + + def _OnConfirmLocaleChange(self): + """User confirmed locale change - trigger the callback.""" + if self.confirmDialog: + self.confirmDialog.Close() + + if not self.selectedLocaleCode: + return + + # Notify parent window to handle the locale change + if self.eventLocaleChanged: + self.eventLocaleChanged(self.selectedLocaleCode) + + def _OnCancelLocaleChange(self): + """User cancelled locale change.""" + if self.confirmDialog: + self.confirmDialog.Close() + self.selectedLocaleCode = None + + def SetLocaleChangedEvent(self, event): + """ + Set callback function to be called when locale is confirmed. + + Args: + event: Callback function(localeCode) to handle locale change + """ + self.eventLocaleChanged = event From 585980817132874247f2922d9e33212e16ba343b Mon Sep 17 00:00:00 2001 From: rtw1x1 Date: Wed, 7 Jan 2026 09:54:28 +0000 Subject: [PATCH 2/2] ML: Refactored Hot-Reload System --- assets/root/intrologin.py | 91 ++++++- assets/root/localeinfo.py | 24 +- assets/root/uilocalechange.py | 472 --------------------------------- assets/root/uilocalerefresh.py | 242 +++++++++++++++++ assets/root/uiscriptlocale.py | 47 +++- 5 files changed, 396 insertions(+), 480 deletions(-) delete mode 100644 assets/root/uilocalechange.py create mode 100644 assets/root/uilocalerefresh.py diff --git a/assets/root/intrologin.py b/assets/root/intrologin.py index 77955da2..5f35868d 100644 --- a/assets/root/intrologin.py +++ b/assets/root/intrologin.py @@ -19,7 +19,6 @@ import uiScriptLocale # Multi-language hot-reload system from uilocaleselector import LocaleSelector -from uilocalechange import LocaleChangeManager LOGIN_DELAY_SEC = 0.0 SKIP_LOGIN_PHASE = False @@ -157,7 +156,6 @@ class LoginWindow(ui.ScriptWindow): # Multi-language hot-reload system self.localeSelector = None - self.localeChangeManager = LocaleChangeManager(self) def __del__(self): net.ClearPhaseWindow(net.PHASE_WINDOW_LOGIN, self) @@ -599,11 +597,94 @@ class LoginWindow(ui.ScriptWindow): self.stream.SetPhaseWindow(0) def __OnLocaleChanged(self, newLocaleCode): + """Handle locale change - save config, reload, and refresh UI""" + import dbg + + # 1) Save locale code to config/locale.cfg + try: + import os + if not os.path.exists("config"): + os.makedirs("config") + with open("config/locale.cfg", "w") as f: + f.write(newLocaleCode) + dbg.TraceError("Saved new locale to config: %s" % newLocaleCode) + except Exception as e: + dbg.TraceError("Failed to save locale.cfg: %s" % str(e)) + return + + # 2) Call C++ to reload locale data (C++ data + Python modules) + if not app.ReloadLocale(): + dbg.TraceError("app.ReloadLocale() failed") + return + + dbg.TraceError("Locale changed successfully, refreshing UI...") + + # 3) Refresh all UI text elements with new locale + self.__RefreshLocaleUI() + + def __RefreshLocaleUI(self): """ - Callback when user confirms locale change. - All the heavy lifting is done by LocaleChangeManager - this is just 3 lines! + Refresh all UI text elements after locale change + + Uses the generic uiLocaleRefresh module to update all visible text elements + without needing to reload the entire UI. """ - self.localeChangeManager.ReloadWithNewLocale(newLocaleCode, "uiscript/LoginWindow.py") + import uiScriptLocale + import uiLocaleRefresh + + try: + # Refresh button and text elements using element mapping + elementMapping = { + self.serverInfo: "LOGIN_DEFAULT_SERVERADDR", + self.selectConnectButton: "LOGIN_SELECT_BUTTON", + self.loginButton: "LOGIN_CONNECT", + self.loginExitButton: "LOGIN_EXIT", + self.serverSelectButton: "OK", + self.serverExitButton: "LOGIN_SELECT_EXIT", + } + uiLocaleRefresh.RefreshByMapping(elementMapping) + + # Refresh ServerBoard Title (special case - accessed via GetChild) + try: + serverBoardTitle = self.GetChild("Title") + serverBoardTitle.SetText(uiScriptLocale.LOGIN_SELECT_TITLE) + except: + pass + + # Rebuild login error message dictionary with new locale strings + loginFailureTemplate = { + "ALREADY" : "LOGIN_FAILURE_ALREAY", + "NOID" : "LOGIN_FAILURE_NOT_EXIST_ID", + "WRONGPWD" : "LOGIN_FAILURE_WRONG_PASSWORD", + "FULL" : "LOGIN_FAILURE_TOO_MANY_USER", + "SHUTDOWN" : "LOGIN_FAILURE_SHUTDOWN", + "REPAIR" : "LOGIN_FAILURE_REPAIR_ID", + "BLOCK" : "LOGIN_FAILURE_BLOCK_ID", + "BESAMEKEY" : "LOGIN_FAILURE_BE_SAME_KEY", + "NOTAVAIL" : "LOGIN_FAILURE_NOT_AVAIL", + "NOBILL" : "LOGIN_FAILURE_NOBILL", + "BLKLOGIN" : "LOGIN_FAILURE_BLOCK_LOGIN", + "WEBBLK" : "LOGIN_FAILURE_WEB_BLOCK", + "BADSCLID" : "LOGIN_FAILURE_WRONG_SOCIALID", + "AGELIMIT" : "LOGIN_FAILURE_SHUTDOWN_TIME", + } + self.loginFailureMsgDict = uiLocaleRefresh.RebuildDictionary(loginFailureTemplate, "localeInfo") + + # Recreate locale selector to ensure it's on top with updated text + if self.localeSelector: + self.localeSelector.Destroy() + self.localeSelector = None + + self.localeSelector = LocaleSelector() + self.localeSelector.Create(self) + self.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.__OnLocaleChanged)) + self.localeSelector.Show() + self.localeSelector.SetTop() + + except: + # import dbg + # dbg.TraceError("LoginWindow.__RefreshLocaleUI failed") + pass def __SetServerInfo(self, name): net.SetServerInfo(name.strip()) diff --git a/assets/root/localeinfo.py b/assets/root/localeinfo.py index 9cae9f65..fe6e8790 100644 --- a/assets/root/localeinfo.py +++ b/assets/root/localeinfo.py @@ -31,9 +31,29 @@ VIRTUAL_KEY_SYMBOLS = "!@#$%^&*()_+|{}:'<>?~" VIRTUAL_KEY_NUMBERS = "1234567890-=\[];',./`" VIRTUAL_KEY_SYMBOLS_BR = "!@#$%^&*()_+|{}:'<>?~aaaaeeeiioooouuc" -# Load locale data by specific path +# Multi-language hot-reload support def LoadLocaleData(): - app.LoadLocaleData(app.GetLocalePath()) + """ + Reload all game locale text strings from locale_game.txt + + Called by app.ReloadLocale() when the user changes language. + Reloads locale_game.txt and updates all module-level locale strings. + + Returns: + True on success, False on failure + """ + try: + localePath = app.GetLocalePath() + localeFilePath = "{:s}/locale_game.txt".format(localePath) + + # Reload locale_game.txt - this updates all global variables in this module + LoadLocaleFile(localeFilePath, globals()) + + return True + except: + # import dbg + # dbg.TraceError("localeInfo.LoadLocaleData failed") + return False # Load locale_game.txt def LoadLocaleFile(srcFileName, localeDict): diff --git a/assets/root/uilocalechange.py b/assets/root/uilocalechange.py deleted file mode 100644 index 54096089..00000000 --- a/assets/root/uilocalechange.py +++ /dev/null @@ -1,472 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Multi-Language Hot-Reload System -================================= - -This module provides a clean, reusable system for hot-reloading locale changes -without restarting the client. It's designed to be window-agnostic and can be -used with any UI window (login, game, etc.). - -Architecture: -- C++ side handles: LoadConfig() → LoadLocaleData() → Clear Python module cache -- Python side handles: Module reimport → UI state save/restore → Window recreation - -Usage: - from uilocalechange import LocaleChangeManager - - # In your window class __init__: - self.localeChangeManager = LocaleChangeManager(self) - - # When locale changes (e.g., from a locale selector): - self.localeChangeManager.ReloadWithNewLocale(newLocaleCode) - -Features: -- Automatically saves and restores UI state (text inputs, selections, etc.) -- Handles board visibility states -- Preserves virtual keyboard state -- Works with any window that follows the standard pattern -""" - -import app -import dbg -import ui -import sys - - -class LocaleChangeManager: - """ - Manages locale hot-reload for a UI window. - - This class handles the complete lifecycle of a locale change: - 1. Save current UI state - 2. Call C++ to reload locale data - 3. Reimport Python locale modules - 4. Recreate UI with new locale - 5. Restore saved state - """ - - def __init__(self, window): - """ - Initialize the locale change manager. - - Args: - window: The UI window instance to manage (e.g., LoginWindow, GameWindow) - """ - self.window = window - self.stateHandlers = {} - self._RegisterDefaultStateHandlers() - - def _RegisterDefaultStateHandlers(self): - """Register default state save/restore handlers for common UI elements.""" - # Text input fields - self.RegisterStateHandler("idEditLine", self._SaveEditLineText, self._RestoreEditLineText) - self.RegisterStateHandler("pwdEditLine", self._SaveEditLineText, self._RestoreEditLineText) - - # List selections - self.RegisterStateHandler("serverList", self._SaveListSelection, self._RestoreListSelection) - self.RegisterStateHandler("channelList", self._SaveListSelection, self._RestoreListSelection) - - # Virtual keyboard - self.RegisterStateHandler("virtualKeyboard", self._SaveVisibility, self._RestoreVisibility) - - # Server info text - self.RegisterStateHandler("serverInfo", self._SaveTextLineText, self._RestoreTextLineText) - - def RegisterStateHandler(self, elementName, saveFunc, restoreFunc): - """ - Register a custom state handler for a UI element. - - Args: - elementName: The attribute name of the UI element (e.g., "idEditLine") - saveFunc: Function(element) -> stateData to save state - restoreFunc: Function(element, stateData) to restore state - """ - self.stateHandlers[elementName] = (saveFunc, restoreFunc) - - def _SaveEditLineText(self, editLine): - """Save text from an edit line.""" - if editLine: - return editLine.GetText() - return "" - - def _RestoreEditLineText(self, editLine, text): - """Restore text to an edit line.""" - if editLine and text: - editLine.SetText(text) - - def _SaveListSelection(self, listBox): - """Save selected item ID from a list box.""" - if listBox: - return listBox.GetSelectedItem() - return None - - def _RestoreListSelection(self, listBox, selectedID): - """Restore selected item in a list box by finding its position.""" - if listBox and selectedID is not None: - # Find position for the saved ID - for position, itemID in listBox.keyDict.items(): - if itemID == selectedID: - listBox.SelectItem(position) - return True - return False - - def _SaveVisibility(self, element): - """Save visibility state of an element.""" - if element: - return element.IsShow() - return False - - def _RestoreVisibility(self, element, wasVisible): - """Restore visibility state of an element.""" - if element: - if wasVisible: - element.Show() - else: - element.Hide() - - def _SaveTextLineText(self, textLine): - """Save text from a text line.""" - if textLine: - return textLine.GetText() - return "" - - def _RestoreTextLineText(self, textLine, text): - """Restore text to a text line.""" - if textLine and text: - textLine.SetText(text) - - def SaveLocaleCode(self, localeCode): - """ - Save locale code to config/locale.cfg file. - - Args: - localeCode: Two-letter locale code (e.g., "en", "ro", "de") - - Returns: - True if saved successfully, False otherwise - """ - try: - import os - if not os.path.exists("config"): - os.makedirs("config") - with open("config/locale.cfg", "w") as f: - f.write(localeCode) - return True - except Exception as e: - dbg.TraceError("Failed to save locale config: %s" % str(e)) - return False - - def ReloadWithNewLocale(self, newLocaleCode, scriptPath="uiscript/LoginWindow.py"): - """ - Hot-reload the UI with a new locale. - - This is the main entry point for locale changes. It handles the complete - reload process: - 1. Save locale code to config file - 2. Call C++ to reload locale data and clear Python module cache - 3. Reimport Python locale modules - 4. Save current UI state - 5. Recreate UI - 6. Restore UI state - - Args: - newLocaleCode: The new locale code to switch to - scriptPath: Path to the UI script file to reload (default: LoginWindow.py) - - Returns: - True if reload succeeded, False otherwise - """ - try: - dbg.TraceError("=== Starting Locale Hot-Reload to '%s' ===" % newLocaleCode) - - # Step 1: Save locale code to file - if not self.SaveLocaleCode(newLocaleCode): - dbg.TraceError("Failed to save locale code") - return False - - # Step 2: Call C++ comprehensive reload - # This does: LoadConfig() → LoadLocaleData() → Clear Python module cache - reloadSuccess = app.ReloadLocale() - if not reloadSuccess: - dbg.TraceError("C++ ReloadLocale() failed") - return False - - # Step 3: Reimport Python locale modules - self._ReimportLocaleModules() - - # Step 4: Save current UI state - savedState = self._SaveWindowState() - - # Step 5: Recreate UI - self._RecreateUI(scriptPath) - - # Step 6: Restore UI state - self._RestoreWindowState(savedState) - - dbg.TraceError("=== Locale Hot-Reload Complete ===") - return True - - except Exception as e: - dbg.TraceError("Error in ReloadWithNewLocale: %s" % str(e)) - import exception - exception.Abort("ReloadWithNewLocale") - return False - - def _ReimportLocaleModules(self): - """Force reimport of locale modules after C++ cleared the cache.""" - dbg.TraceError("Reimporting locale modules...") - - # Import fresh modules - C++ already deleted them from sys.modules - localeInfo = __import__('localeInfo') - uiScriptLocale = __import__('uiScriptLocale') - - # Update sys.modules references - sys.modules['localeInfo'] = localeInfo - sys.modules['uiScriptLocale'] = uiScriptLocale - - # CRITICAL: Update the window module's globals, not ours! - # Get the window's module (e.g., intrologin module) - windowModule = sys.modules.get(self.window.__module__) - if windowModule: - dbg.TraceError("Updating globals in module: %s" % self.window.__module__) - windowModule.localeInfo = localeInfo - windowModule.uiScriptLocale = uiScriptLocale - else: - dbg.TraceError("WARNING: Could not find window module: %s" % self.window.__module__) - - # Also update this module's globals for safety - globals()['localeInfo'] = localeInfo - globals()['uiScriptLocale'] = uiScriptLocale - - dbg.TraceError("Locale modules reimported successfully") - - def _SaveWindowState(self): - """ - Save the current state of all registered UI elements. - - Returns: - Dictionary containing saved state data - """ - state = { - "elements": {}, - "visibleBoard": None, - } - - # Save state of registered elements - for elementName, (saveFunc, _) in self.stateHandlers.items(): - if hasattr(self.window, elementName): - element = getattr(self.window, elementName) - if element: - state["elements"][elementName] = saveFunc(element) - - # Determine which board is currently visible - for boardName in ["loginBoard", "serverBoard", "connectBoard"]: - if hasattr(self.window, boardName): - board = getattr(self.window, boardName) - if board and board.IsShow(): - state["visibleBoard"] = boardName - break - - dbg.TraceError("Saved window state: visibleBoard=%s" % state["visibleBoard"]) - return state - - def _RecreateUI(self, scriptPath): - """ - Recreate the UI by clearing and reloading the script. - - Args: - scriptPath: Path to the UI script file - """ - dbg.TraceError("Recreating UI from script: %s" % scriptPath) - - # Completely destroy the locale selector - we'll recreate it fresh - if hasattr(self.window, 'localeSelector') and self.window.localeSelector: - dbg.TraceError("Destroying old locale selector before UI recreation") - self.window.localeSelector.Destroy() - self.window.localeSelector = None - - # Clear existing UI elements - if hasattr(self.window, 'ClearDictionary'): - self.window.ClearDictionary() - - # Reload the UI script file with new locale strings - # This will create a fresh locale selector through __LoadScript - if hasattr(self.window, '_LoginWindow__LoadScript'): - # Private method name mangling for LoginWindow - self.window._LoginWindow__LoadScript(scriptPath) - elif hasattr(self.window, 'LoadScript'): - self.window.LoadScript(scriptPath) - - # __LoadScript should have created a new locale selector - # Make sure it's visible - if hasattr(self.window, 'localeSelector') and self.window.localeSelector: - dbg.TraceError("Locale selector created by __LoadScript, ensuring visibility") - self.window.localeSelector.Show() - self.window.localeSelector.SetTop() - else: - # If __LoadScript didn't create it, create it manually - dbg.TraceError("Creating locale selector manually") - from uilocaleselector import LocaleSelector - self.window.localeSelector = LocaleSelector() - self.window.localeSelector.Create(self.window) - # Set the event handler to call back to the window's method - if hasattr(self.window, '_LoginWindow__OnLocaleChanged'): - import ui - self.window.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.window._LoginWindow__OnLocaleChanged)) - self.window.localeSelector.Show() - self.window.localeSelector.SetTop() - - # Hide all boards to reset state - for boardName in ["loginBoard", "serverBoard", "connectBoard"]: - if hasattr(self.window, boardName): - board = getattr(self.window, boardName) - if board: - board.Hide() - - # Hide virtual keyboard - if hasattr(self.window, "virtualKeyboard"): - vk = getattr(self.window, "virtualKeyboard") - if vk: - vk.Hide() - - def _RestoreWindowState(self, state): - """ - Restore the saved UI state. - - Args: - state: Dictionary containing saved state data - """ - dbg.TraceError("Restoring window state...") - - # Restore element states - for elementName, savedData in state["elements"].items(): - if elementName in self.stateHandlers: - _, restoreFunc = self.stateHandlers[elementName] - if hasattr(self.window, elementName): - element = getattr(self.window, elementName) - if element: - restoreFunc(element, savedData) - - # Rebuild locale-dependent dictionaries (like loginFailureMsgDict) - self._RebuildLocaleDictionaries() - - # Restore visible board - visibleBoard = state.get("visibleBoard") - if visibleBoard: - self._RestoreBoardVisibility(visibleBoard, state) - - # CRITICAL: Make sure locale selector is visible and on top after everything is restored - if hasattr(self.window, 'localeSelector') and self.window.localeSelector: - dbg.TraceError("Final check: ensuring locale selector is visible") - self.window.localeSelector.Show() - self.window.localeSelector.SetTop() - - def _RebuildLocaleDictionaries(self): - """Rebuild any dictionaries that depend on localeInfo strings.""" - # Check if this is a LoginWindow with loginFailureMsgDict - if hasattr(self.window, 'loginFailureMsgDict'): - import localeInfo - dbg.TraceError("Rebuilding loginFailureMsgDict with new locale strings") - self.window.loginFailureMsgDict = { - "ALREADY" : localeInfo.LOGIN_FAILURE_ALREAY, - "NOID" : localeInfo.LOGIN_FAILURE_NOT_EXIST_ID, - "WRONGPWD" : localeInfo.LOGIN_FAILURE_WRONG_PASSWORD, - "FULL" : localeInfo.LOGIN_FAILURE_TOO_MANY_USER, - "SHUTDOWN" : localeInfo.LOGIN_FAILURE_SHUTDOWN, - "REPAIR" : localeInfo.LOGIN_FAILURE_REPAIR_ID, - "BLOCK" : localeInfo.LOGIN_FAILURE_BLOCK_ID, - "BESAMEKEY" : localeInfo.LOGIN_FAILURE_BE_SAME_KEY, - "NOTAVAIL" : localeInfo.LOGIN_FAILURE_NOT_AVAIL, - "NOBILL" : localeInfo.LOGIN_FAILURE_NOBILL, - "BLKLOGIN" : localeInfo.LOGIN_FAILURE_BLOCK_LOGIN, - "WEBBLK" : localeInfo.LOGIN_FAILURE_WEB_BLOCK, - "BADSCLID" : localeInfo.LOGIN_FAILURE_WRONG_SOCIALID, - "AGELIMIT" : localeInfo.LOGIN_FAILURE_SHUTDOWN_TIME, - } - - if hasattr(self.window, 'loginFailureFuncDict'): - self.window.loginFailureFuncDict = { - "WRONGPWD" : self.window._LoginWindow__DisconnectAndInputPassword if hasattr(self.window, '_LoginWindow__DisconnectAndInputPassword') else None, - "QUIT" : __import__('app').Exit, - } - - def _RestoreBoardVisibility(self, boardName, state): - """ - Restore the visibility of a specific board and its state. - - Args: - boardName: Name of the board to show ("loginBoard", "serverBoard", "connectBoard") - state: Full saved state dictionary - """ - dbg.TraceError("Restoring board visibility: %s" % boardName) - - if boardName == "loginBoard": - self._RestoreLoginBoard() - elif boardName == "serverBoard": - self._RestoreServerBoard(state) - elif boardName == "connectBoard": - self._RestoreConnectBoard() - - def _RestoreLoginBoard(self): - """Show and configure the login board.""" - if hasattr(self.window, "loginBoard"): - self.window.loginBoard.Show() - - def _RestoreServerBoard(self, state): - """Show and configure the server board with saved selections.""" - if not hasattr(self.window, "serverBoard"): - return - - self.window.serverBoard.Show() - - # Refresh server list first - if hasattr(self.window, '_LoginWindow__RefreshServerList'): - self.window._LoginWindow__RefreshServerList() - - # Now restore server selection AFTER the list is refreshed - savedServerID = state["elements"].get("serverList") - if savedServerID is not None and hasattr(self.window, 'serverList') and self.window.serverList: - # Find the position index for the saved server ID - # keyDict maps position -> server ID - for position, serverID in self.window.serverList.keyDict.items(): - if serverID == savedServerID: - # SelectItem expects position index, not server ID - self.window.serverList.SelectItem(position) - - # Refresh channel list for the selected server - if hasattr(self.window, '_LoginWindow__RequestServerStateList'): - self.window._LoginWindow__RequestServerStateList() - if hasattr(self.window, '_LoginWindow__RefreshServerStateList'): - self.window._LoginWindow__RefreshServerStateList() - - # Restore channel selection AFTER channel list is refreshed - savedChannelID = state["elements"].get("channelList") - if savedChannelID is not None and hasattr(self.window, 'channelList') and self.window.channelList: - # Find the position index for the saved channel ID - for channelPos, channelID in self.window.channelList.keyDict.items(): - if channelID == savedChannelID: - self.window.channelList.SelectItem(channelPos) - break - break - - def _RestoreConnectBoard(self): - """Show and configure the connect board.""" - if not hasattr(self.window, "connectBoard"): - return - - # Connect board overlays login board - if hasattr(self.window, "loginBoard"): - self.window.loginBoard.Show() - - self.window.connectBoard.Show() - - # Explicitly show connect board children (they may not auto-show) - if hasattr(self.window, "selectConnectButton"): - btn = getattr(self.window, "selectConnectButton") - if btn: - btn.Show() - - if hasattr(self.window, "serverInfo"): - info = getattr(self.window, "serverInfo") - if info: - info.Show() diff --git a/assets/root/uilocalerefresh.py b/assets/root/uilocalerefresh.py new file mode 100644 index 00000000..fb012509 --- /dev/null +++ b/assets/root/uilocalerefresh.py @@ -0,0 +1,242 @@ +""" +Generic UI Locale Refresh System +This module provides automatic locale refresh for UI windows without hardcoding element names. +""" + +import dbg +import ui + +class LocaleRefreshHelper: + """ + Helper class to automatically refresh UI text elements when locale changes. + Works by re-reading the original UI script and applying new locale strings. + """ + + def __init__(self): + self.scriptCache = {} # Cache loaded UI scripts + + def RefreshWindow(self, window, scriptPath): + """ + Automatically refresh all text elements in a window by re-reading the UI script. + + Args: + window: The ui.ScriptWindow instance to refresh + scriptPath: Path to the UI script file (e.g., "UIScript/LoginWindow.py") + + Returns: + Number of elements successfully refreshed + """ + import uiScriptLocale + import localeInfo + + dbg.TraceError("LocaleRefreshHelper: Refreshing window from %s" % scriptPath) + + # Load the UI script to get the original text definitions + try: + scriptData = self._LoadUIScript(scriptPath) + except Exception as e: + dbg.TraceError("LocaleRefreshHelper: Failed to load script %s: %s" % (scriptPath, str(e))) + return 0 + + # Recursively refresh all elements + refreshCount = self._RefreshElement(window, scriptData.get("window", {}), window) + + dbg.TraceError("LocaleRefreshHelper: Refreshed %d elements" % refreshCount) + return refreshCount + + def RefreshElementsByMapping(self, elementMap): + """ + Refresh UI elements using a manual mapping dictionary. + Useful for elements that can't be auto-detected. + + Args: + elementMap: Dict of {element_instance: locale_string_name} + + Example: + mapping = { + self.loginButton: "LOGIN_CONNECT", + self.exitButton: "LOGIN_EXIT" + } + helper.RefreshElementsByMapping(mapping) + """ + import uiScriptLocale + import localeInfo + + refreshCount = 0 + for element, localeKey in elementMap.items(): + try: + # Try uiScriptLocale first, then localeInfo + if hasattr(uiScriptLocale, localeKey): + text = getattr(uiScriptLocale, localeKey) + elif hasattr(localeInfo, localeKey): + text = getattr(localeInfo, localeKey) + else: + dbg.TraceError("LocaleRefreshHelper: Locale key not found: %s" % localeKey) + continue + + # Set the text + if hasattr(element, 'SetText'): + element.SetText(text) + refreshCount += 1 + except Exception as e: + dbg.TraceError("LocaleRefreshHelper: Failed to refresh element with key %s: %s" % (localeKey, str(e))) + + return refreshCount + + def RefreshDictionaries(self, targetDict, localeModule="localeInfo"): + """ + Rebuild a dictionary with fresh locale strings. + Useful for error message dictionaries, etc. + + Args: + targetDict: Dictionary to rebuild with format {key: "LOCALE_CONSTANT_NAME"} + localeModule: Name of the locale module ("localeInfo" or "uiScriptLocale") + + Returns: + New dictionary with fresh locale values + + Example: + template = { + "WRONGPWD": "LOGIN_FAILURE_WRONG_PASSWORD", + "FULL": "LOGIN_FAILURE_TOO_MANY_USER" + } + newDict = helper.RefreshDictionaries(template) + """ + import localeInfo + import uiScriptLocale + + module = localeInfo if localeModule == "localeInfo" else uiScriptLocale + newDict = {} + + for key, localeKey in targetDict.items(): + if hasattr(module, localeKey): + newDict[key] = getattr(module, localeKey) + else: + dbg.TraceError("LocaleRefreshHelper: Locale key not found: %s" % localeKey) + + return newDict + + def _LoadUIScript(self, scriptPath): + """Load and cache a UI script file.""" + if scriptPath in self.scriptCache: + return self.scriptCache[scriptPath] + + # Execute the UI script to get its data + scriptData = {} + try: + execfile(scriptPath, scriptData) + self.scriptCache[scriptPath] = scriptData + except Exception as e: + dbg.TraceError("LocaleRefreshHelper: Failed to execute script %s: %s" % (scriptPath, str(e))) + raise + + return scriptData + + def _RefreshElement(self, windowInstance, elementDef, currentElement): + """ + Recursively refresh an element and its children. + + Args: + windowInstance: The root window instance + elementDef: Element definition from UI script + currentElement: Current UI element instance + + Returns: + Number of elements refreshed + """ + import uiScriptLocale + import localeInfo + + refreshCount = 0 + + # If this element has text defined in the script, refresh it + if isinstance(elementDef, dict) and "text" in elementDef: + textDef = elementDef["text"] + + # Check if it's a locale reference (starts with uiScriptLocale or localeInfo) + if isinstance(textDef, str): + text = self._ResolveLocaleString(textDef) + if text and hasattr(currentElement, 'SetText'): + try: + currentElement.SetText(text) + refreshCount += 1 + except: + pass + + # Recursively process children + if isinstance(elementDef, dict) and "children" in elementDef: + children = elementDef.get("children", []) + for childDef in children: + if isinstance(childDef, dict) and "name" in childDef: + childName = childDef["name"] + try: + childElement = windowInstance.GetChild(childName) + refreshCount += self._RefreshElement(windowInstance, childDef, childElement) + except: + pass + + return refreshCount + + def _ResolveLocaleString(self, textDef): + """ + Resolve a locale string reference to its current value. + + Args: + textDef: String like "uiScriptLocale.LOGIN_CONNECT" or direct text + + Returns: + The resolved locale string or None + """ + import uiScriptLocale + import localeInfo + + # Check if it's a locale reference + if "uiScriptLocale." in str(textDef): + # Extract the attribute name + parts = str(textDef).split(".") + if len(parts) >= 2: + attrName = parts[-1] + if hasattr(uiScriptLocale, attrName): + return getattr(uiScriptLocale, attrName) + + elif "localeInfo." in str(textDef): + parts = str(textDef).split(".") + if len(parts) >= 2: + attrName = parts[-1] + if hasattr(localeInfo, attrName): + return getattr(localeInfo, attrName) + + return None + + +# Global helper instance for easy access +_globalHelper = LocaleRefreshHelper() + +def RefreshWindowByScript(window, scriptPath): + """ + Convenience function to refresh a window using its UI script. + + Args: + window: The ui.ScriptWindow instance + scriptPath: Path to UI script (e.g., "UIScript/LoginWindow.py") + """ + return _globalHelper.RefreshWindow(window, scriptPath) + +def RefreshByMapping(elementMap): + """ + Convenience function to refresh elements by mapping. + + Args: + elementMap: Dict of {element: "LOCALE_KEY"} + """ + return _globalHelper.RefreshElementsByMapping(elementMap) + +def RebuildDictionary(template, localeModule="localeInfo"): + """ + Convenience function to rebuild a dictionary with fresh locale strings. + + Args: + template: Dict of {key: "LOCALE_KEY"} + localeModule: "localeInfo" or "uiScriptLocale" + """ + return _globalHelper.RefreshDictionaries(template, localeModule) diff --git a/assets/root/uiscriptlocale.py b/assets/root/uiscriptlocale.py index 25d4bc0f..f7fee936 100644 --- a/assets/root/uiscriptlocale.py +++ b/assets/root/uiscriptlocale.py @@ -47,4 +47,49 @@ EMPIREDESC_B = "%s/empiredesc_b.txt" % (name) EMPIREDESC_C = "%s/empiredesc_c.txt" % (name) LOCALE_INTERFACE_FILE_NAME = "%s/locale_interface.txt" % (name) -LoadLocaleFile(LOCALE_INTERFACE_FILE_NAME, locals()) \ No newline at end of file +LoadLocaleFile(LOCALE_INTERFACE_FILE_NAME, locals()) + +def LoadLocaleData(): + """ + Reload all UI locale strings from locale_interface.txt + + Called by app.ReloadLocale() when the user changes language. + Updates all locale-dependent paths and reloads locale_interface.txt. + + Returns: + True on success, False on failure + """ + try: + # Update all locale-dependent paths + global name, LOCALE_UISCRIPT_PATH, LOGIN_PATH, EMPIRE_PATH, GUILD_PATH, SELECT_PATH, WINDOWS_PATH, MAPNAME_PATH + global JOBDESC_WARRIOR_PATH, JOBDESC_ASSASSIN_PATH, JOBDESC_SURA_PATH, JOBDESC_SHAMAN_PATH + global EMPIREDESC_A, EMPIREDESC_B, EMPIREDESC_C, LOCALE_INTERFACE_FILE_NAME + + name = app.GetLocalePath() + LOCALE_UISCRIPT_PATH = "%s/ui/" % (name) + LOGIN_PATH = "%s/ui/login/" % (name) + EMPIRE_PATH = "%s/ui/empire/" % (name) + GUILD_PATH = "%s/ui/guild/" % (name) + SELECT_PATH = "%s/ui/select/" % (name) + WINDOWS_PATH = "%s/ui/windows/" % (name) + MAPNAME_PATH = "%s/ui/mapname/" % (name) + + JOBDESC_WARRIOR_PATH = "%s/jobdesc_warrior.txt" % (name) + JOBDESC_ASSASSIN_PATH = "%s/jobdesc_assassin.txt" % (name) + JOBDESC_SURA_PATH = "%s/jobdesc_sura.txt" % (name) + JOBDESC_SHAMAN_PATH = "%s/jobdesc_shaman.txt" % (name) + + EMPIREDESC_A = "%s/empiredesc_a.txt" % (name) + EMPIREDESC_B = "%s/empiredesc_b.txt" % (name) + EMPIREDESC_C = "%s/empiredesc_c.txt" % (name) + + LOCALE_INTERFACE_FILE_NAME = "%s/locale_interface.txt" % (name) + + # Reload locale_interface.txt - this updates all UI strings + LoadLocaleFile(LOCALE_INTERFACE_FILE_NAME, globals()) + + return True + except: + # import dbg + # dbg.TraceError("uiScriptLocale.LoadLocaleData failed") + return False \ No newline at end of file