This repository has been archived on 2026-10-10. You can view files and clone it, but cannot push or open issues or pull requests.
molotov/docs/superpowers/plans/2026-09-29-molotov-phase4-final-mile.md

20 KiB

Molotov Phase 4: The Final Mile — End-to-End Orchestration, Cleaner, Region Routing & Interactive UI Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Complete the remaining user-facing and integration systems for Molotov: the end-to-end launch pipeline, cache/version cleaner tool, server region router, Discord Rich Presence (RPC) daemon, and interactive GUI/CLI wire-up.

Architecture: Connects molotov-bootstrap, molotov-core, molotov-cli, and molotov-gui. Scans and cleans stale versions from disk, passes region-routing parameters into engine flags, broadcasts playtime over Discord IPC socket (/run/user/<uid>/discord-ipc-0), and wires up the egui dashboard buttons to real operations.

Tech Stack: Rust 1.75+, tokio, serde, serde_json, eframe / egui, clap, tracing.

Spec: docs/superpowers/specs/2026-09-29-molotov-architecture-design.md (Sections 4, 5, 6, 8)

Global Constraints

  • Language & Edition: Rust 1.75+ (2021 edition), strictly zero Wine or Proton.
  • License: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files.
  • Atomic & Clean Operation: Never leave partial files on disk; cleaning must never delete the active/current Roblox version.
  • Safe Mode Fallback: Automatically activates minimal flags if consecutive startup failures are detected.

Review Focus

  1. Active Version Deletion Prevention: Cleaner must never delete the currently active version directory.
  2. Discord Socket Non-Blocking Failure: If Discord is not running or socket is absent, RPC connection attempts must gracefully no-op without lagging the game or throwing panics.
  3. Region Route Parameter Validation: Invalid or malformed region codes must default to Auto without crashing.
  4. GUI Background Threading: Launching games or downloading updates from the GUI must not freeze the egui UI render loop.
  5. Partial Download Purging: Cleaner must cleanly purge .part files in ~/.cache/molotov/downloads/.

Task 1: Real Disk & Cache Cleaner Engine

Files:

  • Create: crates/molotov-bootstrap/src/cleaner.rs
  • Modify: crates/molotov-bootstrap/src/lib.rs
  • Test: crates/molotov-bootstrap/tests/cleaner_test.rs

Interfaces:

  • Consumes: None

  • Produces:

    • molotov_bootstrap::cleaner::Cleaner
    • molotov_bootstrap::cleaner::CleanReport
  • Step 1: Write the failing test for version and cache cleanup

// crates/molotov-bootstrap/tests/cleaner_test.rs
use molotov_bootstrap::cleaner::Cleaner;
use tempfile::tempdir;

#[test]
fn test_cleaner_purges_stale_versions_and_part_files() {
    let temp = tempdir().unwrap();
    let versions_dir = temp.path().join("versions");
    let downloads_dir = temp.path().join("downloads");
    std::fs::create_dir_all(&versions_dir).unwrap();
    std::fs::create_dir_all(&downloads_dir).unwrap();

    // Create current version and stale version
    let current_ver = versions_dir.join("version-current");
    let stale_ver = versions_dir.join("version-stale-1");
    std::fs::create_dir_all(&current_ver).unwrap();
    std::fs::create_dir_all(&stale_ver).unwrap();
    std::fs::write(current_ver.join("libroblox.so"), b"ACTIVE_LIB").unwrap();
    std::fs::write(stale_ver.join("libroblox.so"), b"STALE_LIB").unwrap();

    // Create leftover .part file
    let part_file = downloads_dir.join("download.apk.part");
    std::fs::write(&part_file, b"DANGLING_PART").unwrap();

    let cleaner = Cleaner::new(versions_dir, downloads_dir);
    let report = cleaner.clean_stale_data("version-current").unwrap();

    assert_eq!(report.purged_versions.len(), 1);
    assert_eq!(report.purged_versions[0], "version-stale-1");
    assert_eq!(report.purged_parts_count, 1);
    assert!(report.reclaimed_bytes > 0);
    assert!(!stale_ver.exists(), "Stale version must be deleted");
    assert!(!part_file.exists(), "Dangling .part file must be deleted");
    assert!(current_ver.exists(), "Active version must NOT be deleted");
}
  • Step 2: Run test to verify it fails

