art whoops! and also font whoops! and also failed at fonts and removing demo files.
7
.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Default Collection URLs
|
||||||
|
CORE_COLLECTION_URL=https://steamcommunity.com/workshop/filedetails/?id=3521297585
|
||||||
|
CONTENT_COLLECTION_URL=https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712
|
||||||
|
COSMETICS_COLLECTION_URL=https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646
|
||||||
|
|
||||||
|
# ModsConfig.xml Path Template
|
||||||
|
MODSCONFIG_PATH_TEMPLATE=%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml
|
||||||
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Default Collection URLs
|
||||||
|
CORE_COLLECTION_URL=https://steamcommunity.com/workshop/filedetails/?id=3521297585
|
||||||
|
CONTENT_COLLECTION_URL=steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712
|
||||||
|
COSMETICS_COLLECTION_URL=steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646
|
||||||
|
|
||||||
|
# ModsConfig.xml Path Template
|
||||||
|
MODSCONFIG_PATH_TEMPLATE=%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml
|
||||||
70
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: Build and Upload Release (Windows EXE)
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-windows-exe:
|
||||||
|
name: Build Windows EXE
|
||||||
|
runs-on: windows
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
if (Test-Path requirements.txt) { pip install -r requirements.txt }
|
||||||
|
pip install pyinstaller
|
||||||
|
|
||||||
|
- name: Build EXE with PyInstaller
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
# Stamp version into sealoader_version.py from release tag
|
||||||
|
if ($env:GITHUB_EVENT_NAME -eq 'release') {
|
||||||
|
$tag = '${{ github.event.release.tag_name }}'
|
||||||
|
} else {
|
||||||
|
$tag = (git describe --tags --always) 2>$null
|
||||||
|
if (-not $tag) { $tag = "0.0.0-dev" }
|
||||||
|
}
|
||||||
|
("__version__ = '" + $tag + "'") | Out-File -FilePath sealoader_version.py -Encoding UTF8 -Force
|
||||||
|
# Bundle PNG resources referenced at runtime
|
||||||
|
pyinstaller --noconfirm --onefile --windowed sealoader_gui.py --name SeaLoader `
|
||||||
|
--add-data "SeaLoader.png;." `
|
||||||
|
--add-data "hrsys.png;." `
|
||||||
|
--icon SeaLoader.ico
|
||||||
|
|
||||||
|
- name: Prepare artifact
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Force -Path dist_upload | Out-Null
|
||||||
|
Copy-Item dist\SeaLoader.exe dist_upload\SeaLoader.exe
|
||||||
|
if (Test-Path README.md) { Copy-Item README.md dist_upload\ }
|
||||||
|
if (Test-Path LICENSE) { Copy-Item LICENSE dist_upload\ }
|
||||||
|
Compress-Archive -Path dist_upload\* -DestinationPath SeaLoader_Windows_x64.zip -Force
|
||||||
|
|
||||||
|
- name: Upload asset to Release
|
||||||
|
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||||
|
shell: powershell
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
RELEASE_ID: ${{ github.event.release.id }}
|
||||||
|
SERVER_URL: ${{ github.server_url }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$uploadUrl = "$env:SERVER_URL/api/v1/repos/$env:REPO/releases/$env:RELEASE_ID/assets?name=SeaLoader_Windows_x64.zip"
|
||||||
|
Write-Host "Uploading asset to $uploadUrl"
|
||||||
|
Invoke-RestMethod -Method Post -Uri $uploadUrl -Headers @{ Authorization = "token $env:TOKEN" } -ContentType "application/zip" -InFile "SeaLoader_Windows_x64.zip"
|
||||||
|
|
||||||
|
# CI artifact upload removed for GHES compatibility
|
||||||
|
|
||||||
29
About.xml
@@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ModMetaData>
|
|
||||||
<name>RT Fuse</name>
|
|
||||||
<author>Ratys</author>
|
|
||||||
<packageId>ratys.rtfuse</packageId>
|
|
||||||
<url>https://ludeon.com/forums/index.php?topic=11272</url>
|
|
||||||
<supportedVersions>
|
|
||||||
<li>1.0</li>
|
|
||||||
<li>1.1</li>
|
|
||||||
<li>1.2</li>
|
|
||||||
<li>1.3</li>
|
|
||||||
<li>1.4</li>
|
|
||||||
<li>1.5</li>
|
|
||||||
<li>1.6</li>
|
|
||||||
</supportedVersions>
|
|
||||||
<modDependencies>
|
|
||||||
<li>
|
|
||||||
<packageId>brrainz.harmony</packageId>
|
|
||||||
<displayName>Harmony</displayName>
|
|
||||||
<steamWorkshopUrl>steam://url/CommunityFilePage/2009463077</steamWorkshopUrl>
|
|
||||||
<downloadUrl>https://github.com/pardeike/HarmonyRimWorld/releases/latest</downloadUrl>
|
|
||||||
</li>
|
|
||||||
</modDependencies>
|
|
||||||
<description>Researchable (RT Mods research tab) electric fuses to mitigate short circuits. When placed anywhere on a power network, each fuse will safely discharge up to three of network's batteries, mitigating or preventing the explosion.
|
|
||||||
|
|
||||||
Does not require a new colony to add or remove (might throw a one-time error).
|
|
||||||
|
|
||||||
Refer to forum thread for additional information.</description>
|
|
||||||
</ModMetaData>
|
|
||||||
60
README.md
@@ -1,14 +1,15 @@
|
|||||||
# Steam Workshop Collection Manager
|
# Steam Workshop Collection Manager
|
||||||
|
|
||||||
A Python GUI application for extracting Steam Workshop mod IDs from RimWorld mod collections.
|
A Python GUI application for extracting Steam Workshop mod IDs from RimWorld mod collections with configurable settings.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Dark themed Windows-style GUI
|
- Dark themed Windows-style GUI
|
||||||
- Three input fields for different mod collection types (Core, Content, Cosmetics)
|
- Configurable collection URLs via .env file
|
||||||
- Automatic extraction of Workshop IDs from Steam collection URLs
|
- Automatic workshop path derivation from RimWorld game path
|
||||||
- Real-time output display with scrollable text area
|
- ModsConfig.xml integration for active mod tracking
|
||||||
- Pre-populated with example RimWorld mod collections
|
- Real-time output display with scrollable text areas
|
||||||
|
- Local mod folder listing and comparison
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -18,6 +19,20 @@ A Python GUI application for extracting Steam Workshop mod IDs from RimWorld mod
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application uses a `.env` file for configuration. You can modify the default settings:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Default Collection URLs
|
||||||
|
CORE_COLLECTION_URL=https://steamcommunity.com/workshop/filedetails/?id=3521297585
|
||||||
|
CONTENT_COLLECTION_URL=steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712
|
||||||
|
COSMETICS_COLLECTION_URL=steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646
|
||||||
|
|
||||||
|
# ModsConfig.xml Path Template
|
||||||
|
MODSCONFIG_PATH_TEMPLATE=%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Run the application:
|
1. Run the application:
|
||||||
@@ -25,16 +40,31 @@ A Python GUI application for extracting Steam Workshop mod IDs from RimWorld mod
|
|||||||
python steam_workshop_gui.py
|
python steam_workshop_gui.py
|
||||||
```
|
```
|
||||||
|
|
||||||
2. The application comes pre-populated with example collection URLs:
|
2. **Set RimWorld Path**:
|
||||||
- **Core**: https://steamcommunity.com/workshop/filedetails/?id=3521297585
|
- Right-click RimWorld in Steam → Manage → Browse local files
|
||||||
- **Content**: steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712
|
- Copy and paste that path into the "RimWorld Game Folder" field
|
||||||
- **Cosmetics**: steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646
|
- The workshop path will automatically derive to `steamapps\workshop\content\294100`
|
||||||
|
|
||||||
3. You can modify these URLs or enter your own Steam Workshop collection URLs
|
3. **ModsConfig.xml**:
|
||||||
|
- The path is automatically detected
|
||||||
|
- You can manually edit if needed
|
||||||
|
- Click "Load Active Mods" to see currently enabled mods
|
||||||
|
|
||||||
4. Click "Extract Workshop IDs" to process the collections
|
4. **Collection Analysis**:
|
||||||
|
- Modify collection URLs or use defaults from .env
|
||||||
|
- Click "Extract Workshop IDs" to process collections
|
||||||
|
- See which mods are [INSTALLED] or [MISSING]
|
||||||
|
|
||||||
5. The right panel will display all the Workshop IDs found in each collection
|
5. **Local Mod Management**:
|
||||||
|
- Click "List Local Mod Folders" to see downloaded workshop mods
|
||||||
|
- Compare with collection requirements and active mods
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- `steam_workshop_gui.py` - Main application
|
||||||
|
- `.env` - Configuration file
|
||||||
|
- `requirements.txt` - Python dependencies
|
||||||
|
- `README.md` - This file
|
||||||
|
|
||||||
## URL Formats Supported
|
## URL Formats Supported
|
||||||
|
|
||||||
@@ -43,6 +73,8 @@ A Python GUI application for extracting Steam Workshop mod IDs from RimWorld mod
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The application fetches collection data directly from Steam Workshop
|
- All paths are editable in the GUI
|
||||||
|
- Configuration URLs are loaded from .env file on startup
|
||||||
|
- Workshop path auto-derives from RimWorld path (steamapps\common\RimWorld → steamapps\workshop\content\294100)
|
||||||
- Processing may take a few seconds depending on collection size
|
- Processing may take a few seconds depending on collection size
|
||||||
- Workshop IDs are extracted from the HTML content of collection pages
|
- Workshop IDs are extracted from HTML content of collection pages
|
||||||
BIN
art/GameTitle.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
art/GameTitleOld.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
art/NotOwned_Anomaly.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
art/NotOwned_Biotech.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
art/NotOwned_Ideology.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
art/NotOwned_Royalty.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
art/RimWordFont4.ttf
Normal file
BIN
art/hudsonriggssystems.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
@@ -1 +1,3 @@
|
|||||||
requests>=2.25.1
|
requests>=2.25.1
|
||||||
|
python-dotenv>=0.19.0
|
||||||
|
pyglet>=1.5.0
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, scrolledtext, filedialog, messagebox
|
from tkinter import ttk, scrolledtext, filedialog, messagebox, font
|
||||||
import requests
|
import requests
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
import winreg
|
|
||||||
from pathlib import Path
|
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:
|
class SteamWorkshopGUI:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
@@ -15,11 +27,15 @@ class SteamWorkshopGUI:
|
|||||||
self.root.geometry("1200x800")
|
self.root.geometry("1200x800")
|
||||||
self.root.configure(bg='#2b2b2b')
|
self.root.configure(bg='#2b2b2b')
|
||||||
|
|
||||||
|
# Load custom font
|
||||||
|
self.load_custom_font()
|
||||||
|
|
||||||
# Configure dark theme
|
# Configure dark theme
|
||||||
self.setup_dark_theme()
|
self.setup_dark_theme()
|
||||||
|
|
||||||
# Initialize workshop folder
|
# Initialize paths
|
||||||
self.workshop_folder = None
|
self.workshop_folder = None
|
||||||
|
self.modsconfig_path = None
|
||||||
|
|
||||||
# Create main frame
|
# Create main frame
|
||||||
main_frame = tk.Frame(root, bg='#2b2b2b')
|
main_frame = tk.Frame(root, bg='#2b2b2b')
|
||||||
@@ -37,6 +53,110 @@ class SteamWorkshopGUI:
|
|||||||
self.create_input_section(left_frame)
|
self.create_input_section(left_frame)
|
||||||
self.create_output_section(right_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):
|
def setup_dark_theme(self):
|
||||||
"""Configure dark theme colors"""
|
"""Configure dark theme colors"""
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
@@ -51,7 +171,7 @@ class SteamWorkshopGUI:
|
|||||||
def create_input_section(self, parent):
|
def create_input_section(self, parent):
|
||||||
"""Create the left input section"""
|
"""Create the left input section"""
|
||||||
title_label = tk.Label(parent, text="Steam Workshop Collections",
|
title_label = tk.Label(parent, text="Steam Workshop Collections",
|
||||||
font=('Arial', 14, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
font=self.get_font(14, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
||||||
title_label.pack(pady=(0, 10))
|
title_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
# RimWorld game folder input
|
# RimWorld game folder input
|
||||||
@@ -59,12 +179,12 @@ class SteamWorkshopGUI:
|
|||||||
rimworld_frame.pack(fill='x', pady=(0, 10))
|
rimworld_frame.pack(fill='x', pady=(0, 10))
|
||||||
|
|
||||||
rimworld_label = tk.Label(rimworld_frame, text="RimWorld Game Folder:",
|
rimworld_label = tk.Label(rimworld_frame, text="RimWorld Game Folder:",
|
||||||
font=('Arial', 9, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
||||||
rimworld_label.pack(anchor='w')
|
rimworld_label.pack(anchor='w')
|
||||||
|
|
||||||
rimworld_help = tk.Label(rimworld_frame,
|
rimworld_help = tk.Label(rimworld_frame,
|
||||||
text="Right-click RimWorld in Steam > Manage > Browse local files, copy that path",
|
text="Right-click RimWorld in Steam > Manage > Browse local files, copy that path",
|
||||||
font=('Arial', 8), bg='#2b2b2b', fg='#888888', wraplength=380)
|
font=self.get_font(8), bg='#2b2b2b', fg='#888888', wraplength=380)
|
||||||
rimworld_help.pack(anchor='w', pady=(2, 5))
|
rimworld_help.pack(anchor='w', pady=(2, 5))
|
||||||
|
|
||||||
rimworld_input_frame = tk.Frame(rimworld_frame, bg='#2b2b2b')
|
rimworld_input_frame = tk.Frame(rimworld_frame, bg='#2b2b2b')
|
||||||
@@ -72,64 +192,76 @@ class SteamWorkshopGUI:
|
|||||||
|
|
||||||
self.rimworld_var = tk.StringVar()
|
self.rimworld_var = tk.StringVar()
|
||||||
self.rimworld_entry = tk.Entry(rimworld_input_frame, textvariable=self.rimworld_var,
|
self.rimworld_entry = tk.Entry(rimworld_input_frame, textvariable=self.rimworld_var,
|
||||||
font=('Arial', 9), bg='#404040', fg='#ffffff',
|
font=self.get_font(9), bg='#404040', fg='#ffffff',
|
||||||
insertbackground='#ffffff', relief='flat', bd=5)
|
insertbackground='#ffffff', relief='flat', bd=5)
|
||||||
self.rimworld_entry.pack(side='left', fill='x', expand=True)
|
self.rimworld_entry.pack(side='left', fill='x', expand=True)
|
||||||
self.rimworld_entry.bind('<KeyRelease>', self.on_rimworld_path_change)
|
self.rimworld_entry.bind('<KeyRelease>', self.on_rimworld_path_change)
|
||||||
|
|
||||||
browse_game_btn = tk.Button(rimworld_input_frame, text="Browse",
|
browse_game_btn = tk.Button(rimworld_input_frame, text="Browse",
|
||||||
command=self.browse_rimworld_folder,
|
command=self.browse_rimworld_folder,
|
||||||
bg='#505050', fg='white', font=('Arial', 8),
|
bg='#505050', fg='white', font=self.get_font(8),
|
||||||
relief='flat', padx=10)
|
relief='flat', padx=10)
|
||||||
browse_game_btn.pack(side='right', padx=(5, 0))
|
browse_game_btn.pack(side='right', padx=(5, 0))
|
||||||
|
|
||||||
# Workshop folder display (derived from RimWorld path)
|
# Workshop folder display (derived from RimWorld path)
|
||||||
workshop_frame = tk.Frame(parent, bg='#2b2b2b')
|
workshop_frame = tk.Frame(parent, bg='#2b2b2b')
|
||||||
workshop_frame.pack(fill='x', pady=(0, 15))
|
workshop_frame.pack(fill='x', pady=(0, 10))
|
||||||
|
|
||||||
workshop_label = tk.Label(workshop_frame, text="Workshop Folder (Auto-derived):",
|
workshop_label = tk.Label(workshop_frame, text="Workshop Folder (Auto-derived):",
|
||||||
font=('Arial', 9, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
||||||
workshop_label.pack(anchor='w')
|
workshop_label.pack(anchor='w')
|
||||||
|
|
||||||
self.workshop_var = tk.StringVar(value="Enter RimWorld path above")
|
self.workshop_var = tk.StringVar(value="Enter RimWorld path above")
|
||||||
self.workshop_display = tk.Entry(workshop_frame, textvariable=self.workshop_var,
|
self.workshop_display = tk.Entry(workshop_frame, textvariable=self.workshop_var,
|
||||||
font=('Arial', 8), bg='#404040', fg='#ffffff',
|
font=self.get_font(8), bg='#404040', fg='#ffffff',
|
||||||
insertbackground='#ffffff', relief='flat', bd=5, state='readonly')
|
insertbackground='#ffffff', relief='flat', bd=5)
|
||||||
self.workshop_display.pack(fill='x', pady=(5, 0))
|
self.workshop_display.pack(fill='x', pady=(5, 0))
|
||||||
|
self.workshop_display.bind('<KeyRelease>', 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('<KeyRelease>', self.on_modsconfig_path_change)
|
||||||
|
|
||||||
|
# Initialize ModsConfig path
|
||||||
|
self.find_modsconfig_path()
|
||||||
|
|
||||||
# Core collection
|
# Core collection
|
||||||
self.create_url_input(parent, "Core Collection:",
|
core_url = os.getenv('CORE_COLLECTION_URL', 'https://steamcommunity.com/workshop/filedetails/?id=3521297585')
|
||||||
"https://steamcommunity.com/workshop/filedetails/?id=3521297585")
|
self.create_url_input(parent, "Core Collection:", core_url)
|
||||||
|
|
||||||
# Content collection
|
# Content collection
|
||||||
self.create_url_input(parent, "Content Collection:",
|
content_url = os.getenv('CONTENT_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712')
|
||||||
"steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712")
|
self.create_url_input(parent, "Content Collection:", content_url)
|
||||||
|
|
||||||
# Cosmetics collection
|
# Cosmetics collection
|
||||||
self.create_url_input(parent, "Cosmetics Collection:",
|
cosmetics_url = os.getenv('COSMETICS_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646')
|
||||||
"steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646")
|
self.create_url_input(parent, "Cosmetics Collection:", cosmetics_url)
|
||||||
|
|
||||||
# Process button
|
# Load Progression Pack button
|
||||||
process_btn = tk.Button(parent, text="Extract Workshop IDs",
|
load_btn = tk.Button(parent, text="Load Progression Pack Complete",
|
||||||
command=self.process_collections,
|
command=self.load_progression_pack,
|
||||||
bg='#0078d4', fg='white', font=('Arial', 10, 'bold'),
|
bg='#0078d4', fg='white', font=self.get_font(12, 'bold'),
|
||||||
relief='flat', padx=20, pady=8)
|
relief='flat', padx=30, pady=12)
|
||||||
process_btn.pack(pady=10)
|
load_btn.pack(pady=30)
|
||||||
|
|
||||||
# Check local mods button
|
|
||||||
local_btn = tk.Button(parent, text="List Local Mod Folders",
|
|
||||||
command=self.list_local_mod_folders,
|
|
||||||
bg='#28a745', fg='white', font=('Arial', 10, 'bold'),
|
|
||||||
relief='flat', padx=20, pady=8)
|
|
||||||
local_btn.pack(pady=5)
|
|
||||||
|
|
||||||
def create_url_input(self, parent, label_text, default_url):
|
def create_url_input(self, parent, label_text, default_url):
|
||||||
"""Create a labeled URL input field"""
|
"""Create a labeled URL input field"""
|
||||||
label = tk.Label(parent, text=label_text, font=('Arial', 10, 'bold'),
|
label = tk.Label(parent, text=label_text, font=self.get_font(10, 'bold'),
|
||||||
bg='#2b2b2b', fg='#ffffff')
|
bg='#2b2b2b', fg='#ffffff')
|
||||||
label.pack(anchor='w', pady=(10, 5))
|
label.pack(anchor='w', pady=(10, 5))
|
||||||
|
|
||||||
entry = tk.Entry(parent, font=('Arial', 9), bg='#404040', fg='#ffffff',
|
entry = tk.Entry(parent, font=self.get_font(9), bg='#404040', fg='#ffffff',
|
||||||
insertbackground='#ffffff', relief='flat', bd=5)
|
insertbackground='#ffffff', relief='flat', bd=5)
|
||||||
entry.pack(fill='x', pady=(0, 10))
|
entry.pack(fill='x', pady=(0, 10))
|
||||||
entry.insert(0, default_url)
|
entry.insert(0, default_url)
|
||||||
@@ -143,186 +275,26 @@ class SteamWorkshopGUI:
|
|||||||
|
|
||||||
def create_output_section(self, parent):
|
def create_output_section(self, parent):
|
||||||
"""Create the right output section"""
|
"""Create the right output section"""
|
||||||
# Main output area
|
title_label = tk.Label(parent, text="Workshop IDs and Package Names",
|
||||||
main_output_frame = tk.Frame(parent, bg='#2b2b2b')
|
font=self.get_font(14, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
||||||
main_output_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
title_label = tk.Label(main_output_frame, text="Workshop IDs Output",
|
|
||||||
font=('Arial', 14, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
|
||||||
title_label.pack(pady=(0, 10))
|
title_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
# Output text area with scrollbar
|
# Output text area with scrollbar
|
||||||
self.output_text = scrolledtext.ScrolledText(main_output_frame,
|
self.output_text = scrolledtext.ScrolledText(parent,
|
||||||
font=('Consolas', 10),
|
font=self.get_font(10),
|
||||||
bg='#1e1e1e', fg='#ffffff',
|
bg='#1e1e1e', fg='#ffffff',
|
||||||
insertbackground='#ffffff',
|
insertbackground='#ffffff',
|
||||||
selectbackground='#404040',
|
selectbackground='#404040',
|
||||||
relief='flat', bd=5, height=20)
|
relief='flat', bd=5)
|
||||||
self.output_text.pack(fill=tk.BOTH, expand=True)
|
self.output_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Local mod folders section
|
def on_workshop_path_change(self, event=None):
|
||||||
local_mods_frame = tk.Frame(parent, bg='#2b2b2b')
|
"""Called when workshop path is manually changed"""
|
||||||
local_mods_frame.pack(fill=tk.X, pady=(10, 0))
|
self.workshop_folder = self.workshop_var.get().strip()
|
||||||
|
|
||||||
local_title_label = tk.Label(local_mods_frame, text="Local Mod Folders",
|
def on_modsconfig_path_change(self, event=None):
|
||||||
font=('Arial', 12, 'bold'), bg='#2b2b2b', fg='#ffffff')
|
"""Called when ModsConfig path is manually changed"""
|
||||||
local_title_label.pack(pady=(0, 5))
|
self.modsconfig_path = self.modsconfig_var.get().strip()
|
||||||
|
|
||||||
# Local mods text area
|
|
||||||
self.local_mods_text = scrolledtext.ScrolledText(local_mods_frame,
|
|
||||||
font=('Consolas', 9),
|
|
||||||
bg='#1e1e1e', fg='#ffffff',
|
|
||||||
insertbackground='#ffffff',
|
|
||||||
selectbackground='#404040',
|
|
||||||
relief='flat', bd=5, height=8)
|
|
||||||
self.local_mods_text.pack(fill=tk.X)
|
|
||||||
|
|
||||||
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 process_collections(self):
|
|
||||||
"""Process all collection URLs and extract workshop IDs"""
|
|
||||||
self.output_text.delete(1.0, tk.END)
|
|
||||||
self.output_text.insert(tk.END, "Processing collections...\n\n")
|
|
||||||
self.root.update()
|
|
||||||
|
|
||||||
# Process each collection in a separate thread to avoid blocking UI
|
|
||||||
threading.Thread(target=self._process_collections_thread, daemon=True).start()
|
|
||||||
|
|
||||||
def _process_collections_thread(self):
|
|
||||||
"""Thread function to process collections"""
|
|
||||||
try:
|
|
||||||
for collection_name, entry in self.url_entries.items():
|
|
||||||
url = entry.get().strip()
|
|
||||||
if url:
|
|
||||||
self._safe_update_output(f"=== {collection_name.upper()} COLLECTION ===\n")
|
|
||||||
|
|
||||||
workshop_id = self.extract_workshop_id(url)
|
|
||||||
if workshop_id:
|
|
||||||
self._safe_update_output(f"Collection ID: {workshop_id}\n")
|
|
||||||
|
|
||||||
# Fetch collection contents
|
|
||||||
workshop_ids = self.fetch_collection_items(workshop_id)
|
|
||||||
if workshop_ids:
|
|
||||||
self._safe_update_output(f"Found {len(workshop_ids)} items:\n")
|
|
||||||
|
|
||||||
# Check which mods are installed locally if workshop folder is available
|
|
||||||
local_mods = set()
|
|
||||||
if self.workshop_folder and os.path.exists(self.workshop_folder):
|
|
||||||
try:
|
|
||||||
local_mods = {item for item in os.listdir(self.workshop_folder)
|
|
||||||
if os.path.isdir(os.path.join(self.workshop_folder, item)) and item.isdigit()}
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
missing_count = 0
|
|
||||||
for item_id in workshop_ids:
|
|
||||||
status = " [INSTALLED]" if item_id in local_mods else " [MISSING]"
|
|
||||||
if item_id not in local_mods:
|
|
||||||
missing_count += 1
|
|
||||||
self._safe_update_output(f"{item_id}{status}\n")
|
|
||||||
|
|
||||||
if local_mods:
|
|
||||||
self._safe_update_output(f"\nSummary: {len(workshop_ids) - missing_count} installed, {missing_count} missing\n")
|
|
||||||
else:
|
|
||||||
self._safe_update_output("Could not fetch collection items\n")
|
|
||||||
else:
|
|
||||||
self._safe_update_output("Could not extract workshop ID from URL\n")
|
|
||||||
|
|
||||||
self._safe_update_output("\n")
|
|
||||||
|
|
||||||
self._safe_update_output("Processing complete!")
|
|
||||||
except Exception as e:
|
|
||||||
self._safe_update_output(f"Error in processing: {str(e)}\n")
|
|
||||||
|
|
||||||
def find_steam_workshop_folder(self):
|
|
||||||
"""Automatically find the Steam Workshop folder for RimWorld"""
|
|
||||||
try:
|
|
||||||
# Method 1: Try to find Steam installation path from registry
|
|
||||||
steam_paths = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check HKEY_LOCAL_MACHINE first
|
|
||||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Valve\Steam") as key:
|
|
||||||
install_path = winreg.QueryValueEx(key, "InstallPath")[0]
|
|
||||||
steam_paths.append(install_path)
|
|
||||||
except (FileNotFoundError, OSError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check HKEY_CURRENT_USER
|
|
||||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Valve\Steam") as key:
|
|
||||||
install_path = winreg.QueryValueEx(key, "SteamPath")[0]
|
|
||||||
steam_paths.append(install_path)
|
|
||||||
except (FileNotFoundError, OSError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Method 2: Check common Steam installation locations
|
|
||||||
common_paths = [
|
|
||||||
r"C:\Program Files (x86)\Steam",
|
|
||||||
r"C:\Program Files\Steam",
|
|
||||||
r"D:\Steam",
|
|
||||||
r"E:\Steam",
|
|
||||||
os.path.expanduser(r"~\Steam")
|
|
||||||
]
|
|
||||||
steam_paths.extend(common_paths)
|
|
||||||
|
|
||||||
# Method 3: Check if Steam is in PATH
|
|
||||||
try:
|
|
||||||
import shutil
|
|
||||||
steam_exe = shutil.which("steam.exe")
|
|
||||||
if steam_exe:
|
|
||||||
steam_paths.append(os.path.dirname(steam_exe))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Look for the workshop folder in each potential Steam path
|
|
||||||
for steam_path in steam_paths:
|
|
||||||
if not steam_path:
|
|
||||||
continue
|
|
||||||
|
|
||||||
workshop_path = os.path.join(steam_path, "steamapps", "workshop", "content", "294100")
|
|
||||||
if os.path.exists(workshop_path):
|
|
||||||
return workshop_path
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error finding Steam workshop folder: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def browse_workshop_folder(self):
|
|
||||||
"""Allow user to manually select the workshop folder"""
|
|
||||||
folder = filedialog.askdirectory(
|
|
||||||
title="Select RimWorld Workshop Folder (steamapps/workshop/content/294100)",
|
|
||||||
initialdir=self.workshop_folder or "C:\\"
|
|
||||||
)
|
|
||||||
if folder:
|
|
||||||
# Validate that this looks like a workshop folder
|
|
||||||
if "294100" in folder or messagebox.askyesno(
|
|
||||||
"Confirm Folder",
|
|
||||||
f"Are you sure this is the RimWorld workshop folder?\n\n{folder}"
|
|
||||||
):
|
|
||||||
self._safe_update_output(f"Workshop folder updated: {folder}\n")
|
|
||||||
|
|
||||||
def on_rimworld_path_change(self, event=None):
|
def on_rimworld_path_change(self, event=None):
|
||||||
"""Called when RimWorld path is changed"""
|
"""Called when RimWorld path is changed"""
|
||||||
@@ -359,46 +331,356 @@ class SteamWorkshopGUI:
|
|||||||
self.rimworld_var.set(folder)
|
self.rimworld_var.set(folder)
|
||||||
self.on_rimworld_path_change() # Update workshop path
|
self.on_rimworld_path_change() # Update workshop path
|
||||||
|
|
||||||
def list_local_mod_folders(self):
|
def find_modsconfig_path(self):
|
||||||
"""List all local mod folder names in the separate text box"""
|
"""Find the ModsConfig.xml file in AppData"""
|
||||||
if not self.workshop_folder or not os.path.exists(self.workshop_folder):
|
|
||||||
self.local_mods_text.delete(1.0, tk.END)
|
|
||||||
self.local_mods_text.insert(tk.END, "Workshop folder not found or invalid.\nPlease enter correct RimWorld path above.")
|
|
||||||
return
|
|
||||||
|
|
||||||
threading.Thread(target=self._list_local_mod_folders_thread, daemon=True).start()
|
|
||||||
|
|
||||||
def _list_local_mod_folders_thread(self):
|
|
||||||
"""Thread function to list local mod folders"""
|
|
||||||
try:
|
try:
|
||||||
# Clear the local mods text box
|
# Get path template from .env
|
||||||
self.local_mods_text.delete(1.0, tk.END)
|
path_template = os.getenv('MODSCONFIG_PATH_TEMPLATE',
|
||||||
|
r'%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml')
|
||||||
|
|
||||||
if not os.path.exists(self.workshop_folder):
|
# Expand environment variables
|
||||||
self.local_mods_text.insert(tk.END, f"Workshop folder does not exist:\n{self.workshop_folder}")
|
modsconfig_path = os.path.expandvars(path_template)
|
||||||
return
|
|
||||||
|
|
||||||
# Get all subdirectories (each represents a workshop mod)
|
if os.path.exists(modsconfig_path):
|
||||||
local_mods = []
|
self.modsconfig_path = modsconfig_path
|
||||||
for item in os.listdir(self.workshop_folder):
|
self.modsconfig_var.set(modsconfig_path)
|
||||||
item_path = os.path.join(self.workshop_folder, item)
|
return modsconfig_path
|
||||||
if os.path.isdir(item_path) and item.isdigit():
|
else:
|
||||||
local_mods.append(item)
|
self.modsconfig_var.set("ModsConfig.xml not found")
|
||||||
|
return None
|
||||||
local_mods.sort()
|
|
||||||
|
|
||||||
# Display in the local mods text box
|
|
||||||
self.local_mods_text.insert(tk.END, f"Found {len(local_mods)} local workshop mod folders:\n\n")
|
|
||||||
for mod_id in local_mods:
|
|
||||||
self.local_mods_text.insert(tk.END, f"{mod_id}\n")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.local_mods_text.delete(1.0, tk.END)
|
self.modsconfig_var.set(f"Error finding ModsConfig.xml: {str(e)}")
|
||||||
self.local_mods_text.insert(tk.END, f"Error listing local mods: {str(e)}")
|
return None
|
||||||
|
|
||||||
def check_local_mods(self):
|
def extract_workshop_id(self, url):
|
||||||
"""Check which mods are installed locally in the workshop folder (for backward compatibility)"""
|
"""Extract workshop ID from Steam URL"""
|
||||||
self.list_local_mod_folders()
|
# 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 = [
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>',
|
||||||
|
'<savedModList>',
|
||||||
|
'\t<meta>',
|
||||||
|
'\t\t<gameVersion>1.6.4633 rev1261</gameVersion>',
|
||||||
|
'\t\t<modIds>'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add core mods first
|
||||||
|
for package_id, mod_name in core_mods:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
|
||||||
|
|
||||||
|
# Add workshop mod package IDs to meta section
|
||||||
|
for workshop_id, package_id in mod_data:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
|
||||||
|
|
||||||
|
xml_lines.extend([
|
||||||
|
'\t\t</modIds>',
|
||||||
|
'\t\t<modSteamIds>'
|
||||||
|
])
|
||||||
|
|
||||||
|
# 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<li>0</li>')
|
||||||
|
|
||||||
|
# Add workshop IDs to meta section
|
||||||
|
for workshop_id, package_id in mod_data:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{workshop_id}</li>')
|
||||||
|
|
||||||
|
xml_lines.extend([
|
||||||
|
'\t\t</modSteamIds>',
|
||||||
|
'\t\t<modNames>'
|
||||||
|
])
|
||||||
|
|
||||||
|
# Add core mod names first
|
||||||
|
for package_id, mod_name in core_mods:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
|
||||||
|
|
||||||
|
# Add workshop mod names to meta section
|
||||||
|
for mod_name in mod_names:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
|
||||||
|
|
||||||
|
xml_lines.extend([
|
||||||
|
'\t\t</modNames>',
|
||||||
|
'\t</meta>',
|
||||||
|
'\t<modList>',
|
||||||
|
'\t\t<ids>'
|
||||||
|
])
|
||||||
|
|
||||||
|
# Add core mods first to modList section
|
||||||
|
for package_id, mod_name in core_mods:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
|
||||||
|
|
||||||
|
# Add workshop mod package IDs to modList section
|
||||||
|
for workshop_id, package_id in mod_data:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
|
||||||
|
|
||||||
|
xml_lines.extend([
|
||||||
|
'\t\t</ids>',
|
||||||
|
'\t\t<names>'
|
||||||
|
])
|
||||||
|
|
||||||
|
# Add core mod names first to modList section
|
||||||
|
for package_id, mod_name in core_mods:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
|
||||||
|
|
||||||
|
# Add workshop mod names to modList section
|
||||||
|
for mod_name in mod_names:
|
||||||
|
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
|
||||||
|
|
||||||
|
xml_lines.extend([
|
||||||
|
'\t\t</names>',
|
||||||
|
'\t</modList>',
|
||||||
|
'</savedModList>'
|
||||||
|
])
|
||||||
|
|
||||||
|
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):
|
def _safe_update_output(self, text):
|
||||||
"""Safely update output text without recursion"""
|
"""Safely update output text without recursion"""
|
||||||
@@ -485,14 +767,6 @@ class SteamWorkshopGUI:
|
|||||||
self._safe_update_output(f"Error fetching collection: {str(e)}\n")
|
self._safe_update_output(f"Error fetching collection: {str(e)}\n")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _update_output(self, text):
|
|
||||||
"""Update output text (called from main thread)"""
|
|
||||||
try:
|
|
||||||
self.output_text.insert(tk.END, text)
|
|
||||||
self.output_text.see(tk.END)
|
|
||||||
except Exception:
|
|
||||||
pass # Prevent recursion errors
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
app = SteamWorkshopGUI(root)
|
app = SteamWorkshopGUI(root)
|
||||||
|
|||||||