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
- Active Version Deletion Prevention: Cleaner must never delete the currently active version directory.
- 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.
- Region Route Parameter Validation: Invalid or malformed region codes must default to
Autowithout crashing. - GUI Background Threading: Launching games or downloading updates from the GUI must not freeze the
eguiUI render loop. - Partial Download Purging: Cleaner must cleanly purge
.partfiles 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::Cleanermolotov_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(¤t_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::ServerRegionmolotov_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::DiscordRpcmolotov_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-bootstrapmodules (cdn,apk,fflags,mods,sandbox,watchdog) -
Produces:
molotov_bootstrap::launcher::Launchermolotov_bootstrap::launcher::LaunchOptionsmolotov_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"