use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::Context;
use nitro_pkg::overrides::PackageOverrides;
use nitro_shared::addon::AddonKind;
use nitro_shared::java_args::MemoryNum;
use nitro_shared::loaders::Loader;
use nitro_shared::pkg::PackageStability;
use nitro_shared::util::{merge_options, DefaultExt, DeserListOrSingle};
use nitro_shared::versions::{MinecraftVersionDeser, VersionInfo, VersionPattern};
use nitro_shared::Side;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::package::PackageConfigDeser;
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct InstanceConfig {
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub from: DeserListOrSingle<String>,
#[serde(rename = "type")]
pub side: Option<Side>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<MinecraftVersionDeser>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub loader: Option<String>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub package_stability: Option<PackageStability>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub launch: LaunchConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub datapack_folder: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub packages: Vec<PackageConfigDeser>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub overrides: PackageOverrides,
#[serde(skip_serializing_if = "Option::is_none")]
pub game_dir: Option<String>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub window: ClientWindowConfig,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub from_plugin: bool,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub custom_launch: bool,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub imported: bool,
#[serde(skip_serializing_if = "serde_json::Map::is_empty")]
pub plugin_config: serde_json::Map<String, serde_json::Value>,
}
impl InstanceConfig {
pub fn merge(&mut self, other: Self) {
self.from.merge(other.from);
if other.name.is_some() {
self.name = other.name;
}
self.version = other.version.or(self.version.clone());
self.loader = other.loader.or(self.loader.clone());
self.package_stability = other.package_stability.or(self.package_stability);
self.launch.merge(other.launch);
self.datapack_folder = other.datapack_folder.or(self.datapack_folder.clone());
self.packages.extend(other.packages);
self.overrides.suppress.extend(other.overrides.suppress);
nitro_shared::util::merge_json_objects(&mut self.plugin_config, other.plugin_config);
self.icon = other.icon.or(self.icon.clone());
self.side = other.side.or(self.side);
self.window.merge(other.window);
self.from_plugin = other.from_plugin;
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum Args {
List(Vec<String>),
String(String),
}
impl Args {
pub fn parse(&self) -> Vec<String> {
match self {
Self::List(vec) => vec.clone(),
Self::String(string) => string.split(' ').map(|string| string.to_string()).collect(),
}
}
pub fn merge(&mut self, other: Self) {
let mut out = self.parse();
out.extend(other.parse());
*self = Self::List(out);
}
}
impl Default for Args {
fn default() -> Self {
Self::List(Vec::new())
}
}
#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct LaunchArgs {
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub jvm: Args,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub game: Args,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum LaunchMemory {
#[default]
None,
Single(String),
Both {
min: String,
max: String,
},
}
impl LaunchMemory {
pub fn to_min_max(self) -> (Option<MemoryNum>, Option<MemoryNum>) {
let min_mem = match &self {
LaunchMemory::None => None,
LaunchMemory::Single(string) => MemoryNum::parse(string),
LaunchMemory::Both { min, .. } => MemoryNum::parse(min),
};
let max_mem = match &self {
LaunchMemory::None => None,
LaunchMemory::Single(string) => MemoryNum::parse(string),
LaunchMemory::Both { max, .. } => MemoryNum::parse(max),
};
(min_mem, max_mem)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum QuickPlay {
World {
world: String,
},
Server {
server: String,
port: Option<u16>,
},
Realm {
realm: String,
},
#[default]
None,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct LaunchConfig {
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub args: LaunchArgs,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub memory: LaunchMemory,
#[serde(default)]
pub java: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub env: HashMap<String, String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub wrapper: Option<WrapperCommand>,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub quick_play: QuickPlay,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub use_log4j_config: bool,
}
impl LaunchConfig {
pub fn merge(&mut self, other: Self) -> &mut Self {
self.args.jvm.merge(other.args.jvm);
self.args.game.merge(other.args.game);
if !matches!(other.memory, LaunchMemory::None) {
self.memory = other.memory;
}
if other.java.is_some() {
self.java = other.java;
}
self.env.extend(other.env);
if other.wrapper.is_some() {
self.wrapper = other.wrapper;
}
if !matches!(other.quick_play, QuickPlay::None) {
self.quick_play = other.quick_play;
}
self
}
}
impl Default for LaunchConfig {
fn default() -> Self {
Self {
args: LaunchArgs {
jvm: Args::default(),
game: Args::default(),
},
memory: LaunchMemory::default(),
java: None,
env: HashMap::new(),
wrapper: None,
quick_play: QuickPlay::default(),
use_log4j_config: false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct WrapperCommand {
pub cmd: String,
#[serde(default)]
pub args: Vec<String>,
}
#[derive(Deserialize, Serialize, Clone, Debug, Copy, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct WindowResolution {
pub width: u32,
pub height: u32,
}
#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct ClientWindowConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<WindowResolution>,
}
impl ClientWindowConfig {
pub fn merge(&mut self, other: Self) -> &mut Self {
self.resolution = merge_options(self.resolution, other.resolution);
self
}
}
pub fn is_valid_instance_id(id: &str) -> bool {
for c in id.chars() {
if !c.is_ascii() {
return false;
}
if c.is_ascii_punctuation() {
match c {
'_' | '-' | '.' | ':' => {}
_ => return false,
}
}
if c.is_ascii_whitespace() {
return false;
}
}
true
}
pub fn make_valid_instance_id(string: &str) -> String {
let string = string.to_lowercase();
string
.chars()
.map(|c| {
if !c.is_ascii_alphanumeric() && c != '.' && c != ':' {
'-'
} else {
c
}
})
.collect()
}
pub fn can_install_loader(loader: &Loader) -> bool {
matches!(loader, Loader::Vanilla)
}
pub fn get_addon_paths(
instance: &InstanceConfig,
game_dir: &Path,
addon: AddonKind,
selected_worlds: &[String],
version_info: &VersionInfo,
) -> anyhow::Result<Vec<PathBuf>> {
let side = instance.side.context("Instance side missing")?;
Ok(match addon {
AddonKind::ResourcePack => {
if side == Side::Client {
if VersionPattern::After("13w24a".into()).matches_info(version_info) {
vec![game_dir.join("resourcepacks")]
} else {
vec![game_dir.join("texturepacks")]
}
} else {
vec![game_dir.join("resourcepacks")]
}
}
AddonKind::Mod => vec![game_dir.join("mods")],
AddonKind::Plugin => {
if side == Side::Server {
vec![game_dir.join("plugins")]
} else {
vec![]
}
}
AddonKind::Shader => {
if side == Side::Client {
vec![game_dir.join("shaderpacks")]
} else {
vec![]
}
}
AddonKind::Datapack => {
if let Some(datapack_folder) = &instance.datapack_folder {
vec![game_dir.join(datapack_folder)]
} else {
match side {
Side::Client => {
if selected_worlds.is_empty() {
vec![game_dir.join("world_files/datapacks")]
} else {
selected_worlds
.iter()
.map(|x| game_dir.join("saves").join(x).join("datapacks"))
.collect()
}
}
Side::Server => {
vec![game_dir.join("world").join("datapacks")]
}
}
}
}
})
}