fix(integration): resolve active version detection, prevent watchdog safe mode lock, and add async GUI background channels
This commit is contained in:
parent
66deae64d6
commit
c1d67e3160
8 changed files with 264 additions and 70 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
use crate::error::MolotovError;
|
use crate::error::MolotovError;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct CleanReport {
|
pub struct CleanReport {
|
||||||
|
|
@ -22,33 +22,79 @@ impl Cleaner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clean_stale_data(&self, active_version_guid: &str) -> Result<CleanReport, MolotovError> {
|
pub fn resolve_active_version(base_dir: &Path) -> Option<String> {
|
||||||
let mut report = CleanReport::default();
|
let active_file = base_dir.join("active_version.txt");
|
||||||
|
if let Ok(text) = std::fs::read_to_string(active_file) {
|
||||||
|
let trimmed = text.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
return Some(trimmed.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Scan versions directory
|
// Fallback: pick the latest modified version directory in versions/
|
||||||
if self.versions_root.exists() {
|
let versions_dir = base_dir.join("versions");
|
||||||
for entry in std::fs::read_dir(&self.versions_root)? {
|
if let Ok(entries) = std::fs::read_dir(versions_dir) {
|
||||||
let entry = entry?;
|
let mut latest_name = None;
|
||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let mut latest_time = std::time::SystemTime::UNIX_EPOCH;
|
||||||
if entry.file_type()?.is_dir() && file_name != active_version_guid {
|
|
||||||
let dir_size = calculate_dir_size(&entry.path());
|
for entry in entries.flatten() {
|
||||||
std::fs::remove_dir_all(entry.path())?;
|
if let Ok(meta) = entry.metadata() {
|
||||||
report.purged_versions.push(file_name);
|
if meta.is_dir() {
|
||||||
report.reclaimed_bytes += dir_size;
|
if let Ok(mtime) = meta.modified() {
|
||||||
|
if mtime > latest_time {
|
||||||
|
latest_time = mtime;
|
||||||
|
latest_name = Some(entry.file_name().to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return latest_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clean_stale_data(&self, active_version_guid: &str) -> Result<CleanReport, MolotovError> {
|
||||||
|
let mut report = CleanReport::default();
|
||||||
|
let active_guid = active_version_guid.trim();
|
||||||
|
|
||||||
|
// 1. Scan versions directory only if active version is specified to prevent data loss
|
||||||
|
if !active_guid.is_empty() && self.versions_root.exists() {
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&self.versions_root) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if let Ok(ft) = entry.file_type() {
|
||||||
|
if ft.is_dir() && file_name != active_guid {
|
||||||
|
let dir_size = calculate_dir_size(&entry.path());
|
||||||
|
if std::fs::remove_dir_all(entry.path()).is_ok() {
|
||||||
|
report.purged_versions.push(file_name);
|
||||||
|
report.reclaimed_bytes += dir_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if active_guid.is_empty() {
|
||||||
|
tracing::warn!("Cleaner: active version unspecified; skipping version directory purge for safety");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Scan downloads directory for leftover .part files
|
// 2. Scan downloads directory for leftover .part files
|
||||||
if self.downloads_root.exists() {
|
if self.downloads_root.exists() {
|
||||||
for entry in std::fs::read_dir(&self.downloads_root)? {
|
if let Ok(entries) = std::fs::read_dir(&self.downloads_root) {
|
||||||
let entry = entry?;
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().and_then(|e| e.to_str()) == Some("part") {
|
if path.extension().and_then(|e| e.to_str()) == Some("part") {
|
||||||
let file_size = entry.metadata()?.len();
|
if let Ok(meta) = entry.metadata() {
|
||||||
std::fs::remove_file(path)?;
|
if meta.is_file() {
|
||||||
report.purged_parts_count += 1;
|
let file_size = meta.len();
|
||||||
report.reclaimed_bytes += file_size;
|
if std::fs::remove_file(path).is_ok() {
|
||||||
|
report.purged_parts_count += 1;
|
||||||
|
report.reclaimed_bytes += file_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
use crate::error::MolotovError;
|
use crate::error::MolotovError;
|
||||||
use crate::fflags::{FastFlagManager, PresetProfile};
|
use crate::fflags::{FastFlagManager, FlagValue, PresetProfile};
|
||||||
use crate::region::ServerRegion;
|
use crate::region::ServerRegion;
|
||||||
use crate::sandbox::SandboxConfig;
|
use crate::sandbox::SandboxConfig;
|
||||||
use crate::watchdog::{LaunchStatus, LaunchWatchdog};
|
use crate::watchdog::{LaunchStatus, LaunchWatchdog};
|
||||||
|
|
@ -12,6 +12,8 @@ pub struct LaunchOptions {
|
||||||
pub safe_mode: bool,
|
pub safe_mode: bool,
|
||||||
pub region: ServerRegion,
|
pub region: ServerRegion,
|
||||||
pub instance_id: String,
|
pub instance_id: String,
|
||||||
|
pub fps_cap: Option<i64>,
|
||||||
|
pub vram_skip_mips: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LaunchPlan {
|
pub struct LaunchPlan {
|
||||||
|
|
@ -32,11 +34,10 @@ impl Launcher {
|
||||||
|
|
||||||
pub fn prepare_launch(&self, options: LaunchOptions) -> Result<LaunchPlan, MolotovError> {
|
pub fn prepare_launch(&self, options: LaunchOptions) -> Result<LaunchPlan, MolotovError> {
|
||||||
let state_file = self.base_dir.join("launch_state.json");
|
let state_file = self.base_dir.join("launch_state.json");
|
||||||
let mut watchdog = LaunchWatchdog::new(state_file);
|
let watchdog = LaunchWatchdog::new(state_file);
|
||||||
|
|
||||||
// Check if Safe Mode is required
|
// Check if Safe Mode is required without modifying state
|
||||||
let activate_safe_mode = options.safe_mode || watchdog.status() == LaunchStatus::RequiresSafeMode;
|
let activate_safe_mode = options.safe_mode || watchdog.status() == LaunchStatus::RequiresSafeMode;
|
||||||
watchdog.record_start()?;
|
|
||||||
|
|
||||||
let mut fflag_mgr = FastFlagManager::new();
|
let mut fflag_mgr = FastFlagManager::new();
|
||||||
if activate_safe_mode {
|
if activate_safe_mode {
|
||||||
|
|
@ -45,6 +46,14 @@ impl Launcher {
|
||||||
} else {
|
} else {
|
||||||
fflag_mgr.apply_preset(PresetProfile::LowVram);
|
fflag_mgr.apply_preset(PresetProfile::LowVram);
|
||||||
options.region.apply_to_fflags(&mut fflag_mgr);
|
options.region.apply_to_fflags(&mut fflag_mgr);
|
||||||
|
|
||||||
|
// Apply custom user settings
|
||||||
|
if let Some(fps) = options.fps_cap {
|
||||||
|
fflag_mgr.set_flag("DFIntTaskSchedulerTargetFps", FlagValue::Integer(fps));
|
||||||
|
}
|
||||||
|
if options.vram_skip_mips {
|
||||||
|
fflag_mgr.set_flag("FIntDebugTextureManagerSkipMips", FlagValue::Integer(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fflags_path = self.base_dir.join("ClientAppSettings.json");
|
let fflags_path = self.base_dir.join("ClientAppSettings.json");
|
||||||
|
|
@ -64,4 +73,16 @@ impl Launcher {
|
||||||
is_safe_mode: activate_safe_mode,
|
is_safe_mode: activate_safe_mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_launch_start(&self) -> Result<(), MolotovError> {
|
||||||
|
let state_file = self.base_dir.join("launch_state.json");
|
||||||
|
let mut watchdog = LaunchWatchdog::new(state_file);
|
||||||
|
watchdog.record_start()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_launch_success(&self) -> Result<(), MolotovError> {
|
||||||
|
let state_file = self.base_dir.join("launch_state.json");
|
||||||
|
let mut watchdog = LaunchWatchdog::new(state_file);
|
||||||
|
watchdog.record_success()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
crates/molotov-bootstrap/tests/final_mile_fixes_test.rs
Normal file
48
crates/molotov-bootstrap/tests/final_mile_fixes_test.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
use molotov_bootstrap::cleaner::Cleaner;
|
||||||
|
use molotov_bootstrap::launcher::{LaunchOptions, Launcher};
|
||||||
|
use molotov_bootstrap::region::ServerRegion;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cleaner_fail_safe_when_active_version_unspecified() {
|
||||||
|
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();
|
||||||
|
|
||||||
|
let ver_dir = versions_dir.join("version-real-12345");
|
||||||
|
std::fs::create_dir_all(&ver_dir).unwrap();
|
||||||
|
|
||||||
|
let cleaner = Cleaner::new(versions_dir, downloads_dir);
|
||||||
|
// If active_version_guid is empty or unknown, cleaner must NOT delete installed versions
|
||||||
|
let report = cleaner.clean_stale_data("").unwrap();
|
||||||
|
assert_eq!(report.purged_versions.len(), 0);
|
||||||
|
assert!(ver_dir.exists(), "Version directory must not be deleted when active version is unspecified");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_launcher_prepare_does_not_poison_watchdog_state() {
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let root = temp.path().to_path_buf();
|
||||||
|
|
||||||
|
let launcher = Launcher::new(root);
|
||||||
|
let options = LaunchOptions {
|
||||||
|
place_id: None,
|
||||||
|
safe_mode: false,
|
||||||
|
region: ServerRegion::Auto,
|
||||||
|
instance_id: "default".to_string(),
|
||||||
|
fps_cap: Some(144),
|
||||||
|
vram_skip_mips: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calling prepare_launch 3 times must NOT trigger Safe Mode
|
||||||
|
let plan1 = launcher.prepare_launch(options.clone()).unwrap();
|
||||||
|
assert!(!plan1.is_safe_mode);
|
||||||
|
|
||||||
|
let plan2 = launcher.prepare_launch(options.clone()).unwrap();
|
||||||
|
assert!(!plan2.is_safe_mode);
|
||||||
|
|
||||||
|
let plan3 = launcher.prepare_launch(options).unwrap();
|
||||||
|
assert!(!plan3.is_safe_mode, "Static preparation must not poison watchdog into Safe Mode");
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@ fn test_launcher_prepare_flow() {
|
||||||
safe_mode: false,
|
safe_mode: false,
|
||||||
region: ServerRegion::Europe,
|
region: ServerRegion::Europe,
|
||||||
instance_id: "default".to_string(),
|
instance_id: "default".to_string(),
|
||||||
|
fps_cap: Some(144),
|
||||||
|
vram_skip_mips: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let plan = launcher.prepare_launch(options).expect("Should prepare launch plan");
|
let plan = launcher.prepare_launch(options).expect("Should prepare launch plan");
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ pub fn run(args: &[String]) {
|
||||||
safe_mode,
|
safe_mode,
|
||||||
region: s_region,
|
region: s_region,
|
||||||
instance_id: "default".to_string(),
|
instance_id: "default".to_string(),
|
||||||
|
fps_cap: None,
|
||||||
|
vram_skip_mips: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
match launcher.prepare_launch(options) {
|
match launcher.prepare_launch(options) {
|
||||||
|
|
@ -99,8 +101,9 @@ pub fn run(args: &[String]) {
|
||||||
}
|
}
|
||||||
Some(Commands::Clean) => {
|
Some(Commands::Clean) => {
|
||||||
println!("🧹 Cleaning old versions and temporary cache...");
|
println!("🧹 Cleaning old versions and temporary cache...");
|
||||||
|
let active_guid = Cleaner::resolve_active_version(&base_dir).unwrap_or_default();
|
||||||
let cleaner = Cleaner::new(versions_dir, downloads_dir);
|
let cleaner = Cleaner::new(versions_dir, downloads_dir);
|
||||||
match cleaner.clean_stale_data("active") {
|
match cleaner.clean_stale_data(&active_guid) {
|
||||||
Ok(report) => {
|
Ok(report) => {
|
||||||
println!("✅ Cleaning complete:");
|
println!("✅ Cleaning complete:");
|
||||||
println!(" • Purged old version directories: {}", report.purged_versions.len());
|
println!(" • Purged old version directories: {}", report.purged_versions.len());
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
lazy_static = "1.5"
|
lazy_static = "1.5"
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
use std::io::Write;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const OPCODE_HANDSHAKE: u32 = 0;
|
||||||
|
pub const OPCODE_FRAME: u32 = 1;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DiscordActivity {
|
pub struct DiscordActivity {
|
||||||
pub details: String,
|
pub details: String,
|
||||||
|
|
@ -11,13 +15,25 @@ pub struct DiscordActivity {
|
||||||
|
|
||||||
impl DiscordActivity {
|
impl DiscordActivity {
|
||||||
pub fn to_json_payload(&self) -> String {
|
pub fn to_json_payload(&self) -> String {
|
||||||
format!(
|
serde_json::json!({
|
||||||
r#"{{"cmd":"SET_ACTIVITY","args":{{"pid":{},"activity":{{"details":"{}","state":"{}","timestamps":{{"start":{}}},"assets":{{"large_image":"roblox_logo","large_text":"Roblox on Linux (Molotov)"}}}}}},"nonce":"1"}}"#,
|
"cmd": "SET_ACTIVITY",
|
||||||
std::process::id(),
|
"args": {
|
||||||
self.details,
|
"pid": std::process::id(),
|
||||||
self.state,
|
"activity": {
|
||||||
self.start_timestamp
|
"details": self.details,
|
||||||
)
|
"state": self.state,
|
||||||
|
"timestamps": {
|
||||||
|
"start": self.start_timestamp
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"large_image": "roblox_logo",
|
||||||
|
"large_text": "Roblox on Linux (Molotov)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nonce": "1"
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,23 +56,50 @@ impl DiscordRpc {
|
||||||
|
|
||||||
pub fn try_connect(&mut self) -> Result<bool, String> {
|
pub fn try_connect(&mut self) -> Result<bool, String> {
|
||||||
let uid = unsafe { libc::getuid() };
|
let uid = unsafe { libc::getuid() };
|
||||||
let socket_path = PathBuf::from(format!("/run/user/{}/discord-ipc-0", uid));
|
let base_dir = std::env::var("XDG_RUNTIME_DIR")
|
||||||
|
.unwrap_or_else(|_| format!("/run/user/{}", uid));
|
||||||
|
|
||||||
if !socket_path.exists() {
|
// Scan discord-ipc-0 through discord-ipc-9
|
||||||
return Ok(false);
|
for i in 0..=9 {
|
||||||
}
|
let socket_path = PathBuf::from(&base_dir).join(format!("discord-ipc-{}", i));
|
||||||
|
if socket_path.exists() {
|
||||||
|
if let Ok(mut s) = UnixStream::connect(&socket_path) {
|
||||||
|
// Send Handshake: OPCODE_HANDSHAKE (0)
|
||||||
|
let handshake_payload = serde_json::json!({
|
||||||
|
"v": 1,
|
||||||
|
"client_id": self.client_id
|
||||||
|
}).to_string();
|
||||||
|
|
||||||
match UnixStream::connect(&socket_path) {
|
if Self::send_ipc_frame(&mut s, OPCODE_HANDSHAKE, handshake_payload.as_bytes()).is_ok() {
|
||||||
Ok(s) => {
|
self.stream = Some(s);
|
||||||
self.stream = Some(s);
|
tracing::info!("DiscordRpc: connected to socket {}", socket_path.display());
|
||||||
tracing::info!("DiscordRpc: successfully connected to Discord IPC socket");
|
return Ok(true);
|
||||||
Ok(true)
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
tracing::debug!("DiscordRpc: failed to connect to socket: {}", e);
|
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_activity(&mut self, activity: &DiscordActivity) -> Result<(), String> {
|
||||||
|
if let Some(stream) = &mut self.stream {
|
||||||
|
let payload = activity.to_json_payload();
|
||||||
|
Self::send_ipc_frame(stream, OPCODE_FRAME, payload.as_bytes())
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
Err("Discord IPC stream not connected".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_ipc_frame(stream: &mut UnixStream, opcode: u32, payload: &[u8]) -> std::io::Result<()> {
|
||||||
|
let mut header = [0u8; 8];
|
||||||
|
header[0..4].copy_from_slice(&opcode.to_le_bytes());
|
||||||
|
header[4..8].copy_from_slice(&(payload.len() as u32).to_le_bytes());
|
||||||
|
|
||||||
|
stream.write_all(&header)?;
|
||||||
|
stream.write_all(payload)?;
|
||||||
|
stream.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_connected(&self) -> bool {
|
pub fn is_connected(&self) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use molotov_bootstrap::cleaner::Cleaner;
|
||||||
use molotov_bootstrap::launcher::{LaunchOptions, Launcher};
|
use molotov_bootstrap::launcher::{LaunchOptions, Launcher};
|
||||||
use molotov_bootstrap::region::ServerRegion;
|
use molotov_bootstrap::region::ServerRegion;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Tab {
|
pub enum Tab {
|
||||||
|
|
@ -23,10 +24,13 @@ pub struct MolotovApp {
|
||||||
pub region: ServerRegion,
|
pub region: ServerRegion,
|
||||||
pub discord_rpc: bool,
|
pub discord_rpc: bool,
|
||||||
pub status_message: String,
|
pub status_message: String,
|
||||||
|
tx: Sender<String>,
|
||||||
|
rx: Receiver<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MolotovApp {
|
impl Default for MolotovApp {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let (tx, rx) = channel();
|
||||||
Self {
|
Self {
|
||||||
current_tab: Tab::Play,
|
current_tab: Tab::Play,
|
||||||
safe_mode: false,
|
safe_mode: false,
|
||||||
|
|
@ -35,6 +39,8 @@ impl Default for MolotovApp {
|
||||||
region: ServerRegion::Auto,
|
region: ServerRegion::Auto,
|
||||||
discord_rpc: true,
|
discord_rpc: true,
|
||||||
status_message: "Ready to launch".to_string(),
|
status_message: "Ready to launch".to_string(),
|
||||||
|
tx,
|
||||||
|
rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +68,11 @@ impl MolotovApp {
|
||||||
|
|
||||||
impl eframe::App for MolotovApp {
|
impl eframe::App for MolotovApp {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// Poll asynchronous worker responses without blocking UI thread
|
||||||
|
while let Ok(msg) = self.rx.try_recv() {
|
||||||
|
self.status_message = msg;
|
||||||
|
}
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.selectable_value(&mut self.current_tab, Tab::Play, "🎮 Play");
|
ui.selectable_value(&mut self.current_tab, Tab::Play, "🎮 Play");
|
||||||
|
|
@ -92,28 +103,41 @@ impl eframe::App for MolotovApp {
|
||||||
ui.selectable_value(&mut self.region, ServerRegion::Europe, "Europe (Frankfurt / London)");
|
ui.selectable_value(&mut self.region, ServerRegion::Europe, "Europe (Frankfurt / London)");
|
||||||
ui.selectable_value(&mut self.region, ServerRegion::Asia, "Asia (Tokyo / Singapore)");
|
ui.selectable_value(&mut self.region, ServerRegion::Asia, "Asia (Tokyo / Singapore)");
|
||||||
ui.selectable_value(&mut self.region, ServerRegion::Australia, "Australia (Sydney)");
|
ui.selectable_value(&mut self.region, ServerRegion::Australia, "Australia (Sydney)");
|
||||||
|
ui.selectable_value(&mut self.region, ServerRegion::SouthAmerica, "South America (São Paulo)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
if ui.button("🚀 Launch Roblox").clicked() {
|
if ui.button("🚀 Launch Roblox").clicked() {
|
||||||
let (base_dir, _, _) = Self::get_paths();
|
let (base_dir, _, _) = Self::get_paths();
|
||||||
let launcher = Launcher::new(base_dir);
|
let tx = self.tx.clone();
|
||||||
let options = LaunchOptions {
|
let safe_mode = self.safe_mode;
|
||||||
place_id: None,
|
let region = self.region;
|
||||||
safe_mode: self.safe_mode,
|
let fps_cap = self.fps_cap;
|
||||||
region: self.region,
|
let vram_skip = self.vram_skip_mips;
|
||||||
instance_id: "default".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match launcher.prepare_launch(options) {
|
self.status_message = "Preparing launch in background...".to_string();
|
||||||
Ok(plan) => {
|
|
||||||
self.status_message = format!("Launch plan prepared! Safe Mode: {}", plan.is_safe_mode);
|
std::thread::spawn(move || {
|
||||||
|
let launcher = Launcher::new(base_dir);
|
||||||
|
let options = LaunchOptions {
|
||||||
|
place_id: None,
|
||||||
|
safe_mode,
|
||||||
|
region,
|
||||||
|
instance_id: "default".to_string(),
|
||||||
|
fps_cap: Some(fps_cap),
|
||||||
|
vram_skip_mips: vram_skip,
|
||||||
|
};
|
||||||
|
|
||||||
|
match launcher.prepare_launch(options) {
|
||||||
|
Ok(plan) => {
|
||||||
|
let _ = tx.send(format!("Launch plan ready! Safe Mode: {}, Region: {:?}", plan.is_safe_mode, region));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = tx.send(format!("Launch preparation failed: {}", e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
});
|
||||||
self.status_message = format!("Launch preparation failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tab::FastFlags => {
|
Tab::FastFlags => {
|
||||||
|
|
@ -139,17 +163,23 @@ impl eframe::App for MolotovApp {
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
if ui.button("Clean Stale Versions").clicked() {
|
if ui.button("Clean Stale Versions").clicked() {
|
||||||
let (_, downloads_dir, versions_dir) = Self::get_paths();
|
let (base_dir, downloads_dir, versions_dir) = Self::get_paths();
|
||||||
let cleaner = Cleaner::new(versions_dir, downloads_dir);
|
let tx = self.tx.clone();
|
||||||
match cleaner.clean_stale_data("active") {
|
self.status_message = "Cleaning cache and stale versions in background...".to_string();
|
||||||
Ok(report) => {
|
|
||||||
let mb = (report.reclaimed_bytes as f64) / (1024.0 * 1024.0);
|
std::thread::spawn(move || {
|
||||||
self.status_message = format!("Cleaned {} old versions, reclaimed {:.2} MB", report.purged_versions.len(), mb);
|
let active_ver = Cleaner::resolve_active_version(&base_dir).unwrap_or_default();
|
||||||
|
let cleaner = Cleaner::new(versions_dir, downloads_dir);
|
||||||
|
match cleaner.clean_stale_data(&active_ver) {
|
||||||
|
Ok(report) => {
|
||||||
|
let mb = (report.reclaimed_bytes as f64) / (1024.0 * 1024.0);
|
||||||
|
let _ = tx.send(format!("Cleaned {} old versions, reclaimed {:.2} MB", report.purged_versions.len(), mb));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = tx.send(format!("Clean failed: {}", e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
});
|
||||||
self.status_message = format!("Clean failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Reference in a new issue