- scopenet-shared: wire types for branding, auth, instances, manifests - scopenet-core: Mojang versions/libraries/assets, automatic Java runtimes, Fabric/Quilt/Forge/NeoForge installation, file sync, launch command builder, Microsoft auth, servers.dat injection, server list ping - scopenet-panel: Axum + SQLite admin API with JWT auth, users/groups, branding, settings, instances, Modrinth/CurseForge/zip import, file hosting and launch stats Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
85 lines
3.0 KiB
Rust
85 lines
3.0 KiB
Rust
//! Adds the instance's server to Minecraft's multiplayer list.
|
|
|
|
use anyhow::Result;
|
|
use fastnbt::Value;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
/// Insert (or update) `name`/`address` at the top of `servers.dat`,
|
|
/// preserving every other entry the player has added.
|
|
pub fn upsert(game_dir: &Path, name: &str, address: &str) -> Result<()> {
|
|
let path = game_dir.join("servers.dat");
|
|
let mut root: HashMap<String, Value> = match std::fs::read(&path) {
|
|
Ok(bytes) => match fastnbt::from_bytes::<Value>(&bytes) {
|
|
Ok(Value::Compound(map)) => map,
|
|
_ => HashMap::new(),
|
|
},
|
|
Err(_) => HashMap::new(),
|
|
};
|
|
|
|
let mut servers = match root.remove("servers") {
|
|
Some(Value::List(list)) => list,
|
|
_ => Vec::new(),
|
|
};
|
|
|
|
let same = |v: &Value| match v {
|
|
Value::Compound(m) => matches!(m.get("ip"), Some(Value::String(ip)) if ip.eq_ignore_ascii_case(address)),
|
|
_ => false,
|
|
};
|
|
let existing = servers.iter().position(same);
|
|
let mut entry = match existing {
|
|
Some(i) => match servers.remove(i) {
|
|
Value::Compound(m) => m,
|
|
_ => HashMap::new(),
|
|
},
|
|
None => HashMap::new(),
|
|
};
|
|
entry.insert("name".into(), Value::String(name.to_string()));
|
|
entry.insert("ip".into(), Value::String(address.to_string()));
|
|
entry.entry("acceptTextures".into()).or_insert(Value::Byte(1));
|
|
servers.insert(0, Value::Compound(entry));
|
|
|
|
root.insert("servers".into(), Value::List(servers));
|
|
std::fs::create_dir_all(game_dir)?;
|
|
std::fs::write(&path, fastnbt::to_bytes(&Value::Compound(root))?)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn format_address(host: &str, port: u16) -> String {
|
|
if port == 25565 { host.to_string() } else { format!("{host}:{port}") }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn names(dir: &Path) -> Vec<(String, String)> {
|
|
let v: Value = fastnbt::from_bytes(&std::fs::read(dir.join("servers.dat")).unwrap()).unwrap();
|
|
let Value::Compound(root) = v else { panic!() };
|
|
let Some(Value::List(list)) = root.get("servers") else { panic!() };
|
|
list.iter()
|
|
.map(|s| match s {
|
|
Value::Compound(m) => match (m.get("name"), m.get("ip")) {
|
|
(Some(Value::String(n)), Some(Value::String(i))) => (n.clone(), i.clone()),
|
|
_ => panic!(),
|
|
},
|
|
_ => panic!(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn upserts_without_losing_player_servers() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
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!(format_address("a.b", 25565), "a.b");
|
|
assert_eq!(format_address("a.b", 25570), "a.b:25570");
|
|
}
|
|
}
|