import tkinter as tk from tkinter import ttk, scrolledtext, filedialog, messagebox, font import requests import re from urllib.parse import urlparse, parse_qs import threading import os from pathlib import Path import xml.etree.ElementTree as ET from dotenv import load_dotenv # Load environment variables load_dotenv() # Try to import pyglet for font loading try: import pyglet PYGLET_AVAILABLE = True except ImportError: PYGLET_AVAILABLE = False print("pyglet not available - custom fonts may not work properly") class SteamWorkshopGUI: def __init__(self, root): self.root = root self.root.title("Steam Workshop Collection Manager") self.root.geometry("1200x800") self.root.configure(bg='#2b2b2b') # Load custom font self.load_custom_font() # Configure dark theme self.setup_dark_theme() # Initialize paths self.workshop_folder = None self.modsconfig_path = None # Create main frame main_frame = tk.Frame(root, bg='#2b2b2b') main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Left panel for inputs left_frame = tk.Frame(main_frame, bg='#2b2b2b', width=400) left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) left_frame.pack_propagate(False) # Right panel for output right_frame = tk.Frame(main_frame, bg='#2b2b2b') right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) self.create_input_section(left_frame) self.create_output_section(right_frame) def load_custom_font(self): """Load the RimWorld font using pyglet""" try: font_path = os.path.join("art", "RimWordFont4.ttf") if os.path.exists(font_path): abs_font_path = os.path.abspath(font_path) print(f"Font file found: {abs_font_path}") if PYGLET_AVAILABLE: try: # Use pyglet to add the font pyglet.font.add_file(abs_font_path) # Try to get the font family name from pyglet # Load the font to get its actual family name pyglet_font = pyglet.font.load(None, 12, bold=False, italic=False) # Common RimWorld font family names to try possible_names = [ "RimWorld", "RimWorldFont", "RimWordFont4", "Calibri", "Arial" ] # Test each possible font name for font_name in possible_names: try: test_font = font.Font(family=font_name, size=10) # If we get here, the font worked self.custom_font_family = font_name self.custom_font_available = True print(f"Successfully loaded font with pyglet: {font_name}") return except Exception as e: continue print("pyglet loaded font but couldn't find working family name") self.custom_font_available = False except Exception as e: print(f"pyglet font loading failed: {e}") self.custom_font_available = False else: print("pyglet not available, trying Windows registration...") # Fall back to Windows registration method try: import ctypes from ctypes import wintypes # Add font resource to Windows gdi32 = ctypes.windll.gdi32 result = gdi32.AddFontResourceW(abs_font_path) if result > 0: print(f"Successfully registered font with Windows") # Notify Windows that fonts have changed try: HWND_BROADCAST = 0xFFFF WM_FONTCHANGE = 0x001D ctypes.windll.user32.SendMessageW(HWND_BROADCAST, WM_FONTCHANGE, 0, 0) print("Sent font change notification to Windows") except Exception as notify_error: print(f"Could not send font change notification: {notify_error}") # For Windows registration, let's try using the actual font file name # Sometimes the family name is different from what we expect self.custom_font_family = "RimWorld" # Default assumption self.custom_font_available = True print(f"Using Windows-registered font: {self.custom_font_family}") else: print("Failed to register font with Windows") self.custom_font_available = False except Exception as e: print(f"Windows font registration error: {e}") self.custom_font_available = False else: print(f"Font file not found: {font_path}") self.custom_font_available = False except Exception as e: print(f"Error loading custom font: {e}") self.custom_font_available = False def get_font(self, size=10, weight='normal'): """Get font tuple for UI elements""" if self.custom_font_available: try: # Use the registered font family name font_obj = font.Font(family=self.custom_font_family, size=size, weight=weight) print(f"Created font: {self.custom_font_family}, size={size}, weight={weight}") return font_obj except Exception as e: print(f"Error using custom font: {e}") # Fall back to a distinctive font so we can see the difference return font.Font(family='Courier New', size=size, weight=weight) else: print(f"Using fallback font: Courier New, size={size}") return font.Font(family='Courier New', size=size, weight=weight) def setup_dark_theme(self): """Configure dark theme colors""" style = ttk.Style() style.theme_use('clam') # Configure colors for dark theme style.configure('TLabel', background='#2b2b2b', foreground='#ffffff') style.configure('TButton', background='#404040', foreground='#ffffff') style.map('TButton', background=[('active', '#505050')]) style.configure('TEntry', background='#404040', foreground='#ffffff', fieldbackground='#404040') def create_input_section(self, parent): """Create the left input section""" title_label = tk.Label(parent, text="Steam Workshop Collections", font=self.get_font(14, 'bold'), bg='#2b2b2b', fg='#ffffff') title_label.pack(pady=(0, 10)) # RimWorld game folder input rimworld_frame = tk.Frame(parent, bg='#2b2b2b') rimworld_frame.pack(fill='x', pady=(0, 10)) rimworld_label = tk.Label(rimworld_frame, text="RimWorld Game Folder:", font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff') rimworld_label.pack(anchor='w') rimworld_help = tk.Label(rimworld_frame, text="Right-click RimWorld in Steam > Manage > Browse local files, copy that path", font=self.get_font(8), bg='#2b2b2b', fg='#888888', wraplength=380) rimworld_help.pack(anchor='w', pady=(2, 5)) rimworld_input_frame = tk.Frame(rimworld_frame, bg='#2b2b2b') rimworld_input_frame.pack(fill='x') self.rimworld_var = tk.StringVar() self.rimworld_entry = tk.Entry(rimworld_input_frame, textvariable=self.rimworld_var, font=self.get_font(9), bg='#404040', fg='#ffffff', insertbackground='#ffffff', relief='flat', bd=5) self.rimworld_entry.pack(side='left', fill='x', expand=True) self.rimworld_entry.bind('', self.on_rimworld_path_change) browse_game_btn = tk.Button(rimworld_input_frame, text="Browse", command=self.browse_rimworld_folder, bg='#505050', fg='white', font=self.get_font(8), relief='flat', padx=10) browse_game_btn.pack(side='right', padx=(5, 0)) # Workshop folder display (derived from RimWorld path) workshop_frame = tk.Frame(parent, bg='#2b2b2b') workshop_frame.pack(fill='x', pady=(0, 10)) workshop_label = tk.Label(workshop_frame, text="Workshop Folder (Auto-derived):", font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff') workshop_label.pack(anchor='w') self.workshop_var = tk.StringVar(value="Enter RimWorld path above") self.workshop_display = tk.Entry(workshop_frame, textvariable=self.workshop_var, font=self.get_font(8), bg='#404040', fg='#ffffff', insertbackground='#ffffff', relief='flat', bd=5) self.workshop_display.pack(fill='x', pady=(5, 0)) self.workshop_display.bind('', self.on_workshop_path_change) # ModsConfig.xml folder display modsconfig_frame = tk.Frame(parent, bg='#2b2b2b') modsconfig_frame.pack(fill='x', pady=(0, 15)) modsconfig_label = tk.Label(modsconfig_frame, text="ModsConfig.xml Path:", font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff') modsconfig_label.pack(anchor='w') self.modsconfig_var = tk.StringVar() self.modsconfig_display = tk.Entry(modsconfig_frame, textvariable=self.modsconfig_var, font=self.get_font(8), bg='#404040', fg='#ffffff', insertbackground='#ffffff', relief='flat', bd=5) self.modsconfig_display.pack(fill='x', pady=(5, 0)) self.modsconfig_display.bind('', self.on_modsconfig_path_change) # Initialize ModsConfig path self.find_modsconfig_path() # Core collection core_url = os.getenv('CORE_COLLECTION_URL', 'https://steamcommunity.com/workshop/filedetails/?id=3521297585') self.create_url_input(parent, "Core Collection:", core_url) # Content collection content_url = os.getenv('CONTENT_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712') self.create_url_input(parent, "Content Collection:", content_url) # Cosmetics collection cosmetics_url = os.getenv('COSMETICS_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646') self.create_url_input(parent, "Cosmetics Collection:", cosmetics_url) # Load Progression Pack button load_btn = tk.Button(parent, text="Load Progression Pack Complete", command=self.load_progression_pack, bg='#0078d4', fg='white', font=self.get_font(12, 'bold'), relief='flat', padx=30, pady=12) load_btn.pack(pady=30) def create_url_input(self, parent, label_text, default_url): """Create a labeled URL input field""" label = tk.Label(parent, text=label_text, font=self.get_font(10, 'bold'), bg='#2b2b2b', fg='#ffffff') label.pack(anchor='w', pady=(10, 5)) entry = tk.Entry(parent, font=self.get_font(9), bg='#404040', fg='#ffffff', insertbackground='#ffffff', relief='flat', bd=5) entry.pack(fill='x', pady=(0, 10)) entry.insert(0, default_url) # Store reference to entry widgets if not hasattr(self, 'url_entries'): self.url_entries = {} collection_name = label_text.replace(" Collection:", "").lower() self.url_entries[collection_name] = entry def create_output_section(self, parent): """Create the right output section""" title_label = tk.Label(parent, text="Workshop IDs and Package Names", font=self.get_font(14, 'bold'), bg='#2b2b2b', fg='#ffffff') title_label.pack(pady=(0, 10)) # Output text area with scrollbar self.output_text = scrolledtext.ScrolledText(parent, font=self.get_font(10), bg='#1e1e1e', fg='#ffffff', insertbackground='#ffffff', selectbackground='#404040', relief='flat', bd=5) self.output_text.pack(fill=tk.BOTH, expand=True) def on_workshop_path_change(self, event=None): """Called when workshop path is manually changed""" self.workshop_folder = self.workshop_var.get().strip() def on_modsconfig_path_change(self, event=None): """Called when ModsConfig path is manually changed""" self.modsconfig_path = self.modsconfig_var.get().strip() def on_rimworld_path_change(self, event=None): """Called when RimWorld path is changed""" rimworld_path = self.rimworld_var.get().strip() if rimworld_path: # Derive workshop path from RimWorld path # From: D:\SteamLibrary\steamapps\common\RimWorld # To: D:\SteamLibrary\steamapps\workshop\content\294100 if "steamapps" in rimworld_path.lower(): # Find the steamapps part and replace common\RimWorld with workshop\content\294100 parts = rimworld_path.split(os.sep) try: steamapps_index = next(i for i, part in enumerate(parts) if part.lower() == 'steamapps') # Build new path: everything up to steamapps + steamapps + workshop + content + 294100 workshop_parts = parts[:steamapps_index + 1] + ['workshop', 'content', '294100'] workshop_path = os.sep.join(workshop_parts) self.workshop_folder = workshop_path self.workshop_var.set(workshop_path) except (StopIteration, IndexError): self.workshop_var.set("Invalid RimWorld path - should contain 'steamapps'") else: self.workshop_var.set("Invalid RimWorld path - should contain 'steamapps'") else: self.workshop_var.set("Enter RimWorld path above") def browse_rimworld_folder(self): """Allow user to browse for RimWorld game folder""" folder = filedialog.askdirectory( title="Select RimWorld Game Folder (Right-click RimWorld in Steam > Manage > Browse local files)", initialdir="C:\\" ) if folder: self.rimworld_var.set(folder) self.on_rimworld_path_change() # Update workshop path def find_modsconfig_path(self): """Find the ModsConfig.xml file in AppData""" try: # Get path template from .env path_template = os.getenv('MODSCONFIG_PATH_TEMPLATE', r'%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml') # Expand environment variables modsconfig_path = os.path.expandvars(path_template) if os.path.exists(modsconfig_path): self.modsconfig_path = modsconfig_path self.modsconfig_var.set(modsconfig_path) return modsconfig_path else: self.modsconfig_var.set("ModsConfig.xml not found") return None except Exception as e: self.modsconfig_var.set(f"Error finding ModsConfig.xml: {str(e)}") return None def extract_workshop_id(self, url): """Extract workshop ID from Steam URL""" # Clean up steam:// protocol URLs if url.startswith('steam://openurl/'): url = url.replace('steam://openurl/', '') # Extract ID from URL parameters try: parsed_url = urlparse(url) if 'id=' in parsed_url.query: query_params = parse_qs(parsed_url.query) return query_params.get('id', [None])[0] elif '/filedetails/?id=' in url: # Direct extraction for simple cases match = re.search(r'id=(\d+)', url) if match: return match.group(1) except Exception as e: print(f"Error extracting ID from {url}: {e}") return None def load_progression_pack(self): """Load all progression pack collections and extract package names""" if not self.workshop_folder or not os.path.exists(self.workshop_folder): self.output_text.delete(1.0, tk.END) self.output_text.insert(tk.END, "Workshop folder not found!\nPlease enter the correct RimWorld path above.") return self.output_text.delete(1.0, tk.END) self.output_text.insert(tk.END, "Loading Progression Pack Complete...\n\n") threading.Thread(target=self._load_progression_pack_thread, daemon=True).start() def _load_progression_pack_thread(self): """Thread function to load progression pack""" try: all_workshop_ids = [] # Get collection URLs from entries collections = { "Core": self.url_entries['core'].get().strip(), "Content": self.url_entries['content'].get().strip(), "Cosmetics": self.url_entries['cosmetics'].get().strip() } # Fetch all workshop IDs from collections for collection_name, url in collections.items(): if url: self._safe_update_output(f"Fetching {collection_name} collection...\n") workshop_id = self.extract_workshop_id(url) if workshop_id: workshop_ids = self.fetch_collection_items(workshop_id) all_workshop_ids.extend(workshop_ids) self._safe_update_output(f"Found {len(workshop_ids)} items in {collection_name}\n") # Remove duplicates unique_ids = list(set(all_workshop_ids)) self._safe_update_output(f"\nTotal unique workshop items: {len(unique_ids)}\n") self._safe_update_output("Extracting package names from About.xml files...\n\n") # Extract package names from About.xml files results = [] for workshop_id in sorted(unique_ids): package_name = self.get_package_name_from_about_xml(workshop_id) results.append((workshop_id, package_name)) # Display results self._safe_update_output("=== PROGRESSION PACK COMPLETE ===\n\n") self._safe_update_output(f"{'Workshop ID':<15} | Package Name\n") self._safe_update_output("-" * 80 + "\n") for workshop_id, package_name in results: self._safe_update_output(f"{workshop_id:<15} | {package_name}\n") self._safe_update_output(f"\nTotal mods processed: {len(results)}\n") # Create ProgressionVanilla.rml file self.create_rml_file(results) except Exception as e: self._safe_update_output(f"Error loading progression pack: {str(e)}\n") def create_rml_file(self, mod_data): """Create ProgressionVanilla.rml file with workshop IDs and names""" try: # Get ModLists directory path modlists_path = os.path.expandvars(r"%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\ModLists") # Create directory if it doesn't exist os.makedirs(modlists_path, exist_ok=True) rml_file_path = os.path.join(modlists_path, "ProgressionVanilla.rml") # Create XML content xml_content = self.generate_rml_xml(mod_data) # Write to file with open(rml_file_path, 'w', encoding='utf-8') as f: f.write(xml_content) self._safe_update_output(f"\nProgressionVanilla.rml created successfully at:\n{rml_file_path}\n") except Exception as e: self._safe_update_output(f"Error creating RML file: {str(e)}\n") 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 core_mods = self.get_core_mods_from_config() # Extract mod names from About.xml files for workshop mods mod_names = [] for workshop_id, package_id in mod_data: mod_name = self.get_mod_name_from_about_xml(workshop_id) mod_names.append(mod_name) xml_lines = [ '', '', '\t', '\t\t1.6.4633 rev1261', '\t\t' ] # Add core mods first for package_id, mod_name in core_mods: xml_lines.append(f'\t\t\t
  • {package_id}
  • ') # Add workshop mod package IDs to meta section for workshop_id, package_id in mod_data: xml_lines.append(f'\t\t\t
  • {package_id}
  • ') xml_lines.extend([ '\t\t
    ', '\t\t' ]) # Add core mods (they don't have Steam IDs, so use 0) for package_id, mod_name in core_mods: xml_lines.append(f'\t\t\t
  • 0
  • ') # Add workshop IDs to meta section for workshop_id, package_id in mod_data: xml_lines.append(f'\t\t\t
  • {workshop_id}
  • ') xml_lines.extend([ '\t\t
    ', '\t\t' ]) # Add core mod names first for package_id, mod_name in core_mods: xml_lines.append(f'\t\t\t
  • {mod_name}
  • ') # Add workshop mod names to meta section for mod_name in mod_names: xml_lines.append(f'\t\t\t
  • {mod_name}
  • ') xml_lines.extend([ '\t\t
    ', '\t', '\t', '\t\t' ]) # Add core mods first to modList section for package_id, mod_name in core_mods: xml_lines.append(f'\t\t\t
  • {package_id}
  • ') # Add workshop mod package IDs to modList section for workshop_id, package_id in mod_data: xml_lines.append(f'\t\t\t
  • {package_id}
  • ') xml_lines.extend([ '\t\t
    ', '\t\t' ]) # Add core mod names first to modList section for package_id, mod_name in core_mods: xml_lines.append(f'\t\t\t
  • {mod_name}
  • ') # Add workshop mod names to modList section for mod_name in mod_names: xml_lines.append(f'\t\t\t
  • {mod_name}
  • ') xml_lines.extend([ '\t\t
    ', '\t
    ', '
    ' ]) 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""" 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") return [('ludeon.rimworld', 'RimWorld')] # Parse ModsConfig.xml tree = ET.parse(self.modsconfig_path) root = tree.getroot() # Get RimWorld Data folder path from the RimWorld game path rimworld_data_path = None if self.rimworld_var.get().strip(): rimworld_game_path = self.rimworld_var.get().strip() rimworld_data_path = os.path.join(rimworld_game_path, "Data") # Find knownExpansions section known_expansions_element = root.find('knownExpansions') if known_expansions_element is not None: for li in known_expansions_element.findall('li'): expansion_id = li.text if expansion_id: mod_name = self.get_core_mod_name(expansion_id.strip(), rimworld_data_path) core_mods.append((expansion_id.strip(), mod_name)) if not core_mods: # Fallback if no expansions found core_mods = [('ludeon.rimworld', 'RimWorld')] self._safe_update_output(f"Found {len(core_mods)} core expansions from ModsConfig.xml\n") except Exception as e: self._safe_update_output(f"Error reading core mods: {str(e)}\n") core_mods = [('ludeon.rimworld', 'RimWorld')] return core_mods def get_core_mod_name(self, mod_id, rimworld_data_path): """Get mod name from RimWorld Data folder About.xml""" try: if not rimworld_data_path or not os.path.exists(rimworld_data_path): return self.get_default_mod_name(mod_id) # Look for mod folder in Data directory # Core mods are usually in subfolders like Core, Royalty, etc. possible_folders = [ mod_id.replace('ludeon.rimworld.', '').replace('ludeon.rimworld', 'Core'), mod_id.replace('ludeon.rimworld.', '').capitalize(), mod_id.split('.')[-1].capitalize() if '.' in mod_id else mod_id ] 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 default name return self.get_default_mod_name(mod_id) except Exception as e: return self.get_default_mod_name(mod_id) def get_default_mod_name(self, mod_id): """Get default display name for core mods""" default_names = { 'ludeon.rimworld': 'RimWorld', 'ludeon.rimworld.royalty': 'Royalty', 'ludeon.rimworld.ideology': 'Ideology', 'ludeon.rimworld.biotech': 'Biotech', 'ludeon.rimworld.anomaly': 'Anomaly', 'ludeon.rimworld.odyssey': 'Odyssey' } return default_names.get(mod_id, mod_id) def get_mod_name_from_about_xml(self, workshop_id): """Extract mod name from About/About.xml file""" try: about_xml_path = os.path.join(self.workshop_folder, workshop_id, "About", "About.xml") if not os.path.exists(about_xml_path): return f"Workshop ID {workshop_id}" # Parse the XML file tree = ET.parse(about_xml_path) root = tree.getroot() # Look for name element name_element = root.find('name') if name_element is not None and name_element.text: return name_element.text.strip() else: return f"Workshop ID {workshop_id}" except ET.ParseError: return f"Workshop ID {workshop_id}" except Exception as e: return f"Workshop ID {workshop_id}" def get_package_name_from_about_xml(self, workshop_id): """Extract package name from About/About.xml file""" try: about_xml_path = os.path.join(self.workshop_folder, workshop_id, "About", "About.xml") if not os.path.exists(about_xml_path): return "About.xml not found" # Parse the XML file tree = ET.parse(about_xml_path) root = tree.getroot() # Look for packageId element package_id_element = root.find('packageId') if package_id_element is not None and package_id_element.text: return package_id_element.text.strip() else: return "packageId not found" except ET.ParseError: return "XML parse error" except Exception as e: return f"Error: {str(e)}" def _safe_update_output(self, text): """Safely update output text without recursion""" try: self.output_text.insert(tk.END, text) self.output_text.see(tk.END) except Exception: pass # Ignore update errors to prevent recursion def fetch_collection_items(self, collection_id): """Fetch workshop IDs from a Steam Workshop collection""" try: # Steam Workshop collection URL url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={collection_id}" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } self._safe_update_output(f"Fetching collection page...\n") response = requests.get(url, headers=headers, timeout=15) response.raise_for_status() # Extract workshop IDs from collection items only html_content = response.text workshop_ids = [] # Find the collectionChildren section collection_start = html_content.find('
    ') if collection_start != -1: # Find the proper end of the collectionChildren section # Look for the closing
    that matches the opening collectionChildren div # We need to count div tags to find the matching closing tag search_pos = collection_start + len('
    ') div_count = 1 # We've opened one div collection_end = -1 while search_pos < len(html_content) and div_count > 0: next_open = html_content.find('', search_pos) if next_close == -1: break if next_open != -1 and next_open < next_close: div_count += 1 search_pos = next_open + 4 else: div_count -= 1 if div_count == 0: collection_end = next_close + 6 break search_pos = next_close + 6 if collection_end == -1: collection_end = len(html_content) collection_section = html_content[collection_start:collection_end] # Extract IDs from sharedfile_ elements (these are the actual collection items) sharedfile_ids = re.findall(r'id="sharedfile_(\d+)"', collection_section) workshop_ids.extend(sharedfile_ids) self._safe_update_output(f"Found {len(sharedfile_ids)} collection items\n") else: # Fallback: search the entire page but be more selective self._safe_update_output("Collection section not found, using fallback method\n") sharedfile_ids = re.findall(r'id="sharedfile_(\d+)"', html_content) workshop_ids.extend(sharedfile_ids) # Remove duplicates and the collection ID itself unique_ids = list(set(workshop_ids)) if collection_id in unique_ids: unique_ids.remove(collection_id) return sorted(unique_ids) # Sort for consistent output except requests.RequestException as e: self._safe_update_output(f"Network error: {str(e)}\n") return [] except Exception as e: self._safe_update_output(f"Error fetching collection: {str(e)}\n") return [] def main(): root = tk.Tk() app = SteamWorkshopGUI(root) root.mainloop() if __name__ == "__main__": main()