Skip to main content

mcvm_config/
instance.rs

1use std::collections::HashMap;
2
3use mcvm_core::util::versions::MinecraftVersionDeser;
4use mcvm_shared::modifications::{ClientType, Modloader, ServerType};
5use mcvm_shared::pkg::PackageStability;
6use mcvm_shared::util::{merge_options, DefaultExt, DeserListOrSingle};
7use mcvm_shared::versions::VersionPattern;
8use mcvm_shared::Side;
9#[cfg(feature = "schema")]
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12
13use super::package::PackageConfigDeser;
14
15/// Configuration for an instance
16#[derive(Deserialize, Serialize, Clone, Debug, Default)]
17#[cfg_attr(feature = "schema", derive(JsonSchema))]
18pub struct InstanceConfig {
19	/// The type or side of this instance
20	#[serde(rename = "type")]
21	pub side: Option<Side>,
22	/// The display name of this instance
23	#[serde(default)]
24	#[serde(skip_serializing_if = "Option::is_none")]
25	pub name: Option<String>,
26	/// A path to an icon file for this instance
27	#[serde(default)]
28	#[serde(skip_serializing_if = "Option::is_none")]
29	pub icon: Option<String>,
30	/// The common config of this instance
31	#[serde(flatten)]
32	pub common: CommonInstanceConfig,
33	/// Window configuration
34	#[serde(default)]
35	#[serde(skip_serializing_if = "DefaultExt::is_default")]
36	pub window: ClientWindowConfig,
37}
38
39/// Common full instance config for both client and server
40#[derive(Deserialize, Serialize, Clone, Default, Debug)]
41#[cfg_attr(feature = "schema", derive(JsonSchema))]
42#[serde(default)]
43pub struct CommonInstanceConfig {
44	/// A profile to use
45	#[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
46	pub from: DeserListOrSingle<String>,
47	/// The Minecraft version
48	pub version: Option<MinecraftVersionDeser>,
49	/// Configured modloader
50	#[serde(default)]
51	#[serde(skip_serializing_if = "DefaultExt::is_default")]
52	pub modloader: Option<Modloader>,
53	/// Configured client type
54	#[serde(default)]
55	#[serde(skip_serializing_if = "DefaultExt::is_default")]
56	pub client_type: Option<ClientType>,
57	/// Configured server type
58	#[serde(default)]
59	#[serde(skip_serializing_if = "DefaultExt::is_default")]
60	pub server_type: Option<ServerType>,
61	/// The version of whatever game modification is applied to this instance
62	#[serde(default)]
63	#[serde(skip_serializing_if = "DefaultExt::is_default")]
64	pub game_modification_version: Option<VersionPattern>,
65	/// Default stability setting of packages on this instance
66	#[serde(default)]
67	#[serde(skip_serializing_if = "DefaultExt::is_default")]
68	pub package_stability: Option<PackageStability>,
69	/// Launch configuration
70	#[serde(skip_serializing_if = "DefaultExt::is_default")]
71	pub launch: LaunchConfig,
72	/// The folder for global datapacks to be installed to
73	#[serde(skip_serializing_if = "Option::is_none")]
74	pub datapack_folder: Option<String>,
75	/// Packages for this instance
76	#[serde(skip_serializing_if = "Vec::is_empty")]
77	pub packages: Vec<PackageConfigDeser>,
78	/// Config for plugins
79	#[serde(flatten)]
80	#[serde(skip_serializing_if = "serde_json::Map::is_empty")]
81	pub plugin_config: serde_json::Map<String, serde_json::Value>,
82}
83
84impl CommonInstanceConfig {
85	/// Merge multiple common configs
86	pub fn merge(&mut self, other: Self) -> &mut Self {
87		self.from.merge(other.from);
88		self.version = other.version.or(self.version.clone());
89		self.modloader = other.modloader.or(self.modloader.clone());
90		self.client_type = other.client_type.or(self.client_type.clone());
91		self.server_type = other.server_type.or(self.server_type.clone());
92		self.game_modification_version = other.game_modification_version.or(self.game_modification_version.clone());
93		self.package_stability = other.package_stability.or(self.package_stability);
94		self.launch.merge(other.launch);
95		self.datapack_folder = other.datapack_folder.or(self.datapack_folder.clone());
96		self.packages.extend(other.packages);
97		mcvm_core::util::json::merge_objects(&mut self.plugin_config, other.plugin_config);
98
99		self
100	}
101}
102
103/// Different representations for JVM / game arguments
104#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
105#[cfg_attr(feature = "schema", derive(JsonSchema))]
106#[serde(untagged)]
107pub enum Args {
108	/// A list of separate arguments
109	List(Vec<String>),
110	/// A single string of arguments
111	String(String),
112}
113
114impl Args {
115	/// Parse the arguments into a vector
116	pub fn parse(&self) -> Vec<String> {
117		match self {
118			Self::List(vec) => vec.clone(),
119			Self::String(string) => string.split(' ').map(|string| string.to_string()).collect(),
120		}
121	}
122
123	/// Merge Args
124	pub fn merge(&mut self, other: Self) {
125		let mut out = self.parse();
126		out.extend(other.parse());
127		*self = Self::List(out);
128	}
129}
130
131impl Default for Args {
132	fn default() -> Self {
133		Self::List(Vec::new())
134	}
135}
136
137/// Arguments for the process when launching
138#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)]
139#[cfg_attr(feature = "schema", derive(JsonSchema))]
140pub struct LaunchArgs {
141	/// Arguments for the JVM
142	#[serde(default)]
143	#[serde(skip_serializing_if = "DefaultExt::is_default")]
144	pub jvm: Args,
145	/// Arguments for the game
146	#[serde(default)]
147	#[serde(skip_serializing_if = "DefaultExt::is_default")]
148	pub game: Args,
149}
150
151/// Different representations of both memory arguments for the JVM
152#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)]
153#[cfg_attr(feature = "schema", derive(JsonSchema))]
154#[serde(untagged)]
155pub enum LaunchMemory {
156	/// No memory arguments
157	#[default]
158	None,
159	/// A single memory argument shared for both
160	Single(String),
161	/// Different memory arguments for both
162	Both {
163		/// The minimum memory
164		min: String,
165		/// The maximum memory
166		max: String,
167	},
168}
169
170fn default_java() -> String {
171	"auto".into()
172}
173
174fn default_flags_preset() -> String {
175	"none".into()
176}
177
178/// Options for the Minecraft QuickPlay feature
179#[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)]
180#[cfg_attr(feature = "schema", derive(JsonSchema))]
181#[serde(tag = "type")]
182#[serde(rename_all = "snake_case")]
183pub enum QuickPlay {
184	/// QuickPlay a world
185	World {
186		/// The world to play
187		world: String,
188	},
189	/// QuickPlay a server
190	Server {
191		/// The server address to join
192		server: String,
193		/// The port for the server to connect to
194		port: Option<u16>,
195	},
196	/// QuickPlay a realm
197	Realm {
198		/// The realm name to join
199		realm: String,
200	},
201	/// Don't do any QuickPlay
202	#[default]
203	None,
204}
205
206/// Configuration for the launching of the game
207#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
208#[cfg_attr(feature = "schema", derive(JsonSchema))]
209pub struct LaunchConfig {
210	/// The arguments for the process
211	#[serde(default)]
212	#[serde(skip_serializing_if = "DefaultExt::is_default")]
213	pub args: LaunchArgs,
214	/// JVM memory options
215	#[serde(default)]
216	#[serde(skip_serializing_if = "DefaultExt::is_default")]
217	pub memory: LaunchMemory,
218	/// The java installation to use
219	#[serde(default = "default_java")]
220	pub java: String,
221	/// The preset for flags
222	#[serde(default = "default_flags_preset")]
223	pub preset: String,
224	/// Environment variables
225	#[serde(default)]
226	#[serde(skip_serializing_if = "HashMap::is_empty")]
227	pub env: HashMap<String, String>,
228	/// A wrapper command
229	#[serde(default)]
230	#[serde(skip_serializing_if = "Option::is_none")]
231	pub wrapper: Option<WrapperCommand>,
232	/// QuickPlay options
233	#[serde(default)]
234	#[serde(skip_serializing_if = "DefaultExt::is_default")]
235	pub quick_play: QuickPlay,
236	/// Whether or not to use the Log4J configuration
237	#[serde(default)]
238	#[serde(skip_serializing_if = "DefaultExt::is_default")]
239	pub use_log4j_config: bool,
240}
241
242impl LaunchConfig {
243	/// Merge multiple LaunchConfigs
244	pub fn merge(&mut self, other: Self) -> &mut Self {
245		self.args.jvm.merge(other.args.jvm);
246		self.args.game.merge(other.args.game);
247		if !matches!(other.memory, LaunchMemory::None) {
248			self.memory = other.memory;
249		}
250		self.java = other.java;
251		if other.preset != "none" {
252			self.preset = other.preset;
253		}
254		self.env.extend(other.env);
255		if other.wrapper.is_some() {
256			self.wrapper = other.wrapper;
257		}
258		if !matches!(other.quick_play, QuickPlay::None) {
259			self.quick_play = other.quick_play;
260		}
261
262		self
263	}
264}
265
266impl Default for LaunchConfig {
267	fn default() -> Self {
268		Self {
269			args: LaunchArgs {
270				jvm: Args::default(),
271				game: Args::default(),
272			},
273			memory: LaunchMemory::default(),
274			java: default_java(),
275			preset: default_flags_preset(),
276			env: HashMap::new(),
277			wrapper: None,
278			quick_play: QuickPlay::default(),
279			use_log4j_config: false,
280		}
281	}
282}
283
284/// A wrapper command
285#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
286#[cfg_attr(feature = "schema", derive(JsonSchema))]
287pub struct WrapperCommand {
288	/// The command to run
289	pub cmd: String,
290	/// The command's arguments
291	#[serde(default)]
292	pub args: Vec<String>,
293}
294
295/// Resolution for a client window
296#[derive(Deserialize, Serialize, Clone, Debug, Copy, PartialEq)]
297#[cfg_attr(feature = "schema", derive(JsonSchema))]
298pub struct WindowResolution {
299	/// The width of the window
300	pub width: u32,
301	/// The height of the window
302	pub height: u32,
303}
304
305/// Configuration for the client window
306#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq)]
307#[cfg_attr(feature = "schema", derive(JsonSchema))]
308#[serde(default)]
309pub struct ClientWindowConfig {
310	/// The resolution of the window
311	#[serde(skip_serializing_if = "Option::is_none")]
312	pub resolution: Option<WindowResolution>,
313}
314
315impl ClientWindowConfig {
316	/// Merge two ClientWindowConfigs
317	pub fn merge(&mut self, other: Self) -> &mut Self {
318		self.resolution = merge_options(self.resolution, other.resolution);
319		self
320	}
321}
322
323/// Merge an InstanceConfig with a preset
324///
325/// Some values will be merged while others will have the right side take precendence
326pub fn merge_instance_configs(preset: &InstanceConfig, config: InstanceConfig) -> InstanceConfig {
327	let mut out = preset.clone();
328	out.common.merge(config.common);
329	out.name = config.name.or(out.name);
330	out.icon = config.icon.or(out.icon);
331	out.side = config.side.or(out.side);
332	out.window.merge(config.window);
333
334	out
335}
336
337/// Checks if an instance ID is valid
338pub fn is_valid_instance_id(id: &str) -> bool {
339	for c in id.chars() {
340		if !c.is_ascii() {
341			return false;
342		}
343
344		if c.is_ascii_punctuation() {
345			match c {
346				'_' | '-' | '.' | ':' => {}
347				_ => return false,
348			}
349		}
350
351		if c.is_ascii_whitespace() {
352			return false;
353		}
354	}
355
356	true
357}
358
359/// Game modifications
360#[derive(Clone, Debug)]
361pub struct GameModifications {
362	modloader: Modloader,
363	/// Type of the client
364	client_type: ClientType,
365	/// Type of the server
366	server_type: ServerType,
367}
368
369impl GameModifications {
370	/// Create a new GameModifications
371	pub fn new(modloader: Modloader, client_type: ClientType, server_type: ServerType) -> Self {
372		Self {
373			modloader,
374			client_type,
375			server_type,
376		}
377	}
378
379	/// Gets the client type
380	pub fn client_type(&self) -> ClientType {
381		if let ClientType::None = self.client_type {
382			match &self.modloader {
383				Modloader::Vanilla => ClientType::Vanilla,
384				Modloader::Forge => ClientType::Forge,
385				Modloader::NeoForged => ClientType::NeoForged,
386				Modloader::Fabric => ClientType::Fabric,
387				Modloader::Quilt => ClientType::Quilt,
388				Modloader::LiteLoader => ClientType::LiteLoader,
389				Modloader::Risugamis => ClientType::Risugamis,
390				Modloader::Rift => ClientType::Rift,
391				Modloader::Unknown(modloader) => ClientType::Unknown(modloader.clone()),
392			}
393		} else {
394			self.client_type.clone()
395		}
396	}
397
398	/// Gets the server type
399	pub fn server_type(&self) -> ServerType {
400		if let ServerType::None = self.server_type {
401			match &self.modloader {
402				Modloader::Vanilla => ServerType::Vanilla,
403				Modloader::Forge => ServerType::Forge,
404				Modloader::NeoForged => ServerType::NeoForged,
405				Modloader::Fabric => ServerType::Fabric,
406				Modloader::Quilt => ServerType::Quilt,
407				Modloader::LiteLoader => ServerType::Unknown("liteloader".into()),
408				Modloader::Risugamis => ServerType::Risugamis,
409				Modloader::Rift => ServerType::Rift,
410				Modloader::Unknown(modloader) => ServerType::Unknown(modloader.clone()),
411			}
412		} else {
413			self.server_type.clone()
414		}
415	}
416
417	/// Gets the modloader of a side
418	pub fn get_modloader(&self, side: Side) -> Modloader {
419		match side {
420			Side::Client => match self.client_type {
421				ClientType::None => self.modloader.clone(),
422				ClientType::Vanilla => Modloader::Vanilla,
423				ClientType::Forge => Modloader::Forge,
424				ClientType::NeoForged => Modloader::NeoForged,
425				ClientType::Fabric => Modloader::Fabric,
426				ClientType::Quilt => Modloader::Quilt,
427				ClientType::LiteLoader => Modloader::LiteLoader,
428				ClientType::Risugamis => Modloader::Risugamis,
429				ClientType::Rift => Modloader::Rift,
430				_ => Modloader::Vanilla,
431			},
432			Side::Server => match self.server_type {
433				ServerType::None => self.modloader.clone(),
434				ServerType::Forge | ServerType::SpongeForge => Modloader::Forge,
435				ServerType::NeoForged => Modloader::NeoForged,
436				ServerType::Fabric => Modloader::Fabric,
437				ServerType::Quilt => Modloader::Quilt,
438				ServerType::Risugamis => Modloader::Risugamis,
439				ServerType::Rift => Modloader::Rift,
440				_ => Modloader::Vanilla,
441			},
442		}
443	}
444
445	/// Gets whether both client and server have the same modloader
446	pub fn common_modloader(&self) -> bool {
447		matches!(
448			(&self.client_type, &self.server_type),
449			(ClientType::None, ServerType::None)
450				| (ClientType::Vanilla, ServerType::Vanilla)
451				| (ClientType::Forge, ServerType::Forge)
452				| (ClientType::NeoForged, ServerType::NeoForged)
453				| (ClientType::Fabric, ServerType::Fabric)
454				| (ClientType::Quilt, ServerType::Quilt)
455				| (ClientType::Risugamis, ServerType::Risugamis)
456				| (ClientType::Rift, ServerType::Rift)
457		)
458	}
459}
460
461/// Check if a client type can be installed by MCVM
462pub fn can_install_client_type(client_type: &ClientType) -> bool {
463	matches!(client_type, ClientType::None | ClientType::Vanilla)
464}
465
466/// Check if a server type can be installed by MCVM
467pub fn can_install_server_type(server_type: &ServerType) -> bool {
468	matches!(
469		server_type,
470		ServerType::None
471			| ServerType::Vanilla
472			| ServerType::Paper
473			| ServerType::Folia
474			| ServerType::Sponge
475	)
476}