use serde::{Deserialize, Serialize};
use super::rules::Rule;
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LaunchProfile {
pub id: String,
pub inherits_from: Option<String>,
pub main_class: Option<String>,
#[serde(default)]
pub libraries: Vec<Library>,
pub arguments: Option<Arguments>,
pub minecraft_arguments: Option<String>,
pub asset_index: Option<AssetIndex>,
pub assets: Option<String>,
pub java_version: Option<JavaVersion>,
pub downloads: Option<VersionDownloads>,
pub release_time: Option<String>,
pub time: Option<String>,
#[serde(rename = "type")]
pub type_: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub game_arguments: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub struct Arguments {
#[serde(default)]
pub game: Vec<Argument>,
#[serde(default)]
pub jvm: Vec<Argument>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Argument {
Literal(String),
Conditional {
rules: Vec<Rule>,
value: ArgumentValue,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ArgumentValue {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct Library {
pub name: String,
pub downloads: Option<LibraryDownloads>,
pub rules: Option<Vec<Rule>>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct LibraryDownloads {
pub artifact: Option<Artifact>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct Artifact {
pub url: String,
pub path: String,
pub sha1: String,
pub size: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct AssetIndex {
pub id: String,
pub url: String,
pub sha1: String,
pub size: Option<u64>,
pub total_size: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JavaVersion {
pub component: Option<String>,
pub major_version: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct VersionDownloads {
pub client: Download,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct Download {
pub url: String,
pub sha1: String,
pub size: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::launch_profile::rules::RuleAction;
const MODERN_FIXTURE: &str = r#"{
"id": "1.20.1",
"type": "release",
"mainClass": "net.minecraft.client.main.Main",
"assetIndex": {
"id": "5",
"url": "https://example.invalid/5.json",
"sha1": "0000000000000000000000000000000000000000"
},
"javaVersion": {
"component": "java-runtime-gamma",
"majorVersion": 17
},
"libraries": [
{
"name": "org.lwjgl:lwjgl:3.3.1",
"downloads": {
"artifact": {
"url": "https://example.invalid/lwjgl.jar",
"path": "org/lwjgl/lwjgl/3.3.1/lwjgl-3.3.1.jar",
"sha1": "1111111111111111111111111111111111111111",
"size": 100
}
},
"rules": [
{ "action": "allow", "os": { "name": "linux" } }
]
}
],
"arguments": {
"game": [
"--username", "${auth_player_name}",
{
"rules": [{ "action": "allow", "features": { "is_demo_user": true } }],
"value": "--demo"
}
],
"jvm": [
"-Djava.library.path=${natives_directory}",
{
"rules": [{ "action": "allow", "os": { "name": "osx" } }],
"value": ["-XstartOnFirstThread"]
}
]
}
}"#;
const LEGACY_FIXTURE: &str = r#"{
"id": "1.7.10",
"type": "release",
"mainClass": "net.minecraft.client.main.Main",
"minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory}",
"assetIndex": {
"id": "1.7.10",
"url": "https://example.invalid/1.7.10.json",
"sha1": "0000000000000000000000000000000000000000"
},
"libraries": []
}"#;
const LOADER_FIXTURE: &str = r#"{
"id": "1.20.1-forge-47.2.0",
"inheritsFrom": "1.20.1",
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
"libraries": [
{ "name": "net.minecraftforge:forge:47.2.0" }
],
"arguments": {
"game": ["--launchTarget", "forge_client"],
"jvm": [
"--add-opens", "java.base/sun.security.util=cpw.mods.securejarhandler",
"-DlibraryDirectory=${library_directory}"
]
}
}"#;
#[test]
fn parses_modern_arguments_object() {
let profile: LaunchProfile = serde_json::from_str(MODERN_FIXTURE).unwrap();
assert_eq!(profile.id, "1.20.1");
assert_eq!(
profile.main_class.as_deref(),
Some("net.minecraft.client.main.Main")
);
assert!(profile.inherits_from.is_none());
assert!(profile.minecraft_arguments.is_none());
let args = profile.arguments.as_ref().expect("arguments present");
assert_eq!(args.game.len(), 3);
assert_eq!(args.jvm.len(), 2);
match &args.game[0] {
Argument::Literal(s) => assert_eq!(s, "--username"),
_ => panic!("expected literal"),
}
match &args.game[2] {
Argument::Conditional { rules, value } => {
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].action, RuleAction::Allow);
assert!(matches!(value, ArgumentValue::Single(_)));
}
_ => panic!("expected conditional"),
}
match &args.jvm[1] {
Argument::Conditional { value, .. } => {
assert!(matches!(value, ArgumentValue::Multiple(_)));
}
_ => panic!("expected conditional"),
}
}
#[test]
fn parses_legacy_minecraft_arguments_string() {
let profile: LaunchProfile = serde_json::from_str(LEGACY_FIXTURE).unwrap();
assert_eq!(profile.id, "1.7.10");
assert!(profile.arguments.is_none());
assert!(
profile
.minecraft_arguments
.as_deref()
.unwrap()
.contains("${version_name}")
);
assert!(profile.libraries.is_empty());
}
#[test]
fn parses_loader_profile_with_inherits_from() {
let profile: LaunchProfile = serde_json::from_str(LOADER_FIXTURE).unwrap();
assert_eq!(profile.id, "1.20.1-forge-47.2.0");
assert_eq!(profile.inherits_from.as_deref(), Some("1.20.1"));
assert!(profile.asset_index.is_none()); let args = profile.arguments.as_ref().unwrap();
assert_eq!(args.game.len(), 2);
assert_eq!(args.jvm.len(), 3);
}
#[test]
fn modern_profile_round_trips() {
let original: LaunchProfile = serde_json::from_str(MODERN_FIXTURE).unwrap();
let serialized = serde_json::to_string(&original).unwrap();
let reparsed: LaunchProfile = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, reparsed);
}
#[test]
fn loader_profile_round_trips() {
let original: LaunchProfile = serde_json::from_str(LOADER_FIXTURE).unwrap();
let serialized = serde_json::to_string(&original).unwrap();
let reparsed: LaunchProfile = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, reparsed);
}
#[test]
fn legacy_profile_round_trips() {
let original: LaunchProfile = serde_json::from_str(LEGACY_FIXTURE).unwrap();
let serialized = serde_json::to_string(&original).unwrap();
let reparsed: LaunchProfile = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, reparsed);
}
}