From 365fc81c0319dd6adf461eacf37cc324e927c981 Mon Sep 17 00:00:00 2001 From: Toasterkitten Date: Wed, 29 Jul 2026 16:54:49 -0400 Subject: [PATCH] overlays! simple --- src/auth.rs | 6 ++--- src/main.rs | 36 +++++++++++++++++++++++--- src/overlay.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index eaf87ce..155c016 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -129,7 +129,7 @@ fn poll(agent: &Agent, device: &Device) -> anyhow::Result<(String, String)> { if let Some(token) = body.access_token { let refresh = body.refresh_token.ok_or_else(|| { - anyhow::anyhow!("no refresh token — was offline_access in the scope?") + anyhow::anyhow!("no refresh token, was offline_access in the scope?") })?; return Ok((token, refresh)); } @@ -176,10 +176,10 @@ fn xsts(agent: &Agent, xbl_token: &str) -> anyhow::Result { anyhow::bail!( "{}", match e.xerr { - 2148916233 => "no Xbox profile — sign in at minecraft.net once".into(), + 2148916233 => "no Xbox profile, sign in at minecraft.net once".into(), 2148916235 => "Xbox Live unavailable in this region".into(), 2148916237 => "account needs adult verification".into(), - 2148916238 => "child account — must be added to a Family".into(), + 2148916238 => "child account, must be added to a Family".into(), other => format!("XSTS failed, XErr {other}"), } ); diff --git a/src/main.rs b/src/main.rs index 054b71a..0999add 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ mod util; use clap::{Parser, Subcommand}; use launch::run; use meta::fetch_version; -use modrinth::{download_mod, get_version}; use std::path::{Path, PathBuf}; use util::{is_valid_name, open_path}; @@ -52,7 +51,12 @@ enum ModVerb { item: String, }, } - +#[derive(Subcommand)] +enum OverlayCmd { + /// Apply an overlay + Apply { overlay: String, instance: String }, + // add List, Create, Add, Remove, Unapply, Strip +} #[derive(Subcommand)] enum Command { /// Launch an instance @@ -70,6 +74,11 @@ enum Command { #[command(subcommand)] action: InstanceAction, }, + // Manage overlays + Overlays { + #[command(subcommand)] + cmd: OverlayCmd, + }, /// Manage instances Instances { #[command(subcommand)] @@ -80,6 +89,7 @@ enum Command { } #[derive(Subcommand)] enum InstanceAction { + /// Manage instance mods Mods { #[command(subcommand)] verb: ModVerb, @@ -144,8 +154,8 @@ fn data_dir() -> anyhow::Result { fn main() -> anyhow::Result<()> { let uml_dir = data_dir()?; let instances_dir = uml_dir.join("instances"); + let overlays_dir = uml_dir.join("overlays"); let shared_dir = uml_dir.join("shared"); - let cli = Cli::parse(); match cli.command { Command::Folder { name } => { @@ -290,6 +300,26 @@ fn main() -> anyhow::Result<()> { }, } } + Command::Overlays { cmd } => match cmd { + OverlayCmd::Apply { overlay, instance } => { + if !is_valid_name(&instance) { + anyhow::bail!("invalid instance name: {instance:?}"); + } + if !is_valid_name(&overlay) { + anyhow::bail!("invalid overlay name: {overlay:?}"); + } + let mods_dir = instances_dir.join(&instance).join("mods"); + let (cfg, _) = instance::load(&instances_dir, &instance)?; + let loader = cfg + .loader + .as_ref() + .map(|l| l.kind.as_str()) + .ok_or_else(|| { + anyhow::anyhow!("instance has no loader, overlays need a modded instance") + })?; + overlay::apply(&overlays_dir, &overlay, &mods_dir, loader, &cfg.version)?; + } + }, } Ok(()) } diff --git a/src/overlay.rs b/src/overlay.rs index e69de29..407ff6a 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -0,0 +1,70 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::{content, modrinth}; +use indicatif::{ProgressBar, ProgressStyle}; + +#[derive(Serialize, Deserialize)] +pub struct Overlay { + pub name: String, + pub mods: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct OverlayMod { + pub source: String, + pub id: String, + #[serde(default)] + pub pin: Option, // NOT IMPLEMENTED YET +} +pub fn load(overlays_dir: &Path, name: &str) -> anyhow::Result { + let path = overlays_dir.join(format!("{name}.umloverlay")); + if !path.exists() { + anyhow::bail!("no overlay named {name}"); + } + let json = std::fs::read_to_string(path)?; + Ok(serde_json::from_str(&json)?) +} + +pub fn save(overlays_dir: &Path, overlay: &Overlay) -> anyhow::Result<()> { + let path = overlays_dir.join(format!("{}.umloverlay", overlay.name)); + let json = serde_json::to_string_pretty(overlay)?; + std::fs::write(path, json)?; + Ok(()) +} +pub fn apply( + overlays_dir: &Path, + name: &str, + mods_dir: &Path, + loader: &str, + game_version: &str, +) -> anyhow::Result<()> { + let overlay = load(overlays_dir, name)?; + let existing = content::list(mods_dir)?; + let all_present = overlay + .mods + .iter() + .all(|m| existing.iter().any(|e| e.id == m.id)); + if all_present { + println!( + "Overlay '{}' is already fully applied, nothing to do.", + overlay.name + ); + return Ok(()); + } + let pb = ProgressBar::new(overlay.mods.len() as u64); + pb.set_style(ProgressStyle::with_template("{bar:40} {pos}/{len} {msg}").unwrap()); + for m in overlay.mods { + if existing.iter().any(|e| e.id == m.id) { + println!("Skipping {} (already installed).", m.id); + continue; + } + pb.println(format!("Downloading {}...", m.id)); // not println! + let file = modrinth::get_version(&m.id, loader, game_version)?; + modrinth::download_mod(&file, &mods_dir)?; + pb.inc(1); + } + pb.finish_with_message("done"); + Ok(()) +}