use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::launcher::options::LaunchOptions;
use crate::models::loader::LoaderType;
use crate::models::minecraft::{AssetItem, GameArgEntry, MinecraftVersionJson};
pub struct LoaderContext<'a> {
pub loader_type: Option<&'a LoaderType>,
pub version_id: Option<&'a str>,
pub extra_game_args: &'a [String],
pub extra_jvm_args: &'a [String],
}
pub fn get_game_arguments(
options: &LaunchOptions,
version_json: &MinecraftVersionJson,
loader: Option<&LoaderContext<'_>>,
) -> Vec<String> {
let ph = build_game_placeholders(options, version_json, loader);
let mut args: Vec<String> = Vec::new();
if let Some(raw) = &version_json.minecraft_arguments {
for token in raw.split_whitespace() {
args.push(replace_placeholders(token, &ph));
}
} else if let Some(arguments) = &version_json.arguments {
if let Some(game) = &arguments.game {
for entry in game {
if let GameArgEntry::Plain(s) = entry {
args.push(replace_placeholders(s, &ph));
}
}
}
}
if let Some(ctx) = loader {
for arg in ctx.extra_game_args {
let resolved = replace_placeholders(arg, &ph);
if !args.contains(&resolved) {
args.push(resolved);
}
}
}
for extra in &options.game_args {
args.push(replace_placeholders(extra, &ph));
}
args
}
pub fn get_jvm_arguments(
options: &LaunchOptions,
version_json: &MinecraftVersionJson,
natives_path: &Path,
loader: Option<&LoaderContext<'_>>,
) -> Vec<String> {
let mut args: Vec<String> = Vec::new();
let natives_str = natives_path.to_string_lossy().into_owned();
args.push(format!("-Xms{}", options.memory.min));
args.push(format!("-Xmx{}", options.memory.max));
args.push("-XX:+UnlockExperimentalVMOptions".into());
args.push("-XX:G1NewSizePercent=20".into());
args.push("-XX:G1ReservePercent=20".into());
args.push("-XX:MaxGCPauseMillis=50".into());
args.push("-XX:G1HeapRegionSize=32M".into());
if let Some(ctx) = loader {
if matches!(ctx.loader_type, Some(LoaderType::Forge) | Some(LoaderType::NeoForge)) {
args.push("-Dfml.ignoreInvalidMinecraftCertificates=true".into());
args.push("-Dfml.ignorePatchDiscrepancies=true".into());
}
}
if version_json.minecraft_arguments.is_none() {
match std::env::consts::OS {
"macos" => args.push("-XstartOnFirstThread".into()),
"linux" => args.push("-Xss1M".into()),
"windows" => args.push(
"-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump".into()
),
_ => {}
}
}
args.push(format!("-Djava.library.path={natives_str}"));
args.push(format!("-Djna.tmpdir={natives_str}"));
args.push(format!("-Dorg.lwjgl.system.SharedLibraryExtractPath={natives_str}"));
args.push(format!("-Dio.netty.native.workdir={natives_str}"));
if let Some(arguments) = &version_json.arguments {
if let Some(jvm_entries) = &arguments.jvm {
let ph = build_jvm_placeholders(options, version_json, &natives_str, "");
let mut skip_next = false;
for val in jvm_entries {
if skip_next {
skip_next = false;
continue;
}
if let Some(s) = val.as_str() {
if s == "-cp" || s == "--classpath" {
skip_next = true;
continue;
}
if s.contains("${classpath}") {
continue;
}
args.push(replace_placeholders(s, &ph));
} else if val.is_object() {
if jvm_rule_passes(val) {
for token in extract_jvm_value(val) {
if token == "-cp" || token == "--classpath" || token.contains("${classpath}") {
continue;
}
args.push(replace_placeholders(&token, &ph));
}
}
}
}
}
}
if options.bypass_offline {
args.push("-Dminecraft.api.auth.host=https://invalidAuthServer.invalid".into());
args.push("-Dminecraft.api.account.host=https://invalidAccountServer.invalid".into());
args.push("-Dminecraft.api.session.host=https://invalidSessionServer.invalid".into());
args.push("-Dminecraft.api.services.host=https://invalidServicesServer.invalid".into());
}
for extra in &options.jvm_args {
args.push(extra.clone());
}
if let Some(ctx) = loader {
for arg in ctx.extra_jvm_args {
args.push(arg.clone());
}
}
args
}
pub fn get_classpath(
version_json: &MinecraftVersionJson,
bundle: &[AssetItem],
) -> (Vec<String>, String) {
let jar_paths: Vec<PathBuf> = bundle
.iter()
.filter_map(|item| match item {
AssetItem::Asset { path, .. } => {
if path.ends_with(".jar") && !path.contains("/assets/objects/") {
Some(PathBuf::from(path))
} else {
None
}
}
_ => None,
})
.collect();
let mut deduped = deduplicate_classpath(jar_paths);
let has_slf4j2 = deduped.iter().any(|p| {
p.file_name()
.map_or(false, |f| f.to_string_lossy().contains("log4j-slf4j2-impl"))
});
if has_slf4j2 {
deduped.retain(|p| {
let name = p.file_name().map_or(String::new(), |f| f.to_string_lossy().into_owned());
!name.contains("log4j-slf4j-impl") || name.contains("log4j-slf4j2-impl")
});
}
let sep = classpath_separator();
let cp = deduped
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(sep);
let main_class = version_json.main_class.clone().unwrap_or_default();
(vec!["-cp".into(), cp], main_class)
}
fn build_game_placeholders<'a>(
options: &'a LaunchOptions,
version_json: &'a MinecraftVersionJson,
loader: Option<&LoaderContext<'_>>,
) -> HashMap<&'a str, String> {
let auth = &options.authenticator;
let assets_id = version_json
.asset_index
.as_ref()
.map(|ai| ai.id.clone())
.or_else(|| version_json.assets.clone())
.unwrap_or_default();
let user_type = if version_json.id.starts_with("1.16") {
"Xbox".to_string()
} else if auth.xbox_account.is_some() {
"msa".to_string()
} else {
"legacy".to_string()
};
let version_name = loader
.and_then(|ctx| ctx.version_id)
.unwrap_or(version_json.id.as_str())
.to_owned();
let auth_xuid = auth
.xbox_account
.as_ref()
.map(|x| x.xuid.clone())
.unwrap_or_else(|| auth.access_token.clone());
let clientid = auth
.client_id
.clone()
.or_else(|| auth.client_token.clone())
.unwrap_or_else(|| auth.access_token.clone());
let game_directory = options.save_dir().to_string_lossy().into_owned();
let is_legacy = matches!(version_json.assets.as_deref(), Some("legacy") | Some("pre-1.6"));
let assets_root = if is_legacy {
options.path.join("resources").to_string_lossy().into_owned()
} else {
options.path.join("assets").to_string_lossy().into_owned()
};
let mut ph: HashMap<&str, String> = HashMap::new();
ph.insert("auth_player_name", auth.name.clone());
ph.insert("version_name", version_name);
ph.insert("game_directory", game_directory);
ph.insert("assets_root", assets_root.clone());
ph.insert("game_assets", assets_root); ph.insert("assets_index_name", assets_id);
ph.insert("auth_uuid", auth.uuid.clone());
ph.insert("auth_access_token", auth.access_token.clone());
ph.insert("auth_session", auth.access_token.clone()); ph.insert("auth_xuid", auth_xuid);
ph.insert("user_type", user_type);
ph.insert("version_type", version_json.version_type.clone());
ph.insert(
"user_properties",
auth.user_properties.clone().unwrap_or_else(|| "{}".into()),
);
ph.insert("clientid", clientid);
ph
}
fn build_jvm_placeholders<'a>(
options: &'a LaunchOptions,
_version_json: &'a MinecraftVersionJson,
natives_str: &'a str,
classpath: &'a str,
) -> HashMap<&'a str, String> {
let mut ph: HashMap<&str, String> = HashMap::new();
ph.insert("natives_directory", natives_str.to_owned());
ph.insert("launcher_name", "minecraft-java-rs-core".into());
ph.insert("launcher_version", env!("CARGO_PKG_VERSION").into());
ph.insert(
"classpath_separator",
classpath_separator().to_string(),
);
ph.insert("classpath", classpath.to_owned());
ph.insert(
"library_directory",
options.path.join("libraries").to_string_lossy().into_owned(),
);
ph
}
fn replace_placeholders(s: &str, ph: &HashMap<&str, String>) -> String {
let mut result = s.to_owned();
for (key, val) in ph {
result = result.replace(&format!("${{{key}}}"), val);
}
result
}
fn jvm_rule_passes(val: &serde_json::Value) -> bool {
let rules = match val.get("rules").and_then(|r| r.as_array()) {
Some(r) => r,
None => return true,
};
let os_name = std::env::consts::OS;
let mojang_os = match os_name {
"macos" => "osx",
"windows" => "windows",
"linux" => "linux",
other => other,
};
let mut result = false;
for rule in rules {
let action = rule.get("action").and_then(|a| a.as_str()).unwrap_or("disallow");
let allow = action == "allow";
if let Some(os) = rule.get("os") {
let name_matches = os
.get("name")
.and_then(|n| n.as_str())
.map(|n| n == mojang_os)
.unwrap_or(true);
if name_matches {
result = allow;
}
} else {
result = allow;
}
}
result
}
fn extract_jvm_value(val: &serde_json::Value) -> Vec<String> {
match val.get("value") {
Some(serde_json::Value::String(s)) => vec![s.clone()],
Some(serde_json::Value::Array(arr)) => arr
.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect(),
_ => vec![],
}
}
pub fn classpath_separator() -> &'static str {
if cfg!(target_os = "windows") {
";"
} else {
":"
}
}
fn deduplicate_classpath(paths: Vec<PathBuf>) -> Vec<PathBuf> {
let mut entries: HashMap<String, (String, PathBuf)> = HashMap::new();
let mut key_order: Vec<String> = Vec::new();
for path in paths {
let components: Vec<_> = path.components().collect();
let n = components.len();
let (artifact_key, version_dir) = if n >= 3 {
let version = components[n - 2].as_os_str().to_string_lossy().into_owned();
let artifact = components[n - 3].as_os_str().to_string_lossy().into_owned();
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let base = format!("{artifact}-{version}");
let key = if stem.starts_with(&format!("{base}-")) {
let classifier = &stem[base.len() + 1..];
format!("{artifact}-{classifier}")
} else {
artifact
};
(key, version)
} else {
(path.to_string_lossy().into_owned(), String::new())
};
if let Some((existing_ver, existing_path)) = entries.get_mut(&artifact_key) {
if version_is_higher(&version_dir, existing_ver) {
*existing_ver = version_dir;
*existing_path = path;
}
} else {
key_order.push(artifact_key.clone());
entries.insert(artifact_key, (version_dir, path));
}
}
key_order
.into_iter()
.filter_map(|k| entries.remove(&k).map(|(_, p)| p))
.collect()
}
fn version_is_higher(a: &str, b: &str) -> bool {
if let (Ok(va), Ok(vb)) = (semver::Version::parse(a), semver::Version::parse(b)) {
return va > vb;
}
let parse_parts = |s: &str| -> Vec<u64> {
s.split(|c: char| c == '.' || c == '-')
.map(|p| p.parse::<u64>().unwrap_or(0))
.collect()
};
parse_parts(a) > parse_parts(b)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn make_opts() -> LaunchOptions {
use crate::launcher::options::{JavaOptions, LoaderConfig, MemoryConfig, ScreenConfig};
use crate::models::minecraft::Authenticator;
LaunchOptions {
path: PathBuf::from("/mc"),
version: "1.20.4".into(),
authenticator: Authenticator {
access_token: "token123".into(),
name: "Steve".into(),
uuid: "uuid-1234".into(),
xbox_account: None,
user_properties: None,
client_id: None,
client_token: None,
},
timeout_secs: 10,
download_concurrency: 5,
verify_concurrency: 4,
memory: MemoryConfig { min: "512M".into(), max: "4G".into() },
java: JavaOptions::default(),
loader: LoaderConfig::default(),
screen: ScreenConfig::default(),
verify: false,
game_args: vec![],
jvm_args: vec![],
instance: None,
url: None,
mcp: None,
intel_enabled_mac: false,
bypass_offline: false,
skip_bundle_check: false,
}
}
fn bare_version() -> MinecraftVersionJson {
MinecraftVersionJson {
id: "1.20.4".into(),
version_type: "release".into(),
assets: Some("17".into()),
asset_index: None,
downloads: None,
libraries: vec![],
arguments: None,
minecraft_arguments: None,
java_version: None,
main_class: Some("net.minecraft.client.main.Main".into()),
has_natives: false,
}
}
#[test]
fn legacy_game_args_split_and_replace() {
let opts = make_opts();
let mut vj = bare_version();
vj.minecraft_arguments = Some(
"--username ${auth_player_name} --version ${version_name}".into(),
);
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args[0], "--username");
assert_eq!(args[1], "Steve");
assert_eq!(args[2], "--version");
assert_eq!(args[3], "1.20.4");
}
#[test]
fn modern_game_args_plain_strings_only() {
use crate::models::minecraft::Arguments;
let opts = make_opts();
let mut vj = bare_version();
vj.arguments = Some(Arguments {
game: Some(vec![
GameArgEntry::Plain("--username".into()),
GameArgEntry::Plain("${auth_player_name}".into()),
GameArgEntry::Conditional(serde_json::json!({"rules": [], "value": "--demo"})),
]),
jvm: None,
});
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args.len(), 2);
assert_eq!(args[0], "--username");
assert_eq!(args[1], "Steve");
}
#[test]
fn extra_game_args_appended() {
let mut opts = make_opts();
opts.game_args = vec!["--demo".into()];
let mut vj = bare_version();
vj.minecraft_arguments = Some("--username ${auth_player_name}".into());
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args.last().unwrap(), "--demo");
}
#[test]
fn user_type_is_msa_when_xbox_account_present() {
use crate::models::minecraft::XboxAccount;
let mut opts = make_opts();
opts.authenticator.xbox_account = Some(XboxAccount { xuid: "x123".into() });
let mut vj = bare_version();
vj.minecraft_arguments = Some("${user_type}".into());
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args[0], "msa");
}
#[test]
fn user_type_is_xbox_on_116() {
let opts = make_opts();
let mut vj = bare_version();
vj.id = "1.16.5".into();
vj.minecraft_arguments = Some("${user_type}".into());
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args[0], "Xbox");
}
#[test]
fn loader_version_id_overrides_version_name() {
let opts = make_opts();
let mut vj = bare_version();
vj.minecraft_arguments = Some("${version_name}".into());
let ctx = LoaderContext {
loader_type: Some(&LoaderType::Forge),
version_id: Some("1.20.4-forge-47.4.20"),
extra_game_args: &[],
extra_jvm_args: &[],
};
let args = get_game_arguments(&opts, &vj, Some(&ctx));
assert_eq!(args[0], "1.20.4-forge-47.4.20");
}
#[test]
fn loader_extra_game_args_merged_deduped() {
let opts = make_opts();
let mut vj = bare_version();
vj.minecraft_arguments = Some("--username ${auth_player_name}".into());
let extra = vec!["--launchTarget".into(), "fmlclient".into(), "--username".into()];
let ctx = LoaderContext {
loader_type: Some(&LoaderType::Forge),
version_id: None,
extra_game_args: &extra,
extra_jvm_args: &[],
};
let args = get_game_arguments(&opts, &vj, Some(&ctx));
let username_count = args.iter().filter(|a| *a == "--username").count();
assert_eq!(username_count, 1);
assert!(args.contains(&"--launchTarget".to_string()));
assert!(args.contains(&"fmlclient".to_string()));
}
#[test]
fn auth_session_placeholder_resolved() {
let opts = make_opts();
let mut vj = bare_version();
vj.minecraft_arguments = Some("${auth_session}".into());
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args[0], "token123");
}
#[test]
fn clientid_falls_back_to_client_token() {
let mut opts = make_opts();
opts.authenticator.client_token = Some("ct-abc".into());
let mut vj = bare_version();
vj.minecraft_arguments = Some("${clientid}".into());
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args[0], "ct-abc");
}
#[test]
fn clientid_falls_back_to_access_token() {
let opts = make_opts(); let mut vj = bare_version();
vj.minecraft_arguments = Some("${clientid}".into());
let args = get_game_arguments(&opts, &vj, None);
assert_eq!(args[0], "token123");
}
#[test]
fn jvm_args_contain_memory_and_natives() {
let opts = make_opts();
let vj = bare_version();
let natives = PathBuf::from("/mc/versions/1.20.4/natives");
let args = get_jvm_arguments(&opts, &vj, &natives, None);
assert!(args.contains(&"-Xms512M".to_string()));
assert!(args.contains(&"-Xmx4G".to_string()));
assert!(args.iter().any(|a| a.contains("-Djava.library.path=")));
}
#[test]
fn jvm_args_contain_gc_flags() {
let opts = make_opts();
let args = get_jvm_arguments(&opts, &bare_version(), Path::new("/n"), None);
assert!(args.contains(&"-XX:+UnlockExperimentalVMOptions".to_string()));
assert!(args.contains(&"-XX:G1NewSizePercent=20".to_string()));
assert!(args.contains(&"-XX:MaxGCPauseMillis=50".to_string()));
}
#[test]
fn jvm_args_contain_jna_dirs() {
let opts = make_opts();
let natives = Path::new("/natives");
let args = get_jvm_arguments(&opts, &bare_version(), natives, None);
assert!(args.iter().any(|a| a.starts_with("-Djna.tmpdir=")));
assert!(args.iter().any(|a| a.starts_with("-Dorg.lwjgl.system.SharedLibraryExtractPath=")));
assert!(args.iter().any(|a| a.starts_with("-Dio.netty.native.workdir=")));
}
#[test]
fn jvm_args_forge_adds_fml_flags() {
let opts = make_opts();
let ctx = LoaderContext {
loader_type: Some(&LoaderType::Forge),
version_id: None,
extra_game_args: &[],
extra_jvm_args: &[],
};
let args = get_jvm_arguments(&opts, &bare_version(), Path::new("/n"), Some(&ctx));
assert!(args.contains(&"-Dfml.ignoreInvalidMinecraftCertificates=true".to_string()));
assert!(args.contains(&"-Dfml.ignorePatchDiscrepancies=true".to_string()));
}
#[test]
fn jvm_args_fabric_no_fml_flags() {
let opts = make_opts();
let ctx = LoaderContext {
loader_type: Some(&LoaderType::Fabric),
version_id: None,
extra_game_args: &[],
extra_jvm_args: &[],
};
let args = get_jvm_arguments(&opts, &bare_version(), Path::new("/n"), Some(&ctx));
assert!(!args.iter().any(|a| a.contains("fml")));
}
#[test]
fn bypass_offline_adds_sys_properties() {
let mut opts = make_opts();
opts.bypass_offline = true;
let args = get_jvm_arguments(&opts, &bare_version(), Path::new("/n"), None);
assert!(args.iter().any(|a| a.contains("invalidAuthServer")));
}
#[test]
fn jvm_args_no_classpath_entry() {
let opts = make_opts();
let args = get_jvm_arguments(&opts, &bare_version(), Path::new("/n"), None);
assert!(!args.iter().any(|a| a == "-cp" || a == "--classpath"));
}
#[test]
fn classpath_contains_jar_paths() {
let vj = bare_version();
let bundle = vec![
AssetItem::Asset {
path: "/mc/libraries/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar".into(),
sha1: "aaa".into(),
size: 100,
url: "http://x".into(),
},
AssetItem::Asset {
path: "/mc/assets/objects/aa/aabbcc".into(),
sha1: "bbb".into(),
size: 10,
url: "http://y".into(),
},
];
let (cp_args, main) = get_classpath(&vj, &bundle);
assert_eq!(cp_args[0], "-cp");
let cp = &cp_args[1];
assert!(cp.contains("jopt-simple-5.0.4.jar"));
assert!(!cp.contains("aabbcc"));
assert_eq!(main, "net.minecraft.client.main.Main");
}
#[test]
fn classpath_deduplicates_lower_version() {
let vj = bare_version();
let bundle = vec![
AssetItem::Asset {
path: "/mc/libraries/com/google/guava/guava/21.0/guava-21.0.jar".into(),
sha1: "a".into(),
size: 1,
url: "http://x".into(),
},
AssetItem::Asset {
path: "/mc/libraries/com/google/guava/guava/32.1.2/guava-32.1.2.jar".into(),
sha1: "b".into(),
size: 2,
url: "http://x".into(),
},
];
let (cp_args, _) = get_classpath(&vj, &bundle);
let cp = &cp_args[1];
assert!(cp.contains("32.1.2"), "should keep higher version: {cp}");
assert!(!cp.contains("21.0"), "should drop lower version: {cp}");
}
#[test]
fn classpath_preserves_loader_first_order() {
let vj = bare_version();
let bundle = vec![
AssetItem::Asset {
path: "/loader/forge/libraries/net/minecraftforge/forge/1.0/forge-1.0.jar".into(),
sha1: "a".into(),
size: 1,
url: "http://x".into(),
},
AssetItem::Asset {
path: "/mc/libraries/org/lwjgl/lwjgl/3.3.1/lwjgl-3.3.1.jar".into(),
sha1: "b".into(),
size: 2,
url: "http://x".into(),
},
];
let (cp_args, _) = get_classpath(&vj, &bundle);
let cp = &cp_args[1];
let forge_pos = cp.find("forge-1.0.jar").unwrap();
let lwjgl_pos = cp.find("lwjgl-3.3.1.jar").unwrap();
assert!(forge_pos < lwjgl_pos, "loader lib should come before vanilla lib");
}
#[test]
fn classpath_removes_slf4j1_when_slf4j2_present() {
let vj = bare_version();
let bundle = vec![
AssetItem::Asset {
path: "/loader/forge/libraries/log4j/log4j-slf4j2-impl/18.0/log4j-slf4j2-impl-18.0.jar".into(),
sha1: "a".into(),
size: 1,
url: "http://x".into(),
},
AssetItem::Asset {
path: "/mc/libraries/log4j/log4j-slf4j-impl/18.0/log4j-slf4j-impl-18.0.jar".into(),
sha1: "b".into(),
size: 2,
url: "http://x".into(),
},
];
let (cp_args, _) = get_classpath(&vj, &bundle);
let cp = &cp_args[1];
assert!(cp.contains("log4j-slf4j2-impl"), "should keep slf4j2 binding: {cp}");
assert!(!cp.contains("log4j-slf4j-impl-18"), "should drop slf4j1 binding: {cp}");
}
#[test]
fn classpath_keeps_both_classifiers_in_same_version_dir() {
let vj = bare_version();
let bundle = vec![
AssetItem::Asset {
path: "/loader/forge/libraries/net/minecraftforge/forge/26.1.2-64.0.8/forge-26.1.2-64.0.8-universal.jar".into(),
sha1: "a".into(),
size: 1,
url: "http://x".into(),
},
AssetItem::Asset {
path: "/loader/forge/libraries/net/minecraftforge/forge/26.1.2-64.0.8/forge-26.1.2-64.0.8-client.jar".into(),
sha1: "b".into(),
size: 2,
url: "http://x".into(),
},
];
let (cp_args, _) = get_classpath(&vj, &bundle);
let cp = &cp_args[1];
assert!(cp.contains("forge-26.1.2-64.0.8-universal.jar"), "universal must be kept: {cp}");
assert!(cp.contains("forge-26.1.2-64.0.8-client.jar"), "client must be kept: {cp}");
}
#[test]
fn classpath_separator_is_colon_on_non_windows() {
assert_eq!(classpath_separator(), ":");
}
#[test]
fn higher_semver_wins() {
assert!(version_is_higher("2.0.0", "1.9.9"));
assert!(!version_is_higher("1.0.0", "1.0.0"));
}
#[test]
fn numeric_dot_split_fallback() {
assert!(version_is_higher("1.10.0", "1.9.0"));
assert!(!version_is_higher("1.9.0", "1.10.0"));
}
}