Control Flow Documentation - DeckCheatz/wemod-launcher GitHub Wiki
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 tracking
Total 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"](/DeckCheatz/wemod-launcher/wiki/"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 symlink
Step 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 -nosound
Step 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 running
Step 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 process
Configuration 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 state
Configuration 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 menu
Flow 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 error
3. 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 data
4. 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 extraction
Environment 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.exe
2. 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.zip
Error 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 errors
2. Wine Prefix Issues:
Error: wine prefix is missing
Solution: Run the game once without WeMod launcher
Log: Check STEAM_COMPAT_DATA_PATH environment variable
3. 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.log
4. Network Errors:
Error: Failed to download WeMod
Solution: Check internet connection, try FORCE_UPDATE_WEMOD=1
Log: Check download URLs and HTTP response codes
5. Infinite Rerun Loop:
Error: Infinite script reruns were detected
Solution: Delete wemod.conf, check environment variables
Log: Check WeModInfProtect counter in wemod.log
Troubleshooting 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