diff --git a/steam_workshop_gui.py b/steam_workshop_gui.py index 6635a37..7c10d11 100644 --- a/steam_workshop_gui.py +++ b/steam_workshop_gui.py @@ -1160,7 +1160,7 @@ class SteamWorkshopGUI: self.log_to_output("🔍 Please provide a valid RimWorld installation path to continue.\n") self.log_to_output(" The 'RimWorld Game Folder' label will stop blinking once a valid path is detected.\n\n") - # Initialize update checker + # Initialize update checker (for manual checks only) config = get_update_config() self.update_checker = UpdateChecker(config["current_version"]) @@ -1173,10 +1173,6 @@ class SteamWorkshopGUI: # Start blinking animation for RimWorld label (with slight delay to ensure GUI is ready) self.root.after(1000, self.start_rimworld_label_blink) - # Check for updates on startup if enabled - if config["check_on_startup"]: - self.root.after(config["startup_check_delay"], self.check_for_updates_on_startup) - def create_header_frame(self, parent): """Create header frame with progression logo""" header_frame = tk.Frame(parent, bg='#2b2b2b', height=100) @@ -2127,7 +2123,13 @@ class SteamWorkshopGUI: def generate_rml_xml(self, mod_data): """Generate the XML content for the .rml file""" # Get core mods from ModsConfig.xml and RimWorld Data folder + msg = "=== GENERATING RML XML ===\n" + self._safe_update_output(msg) + print(msg.strip()) core_mods = self.get_core_mods_from_config() + msg = f"Core mods returned: {core_mods}\n" + self._safe_update_output(msg) + print(msg.strip()) # Extract mod names from About.xml files for workshop mods mod_names = [] @@ -2144,8 +2146,14 @@ class SteamWorkshopGUI: ] # Add core mods first + msg = "Adding core mods to XML:\n" + self._safe_update_output(msg) + print(msg.strip()) for package_id, mod_name in core_mods: xml_lines.append(f'\t\t\t
  • {package_id}
  • ') + msg = f" Added core mod: {package_id} ({mod_name})\n" + self._safe_update_output(msg) + print(msg.strip()) # Add workshop mod package IDs to meta section for workshop_id, package_id in mod_data: @@ -2214,14 +2222,20 @@ class SteamWorkshopGUI: return '\n'.join(xml_lines) def get_core_mods_from_config(self): - """Get core mods from ModsConfig.xml knownExpansions section and their info from RimWorld Data folder""" + """Get core mods from ModsConfig.xml knownExpansions section - includes ALL DLC the user owns""" core_mods = [] try: if not self.modsconfig_path or not os.path.exists(self.modsconfig_path): - self._safe_update_output("ModsConfig.xml not found, using default core mods\n") + msg = "ModsConfig.xml not found, using default core mods\n" + self._safe_update_output(msg) + print(msg.strip()) return [('ludeon.rimworld', 'RimWorld')] + msg = f"Reading ModsConfig.xml from: {self.modsconfig_path}\n" + self._safe_update_output(msg) + print(msg.strip()) + # Parse ModsConfig.xml tree = ET.parse(self.modsconfig_path) root = tree.getroot() @@ -2231,31 +2245,122 @@ class SteamWorkshopGUI: if self.rimworld_var.get().strip(): rimworld_game_path = self.rimworld_var.get().strip() rimworld_data_path = os.path.join(rimworld_game_path, "Data") + msg = f"RimWorld Data path: {rimworld_data_path}\n" + self._safe_update_output(msg) + print(msg.strip()) + else: + msg = "No RimWorld path set, using fallback names\n" + self._safe_update_output(msg) + print(msg.strip()) - # Find knownExpansions section + # Find knownExpansions section and include ALL expansions found known_expansions_element = root.find('knownExpansions') if known_expansions_element is not None: + msg = "Found knownExpansions section, processing all DLC...\n" + self._safe_update_output(msg) + print(msg.strip()) for li in known_expansions_element.findall('li'): expansion_id = li.text if expansion_id: + expansion_id = expansion_id.strip() + msg = f"Processing DLC: {expansion_id}\n" + self._safe_update_output(msg) + print(msg.strip()) # Use the same method as DLC detection to get real names - mod_name = self.get_expansion_real_name(expansion_id.strip(), rimworld_data_path) + mod_name = self.get_expansion_real_name(expansion_id, rimworld_data_path) + msg = f"get_expansion_real_name returned: {mod_name}\n" + self._safe_update_output(msg) + print(msg.strip()) if not mod_name: - mod_name = self.get_default_mod_name(expansion_id.strip()) - core_mods.append((expansion_id.strip(), mod_name)) + mod_name = self.get_default_mod_name(expansion_id) + msg = f"Using fallback name: {mod_name}\n" + self._safe_update_output(msg) + print(msg.strip()) + core_mods.append((expansion_id, mod_name)) + msg = f"Added to core_mods: {expansion_id} -> {mod_name}\n" + self._safe_update_output(msg) + print(msg.strip()) + else: + msg = "No knownExpansions section found in ModsConfig.xml\n" + self._safe_update_output(msg) + print(msg.strip()) if not core_mods: - # Fallback if no expansions found + # Fallback if no expansions found - just base game core_mods = [('ludeon.rimworld', 'RimWorld')] + msg = "No expansions found, using fallback base game only\n" + self._safe_update_output(msg) + print(msg.strip()) - self._safe_update_output(f"Found {len(core_mods)} core expansions from ModsConfig.xml\n") + msg = f"Final core_mods list: {core_mods}\n" + self._safe_update_output(msg) + print(msg.strip()) + msg = f"Found {len(core_mods)} core expansions from knownExpansions in ModsConfig.xml\n" + self._safe_update_output(msg) + print(msg.strip()) except Exception as e: - self._safe_update_output(f"Error reading core mods: {str(e)}\n") + msg = f"Error reading core mods: {str(e)}\n" + self._safe_update_output(msg) + print(msg.strip()) core_mods = [('ludeon.rimworld', 'RimWorld')] return core_mods + def get_expansion_real_name(self, expansion_id, rimworld_data_path): + """Get the real expansion name from RimWorld Data folder About.xml""" + try: + if not rimworld_data_path or not os.path.exists(rimworld_data_path): + # Fallback to extracting from ID if no data path + return self.extract_name_from_id(expansion_id) + + # Look for expansion folder in Data directory + # Core expansions are usually in subfolders like Royalty, Ideology, etc. + possible_folders = [ + expansion_id.replace('ludeon.rimworld.', '').capitalize(), + expansion_id.split('.')[-1].capitalize() if '.' in expansion_id else expansion_id, + expansion_id.replace('ludeon.rimworld.', ''), + expansion_id.replace('ludeon.rimworld', 'Core') + ] + + for folder_name in possible_folders: + about_xml_path = os.path.join(rimworld_data_path, folder_name, "About", "About.xml") + if os.path.exists(about_xml_path): + try: + tree = ET.parse(about_xml_path) + root = tree.getroot() + + name_element = root.find('name') + if name_element is not None and name_element.text: + return name_element.text.strip() + except ET.ParseError: + continue + + # If not found in Data folder, use fallback + return self.extract_name_from_id(expansion_id) + + except Exception as e: + return self.extract_name_from_id(expansion_id) + + def extract_name_from_id(self, expansion_id): + """Extract expansion name from ID as fallback""" + if 'royalty' in expansion_id.lower(): + return 'Royalty' + elif 'ideology' in expansion_id.lower(): + return 'Ideology' + elif 'biotech' in expansion_id.lower(): + return 'Biotech' + elif 'anomaly' in expansion_id.lower(): + return 'Anomaly' + elif 'odyssey' in expansion_id.lower(): + return 'Odyssey' + else: + # Extract the last part after the last dot and capitalize + parts = expansion_id.split('.') + if len(parts) > 1: + return parts[-1].capitalize() + return expansion_id + def get_default_mod_name(self, mod_id): """Get default display name for core mods""" default_names = { @@ -2603,20 +2708,6 @@ you must auto sort after this!""" except Exception as e: print(f"Could not add update button: {e}") - def check_for_updates_on_startup(self): - """Check for updates when the application starts""" - def update_callback(release_info, error): - if error or not release_info: - return # Silently fail on startup - - latest_version = release_info['version'] - if (self.update_checker.is_newer_version(latest_version) and - not self.update_checker.should_skip_version(latest_version)): - # Show update dialog - self.update_checker.show_update_dialog(self.root, release_info) - - self.update_checker.check_for_updates_async(update_callback) - def manual_update_check(self): """Manually triggered update check""" self.update_checker.manual_check_for_updates(self.root) @@ -2625,6 +2716,9 @@ def main(): root = tk.Tk() root.withdraw() # Hide main window initially + # Import config + from update_config import get_update_config + # Set window icon try: icon_path = get_resource_path(os.path.join("art", "Progression.ico")) @@ -2638,10 +2732,206 @@ def main(): root.deiconify() # Show main window app = SteamWorkshopGUI(root) - # Show loading screen first - loading_screen = LoadingScreen(root, show_main_app) + # Check for updates SYNCHRONOUSLY before starting anything + config = get_update_config() + update_info = check_for_updates_before_startup(root) + + if update_info and config.get("updates_required", True): + # Show blocking update dialog - app won't continue until user updates + show_blocking_update_dialog(root, update_info['checker'], update_info['release']) + else: + # No update needed or updates not required, start loading screen + loading_screen = LoadingScreen(root, show_main_app) + + # If update available but not required, show non-blocking dialog + if update_info: + root.after(1000, lambda: show_optional_update_dialog(root, update_info['checker'], update_info['release'])) root.mainloop() +def check_for_updates_before_startup(root): + """Check for updates synchronously before starting the application""" + try: + from update_checker import UpdateChecker + from update_config import get_update_config + + print("Checking for updates...") + + config = get_update_config() + update_checker = UpdateChecker(config["current_version"]) + + # Use synchronous check to block until complete + release_info, error = update_checker.check_for_updates_sync() + + if error: + print(f"Update check failed: {error}") + return None + elif release_info: + latest_version = release_info['version'] + if update_checker.is_newer_version(latest_version): + print(f"Update required: {latest_version}") + return { + 'checker': update_checker, + 'release': release_info + } + else: + print(f"Already up to date: {latest_version}") + else: + print("No release info available") + + return None + + except Exception as e: + print(f"Update check failed: {e}") + return None + +def show_blocking_update_dialog(root, update_checker, release_info): + """Show a blocking update dialog that prevents app from continuing""" + # Create a new window for the update dialog + update_window = tk.Toplevel(root) + update_window.title("Update Required - Progression Loader") + update_window.configure(bg='#2b2b2b') + update_window.resizable(False, False) + update_window.attributes('-topmost', True) + + # Make it modal - user can't interact with other windows + update_window.transient(root) + update_window.grab_set() + + # Set window icon + try: + icon_path = get_resource_path(os.path.join("art", "Progression.ico")) + if os.path.exists(icon_path): + update_window.iconbitmap(icon_path) + except: + pass + + # Calculate window size and center it + window_width = 600 + window_height = 500 + + # Get screen dimensions + screen_width = update_window.winfo_screenwidth() + screen_height = update_window.winfo_screenheight() + + # Calculate center position + x = (screen_width - window_width) // 2 + y = (screen_height - window_height) // 2 + + update_window.geometry(f"{window_width}x{window_height}+{x}+{y}") + + # Title + title_label = tk.Label(update_window, + text="🔄 Update Required", + fg='#ff6b6b', bg='#2b2b2b', + font=('Arial', 20, 'bold')) + title_label.pack(pady=30) + + # Message + message_label = tk.Label(update_window, + text="A new version is available and required to continue.", + fg='white', bg='#2b2b2b', + font=('Arial', 14)) + message_label.pack(pady=10) + + # Version info + version_frame = tk.Frame(update_window, bg='#2b2b2b') + version_frame.pack(pady=20) + + current_label = tk.Label(version_frame, + text=f"Current Version: {update_checker.current_version}", + fg='#cccccc', bg='#2b2b2b', + font=('Arial', 12)) + current_label.pack() + + latest_label = tk.Label(version_frame, + text=f"Required Version: {release_info['version']}", + fg='#4ecdc4', bg='#2b2b2b', + font=('Arial', 12, 'bold')) + latest_label.pack(pady=5) + + # Release notes + if release_info.get('body'): + notes_label = tk.Label(update_window, + text="What's New:", + fg='white', bg='#2b2b2b', + font=('Arial', 14, 'bold')) + notes_label.pack(pady=(30, 10)) + + # Create scrollable text widget for release notes + notes_frame = tk.Frame(update_window, bg='#2b2b2b') + notes_frame.pack(fill='both', expand=True, padx=40, pady=(0, 20)) + + notes_text = tk.Text(notes_frame, + height=8, + bg='#404040', fg='#ffffff', + font=('Arial', 11), + wrap=tk.WORD, + state='disabled', + relief='flat', + bd=0) + + scrollbar = tk.Scrollbar(notes_frame, bg='#404040') + scrollbar.pack(side='right', fill='y') + + notes_text.pack(side='left', fill='both', expand=True) + notes_text.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=notes_text.yview) + + # Insert release notes + notes_text.config(state='normal') + notes_text.insert('1.0', release_info['body']) + notes_text.config(state='disabled') + + # Buttons + button_frame = tk.Frame(update_window, bg='#2b2b2b') + button_frame.pack(pady=30) + + def download_and_exit(): + """Open download page and exit the application""" + webbrowser.open(release_info['html_url']) + print("Please download and install the update, then restart the application.") + root.quit() # Exit the entire application + + def exit_app(): + """Exit the application without updating""" + print("Application cannot continue without updating.") + root.quit() + + # Download Update button (primary action) + download_btn = tk.Button(button_frame, + text="Download Update & Exit", + command=download_and_exit, + bg='#4ecdc4', fg='white', + font=('Arial', 14, 'bold'), + padx=30, pady=10, + cursor='hand2') + download_btn.pack(side='left', padx=10) + + # Exit button (secondary action) + exit_btn = tk.Button(button_frame, + text="Exit Application", + command=exit_app, + bg='#666666', fg='white', + font=('Arial', 12), + padx=20, pady=10, + cursor='hand2') + exit_btn.pack(side='left', padx=10) + + # Handle window close (same as exit) + update_window.protocol("WM_DELETE_WINDOW", exit_app) + + # Focus on the download button + download_btn.focus_set() + + print("Blocking update dialog shown - app will not continue until user updates") + +def show_optional_update_dialog(root, update_checker, release_info): + """Show a non-blocking update dialog""" + try: + update_checker.show_update_dialog(root, release_info) + except Exception as e: + print(f"Could not show update dialog: {e}") + if __name__ == "__main__": main() diff --git a/update_config.py b/update_config.py index 1f781c2..0e52a6b 100644 --- a/update_config.py +++ b/update_config.py @@ -5,7 +5,7 @@ Configuration settings for the update checker # Update checker configuration UPDATE_CONFIG = { # Current version of the application - "current_version": "0.0.2", + "current_version": "0.0.1", # Repository information "repo_owner": "HRiggs", @@ -18,8 +18,8 @@ UPDATE_CONFIG = { # Whether to check for updates on startup "check_on_startup": True, - # Delay before checking for updates on startup (in milliseconds) - "startup_check_delay": 5000, + # Whether updates are required (blocks app if true) + "updates_required": True, # Whether to include pre-releases in update checks "include_prereleases": True,