use std::collections::HashMap;
use mcvm_core::util::versions::MinecraftVersionDeser;
use mcvm_shared::modifications::{ClientType, Modloader, ServerType};
use mcvm_shared::pkg::PackageStability;
use mcvm_shared::util::{merge_options, DefaultExt, DeserListOrSingle};
use mcvm_shared::versions::VersionPattern;
use mcvm_shared::Side;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::package::PackageConfigDeser;
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct InstanceConfig {
#[serde(rename = "type")]
pub side: Option<Side>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(flatten)]
pub common: CommonInstanceConfig,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub window: ClientWindowConfig,
}
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct CommonInstanceConfig {
#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
pub from: DeserListOrSingle<String>,
pub version: Option<MinecraftVersionDeser>,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub modloader: Option<Modloader>,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub client_type: Option<ClientType>,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub server_type: Option<ServerType>,
#[serde(default)]
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub game_modification_version: Option<VersionPattern>,
#[serde(default)]
#[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(flatten)]
#[serde(skip_serializing_if = "serde_json::Map::is_empty")]
pub plugin_config: serde_json::Map<String, serde_json::Value>,
}
impl CommonInstanceConfig {
pub fn merge(&mut self, other: Self) -> &mut Self {
self.from.merge(other.from);
self.version = other.version.or(self.version.clone());
self.modloader = other.modloader.or(self.modloader.clone());
self.client_type = other.client_type.or(self.client_type.clone());
self.server_type = other.server_type.or(self.server_type.clone());
self.game_modification_version = other.game_modification_version.or(self.game_modification_version.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);
mcvm_core::util::json::merge_objects(&mut self.plugin_config, other.plugin_config);
self
}
}
#[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,
},
}
fn default_java() -> String {
"auto".into()
}
fn default_flags_preset() -> String {
"none".into()
}
#[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 = "default_java")]
pub java: String,
#[serde(default = "default_flags_preset")]
pub preset: 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;
}
self.java = other.java;
if other.preset != "none" {
self.preset = other.preset;
}
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: default_java(),
preset: default_flags_preset(),
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 merge_instance_configs(preset: &InstanceConfig, config: InstanceConfig) -> InstanceConfig {
let mut out = preset.clone();
out.common.merge(config.common);
out.name = config.name.or(out.name);
out.icon = config.icon.or(out.icon);
out.side = config.side.or(out.side);
out.window.merge(config.window);
out
}
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
}
#[derive(Clone, Debug)]
pub struct GameModifications {
modloader: Modloader,
client_type: ClientType,
server_type: ServerType,
}
impl GameModifications {
pub fn new(modloader: Modloader, client_type: ClientType, server_type: ServerType) -> Self {
Self {
modloader,
client_type,
server_type,
}
}
pub fn client_type(&self) -> ClientType {
if let ClientType::None = self.client_type {
match &self.modloader {
Modloader::Vanilla => ClientType::Vanilla,
Modloader::Forge => ClientType::Forge,
Modloader::NeoForged => ClientType::NeoForged,
Modloader::Fabric => ClientType::Fabric,
Modloader::Quilt => ClientType::Quilt,
Modloader::LiteLoader => ClientType::LiteLoader,
Modloader::Risugamis => ClientType::Risugamis,
Modloader::Rift => ClientType::Rift,
Modloader::Unknown(modloader) => ClientType::Unknown(modloader.clone()),
}
} else {
self.client_type.clone()
}
}
pub fn server_type(&self) -> ServerType {
if let ServerType::None = self.server_type {
match &self.modloader {
Modloader::Vanilla => ServerType::Vanilla,
Modloader::Forge => ServerType::Forge,
Modloader::NeoForged => ServerType::NeoForged,
Modloader::Fabric => ServerType::Fabric,
Modloader::Quilt => ServerType::Quilt,
Modloader::LiteLoader => ServerType::Unknown("liteloader".into()),
Modloader::Risugamis => ServerType::Risugamis,
Modloader::Rift => ServerType::Rift,
Modloader::Unknown(modloader) => ServerType::Unknown(modloader.clone()),
}
} else {
self.server_type.clone()
}
}
pub fn get_modloader(&self, side: Side) -> Modloader {
match side {
Side::Client => match self.client_type {
ClientType::None => self.modloader.clone(),
ClientType::Vanilla => Modloader::Vanilla,
ClientType::Forge => Modloader::Forge,
ClientType::NeoForged => Modloader::NeoForged,
ClientType::Fabric => Modloader::Fabric,
ClientType::Quilt => Modloader::Quilt,
ClientType::LiteLoader => Modloader::LiteLoader,
ClientType::Risugamis => Modloader::Risugamis,
ClientType::Rift => Modloader::Rift,
_ => Modloader::Vanilla,
},
Side::Server => match self.server_type {
ServerType::None => self.modloader.clone(),
ServerType::Forge | ServerType::SpongeForge => Modloader::Forge,
ServerType::NeoForged => Modloader::NeoForged,
ServerType::Fabric => Modloader::Fabric,
ServerType::Quilt => Modloader::Quilt,
ServerType::Risugamis => Modloader::Risugamis,
ServerType::Rift => Modloader::Rift,
_ => Modloader::Vanilla,
},
}
}
pub fn common_modloader(&self) -> bool {
matches!(
(&self.client_type, &self.server_type),
(ClientType::None, ServerType::None)
| (ClientType::Vanilla, ServerType::Vanilla)
| (ClientType::Forge, ServerType::Forge)
| (ClientType::NeoForged, ServerType::NeoForged)
| (ClientType::Fabric, ServerType::Fabric)
| (ClientType::Quilt, ServerType::Quilt)
| (ClientType::Risugamis, ServerType::Risugamis)
| (ClientType::Rift, ServerType::Rift)
)
}
}
pub fn can_install_client_type(client_type: &ClientType) -> bool {
matches!(client_type, ClientType::None | ClientType::Vanilla)
}
pub fn can_install_server_type(server_type: &ServerType) -> bool {
matches!(
server_type,
ServerType::None
| ServerType::Vanilla
| ServerType::Paper
| ServerType::Folia
| ServerType::Sponge
)
}