use std::io::Read;
use std::path::PathBuf;
use yog_data::Value;
const MOJANG_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
const INTERMEDIARY_BASE: &str = "https://maven.fabricmc.net/net/fabricmc/intermediary";
const INTERMEDIARY_ENTRY: &str = "mappings/mappings.tiny";
const MCP_BASE: &str = "https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp_config";
const MCP_METADATA: &str = "maven-metadata.xml";
const MCP_ENTRY: &str = "config/joined.tsrg";
const CACHE: &str = "mappings";
const VERSIONS: &str = "versions";
const ID: &str = "id";
const URL: &str = "url";
const DOWNLOADS: &str = "downloads";
pub const CLIENT_MAPPINGS: &str = "client_mappings";
pub const SERVER_MAPPINGS: &str = "server_mappings";
pub const CLIENT_JAR: &str = "client";
pub const SERVER_JAR: &str = "server";
const VERSION_OPEN: &str = "<version>";
const VERSION_CLOSE: &str = "</version>";
fn get(url: &str) -> Result<Vec<u8>, String> {
let mut response = ureq::get(url).call().map_err(|e| format!("{url}: {e}"))?;
response
.body_mut()
.with_config()
.limit(u64::MAX)
.read_to_vec()
.map_err(|e| format!("{url}: {e}"))
}
fn text(url: &str) -> Result<String, String> {
String::from_utf8(get(url)?).map_err(|e| format!("{url}: {e}"))
}
fn cache_path(name: &str) -> Result<PathBuf, String> {
Ok(yog_home::Home::locate()?.cache_dir().join(CACHE).join(name))
}
fn cached(name: &str, make: impl FnOnce() -> Result<String, String>) -> Result<String, String> {
let path = cache_path(name)?;
let dir = path.parent().map(PathBuf::from).unwrap_or_default();
if let Ok(t) = std::fs::read_to_string(&path) {
return Ok(t);
}
let t = make()?;
std::fs::create_dir_all(&dir)
.and_then(|_| std::fs::write(&path, &t))
.map_err(|e| format!("{}: {e}", path.display()))?;
Ok(t)
}
fn zip_entry(url: &str, entry: &str) -> Result<String, String> {
let bytes = get(url)?;
let mut zip =
zip::ZipArchive::new(std::io::Cursor::new(bytes)).map_err(|e| format!("{url}: {e}"))?;
let mut file = zip
.by_name(entry)
.map_err(|e| format!("{url}: {entry}: {e}"))?;
let mut out = String::new();
file.read_to_string(&mut out)
.map_err(|e| format!("{url}: {e}"))?;
Ok(out)
}
fn json(url: &str) -> Result<Value, String> {
yog_data::json::from_json_text(&text(url)?).ok_or_else(|| format!("{url}: not JSON"))
}
fn download_url(mc: &str, key: &str) -> Result<Option<String>, String> {
let manifest = json(MOJANG_MANIFEST)?;
let version = manifest
.get(VERSIONS)
.and_then(Value::as_list)
.and_then(|vs| {
vs.iter()
.find(|v| v.get(ID).and_then(Value::as_str) == Some(mc))
})
.and_then(|v| v.get(URL)?.as_str())
.ok_or_else(|| format!("Mojang has no version {mc}"))?;
Ok(json(version)?
.get(DOWNLOADS)
.and_then(|d| d.get(key)?.get(URL)?.as_str().map(str::to_owned)))
}
pub fn mojang(mc: &str) -> Result<String, String> {
side_mappings(mc, CLIENT_MAPPINGS)?
.ok_or_else(|| format!("{mc} has no mappings (unobfuscated: its names are Mojang's)"))
}
pub fn side_mappings(mc: &str, key: &str) -> Result<Option<String>, String> {
let name = format!("{mc}.{key}.txt");
if let Some(t) = cache_path(&name)
.ok()
.and_then(|p| std::fs::read_to_string(p).ok())
{
return Ok(Some(t));
}
match download_url(mc, key)? {
Some(url) => cached(&name, || text(&url)).map(Some),
None => Ok(None),
}
}
pub fn game_jar(mc: &str, side: &str) -> Result<Vec<u8>, String> {
let path = cache_path(&format!("{mc}.{side}.jar"))?;
let dir = path.parent().map(PathBuf::from).unwrap_or_default();
if let Ok(b) = std::fs::read(&path) {
return Ok(b);
}
let url = download_url(mc, side)?.ok_or_else(|| format!("{mc} has no {side} jar"))?;
let bytes = get(&url)?;
std::fs::create_dir_all(&dir)
.and_then(|_| std::fs::write(&path, &bytes))
.map_err(|e| format!("{}: {e}", path.display()))?;
Ok(bytes)
}
pub fn intermediary(mc: &str) -> Result<String, String> {
cached(&format!("{mc}.intermediary.tiny"), || {
zip_entry(
&format!("{INTERMEDIARY_BASE}/{mc}/intermediary-{mc}-v2.jar"),
INTERMEDIARY_ENTRY,
)
})
}
pub fn srg(mc: &str) -> Result<String, String> {
cached(&format!("{mc}.srg.tsrg"), || {
let metadata = text(&format!("{MCP_BASE}/{MCP_METADATA}"))?;
let prefix = format!("{mc}-");
let version = metadata
.split(VERSION_OPEN)
.filter_map(|s| s.split(VERSION_CLOSE).next())
.filter(|v| v.starts_with(&prefix))
.last()
.ok_or_else(|| format!("MCPConfig has no build for {mc}"))?;
zip_entry(
&format!("{MCP_BASE}/{version}/mcp_config-{version}.zip"),
MCP_ENTRY,
)
})
}