Files
Unlucky-Launcher/crates/cli/src/context.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

239 lines
8.6 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.
//! 数据根解析与目录视图。
//!
//! 三模式(D6 / `04-data-and-config.md` §1)全部交给 `core::platform::data_dir`:
//! - `--data-dir <dir>` → custom 模式(`<dir>/.ULL` + `<dir>/.minecraft`);
//! - 否则先按便携目录的 `launcher.json` 读 `core.data_mode` / `core.data_custom_root` /
//! `core.launcher_home` / `core.game_home`,再交 [`Resolver`] 解析(含 `redirect.json` 跟随)。
//!
//! CLI 不自己拼路径:目录形状的真相在 `core::platform::layout`。
//!
//! `ull config get/set` 的落点也从这里出:[`Ctx::setting_path_for`] 让决定数据根的
//! `core.*` 键落在**便携目录那份** `launcher.json`(与解析同源),其余键落解析后的数据根。
use std::path::{Path, PathBuf};
use unlucky_core::game::GameLayout;
use unlucky_core::platform::data_dir::{DataDirRequest, DataMode, ResolvedData, Resolver};
use unlucky_core::platform::layout::{
LauncherLayout, CONFIG_DIR, LAUNCHER_CONFIG_FILE, LAUNCHER_HOME_DIR,
};
use unlucky_core::platform::paths::Platform;
use unlucky_core::setting::keys::launcher_schema;
use unlucky_core::setting::store::SettingStore;
use crate::cli::Globals;
use crate::error::CliError;
use crate::os;
/// 决定数据根本身的配置键(`request_from_portable_config` 读的就是它们)。
///
/// `ull config get/set` 对这几个键必须读写**同一份文件**(便携目录那份),否则会出现
/// 「`config get core.data_mode` 说 portable、实际按 standard 跑」这种两套真相
/// (finding 20261030-worker-java2-improve-ull-config-get-set-launcher-json)。
pub const DATA_ROOT_KEYS: [&str; 4] = [
"core.data_mode",
"core.data_custom_root",
"core.launcher_home",
"core.game_home",
];
/// 该键是否决定数据根。
#[must_use]
pub fn is_data_root_key(key: &str) -> bool {
DATA_ROOT_KEYS.contains(&key)
}
/// 便携目录那份 `launcher.json` 的路径(`<exe 目录>/.ULL/config/launcher.json`)。
#[must_use]
pub fn portable_config_path(exe_dir: &Path) -> PathBuf {
exe_dir
.join(LAUNCHER_HOME_DIR)
.join(CONFIG_DIR)
.join(LAUNCHER_CONFIG_FILE)
}
/// 一次命令执行的上下文:数据根 + 平台 + 系统摘要。
#[derive(Debug, Clone)]
pub struct Ctx {
/// 解析后的数据目录(launcher home / game home)。
pub data: ResolvedData,
/// 宿主平台族。
pub platform: Platform,
/// 系统信息与支持等级(D19 探测 / D21 Win10+ 单基线)。
pub os: unlucky_core::platform::os_info::OsInfo,
/// exe 目录(便携目录那份 `launcher.json` 的根;拿不到时为 `None`)。
exe_dir: Option<PathBuf>,
/// `--data-dir` 是否由命令行强制指定(此时数据根不看 `core.*` 键)。
data_dir_forced: bool,
}
impl Ctx {
/// 解析数据根。
///
/// # Errors
///
/// `--data-dir` 非绝对路径、exe 目录拿不到、`redirect.json` 损坏等返回用户错误。
pub fn resolve(globals: &Globals) -> Result<Self, CliError> {
let exe_dir = std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(Path::to_path_buf));
Self::resolve_in(globals, exe_dir)
}
/// 指定 exe 目录的解析入口(单测用它把便携目录钉在临时目录里)。
///
/// # Errors
///
/// 同 [`Self::resolve`]。
pub fn resolve_in(globals: &Globals, exe_dir: Option<PathBuf>) -> Result<Self, CliError> {
let platform = Platform::current();
let data_dir_forced = globals.data_dir.is_some();
let request = match &globals.data_dir {
Some(dir) => {
if !dir.is_absolute() {
return Err(CliError::usage(format!(
"--data-dir 需要绝对路径,收到 `{}`",
dir.display()
)));
}
DataDirRequest {
mode: DataMode::Custom,
custom_root: Some(dir.clone()),
..DataDirRequest::default()
}
}
None => request_from_portable_config(exe_dir.clone()),
};
let resolver = Resolver::new(platform, exe_dir.clone());
let data = resolver
.resolve_default(&request)
.map_err(|error| CliError::user("data_dir", error.to_string()))?;
Ok(Self {
data,
platform,
os: os::detect(),
exe_dir,
data_dir_forced,
})
}
/// 启动器数据布局(`.ULL/` 四分区)。
#[must_use]
pub fn layout(&self) -> LauncherLayout {
self.data.layout()
}
/// 游戏目录布局(`.minecraft/`)。
#[must_use]
pub fn game(&self) -> GameLayout {
GameLayout::new(self.data.game_home.clone())
}
/// `launcher.json` 路径。
#[must_use]
pub fn launcher_config(&self) -> PathBuf {
self.layout().launcher_config()
}
/// 便携目录那份 `launcher.json`(数据根解析的唯一输入;拿不到 exe 目录时为 `None`)。
#[must_use]
pub fn portable_launcher_config(&self) -> Option<PathBuf> {
self.exe_dir.as_deref().map(portable_config_path)
}
/// `--data-dir` 是否由命令行强制指定(此时 `core.*` 数据根键本轮不参与解析)。
#[must_use]
pub const fn data_dir_forced(&self) -> bool {
self.data_dir_forced
}
/// 某个配置键的读写落点。
///
/// 数据根键([`DATA_ROOT_KEYS`])走便携目录那份——和 [`Self::resolve`] 读的是同一份文件,
/// 这样 `config get core.data_mode` 展示的就是真正生效的值,`config set` 也真的会影响
/// 下次解析;其余键走解析后的数据根(`<data>/.ULL/config/launcher.json`)。
#[must_use]
pub fn setting_path_for(&self, key: &str) -> PathBuf {
if is_data_root_key(key) {
if let Some(path) = self.portable_launcher_config() {
return path;
}
}
self.launcher_config()
}
/// `profiles.json` 路径。
#[must_use]
pub fn profiles_config(&self) -> PathBuf {
self.layout().profiles_config()
}
/// 会话根(`<data>/agent/sessions`,launch 的事件流与索引都在这下面)。
#[must_use]
pub fn sessions_root(&self) -> PathBuf {
self.layout().agent_sessions()
}
/// 账户数据目录(`data/auth/`:`accounts.json` + `credentials.dat`)。
#[must_use]
pub fn auth_data(&self) -> PathBuf {
self.layout().auth_data()
}
/// 会话索引文件(`<sessions_root>/game/index.jsonl`)。
#[must_use]
pub fn session_index(&self) -> PathBuf {
unlucky_core::launch::session_index::index_path(&self.sessions_root())
}
/// D19/D21:低于基线(Win10+ / macOS 12+ / Linux 达标)时拒绝执行。
///
/// # Errors
///
/// 系统不支持时返回 `unsupported_os` 用户错误(文案与核心自检同一份)。
pub fn ensure_supported(&self) -> Result<(), CliError> {
let check = os::check();
if check.is_usable() {
return Ok(());
}
Err(CliError::user("unsupported_os", check.message)
.with_hint(format!("最低要求:{}", os::BASELINE)))
}
/// 环境级提示(低于基线时的升级指引;一等基线时为空)。
#[must_use]
pub fn notes(&self) -> Vec<String> {
os::notes(&self.os)
}
}
/// 便携目录的 `launcher.json` → 三模式请求。
///
/// 只读 `effective`(文件值 → 默认值),坏文件也永不 panic(`SettingStore` 的加载降级)。
/// 路径由 [`portable_config_path`] 决定,`config get/set` 用的是同一个函数(同源)。
fn request_from_portable_config(exe_dir: Option<PathBuf>) -> DataDirRequest {
let mut request = DataDirRequest::default();
let Some(exe_dir) = exe_dir else {
return request;
};
let path = portable_config_path(&exe_dir);
if !path.is_file() {
return request;
}
let store = SettingStore::load(&path, launcher_schema());
let text = |key: &str| store.doc().effective_str(key).map(str::to_string);
if let Some(mode) = text("core.data_mode").as_deref().and_then(DataMode::parse) {
request.mode = mode;
}
if let Some(root) = text("core.data_custom_root") {
request.custom_root = Some(PathBuf::from(root));
}
if let Some(home) = text("core.launcher_home") {
request.launcher_home = Some(PathBuf::from(home));
}
if let Some(home) = text("core.game_home") {
request.game_home = Some(PathBuf::from(home));
}
request
}