Run: cargo test -p molotov-bootstrap --test cleaner_test Expected: FAIL with unexported module cleaner.

  • Step 3: Implement Cleaner
// crates/molotov-bootstrap/src/cleaner.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use std::path::PathBuf;

#[derive(Debug, Clone, Default)]
pub struct CleanReport {
    pub purged_versions: Vec<String>,
    pub purged_parts_count: usize,
    pub reclaimed_bytes: u64,
}

pub struct Cleaner {
    versions_root: PathBuf,
    downloads_root: PathBuf,
}

impl Cleaner {
    pub fn new(versions_root: PathBuf, downloads_root: PathBuf) -> Self {
        Self {
            versions_root,
            downloads_root,
        }
    }

    pub fn clean_stale_data(&self, active_version_guid: &str) -> Result<CleanReport, MolotovError> {
        let mut report = CleanReport::default();

        // 1. Scan versions directory
        if self.versions_root.exists() {
            for entry in std::fs::read_dir(&self.versions_root)? {
                let entry = entry?;
                let file_name = entry.file_name().to_string_lossy().to_string();
                if entry.file_type()?.is_dir() && file_name != active_version_guid {
                    let dir_size = calculate_dir_size(&entry.path());
                    std::fs::remove_dir_all(entry.path())?;
                    report.purged_versions.push(file_name);
                    report.reclaimed_bytes += dir_size;
                }
            }
        }

        // 2. Scan downloads directory for leftover .part files
        if self.downloads_root.exists() {
            for entry in std::fs::read_dir(&self.downloads_root)? {
                let entry = entry?;
                let path = entry.path();
                if path.extension().and_then(|e| e.to_str()) == Some("part") {
                    let file_size = entry.metadata()?.len();
                    std::fs::remove_file(path)?;
                    report.purged_parts_count += 1;
                    report.reclaimed_bytes += file_size;
                }
            }
        }

        Ok(report)
    }
}

fn calculate_dir_size(path: &std::path::Path) -> u64 {
    let mut total = 0;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            if let Ok(meta) = entry.metadata() {
                if meta.is_dir() {
                    total += calculate_dir_size(&entry.path());
                } else {
                    total += meta.len();
                }
            }
        }
    }
    total
}

Update crates/molotov-bootstrap/src/lib.rs to expose pub mod cleaner;.

  • Step 4: Run test to verify it passes

Run: cargo test -p molotov-bootstrap --test cleaner_test Expected: PASS.

  • Step 5: Commit
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement disk cache and version cleaner"

Task 2: Preferred Server Region Selector & Network Configuration

Files:

  • Create: crates/molotov-bootstrap/src/region.rs
  • Modify: crates/molotov-bootstrap/src/lib.rs
  • Test: crates/molotov-bootstrap/tests/region_test.rs

Interfaces:

  • Consumes: None

  • Produces:

    • molotov_bootstrap::region::ServerRegion
    • molotov_bootstrap::region::ServerRegion::from_str()
    • molotov_bootstrap::region::ServerRegion::apply_to_fflags()
  • Step 1: Write the failing test for region parsing and FastFlag routing

// crates/molotov-bootstrap/tests/region_test.rs
use molotov_bootstrap::fflags::FastFlagManager;
use molotov_bootstrap::region::ServerRegion;

#[test]
fn test_server_region_parsing_and_flags() {
    assert_eq!(ServerRegion::from_code("eu"), ServerRegion::Europe);
    assert_eq!(ServerRegion::from_code("us-east"), ServerRegion::USEast);
    assert_eq!(ServerRegion::from_code("invalid_garbage"), ServerRegion::Auto);

    let mut manager = FastFlagManager::new();
    ServerRegion::Europe.apply_to_fflags(&mut manager);

    // Verify flag was applied
    assert_eq!(
        manager.active_profile().flags.get("FStringPreferredGameRegion").map(|v| format!("{:?}", v)),
        Some("Text(\"eu-central-1\")".to_string())
    );
}
  • Step 2: Run test to verify it fails

