Add Docker image, Compose, CI/CD workflows and docs
- Multi-arch (amd64/arm64) panel image: static musl binary on scratch, cross-compiled with cargo-zigbuild (no QEMU), non-root, healthcheck - docker-compose.yml + .env.example for one-command deployment - CI: fmt, clippy, tests, web type-checks, Windows launcher build, real-network installs of vanilla/Fabric/Quilt/Forge/NeoForge, Docker build - Release: Windows NSIS installer with baked-in panel URL, GHCR image push and GitHub release (tag push or manual dispatch) - README, admin guide, Microsoft auth setup, architecture, development - rustfmt config and formatting pass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
54 files changed
+1019
-219
No files matched your search
@@ -33,8 +33,7 @@ pub fn object_path(layout: &Layout, hash: &str) -> PathBuf {
|
||||
pub async fn load_index(client: &reqwest::Client, layout: &Layout, index: &AssetIndexRef) -> Result<AssetIndex> {
|
||||
let path = layout.assets().join("indexes").join(format!("{}.json", index.id));
|
||||
if !http::file_ok(&path, index.sha1.as_deref(), index.size, true) {
|
||||
http::download_one(client, &Download::new(index.url.clone(), path.clone(), index.sha1.clone(), index.size), &|_| {})
|
||||
.await?;
|
||||
http::download_one(client, &Download::new(index.url.clone(), path.clone(), index.sha1.clone(), index.size), &|_| {}).await?;
|
||||
}
|
||||
Ok(serde_json::from_slice(&std::fs::read(&path)?)?)
|
||||
}
|
||||
@@ -64,11 +63,7 @@ pub fn materialize_legacy(layout: &Layout, index_id: &str, index: &AssetIndex, g
|
||||
if !index.is_virtual && !index.map_to_resources {
|
||||
return Ok(None);
|
||||
}
|
||||
let target = if index.map_to_resources {
|
||||
game_dir.join("resources")
|
||||
} else {
|
||||
layout.assets().join("virtual").join(index_id)
|
||||
};
|
||||
let target = if index.map_to_resources { game_dir.join("resources") } else { layout.assets().join("virtual").join(index_id) };
|
||||
for (name, obj) in &index.objects {
|
||||
let Some(dest) = crate::paths::safe_join(&target, name) else { continue };
|
||||
if dest.exists() {
|
||||
|
||||
@@ -129,10 +129,8 @@ async fn try_download(client: &reqwest::Client, dl: &Download, on_bytes: &(dyn F
|
||||
tokio::fs::create_dir_all(dir).await?;
|
||||
}
|
||||
let resp = client.get(&dl.url).send().await?.error_for_status()?;
|
||||
let part = dl.dest.with_extension(format!(
|
||||
"{}part",
|
||||
dl.dest.extension().map(|e| format!("{}.", e.to_string_lossy())).unwrap_or_default()
|
||||
));
|
||||
let part =
|
||||
dl.dest.with_extension(format!("{}part", dl.dest.extension().map(|e| format!("{}.", e.to_string_lossy())).unwrap_or_default()));
|
||||
let mut file = tokio::fs::File::create(&part).await?;
|
||||
let mut hasher = Sha1::new();
|
||||
let mut stream = resp.bytes_stream();
|
||||
|
||||
+13
-20
@@ -47,13 +47,8 @@ impl Installed {
|
||||
pub fn classpath(&self, layout: &Layout) -> Vec<PathBuf> {
|
||||
let libs_dir = layout.libraries();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut cp: Vec<PathBuf> = self
|
||||
.libraries
|
||||
.iter()
|
||||
.filter(|l| l.classpath)
|
||||
.map(|l| l.file(&libs_dir))
|
||||
.filter(|p| seen.insert(p.clone()))
|
||||
.collect();
|
||||
let mut cp: Vec<PathBuf> =
|
||||
self.libraries.iter().filter(|l| l.classpath).map(|l| l.file(&libs_dir)).filter(|p| seen.insert(p.clone())).collect();
|
||||
cp.push(self.client_jar.clone());
|
||||
cp
|
||||
}
|
||||
@@ -69,11 +64,8 @@ async fn vanilla_json(client: &reqwest::Client, layout: &Layout, mc: &str) -> Re
|
||||
return Ok(serde_json::from_slice(&bytes)?);
|
||||
}
|
||||
};
|
||||
let entry = manifest
|
||||
.versions
|
||||
.iter()
|
||||
.find(|v| v.id == mc)
|
||||
.ok_or_else(|| anyhow!("Minecraft {mc} doesn't exist in Mojang's version list"))?;
|
||||
let entry =
|
||||
manifest.versions.iter().find(|v| v.id == mc).ok_or_else(|| anyhow!("Minecraft {mc} doesn't exist in Mojang's version list"))?;
|
||||
if !http::file_ok(&path, entry.sha1.as_deref(), None, true) {
|
||||
http::download_one(client, &Download::new(entry.url.clone(), path.clone(), entry.sha1.clone(), None), &|_| {}).await?;
|
||||
}
|
||||
@@ -82,11 +74,8 @@ async fn vanilla_json(client: &reqwest::Client, layout: &Layout, mc: &str) -> Re
|
||||
|
||||
fn library_downloads(libs: &[ResolvedLib], layout: &Layout, deep: bool) -> Vec<Download> {
|
||||
let dir = layout.libraries();
|
||||
let mut out: Vec<Download> = libs
|
||||
.iter()
|
||||
.filter(|l| !http::file_ok(&l.file(&dir), l.sha1.as_deref(), l.size, deep))
|
||||
.filter_map(|l| l.download(&dir))
|
||||
.collect();
|
||||
let mut out: Vec<Download> =
|
||||
libs.iter().filter(|l| !http::file_ok(&l.file(&dir), l.sha1.as_deref(), l.size, deep)).filter_map(|l| l.download(&dir)).collect();
|
||||
out.sort_by(|a, b| a.dest.cmp(&b.dest));
|
||||
out.dedup_by(|a, b| a.dest == b.dest);
|
||||
out
|
||||
@@ -146,7 +135,11 @@ pub async fn install(client: &reqwest::Client, layout: &Layout, spec: &InstallSp
|
||||
Loader::Vanilla => None,
|
||||
Loader::Fabric | Loader::Quilt => {
|
||||
let lv = meta::resolve_loader_version(client, spec.loader, mc, spec.loader_version.as_deref()).await?.unwrap();
|
||||
progress::stage(reporter, Stage::Loader, format!("Installing {} {lv}", if spec.loader == Loader::Fabric { "Fabric" } else { "Quilt" }));
|
||||
progress::stage(
|
||||
reporter,
|
||||
Stage::Loader,
|
||||
format!("Installing {} {lv}", if spec.loader == Loader::Fabric { "Fabric" } else { "Quilt" }),
|
||||
);
|
||||
Some(loaders::fabric::profile(client, layout, spec.loader, mc, &lv).await?)
|
||||
}
|
||||
Loader::Forge | Loader::NeoForge => {
|
||||
@@ -204,8 +197,8 @@ pub async fn install(client: &reqwest::Client, layout: &Layout, spec: &InstallSp
|
||||
};
|
||||
|
||||
let assets_root = layout.assets();
|
||||
let game_assets = assets::materialize_legacy(layout, &asset_ref.id, &asset_index, &spec.game_dir)?
|
||||
.unwrap_or_else(|| assets_root.clone());
|
||||
let game_assets =
|
||||
assets::materialize_legacy(layout, &asset_ref.id, &asset_index, &spec.game_dir)?.unwrap_or_else(|| assets_root.clone());
|
||||
|
||||
Ok(Installed {
|
||||
java: java_windowed,
|
||||
|
||||
+18
-11
@@ -86,7 +86,11 @@ pub fn sibling_exe(java: &Path, windowed: bool) -> PathBuf {
|
||||
}
|
||||
let name = if windowed { "javaw.exe" } else { "java.exe" };
|
||||
let candidate = java.with_file_name(name);
|
||||
if candidate.exists() { candidate } else { java.to_path_buf() }
|
||||
if candidate.exists() {
|
||||
candidate
|
||||
} else {
|
||||
java.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
/// Make sure the Mojang runtime `component` (e.g. `java-runtime-delta`) is
|
||||
@@ -102,15 +106,14 @@ pub async fn ensure_runtime(
|
||||
let marker = dir.join(".scopenet-runtime");
|
||||
let installed = std::fs::read_to_string(&marker).ok();
|
||||
|
||||
let index: HashMap<String, HashMap<String, Vec<RuntimeEntry>>> =
|
||||
match http::get_json(client, RUNTIME_INDEX).await {
|
||||
Ok(i) => i,
|
||||
Err(e) if installed.is_some() && java_exe(&dir, false).exists() => {
|
||||
tracing::warn!("java index unreachable ({e}); using installed {component}");
|
||||
return Ok(dir);
|
||||
}
|
||||
Err(e) => return Err(e).context("fetching Java runtime index"),
|
||||
};
|
||||
let index: HashMap<String, HashMap<String, Vec<RuntimeEntry>>> = match http::get_json(client, RUNTIME_INDEX).await {
|
||||
Ok(i) => i,
|
||||
Err(e) if installed.is_some() && java_exe(&dir, false).exists() => {
|
||||
tracing::warn!("java index unreachable ({e}); using installed {component}");
|
||||
return Ok(dir);
|
||||
}
|
||||
Err(e) => return Err(e).context("fetching Java runtime index"),
|
||||
};
|
||||
let entry = index
|
||||
.get(platform_key())
|
||||
.and_then(|p| p.get(component))
|
||||
@@ -188,7 +191,11 @@ pub fn parse_java_version(text: &str) -> Option<u32> {
|
||||
let ver = &rest[..rest.find('"')?];
|
||||
let mut parts = ver.split(['.', '_', '-', '+']);
|
||||
let first: u32 = parts.next()?.parse().ok()?;
|
||||
if first == 1 { parts.next()?.parse().ok() } else { Some(first) }
|
||||
if first == 1 {
|
||||
parts.next()?.parse().ok()
|
||||
} else {
|
||||
Some(first)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -167,12 +167,7 @@ pub struct Command {
|
||||
pub fn build(installed: &Installed, layout: &Layout, opts: &LaunchOptions) -> Command {
|
||||
let v = &installed.version;
|
||||
let sep = if cfg!(windows) { ";" } else { ":" };
|
||||
let classpath = installed
|
||||
.classpath(layout)
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(sep);
|
||||
let classpath = installed.classpath(layout).iter().map(|p| p.to_string_lossy().into_owned()).collect::<Vec<_>>().join(sep);
|
||||
|
||||
let quick_play = v.supports_quick_play();
|
||||
let env = Env::current()
|
||||
@@ -259,11 +254,7 @@ pub fn build(installed: &Installed, layout: &Layout, opts: &LaunchOptions) -> Co
|
||||
/// Java 9+ can read arguments from a file, which sidesteps Windows'
|
||||
/// 32k command-line limit on huge modpacks.
|
||||
fn write_argfile(path: &Path, args: &[String]) -> Result<()> {
|
||||
let body = args
|
||||
.iter()
|
||||
.map(|a| format!("\"{}\"", a.replace('\\', "\\\\").replace('"', "\\\"")))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let body = args.iter().map(|a| format!("\"{}\"", a.replace('\\', "\\\\").replace('"', "\\\""))).collect::<Vec<_>>().join("\n");
|
||||
std::fs::write(path, body)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -340,7 +331,13 @@ mod tests {
|
||||
|
||||
fn opts(join: bool) -> LaunchOptions {
|
||||
LaunchOptions {
|
||||
auth: Auth { username: "Steve".into(), uuid: "b50ad385-829d-3141-a216-7e7d7539ba7f".into(), access_token: "0".into(), user_type: "legacy".into(), xuid: None },
|
||||
auth: Auth {
|
||||
username: "Steve".into(),
|
||||
uuid: "b50ad385-829d-3141-a216-7e7d7539ba7f".into(),
|
||||
access_token: "0".into(),
|
||||
user_type: "legacy".into(),
|
||||
xuid: None,
|
||||
},
|
||||
game_dir: "/g".into(),
|
||||
memory_min_mb: 1024,
|
||||
memory_max_mb: 4096,
|
||||
|
||||
@@ -37,7 +37,11 @@ impl ResolvedLib {
|
||||
}
|
||||
|
||||
fn join_url(base: &str, path: &str) -> String {
|
||||
if base.ends_with('/') { format!("{base}{path}") } else { format!("{base}/{path}") }
|
||||
if base.ends_with('/') {
|
||||
format!("{base}{path}")
|
||||
} else {
|
||||
format!("{base}/{path}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(libs: &[Library], env: &Env) -> Vec<ResolvedLib> {
|
||||
@@ -56,11 +60,7 @@ pub fn resolve(libs: &[Library], env: &Env) -> Vec<ResolvedLib> {
|
||||
if let Some(natives) = &lib.natives {
|
||||
if let Some(classifier) = natives.get(env.os) {
|
||||
let classifier = classifier.replace("${arch}", env.bits());
|
||||
let from_downloads = lib
|
||||
.downloads
|
||||
.as_ref()
|
||||
.and_then(|d| d.classifiers.as_ref())
|
||||
.and_then(|c| c.get(&classifier));
|
||||
let from_downloads = lib.downloads.as_ref().and_then(|d| d.classifiers.as_ref()).and_then(|c| c.get(&classifier));
|
||||
let native_coord = coord.with_classifier(&classifier);
|
||||
let (path, url, sha1, size) = match from_downloads {
|
||||
Some(a) => (
|
||||
@@ -207,10 +207,7 @@ mod tests {
|
||||
assert_eq!(libs.len(), 3);
|
||||
assert!(libs[0].native && !libs[0].classpath);
|
||||
assert_eq!(libs[0].path, "p/w64.jar");
|
||||
assert_eq!(
|
||||
libs[1].url.as_deref(),
|
||||
Some("https://maven.fabricmc.net/net/fabricmc/intermediary/1.20.1/intermediary-1.20.1.jar")
|
||||
);
|
||||
assert_eq!(libs[1].url.as_deref(), Some("https://maven.fabricmc.net/net/fabricmc/intermediary/1.20.1/intermediary-1.20.1.jar"));
|
||||
assert!(libs[2].url.is_none(), "empty url = bundled in installer");
|
||||
}
|
||||
}
|
||||
@@ -253,11 +253,8 @@ async fn run_processors(ctx: &ForgeInstall<'_>, zip: &mut Zip, profile: &Install
|
||||
data.insert("LIBRARY_DIR".into(), libs_dir.to_string_lossy().into_owned());
|
||||
|
||||
let sep = if cfg!(windows) { ";" } else { ":" };
|
||||
let processors: Vec<&Processor> = profile
|
||||
.processors
|
||||
.iter()
|
||||
.filter(|p| p.sides.as_ref().map(|s| s.iter().any(|x| x == "client")).unwrap_or(true))
|
||||
.collect();
|
||||
let processors: Vec<&Processor> =
|
||||
profile.processors.iter().filter(|p| p.sides.as_ref().map(|s| s.iter().any(|x| x == "client")).unwrap_or(true)).collect();
|
||||
let total = processors.len() as u32;
|
||||
|
||||
for (i, proc) in processors.into_iter().enumerate() {
|
||||
|
||||
+4
-11
@@ -106,11 +106,9 @@ pub async fn loader_versions(client: &reqwest::Client, loader: Loader, mc: &str)
|
||||
.collect()
|
||||
}
|
||||
Loader::Forge => {
|
||||
let all: HashMap<String, Vec<String>> =
|
||||
get_json(client, &format!("{FORGE_FILES}/maven-metadata.json")).await?;
|
||||
let promos: ForgePromos = get_json(client, &format!("{FORGE_FILES}/promotions_slim.json"))
|
||||
.await
|
||||
.unwrap_or(ForgePromos { promos: HashMap::new() });
|
||||
let all: HashMap<String, Vec<String>> = get_json(client, &format!("{FORGE_FILES}/maven-metadata.json")).await?;
|
||||
let promos: ForgePromos =
|
||||
get_json(client, &format!("{FORGE_FILES}/promotions_slim.json")).await.unwrap_or(ForgePromos { promos: HashMap::new() });
|
||||
let recommended = promos.promos.get(&format!("{mc}-recommended")).map(|v| format!("{mc}-{v}"));
|
||||
let mut list: Vec<LoaderVersion> = all
|
||||
.get(mc)
|
||||
@@ -137,12 +135,7 @@ pub async fn loader_versions(client: &reqwest::Client, loader: Loader, mc: &str)
|
||||
}
|
||||
|
||||
/// Turn "latest"/"recommended"/empty into a concrete loader version.
|
||||
pub async fn resolve_loader_version(
|
||||
client: &reqwest::Client,
|
||||
loader: Loader,
|
||||
mc: &str,
|
||||
requested: Option<&str>,
|
||||
) -> Result<Option<String>> {
|
||||
pub async fn resolve_loader_version(client: &reqwest::Client, loader: Loader, mc: &str, requested: Option<&str>) -> Result<Option<String>> {
|
||||
if loader == Loader::Vanilla {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
+4
-23
@@ -64,11 +64,7 @@ pub struct McSkin {
|
||||
}
|
||||
|
||||
pub async fn start_device_code(client: &reqwest::Client, client_id: &str) -> Result<DeviceCode> {
|
||||
let resp = client
|
||||
.post(DEVICE_CODE_URL)
|
||||
.form(&[("client_id", client_id), ("scope", SCOPE)])
|
||||
.send()
|
||||
.await?;
|
||||
let resp = client.post(DEVICE_CODE_URL).form(&[("client_id", client_id), ("scope", SCOPE)]).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
bail!("Microsoft rejected the sign-in request: {body}");
|
||||
@@ -116,12 +112,7 @@ pub async fn finish_device_code(client: &reqwest::Client, client_id: &str, code:
|
||||
pub async fn refresh(client: &reqwest::Client, client_id: &str, refresh_token: &str) -> Result<MsaSession> {
|
||||
let resp: TokenResponse = client
|
||||
.post(TOKEN_URL)
|
||||
.form(&[
|
||||
("grant_type", "refresh_token"),
|
||||
("client_id", client_id),
|
||||
("refresh_token", refresh_token),
|
||||
("scope", SCOPE),
|
||||
])
|
||||
.form(&[("grant_type", "refresh_token"), ("client_id", client_id), ("refresh_token", refresh_token), ("scope", SCOPE)])
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
@@ -210,22 +201,12 @@ async fn complete(client: &reqwest::Client, ms_access: &str, refresh_token: Stri
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
let resp = client
|
||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||
.bearer_auth(&mc.access_token)
|
||||
.send()
|
||||
.await?;
|
||||
let resp = client.get("https://api.minecraftservices.com/minecraft/profile").bearer_auth(&mc.access_token).send().await?;
|
||||
if resp.status().as_u16() == 404 {
|
||||
bail!("this Microsoft account doesn't own Minecraft: Java Edition");
|
||||
}
|
||||
let profile: McProfile = resp.error_for_status()?.json().await?;
|
||||
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
|
||||
Ok(MsaSession {
|
||||
refresh_token,
|
||||
mc_access_token: mc.access_token,
|
||||
mc_expires_at: now + mc.expires_in,
|
||||
profile,
|
||||
xuid,
|
||||
})
|
||||
Ok(MsaSession { refresh_token, mc_access_token: mc.access_token, mc_expires_at: now + mc.expires_in, profile, xuid })
|
||||
}
|
||||
@@ -49,12 +49,13 @@ impl Layout {
|
||||
|
||||
/// Keep instance ids filesystem-safe no matter what the panel sends.
|
||||
pub fn sanitize_id(id: &str) -> String {
|
||||
let s: String = id
|
||||
.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' { c } else { '_' })
|
||||
.collect();
|
||||
let s: String = id.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' { c } else { '_' }).collect();
|
||||
let s = s.trim_matches('.').to_string();
|
||||
if s.is_empty() { "_".into() } else { s }
|
||||
if s.is_empty() {
|
||||
"_".into()
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// Join a server-provided relative path onto `base`, refusing anything that
|
||||
@@ -72,7 +73,11 @@ pub fn safe_join(base: &Path, rel: &str) -> Option<PathBuf> {
|
||||
p => out.push(p),
|
||||
}
|
||||
}
|
||||
if out == base { None } else { Some(out) }
|
||||
if out == base {
|
||||
None
|
||||
} else {
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -231,7 +231,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn flattens_motd() {
|
||||
let v: serde_json::Value = serde_json::from_str(r#"{"text":"","extra":[{"text":"Scope","color":"aqua","bold":true},{"text":"Net"}]}"#).unwrap();
|
||||
let v: serde_json::Value =
|
||||
serde_json::from_str(r#"{"text":"","extra":[{"text":"Scope","color":"aqua","bold":true},{"text":"Net"}]}"#).unwrap();
|
||||
assert_eq!(flatten_chat(&v), "§b§lScope§rNet");
|
||||
assert_eq!(flatten_chat(&serde_json::json!("§aHello")), "§aHello");
|
||||
}
|
||||
|
||||
@@ -57,7 +57,11 @@ impl Env {
|
||||
|
||||
/// `${arch}` substitution used in legacy native classifiers.
|
||||
pub fn bits(&self) -> &'static str {
|
||||
if self.arch == "x86" { "32" } else { "64" }
|
||||
if self.arch == "x86" {
|
||||
"32"
|
||||
} else {
|
||||
"64"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,10 +130,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn evaluates_rules() {
|
||||
let rules: Vec<Rule> = serde_json::from_str(
|
||||
r#"[{"action":"allow"},{"action":"disallow","os":{"name":"osx"}}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
let rules: Vec<Rule> = serde_json::from_str(r#"[{"action":"allow"},{"action":"disallow","os":{"name":"osx"}}]"#).unwrap();
|
||||
assert!(allowed(&rules, &env("windows")));
|
||||
assert!(!allowed(&rules, &env("osx")));
|
||||
|
||||
|
||||
@@ -46,7 +46,11 @@ pub fn upsert(game_dir: &Path, name: &str, address: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn format_address(host: &str, port: u16) -> String {
|
||||
if port == 25565 { host.to_string() } else { format!("{host}:{port}") }
|
||||
if port == 25565 {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{host}:{port}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -74,10 +78,7 @@ mod tests {
|
||||
upsert(dir.path(), "Friend", "friend.net").unwrap();
|
||||
upsert(dir.path(), "ScopeNet", "play.scopenet.gg").unwrap();
|
||||
upsert(dir.path(), "ScopeNet SMP", "play.scopenet.gg").unwrap();
|
||||
assert_eq!(
|
||||
names(dir.path()),
|
||||
vec![("ScopeNet SMP".into(), "play.scopenet.gg".into()), ("Friend".into(), "friend.net".into())]
|
||||
);
|
||||
assert_eq!(names(dir.path()), vec![("ScopeNet SMP".into(), "play.scopenet.gg".into()), ("Friend".into(), "friend.net".into())]);
|
||||
assert_eq!(format_address("a.b", 25565), "a.b");
|
||||
assert_eq!(format_address("a.b", 25570), "a.b:25570");
|
||||
}
|
||||
|
||||
@@ -76,11 +76,7 @@ pub async fn sync(
|
||||
continue;
|
||||
};
|
||||
wanted.insert(f.path.replace('\\', "/"));
|
||||
let ok = if changed {
|
||||
http::file_ok(&dest, Some(&f.sha1), Some(f.size), true)
|
||||
} else {
|
||||
dest.exists()
|
||||
};
|
||||
let ok = if changed { http::file_ok(&dest, Some(&f.sha1), Some(f.size), true) } else { dest.exists() };
|
||||
if !ok {
|
||||
downloads.push(Download::new(resolve_url(panel_base, &f.url), dest, Some(f.sha1.clone()), Some(f.size)));
|
||||
}
|
||||
|
||||
@@ -170,11 +170,7 @@ impl VersionJson {
|
||||
}
|
||||
|
||||
pub fn java_component(&self) -> String {
|
||||
self.java_version
|
||||
.as_ref()
|
||||
.map(|j| j.component.clone())
|
||||
.filter(|c| !c.is_empty())
|
||||
.unwrap_or_else(|| "jre-legacy".into())
|
||||
self.java_version.as_ref().map(|j| j.component.clone()).filter(|c| !c.is_empty()).unwrap_or_else(|| "jre-legacy".into())
|
||||
}
|
||||
|
||||
/// True for 1.13+ style argument lists.
|
||||
|
||||
Reference in new issue
Block a user