Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1830f63f5 | ||
|
|
f9752b3ea6 | ||
|
|
955890dabf | ||
|
|
a7842e2c12 | ||
|
|
615ac0c530 | ||
|
|
6d253aaf4f | ||
|
|
ac9108f507 | ||
|
|
19b19ba08e | ||
|
|
2bb686baaf | ||
|
|
38207a1f30 | ||
|
|
43b9d5910e | ||
|
|
fba51d78b8 | ||
|
|
950d7f0a1d | ||
|
|
450ade4b53 | ||
|
|
9769a2d6c1 | ||
|
|
725ac2187f |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "saturn-launcher",
|
"name": "saturn-launcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Generated
+1
-1
@@ -3887,7 +3887,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "saturn-launcher"
|
name = "saturn-launcher"
|
||||||
version = "0.1.1"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"lyceris",
|
"lyceris",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "saturn-launcher"
|
name = "saturn-launcher"
|
||||||
version = "0.1.1"
|
version = "0.1.3"
|
||||||
description = "A minecraft launcher for Saturn Client"
|
description = "A minecraft launcher for Saturn Client"
|
||||||
authors = ["SaturnClient"]
|
authors = ["SaturnClient"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -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<String, String> {
|
||||||
|
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, String>) -> String {
|
||||||
|
map.iter()
|
||||||
|
.map(|(k, v)| format!("{k}:{v}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
@@ -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<Vec<Box<dyn Feature>>>);
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ pub fn auth_create_link(app: AppHandle) -> Result<String, String> {
|
|||||||
// Check for auth code
|
// Check for auth code
|
||||||
if url_str.contains("code=") {
|
if url_str.contains("code=") {
|
||||||
if let Some(code) = extract_code(&url_str) {
|
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);
|
let _ = app_handle.emit("auth-code", code);
|
||||||
|
|
||||||
@@ -125,8 +125,11 @@ pub async fn auth_refresh(uuid: String) -> Result<MinecraftAccount, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub fn auth_list() -> Result<Vec<MinecraftAccount>, String> {
|
pub fn auth_list() -> Result<Vec<(String, String)>, String> {
|
||||||
Ok(load_accounts())
|
Ok(load_accounts()
|
||||||
|
.iter()
|
||||||
|
.map(|account| (account.uuid.clone(), account.username.clone()))
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use lyceris::{
|
|||||||
|
|
||||||
use tauri::{Emitter as _, State};
|
use tauri::{Emitter as _, State};
|
||||||
|
|
||||||
use crate::{GameVersion, SharedState};
|
use crate::{features::FeatureState, GameVersion, SharedState};
|
||||||
|
|
||||||
pub fn launcher_dir() -> PathBuf {
|
pub fn launcher_dir() -> PathBuf {
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
@@ -47,6 +47,7 @@ pub fn launcher_dir() -> PathBuf {
|
|||||||
pub async fn launch_game(
|
pub async fn launch_game(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
state: State<'_, SharedState>,
|
state: State<'_, SharedState>,
|
||||||
|
feature_state: State<'_, FeatureState>,
|
||||||
version: GameVersion,
|
version: GameVersion,
|
||||||
account: MinecraftAccount,
|
account: MinecraftAccount,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -95,14 +96,14 @@ pub async fn launch_game(
|
|||||||
launcher_dir().join(&version.id),
|
launcher_dir().join(&version.id),
|
||||||
version.id.clone(),
|
version.id.clone(),
|
||||||
lyceris::auth::AuthMethod::Microsoft {
|
lyceris::auth::AuthMethod::Microsoft {
|
||||||
username: account.username,
|
username: account.username.clone(),
|
||||||
xuid: account.xuid,
|
xuid: account.xuid.clone(),
|
||||||
uuid: account.uuid,
|
uuid: account.uuid.clone(),
|
||||||
access_token: account.access_token,
|
access_token: account.access_token.clone(),
|
||||||
refresh_token: account.refresh_token,
|
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(
|
.memory(lyceris::minecraft::config::Memory::Megabyte(
|
||||||
state.lock().unwrap().settings.max_memory as u64,
|
state.lock().unwrap().settings.max_memory as u64,
|
||||||
))
|
))
|
||||||
@@ -124,6 +125,11 @@ pub async fn launch_game(
|
|||||||
|
|
||||||
app.emit("launch-status", "Launching").unwrap();
|
app.emit("launch-status", "Launching").unwrap();
|
||||||
|
|
||||||
|
feature_state
|
||||||
|
.inner()
|
||||||
|
.launch(&state, &version, &account)
|
||||||
|
.await?;
|
||||||
|
|
||||||
launch(&config, Some(&emitter))
|
launch(&config, Some(&emitter))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
@@ -131,6 +137,11 @@ pub async fn launch_game(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
feature_state
|
||||||
|
.inner()
|
||||||
|
.after_launch(&state, &version, &account)
|
||||||
|
.await?;
|
||||||
|
|
||||||
app.emit("is-launching", "false").unwrap();
|
app.emit("is-launching", "false").unwrap();
|
||||||
app.emit("launch-status", "").unwrap();
|
app.emit("launch-status", "").unwrap();
|
||||||
emit_progress(&app, 0.0);
|
emit_progress(&app, 0.0);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod features;
|
||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -8,7 +9,7 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::launcher::launcher_dir;
|
use crate::{features::FeatureSettings, launcher::launcher_dir};
|
||||||
|
|
||||||
pub type SharedState = Arc<Mutex<AppState>>;
|
pub type SharedState = Arc<Mutex<AppState>>;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ pub struct GameVersion {
|
|||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct LauncherSettings {
|
pub struct LauncherSettings {
|
||||||
pub max_memory: u32,
|
pub max_memory: u32,
|
||||||
|
pub features: FeatureSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- App State ---
|
// --- App State ---
|
||||||
@@ -47,8 +49,11 @@ pub struct AppState {
|
|||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
settings: LauncherSettings { max_memory: 4096 },
|
settings: LauncherSettings {
|
||||||
version: String::from("1.21.6"),
|
max_memory: 2048,
|
||||||
|
features: FeatureSettings::default(),
|
||||||
|
},
|
||||||
|
version: String::from("1.21.11"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![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};
|
use tauri::{Emitter, Manager, WindowEvent};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -9,6 +9,7 @@ fn main() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.manage(load_state())
|
.manage(load_state())
|
||||||
|
.manage(FeatureState::new())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// Saturn Launcher Commands
|
// Saturn Launcher Commands
|
||||||
saturn_launcher_lib::commands::get_installed_mods,
|
saturn_launcher_lib::commands::get_installed_mods,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "saturn-launcher",
|
"productName": "saturn-launcher",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"identifier": "org.saturnclient.saturn-launcher",
|
"identifier": "org.saturnclient.saturn-launcher",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Saturn Launcher v0.1.1",
|
"title": "Saturn Launcher v0.1.3",
|
||||||
"width": 1000,
|
"width": 1000,
|
||||||
"height": 650,
|
"height": 650,
|
||||||
"backgroundColor": "#0a0a0a",
|
"backgroundColor": "#0a0a0a",
|
||||||
|
|||||||
+3
-4
@@ -6,7 +6,6 @@ import ModsSection from "./components/ModsSection";
|
|||||||
import SettingsSection from "./components/SettingsSection";
|
import SettingsSection from "./components/SettingsSection";
|
||||||
import { versions } from "./lib/launcher";
|
import { versions } from "./lib/launcher";
|
||||||
import { GameVersion } from "./lib/types";
|
import { GameVersion } from "./lib/types";
|
||||||
import { MinecraftAccount } from "./lib/auth";
|
|
||||||
import { getVersion, installPaths, updateVersion } from "./lib/saturn";
|
import { getVersion, installPaths, updateVersion } from "./lib/saturn";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
@@ -26,8 +25,8 @@ export default function App() {
|
|||||||
const versionRef = useRef<HTMLDivElement | null>(null);
|
const versionRef = useRef<HTMLDivElement | null>(null);
|
||||||
const accountRef = useRef<HTMLDivElement | null>(null);
|
const accountRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const [accounts, setAccounts] = useState<MinecraftAccount[]>([]);
|
const [accounts, setAccounts] = useState<[string, string][]>([]);
|
||||||
const [activeAccount, setActiveAccount] = useState<MinecraftAccount | null>(
|
const [activeAccount, setActiveAccount] = useState<[string, string] | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -137,7 +136,7 @@ export default function App() {
|
|||||||
|
|
||||||
<div className="flex-1 p-8 overflow-y-scroll">
|
<div className="flex-1 p-8 overflow-y-scroll">
|
||||||
{active === "play" && (
|
{active === "play" && (
|
||||||
<PlaySection version={selectedVersion} account={activeAccount} />
|
<PlaySection version={selectedVersion} activeAccount={activeAccount} setActiveAccount={setActiveAccount} accounts={accounts} setAccounts={setAccounts} />
|
||||||
)}
|
)}
|
||||||
{active === "mods" && selectedVersion && (
|
{active === "mods" && selectedVersion && (
|
||||||
<ModsSection version={selectedVersion} />
|
<ModsSection version={selectedVersion} />
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import { GameVersion, Mod } from "@/lib/types";
|
import { GameVersion, Mod } from "@/lib/types";
|
||||||
import { versions } from "@/lib/launcher";
|
import { versions } from "@/lib/launcher";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
function cn(...inputs: ClassValue[]) {
|
function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -44,18 +45,21 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
|
|||||||
string[]
|
string[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const requestIdRef = useRef(0);
|
||||||
|
|
||||||
const loadMods = async () => {
|
const loadMods = async () => {
|
||||||
|
const requestId = ++requestIdRef.current;
|
||||||
|
|
||||||
if (activeTab === "installed") {
|
if (activeTab === "installed") {
|
||||||
const m = await getInstalledMods(version.id);
|
const m = await getInstalledMods(version.id);
|
||||||
setMods(m);
|
|
||||||
if (activeTab === "installed") {
|
if (requestId === requestIdRef.current) {
|
||||||
// Fix: If discover mods takes too long and the user switches tabs it glitches
|
|
||||||
setMods(m);
|
setMods(m);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const m = await discoverMods(searchQuery);
|
const m = await discoverMods(searchQuery);
|
||||||
if (activeTab === "discover") {
|
|
||||||
// Fix: If discover mods takes too long and the user switches tabs it glitches
|
if (requestId === requestIdRef.current) {
|
||||||
setMods(m);
|
setMods(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Play, Loader2 } from "lucide-react";
|
import { Play, Loader2 } from "lucide-react";
|
||||||
// import { launchGame } from "../services/SaturnApi";
|
|
||||||
import Bg from "@/assets/bg.png";
|
import Bg from "@/assets/bg.png";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { GameVersion } from "@/lib/types";
|
import { GameVersion } from "@/lib/types";
|
||||||
import { launch } from "@/lib/launcher";
|
import { launch } from "@/lib/launcher";
|
||||||
import { MinecraftAccount } from "@/lib/auth";
|
import { authGetValid } from "@/lib/auth";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { SetState } from "./layout/props";
|
||||||
|
|
||||||
interface PlaySectionProps {
|
interface PlaySectionProps {
|
||||||
version: GameVersion | null;
|
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<PlaySectionProps> = ({ version, account }) => {
|
const PlaySection: React.FC<PlaySectionProps> = ({ version, activeAccount: account }) => {
|
||||||
const [isLaunching, setIsLaunching] = useState(false);
|
const [isLaunching, setIsLaunching] = useState(false);
|
||||||
const [launchStatus, setLaunchStatus] = useState("");
|
const [launchStatus, setLaunchStatus] = useState("");
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
@@ -39,6 +42,13 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleLaunch = async () => {
|
||||||
|
if (version && account) {
|
||||||
|
const validAccount = await authGetValid(account[0]);
|
||||||
|
launch(version, validAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
@@ -54,7 +64,7 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
|||||||
<div className="absolute -inset-1 bg-linear-to-r from-blue-600 to-cyan-500 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
<div className="absolute -inset-1 bg-linear-to-r from-blue-600 to-cyan-500 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => version && account && launch(version, account)}
|
onClick={handleLaunch}
|
||||||
disabled={isLaunching || !version || !account}
|
disabled={isLaunching || !version || !account}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center",
|
"relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center",
|
||||||
|
|||||||
@@ -1,9 +1,210 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { MemoryStick, Save, RefreshCcw, Info } from "lucide-react";
|
import { Save, RefreshCcw } from "lucide-react";
|
||||||
import { getSettings, updateSettings } from "../lib/saturn";
|
import { getSettings, updateSettings } from "../lib/saturn";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { LauncherSettings } from "@/lib/types";
|
import { LauncherSettings } from "@/lib/types";
|
||||||
|
|
||||||
|
export type SettingSchemaItem =
|
||||||
|
| SettingSchemaLeaf
|
||||||
|
| SettingSchemaGroup;
|
||||||
|
|
||||||
|
export interface SettingSchemaBase {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingSchemaLeaf extends SettingSchemaBase {
|
||||||
|
type: "slider" | "toggle" | "input";
|
||||||
|
|
||||||
|
key: string; // leaf key (resolved in context)
|
||||||
|
default: any;
|
||||||
|
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
format?: (value: any) => string;
|
||||||
|
ticks?: number[];
|
||||||
|
info?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingSchemaGroup extends SettingSchemaBase {
|
||||||
|
type: "group";
|
||||||
|
children: SettingSchemaItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsSchema: SettingSchemaItem[] = [
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
label: "Performance",
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "max_memory",
|
||||||
|
label: "Maximum Memory (RAM)",
|
||||||
|
type: "slider",
|
||||||
|
min: 1024,
|
||||||
|
max: 16384,
|
||||||
|
step: 512,
|
||||||
|
default: 2048,
|
||||||
|
ticks: [1024, 4096, 8192, 12288, 16384],
|
||||||
|
|
||||||
|
format: (v) => `${(v / 1024).toFixed(1)} GB`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
label: "Features",
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "features.sync_options",
|
||||||
|
label: "Sync options.txt",
|
||||||
|
type: "toggle",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getValue = (obj: any, path: string) =>
|
||||||
|
path.split(".").reduce((acc, key) => acc?.[key], obj);
|
||||||
|
|
||||||
|
const setValue = (obj: any, path: string, value: any) => {
|
||||||
|
const keys = path.split(".");
|
||||||
|
const last = keys.pop()!;
|
||||||
|
const clone = structuredClone(obj);
|
||||||
|
|
||||||
|
let current = clone;
|
||||||
|
for (const key of keys) current = current[key];
|
||||||
|
|
||||||
|
current[last] = value;
|
||||||
|
return clone;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingItem: React.FC<{
|
||||||
|
item: SettingSchemaItem;
|
||||||
|
settings: LauncherSettings;
|
||||||
|
setSettings: React.Dispatch<
|
||||||
|
React.SetStateAction<LauncherSettings | null>
|
||||||
|
>;
|
||||||
|
}> = ({ item, settings, setSettings }) => {
|
||||||
|
if (item.type === "group") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 border-l border-saturn-border pl-4">
|
||||||
|
<div className="text-sm font-semibold text-saturn-text-secondary">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.children.map((child, i) => (
|
||||||
|
<SettingItem
|
||||||
|
key={i}
|
||||||
|
item={child}
|
||||||
|
settings={settings}
|
||||||
|
setSettings={setSettings}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = getValue(settings, item.key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{item.type === "slider" && (
|
||||||
|
<div key={item.key}>
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span className="text-md font-medium">{item.label}</span>
|
||||||
|
<span className="text-md font-medium text-saturn-accent">{(item.format && item.format(value)) || value}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={item.min}
|
||||||
|
max={item.max}
|
||||||
|
step={item.step}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((prev) =>
|
||||||
|
prev
|
||||||
|
? setValue(prev, item.key, parseInt(e.target.value))
|
||||||
|
: prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full h-2 bg-saturn-border rounded-lg appearance-none cursor-pointer accent-saturn-accent"
|
||||||
|
/>
|
||||||
|
{item.ticks && (
|
||||||
|
<div className="relative w-full mt-2 h-6">
|
||||||
|
{item.ticks.map((tick) => {
|
||||||
|
const percent =
|
||||||
|
(((tick - item.min!) / (item.max! - item.min!)) * 100 - 50) *
|
||||||
|
0.97 +
|
||||||
|
50;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={tick}
|
||||||
|
className="absolute text-[10px] text-saturn-text-secondary font-bold uppercase tracking-widest -translate-x-1/2 whitespace-nowrap"
|
||||||
|
style={{ left: `${percent}%` }}
|
||||||
|
>
|
||||||
|
{tick / 1024} GB
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.type === "toggle" && (
|
||||||
|
<div className="flex gap-2 items-center" key={item.key}>
|
||||||
|
<input
|
||||||
|
className="appearance-none w-4 h-4 bg-saturn-panel border border-saturn-border rounded checked:bg-saturn-accent"
|
||||||
|
type="checkbox"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((prev) =>
|
||||||
|
prev ? setValue(prev, item.key, e.target.checked) : prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function setNestedValue(obj: any, path: string, value: any) {
|
||||||
|
const keys = path.split(".");
|
||||||
|
let current = obj;
|
||||||
|
|
||||||
|
keys.forEach((key, index) => {
|
||||||
|
if (index === keys.length - 1) {
|
||||||
|
current[key] = value;
|
||||||
|
} else {
|
||||||
|
if (!current[key] || typeof current[key] !== "object") {
|
||||||
|
current[key] = {};
|
||||||
|
}
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildDefaultSettings = (schema: SettingSchemaItem[]): any => {
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const item of schema) {
|
||||||
|
if (item.type === "group") {
|
||||||
|
Object.assign(result, buildDefaultSettings(item.children));
|
||||||
|
} else {
|
||||||
|
setNestedValue(result, item.key, item.default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsSection: React.FC = () => {
|
const SettingsSection: React.FC = () => {
|
||||||
const [settings, setSettings] = useState<LauncherSettings | null>(null);
|
const [settings, setSettings] = useState<LauncherSettings | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -32,11 +233,7 @@ const SettingsSection: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
// In a real app, this would reset to defaults in the service
|
setSettings(buildDefaultSettings(settingsSchema) as LauncherSettings);
|
||||||
const defaultSettings: LauncherSettings = {
|
|
||||||
max_memory: 4096,
|
|
||||||
};
|
|
||||||
setSettings(defaultSettings);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
@@ -70,69 +267,7 @@ const SettingsSection: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8">
|
<div className="grid grid-cols-1 gap-8">
|
||||||
{/* Memory Allocation */}
|
{settingsSchema.map((item) => (<SettingItem item={item} settings={settings} setSettings={setSettings} />))}
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="panel p-6 space-y-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 rounded-lg bg-saturn-accent/10 flex items-center justify-center text-saturn-accent">
|
|
||||||
<MemoryStick size={18} />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-lg font-bold">Memory Allocation</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm font-medium">Maximum Memory (RAM)</span>
|
|
||||||
<span className="text-lg font-bold text-saturn-accent">
|
|
||||||
{(settings.max_memory / 1024).toFixed(1)} GB
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1024"
|
|
||||||
max="16384"
|
|
||||||
step="512"
|
|
||||||
value={settings.max_memory}
|
|
||||||
onChange={(e) =>
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative w-full mt-2 h-6">
|
|
||||||
{[1024, 4096, 8192, 12288, 16384].map((value) => {
|
|
||||||
const percent =
|
|
||||||
(((value - 1024) / (16384 - 1024)) * 100 - 50) * 0.97 + 50;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={value}
|
|
||||||
className="absolute text-[10px] text-saturn-text-secondary font-bold uppercase tracking-widest -translate-x-1/2 whitespace-nowrap"
|
|
||||||
style={{ left: `${percent}%` }}
|
|
||||||
>
|
|
||||||
{value / 1024} GB
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 bg-blue-500/5 border border-blue-500/10 rounded-lg flex gap-3">
|
|
||||||
<Info size={16} className="text-blue-400 shrink-0 mt-0.5" />
|
|
||||||
<p className="text-xs text-blue-300/80 leading-relaxed">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
authRemove,
|
authRemove,
|
||||||
authCreateLink,
|
authCreateLink,
|
||||||
authLogin,
|
authLogin,
|
||||||
MinecraftAccount,
|
|
||||||
} from "@/lib/auth";
|
} from "@/lib/auth";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ export default function AccountDropdown({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Switch account
|
// Switch account
|
||||||
function switchAccount(acc: MinecraftAccount) {
|
function switchAccount(acc: [string, string]) {
|
||||||
setActiveAccount(acc);
|
setActiveAccount(acc);
|
||||||
setIsAccountDropdownOpen(false);
|
setIsAccountDropdownOpen(false);
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,7 @@ export default function AccountDropdown({
|
|||||||
async function logout() {
|
async function logout() {
|
||||||
if (!activeAccount) return;
|
if (!activeAccount) return;
|
||||||
|
|
||||||
await authRemove(activeAccount.uuid);
|
await authRemove(activeAccount[0]);
|
||||||
|
|
||||||
const updated = await authList();
|
const updated = await authList();
|
||||||
setAccounts(updated);
|
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 (
|
return (
|
||||||
<div ref={accountRef} className="relative group overflow-visible">
|
<div ref={accountRef} className="relative group overflow-visible">
|
||||||
@@ -106,7 +105,7 @@ export default function AccountDropdown({
|
|||||||
<div className="absolute -top-2 left-4 group-hover:scale-110 transition-transform duration-700 w-12 overflow-hidden z-20">
|
<div className="absolute -top-2 left-4 group-hover:scale-110 transition-transform duration-700 w-12 overflow-hidden z-20">
|
||||||
{activeAccount && (
|
{activeAccount && (
|
||||||
<img
|
<img
|
||||||
src={`https://render.crafty.gg/3d/bust/${activeAccount.uuid}`}
|
src={`https://render.crafty.gg/3d/bust/${activeAccount[0]}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -123,7 +122,7 @@ export default function AccountDropdown({
|
|||||||
Playing as
|
Playing as
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-bold text-white">
|
<p className="text-sm font-bold text-white">
|
||||||
{activeAccount?.username || "No Account"}
|
{activeAccount?.[1] || "No Account"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -140,17 +139,17 @@ export default function AccountDropdown({
|
|||||||
{/* Current account */}
|
{/* Current account */}
|
||||||
{activeAccount && (
|
{activeAccount && (
|
||||||
<UserComponent
|
<UserComponent
|
||||||
username={activeAccount.username}
|
username={activeAccount[1]}
|
||||||
uuid={activeAccount.uuid}
|
uuid={activeAccount[0]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Other accounts */}
|
{/* Other accounts */}
|
||||||
{otherAccounts.map((acc) => (
|
{otherAccounts.map((acc) => (
|
||||||
<UserComponent
|
<UserComponent
|
||||||
key={acc.uuid}
|
key={acc[0]}
|
||||||
username={acc.username}
|
username={acc[1]}
|
||||||
uuid={acc.uuid}
|
uuid={acc[0]}
|
||||||
onClick={() => switchAccount(acc)}
|
onClick={() => switchAccount(acc)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MinecraftAccount } from "@/lib/auth";
|
|
||||||
import { GameVersion } from "@/lib/types";
|
import { GameVersion } from "@/lib/types";
|
||||||
|
|
||||||
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||||
@@ -17,9 +16,9 @@ export interface Props {
|
|||||||
versionRef: React.RefObject<HTMLDivElement | null>;
|
versionRef: React.RefObject<HTMLDivElement | null>;
|
||||||
accountRef: React.RefObject<HTMLDivElement | null>;
|
accountRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
|
||||||
accounts: MinecraftAccount[];
|
accounts: [string, string][];
|
||||||
setAccounts: SetState<MinecraftAccount[]>;
|
setAccounts: SetState<[string, string][]>;
|
||||||
|
|
||||||
activeAccount: MinecraftAccount | null;
|
activeAccount: [string, string] | null;
|
||||||
setActiveAccount: SetState<MinecraftAccount | null>;
|
setActiveAccount: SetState<[string, string] | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -33,8 +33,9 @@ export async function authRefresh(uuid: string): Promise<MinecraftAccount> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all stored accounts
|
* Get all stored accounts
|
||||||
|
* Format [UUID, Username]
|
||||||
*/
|
*/
|
||||||
export async function authList(): Promise<MinecraftAccount[]> {
|
export async function authList(): Promise<[string, string][]> {
|
||||||
return await invoke("auth_list");
|
return await invoke("auth_list");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-3
@@ -6,17 +6,42 @@ export const versions: GameVersion[] = [
|
|||||||
{
|
{
|
||||||
id: "1.21.4",
|
id: "1.21.4",
|
||||||
name: "Fabric 1.21.4",
|
name: "Fabric 1.21.4",
|
||||||
loader_version: "0.18.6",
|
loader_version: "0.19.0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "1.21.5",
|
id: "1.21.5",
|
||||||
name: "Fabric 1.21.5",
|
name: "Fabric 1.21.5",
|
||||||
loader_version: "0.18.6",
|
loader_version: "0.19.0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "1.21.6",
|
id: "1.21.6",
|
||||||
name: "Fabric 1.21.6",
|
name: "Fabric 1.21.6",
|
||||||
loader_version: "0.18.6",
|
loader_version: "0.19.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1.21.7",
|
||||||
|
name: "Fabric 1.21.7",
|
||||||
|
loader_version: "0.19.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1.21.8",
|
||||||
|
name: "Fabric 1.21.8",
|
||||||
|
loader_version: "0.19.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1.21.9",
|
||||||
|
name: "Fabric 1.21.9",
|
||||||
|
loader_version: "0.19.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1.21.10",
|
||||||
|
name: "Fabric 1.21.10",
|
||||||
|
loader_version: "0.19.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1.21.11",
|
||||||
|
name: "Fabric 1.21.11",
|
||||||
|
loader_version: "0.19.0",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,7 @@ export interface GameVersion {
|
|||||||
|
|
||||||
export interface LauncherSettings {
|
export interface LauncherSettings {
|
||||||
max_memory: number;
|
max_memory: number;
|
||||||
|
features: {
|
||||||
|
sync_options: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user