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#[derive(Deserialize, Serialize, Clone, Debug, Default)]
17#[cfg_attr(feature = "schema", derive(JsonSchema))]
18pub struct InstanceConfig {
19 #[serde(rename = "type")]
21 pub side: Option<Side>,
22 #[serde(default)]
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub name: Option<String>,
26 #[serde(default)]
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub icon: Option<String>,
30 #[serde(flatten)]
32 pub common: CommonInstanceConfig,
33 #[serde(default)]
35 #[serde(skip_serializing_if = "DefaultExt::is_default")]
36 pub window: ClientWindowConfig,
37}
38
39#[derive(Deserialize, Serialize, Clone, Default, Debug)]
41#[cfg_attr(feature = "schema", derive(JsonSchema))]
42#[serde(default)]
43pub struct CommonInstanceConfig {
44 #[serde(skip_serializing_if = "DeserListOrSingle::is_empty")]
46 pub from: DeserListOrSingle<String>,
47 pub version: Option<MinecraftVersionDeser>,
49 #[serde(default)]
51 #[serde(skip_serializing_if = "DefaultExt::is_default")]
52 pub modloader: Option<Modloader>,
53 #[serde(default)]
55 #[serde(skip_serializing_if = "DefaultExt::is_default")]
56 pub client_type: Option<ClientType>,
57 #[serde(default)]
59 #[serde(skip_serializing_if = "DefaultExt::is_default")]
60 pub server_type: Option<ServerType>,
61 #[serde(default)]
63 #[serde(skip_serializing_if = "DefaultExt::is_default")]
64 pub game_modification_version: Option<VersionPattern>,
65 #[serde(default)]
67 #[serde(skip_serializing_if = "DefaultExt::is_default")]
68 pub package_stability: Option<PackageStability>,
69 #[serde(skip_serializing_if = "DefaultExt::is_default")]
71 pub launch: LaunchConfig,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub datapack_folder: Option<String>,
75 #[serde(skip_serializing_if = "Vec::is_empty")]
77 pub packages: Vec<PackageConfigDeser>,
78 #[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 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#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
105#[cfg_attr(feature = "schema", derive(JsonSchema))]
106#[serde(untagged)]
107pub enum Args {
108 List(Vec<String>),
110 String(String),
112}
113
114impl Args {
115 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 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#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)]
139#[cfg_attr(feature = "schema", derive(JsonSchema))]
140pub struct LaunchArgs {
141 #[serde(default)]
143 #[serde(skip_serializing_if = "DefaultExt::is_default")]
144 pub jvm: Args,
145 #[serde(default)]
147 #[serde(skip_serializing_if = "DefaultExt::is_default")]
148 pub game: Args,
149}
150
151#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)]
153#[cfg_attr(feature = "schema", derive(JsonSchema))]
154#[serde(untagged)]
155pub enum LaunchMemory {
156 #[default]
158 None,
159 Single(String),
161 Both {
163 min: String,
165 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#[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 World {
186 world: String,
188 },
189 Server {
191 server: String,
193 port: Option<u16>,
195 },
196 Realm {
198 realm: String,
200 },
201 #[default]
203 None,
204}
205
206#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
208#[cfg_attr(feature = "schema", derive(JsonSchema))]
209pub struct LaunchConfig {
210 #[serde(default)]
212 #[serde(skip_serializing_if = "DefaultExt::is_default")]
213 pub args: LaunchArgs,
214 #[serde(default)]
216 #[serde(skip_serializing_if = "DefaultExt::is_default")]
217 pub memory: LaunchMemory,
218 #[serde(default = "default_java")]
220 pub java: String,
221 #[serde(default = "default_flags_preset")]
223 pub preset: String,
224 #[serde(default)]
226 #[serde(skip_serializing_if = "HashMap::is_empty")]
227 pub env: HashMap<String, String>,
228 #[serde(default)]
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub wrapper: Option<WrapperCommand>,
232 #[serde(default)]
234 #[serde(skip_serializing_if = "DefaultExt::is_default")]
235 pub quick_play: QuickPlay,
236 #[serde(default)]
238 #[serde(skip_serializing_if = "DefaultExt::is_default")]
239 pub use_log4j_config: bool,
240}
241
242impl LaunchConfig {
243 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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
286#[cfg_attr(feature = "schema", derive(JsonSchema))]
287pub struct WrapperCommand {
288 pub cmd: String,
290 #[serde(default)]
292 pub args: Vec<String>,
293}
294
295#[derive(Deserialize, Serialize, Clone, Debug, Copy, PartialEq)]
297#[cfg_attr(feature = "schema", derive(JsonSchema))]
298pub struct WindowResolution {
299 pub width: u32,
301 pub height: u32,
303}
304
305#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq)]
307#[cfg_attr(feature = "schema", derive(JsonSchema))]
308#[serde(default)]
309pub struct ClientWindowConfig {
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub resolution: Option<WindowResolution>,
313}
314
315impl ClientWindowConfig {
316 pub fn merge(&mut self, other: Self) -> &mut Self {
318 self.resolution = merge_options(self.resolution, other.resolution);
319 self
320 }
321}
322
323pub 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
337pub 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#[derive(Clone, Debug)]
361pub struct GameModifications {
362 modloader: Modloader,
363 client_type: ClientType,
365 server_type: ServerType,
367}
368
369impl GameModifications {
370 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 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 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 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 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
461pub fn can_install_client_type(client_type: &ClientType) -> bool {
463 matches!(client_type, ClientType::None | ClientType::Vanilla)
464}
465
466pub 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}