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,83 @@
|
||||
//! Game assets (sounds, languages, textures) — content-addressed objects.
|
||||
|
||||
use crate::http::{self, Download};
|
||||
use crate::paths::Layout;
|
||||
use crate::version::AssetIndexRef;
|
||||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const RESOURCES: &str = "https://resources.download.minecraft.net";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AssetIndex {
|
||||
pub objects: HashMap<String, AssetObject>,
|
||||
#[serde(default)]
|
||||
pub map_to_resources: bool,
|
||||
#[serde(default, rename = "virtual")]
|
||||
pub is_virtual: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct AssetObject {
|
||||
pub hash: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
pub fn object_path(layout: &Layout, hash: &str) -> PathBuf {
|
||||
layout.assets().join("objects").join(&hash[..2]).join(hash)
|
||||
}
|
||||
|
||||
/// Load (downloading if needed) the asset index.
|
||||
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?;
|
||||
}
|
||||
Ok(serde_json::from_slice(&std::fs::read(&path)?)?)
|
||||
}
|
||||
|
||||
/// Downloads needed for missing/corrupt objects.
|
||||
pub fn missing_objects(layout: &Layout, index: &AssetIndex, deep: bool) -> Vec<Download> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
index
|
||||
.objects
|
||||
.values()
|
||||
.filter(|o| o.hash.len() > 2 && seen.insert(o.hash.clone()))
|
||||
.filter_map(|o| {
|
||||
let path = object_path(layout, &o.hash);
|
||||
if http::file_ok(&path, Some(&o.hash), Some(o.size), deep) {
|
||||
None
|
||||
} else {
|
||||
Some(Download::new(format!("{RESOURCES}/{}/{}", &o.hash[..2], o.hash), path, Some(o.hash.clone()), Some(o.size)))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Pre-1.7 versions read assets by name, not hash. Copy them into the
|
||||
/// virtual (or `resources/`) directory. Returns the directory to pass as
|
||||
/// `game_assets`.
|
||||
pub fn materialize_legacy(layout: &Layout, index_id: &str, index: &AssetIndex, game_dir: &Path) -> Result<Option<PathBuf>> {
|
||||
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)
|
||||
};
|
||||
for (name, obj) in &index.objects {
|
||||
let Some(dest) = crate::paths::safe_join(&target, name) else { continue };
|
||||
if dest.exists() {
|
||||
continue;
|
||||
}
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::copy(object_path(layout, &obj.hash), &dest).ok();
|
||||
}
|
||||
Ok(Some(target))
|
||||
}
|
||||
Reference in new issue
Block a user