diff --git a/src-tauri/src/features/instance_sync.rs b/src-tauri/src/features/instance_sync.rs new file mode 100644 index 0000000..68d9413 --- /dev/null +++ b/src-tauri/src/features/instance_sync.rs @@ -0,0 +1,123 @@ +use std::{collections::HashMap, fs, path::PathBuf}; + +use lyceris::auth::microsoft::MinecraftAccount; +use tauri::State; + +use crate::{features::Feature, launcher::launcher_dir, GameVersion, SharedState}; + +pub struct InstanceSync {} + +impl Default for InstanceSync { + fn default() -> Self { + Self {} + } +} + +impl Feature for InstanceSync { + fn launch( + &mut self, + state: &State<'_, SharedState>, + version: &GameVersion, + _account: &MinecraftAccount, + ) -> super::FeatureResult { + if state.lock().unwrap().settings.features.sync_options { + sync_to_instance(version).map_err(|e| e.to_string())?; + } + + Ok(()) + } + + fn after_launch( + &mut self, + state: &State<'_, SharedState>, + version: &GameVersion, + _account: &MinecraftAccount, + ) -> super::FeatureResult { + if state.lock().unwrap().settings.features.sync_options { + sync_back_from_instance(version).map_err(|e| e.to_string())?; + } + + Ok(()) + } +} + +fn sync_to_instance(version: &GameVersion) -> std::io::Result<()> { + let global_path = options_path_global(); + let instance_path = options_path_instance(version); + + if !global_path.exists() { + if let Ok(instance_content) = fs::read_to_string(instance_path) { + fs::write(global_path, instance_content)?; + } + + return Ok(()); + } + + let global_content = fs::read_to_string(global_path)?; + + // Ensure instance dir exists + if let Some(parent) = instance_path.parent() { + fs::create_dir_all(parent)?; + } + + fs::write(instance_path, global_content)?; + + Ok(()) +} + +fn sync_back_from_instance(version: &GameVersion) -> std::io::Result<()> { + let global_path = options_path_global(); + let instance_path = options_path_instance(version); + + let global_content = fs::read_to_string(&global_path)?; + let instance_content = fs::read_to_string(&instance_path)?; + + let mut global_map = parse_options(&global_content); + let instance_map = parse_options(&instance_content); + + // Only apply changes (ignore deletions) + for (key, instance_value) in instance_map { + match global_map.get(&key) { + Some(global_value) if global_value == &instance_value => {} + _ => { + // changed or new → update global + global_map.insert(key, instance_value); + } + } + } + + fs::write(global_path, serialize_options(&global_map))?; + + Ok(()) +} + +fn options_path_global() -> PathBuf { + launcher_dir().join("options.txt") +} + +fn options_path_instance(version: &GameVersion) -> PathBuf { + launcher_dir().join(&version.id).join("options.txt") +} + +fn parse_options(content: &str) -> HashMap { + let mut map = HashMap::new(); + + for line in content.lines() { + if line.trim().is_empty() { + continue; + } + + if let Some((key, value)) = line.split_once(':') { + map.insert(key.to_string(), value.to_string()); + } + } + + map +} + +fn serialize_options(map: &HashMap) -> String { + map.iter() + .map(|(k, v)| format!("{k}:{v}")) + .collect::>() + .join("\n") +} diff --git a/src-tauri/src/features/mod.rs b/src-tauri/src/features/mod.rs new file mode 100644 index 0000000..28a780d --- /dev/null +++ b/src-tauri/src/features/mod.rs @@ -0,0 +1,78 @@ +use lyceris::auth::microsoft::MinecraftAccount; +use serde::{Deserialize, Serialize}; +use tauri::State; +use tokio::sync::Mutex; + +use crate::{features::instance_sync::InstanceSync, GameVersion, SharedState}; + +mod instance_sync; + +pub type FeatureResult = Result<(), String>; + +#[derive(Clone, Serialize, Deserialize)] +pub struct FeatureSettings { + // Instance Sync + sync_options: bool, +} + +impl Default for FeatureSettings { + fn default() -> Self { + Self { sync_options: true } + } +} + +#[allow(unused_variables)] +pub trait Feature: Send + Sync { + fn launch( + &mut self, + state: &State<'_, SharedState>, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { + Ok(()) + } + fn after_launch( + &mut self, + state: &State<'_, SharedState>, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { + Ok(()) + } +} + +pub struct FeatureState(Mutex>>); + +impl FeatureState { + pub fn new() -> Self { + Self(Mutex::new(vec![Box::new(InstanceSync::default())])) + } +} + +impl FeatureState { + pub async fn launch( + &self, + state: &State<'_, SharedState>, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { + for feature in self.0.lock().await.iter_mut() { + feature.launch(state, version, account)?; + } + + Ok(()) + } + + pub async fn after_launch( + &self, + state: &State<'_, SharedState>, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { + for feature in self.0.lock().await.iter_mut() { + feature.after_launch(state, version, account)?; + } + + Ok(()) + } +} diff --git a/src-tauri/src/launcher/auth.rs b/src-tauri/src/launcher/auth.rs index a1ac007..70bd0c3 100644 --- a/src-tauri/src/launcher/auth.rs +++ b/src-tauri/src/launcher/auth.rs @@ -61,7 +61,7 @@ pub fn auth_create_link(app: AppHandle) -> Result { // Check for auth code if url_str.contains("code=") { if let Some(code) = extract_code(&url_str) { - println!("Auth code received: {}", code); + println!("Auth code received: {} ({})", &code[0..5], code.len()); let _ = app_handle.emit("auth-code", code); @@ -125,8 +125,11 @@ pub async fn auth_refresh(uuid: String) -> Result { } #[command] -pub fn auth_list() -> Result, String> { - Ok(load_accounts()) +pub fn auth_list() -> Result, String> { + Ok(load_accounts() + .iter() + .map(|account| (account.uuid.clone(), account.username.clone())) + .collect()) } #[command] diff --git a/src-tauri/src/launcher/mod.rs b/src-tauri/src/launcher/mod.rs index de96d04..5d686a7 100644 --- a/src-tauri/src/launcher/mod.rs +++ b/src-tauri/src/launcher/mod.rs @@ -16,7 +16,7 @@ use lyceris::{ use tauri::{Emitter as _, State}; -use crate::{GameVersion, SharedState}; +use crate::{features::FeatureState, GameVersion, SharedState}; pub fn launcher_dir() -> PathBuf { if cfg!(target_os = "macos") { @@ -47,6 +47,7 @@ pub fn launcher_dir() -> PathBuf { pub async fn launch_game( app: tauri::AppHandle, state: State<'_, SharedState>, + feature_state: State<'_, FeatureState>, version: GameVersion, account: MinecraftAccount, ) -> Result<(), String> { @@ -95,14 +96,14 @@ pub async fn launch_game( launcher_dir().join(&version.id), version.id.clone(), lyceris::auth::AuthMethod::Microsoft { - username: account.username, - xuid: account.xuid, - uuid: account.uuid, - access_token: account.access_token, - refresh_token: account.refresh_token, + username: account.username.clone(), + xuid: account.xuid.clone(), + uuid: account.uuid.clone(), + access_token: account.access_token.clone(), + refresh_token: account.refresh_token.clone(), }, ) - .loader(Fabric(version.loader_version).into()) + .loader(Fabric(version.loader_version.clone()).into()) .memory(lyceris::minecraft::config::Memory::Megabyte( state.lock().unwrap().settings.max_memory as u64, )) @@ -124,6 +125,11 @@ pub async fn launch_game( app.emit("launch-status", "Launching").unwrap(); + feature_state + .inner() + .launch(&state, &version, &account) + .await?; + launch(&config, Some(&emitter)) .await .map_err(|e| e.to_string())? @@ -131,6 +137,11 @@ pub async fn launch_game( .await .map_err(|e| e.to_string())?; + feature_state + .inner() + .after_launch(&state, &version, &account) + .await?; + app.emit("is-launching", "false").unwrap(); app.emit("launch-status", "").unwrap(); emit_progress(&app, 0.0); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8cb4471..613b035 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ pub mod commands; +pub mod features; pub mod launcher; use serde::{Deserialize, Serialize}; @@ -8,7 +9,7 @@ use std::{ sync::{Arc, Mutex}, }; -use crate::launcher::launcher_dir; +use crate::{features::FeatureSettings, launcher::launcher_dir}; pub type SharedState = Arc>; @@ -35,6 +36,7 @@ pub struct GameVersion { #[derive(Clone, Serialize, Deserialize)] pub struct LauncherSettings { pub max_memory: u32, + pub features: FeatureSettings, } // --- App State --- @@ -47,7 +49,10 @@ pub struct AppState { impl Default for AppState { fn default() -> Self { Self { - settings: LauncherSettings { max_memory: 4096 }, + settings: LauncherSettings { + max_memory: 2048, + features: FeatureSettings::default(), + }, version: String::from("1.21.6"), } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 72fa828..92f17e9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,7 +1,7 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use saturn_launcher_lib::load_state; +use saturn_launcher_lib::{features::FeatureState, load_state}; use tauri::{Emitter, Manager, WindowEvent}; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -9,6 +9,7 @@ fn main() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(load_state()) + .manage(FeatureState::new()) .invoke_handler(tauri::generate_handler![ // Saturn Launcher Commands saturn_launcher_lib::commands::get_installed_mods, diff --git a/src/App.tsx b/src/App.tsx index daa7827..6dfe2f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,6 @@ import ModsSection from "./components/ModsSection"; import SettingsSection from "./components/SettingsSection"; import { versions } from "./lib/launcher"; import { GameVersion } from "./lib/types"; -import { MinecraftAccount } from "./lib/auth"; import { getVersion, installPaths, updateVersion } from "./lib/saturn"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { listen } from "@tauri-apps/api/event"; @@ -26,8 +25,8 @@ export default function App() { const versionRef = useRef(null); const accountRef = useRef(null); - const [accounts, setAccounts] = useState([]); - const [activeAccount, setActiveAccount] = useState( + const [accounts, setAccounts] = useState<[string, string][]>([]); + const [activeAccount, setActiveAccount] = useState<[string, string] | null>( null, ); @@ -137,7 +136,7 @@ export default function App() {
{active === "play" && ( - + )} {active === "mods" && selectedVersion && ( diff --git a/src/components/PlaySection.tsx b/src/components/PlaySection.tsx index bc97ef6..55fab7d 100644 --- a/src/components/PlaySection.tsx +++ b/src/components/PlaySection.tsx @@ -1,19 +1,22 @@ import React, { useEffect, useState } from "react"; import { Play, Loader2 } from "lucide-react"; -// import { launchGame } from "../services/SaturnApi"; import Bg from "@/assets/bg.png"; import { listen } from "@tauri-apps/api/event"; import { GameVersion } from "@/lib/types"; import { launch } from "@/lib/launcher"; -import { MinecraftAccount } from "@/lib/auth"; +import { authGetValid } from "@/lib/auth"; import { cn } from "@/lib/utils"; +import { SetState } from "./layout/props"; interface PlaySectionProps { version: GameVersion | null; - account: MinecraftAccount | null; + activeAccount: [string, string] | null; + setActiveAccount: SetState<[string, string] | null>, + accounts: [string, string][], + setAccounts: SetState<[string, string][]>, } -const PlaySection: React.FC = ({ version, account }) => { +const PlaySection: React.FC = ({ version, activeAccount: account }) => { const [isLaunching, setIsLaunching] = useState(false); const [launchStatus, setLaunchStatus] = useState(""); const [progress, setProgress] = useState(0); @@ -39,6 +42,13 @@ const PlaySection: React.FC = ({ version, account }) => { }; }, []); + const handleLaunch = async () => { + if (version && account) { + const validAccount = await authGetValid(account[0]); + launch(version, validAccount); + } + } + return (
{/* Background Image */} @@ -54,12 +64,12 @@ const PlaySection: React.FC = ({ version, account }) => {
- {/* Memory Allocation */} - -
-
- -
-

Memory Allocation

-
- -
-
- Maximum Memory (RAM) - - {(settings.max_memory / 1024).toFixed(1)} GB - -
- - - setSettings({ - ...settings, - max_memory: parseInt(e.target.value), - }) - } - className="w-full h-2 bg-saturn-border rounded-lg appearance-none cursor-pointer accent-saturn-accent" - /> - -
- {[1024, 4096, 8192, 12288, 16384].map((value) => { - const percent = - (((value - 1024) / (16384 - 1024)) * 100 - 50) * 0.97 + 50; - - return ( - - {value / 1024} GB - - ); - })} -
- -
- -

- Allocating too much memory can cause system instability, while - too little can lead to performance issues or crashes. 4GB - 8GB - is recommended for most users. -

-
-
-
+ {settingsSchema.map((item) => ())}
); diff --git a/src/components/layout/AccountDropdown.tsx b/src/components/layout/AccountDropdown.tsx index 398fe7d..6ea5ba5 100644 --- a/src/components/layout/AccountDropdown.tsx +++ b/src/components/layout/AccountDropdown.tsx @@ -7,7 +7,6 @@ import { authRemove, authCreateLink, authLogin, - MinecraftAccount, } from "@/lib/auth"; import { listen } from "@tauri-apps/api/event"; @@ -64,7 +63,7 @@ export default function AccountDropdown({ } // Switch account - function switchAccount(acc: MinecraftAccount) { + function switchAccount(acc: [string, string]) { setActiveAccount(acc); setIsAccountDropdownOpen(false); } @@ -73,7 +72,7 @@ export default function AccountDropdown({ async function logout() { if (!activeAccount) return; - await authRemove(activeAccount.uuid); + await authRemove(activeAccount[0]); const updated = await authList(); setAccounts(updated); @@ -91,7 +90,7 @@ export default function AccountDropdown({ }); } - const otherAccounts = accounts.filter((a) => a.uuid !== activeAccount?.uuid); + const otherAccounts = accounts.filter((a) => a[0] !== activeAccount?.[0]); return (
@@ -106,7 +105,7 @@ export default function AccountDropdown({
{activeAccount && ( )} @@ -123,7 +122,7 @@ export default function AccountDropdown({ Playing as

- {activeAccount?.username || "No Account"} + {activeAccount?.[1] || "No Account"}

@@ -140,17 +139,17 @@ export default function AccountDropdown({ {/* Current account */} {activeAccount && ( )} {/* Other accounts */} {otherAccounts.map((acc) => ( switchAccount(acc)} /> ))} diff --git a/src/components/layout/props.ts b/src/components/layout/props.ts index c2aeed6..ff394cd 100644 --- a/src/components/layout/props.ts +++ b/src/components/layout/props.ts @@ -1,4 +1,3 @@ -import { MinecraftAccount } from "@/lib/auth"; import { GameVersion } from "@/lib/types"; export type SetState = React.Dispatch>; @@ -17,9 +16,9 @@ export interface Props { versionRef: React.RefObject; accountRef: React.RefObject; - accounts: MinecraftAccount[]; - setAccounts: SetState; + accounts: [string, string][]; + setAccounts: SetState<[string, string][]>; - activeAccount: MinecraftAccount | null; - setActiveAccount: SetState; + activeAccount: [string, string] | null; + setActiveAccount: SetState<[string, string] | null>; } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 311c8ee..d17d8d9 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -33,8 +33,9 @@ export async function authRefresh(uuid: string): Promise { /** * Get all stored accounts + * Format [UUID, Username] */ -export async function authList(): Promise { +export async function authList(): Promise<[string, string][]> { return await invoke("auth_list"); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 81fd836..ad6b013 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -17,4 +17,7 @@ export interface GameVersion { export interface LauncherSettings { max_memory: number; + features: { + sync_options: boolean + } }