use std::collections::{HashMap, HashSet}; use std::sync::Arc; use oneclient_cluster::naming::{MAX_NAME_CHARS, validate_instance_name}; use oneclient_common::domain::GameLoader; use oneclient_content::packages::ProviderId; use oneclient_core::clusters::ClusterKind; use oneclient_core::{BundleArchive, GameVersionKind}; use super::model::*; use super::package_names; use crate::hooks::{ GameVersion, available_bundles, game_versions, java_majors, loader_versions, package_meta_batch, query_error, settled_or_loading, use_available_bundles, use_game_versions, use_java_majors, use_loader_versions, use_package_meta_batch, use_version_loaders, use_versions, versions_metadata, }; use crate::utils::version_sort_key; pub struct VersionPicks { pub chosen: Option, pub list: VersionList, pub loaded: bool, pub error: Option, pub filter: u8, } pub struct LoaderPicks { pub chosen: Option, pub available: Option>, pub version: Option, pub versions: Arc<[String]>, } pub struct BundlePicks { pub archives: Arc<[BundleArchive]>, pub loaded: bool, pub names: HashMap, pub declined: HashSet, } impl BundlePicks { pub fn taken(&self) -> Vec { self.names_of_taken().collect() } pub fn taken_count(&self) -> usize { self.names_of_taken().count() } fn names_of_taken(&self) -> impl Iterator + '_ { self.archives .iter() .map(|archive| archive.manifest.name.clone()) .filter(|name| !self.declined.contains(name)) } } pub struct Picks { pub steps: &'static [Step], pub index: usize, pub step: Step, pub choice: TypeChoice, pub modpack_origin: ModpackOrigin, pub kind: ClusterKind, pub name: String, pub suggested: String, pub versions: VersionPicks, pub loader: LoaderPicks, pub bundles: BundlePicks, } impl Picks { pub fn loader_label(&self) -> String { match (self.loader.chosen, self.loader.version.as_deref()) { (None | Some(GameLoader::Vanilla), _) => "Vanilla".to_string(), (Some(loader), Some(version)) => format!("{loader} {version}"), (Some(loader), None) => loader.to_string(), } } } fn matches_filter(kind: GameVersionKind, filter: u8) -> bool { filter & kind_bit(kind) != 0 } struct OneClientVersion { id: String, loader: GameLoader, } struct Catalogue { oneclient: Vec, oneclient_settled: bool, vanilla: Option>, java: HashMap, error: Option, } impl Catalogue { fn loader_for(&self, version: &str) -> Option { self.oneclient .iter() .find(|entry| entry.id == version) .map(|entry| entry.loader) } fn lists(&self, version: &str) -> bool { self.oneclient.iter().any(|entry| entry.id == version) } } fn use_catalogue(choice: TypeChoice) -> Catalogue { let oneclient_query = use_versions(); let vanilla_query = use_game_versions(); let metadata = versions_metadata(&oneclient_query); let mut oneclient: Vec = metadata .clone() .unwrap_or_default() .into_iter() .filter_map(|entry| { Some(OneClientVersion { id: oneclient_common::version::format_mc_version( entry.major_version, entry.minor_version?, entry.patch_version, ), loader: entry.loader.as_deref()?.parse().ok()?, }) }) .collect(); oneclient.sort_by_key(|entry| std::cmp::Reverse(version_sort_key(&entry.id))); let java_query = use_java_majors(if choice == TypeChoice::OneClient { oneclient.iter().map(|entry| entry.id.clone()).collect() } else { Vec::new() }); let error = if choice == TypeChoice::OneClient { query_error(&oneclient_query) } else { query_error(&vanilla_query) }; Catalogue { oneclient, oneclient_settled: metadata.is_some(), vanilla: game_versions(&vanilla_query), java: java_majors(&java_query), error, } } struct BundleContext { archives: Arc<[BundleArchive]>, settled: bool, names: HashMap, } fn use_bundle_context( version: Option<&String>, loader: Option, takes_bundles: bool, wants_names: bool, ) -> BundleContext { let key = match (takes_bundles, version) { (true, Some(version)) => version.clone(), _ => String::new(), }; let query = use_available_bundles(key, loader.unwrap_or(GameLoader::Vanilla)); let bundles = available_bundles(&query); let archives = bundles.clone().unwrap_or_else(|| Arc::from([])); let mut modrinth_ids = Vec::new(); let mut curseforge_ids = Vec::new(); for file in archives.iter().flat_map(|archive| &archive.manifest.files) { match file.kind.metadata_provider() { ProviderId::Modrinth => modrinth_ids.push(file.kind.package_id()), ProviderId::CurseForge => curseforge_ids.push(file.kind.package_id()), ProviderId::Local => {} } } let modrinth = use_package_meta_batch(ProviderId::Modrinth, modrinth_ids); let curseforge = use_package_meta_batch(ProviderId::CurseForge, curseforge_ids); BundleContext { names: if wants_names { package_names( &archives, &package_meta_batch(&modrinth), &package_meta_batch(&curseforge), ) } else { HashMap::new() }, archives, settled: bundles.is_some(), } } fn build_version_list( choice: TypeChoice, catalogue: &Catalogue, filter: u8, needle: &str, ) -> VersionList { let matches_needle = |id: &str| needle.is_empty() || id.to_lowercase().contains(needle); if choice == TypeChoice::OneClient { return VersionList::Curated( catalogue .oneclient .iter() .filter(|entry| matches_needle(&entry.id)) .map(|entry| VersionRow { badge: catalogue .java .get(&entry.id) .map(|major| format!("Java {major}")), id: entry.id.clone(), meta: entry.loader.to_string(), date: String::new(), }) .collect(), ); } let all = catalogue.vanilla.clone().unwrap_or_else(|| Arc::from([])); let visible = all .iter() .enumerate() .filter(|(_, entry)| matches_filter(entry.kind, filter)) .filter(|(_, entry)| matches_needle(&entry.id)) .map(|(index, _)| index as u32) .collect(); VersionList::Catalogue { all, visible } } fn default_version(choice: TypeChoice, catalogue: &Catalogue, filter: u8) -> Option { if choice == TypeChoice::OneClient { return catalogue.oneclient.first().map(|entry| entry.id.clone()); } let all = catalogue.vanilla.as_deref().unwrap_or(&[]); let mut scoped = all .iter() .filter(|entry| matches_filter(entry.kind, filter)); let first = scoped.clone().next(); scoped .find(|entry| entry.kind.is_release()) .or(first) .map(|entry| entry.id.clone()) } fn kind_for(choice: TypeChoice, loader: Option) -> ClusterKind { match (choice, loader) { (TypeChoice::OneClient, _) => ClusterKind::OneClient, (TypeChoice::Scratch, Some(GameLoader::Vanilla) | None) => ClusterKind::Vanilla, (TypeChoice::Scratch, Some(_)) | (TypeChoice::Modpack, _) => ClusterKind::Modded, } } fn suggested_name( version: Option<&String>, kind: ClusterKind, loader: Option, ) -> String { let Some(version) = version else { return String::new(); }; let full = match (kind, loader) { (ClusterKind::Vanilla, _) => format!("{version} Vanilla"), (_, Some(loader)) => format!("{version} {loader}"), (_, None) => version.clone(), }; if validate_instance_name(&full).is_ok() { return full; } version .chars() .take(MAX_NAME_CHARS) .collect::() .trim() .to_string() } pub fn resolve(w: Wizard) -> Picks { let choice = *w.choice.read(); let steps = step_order(choice); let index = (*w.step.read()).min(steps.len().saturating_sub(1)); let step = steps[index]; let catalogue = use_catalogue(choice); let filter = *w.filter.read(); let needle = w.query.read().trim().to_lowercase(); let versions = build_version_list(choice, &catalogue, filter, &needle); let oneclient = choice == TypeChoice::OneClient; let versions_settled = if oneclient { catalogue.oneclient_settled } else { catalogue.vanilla.is_some() }; let versions_error = catalogue.error.clone(); let still_offered = |chosen: &String| { if oneclient { catalogue.lists(chosen) } else { catalogue .vanilla .iter() .flat_map(|all| all.iter()) .any(|entry: &GameVersion| { &entry.id == chosen && matches_filter(entry.kind, filter) }) } }; let version = (w.version.read().clone()) .filter(still_offered) .or_else(|| default_version(choice, &catalogue, filter)); let available_query = use_version_loaders(version.clone().unwrap_or_default()); let available = settled_or_loading(&available_query); let loader = match choice { TypeChoice::OneClient => version.as_ref().and_then(|id| catalogue.loader_for(id)), TypeChoice::Scratch | TypeChoice::Modpack => { w.loader.read().resolve(available.as_deref().unwrap_or(&[])) } }; let loader_versions = loader_versions(&use_loader_versions( version.clone().unwrap_or_default(), loader.unwrap_or(GameLoader::Vanilla), )); let loader_version = match (choice, loader) { (TypeChoice::Scratch, Some(loader)) if loader != GameLoader::Vanilla => { (w.loader_version.read().clone()) .filter(|chosen| loader_versions.contains(chosen)) .or_else(|| loader_versions.first().cloned()) } _ => None, }; let bundles = use_bundle_context(version.as_ref(), loader, oneclient, step == Step::Bundles); let kind = kind_for(choice, loader); let suggested = suggested_name(version.as_ref(), kind, loader); let declined = w.declined.read().clone().unwrap_or_else(|| { bundles .archives .iter() .filter(|archive| !archive.manifest.enabled) .map(|archive| archive.manifest.name.clone()) .collect() }); Picks { steps, index, step, choice, modpack_origin: *w.modpack_origin.read(), kind, name: w.details.effective_name(&suggested), suggested, versions: VersionPicks { chosen: version, list: versions, loaded: versions_settled, error: versions_error, filter, }, loader: LoaderPicks { chosen: loader, available, version: loader_version, versions: loader_versions, }, bundles: BundlePicks { archives: bundles.archives, loaded: bundles.settled, names: bundles.names, declined, }, } }