Update blocking

This commit is contained in:
2026-01-24 01:47:38 -05:00
parent 958fe26cad
commit 49ba0a4602
2 changed files with 323 additions and 33 deletions

View File

@@ -1160,7 +1160,7 @@ class SteamWorkshopGUI:
self.log_to_output("🔍 Please provide a valid RimWorld installation path to continue.\n") 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") 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() config = get_update_config()
self.update_checker = UpdateChecker(config["current_version"]) 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) # Start blinking animation for RimWorld label (with slight delay to ensure GUI is ready)
self.root.after(1000, self.start_rimworld_label_blink) 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): def create_header_frame(self, parent):
"""Create header frame with progression logo""" """Create header frame with progression logo"""
header_frame = tk.Frame(parent, bg='#2b2b2b', height=100) header_frame = tk.Frame(parent, bg='#2b2b2b', height=100)
@@ -2127,7 +2123,13 @@ class SteamWorkshopGUI:
def generate_rml_xml(self, mod_data): def generate_rml_xml(self, mod_data):
"""Generate the XML content for the .rml file""" """Generate the XML content for the .rml file"""
# Get core mods from ModsConfig.xml and RimWorld Data folder # 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() 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 # Extract mod names from About.xml files for workshop mods
mod_names = [] mod_names = []
@@ -2144,8 +2146,14 @@ class SteamWorkshopGUI:
] ]
# Add core mods first # 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: for package_id, mod_name in core_mods:
xml_lines.append(f'\t\t\t<li>{package_id}</li>') xml_lines.append(f'\t\t\t<li>{package_id}</li>')
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 # Add workshop mod package IDs to meta section
for workshop_id, package_id in mod_data: for workshop_id, package_id in mod_data:
@@ -2214,14 +2222,20 @@ class SteamWorkshopGUI:
return '\n'.join(xml_lines) return '\n'.join(xml_lines)
def get_core_mods_from_config(self): 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 = [] core_mods = []
try: try:
if not self.modsconfig_path or not os.path.exists(self.modsconfig_path): 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')] 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 # Parse ModsConfig.xml
tree = ET.parse(self.modsconfig_path) tree = ET.parse(self.modsconfig_path)
root = tree.getroot() root = tree.getroot()
@@ -2231,31 +2245,122 @@ class SteamWorkshopGUI:
if self.rimworld_var.get().strip(): if self.rimworld_var.get().strip():
rimworld_game_path = self.rimworld_var.get().strip() rimworld_game_path = self.rimworld_var.get().strip()
rimworld_data_path = os.path.join(rimworld_game_path, "Data") 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') known_expansions_element = root.find('knownExpansions')
if known_expansions_element is not None: 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'): for li in known_expansions_element.findall('li'):
expansion_id = li.text expansion_id = li.text
if expansion_id: 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 # 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: if not mod_name:
mod_name = self.get_default_mod_name(expansion_id.strip()) mod_name = self.get_default_mod_name(expansion_id)
core_mods.append((expansion_id.strip(), mod_name)) 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: if not core_mods:
# Fallback if no expansions found # Fallback if no expansions found - just base game
core_mods = [('ludeon.rimworld', 'RimWorld')] 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: 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')] core_mods = [('ludeon.rimworld', 'RimWorld')]
return core_mods 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): def get_default_mod_name(self, mod_id):
"""Get default display name for core mods""" """Get default display name for core mods"""
default_names = { default_names = {
@@ -2603,20 +2708,6 @@ you must auto sort after this!"""
except Exception as e: except Exception as e:
print(f"Could not add update button: {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): def manual_update_check(self):
"""Manually triggered update check""" """Manually triggered update check"""
self.update_checker.manual_check_for_updates(self.root) self.update_checker.manual_check_for_updates(self.root)
@@ -2625,6 +2716,9 @@ def main():
root = tk.Tk() root = tk.Tk()
root.withdraw() # Hide main window initially root.withdraw() # Hide main window initially
# Import config
from update_config import get_update_config
# Set window icon # Set window icon
try: try:
icon_path = get_resource_path(os.path.join("art", "Progression.ico")) icon_path = get_resource_path(os.path.join("art", "Progression.ico"))
@@ -2638,10 +2732,206 @@ def main():
root.deiconify() # Show main window root.deiconify() # Show main window
app = SteamWorkshopGUI(root) app = SteamWorkshopGUI(root)
# Show loading screen first # 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) 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() 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__": if __name__ == "__main__":
main() main()

View File

@@ -5,7 +5,7 @@ Configuration settings for the update checker
# Update checker configuration # Update checker configuration
UPDATE_CONFIG = { UPDATE_CONFIG = {
# Current version of the application # Current version of the application
"current_version": "0.0.2", "current_version": "0.0.1",
# Repository information # Repository information
"repo_owner": "HRiggs", "repo_owner": "HRiggs",
@@ -18,8 +18,8 @@ UPDATE_CONFIG = {
# Whether to check for updates on startup # Whether to check for updates on startup
"check_on_startup": True, "check_on_startup": True,
# Delay before checking for updates on startup (in milliseconds) # Whether updates are required (blocks app if true)
"startup_check_delay": 5000, "updates_required": True,
# Whether to include pre-releases in update checks # Whether to include pre-releases in update checks
"include_prereleases": True, "include_prereleases": True,