Run: cargo test -p molotov-bootstrap --test region_test Expected: FAIL with unexported module region.

  • Step 3: Implement ServerRegion
// crates/molotov-bootstrap/src/region.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::fflags::{FastFlagManager, FlagValue};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ServerRegion {
    #[default]
    Auto,
    USEast,
    USWest,
    Europe,
    Asia,
    Australia,
    SouthAmerica,
}

impl ServerRegion {
    pub fn from_code(code: &str) -> Self {
        let clean = code.trim().to_lowercase();
        match clean.as_str() {
            "us-east" | "useast" | "virginia" => ServerRegion::USEast,
            "us-west" | "uswest" | "california" => ServerRegion::USWest,
            "eu" | "europe" | "frankfurt" | "london" => ServerRegion::Europe,
            "asia" | "tokyo" | "singapore" => ServerRegion::Asia,
            "aus" | "australia" | "sydney" => ServerRegion::Australia,
            "sa" | "south-america" | "brazil" => ServerRegion::SouthAmerica,
            _ => ServerRegion::Auto,
        }
    }

    pub fn display_name(&self) -> &'static str {
        match self {
            ServerRegion::Auto => "Auto / Default",
            ServerRegion::USEast => "US East (Virginia)",
            ServerRegion::USWest => "US West (California)",
            ServerRegion::Europe => "Europe (Frankfurt / London)",
            ServerRegion::Asia => "Asia (Tokyo / Singapore)",
            ServerRegion::Australia => "Australia (Sydney)",
            ServerRegion::SouthAmerica => "South America (São Paulo)",
        }
    }

    pub fn region_identifier(&self) -> Option<&'static str> {
        match self {
            ServerRegion::Auto => None,
            ServerRegion::USEast => Some("us-east-1"),
            ServerRegion::USWest => Some("us-west-1"),
            ServerRegion::Europe => Some("eu-central-1"),
            ServerRegion::Asia => Some("ap-northeast-1"),
            ServerRegion::Australia => Some("ap-southeast-2"),
            ServerRegion::SouthAmerica => Some("sa-east-1"),
        }
    }

    pub fn apply_to_fflags(&self, manager: &mut FastFlagManager) {
        if let Some(id) = self.region_identifier() {
            manager.set_flag("FStringPreferredGameRegion", FlagValue::Text(id.to_string()));
        }
    }
}

Update crates/molotov-bootstrap/src/lib.rs to expose pub mod region;.

  • Step 4: Run test to verify it passes

Run: cargo test -p molotov-bootstrap --test region_test Expected: PASS.

  • Step 5: Commit
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement server region selector and FastFlag network routing"

Task 3: Zero-Dependency Discord Rich Presence Client

Files:

  • Create: crates/molotov-core/src/discord.rs
  • Modify: crates/molotov-core/src/lib.rs
  • Test: crates/molotov-core/tests/discord_rpc_test.rs

Interfaces:

  • Consumes: None

  • Produces:

    • molotov_core::discord::DiscordRpc
    • molotov_core::discord::DiscordActivity
  • Step 1: Write the failing test for Discord IPC frame creation and non-blocking failure

// crates/molotov-core/tests/discord_rpc_test.rs
use molotov_core::discord::{DiscordActivity, DiscordRpc};

#[test]
fn test_discord_rpc_payload_construction() {
    let activity = DiscordActivity {
        details: "Playing Brookhaven RP".to_string(),
        state: "In Game".to_string(),
        start_timestamp: 1690000000,
    };

    let payload = activity.to_json_payload();
    assert!(payload.contains("Brookhaven RP"));
    assert!(payload.contains("In Game"));
}

#[test]
fn test_discord_rpc_connect_non_blocking_when_socket_missing() {
    let mut rpc = DiscordRpc::new("123456789012345678");
    // Connect attempt when Discord socket is absent must return Ok(false) or Err cleanly, never panic
    let connected = rpc.try_connect().unwrap_or(false);
    assert!(!connected);
}
  • Step 2: Run test to verify it fails

