D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate, Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。 - 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`), workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、 iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。 - `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、 prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、 mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等), 内部实现零改动。 - 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS / SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport / system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。 - crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge| toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*` (64 处),backend.rs 的 config_path/build_backend 提为 pub。 - crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。 - prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
171 lines
6.2 KiB
Rust
171 lines
6.2 KiB
Rust
//! host 的环境快照:一份 `ResolvedData` + 由其派生的布局视图。
|
||
//!
|
||
//! app 自己不解析三模式数据根(那是 `core::platform::data_dir` 的活);这里只做两件事:
|
||
//! ①按「exe 同级便携 `launcher.json` 的 `core.*` 四键」拼请求;②把解析结果与
|
||
//! `Platform` / `DataMode` 打成一个可克隆的结构,交给后台线程。
|
||
//!
|
||
//! 截图/演示通道用 [`HostEnv::at_root`] 直接钉一个根(`<root>/.ULL` + `<root>/.minecraft`)。
|
||
|
||
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;
|
||
use unlucky_core::platform::paths::Platform;
|
||
use unlucky_core::setting::keys::launcher_schema;
|
||
use unlucky_core::setting::store::SettingStore;
|
||
|
||
/// 数据根决定键(与 CLI 的 `DATA_ROOT_KEYS` 同源同义)。
|
||
pub(crate) const DATA_ROOT_KEYS: [&str; 4] = [
|
||
"core.data_mode",
|
||
"core.data_custom_root",
|
||
"core.launcher_home",
|
||
"core.game_home",
|
||
];
|
||
|
||
/// host 运行期需要的一切路径/平台信息(可克隆进线程)。
|
||
#[derive(Debug, Clone)]
|
||
pub struct HostEnv {
|
||
/// 数据根解析结果
|
||
pub data: ResolvedData,
|
||
/// `.ULL/` 布局
|
||
pub layout: LauncherLayout,
|
||
/// `.minecraft/` 布局
|
||
pub game: GameLayout,
|
||
/// 平台
|
||
pub platform: Platform,
|
||
}
|
||
|
||
impl HostEnv {
|
||
/// 按便携目录的 `core.*` 解析真实数据根;解析不出来返回 `None`(host 退回 mock)。
|
||
pub fn resolve() -> Option<Self> {
|
||
let exe_dir = std::env::current_exe()
|
||
.ok()
|
||
.and_then(|path| path.parent().map(Path::to_path_buf))?;
|
||
let request = request_from_portable(&exe_dir);
|
||
let resolver = Resolver::new(Platform::current(), Some(exe_dir));
|
||
let data = resolver.resolve_default(&request).ok()?.clone();
|
||
Some(Self::from_data(data))
|
||
}
|
||
|
||
/// 直接钉一个根:`<root>/.ULL` + `<root>/.minecraft`(截图/演示通道)。
|
||
///
|
||
/// 根下已有 `config/launcher.json` 时**尊重其中的 `core.game_home` / `core.launcher_home`**:
|
||
/// 演示根常用它把游戏目录指到既有真数据上(避免复制几 GB 的 assets/libraries)。
|
||
pub fn at_root(root: impl AsRef<Path>) -> Self {
|
||
let root = root.as_ref();
|
||
let request = request_from_portable(root);
|
||
let absolute = |path: Option<PathBuf>| path.filter(|path| path.is_absolute());
|
||
Self::from_data(ResolvedData {
|
||
mode: DataMode::Portable,
|
||
launcher_home: absolute(request.launcher_home).unwrap_or_else(|| root.join(".ULL")),
|
||
game_home: absolute(request.game_home).unwrap_or_else(|| root.join(".minecraft")),
|
||
redirected_from: None,
|
||
})
|
||
}
|
||
|
||
/// 由解析结果建环境。
|
||
pub fn from_data(data: ResolvedData) -> Self {
|
||
Self {
|
||
layout: data.layout(),
|
||
game: GameLayout::new(data.game_home.clone()),
|
||
platform: Platform::current(),
|
||
data,
|
||
}
|
||
}
|
||
|
||
/// `config/launcher.json` 路径。
|
||
pub fn launcher_config(&self) -> PathBuf {
|
||
self.layout.launcher_config()
|
||
}
|
||
|
||
/// 设置存储(每次现读一份;写操作自己 save)。
|
||
pub fn store(&self) -> SettingStore {
|
||
SettingStore::load(self.launcher_config(), launcher_schema())
|
||
}
|
||
|
||
/// 启动会话根(`.ULL/data/agent/sessions`)。
|
||
pub fn sessions_root(&self) -> PathBuf {
|
||
self.layout.agent_sessions()
|
||
}
|
||
|
||
/// 数据目录模式。
|
||
pub fn mode(&self) -> DataMode {
|
||
self.data.mode
|
||
}
|
||
|
||
/// 人读一行(设置页「数据」分组展示)。
|
||
pub fn data_line(&self) -> String {
|
||
format!(
|
||
"{} · 启动器 {} · 游戏 {}",
|
||
self.data.mode,
|
||
self.data.launcher_home.display(),
|
||
self.data.game_home.display()
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 便携目录那份 `launcher.json` → 数据根请求(只读 `core.*` 四键)。
|
||
pub(crate) fn request_from_portable(exe_dir: &Path) -> DataDirRequest {
|
||
let mut request = DataDirRequest::default();
|
||
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(DATA_ROOT_KEYS[0]).as_deref().and_then(DataMode::parse) {
|
||
request.mode = mode;
|
||
}
|
||
if let Some(root) = text(DATA_ROOT_KEYS[1]) {
|
||
request.custom_root = Some(PathBuf::from(root));
|
||
}
|
||
if let Some(home) = text(DATA_ROOT_KEYS[2]) {
|
||
request.launcher_home = Some(PathBuf::from(home));
|
||
}
|
||
if let Some(home) = text(DATA_ROOT_KEYS[3]) {
|
||
request.game_home = Some(PathBuf::from(home));
|
||
}
|
||
request
|
||
}
|
||
|
||
/// `<exe 目录>/.ULL/config/launcher.json`。
|
||
pub(crate) fn portable_config_path(exe_dir: &Path) -> PathBuf {
|
||
LauncherLayout::new(exe_dir.join(".ULL")).launcher_config()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn at_root_splits_launcher_and_game_homes() {
|
||
let env = HostEnv::at_root("D:/tmp/ull-demo");
|
||
assert_eq!(env.layout.home(), Path::new("D:/tmp/ull-demo/.ULL"));
|
||
assert_eq!(env.game.root(), Path::new("D:/tmp/ull-demo/.minecraft"));
|
||
assert_eq!(env.mode(), DataMode::Portable);
|
||
assert!(env.data_line().contains("portable"));
|
||
}
|
||
|
||
#[test]
|
||
fn portable_request_reads_core_keys_only_when_file_exists() {
|
||
let dir = std::env::temp_dir().join(format!("ull-host-env-{}", std::process::id()));
|
||
std::fs::create_dir_all(dir.join(".ULL/config")).expect("建目录");
|
||
let request = request_from_portable(&dir);
|
||
assert_eq!(request, DataDirRequest::default());
|
||
|
||
std::fs::write(
|
||
portable_config_path(&dir),
|
||
r#"{"core":{"data_mode":"custom","data_custom_root":"D:/data/ull"}}"#,
|
||
)
|
||
.expect("写配置");
|
||
let request = request_from_portable(&dir);
|
||
assert_eq!(request.mode, DataMode::Custom);
|
||
assert_eq!(
|
||
request.custom_root.as_deref(),
|
||
Some(Path::new("D:/data/ull"))
|
||
);
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
}
|