use std::collections::HashMap;
use nitro_shared::Side;
use nitro_shared::java_args::MemoryNum;
use nitro_shared::loaders::Loader;
use nitro_shared::pkg::{PackageOverrides, PackageStability};
use nitro_shared::util::{DefaultExt, DeserListOrSingle, merge_options};
use nitro_shared::versions::MinecraftVersionDeser;
#[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 launch: LaunchConfig,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub window: ClientWindowConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub modpack: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub packages: Vec<PackageConfigDeser>,
#[serde(skip_serializing_if = "Option::is_none")]
pub datapack_folder: Option<String>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub package_stability: Option<PackageStability>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub overrides: PackageOverrides,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "game_dir")]
pub dir: Option<String>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub source_plugin: Option<String>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub is_editable: bool,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub is_deletable: bool,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub custom_launch: bool,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub custom_logging_plugin: Option<String>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub imported: bool,
#[serde(skip_serializing_if = "serde_json::Map::is_empty")]
#[serde(flatten)]
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.merge(other.overrides);
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.dir = other.dir;
self.source_plugin = other.source_plugin;
self.is_editable = other.is_editable;
self.is_deletable = other.is_deletable;
self.custom_launch = other.custom_launch;
self.imported = other.imported;
}
pub fn remove_plugin_only_fields(&mut self) {
self.source_plugin = None;
self.is_editable = false;
self.is_deletable = false;
self.custom_launch = false;
self.custom_logging_plugin = None;
}
pub fn restore_plugin_only_fields(&mut self, original_config: &Self) {
self.source_plugin = original_config.source_plugin.clone();
self.is_editable = original_config.is_editable;
self.is_deletable = original_config.is_deletable;
self.custom_launch = original_config.custom_launch;
}
}
#[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)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quickplay_deser() {
#[derive(Deserialize)]
struct Test {
quick_play: QuickPlay,
}
let test = serde_json::from_str::<Test>(
r#"{
"quick_play": {
"type": "server",
"server": "localhost",
"port": 25565,
"world": "test",
"realm": "my_realm"
}
}"#,
)
.unwrap();
assert_eq!(
test.quick_play,
QuickPlay::Server {
server: "localhost".into(),
port: Some(25565)
}
);
}
}