Files
Unlucky-Launcher/crates/core/src/game/layout.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

166 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `.minecraft` 实例目录布局:`versions/` `libraries/` `assets/` + ULL 扩展 `.ull/`。
//!
//! 依据:`DevDocs/analyse/03-version-download.md` §6.8(目录布局速查)、
//! `DevDocs/PlanDoc/04-data-and-config.md` §2(`.minecraft/` 遵循 Mojang 标准 +
//! ULL 扩展 `.ull/`)。
//!
//! 布局是**纯路径计算**:不建目录、不写文件。所有路径规则集中在这里,
//! 这样将来要支持 MultiMC 家族布局(`instances/<id>/minecraft/`)只需再加一个实现
//! (analyse/01 §3.3 的 `GameRepositoryLayout` 抽象意义)。
use std::path::{Path, PathBuf};
use super::error::GameError;
use super::library::MavenCoordinate;
/// 一个游戏根目录(通常就是 `.minecraft/`)的布局规则。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GameLayout {
root: PathBuf,
}
impl GameLayout {
/// 以给定根目录构造布局。
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
/// 根目录。
pub fn root(&self) -> &Path {
&self.root
}
/// `versions/`。
pub fn versions_dir(&self) -> PathBuf {
self.root.join("versions")
}
/// `libraries/`。
pub fn libraries_dir(&self) -> PathBuf {
self.root.join("libraries")
}
/// `assets/`。
pub fn assets_dir(&self) -> PathBuf {
self.root.join("assets")
}
/// `assets/indexes/`。
pub fn indexes_dir(&self) -> PathBuf {
self.assets_dir().join("indexes")
}
/// `assets/objects/`(两级分片,避开 NTFS 单目录塌陷)。
pub fn objects_dir(&self) -> PathBuf {
self.assets_dir().join("objects")
}
/// `assets/log_configs/`(日志配置缓存)。
pub fn log_configs_dir(&self) -> PathBuf {
self.assets_dir().join("log_configs")
}
/// 实例根目录 `versions/<id>/`。
pub fn version_dir(&self, id: &str) -> PathBuf {
self.versions_dir().join(id)
}
/// 实例清单 `versions/<id>/<id>.json`。
pub fn version_json(&self, id: &str) -> PathBuf {
self.version_dir(id).join(format!("{id}.json"))
}
/// 实例 jar `versions/<id>/<id>.jar`。
pub fn version_jar(&self, id: &str) -> PathBuf {
self.version_dir(id).join(format!("{id}.jar"))
}
/// natives 解压目录 `versions/<id>/natives/`(ULL 约定,HMCL 用 `natives/<ver>`)。
pub fn natives_dir(&self, id: &str) -> PathBuf {
self.version_dir(id).join("natives")
}
/// 实例私有状态目录 `versions/<id>/.ull/`(上次会话日志索引、安装收据等)。
pub fn version_state_dir(&self, id: &str) -> PathBuf {
self.version_dir(id).join(".ull")
}
/// 实例私有库目录 `versions/<id>/libraries/`(HMCL `hint=local` 的落点)。
pub fn version_libraries_dir(&self, id: &str) -> PathBuf {
self.version_dir(id).join("libraries")
}
/// 启动器全局私有目录 `<root>/.ull/`。
pub fn ull_root(&self) -> PathBuf {
self.root.join(".ull")
}
/// 资源索引 `assets/indexes/<assetsId>.json`。
pub fn asset_index(&self, assets_id: &str) -> PathBuf {
self.indexes_dir().join(format!("{assets_id}.json"))
}
/// 资源对象 `assets/objects/<hash[0..2]>/<hash>`。
///
/// # Errors
///
/// 哈希短于两个字符时返回 [`GameError::InvalidAssetHash`]。
pub fn asset_object(&self, hash: &str) -> Result<PathBuf, GameError> {
let shard = hash.get(0..2).ok_or_else(|| GameError::InvalidAssetHash {
hash: hash.to_owned(),
})?;
Ok(self.objects_dir().join(shard).join(hash))
}
/// 公共库 `libraries/<group path>/<artifact>-<version>[-<classifier>].<ext>`。
pub fn library(&self, coordinate: &MavenCoordinate) -> PathBuf {
self.libraries_dir().join(coordinate.relative_path())
}
/// 实例私有库 `versions/<id>/libraries/...`。
pub fn version_library(&self, version_id: &str, coordinate: &MavenCoordinate) -> PathBuf {
self.version_libraries_dir(version_id)
.join(coordinate.relative_path())
}
/// 日志配置文件 `assets/log_configs/<id>`。
pub fn log_config(&self, file_id: &str) -> PathBuf {
self.log_configs_dir().join(file_id)
}
/// 该版本是否已安装(判据:清单文件存在)。
pub fn is_version_installed(&self, id: &str) -> bool {
self.version_json(id).is_file()
}
/// 列出已安装的版本 id(有 `versions/<id>/<id>.json` 的目录),按字典序排列。
///
/// # Errors
///
/// 读 `versions/` 失败时返回 [`GameError::Io`];目录不存在视为空列表。
pub fn installed_versions(&self) -> Result<Vec<String>, GameError> {
let dir = self.versions_dir();
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(GameError::io(&dir, e)),
};
let mut ids = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| GameError::io(&dir, e))?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if path.join(format!("{name}.json")).is_file() {
ids.push(name.to_owned());
}
}
ids.sort();
Ok(ids)
}
}