Run: cargo test -p molotov-core --test discord_rpc_test Expected: FAIL with unexported module discord.

  • Step 3: Implement DiscordRpc
// crates/molotov-core/src/discord.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use std::os::unix::net::UnixStream;
use std::path::PathBuf;

#[derive(Debug, Clone)]
pub struct DiscordActivity {
    pub details: String,
    pub state: String,
    pub start_timestamp: u64,
}

impl DiscordActivity {
    pub fn to_json_payload(&self) -> String {
        format!(
            r#"{{"cmd":"SET_ACTIVITY","args":{{"pid":{},"activity":{{"details":"{}","state":"{}","timestamps":{{"start":{}}},"assets":{{"large_image":"roblox_logo","large_text":"Roblox on Linux (Molotov)"}}}}}},"nonce":"1"}}"#,
            std::process::id(),
            self.details,
            self.state,
            self.start_timestamp
        )
    }
}

pub struct DiscordRpc {
    client_id: String,
    stream: Option<UnixStream>,
}

impl DiscordRpc {
    pub fn new(client_id: &str) -> Self {
        Self {
            client_id: client_id.to_string(),
            stream: None,
        }
    }

    pub fn try_connect(&mut self) -> Result<bool, String> {
        let uid = unsafe { libc::getuid() };
        let socket_path = PathBuf::from(format!("/run/user/{}/discord-ipc-0", uid));

        if !socket_path.exists() {
            return Ok(false);
        }

        match UnixStream::connect(&socket_path) {
            Ok(s) => {
                self.stream = Some(s);
                tracing::info!("DiscordRpc: successfully connected to Discord IPC socket");
                Ok(true)
            }
            Err(e) => {
                tracing::debug!("DiscordRpc: failed to connect to socket: {}", e);
                Ok(false)
            }
        }
    }

    pub fn is_connected(&self) -> bool {
        self.stream.is_some()
    }
}

Update crates/molotov-core/src/lib.rs to expose pub mod discord;.

  • Step 4: Run test to verify it passes

Run: cargo test -p molotov-core --test discord_rpc_test Expected: PASS.

  • Step 5: Commit
git add crates/molotov-core/
git commit -m "feat(core): implement zero-dependency Discord Rich Presence IPC client"

Task 4: Unified Launch Orchestrator

Files:

  • Create: crates/molotov-bootstrap/src/launcher.rs
  • Modify: crates/molotov-bootstrap/src/lib.rs
  • Test: crates/molotov-bootstrap/tests/launcher_test.rs

Interfaces:

  • Consumes: All molotov-bootstrap modules (cdn, apk, fflags, mods, sandbox, watchdog)

  • Produces:

    • molotov_bootstrap::launcher::Launcher
    • molotov_bootstrap::launcher::LaunchOptions
    • molotov_bootstrap::launcher::Launcher::prepare_launch()
  • Step 1: Write the failing test for launcher orchestration flow

// crates/molotov-bootstrap/tests/launcher_test.rs
use molotov_bootstrap::launcher::{LaunchOptions, Launcher};
use molotov_bootstrap::region::ServerRegion;
use tempfile::tempdir;

#[test]
fn test_launcher_prepare_flow() {
    let temp = tempdir().unwrap();
    let root = temp.path().to_path_buf();

    let launcher = Launcher::new(root);
    let options = LaunchOptions {
        place_id: Some(1818),
        safe_mode: false,
        region: ServerRegion::Europe,
        instance_id: "default".to_string(),
    };

    let plan = launcher.prepare_launch(options).expect("Should prepare launch plan");
    assert!(plan.fflags_path.exists());
    assert_eq!(plan.place_id, Some(1818));
}
  • Step 2: Run test to verify it fails

Run: cargo test -p molotov-bootstrap --test launcher_test Expected: FAIL with unexported module launcher.

  • Step 3: Implement Launcher
// crates/molotov-bootstrap/src/launcher.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use crate::fflags::{FastFlagManager, PresetProfile};
use crate::region::ServerRegion;
use crate::sandbox::{SandboxConfig, SandboxRunner};
use crate::watchdog::{LaunchStatus, LaunchWatchdog};
use std::path::PathBuf;

