1#[cfg(feature = "builder")]
3pub mod builder;
4pub mod instance;
6pub mod modifications;
8pub mod package;
10pub mod plugin;
12pub mod preferences;
14pub mod profile;
16pub 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
49pub struct Config {
52 pub users: UserManager,
54 pub instances: HashMap<InstanceID, Instance>,
56 pub instance_groups: HashMap<Arc<str>, Vec<InstanceID>>,
58 pub packages: PkgRegistry,
60 pub plugins: PluginManager,
62 pub prefs: ConfigPreferences,
64}
65
66#[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 pub fn get_path(paths: &Paths) -> PathBuf {
83 paths.project.config_dir().join("mcvm.json")
84 }
85
86 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 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 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 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 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 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 let profiles = consolidate_profile_configs(config.profiles, config.global_profile.as_ref())
160 .context("Failed to merge profiles")?;
161
162 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 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 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
257fn 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}