Add launcher engine (scopenet-core) and admin panel server
- 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
This commit is contained in:
43 files changed
+9317
No files matched your search
@@ -0,0 +1,84 @@
|
||||
//! 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");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user