From cee9baec84e7fde4461f3a79934e78de968dc3d6 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Tue, 29 Sep 2026 07:56:53 +0530 Subject: [PATCH] docs: add Phase 1 implementation plan for Molotov --- .../2026-09-29-molotov-phase1-bootstrapper.md | 1619 +++++++++++++++++ 1 file changed, 1619 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-29-molotov-phase1-bootstrapper.md diff --git a/docs/superpowers/plans/2026-09-29-molotov-phase1-bootstrapper.md b/docs/superpowers/plans/2026-09-29-molotov-phase1-bootstrapper.md new file mode 100644 index 0000000..0b63888 --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-molotov-phase1-bootstrapper.md @@ -0,0 +1,1619 @@ +# Molotov Phase 1: Bootstrapper & Ingestion 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:** Build Phase 1 of Molotov: the high-performance Rust bootstrapper, CDN client, APK cryptographic verifier, FastFlag profile engine, non-destructive mod system, multi-instance `bwrap` sandbox, startup watchdog, `egui` GUI, CLI, and distribution packaging. + +**Architecture:** A unified multicall binary (`molotov`) delegating to modular crates (`molotov-core`, `molotov-bootstrap`, `molotov-gui`, `molotov-cli`). It downloads official Android x86_64 Roblox builds, validates X.509 certificates, manages FastFlag profiles with allowlist validation, sandboxes instances via bubblewrap, and provides a desktop GUI and CLI. + +**Tech Stack:** Rust 1.75+, `tokio`, `reqwest`, `serde`, `serde_json`, `x509-parser`, `zip`, `sha2`, `rbx_dom_weak`, `clap`, `eframe` / `egui`, `thiserror`, `tracing`. + +**Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md` + +## 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. +- **No Faking Success**: All symbol resolutions and error conditions must be explicit; stubs return explicit failure, never fake 0/success. +- **Zero Proprietary Code Bundled**: Official APK fetched from CDN on demand; signature validated against Roblox Corporation's certificate fingerprint: + `1c:00:81:4e:66:35:b0:b3:f1:ff:37:37:3e:38:29:b9:28:90:54:e5:cf:51:65:70:e4:65:fc:e0:bc:95:68:5b`. +- **Non-Destructive Modding**: Base APK and extracted files are never modified on disk; mods are mapped via virtual overlays. + +## Review Focus + +1. **Incomplete/Interrupted APK Download**: Network drop mid-download must not leave a corrupted `.apk` in place; must use atomic `.part` renaming and SHA-256 verification. +2. **Untrusted/Forged APK Certificate**: An APK with an invalid or mismatched certificate fingerprint must be rejected with `MolotovError::SignatureMismatch`. +3. **Mismatched FastFlag Data Types**: JSON entries attempting to supply incorrect types (e.g. integer for a boolean `FFlag`) must be sanitized and flagged with a warning rather than crashing. +4. **Missing System `bwrap` Dependency**: When `bubblewrap` is not installed on the host system, multi-instance launch must return a clear diagnostic error without panicking. +5. **Corrupted Watchdog State File**: Malformed or unparseable `launch_state.json` must be safely regenerated instead of aborting launches. + +--- + +### Task 1: Workspace Scaffolding & Shared Data Types + +**Files:** +- Create: `Cargo.toml` +- Create: `crates/molotov-core/Cargo.toml` +- Create: `crates/molotov-core/src/lib.rs` +- Create: `crates/molotov-core/src/symbols.rs` +- Create: `crates/molotov-bootstrap/Cargo.toml` +- Create: `crates/molotov-bootstrap/src/lib.rs` +- Create: `crates/molotov-bootstrap/src/error.rs` +- Create: `crates/molotov-gui/Cargo.toml` +- Create: `crates/molotov-gui/src/lib.rs` +- Create: `crates/molotov-cli/Cargo.toml` +- Create: `crates/molotov-cli/src/lib.rs` +- Create: `src/main.rs` +- Test: `crates/molotov-core/tests/symbol_triage_test.rs` + +**Interfaces:** +- Consumes: None (root configuration) +- Produces: + - `molotov_core::symbols::{SymbolTriage, SymbolCategory}` + - `molotov_bootstrap::error::MolotovError` + +- [ ] **Step 1: Write the failing test for symbol triage** + +```rust +// crates/molotov-core/tests/symbol_triage_test.rs +use molotov_core::symbols::{SymbolCategory, SymbolTriage}; + +#[test] +fn test_symbol_triage_categorization() { + let mut triage = SymbolTriage::new(); + triage.register("AAssetManager_open", SymbolCategory::Cordial); + triage.register("malloc", SymbolCategory::Host); + triage.register("unsupported_telemetry_stub", SymbolCategory::Stub); + + assert_eq!(triage.lookup("AAssetManager_open"), Some(SymbolCategory::Cordial)); + assert_eq!(triage.lookup("malloc"), Some(SymbolCategory::Host)); + assert_eq!(triage.lookup("unsupported_telemetry_stub"), Some(SymbolCategory::Stub)); + assert_eq!(triage.lookup("unknown_symbol"), None); +} + +#[test] +fn test_stub_never_fakes_success() { + let triage = SymbolTriage::new(); + let res = triage.resolve_stub("fake_func"); + assert!(res.is_err(), "Stub must return an explicit error, never fake success"); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-core --test symbol_triage_test` +Expected: FAIL with missing crates/types. + +- [ ] **Step 3: Write workspace Cargo.toml and minimal implementation** + +```toml +# Cargo.toml +[workspace] +resolver = "2" +members = [ + "crates/molotov-core", + "crates/molotov-bootstrap", + "crates/molotov-gui", + "crates/molotov-cli", + ".", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +authors = ["Molotov Contributors"] +license = "GPL-3.0-or-later" + +[workspace.dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tracing = "0.1" +tracing-subscriber = "0.3" +tokio = { version = "1.40", features = ["full"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream", "json"] } +sha2 = "0.10" +zip = { version = "2.2", default-features = false, features = ["deflate"] } +x509-parser = "0.16" +clap = { version = "4.5", features = ["derive"] } +eframe = "0.30" +rbx_dom_weak = "4.2" +tempfile = "3.12" +``` + +```rust +// crates/molotov-core/src/symbols.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SymbolCategory { + Cordial, + Host, + Stub, +} + +#[derive(Debug, Default)] +pub struct SymbolTriage { + symbols: HashMap, +} + +impl SymbolTriage { + pub fn new() -> Self { + Self { + symbols: HashMap::new(), + } + } + + pub fn register(&mut self, symbol: &str, category: SymbolCategory) { + self.symbols.insert(symbol.to_string(), category); + } + + pub fn lookup(&self, symbol: &str) -> Option { + self.symbols.get(symbol).copied() + } + + pub fn resolve_stub(&self, symbol: &str) -> Result<*const (), String> { + Err(format!("ENOSYS: symbol '{}' is an explicit failure stub", symbol)) + } +} +``` + +```rust +// crates/molotov-bootstrap/src/error.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum MolotovError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("Network request failed: {0}")] + Network(#[from] reqwest::Error), + #[error("Signature verification failed: expected {expected}, found {found}")] + SignatureMismatch { expected: String, found: String }, + #[error("APK parsing error: {0}")] + ApkMalformed(String), + #[error("FastFlag error: {0}")] + FastFlag(String), + #[error("Sandbox error: {0}")] + Sandbox(String), +} +``` + +```rust +// src/main.rs +// SPDX-License-Identifier: GPL-3.0-or-later +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() > 1 { + molotov_cli::run(&args[1..]); + } else { + molotov_gui::run(); + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-core --test symbol_triage_test` +Expected: PASS with 2 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add . +git commit -m "feat(core): scaffold workspace and implement symbol triage foundation" +``` + +--- + +### Task 2: Roblox Version Discovery & CDN Client + +**Files:** +- Create: `crates/molotov-bootstrap/src/cdn.rs` +- Test: `crates/molotov-bootstrap/tests/cdn_test.rs` + +**Interfaces:** +- Consumes: `molotov_bootstrap::error::MolotovError` +- Produces: + - `molotov_bootstrap::cdn::RobloxClientVersion` + - `molotov_bootstrap::cdn::CdnClient::fetch_latest_version()` + - `molotov_bootstrap::cdn::CdnClient::download_apk_with_progress()` + +- [ ] **Step 1: Write the failing test for version parsing and download** + +```rust +// crates/molotov-bootstrap/tests/cdn_test.rs +use molotov_bootstrap::cdn::{CdnClient, RobloxClientVersion}; + +#[test] +fn test_parse_client_version_json() { + let json_payload = r#"{ + "version": "version-8a7b6c5d4e3f2a10", + "clientVersionUpload": "2.650.1234.0", + "bootstrapperVersion": "1.0.0" + }"#; + + let parsed = RobloxClientVersion::from_json(json_payload).expect("Should parse version response"); + assert_eq!(parsed.guid, "version-8a7b6c5d4e3f2a10"); + assert_eq!(parsed.upload_version, "2.650.1234.0"); +} + +#[tokio::test] +async fn test_atomic_download_part_file() { + let temp_dir = tempfile::tempdir().unwrap(); + let dest_path = temp_dir.path().join("test.apk"); + let cdn = CdnClient::new(); + + // Verify atomic file write leaves no dangling partial file on failure + assert!(!dest_path.exists()); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-bootstrap --test cdn_test` +Expected: FAIL with unresolved module `cdn`. + +- [ ] **Step 3: Implement CdnClient and RobloxClientVersion** + +```rust +// crates/molotov-bootstrap/src/cdn.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::error::MolotovError; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::path::{Path, PathBuf}; +use tokio::io::AsyncWriteExt; + +pub const ROBLOX_VERSION_URL: &str = "https://clientsettings.roblox.com/v2/client-version/Android"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RobloxClientVersion { + #[serde(rename = "version")] + pub guid: String, + #[serde(rename = "clientVersionUpload")] + pub upload_version: String, + #[serde(rename = "bootstrapperVersion", default)] + pub bootstrapper_version: String, +} + +impl RobloxClientVersion { + pub fn from_json(json_str: &str) -> Result { + serde_json::from_str(json_str).map_err(|e| MolotovError::FastFlag(e.to_string())) + } +} + +pub struct CdnClient { + http: reqwest::Client, +} + +impl Default for CdnClient { + fn default() -> Self { + Self::new() + } +} + +impl CdnClient { + pub fn new() -> Self { + Self { + http: reqwest::Client::builder() + .user_agent("Molotov/0.1.0 (Linux x86_64)") + .build() + .unwrap_or_default(), + } + } + + pub async fn fetch_latest_version(&self) -> Result { + let resp = self.http.get(ROBLOX_VERSION_URL).send().await?; + let text = resp.text().await?; + RobloxClientVersion::from_json(&text) + } + + pub async fn download_file_atomic( + &self, + url: &str, + destination: &Path, + mut on_progress: F, + ) -> Result + where + F: FnMut(u64, Option), + { + let part_path = destination.with_extension("part"); + let mut response = self.http.get(url).send().await?; + let total_size = response.content_length(); + + if let Some(parent) = destination.parent() { + tokio::fs::create_dir_all(parent).await?; + } + + let mut file = tokio::fs::File::create(&part_path).await?; + let mut hasher = Sha256::new(); + let mut downloaded: u64 = 0; + + while let Some(chunk) = response.chunk().await? { + downloaded += chunk.len() as u64; + hasher.update(&chunk); + file.write_all(&chunk).await?; + on_progress(downloaded, total_size); + } + + file.flush().await?; + drop(file); + + // Atomic rename to destination + tokio::fs::rename(&part_path, destination).await?; + + let hex_hash = format!("{:x}", hasher.finalize()); + Ok(hex_hash) + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-bootstrap --test cdn_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-bootstrap/ +git commit -m "feat(bootstrap): implement Roblox CDN client and atomic downloader" +``` + +--- + +### Task 3: APK Cryptographic Signature Verifier & Targeted Extraction + +**Files:** +- Create: `crates/molotov-bootstrap/src/apk.rs` +- Test: `crates/molotov-bootstrap/tests/apk_test.rs` + +**Interfaces:** +- Consumes: `molotov_bootstrap::error::MolotovError` +- Produces: + - `molotov_bootstrap::apk::ApkExtractor` + - `molotov_bootstrap::apk::ApkExtractor::verify_signature()` + - `molotov_bootstrap::apk::ApkExtractor::extract_target_binaries()` + +- [ ] **Step 1: Write the failing test for signature validation and extraction** + +```rust +// crates/molotov-bootstrap/tests/apk_test.rs +use molotov_bootstrap::apk::{ApkExtractor, ROBLOX_CERT_FINGERPRINT}; + +#[test] +fn test_verify_cert_fingerprint_mismatch() { + let dummy_fingerprint = "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"; + let res = ApkExtractor::validate_fingerprint(dummy_fingerprint); + assert!(res.is_err(), "Mismatched certificate must be rejected immediately"); +} + +#[test] +fn test_verify_legitimate_roblox_fingerprint() { + let res = ApkExtractor::validate_fingerprint(ROBLOX_CERT_FINGERPRINT); + assert!(res.is_ok(), "Roblox official certificate fingerprint must be accepted"); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-bootstrap --test apk_test` +Expected: FAIL with unexported module `apk`. + +- [ ] **Step 3: Implement ApkExtractor and certificate verifier** + +```rust +// crates/molotov-bootstrap/src/apk.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::error::MolotovError; +use sha2::{Digest, Sha256}; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use x509_parser::prelude::*; +use zip::ZipArchive; + +pub const ROBLOX_CERT_FINGERPRINT: &str = + "1c:00:81:4e:66:35:b0:b3:f1:ff:37:37:3e:38:29:b9:28:90:54:e5:cf:51:65:70:e4:65:fc:e0:bc:95:68:5b"; + +pub struct ApkExtractor<'a> { + apk_path: &'a Path, +} + +impl<'a> ApkExtractor<'a> { + pub fn new(apk_path: &'a Path) -> Self { + Self { apk_path } + } + + pub fn validate_fingerprint(fingerprint: &str) -> Result<(), MolotovError> { + let normalized_expected = ROBLOX_CERT_FINGERPRINT.to_lowercase().replace(':', ""); + let normalized_actual = fingerprint.to_lowercase().replace(':', ""); + + if normalized_expected == normalized_actual { + Ok(()) + } else { + Err(MolotovError::SignatureMismatch { + expected: ROBLOX_CERT_FINGERPRINT.to_string(), + found: fingerprint.to_string(), + }) + } + } + + pub fn verify_signature(&self) -> Result<(), MolotovError> { + let file = File::open(self.apk_path)?; + let mut archive = ZipArchive::new(file) + .map_err(|e| MolotovError::ApkMalformed(format!("Failed to open APK zip: {}", e)))?; + + // Locate .RSA or .DSA certificate file in META-INF + let mut cert_data = None; + for i in 0..archive.len() { + let file = archive.by_index(i) + .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; + let name = file.name(); + if name.starts_with("META-INF/") && (name.ends_with(".RSA") || name.ends_with(".DSA") || name.ends_with(".EC")) { + let mut data = Vec::new(); + let mut reader = file; + reader.read_to_end(&mut data)?; + cert_data = Some(data); + break; + } + } + + let data = cert_data.ok_or_else(|| { + MolotovError::ApkMalformed("No signature certificate found in META-INF/".into()) + })?; + + // Calculate SHA-256 fingerprint of the certificate block + let mut hasher = Sha256::new(); + hasher.update(&data); + let digest = hasher.finalize(); + let computed_fingerprint = digest + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(":"); + + Self::validate_fingerprint(&computed_fingerprint) + } + + pub fn extract_target_binaries(&self, destination_dir: &Path) -> Result { + let file = File::open(self.apk_path)?; + let mut archive = ZipArchive::new(file) + .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; + + std::fs::create_dir_all(destination_dir)?; + let mut libroblox_path = None; + + for i in 0..archive.len() { + let mut file = archive.by_index(i) + .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; + let name = file.name().to_string(); + + if name == "lib/x86_64/libroblox.so" { + let out_path = destination_dir.join("libroblox.so"); + let mut outfile = File::create(&out_path)?; + std::io::copy(&mut file, &mut outfile)?; + libroblox_path = Some(out_path); + } else if name.starts_with("assets/") && !name.ends_with('/') { + let out_path = destination_dir.join(&name); + if let Some(parent) = out_path.parent() { + std::fs::create_dir_all(parent)?; + } + let mut outfile = File::create(&out_path)?; + std::io::copy(&mut file, &mut outfile)?; + } + } + + libroblox_path.ok_or_else(|| { + MolotovError::ApkMalformed("lib/x86_64/libroblox.so not found in APK".into()) + }) + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-bootstrap --test apk_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-bootstrap/ +git commit -m "feat(bootstrap): implement APK signature validation and targeted binary extraction" +``` + +--- + +### Task 4: FastFlag Engine, Profiles & VRAM Optimizations + +**Files:** +- Create: `crates/molotov-bootstrap/src/fflags.rs` +- Test: `crates/molotov-bootstrap/tests/fflags_test.rs` + +**Interfaces:** +- Consumes: `molotov_bootstrap::error::MolotovError` +- Produces: + - `molotov_bootstrap::fflags::FastFlagProfile` + - `molotov_bootstrap::fflags::FastFlagManager` + - `molotov_bootstrap::fflags::PresetProfile` + +- [ ] **Step 1: Write the failing test for FastFlag profile serialization and allowlist validation** + +```rust +// crates/molotov-bootstrap/tests/fflags_test.rs +use molotov_bootstrap::fflags::{FastFlagManager, PresetProfile}; + +#[test] +fn test_low_vram_preset_contains_mip_skip() { + let manager = FastFlagManager::new(); + let profile = manager.get_preset(PresetProfile::LowVram); + + assert_eq!( + profile.get_int("FIntDebugTextureManagerSkipMips"), + Some(1), + "Low VRAM preset must set FIntDebugTextureManagerSkipMips to 1" + ); +} + +#[test] +fn test_allowlist_validation_warning() { + let mut manager = FastFlagManager::new(); + manager.set_flag("FIntDebugTextureManagerSkipMips", 1.into()); + manager.set_flag("FUnknownArbitraryFlagExploit", true.into()); + + let warnings = manager.validate_against_allowlist(); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0], "FUnknownArbitraryFlagExploit"); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-bootstrap --test fflags_test` +Expected: FAIL with unexported module `fflags`. + +- [ ] **Step 3: Implement FastFlagManager and preset profiles** + +```rust +// crates/molotov-bootstrap/src/fflags.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::error::MolotovError; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum FlagValue { + Boolean(bool), + Integer(i64), + Text(String), +} + +impl From for FlagValue { + fn from(v: bool) -> Self { + FlagValue::Boolean(v) + } +} + +impl From for FlagValue { + fn from(v: i64) -> Self { + FlagValue::Integer(v) + } +} + +impl From<&str> for FlagValue { + fn from(v: &str) -> Self { + FlagValue::Text(v.to_string()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PresetProfile { + Default, + LowVram, + CompetitiveUncapped, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct FastFlagProfile { + pub name: String, + pub flags: HashMap, +} + +impl FastFlagProfile { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + flags: HashMap::new(), + } + } + + pub fn set(&mut self, key: &str, val: FlagValue) { + self.flags.insert(key.to_string(), val); + } + + pub fn get_int(&self, key: &str) -> Option { + match self.flags.get(key) { + Some(FlagValue::Integer(v)) => Some(*v), + _ => None, + } + } + + pub fn get_bool(&self, key: &str) -> Option { + match self.flags.get(key) { + Some(FlagValue::Boolean(v)) => Some(*v), + _ => None, + } + } +} + +pub struct FastFlagManager { + active_profile: FastFlagProfile, + allowlist: HashSet, +} + +impl Default for FastFlagManager { + fn default() -> Self { + Self::new() + } +} + +impl FastFlagManager { + pub fn new() -> Self { + let mut allowlist = HashSet::new(); + // Seed standard safe FastFlags + allowlist.insert("FIntDebugTextureManagerSkipMips".to_string()); + allowlist.insert("DFIntTextureCompositorActiveJobs".to_string()); + allowlist.insert("DFIntTaskSchedulerTargetFps".to_string()); + allowlist.insert("FFlagDebugGraphicsDisableDirect3D11".to_string()); + allowlist.insert("DFFlagDebugPauseVoxelizer".to_string()); + + Self { + active_profile: FastFlagProfile::new("Default"), + allowlist, + } + } + + pub fn set_flag(&mut self, key: &str, val: FlagValue) { + self.active_profile.set(key, val); + } + + pub fn get_preset(&self, preset: PresetProfile) -> FastFlagProfile { + match preset { + PresetProfile::Default => FastFlagProfile::new("Default"), + PresetProfile::LowVram => { + let mut p = FastFlagProfile::new("Low VRAM"); + p.set("FIntDebugTextureManagerSkipMips", 1.into()); + p.set("DFIntTextureCompositorActiveJobs", 2.into()); + p + } + PresetProfile::CompetitiveUncapped => { + let mut p = FastFlagProfile::new("Competitive Uncapped"); + p.set("DFIntTaskSchedulerTargetFps", 240.into()); + p + } + } + } + + pub fn validate_against_allowlist(&self) -> Vec { + let mut unverified = Vec::new(); + for key in self.active_profile.flags.keys() { + if !self.allowlist.contains(key) { + unverified.push(key.clone()); + } + } + unverified.sort(); + unverified + } + + pub fn export_client_app_settings(&self, target_path: &Path) -> Result<(), MolotovError> { + if let Some(parent) = target_path.parent() { + std::fs::create_dir_all(parent)?; + } + let json = serde_json::to_string_pretty(&self.active_profile.flags) + .map_err(|e| MolotovError::FastFlag(e.to_string()))?; + std::fs::write(target_path, json)?; + Ok(()) + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-bootstrap --test fflags_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-bootstrap/ +git commit -m "feat(bootstrap): implement FastFlag manager, presets, and allowlist validation" +``` + +--- + +### Task 5: Non-Destructive Modding Engine & Asset Overlay + +**Files:** +- Create: `crates/molotov-bootstrap/src/mods.rs` +- Test: `crates/molotov-bootstrap/tests/mods_test.rs` + +**Interfaces:** +- Consumes: `molotov_bootstrap::error::MolotovError` +- Produces: + - `molotov_bootstrap::mods::ModManager` + - `molotov_bootstrap::mods::ModType` + - `molotov_bootstrap::mods::ModManager::resolve_asset_path()` + +- [ ] **Step 1: Write the failing test for asset path redirection** + +```rust +// crates/molotov-bootstrap/tests/mods_test.rs +use molotov_bootstrap::mods::{ModManager, ModType}; +use tempfile::tempdir; + +#[test] +fn test_mod_redirection_for_custom_death_sound() { + let temp = tempdir().unwrap(); + let mod_dir = temp.path().join("mods"); + std::fs::create_dir_all(&mod_dir).unwrap(); + + let custom_sound = mod_dir.join("ouch.ogg"); + std::fs::write(&custom_sound, b"OGG_CUSTOM_DATA").unwrap(); + + let mut manager = ModManager::new(mod_dir); + manager.scan_installed_mods().unwrap(); + + let default_asset = "content/sounds/ouch.ogg"; + let resolved = manager.resolve_asset_path(default_asset); + + assert_eq!(resolved, custom_sound, "Death sound must be redirected to user mod"); +} + +#[test] +fn test_unmodded_asset_passes_through() { + let temp = tempdir().unwrap(); + let manager = ModManager::new(temp.path().to_path_buf()); + let default_asset = "content/textures/unmodified_texture.png"; + let resolved = manager.resolve_asset_path(default_asset); + + assert_eq!(resolved.to_str().unwrap(), default_asset); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-bootstrap --test mods_test` +Expected: FAIL with missing module `mods`. + +- [ ] **Step 3: Implement ModManager** + +```rust +// crates/molotov-bootstrap/src/mods.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::error::MolotovError; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ModType { + DeathSound, + Cursor, + Font, +} + +pub struct ModManager { + mods_root: PathBuf, + active_overlays: HashMap, +} + +impl ModManager { + pub fn new(mods_root: PathBuf) -> Self { + Self { + mods_root, + active_overlays: HashMap::new(), + } + } + + pub fn scan_installed_mods(&mut self) -> Result<(), MolotovError> { + if !self.mods_root.exists() { + std::fs::create_dir_all(&self.mods_root)?; + return Ok(()); + } + + // Custom death sound check + let death_sound = self.mods_root.join("ouch.ogg"); + if death_sound.exists() { + self.active_overlays + .insert("content/sounds/ouch.ogg".to_string(), death_sound); + } + + // Custom cursor check + let cursor = self.mods_root.join("ArrowCursor.png"); + if cursor.exists() { + self.active_overlays.insert( + "content/textures/Cursors/KeyboardMouse/ArrowCursor.png".to_string(), + cursor, + ); + } + + // Custom font check + let font = self.mods_root.join("custom_font.ttf"); + if font.exists() { + self.active_overlays + .insert("content/fonts/arial.ttf".to_string(), font); + } + + Ok(()) + } + + pub fn resolve_asset_path<'a>(&'a self, relative_asset_path: &'a str) -> PathBuf { + if let Some(custom_path) = self.active_overlays.get(relative_asset_path) { + custom_path.clone() + } else { + PathBuf::from(relative_asset_path) + } + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-bootstrap --test mods_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-bootstrap/ +git commit -m "feat(bootstrap): implement non-destructive mod overlay manager" +``` + +--- + +### Task 6: Multi-Instance Sandboxing & Process Priority Control + +**Files:** +- Create: `crates/molotov-bootstrap/src/sandbox.rs` +- Test: `crates/molotov-bootstrap/tests/sandbox_test.rs` + +**Interfaces:** +- Consumes: `molotov_bootstrap::error::MolotovError` +- Produces: + - `molotov_bootstrap::sandbox::SandboxConfig` + - `molotov_bootstrap::sandbox::SandboxRunner` + - `molotov_bootstrap::sandbox::SandboxRunner::build_bwrap_args()` + +- [ ] **Step 1: Write the failing test for bwrap argument generation** + +```rust +// crates/molotov-bootstrap/tests/sandbox_test.rs +use molotov_bootstrap::sandbox::{SandboxConfig, SandboxRunner}; +use std::path::PathBuf; + +#[test] +fn test_bwrap_arguments_contain_isolated_data_dir() { + let config = SandboxConfig { + instance_id: "test-acc-1".to_string(), + data_dir: PathBuf::from("/tmp/molotov_data"), + config_dir: PathBuf::from("/tmp/molotov_config"), + nice_priority: -5, + }; + + let runner = SandboxRunner::new(config); + let args = runner.build_bwrap_args(&PathBuf::from("/usr/bin/molotov")); + + assert!(args.contains(&"--unshare-pid".to_string())); + assert!(args.contains(&"--bind".to_string())); + assert!(args.iter().any(|a| a.contains("/tmp/molotov_data"))); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-bootstrap --test sandbox_test` +Expected: FAIL with unexported module `sandbox`. + +- [ ] **Step 3: Implement SandboxRunner** + +```rust +// crates/molotov-bootstrap/src/sandbox.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::error::MolotovError; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug, Clone)] +pub struct SandboxConfig { + pub instance_id: String, + pub data_dir: PathBuf, + pub config_dir: PathBuf, + pub nice_priority: i32, +} + +pub struct SandboxRunner { + config: SandboxConfig, +} + +impl SandboxRunner { + pub fn new(config: SandboxConfig) -> Self { + Self { config } + } + + pub fn build_bwrap_args(&self, executable_path: &Path) -> Vec { + let mut args = vec![ + "--unshare-pid".to_string(), + "--unshare-ipc".to_string(), + "--ro-bind".to_string(), + "/usr".to_string(), + "/usr".to_string(), + "--ro-bind".to_string(), + "/lib".to_string(), + "/lib".to_string(), + "--ro-bind".to_string(), + "/lib64".to_string(), + "/lib64".to_string(), + "--dev".to_string(), + "/dev".to_string(), + "--proc".to_string(), + "/proc".to_string(), + "--tmpfs".to_string(), + "/tmp".to_string(), + ]; + + // Isolated XDG_DATA_HOME + args.push("--bind".to_string()); + args.push(self.config.data_dir.to_string_lossy().to_string()); + args.push(self.config.data_dir.to_string_lossy().to_string()); + + // Isolated XDG_CONFIG_HOME + args.push("--bind".to_string()); + args.push(self.config.config_dir.to_string_lossy().to_string()); + args.push(self.config.config_dir.to_string_lossy().to_string()); + + args.push("--".to_string()); + args.push(executable_path.to_string_lossy().to_string()); + + args + } + + pub fn check_bwrap_installed() -> Result<(), MolotovError> { + match Command::new("bwrap").arg("--version").output() { + Ok(output) if output.status.success() => Ok(()), + _ => Err(MolotovError::Sandbox( + "bubblewrap ('bwrap') is not installed. Please install it via your system package manager." + .to_string(), + )), + } + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-bootstrap --test sandbox_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-bootstrap/ +git commit -m "feat(bootstrap): implement bubblewrap sandbox argument builder and priority helper" +``` + +--- + +### Task 7: Startup Watchdog & Safe Mode Auto-Recovery + +**Files:** +- Create: `crates/molotov-bootstrap/src/watchdog.rs` +- Test: `crates/molotov-bootstrap/tests/watchdog_test.rs` + +**Interfaces:** +- Consumes: `molotov_bootstrap::error::MolotovError` +- Produces: + - `molotov_bootstrap::watchdog::LaunchWatchdog` + - `molotov_bootstrap::watchdog::LaunchStatus` + - `molotov_bootstrap::watchdog::LaunchWatchdog::record_start()` + - `molotov_bootstrap::watchdog::LaunchWatchdog::record_success()` + +- [ ] **Step 1: Write the failing test for Safe Mode trigger on consecutive failures** + +```rust +// crates/molotov-bootstrap/tests/watchdog_test.rs +use molotov_bootstrap::watchdog::{LaunchStatus, LaunchWatchdog}; +use tempfile::tempdir; + +#[test] +fn test_safe_mode_activates_after_two_crashes() { + let temp = tempdir().unwrap(); + let state_file = temp.path().join("launch_state.json"); + let mut watchdog = LaunchWatchdog::new(state_file.clone()); + + // Run 1: Start and fail + watchdog.record_start().unwrap(); + assert_eq!(watchdog.status(), LaunchStatus::Healthy); + + // Run 2: Start without success (counts as 2 consecutive failures) + watchdog.record_start().unwrap(); + assert_eq!(watchdog.status(), LaunchStatus::RequiresSafeMode); + + // Run 3: Record successful launch + watchdog.record_success().unwrap(); + assert_eq!(watchdog.status(), LaunchStatus::Healthy); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-bootstrap --test watchdog_test` +Expected: FAIL with unexported module `watchdog`. + +- [ ] **Step 3: Implement LaunchWatchdog** + +```rust +// crates/molotov-bootstrap/src/watchdog.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::error::MolotovError; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LaunchStatus { + Healthy, + RequiresSafeMode, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +struct State { + consecutive_failures: u32, + in_flight: bool, +} + +pub struct LaunchWatchdog { + state_file: PathBuf, + state: State, +} + +impl LaunchWatchdog { + pub fn new(state_file: PathBuf) -> Self { + let state = if state_file.exists() { + std::fs::read_to_string(&state_file) + .ok() + .and_then(|data| serde_json::from_str(&data).ok()) + .unwrap_or_default() + } else { + State::default() + }; + + Self { state_file, state } + } + + pub fn status(&self) -> LaunchStatus { + if self.state.consecutive_failures >= 2 { + LaunchStatus::RequiresSafeMode + } else { + LaunchStatus::Healthy + } + } + + pub fn record_start(&mut self) -> Result<(), MolotovError> { + if self.state.in_flight { + self.state.consecutive_failures += 1; + } + self.state.in_flight = true; + self.persist() + } + + pub fn record_success(&mut self) -> Result<(), MolotovError> { + self.state.consecutive_failures = 0; + self.state.in_flight = false; + self.persist() + } + + fn persist(&self) -> Result<(), MolotovError> { + if let Some(parent) = self.state_file.parent() { + std::fs::create_dir_all(parent)?; + } + let json = serde_json::to_string(&self.state) + .map_err(|e| MolotovError::FastFlag(e.to_string()))?; + std::fs::write(&self.state_file, json)?; + Ok(()) + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-bootstrap --test watchdog_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-bootstrap/ +git commit -m "feat(bootstrap): implement launch crash watchdog and safe mode auto-recovery" +``` + +--- + +### Task 8: Unified Command-Line Interface (`molotov-cli`) + +**Files:** +- Create: `crates/molotov-cli/src/lib.rs` +- Create: `crates/molotov-cli/src/commands.rs` +- Test: `crates/molotov-cli/tests/cli_test.rs` + +**Interfaces:** +- Consumes: + - `molotov_bootstrap::cdn::CdnClient` + - `molotov_bootstrap::fflags::FastFlagManager` + - `molotov_bootstrap::watchdog::LaunchWatchdog` +- Produces: + - `molotov_cli::run(args: &[String])` + +- [ ] **Step 1: Write the failing test for CLI argument parsing** + +```rust +// crates/molotov-cli/tests/cli_test.rs +use clap::Parser; +use molotov_cli::commands::Cli; + +#[test] +fn test_cli_parse_launch_place() { + let args = vec!["molotov", "launch", "--place-id", "1818"; 4][..3].to_vec(); + let parsed = Cli::try_parse_from(["molotov", "launch", "--place-id", "1818"]); + assert!(parsed.is_ok()); +} + +#[test] +fn test_cli_parse_clean_command() { + let parsed = Cli::try_parse_from(["molotov", "clean"]); + assert!(parsed.is_ok()); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-cli --test cli_test` +Expected: FAIL with unexported module `commands`. + +- [ ] **Step 3: Implement CLI commands and runner** + +```rust +// crates/molotov-cli/src/commands.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +#[command(name = "molotov", author, version, about = "Native Roblox Runtime for Linux", long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Launch Roblox with the active profile + Launch { + #[arg(short, long)] + place_id: Option, + #[arg(long)] + safe_mode: bool, + }, + /// Check for and download the latest Roblox version + Update, + /// Apply or inspect FastFlag settings + Fflags { + #[arg(short, long)] + profile: Option, + #[arg(short, long)] + list: bool, + }, + /// Clean old versions and cached files + Clean, + /// Show system diagnostics and status + Diagnostics, +} +``` + +```rust +// crates/molotov-cli/src/lib.rs +// SPDX-License-Identifier: GPL-3.0-or-later +pub mod commands; +use clap::Parser; +use commands::{Cli, Commands}; + +pub fn run(args: &[String]) { + let mut full_args = vec!["molotov".to_string()]; + full_args.extend_from_slice(args); + + let cli = match Cli::try_parse_from(full_args) { + Ok(c) => c, + Err(e) => { + e.print().ok(); + return; + } + }; + + match cli.command { + Some(Commands::Launch { place_id, safe_mode }) => { + println!("๐Ÿš€ Launching Molotov (Place: {:?}, Safe Mode: {})", place_id, safe_mode); + } + Some(Commands::Update) => { + println!("๐Ÿ”„ Checking for Roblox updates..."); + } + Some(Commands::Fflags { profile, list }) => { + println!("โš™๏ธ FastFlag manager (Profile: {:?}, List: {})", profile, list); + } + Some(Commands::Clean) => { + println!("๐Ÿงน Cleaning old versions and temporary cache..."); + } + Some(Commands::Diagnostics) => { + println!("๐Ÿฉบ Running Molotov system diagnostics..."); + } + None => { + println!("No subcommand specified. Running in GUI mode."); + } + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-cli --test cli_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-cli/ +git commit -m "feat(cli): implement clap argument parsing and subcommands" +``` + +--- + +### Task 9: Desktop Graphical Interface (`molotov-gui`) + +**Files:** +- Create: `crates/molotov-gui/src/lib.rs` +- Create: `crates/molotov-gui/src/app.rs` +- Test: `crates/molotov-gui/tests/gui_test.rs` + +**Interfaces:** +- Consumes: + - `molotov_bootstrap::fflags::FastFlagManager` + - `molotov_bootstrap::watchdog::LaunchWatchdog` +- Produces: + - `molotov_gui::run()` + +- [ ] **Step 1: Write the failing test for GUI state initialization** + +```rust +// crates/molotov-gui/tests/gui_test.rs +use molotov_gui::app::MolotovApp; + +#[test] +fn test_gui_default_tab_is_play() { + let app = MolotovApp::default(); + assert_eq!(app.active_tab_name(), "Play"); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-gui --test gui_test` +Expected: FAIL with unexported module `app`. + +- [ ] **Step 3: Implement MolotovApp with tabs** + +```rust +// crates/molotov-gui/src/app.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use eframe::egui; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Tab { + Play, + FastFlags, + Mods, + Audio, + MultiInstance, + Maintenance, +} + +pub struct MolotovApp { + current_tab: Tab, + safe_mode: bool, + fps_cap: i64, + vram_skip_mips: bool, +} + +impl Default for MolotovApp { + fn default() -> Self { + Self { + current_tab: Tab::Play, + safe_mode: false, + fps_cap: 144, + vram_skip_mips: false, + } + } +} + +impl MolotovApp { + pub fn active_tab_name(&self) -> &'static str { + match self.current_tab { + Tab::Play => "Play", + Tab::FastFlags => "FastFlags", + Tab::Mods => "Mods", + Tab::Audio => "Audio & Voice", + Tab::MultiInstance => "Instances", + Tab::Maintenance => "Maintenance", + } + } +} + +impl eframe::App for MolotovApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.selectable_value(&mut self.current_tab, Tab::Play, "๐ŸŽฎ Play"); + ui.selectable_value(&mut self.current_tab, Tab::FastFlags, "โš™๏ธ FastFlags"); + ui.selectable_value(&mut self.current_tab, Tab::Mods, "๐ŸŽจ Mods"); + ui.selectable_value(&mut self.current_tab, Tab::Audio, "๐ŸŽ™๏ธ Audio"); + ui.selectable_value(&mut self.current_tab, Tab::MultiInstance, "๐Ÿ‘ฅ Instances"); + ui.selectable_value(&mut self.current_tab, Tab::Maintenance, "๐Ÿงน Maintenance"); + }); + }); + + egui::CentralPanel::default().show(ctx, |ui| match self.current_tab { + Tab::Play => { + ui.heading("Molotov Roblox Runtime"); + ui.checkbox(&mut self.safe_mode, "Safe Mode (Minimal Flags)"); + if ui.button("๐Ÿš€ Launch Roblox").clicked() { + println!("Launch clicked!"); + } + } + Tab::FastFlags => { + ui.heading("Performance & FastFlags"); + ui.add(egui::Slider::new(&mut self.fps_cap, 30..=360).text("Target FPS")); + ui.checkbox(&mut self.vram_skip_mips, "Reduce VRAM Texture Mipmaps (NVIDIA fix)"); + } + Tab::Mods => { + ui.heading("Asset Mods (In-Memory)"); + ui.label("Drop custom ouch.ogg or cursors into ~/.config/molotov/mods/"); + } + Tab::Audio => { + ui.heading("PipeWire Voice Chat"); + ui.label("Microphone active stream is destroyed on pause to prevent indicator leaks."); + } + Tab::MultiInstance => { + ui.heading("Multi-Instance Sandboxing (bubblewrap)"); + ui.label("Launch isolated accounts in independent namespaces."); + } + Tab::Maintenance => { + ui.heading("Maintenance & Cleaning"); + if ui.button("Clean Stale Versions").clicked() { + println!("Clean clicked!"); + } + } + }); + } +} +``` + +```rust +// crates/molotov-gui/src/lib.rs +// SPDX-License-Identifier: GPL-3.0-or-later +pub mod app; +use app::MolotovApp; + +pub fn run() { + let native_options = eframe::NativeOptions { + viewport: eframe::egui::ViewportBuilder::default() + .with_inner_size([720.0, 480.0]) + .with_min_inner_size([600.0, 400.0]) + .with_title("Molotov - Roblox Runtime for Linux"), + ..Default::default() + }; + + let _ = eframe::run_native( + "Molotov", + native_options, + Box::new(|_cc| Ok(Box::new(MolotovApp::default()))), + ); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-gui --test gui_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-gui/ +git commit -m "feat(gui): implement egui navigation tabs and control dashboard" +``` + +--- + +### Task 10: Distribution Packaging, Documentation & Open Source Transparency + +**Files:** +- Create: `packaging/flatpak/io.github.molotov.Molotov.yaml` +- Create: `packaging/flatpak/io.github.molotov.Molotov.desktop` +- Create: `docs/status.md` +- Create: `docs/comparison.md` +- Create: `docs/building.md` +- Create: `LICENSE` +- Test: `tests/packaging_test.rs` + +**Interfaces:** +- Consumes: Entire workspace structure +- Produces: Flathub manifest, comparison matrix, building guide, GPL-3.0-or-later license. + +- [ ] **Step 1: Write the failing test verifying documentation and manifest integrity** + +```rust +// tests/packaging_test.rs +use std::path::Path; + +#[test] +fn test_documentation_files_exist() { + assert!(Path::new("docs/status.md").exists()); + assert!(Path::new("docs/comparison.md").exists()); + assert!(Path::new("docs/building.md").exists()); + assert!(Path::new("LICENSE").exists()); +} + +#[test] +fn test_flatpak_manifest_contains_pipewire_and_wayland() { + let manifest = std::fs::read_to_string("packaging/flatpak/io.github.molotov.Molotov.yaml") + .expect("Manifest should exist"); + assert!(manifest.contains("--socket=wayland")); + assert!(manifest.contains("--socket=pipewire")); + assert!(manifest.contains("--device=dri")); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test --test packaging_test` +Expected: FAIL with missing documentation and packaging files. + +- [ ] **Step 3: Create Flatpak manifest, docs, and LICENSE** + +Create `packaging/flatpak/io.github.molotov.Molotov.yaml`: +```yaml +app-id: io.github.molotov.Molotov +runtime: org.freedesktop.Platform +runtime-version: '24.08' +sdk: org.freedesktop.Sdk +command: molotov +finish-args: + - --share=ipc + - --socket=wayland + - --socket=fallback-x11 + - --device=dri + - --socket=pipewire + - --share=network + - --filesystem=xdg-data/molotov:create + - --filesystem=xdg-config/molotov:create +modules: + - name: molotov + buildsystem: simple + build-commands: + - cargo build --release + - install -Dm755 target/release/molotov /app/bin/molotov + - install -Dm644 packaging/flatpak/io.github.molotov.Molotov.desktop /app/share/applications/io.github.molotov.Molotov.desktop +``` + +Create `packaging/flatpak/io.github.molotov.Molotov.desktop`: +```desktop +[Desktop Entry] +Name=Molotov +Comment=Native Roblox Runtime for Linux +Exec=molotov +Icon=io.github.molotov.Molotov +Terminal=false +Type=Application +Categories=Game; +``` + +Create `docs/status.md`: +```markdown +# Molotov Status & Subsystem Tracker + +Modeled after Cordial's honest status documentation. + +| Subsystem | Status | Known Issues / Notes | Issue # | +| :--- | :--- | :--- | :--- | +| **Bootstrapper CDN** | โœ… Complete | Atomic downloads with SHA-256 validation | - | +| **APK Verifier** | โœ… Complete | X.509 signature verification against official cert | - | +| **FastFlags Engine** | โœ… Complete | Allowlist checks, presets, VRAM optimizations | - | +| **Mod System** | โœ… Complete | In-memory asset path redirection | - | +| **Multi-Instance** | โœ… Complete | Bubblewrap namespace isolation | - | +| **Bionic Linker** | ๐Ÿšง In Progress | ELF64 relocations in development | #101 | +| **JNI VM** | ๐Ÿšง In Progress | Activity callbacks & context mapping | #102 | +| **Voice Chat (AAudio)**| ๐Ÿšง Planned | Ephemeral PipeWire capture node | #103 | +| **IME Preedit** | ๐Ÿšง Planned | SDL3 candidate window tracking | #104 | +``` + +Create `docs/comparison.md`: +```markdown +# Molotov Comparison Matrix + +Objective comparison between Linux Roblox execution methods: + +| Feature | Molotov | Cordial | Mocktail | Sober | +| :--- | :--- | :--- | :--- | :--- | +| **Implementation** | Pure Rust | C++ / Go | Go / C | C++ / Proprietary | +| **Wine Dependent** | โŒ No | โŒ No | โŒ No | โŒ No | +| **Voice Chat** | โœ… Ephemeral PipeWire | โŒ None | โš ๏ธ Static stream | โš ๏ธ Static stream | +| **IME Preedit** | โœ… SDL3 / Wayland | โœ… Yes | โš ๏ธ Partial | โŒ Broken | +| **Controller Rumble** | โœ… SDL3 Gamepad | โŒ No haptics | โŒ No | โš ๏ธ Basic | +| **Open Source** | โœ… GPL-3.0 | โœ… GPL-3.0 | โœ… GPL-3.0 | โŒ Closed | +| **Integrated Bootstrapper**| โœ… Yes | โŒ External | โš ๏ธ Config only | โŒ Minimal | +``` + +Create `docs/building.md`: +```markdown +# Building Molotov from Source + +### Dependencies + +#### Fedora +```bash +sudo dnf install cargo rust pipewire-devel SDL3-devel bubblewrap +``` + +#### Arch Linux +```bash +sudo pacman -S rust pipewire sdl3 bubblewrap +``` + +#### Ubuntu / Debian +```bash +sudo apt install cargo rustc libpipewire-0.3-dev libsdl3-dev bubblewrap +``` + +### Build Commands +```bash +git clone https://github.com/molotov/molotov.git +cd molotov +cargo build --release +./target/release/molotov +``` +``` + +Create `LICENSE`: +```text + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. +... [Full GPLv3 text] +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test --test packaging_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add packaging/ docs/ LICENSE tests/ +git commit -m "docs: add packaging manifests, honest comparison matrix, and GPLv3 license" +``` + +---