mcvm/config/
mod.rs

1/// Easy programatic creation of config
2#[cfg(feature = "builder")]
3pub mod builder;
4/// Configuring instances
5pub mod instance;
6/// Configuring profile modifications
7pub mod modifications;
8/// Configuring packages
9pub mod package;
10/// Configuring plugins
11pub mod plugin;
12/// Configuring global preferences
13pub mod preferences;
14/// Configuring profiles
15pub mod profile;
16/// Configuring users
17pub mod user;
18
19use self::instance::{read_instance_config, InstanceConfig};
20use self::preferences::PrefDeser;
21use self::profile::ProfileConfig;
22use self::user::UserConfig;
23use crate::plugin::PluginManager;
24use anyhow::{bail, Context};
25use mcvm_core::auth_crate::mc::ClientId;
26use mcvm_core::io::{json_from_file, json_to_file_pretty};
27use mcvm_core::user::UserManager;
28use mcvm_plugin::hooks::{AddSupportedGameModifications, SupportedGameModifications};
29use mcvm_shared::id::{InstanceID, ProfileID};
30use mcvm_shared::output::{MCVMOutput, MessageContents, MessageLevel};
31use mcvm_shared::translate;
32use mcvm_shared::util::is_valid_identifier;
33use preferences::ConfigPreferences;
34use profile::consolidate_profile_configs;
35#[cfg(feature = "schema")]
36use schemars::JsonSchema;
37use serde::{Deserialize, Serialize};
38
39use super::instance::Instance;
40use crate::io::paths::Paths;
41use crate::pkg::reg::PkgRegistry;
42
43use serde_json::json;
44
45use std::collections::HashMap;
46use std::path::{Path, PathBuf};
47use std::sync::Arc;
48
49/// The data resulting from reading configuration.
50/// Represents all of the configured data that MCVM will use
51pub struct Config {
52	/// The user manager
53	pub users: UserManager,
54	/// Instances
55	pub instances: HashMap<InstanceID, Instance>,
56	/// Named groups of instances
57	pub instance_groups: HashMap<Arc<str>, Vec<InstanceID>>,
58	/// The registry of packages. Will include packages that are configured when created this way
59	pub packages: PkgRegistry,
60	/// Configured plugins
61	pub plugins: PluginManager,
62	/// Global user preferences
63	pub prefs: ConfigPreferences,
64}
65
66/// Deserialization struct for user configuration
67#[derive(Deserialize, Serialize, Default)]
68#[cfg_attr(feature = "schema", derive(JsonSchema))]
69#[serde(default)]
70pub struct ConfigDeser {
71	users: HashMap<String, UserConfig>,
72	default_user: Option<String>,
73	instances: HashMap<InstanceID, InstanceConfig>,
74	instance_groups: HashMap<Arc<str>, Vec<InstanceID>>,
75	profiles: HashMap<ProfileID, ProfileConfig>,
76	global_profile: Option<ProfileConfig>,
77	preferences: PrefDeser,
78}
79
80impl Config {
81	/// Get the config path
82	pub fn get_path(paths: &Paths) -> PathBuf {
83		paths.project.config_dir().join("mcvm.json")
84	}
85
86	/// Open the config from a file
87	pub fn open(path: &Path) -> anyhow::Result<ConfigDeser> {
88		if path.exists() {
89			Ok(json_from_file(path).context("Failed to open config")?)
90		} else {
91			let config = default_config();
92			json_to_file_pretty(path, &config).context("Failed to write default configuration")?;
93			Ok(serde_json::from_value(config).context("Failed to parse default configuration")?)
94		}
95	}
96
97	/// Create the default config at the specified path if it does not exist
98	pub fn create_default(path: &Path) -> anyhow::Result<()> {
99		if !path.exists() {
100			let doc = default_config();
101			json_to_file_pretty(path, &doc).context("Failed to write default configuration")?;
102		}
103		Ok(())
104	}
105
106	/// Create the Config struct from deserialized config
107	fn load_from_deser(
108		config: ConfigDeser,
109		plugins: PluginManager,
110		show_warnings: bool,
111		paths: &Paths,
112		client_id: ClientId,
113		o: &mut impl MCVMOutput,
114	) -> anyhow::Result<Self> {
115		let mut users = UserManager::new(client_id);
116		let mut instances = HashMap::with_capacity(config.instances.len());
117		// Preferences
118		let (prefs, repositories) =
119			ConfigPreferences::read(&config.preferences).context("Failed to read preferences")?;
120
121		let packages = PkgRegistry::new(repositories, prefs.package_caching_strategy.clone());
122
123		// Users
124		for (user_id, user_config) in config.users.iter() {
125			if !is_valid_identifier(user_id) {
126				bail!("Invalid user ID '{user_id}'");
127			}
128			let user = user_config.to_user(user_id);
129			// Disabled until we can verify game ownership.
130			// We don't want to be a cracked launcher.
131			if user.is_demo() {
132				bail!("Unverified and Demo users are currently disabled");
133			}
134
135			users.add_user(user);
136		}
137
138		if let Some(default_user_id) = &config.default_user {
139			if users.user_exists(default_user_id) {
140				users
141					.choose_user(default_user_id)
142					.expect("Default user should exist");
143			} else {
144				bail!("Provided default user '{default_user_id}' does not exist");
145			}
146		} else if config.users.is_empty() && show_warnings {
147			o.display(
148				MessageContents::Warning(translate!(o, NoDefaultUser)),
149				MessageLevel::Important,
150			);
151		} else if show_warnings {
152			o.display(
153				MessageContents::Warning(translate!(o, NoUsers)),
154				MessageLevel::Important,
155			);
156		}
157
158		// Consolidate profiles
159		let profiles = consolidate_profile_configs(config.profiles, config.global_profile.as_ref())
160			.context("Failed to merge profiles")?;
161
162		// Load extra supported game modifications
163		let mut supported_game_modifications = SupportedGameModifications {
164			client_types: Vec::new(),
165			server_types: Vec::new(),
166		};
167		let results = plugins
168			.call_hook(AddSupportedGameModifications, &(), paths, o)
169			.context("Failed to get supported game modifications")?;
170		for result in results {
171			let result = result.result(o)?;
172			supported_game_modifications
173				.client_types
174				.extend(result.client_types);
175			supported_game_modifications
176				.server_types
177				.extend(result.server_types);
178		}
179
180		// Instances
181		for (instance_id, instance_config) in config.instances {
182			let instance = read_instance_config(
183				instance_id.clone(),
184				instance_config,
185				&profiles,
186				&plugins,
187				paths,
188				o,
189			)
190			.with_context(|| format!("Failed to read config for instance {instance_id}"))?;
191
192			if show_warnings
193				&& !profile::can_install_client_type(&instance.config.modifications.client_type())
194				&& !supported_game_modifications
195					.client_types
196					.contains(&instance.config.modifications.client_type())
197			{
198				o.display(
199					MessageContents::Warning(translate!(
200						o,
201						ModificationNotSupported,
202						"mod" = &format!("{}", instance.config.modifications.client_type())
203					)),
204					MessageLevel::Important,
205				);
206			}
207
208			if show_warnings
209				&& !profile::can_install_server_type(&instance.config.modifications.server_type())
210				&& !supported_game_modifications
211					.server_types
212					.contains(&instance.config.modifications.server_type())
213			{
214				o.display(
215					MessageContents::Warning(translate!(
216						o,
217						ModificationNotSupported,
218						"mod" = &format!("{}", instance.config.modifications.server_type())
219					)),
220					MessageLevel::Important,
221				);
222			}
223
224			instances.insert(instance_id, instance);
225		}
226
227		for group in config.instance_groups.keys() {
228			if !is_valid_identifier(group) {
229				bail!("Invalid ID for group '{group}'");
230			}
231		}
232
233		Ok(Self {
234			users,
235			instances,
236			instance_groups: config.instance_groups,
237			packages,
238			plugins,
239			prefs,
240		})
241	}
242
243	/// Load the configuration from the config file
244	pub fn load(
245		path: &Path,
246		plugins: PluginManager,
247		show_warnings: bool,
248		paths: &Paths,
249		client_id: ClientId,
250		o: &mut impl MCVMOutput,
251	) -> anyhow::Result<Self> {
252		let obj = Self::open(path)?;
253		Self::load_from_deser(obj, plugins, show_warnings, paths, client_id, o)
254	}
255}
256
257/// Default program configuration
258fn default_config() -> serde_json::Value {
259	json!(
260		{
261			"users": {
262				"example": {
263					"type": "microsoft"
264				}
265			},
266			"default_user": "example",
267			"profiles": {
268				"1.20": {
269					"version": "1.19.3",
270					"modloader": "vanilla",
271					"server_type": "none"
272				}
273			},
274			"instances": {
275				"example-client": {
276					"from": "1.20",
277					"type": "client"
278				},
279				"example-server": {
280					"from": "1.20",
281					"type": "server"
282				}
283			}
284		}
285	)
286}
287
288#[cfg(test)]
289mod tests {
290	use super::*;
291
292	use mcvm_shared::output;
293
294	#[test]
295	fn test_default_config() {
296		let deser = serde_json::from_value(default_config()).unwrap();
297		Config::load_from_deser(
298			deser,
299			PluginManager::new(),
300			true,
301			&Paths::new_no_create().unwrap(),
302			ClientId::new(String::new()),
303			&mut output::Simple(output::MessageLevel::Debug),
304		)
305		.unwrap();
306	}
307}