Control Flow Documentation
WeMod Launcher - Control Flow and Logic Documentation
Table of Contents
- Project Overview
- Architecture Summary
- Entry Points and Initialization
- Core Control Flow
- Module Responsibilities
- Configuration System
- Process Flows
- Data Flow Patterns
- Environment Variables
- Error Handling and Logging
Project Overview
Purpose
The WeMod Launcher is a Python-based application that enables running WeMod (a Windows game modding/cheating tool) on Linux and Steam Deck systems via Wine/Proton compatibility layers. It transparently integrates WeMod into Steam games by managing Wine prefixes, handling dependencies, and orchestrating the game launch process.
Technology Stack
- Language: Python 3
- GUI Framework: FreeSimpleGUI
- HTTP Library: requests
- Compatibility Layer: Wine/Proton/GE-Proton
- Package Manager: winetricks
- Version Control: Git (for self-updates)
- Container Support: Flatpak (with sandbox escape)
Project Structure
wemod-launcher/
├── wemod # Main executable entry point (936 lines)
├── setup.py # Initialization and setup logic (473 lines)
├── wemod.bat # Windows batch file for game/WeMod orchestration
├── consts.py # Constants and path initialization (144 lines)
├── corenodep.py # Dependency-free core utilities (163 lines)
├── coreutils.py # GUI and logging infrastructure (478 lines)
├── constutils.py # Wine/Proton environment utilities (402 lines)
├── mainutils.py # Downloads, file operations, UI (671 lines)
├── requirements.txt # Python dependencies
├── wemod.conf # User configuration (INI format)
├── wemod.log # Application log file
├── wemod_data/ # Shared WeMod user data directory
├── wemod_bin/ # WeMod.exe installation directory
├── winetricks # Downloaded winetricks utility
└── .cache/ # Temporary files and trackingTotal Code: ~2,329 lines of Python + batch script
Architecture Summary
Architectural Patterns
┌─────────────────────────────────────────────────────────────┐
│ Steam Game Launch │
│ /path/to/wemod-launcher/wemod %command% │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────▼────────────────┐
│ Phase 1: Initialization │
│ ------------------------- │
│ • Version tracking │
│ • Dependency management │
│ • Self-update from Git │
│ • Flatpak sandbox detection │
└────────────────┬────────────────┘
│
┌────────────────▼────────────────┐
│ Phase 2: Wine Prefix Setup │
│ ------------------------- │
│ • Detect/create prefix │
│ • Sync WeMod data │
│ • Download/copy compatible │
│ • Build (dotnet48, dxvk) │
└────────────────┬────────────────┘
│
┌────────────────▼────────────────┐
│ Phase 3: Game Execution │
│ ------------------------- │
│ • Construct launch command │
│ • Monitor game timing │
│ • Execute via Proton/Wine │
│ • Post-game troubleshooting │
└─────────────────────────────────┘Component Layering
┌─────────────────────────────────────────────┐
│ User Interface Layer │
│ (FreeSimpleGUI dialogs, progress bars) │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Application Logic Layer │
│ (wemod script, setup.py, run()) │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Service Layer │
│ (mainutils, coreutils, constutils) │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Infrastructure Layer │
│ (Wine/Proton, winetricks, GitHub API) │
└─────────────────────────────────────────────┘Entry Points and Initialization
Primary Entry Point: /wemod Script
The main entry point is the wemod executable script invoked by Steam with the launch option:
/path/to/wemod-launcher/wemod %command%Initialization Sequence (wemod:64-111)
if __name__ == "__main__":
# 1. Version tracking and config update
script_manager()
# 2. Ensure dependencies are installed
python_venv = venv_manager()
# 3. Auto-update from Git if configured
python_venv = self_update(python_venv)
# 4. Detect and escape Flatpak sandbox if needed
if_flatpak_list = check_flatpak(python_venv)
# 5. Re-run in venv or on host if needed
if len(if_flatpak_list) > 0:
# Infinite loop protection
inf_protect = os.getenv("WeModInfProtect", "1")
if int(inf_protect) > 4:
exit_with_message("Infinite rerun detected")
# Increment counter and re-execute
os.environ["WeModInfProtect"] = str(int(inf_protect) + 1)
# Build command with venv python or flatpak-spawn
command = if_flatpak_list + [SCRIPT_FILE] + sys.argv[1:]
process = subprocess.run(command, capture_output=True)
sys.exit(process.returncode)Key Functions Called:
script_manager()(coreutils.py): Tracks version, updates configvenv_manager()(setup.py:201): Creates/manages Python virtual environmentself_update()(setup.py:257): Updates launcher from Git repositorycheck_flatpak()(setup.py:345): Detects Flatpak and escapes sandbox
Infinite Loop Protection
The WeModInfProtect environment variable prevents infinite re-runs when:
- Creating virtual environments
- Escaping Flatpak sandbox
- Auto-updating from Git
Maximum allowed reruns: 4 (wemod:78)
Core Control Flow
Main Execution Path: run() Function (wemod:649-914)
The run() function is the core orchestrator that handles the entire game launch process.
Step 1: Parse Steam Arguments (wemod:651-721)
Steam passes arguments in one of two formats:
Format A: Standard Proton
[reaper_cmd...] /proton/path waitforexitandrun game.exe [options...]Format B: Custom Launcher
[reaper_cmd...] -- /proton/path custom_launcher.exe game.exe [options...]The parser identifies:
REAPER_CMD: Steam’s command prefix (monitoring, logging)PROTON: Path to Proton executableverb: “waitforexitandrun” or empty (for custom launchers)GAME_EXE: Actual game executableLAUNCH_OPTIONS: Game launch arguments
Step 2: Initialize Wine Prefix (wemod:724-725)
init(PROTON, not bool(verb))The init() function (wemod:295-442) handles prefix setup:
def init(proton: str, iswine: bool = False) -> None:
# 1. Create wine prefix if doesn't exist
if not os.path.isdir(WINEPREFIX):
os.makedirs(BASE_STEAM_COMPAT, exist_ok=True)
# 2. Set up wine environment and get version
prefix_version_file = ensure_wine()
current_version_parts = parse_version(
read_file(prefix_version_file)
)
# 3. Check if WeMod is installed
if not os.path.exists(INIT_FILE):
# Try to find compatible existing prefix
closest_version, closest_prefix_folder = \
scanfolderforversions(current_version_parts)
# Prompt user to copy compatible prefix
if closest_version and user_accepts:
copy_folder_with_progress(
closest_prefix_folder, BASE_STEAM_COMPAT
)
syncwemod()
return
# If no compatible prefix or user declined
prefix_op = popup_options(
"Prefix Setup",
"Download or build prefix?",
[["download", "build"]]
)
if prefix_op == "build":
build_prefix(proton_dir)
else:
download_prefix(proton_dir)
syncwemod()Prefix Setup Decision Tree:
WeMod installed? ───┐
│
NO ◄─────┘
│
├─► Scan for compatible prefix
│ ├─► Found exact match (same major.minor)
│ │ └─► Prompt: "Very likely compatible"
│ ├─► Found same major, different minor
│ │ └─► Prompt: "Likely compatible"
│ └─► Found different major
│ └─► Prompt: "Maybe compatible"
│
├─► User accepts? ──YES──► Copy prefix
│ │
│ NO
│ │
└───► Prompt: "Download or build?"
├─► download ──► download_prefix()
└─► build ─────► build_prefix()Step 3: Sync WeMod Data (wemod:135-292)
The syncwemod() function ensures all Wine prefixes share the same WeMod user data:
def syncwemod(folder: Optional[str] = None) -> None:
WeModData = os.path.join(SCRIPT_PATH, "wemod_data")
WeModExternal = os.path.join(
folder or BASE_STEAM_COMPAT,
"pfx/drive_c/users/steamuser/AppData/Roaming/WeMod"
)
# Create launcher data dir if doesn't exist
if not os.path.isdir(WeModData):
os.makedirs(WeModData)
# If external is a real directory (not symlink)
if os.path.isdir(WeModExternal) and \
not os.path.islink(WeModExternal):
# Handle account conflict
if both_have_data:
response = show_message(
"Use Launcher dir account (Yes) or "
"prefix dir account (No)?",
yesno=True
)
if response == "No":
# Copy external to launcher dir
shutil.copytree(WeModExternal, WeModData)
# Remove external directory
shutil.rmtree(WeModExternal)
# Create symlink
if not os.path.exists(WeModExternal):
os.symlink(WeModData, WeModExternal)Data Sync Flow:
Launcher Dir: wemod_data/
Prefix Dir: pfx/drive_c/.../WeMod/
Case 1: Both empty
└─► Create symlink
Case 2: Launcher has data, prefix empty
└─► Create symlink
Case 3: Prefix has data, launcher empty
└─► Copy prefix → launcher
└─► Create symlink
Case 4: Both have data
└─► Prompt user
└─► Overwrite chosen target
└─► Create symlinkStep 4: Build Final Command (wemod:727-790)
# Construct the final command
FINAL = (
REAPER_CMD + # Steam's command prefix
[PROTON] + # Proton executable
verb + # ["waitforexitandrun"] or []
BAT_COMMAND + # ["start", "Z:\\...\\wemod.bat"]
GAME_FRONT + # Optional pre-game command
[WIN_CMD] + # Windows-format game path
LAUNCH_OPTIONS # Game arguments
)Example Final Command:
/home/.../reaper SteamLaunch ... \
/steamapps/common/Proton/proton \
waitforexitandrun \
start "Z:\\home\\...\\wemod.bat" \
"C:\\game\\game.exe" \
-windowed -nosoundStep 5: Monitor Game Timing (wemod:793-809)
A background thread monitors if the game closes too quickly:
ttfile = ".cache/early.tmp"
returnfile = ".cache/return.tmp"
# Create tracking file
open(ttfile, "w").close()
# Start monitoring thread
ttime_thread = threading.Thread(
target=monitor_file,
args=(ttfile, 90, returnfile) # 90 second timeout
)
ttime_thread.start()Monitoring Logic (coreutils.py):
def monitor_file(ttfile, timeout, returnfile):
# Wait for timeout (90 seconds)
time.sleep(timeout)
# If tracking file still exists, game is running
if os.path.exists(ttfile):
return # Game started successfully
# Game closed early, check if batch wrote message
if os.path.exists(returnfile):
message = read_file(returnfile)
response = bat_respond(returnfile, timeout)
# User can choose to keep WeMod runningStep 6: Execute Command (wemod:861-900)
Regular Mode:
process = subprocess.Popen(
FINAL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate()
# Wait for Wine to fully shut down
if using_custom_launcher:
subprocess.run(["wineserver", "--wait"])Flatpak Mode (wemod:804-855):
# Write command to file for host to execute
with open("insideflatpak.tmp", "w") as f:
for line in FINAL:
f.write(line + "\n")
# Wait for file to be removed (host executing)
while os.path.isfile("insideflatpak.tmp"):
time.sleep(1)
# Check for errors
if os.path.isfile("flatpakerror.tmp"):
raise Exception(read_file("flatpakerror.tmp"))Step 7: Post-Game Troubleshooting (wemod:911)
troubleshooter()The troubleshooter presents options after the game closes (constutils.py):
- Disable/enable troubleshooter (globally or per-game)
- Delete game prefix (full reinstall)
- Delete WeMod.exe (force WeMod update)
- View logs
Module Responsibilities
1. wemod - Main Launcher Script (936 lines)
Primary Responsibilities:
- Application entry point
- Initialization orchestration
- Game launch coordination
- Command parsing and construction
Key Functions:
run(skip_init): Main game launcher (wemod:649)init(proton, iswine): Initialize Wine prefix (wemod:295)syncwemod(folder): Sync WeMod user data (wemod:135)download_prefix(proton_dir): Download pre-built prefix (wemod:445)build_prefix(proton_dir): Build prefix from scratch (wemod:568)
2. setup.py - Setup and Initialization (473 lines)
Primary Responsibilities:
- First-time setup
- Dependency management
- Self-update mechanism
- Flatpak integration
Key Functions:
venv_manager(): Create/manage virtual environment (setup.py:201)self_update(path): Update from Git repository (setup.py:257)check_flatpak(flatpak_cmd): Detect and escape Flatpak (setup.py:345)setup_main(): Download WeMod and winetricks (setup.py:387)download_wemod(temp_dir): Download WeMod installer (setup.py:64)get_wemod_exe_url(): Fetch WeMod URL from Scoop bucket (setup.py:115)unpack_wemod(): Extract WeMod from ZIP (setup.py:135)
3. consts.py - Constants and Path Initialization (144 lines)
Primary Responsibilities:
- Initialize global constants
- Resolve Wine prefix paths
- Handle environment variables
Key Functions:
getbatcmd(): Load or download wemod.bat (consts.py:24)get_compat(): Resolve Steam compatibility path (consts.py:70)get_scan_folder(): Determine folder to scan for prefixes (consts.py:130)
Global Constants:
BAT_COMMAND # ["start", "Z:\\...\\wemod.bat"]
BASE_STEAM_COMPAT # Wine prefix root directory
STEAM_COMPAT_FOLDER # Parent of all prefix directories
SCAN_FOLDER # Directory to scan for WeMod prefixes
WINEPREFIX # Wine prefix path (.../pfx)
INIT_FILE # Marker file (.wemod_installer)4. corenodep.py - Dependency-Free Core (163 lines)
Primary Responsibilities:
- Configuration file I/O
- Version parsing
- Path conversion
- List manipulation
Key Functions:
load_conf_setting(setting, section): Read from wemod.conf (corenodep.py)save_conf_setting(setting, value, section): Write to wemod.confparse_version(version_str): Extract major.minor versionwinpath(path, dobble, addfront): Convert Linux → Windows pathscheck_dependencies(requirements_file): Verify Python packages
No external dependencies - safe for early initialization.
5. coreutils.py - GUI and Logging (478 lines)
Primary Responsibilities:
- User interface dialogs
- Logging infrastructure
- Process monitoring
- File caching
Key Functions:
log(message, open_log): Write to wemod.logshow_message(message, title, timeout, yesno): Display popupexit_with_message(): Error dialog and exitmonitor_file(ttfile, timeout): Background game monitoringbat_respond(responsefile, timeout): Handle early game closepopup_options(title, message, options, timeout): Multi-button dialogget_user_input(title, message, default, timeout): Text inputscript_manager(): Version trackingpip(command, venv_path): Manage pip installationcache(file_path, default_func): Simple file caching
6. constutils.py - Wine/Proton Environment (402 lines)
Primary Responsibilities:
- Wine prefix management
- Prefix compatibility scanning
- Winetricks integration
- Post-game troubleshooting
Key Functions:
ensure_wine(verstr): Initialize wine prefix (constutils.py:57)scanfolderforversions(current_version): Find compatible prefixes (constutils.py:129)winetricks(command, proton_bin): Execute winetricks (constutils.py)wine(command, proton_bin): Execute wine commands (constutils.py)troubleshooter(): Post-game menu dialog (constutils.py)
Prefix Compatibility Priority:
- Priority 1: Exact match (same major.minor)
- Priority 2: Same major, lower minor (prefer higher)
- Priority 3: Same major, higher minor (prefer lower)
- Priority 4: Lower major (prefer higher)
- Priority 5: Higher major (prefer lower)
7. mainutils.py - Downloads and File Operations (671 lines)
Primary Responsibilities:
- HTTP downloads with progress
- File operations (copy, zip, extract)
- GitHub API integration
- Flatpak utilities
Key Functions:
download_progress(link, file_name, callback): Download with progresspopup_download(title, link, file_name): Download with GUIunpack_zip_with_progress(zip_path, dest_path): Extract ZIPcopy_folder_with_progress(source, dest, zipup, ...): Smart copyget_github_releases(repo_name): Fetch releases from GitHubfind_closest_compatible_release(releases, version): Match versionget_dotnet48(): Cache/download .NET Framework 4.8popup_execute(title, command): Run command with live outputis_flatpak(): Detect Flatpak sandboxflatpakrunner(): Run commands on host from Flatpakderef(path): Convert symlinks to real files
8. wemod.bat - Windows Batch Script (110 lines)
Primary Responsibilities:
- Launch WeMod in background
- Wait for game to close
- Detect early game close
- Kill WeMod when game exits
Execution Flow:
1. Parse paths and arguments
2. Start WeMod.exe in background
3. Retry finding WeMod PID (up to 3 attempts)
4. Start game and wait for exit
5. If game closed too fast (<90s):
- Delete .cache/early.tmp
- Write message to .cache/return.tmp
- Wait for Python to delete return.tmp
6. Kill WeMod processConfiguration System
Configuration File: wemod.conf (INI Format)
Location: wemod-launcher/wemod.conf
Sections and Settings:
[Settings]
# Launcher metadata
Version=1.535 # Current launcher version
ScriptName=wemod-launcher # Script identifier
# Paths
WeModLog=wemod.log # Log file path
VirtualEnvironment=wemod_venv # Virtual environment directory
SteamCompatDataPath= # Override Steam compat path
WinePrefixPath= # Override wine prefix path
ScanFolder= # Override scan folder
# GitHub integration
RepoUser=DeckCheatz # GitHub user for prefixes
RepoName=BuiltPrefixes-dev # GitHub repo for prefixes
# Features
Troubleshoot=true # Enable post-game troubleshooter
PackagePrefix= # Zip current prefix to file
SelfUpdate= # Allow auto-update from Git
NoEXE= # Skip game EXE validation
ProtonMinorSeven= # Track GE-Proton7 for uploading
FlatpakRunning= # Flatpak execution stateConfiguration Functions (corenodep.py):
def load_conf_setting(setting, section="Settings"):
config = configparser.ConfigParser()
config.read(os.path.join(SCRIPT_PATH, "wemod.conf"))
if not config.has_section(section):
return None
if not config.has_option(section, setting):
return None
return config.get(section, setting)
def save_conf_setting(setting, value, section="Settings"):
config = configparser.ConfigParser()
config.read(os.path.join(SCRIPT_PATH, "wemod.conf"))
if not config.has_section(section):
config.add_section(section)
if value is None:
config.remove_option(section, setting)
else:
config.set(section, setting, str(value))
with open(os.path.join(SCRIPT_PATH, "wemod.conf"), "w") as f:
config.write(f)Configuration Precedence
For most settings, the precedence order is:
- Environment variable (highest priority)
- wemod.conf file
- Default value (lowest priority)
Example: Wine prefix path
# 1. Check config file
ccompat = load_conf_setting("SteamCompatDataPath")
wcompat = load_conf_setting("WinePrefixPath")
# 2. Check environment variables
if os.getenv("WINE_PREFIX_PATH"):
os.environ["WINEPREFIX"] = os.getenv("WINE_PREFIX_PATH")
ecompat = os.getenv("STEAM_COMPAT_DATA_PATH")
# 3. Use environment or fall back to config
if not ecompat:
ecompat = wcompat or os.getenv("WINEPREFIX")
# 4. Default (error if still not set)
if not ecompat:
exit_with_message("Not running wine")Process Flows
Flow 1: First-Time Setup
User launches game with wemod %command%
│
├─► script_manager() - Create wemod.conf
│
├─► venv_manager() - Check dependencies
│ ├─► Dependencies OK? ──YES──► Continue
│ └─► NO
│ ├─► Try: pip install -r requirements.txt
│ ├─► Success? ──YES──► Continue
│ └─► NO
│ ├─► mk_venv() - Create virtual environment
│ ├─► pip install in venv
│ └─► Re-run with venv python
│
├─► self_update() - Check Git for updates
│ ├─► On main branch? ──NO──► Skip update
│ └─► YES
│ ├─► git fetch
│ ├─► Local == Remote? ──YES──► Skip update
│ └─► NO
│ ├─► git reset --hard origin
│ ├─► git pull
│ └─► chmod +x scripts
│
├─► check_flatpak() - Detect sandbox
│ ├─► In Flatpak? ──NO──► Continue
│ └─► YES
│ ├─► FROM_FLATPAK set? ──YES──► Continue
│ └─► NO
│ ├─► Build flatpak-spawn command
│ ├─► Re-execute on host
│ └─► Exit with host return code
│
└─► run() - Main game launch
├─► init(PROTON) - Setup prefix
│ ├─► ensure_wine() - Create prefix structure
│ ├─► WeMod installed? ──YES──► syncwemod()
│ └─► NO
│ ├─► scanfolderforversions()
│ │ ├─► Found compatible? ──YES──► copy_folder()
│ │ └─► NO
│ │
│ ├─► Prompt: "Download or build?"
│ │ ├─► download
│ │ │ ├─► get_github_releases()
│ │ │ ├─► find_closest_compatible_release()
│ │ │ ├─► popup_download()
│ │ │ └─► unpack_zip_with_progress()
│ │ │
│ │ └─► build
│ │ ├─► deref() - Dereference symlinks
│ │ ├─► popup_options() - dotnet48 method
│ │ ├─► winetricks() - Install deps
│ │ └─► wine() - Install dotnet48 (if selected)
│ │
│ └─► syncwemod() - Link WeMod data
│
├─► Construct FINAL command
├─► monitor_file() thread - 90s timeout
├─► subprocess.Popen(FINAL)
├─► wineserver --wait (if custom launcher)
└─► troubleshooter() - Post-game menuFlow 2: Subsequent Launches
User launches game
│
├─► script_manager() - Update config
├─► venv_manager() - Check dependencies (already installed)
├─► self_update() - Skip (no updates)
├─► check_flatpak() - Skip (FROM_FLATPAK set or not in Flatpak)
│
└─► run()
├─► init(PROTON)
│ ├─► ensure_wine() - Verify prefix exists
│ ├─► WeMod installed? ──YES──► syncwemod()
│ └─► Continue
│
├─► Construct FINAL command
├─► monitor_file() thread
├─► subprocess.Popen(FINAL)
│ │
│ └─► Proton executes wemod.bat
│ ├─► start WeMod.exe (background)
│ ├─► Get WeMod PID from tasklist
│ ├─► start /wait game.exe
│ │ │
│ │ └─► Game runs
│ │ ├─► Game closes < 90s
│ │ │ ├─► Delete .cache/early.tmp
│ │ │ ├─► Write to .cache/return.tmp
│ │ │ └─► bat_respond() - Prompt user
│ │ │
│ │ └─► Game closes > 90s
│ │ └─► monitor_file() deletes early.tmp
│ │
│ └─► taskkill /PID WeMod.exe
│
├─► wineserver --wait
└─► troubleshooter()
├─► Disable troubleshooter?
├─► Enable troubleshooter?
├─► Delete game prefix?
└─► Delete WeMod.exe?Flow 3: Flatpak Execution
User launches from Flatpak Steam
│
├─► is_flatpak() ──YES──► FROM_FLATPAK set? ──NO
│ │
│ └─► check_flatpak()
│ ├─► Build command:
│ │ flatpak-spawn --host
│ │ --env=STEAM_COMPAT_DATA_PATH=...
│ │ --env=FROM_FLATPAK=true
│ │ --env=WeModInfProtect=2
│ │ -- python wemod [args]
│ │
│ ├─► subprocess.run(command)
│ └─► Exit with return code
│
└─► Host execution (FROM_FLATPAK=true)
├─► run()
│ ├─► init(PROTON)
│ ├─► Construct FINAL command
│ ├─► Write command to insideflatpak.tmp
│ │
│ └─► Flatpak side (original process):
│ ├─► flatpakrunner() thread
│ │ ├─► Read insideflatpak.tmp
│ │ ├─► subprocess.Popen(FINAL)
│ │ ├─► Capture stdout/stderr
│ │ ├─► Write errors to flatpakerror.tmp
│ │ ├─► Delete insideflatpak.tmp
│ │ └─► Exit thread
│ │
│ └─► Host side (current process):
│ ├─► while insideflatpak.tmp exists:
│ │ └─► sleep(1)
│ │
│ ├─► Check flatpakerror.tmp
│ │ └─► Raise exception if exists
│ │
│ └─► troubleshooter()
│
└─► Regular execution continues...Flow 4: Prefix Compatibility Scan
scanfolderforversions(current_version=[8, 26])
│
├─► For each folder in STEAM_COMPAT_FOLDER:
│ ├─► Read version file
│ ├─► Parse version string
│ ├─► Check if .wemod_installer exists
│ │
│ └─► Version comparison:
│ ├─► 8.26 == 8.26? ──YES──► Priority 1 (best)
│ ├─► 8.25 (same major, lower minor)? ──YES──► Priority 2
│ ├─► 8.27 (same major, higher minor)? ──YES──► Priority 3
│ ├─► 7.50 (lower major)? ──YES──► Priority 4
│ └─► 9.0 (higher major)? ──YES──► Priority 5 (worst)
│
└─► Return closest_version, closest_prefix_folder
Example scan results:
Current: 8.26
Available prefixes:
- 8.26 (priority 1) ← chosen
- 8.25 (priority 2)
- 8.30 (priority 3)
- 7.55 (priority 4)
- 9.0 (priority 5)Flow 5: GitHub Prefix Download
download_prefix(proton_dir)
│
├─► Load RepoUser and RepoName from config
│ Default: DeckCheatz/BuiltPrefixes-dev
│
├─► Get current Proton version from version file
│ Example: "GE-Proton8-26" → [8, 26]
│
├─► get_github_releases(repo_concat)
│ ├─► requests.get("https://api.github.com/repos/.../releases")
│ └─► Return list of releases
│
├─► find_closest_compatible_release(releases, [8, 26])
│ ├─► For each release:
│ │ ├─► Parse tag: "GE-Proton8.25" → [8, 25]
│ │ └─► Calculate compatibility priority
│ │
│ └─► Return closest_version, download_url
│
├─► Show compatibility dialog
│ ├─► Exact match: Auto-accept
│ ├─► Same major: "Likely compatible, download?"
│ └─► Different major: "Maybe compatible, download?"
│
├─► cache(file_name, download_func)
│ ├─► Check .cache/file_name exists?
│ │ ├─► YES: Return cached path
│ │ └─► NO: popup_download(url, file_name)
│ │ ├─► download_progress() with GUI
│ │ └─► Save to .cache/
│
├─► unpack_zip_with_progress(zip_path, BASE_STEAM_COMPAT)
│ ├─► Extract all files with progress bar
│ └─► Show percentage complete
│
├─► Delete cached ZIP (no longer needed)
└─► syncwemod()Flow 6: Prefix Building from Scratch
build_prefix(proton_dir)
│
├─► deref(winfolder)
│ ├─► For each file in winfolder:
│ │ ├─► Is symlink? ──YES──► Copy actual file
│ │ └─► NO ──► Continue
│ │
│ └─► Show progress bar
│
├─► popup_options("dotnet48 method")
│ ├─► "winetricks" (default for GE-Proton8+)
│ └─► "wemod-launcher" (ONLY for GE-Proton7)
│
├─► Prepare dependency list
│ ├─► Always: "sdl cjkfonts vkd3d dxvk2030"
│ └─► If winetricks: "dotnet48"
│
├─► setup_main() - Download WeMod.exe and winetricks
│
├─► For each dependency:
│ └─► winetricks(dep, proton_dir)
│ ├─► Set PATH and WINEPREFIX
│ ├─► Execute winetricks with dependency
│ └─► popup_execute() - Show live output
│
├─► If wemod-launcher method:
│ ├─► get_dotnet48() - Download/cache installer
│ ├─► wine("winecfg -v win7", path)
│ ├─► wine(dotnet48_installer, path)
│ │ └─► Check return code (0, 194, -15 are OK)
│ └─► wine("winecfg -v win10", path)
│
├─► Write INIT_FILE (.wemod_installer)
└─► syncwemod()Data Flow Patterns
1. Inter-Process Communication
The launcher uses file-based IPC for communication between processes:
┌─────────────────────────────────────────────────────────┐
│ Python Process │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Main Thread │ │
│ │ • Parse arguments │ │
│ │ • Init wine prefix │ │
│ │ • Build command │ │
│ │ • Execute subprocess │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Monitor Thread │ │
│ │ • sleep(90) │ │
│ │ • Check .cache/early.tmp exists │ │
│ │ • Read .cache/return.tmp if early close │ │
│ │ • Show dialog, wait for user │ │
│ │ • Delete .cache/return.tmp │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ subprocess.Popen(FINAL)
▼
┌─────────────────────────────────────────────────────────┐
│ Proton/Wine Process │
│ ┌──────────────────────────────────────────────────┐ │
│ │ wemod.bat │ │
│ │ • start WeMod.exe (background) │ │
│ │ • Get WeMod PID │ │
│ │ • start /wait game.exe │ │
│ │ • If .cache/early.tmp exists when game closes: │ │
│ │ - Delete .cache/early.tmp │ │
│ │ - Write message to .cache/return.tmp │ │
│ │ - Wait for .cache/return.tmp to be deleted │ │
│ │ • taskkill WeMod.exe │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘IPC Files:
.cache/early.tmp: Game timing tracker (created by Python, deleted by BAT or monitor thread).cache/return.tmp: Early close message (created by BAT, deleted by Python).cache/insideflatpak.tmp: Flatpak command file (created by host, deleted by Flatpak).cache/flatpakerror.tmp: Flatpak error message (created by Flatpak, deleted by host)
2. Configuration Data Flow
┌────────────────────────────────────────────────┐
│ Environment Variables │
│ (Highest Priority) │
│ • STEAM_COMPAT_DATA_PATH │
│ • WINEPREFIX / WINE_PREFIX_PATH │
│ • TROUBLESHOOT, SELF_UPDATE, etc. │
└────────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ wemod.conf File │
│ (Medium Priority) │
│ [Settings] │
│ SteamCompatDataPath=... │
│ WinePrefixPath=... │
│ Troubleshoot=true │
└────────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ Default Values │
│ (Lowest Priority) │
│ RepoUser=DeckCheatz │
│ RepoName=BuiltPrefixes-dev │
└────────────────────────────────────────────────┘
Reading Flow:
load_conf_setting("SteamCompatDataPath")
│
├─► Check environment: os.getenv("STEAM_COMPAT_DATA_PATH")
│ └─► If set, use this value
│
├─► Check config file: config.get("Settings", "SteamCompatDataPath")
│ └─► If exists, use this value
│
└─► Use default or exit with error3. WeMod Data Sync Flow
┌────────────────────────────────────────────────┐
│ Launcher Directory │
│ wemod_data/ │
│ ├── app_settings.json │
│ ├── user_data.db │
│ └── ... │
└────────────────┬───────────────────────────────┘
│
│ os.symlink()
│
▼
┌────────────────────────────────────────────────┐
│ Wine Prefix (Game 1) │
│ .../pfx/drive_c/users/steamuser/ │
│ AppData/Roaming/WeMod/ ───► (symlink) │
└─────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│ Wine Prefix (Game 2) │
│ .../pfx/drive_c/users/steamuser/ │
│ AppData/Roaming/WeMod/ ───► (symlink) │
└─────────────────────────────────────────────────┘
All game prefixes share the same WeMod account data4. Log Data Flow
┌─────────────────────────────────────────────────┐
│ Code Execution │
│ log("Message") │
└────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ coreutils.log() │
│ 1. Determine log file path: │
│ • WEMOD_LOG env var │
│ • WeModLog config setting │
│ • Default: wemod.log │
│ 2. Create parent directories │
│ 3. Append message with timestamp │
│ 4. If open_log=True, open file in editor │
└────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ wemod.log │
│ 2026-01-14 10:30:15 - Message line 1 │
│ 2026-01-14 10:30:16 - Message line 2 │
│ ... │
└─────────────────────────────────────────────────┘5. Prefix Download Cache Flow
User requests prefix download
│
▼
cache("wemod_prefix8.26.zip", download_func)
│
├─► Check .cache/wemod_prefix8.26.zip exists?
│ │
│ ├─► YES: Return ".cache/wemod_prefix8.26.zip"
│ │
│ └─► NO:
│ ├─► download_func()
│ │ └─► popup_download(url, file_name)
│ │ ├─► download_progress() with callback
│ │ ├─► Update progress bar
│ │ └─► Save to .cache/
│ │
│ └─► Return ".cache/wemod_prefix8.26.zip"
│
└─► unpack_zip_with_progress(cached_path, BASE_STEAM_COMPAT)
│
└─► Delete cached ZIP after extractionEnvironment Variables
Critical Environment Variables
| Variable | Purpose | Set By | Used By |
|---|---|---|---|
STEAM_COMPAT_TOOL_PATHS | Proton installation directories | Steam | consts.py, wemod |
STEAM_COMPAT_DATA_PATH | Game-specific Wine prefix path | Steam | consts.py |
WINEPREFIX | Wine prefix to use | Launcher/User | Wine, constutils.py |
WINE_PREFIX_PATH | Alternative wine prefix env var | User | consts.py |
WINE | Wine executable path (external runners) | User | consts.py |
Configuration Override Variables
| Variable | Config Equivalent | Purpose |
|---|---|---|
TROUBLESHOOT | Troubleshoot | Override troubleshooter setting |
SELF_UPDATE | SelfUpdate | Enable/disable auto-update |
WEMOD_LOG | WeModLog | Override log file path |
SCANFOLDER | ScanFolder | Override prefix scan directory |
NO_EXE | NoEXE | Skip game EXE validation |
REPO_STRING | RepoUser/RepoName | Override GitHub repo (user/repo) |
WAIT_ON_GAMECLOSE | - | Timeout for early close dialog (seconds) |
Special Control Variables
| Variable | Purpose | Values |
|---|---|---|
GAME_FRONT | Pre-game command to execute | JSON array string |
PACKAGEPREFIX | Package current prefix as ZIP | “true” or counter |
FORCE_UPDATE_WEMOD | Force WeMod redownload | “1” |
WeModInfProtect | Infinite rerun protection counter | “1” to “4” |
FROM_FLATPAK | Running from Flatpak host | “true” |
FLATPAK_ID | Flatpak container identifier | Container ID |
Variable Usage Examples
1. Using external Wine instead of Proton:
export WINE=/usr/bin/wine
export WINEPREFIX=/home/user/.wine
/path/to/wemod game.exe2. Overriding GitHub repository:
export REPO_STRING=MyUser/MyPrefixRepo
/path/to/wemod %command%3. Running pre-game command:
export GAME_FRONT='["cmd", "/c", "echo Starting game"]'
/path/to/wemod %command%4. Packaging current prefix:
export PACKAGEPREFIX=true
/path/to/wemod %command%
# Creates .../prefixes/GE-Proton8.26.zipError Handling and Logging
Logging System
Log Function (coreutils.py):
def log(message, open_log=False):
# 1. Determine log file
log_file = (
os.getenv("WEMOD_LOG") or
load_conf_setting("WeModLog") or
os.path.join(SCRIPT_PATH, "wemod.log")
)
# 2. Create parent directories
os.makedirs(os.path.dirname(log_file), exist_ok=True)
# 3. Append message with timestamp
with open(log_file, "a") as f:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
f.write(f"{timestamp} - {message}\n")
# 4. Optionally open log in editor
if open_log:
os.system(f"xdg-open '{log_file}'")Logging Levels:
- Info: Regular operation messages
- Warning: Unexpected but handled situations
- Error: Fatal errors (logged before exit_with_message)
- Debug: Detailed command and state information
Error Handling Patterns
1. Exit with Message (coreutils.py):
def exit_with_message(title, message, code=1, timeout=None, ask_for_log=False):
# Log the error
log(f"ERROR [{title}]: {message}")
# Show GUI dialog
if ask_for_log:
response = show_message(
f"{message}\n\nOpen log file?",
title, timeout, yesno=True
)
if response == "Yes":
log(open_log=True)
else:
show_message(message, title, timeout)
# Exit with code
sys.exit(code)2. Exception Handling in Main Loop (wemod:922-935):
if __name__ == "__main__":
RESPONCE = ""
logy = "No"
try:
RESPONCE = run()
except Exception as e:
RESPONCE = "ERR:\n" + str(e)
logy = show_message(
"Error occurred. Open the log?",
"Error occurred", 30, True
)
# Log final response
log(str(RESPONCE))
# Open log if user requested
if logy == "Yes":
log(open_log=True)3. Command Execution Error Handling (constutils.py):
def winetricks(command, proton_bin):
try:
returncode = popup_execute(
"Installing dependencies",
[WINETRICKS] + command.split()
)
if returncode != 0:
log(f"Winetricks failed with code {returncode}")
response = show_message(
f"Dependency installation failed (code {returncode}). Continue anyway?",
yesno=True
)
if response == "No":
exit_with_message("Installation failed", ...)
return returncode
except Exception as e:
log(f"Winetricks exception: {e}")
exit_with_message("Winetricks error", str(e), ask_for_log=True)Common Error Scenarios
1. Missing Dependencies:
Error: The python package 'venv' is not installed
Solution: Install python-venv or python3-venv package
Log: Check wemod.log for pip installation errors2. Wine Prefix Issues:
Error: wine prefix is missing
Solution: Run the game once without WeMod launcher
Log: Check STEAM_COMPAT_DATA_PATH environment variable3. Flatpak Sandbox:
Error: flatpak-xdg-utils not installed
Solution: Install flatpak-xdg-utils, allow system/session bus in Flatseal
Log: Check for flatpak-spawn errors in wemod.log4. Network Errors:
Error: Failed to download WeMod
Solution: Check internet connection, try FORCE_UPDATE_WEMOD=1
Log: Check download URLs and HTTP response codes5. Infinite Rerun Loop:
Error: Infinite script reruns were detected
Solution: Delete wemod.conf, check environment variables
Log: Check WeModInfProtect counter in wemod.logTroubleshooting Menu
The post-game troubleshooter (constutils.py) provides recovery options:
┌─────────────────────────────────────────────┐
│ Troubleshooter Menu │
├─────────────────────────────────────────────┤
│ 1. Disable troubleshooter (globally) │
│ 2. Disable troubleshooter (this game only) │
│ 3. Enable troubleshooter (globally) │
│ 4. Enable troubleshooter (this game only) │
│ 5. Delete game prefix (full reinstall) │
│ 6. Delete WeMod.exe (force WeMod update) │
│ 7. View logs │
│ 8. Close │
└─────────────────────────────────────────────┘Triggered When:
- Game closes normally (if Troubleshoot=true in config)
- Game closes early and user chooses to troubleshoot
- User manually requests troubleshooter via environment variable
Summary
This WeMod Launcher is a sophisticated Python application that:
- Manages Wine/Proton prefixes with automatic compatibility detection
- Downloads and builds pre-configured Wine environments
- Synchronizes WeMod data across multiple game installations
- Handles Flatpak sandboxing with transparent host execution
- Monitors game execution to detect launcher vs actual game
- Provides troubleshooting tools for common issues
- Auto-updates from Git repository
- Integrates with GitHub for prefix distribution
The control flow is designed to be robust, with multiple fallback mechanisms, user prompts for ambiguous situations, and comprehensive logging for debugging. The architecture separates concerns into logical modules while maintaining tight integration through shared constants and configuration.
Total Lines of Code: ~2,329 Python + 110 Batch = ~2,439 lines
Key Design Patterns:
- Separation of Concerns: Each module has distinct responsibilities
- Configuration Hierarchy: Env vars > Config file > Defaults
- Progressive Enhancement: Works without venv, Flatpak, or GitHub
- User Confirmation: Prompts for destructive or uncertain operations
- Comprehensive Logging: All significant operations are logged
- Error Recovery: Troubleshooting menu and clear error messages