#[derive(Debug, Clone)]
pub struct LaunchOptions {
    pub place_id: Option<u64>,
    pub safe_mode: bool,
    pub region: ServerRegion,
    pub instance_id: String,
}

pub struct LaunchPlan {
    pub place_id: Option<u64>,
    pub fflags_path: PathBuf,
    pub sandbox_config: SandboxConfig,
    pub is_safe_mode: bool,
}

pub struct Launcher {
    base_dir: PathBuf,
}

impl Launcher {
    pub fn new(base_dir: PathBuf) -> Self {
        Self { base_dir }
    }

    pub fn prepare_launch(&self, options: LaunchOptions) -> Result<LaunchPlan, MolotovError> {
        let state_file = self.base_dir.join("launch_state.json");
        let mut watchdog = LaunchWatchdog::new(state_file);

        // Check if Safe Mode is required
        let activate_safe_mode = options.safe_mode || watchdog.status() == LaunchStatus::RequiresSafeMode;
        watchdog.record_start()?;

        let mut fflag_mgr = FastFlagManager::new();
        if activate_safe_mode {
            tracing::warn!("Launcher: launching in SAFE MODE with minimal default flags");
            fflag_mgr.apply_preset(PresetProfile::Default);
        } else {
            fflag_mgr.apply_preset(PresetProfile::LowVram);
            options.region.apply_to_fflags(&mut fflag_mgr);
        }

        let fflags_path = self.base_dir.join("ClientAppSettings.json");
        fflag_mgr.export_client_app_settings(&fflags_path)?;

        let sandbox_config = SandboxConfig {
            instance_id: options.instance_id.clone(),
            data_dir: self.base_dir.join("instances").join(&options.instance_id).join("data"),
            config_dir: self.base_dir.join("instances").join(&options.instance_id).join("config"),
            nice_priority: -5,
        };

        Ok(LaunchPlan {
            place_id: options.place_id,
            fflags_path,
            sandbox_config,
            is_safe_mode: activate_safe_mode,
        })
    }
}

Update crates/molotov-bootstrap/src/lib.rs to expose pub mod launcher;.

  • Step 4: Run test to verify it passes

Run: cargo test -p molotov-bootstrap --test launcher_test Expected: PASS.

  • Step 5: Commit
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement unified Launch orchestrator"

Task 5: Interactive GUI & CLI Wire-Up

Files:

  • Modify: crates/molotov-cli/src/lib.rs
  • Modify: crates/molotov-cli/src/commands.rs
  • Modify: crates/molotov-gui/src/app.rs
  • Test: crates/molotov-cli/tests/cli_test.rs
  • Test: crates/molotov-gui/tests/gui_test.rs

Interfaces:

  • Consumes: molotov_bootstrap::launcher::Launcher, molotov_bootstrap::cleaner::Cleaner, molotov_core::discord::DiscordRpc

  • Produces: Live executable commands and GUI interactions

  • Step 1: Write test verifying CLI clean and launch execution

// In crates/molotov-cli/tests/cli_test.rs
use clap::Parser;
use molotov_cli::commands::Cli;

#[test]
fn test_cli_parse_region_flag() {
    let parsed = Cli::try_parse_from(["molotov", "launch", "--region", "eu"]);
    assert!(parsed.is_ok());
}
  • Step 2: Implement CLI and GUI connections

Update crates/molotov-cli/src/commands.rs to add --region <REGION> option to Commands::Launch. Update crates/molotov-cli/src/lib.rs to execute Launcher::prepare_launch and Cleaner::clean_stale_data. Update crates/molotov-gui/src/app.rs to invoke Launcher, Cleaner, and display region dropdown and Discord RPC status.

  • Step 3: Run tests to verify they pass

Run: cargo test --workspace Expected: PASS.

  • Step 4: Commit
git add crates/molotov-cli/ crates/molotov-gui/
git commit -m "feat: wire up interactive GUI and CLI subcommands to launcher, cleaner, and region router"