Add launcher preferences and modular landing studio [skip ci]
This commit is contained in:
1 parent
454620f21e
commit
6deff62a3a
25 files changed
+822
-682
No files matched your search
@@ -135,7 +135,7 @@ fn save_panel_account(state: &AppState, panel: &str, auth: AuthResponse) -> Resu
|
||||
}
|
||||
|
||||
pub async fn rename_panel(state: &AppState, username: &str, password: &str) -> Result<Account> {
|
||||
if state.game.lock().unwrap().is_some() || state.task.lock().unwrap().as_ref().is_some_and(|t| !t.is_finished()) {
|
||||
if !state.game.lock().unwrap().is_empty() || state.task.lock().unwrap().as_ref().is_some_and(|t| !t.is_finished()) {
|
||||
bail!("close Minecraft and wait for any installation to finish before changing your username");
|
||||
}
|
||||
let panel = state.panel_url().ok_or_else(|| anyhow!("no panel configured"))?;
|
||||
|
||||
@@ -52,9 +52,12 @@ pub struct Bootstrap {
|
||||
accounts: Vec<Account>,
|
||||
active_account: Option<String>,
|
||||
manifest: Option<LauncherManifest>,
|
||||
game_running: Option<String>,
|
||||
game_running: Vec<RunningGameInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RunningGameInfo { run_id: String, instance_id: String }
|
||||
|
||||
#[tauri::command]
|
||||
pub fn bootstrap(state: State<'_, AppState>) -> Bootstrap {
|
||||
let accounts = state.accounts.read().unwrap().clone();
|
||||
@@ -74,7 +77,7 @@ pub fn bootstrap(state: State<'_, AppState>) -> Bootstrap {
|
||||
accounts: accounts.accounts,
|
||||
active_account: accounts.active,
|
||||
manifest: state.manifest.read().unwrap().clone(),
|
||||
game_running: state.game.lock().unwrap().as_ref().map(|g| g.instance_id.clone()),
|
||||
game_running: state.game.lock().unwrap().iter().map(|g| RunningGameInfo { run_id: g.run_id.clone(), instance_id: g.instance_id.clone() }).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +122,24 @@ pub async fn set_panel_url(state: State<'_, AppState>, url: String) -> Res<Launc
|
||||
pub fn save_settings(state: State<'_, AppState>, settings: Settings) -> Res<()> {
|
||||
let mut current = state.settings.write().unwrap();
|
||||
let panel = current.panel_url.clone();
|
||||
*current = Settings { panel_url: panel, ..settings };
|
||||
let captured = current.instance_game_options.clone();
|
||||
*current = Settings { panel_url: panel, instance_game_options: captured, ..settings };
|
||||
drop(current);
|
||||
state.save_settings().map_err(aerr)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_instance_options(state: State<'_, AppState>, instance_id: String) -> Res<std::collections::BTreeMap<String, String>> {
|
||||
if state.game.lock().unwrap().iter().any(|g| g.instance_id == instance_id) {
|
||||
return Err("close this instance before saving its game settings".into());
|
||||
}
|
||||
let options = scopenet_core::options::read_vanilla_preferences(&state.layout.instance_dir(&instance_id)).map_err(aerr)?;
|
||||
if options.is_empty() { return Err("no vanilla options.txt settings found yet".into()); }
|
||||
state.settings.write().unwrap().instance_game_options.insert(instance_id, options.clone());
|
||||
state.save_settings().map_err(aerr)?;
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
// ---- accounts ----
|
||||
|
||||
#[tauri::command]
|
||||
@@ -345,8 +361,8 @@ pub fn repair_instance(app: AppHandle, state: State<'_, AppState>, instance_id:
|
||||
}
|
||||
|
||||
fn start_task(app: AppHandle, state: &AppState, instance_id: String, start: bool, deep: bool) -> Res<()> {
|
||||
if state.game.lock().unwrap().is_some() {
|
||||
return Err("Minecraft is already running".into());
|
||||
if deep && state.game.lock().unwrap().iter().any(|g| g.instance_id == instance_id) {
|
||||
return Err("Close this instance's game windows before repairing it".into());
|
||||
}
|
||||
let mut task = state.task.lock().unwrap();
|
||||
if task.as_ref().is_some_and(|t| !t.is_finished()) {
|
||||
@@ -373,9 +389,9 @@ pub fn cancel_launch(app: AppHandle, state: State<'_, AppState>, instance_id: St
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn kill_game(state: State<'_, AppState>) {
|
||||
if let Some(g) = state.game.lock().unwrap().take() {
|
||||
g.kill.send(()).ok();
|
||||
pub fn kill_game(state: State<'_, AppState>, run_id: String) {
|
||||
if let Some(g) = state.game.lock().unwrap().iter_mut().find(|g| g.run_id == run_id) {
|
||||
if let Some(kill) = g.kill.take() { kill.send(()).ok(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +460,7 @@ pub async fn storage_info(state: State<'_, AppState>) -> Res<Storage> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_instance_data(state: State<'_, AppState>, instance_id: String) -> Res<()> {
|
||||
if state.game.lock().unwrap().as_ref().is_some_and(|g| g.instance_id == instance_id) {
|
||||
if state.game.lock().unwrap().iter().any(|g| g.instance_id == instance_id) {
|
||||
return Err("close the game first".into());
|
||||
}
|
||||
let dir = state.layout.instance_dir(&instance_id);
|
||||
|
||||
@@ -19,6 +19,7 @@ use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct StatePayload {
|
||||
pub instance_id: String,
|
||||
pub run_id: Option<String>,
|
||||
/// "preparing" | "running" | "idle" | "error"
|
||||
pub state: String,
|
||||
pub message: Option<String>,
|
||||
@@ -27,6 +28,7 @@ pub struct StatePayload {
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ExitPayload {
|
||||
pub instance_id: String,
|
||||
pub run_id: String,
|
||||
pub code: Option<i32>,
|
||||
pub crashed: bool,
|
||||
pub tail: Vec<String>,
|
||||
@@ -34,7 +36,7 @@ pub struct ExitPayload {
|
||||
}
|
||||
|
||||
pub fn emit_state(app: &AppHandle, id: &str, state: &str, message: Option<String>) {
|
||||
app.emit("launch://state", StatePayload { instance_id: id.into(), state: state.into(), message }).ok();
|
||||
app.emit("launch://state", StatePayload { instance_id: id.into(), run_id: None, state: state.into(), message }).ok();
|
||||
}
|
||||
|
||||
/// Fetch the instance definition, falling back to the last copy we saw so
|
||||
@@ -119,7 +121,19 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
|
||||
deep_verify: deep,
|
||||
};
|
||||
let installed = install::install(&state.http, &state.layout, &spec, &report).await?;
|
||||
sync::sync(&state.http, &panel, &game_dir, inst.revision, &manifest.files, spec.concurrency, deep, &report).await?;
|
||||
let already_running = state.game.lock().unwrap().iter().any(|g| g.instance_id == instance_id);
|
||||
// The on-disk file may be newer when the launcher was closed before Minecraft.
|
||||
let current_options = options::read_vanilla_preferences(&game_dir).unwrap_or_default();
|
||||
let mut saved_options = if current_options.is_empty() {
|
||||
settings.instance_game_options.get(&instance_id).cloned().unwrap_or_default()
|
||||
} else {
|
||||
current_options
|
||||
};
|
||||
// Do not rewrite pack files or options while another copy is using this directory.
|
||||
if !already_running {
|
||||
sync::sync(&state.http, &panel, &game_dir, inst.revision, &manifest.files, spec.concurrency, deep, &report).await?;
|
||||
options::apply(&game_dir, &mut saved_options)?;
|
||||
}
|
||||
|
||||
if let Some(server) = &inst.server {
|
||||
if server.inject && !server.address.is_empty() {
|
||||
@@ -127,7 +141,13 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
|
||||
servers_dat::upsert(&game_dir, &name, &servers_dat::format_address(&server.address, server.port)).ok();
|
||||
}
|
||||
}
|
||||
if settings.keybinds_enabled && installed.version.is_modern() && !settings.keybinds.is_empty() {
|
||||
if !already_running && settings.vanilla_controls_enabled {
|
||||
let mut controls = std::collections::BTreeMap::new();
|
||||
controls.insert("autoJump".to_string(), settings.auto_jump.to_string());
|
||||
controls.insert("mouseSensitivity".to_string(), settings.sensitivity.clamp(0.0, 1.0).to_string());
|
||||
options::apply(&game_dir, &mut controls)?;
|
||||
}
|
||||
if !already_running && settings.keybinds_enabled && installed.version.is_modern() && !settings.keybinds.is_empty() {
|
||||
options::apply_keybinds(&game_dir, &settings.keybinds).ok();
|
||||
}
|
||||
|
||||
@@ -203,8 +223,9 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
|
||||
}
|
||||
|
||||
let (kill_tx, kill_rx) = tokio::sync::oneshot::channel();
|
||||
*state.game.lock().unwrap() = Some(RunningGame { instance_id: instance_id.clone(), kill: kill_tx });
|
||||
emit_state(&app, &instance_id, "running", None);
|
||||
let run_id = uuid::Uuid::new_v4().to_string();
|
||||
state.game.lock().unwrap().push(RunningGame { run_id: run_id.clone(), instance_id: instance_id.clone(), kill: Some(kill_tx) });
|
||||
app.emit("launch://state", StatePayload { instance_id: instance_id.clone(), run_id: Some(run_id.clone()), state: "running".into(), message: None }).ok();
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
match settings.after_launch.as_str() {
|
||||
@@ -241,6 +262,7 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
|
||||
|
||||
let app2 = app.clone();
|
||||
let id2 = instance_id.clone();
|
||||
let run_id2 = run_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut tail: VecDeque<String> = VecDeque::with_capacity(200);
|
||||
let mut batch: Vec<String> = Vec::new();
|
||||
@@ -297,15 +319,22 @@ pub async fn run(app: AppHandle, instance_id: String, start: bool, deep: bool) -
|
||||
},
|
||||
&id2,
|
||||
);
|
||||
*st.game.lock().unwrap() = None;
|
||||
if let Some(w) = app2.get_webview_window("main") {
|
||||
w.show().ok();
|
||||
w.unminimize().ok();
|
||||
if crashed {
|
||||
w.set_focus().ok();
|
||||
let preferences = options::read_vanilla_preferences(&st.layout.instance_dir(&id2)).unwrap_or_default();
|
||||
if !preferences.is_empty() {
|
||||
st.settings.write().unwrap().instance_game_options.insert(id2.clone(), preferences);
|
||||
st.save_settings().ok();
|
||||
}
|
||||
st.game.lock().unwrap().retain(|g| g.run_id != run_id2);
|
||||
if st.game.lock().unwrap().is_empty() {
|
||||
if let Some(w) = app2.get_webview_window("main") {
|
||||
w.show().ok();
|
||||
w.unminimize().ok();
|
||||
if crashed {
|
||||
w.set_focus().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
app2.emit("game://exit", ExitPayload { instance_id: id2.clone(), code, crashed, tail: tail.into_iter().collect(), crash_report })
|
||||
app2.emit("game://exit", ExitPayload { instance_id: id2.clone(), run_id: run_id2, code, crashed, tail: tail.into_iter().collect(), crash_report })
|
||||
.ok();
|
||||
emit_state(&app2, &id2, "idle", None);
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ pub fn run() {
|
||||
commands::refresh_manifest,
|
||||
commands::set_panel_url,
|
||||
commands::save_settings,
|
||||
commands::save_instance_options,
|
||||
commands::login_panel,
|
||||
commands::register_panel,
|
||||
commands::add_offline,
|
||||
|
||||
@@ -57,6 +57,12 @@ pub struct Settings {
|
||||
// ---- Controls ----
|
||||
pub keybinds_enabled: bool,
|
||||
pub keybinds: BTreeMap<String, String>,
|
||||
pub vanilla_controls_enabled: bool,
|
||||
pub auto_jump: bool,
|
||||
pub sensitivity: f64,
|
||||
pub show_news: bool,
|
||||
/// Vanilla options captured from each instance after the game exits.
|
||||
pub instance_game_options: HashMap<String, BTreeMap<String, String>>,
|
||||
|
||||
pub instance_overrides: HashMap<String, InstanceOverride>,
|
||||
}
|
||||
@@ -88,6 +94,11 @@ impl Default for Settings {
|
||||
send_stats: true,
|
||||
keybinds_enabled: false,
|
||||
keybinds: BTreeMap::new(),
|
||||
vanilla_controls_enabled: false,
|
||||
auto_jump: true,
|
||||
sensitivity: 0.5,
|
||||
show_news: true,
|
||||
instance_game_options: HashMap::new(),
|
||||
instance_overrides: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ pub mod build {
|
||||
}
|
||||
|
||||
pub struct RunningGame {
|
||||
pub run_id: String,
|
||||
pub instance_id: String,
|
||||
pub kill: tokio::sync::oneshot::Sender<()>,
|
||||
pub kill: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
@@ -35,7 +36,7 @@ pub struct AppState {
|
||||
pub manifest: RwLock<Option<LauncherManifest>>,
|
||||
/// Install/launch pipeline in progress (abortable).
|
||||
pub task: Mutex<Option<tokio::task::AbortHandle>>,
|
||||
pub game: Mutex<Option<RunningGame>>,
|
||||
pub game: Mutex<Vec<RunningGame>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -52,7 +53,7 @@ impl AppState {
|
||||
accounts: RwLock::new(accounts),
|
||||
manifest: RwLock::new(manifest),
|
||||
task: Mutex::new(None),
|
||||
game: Mutex::new(None),
|
||||
game: Mutex::new(Vec::new()),
|
||||
data_dir,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user