app 从「mock 骨架」切到「真实 core host」:新增 src/host 装配层(env/job/ game/launch/identity/versions/install/meter/java/accounts/logs/settings/engine/ dispatch),是 app 里除 settings_bridge 外唯一 use unlucky_core 的地方,出口只有 crate::dto 的 DTO/事件(三通道纪律,AGENTS §6)。 页面接线: - 实例页:profiles+版本 JSON 继承链+会话索引 → 磁贴(真实版本/加载器徽标/最后启动/ 隔离档位/载荷缺失);启动按钮走 LaunchRequest 装配 → Launcher::spawn → JSONL 尾随 → 运行/失败回写;M30 ModeChangeWarning 进横幅通知条。 - 新建实例向导:版本 → 加载器 → 名字 → 确认 → 真实安装进度;Fabric/Quilt 可装, Forge/NeoForge 因需要 installer processors(仅 CLI 有)显式拒装并给出 CLI 命令。 - 下载页:远端 manifest(12h 缓存)+ 内嵌 unlisted → 分类(正式/预览/远古四子类); 安装进度(文件数/字节/速度/ETA,速度由相邻采样差分)+ 阶段边界可取消; Java 分组:四家发行版 + 回退链可视化(计划链/真实 ChainLog)+ 本地运行时。 - 账户页:三轨账户列表(只读公开字段)+ 离线新增 + 删除/切换 + 微软设备码登录 (用户码/链接/轮询状态;取消后丢弃结果)。凭据只在 host/identity 与 host/accounts 出现,DTO 无 token 字段(单测钉住)。 - 设置页:新增 download/java/data/ai/mcp 五组,行由 core::setting schema 键生成, 写回走 SettingStore::set(与 ull config set / AI set_config 同源 launcher.json)。 - 日志页:会话索引 → 列表;事件 JSONL → 日志行(等级高亮)+ D8 规则引擎诊断卡片; 启动中实时尾随(600ms/拍,只读新增字节);崩溃日志导入分析(hs_err/crash/latest)。 架构与守线: - 新页面状态走 ui/page_state.slint 的 PageState global(main.slint 再导出), Rust 侧 page_sync.rs 灌入、callbacks_pages.rs 接线;避免属性逐层转发顶破 400 行。 - DTO 全部 Serialize+Deserialize(IPC 契约),新增 accounts/java/settings/install/ notices 五个子模块;事件扩展 LogAppended/LaunchFinished/AccountsChanged/ DeviceLoginChanged/JavaChanged/InstallProgress/SettingsChanged/NoticesChanged。 - 截图通道扩展:--data-dir(钉数据根且 settings 同源)/--mock/--screenshot-page/ --screenshot-wizard/--screenshot-session/--screenshot-install/--screenshot-launch (真实中间态,取图后自动 stop 防孤儿进程)。 已知边界:GUI 账户「当前启动账户」仅内存态(core schema 无 auth.* 键,app 不自建键); Forge/NeoForge GUI 安装待 processors 执行器上移。
153 lines
7.1 KiB
Rust
153 lines
7.1 KiB
Rust
//! `CoreHost::dispatch` 的指令实现(从 `host/mod.rs` 拆出,守 400 行上限)。
|
||
//!
|
||
//! 父模块的私有字段对子模块可见,所以这里直接读写 `self.env` / `self.tx` 等。
|
||
|
||
use crate::dto::{AppCommand, Backend, CoreEvent, CreateRequest, LoaderKind};
|
||
use crate::toast::ToastKind;
|
||
|
||
use super::accounts::{self, LoginJob};
|
||
use super::{game, install, java, launch, logs, settings, versions, CoreHost};
|
||
|
||
impl CoreHost {
|
||
/// 下发一条用户意图(`Backend::dispatch` 的实现体)。
|
||
pub(super) fn dispatch_command(&mut self, command: AppCommand) {
|
||
self.dispatch_inner(command);
|
||
}
|
||
}
|
||
impl CoreHost {
|
||
/// 指令分发(见 `dispatch_command`)。
|
||
fn dispatch_inner(&mut self, command: AppCommand) {
|
||
match command {
|
||
AppCommand::RefreshAll => self.refresh_all(),
|
||
AppCommand::LaunchInstance(id) => self.start_launch(&id),
|
||
AppCommand::StopInstance(id) => match self.running_pid(&id) {
|
||
Some(pid) => match launch::stop(pid) {
|
||
Ok(()) => self.toast(ToastKind::Info, "已请求停止游戏进程"),
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
None => self.toast(ToastKind::Warn, "这个实例当前没有在跑"),
|
||
},
|
||
AppCommand::ListVersions(_) => versions::spawn_list(self.env.clone(), self.tx.clone()),
|
||
AppCommand::InstallVersion(version) => {
|
||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||
self.install_cancel = Some(std::sync::Arc::clone(&cancel));
|
||
install::spawn_install(
|
||
self.env.clone(),
|
||
self.tx.clone(),
|
||
CreateRequest {
|
||
name: version.clone(),
|
||
version,
|
||
loader: LoaderKind::Vanilla,
|
||
loader_version: String::new(),
|
||
},
|
||
cancel,
|
||
);
|
||
}
|
||
AppCommand::CreateInstance(request) => {
|
||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||
self.install_cancel = Some(std::sync::Arc::clone(&cancel));
|
||
install::spawn_install(self.env.clone(), self.tx.clone(), request, cancel);
|
||
}
|
||
AppCommand::CancelInstall => {
|
||
// 取消只在阶段边界生效(core 的下载循环没有取消钩子):这里先把面板收回,
|
||
// 后台线程会在下一阶段开始前看到标记并收尾。
|
||
if let Some(cancel) = &self.install_cancel {
|
||
cancel.store(true, std::sync::atomic::Ordering::SeqCst);
|
||
}
|
||
self.install = None;
|
||
self.send(CoreEvent::InstallProgress(None));
|
||
}
|
||
AppCommand::SetIsolation { id, mode } => {
|
||
match game::set_isolation(&self.env, &id, &mode) {
|
||
Ok(()) => {
|
||
self.toast(ToastKind::Success, "隔离档位已更新");
|
||
self.refresh_instances();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
}
|
||
}
|
||
AppCommand::RefreshLogSessions => {
|
||
self.send(CoreEvent::LogSessionsChanged(logs::sessions(&self.env)));
|
||
}
|
||
AppCommand::OpenLogSession(id) => self.open_session(&id),
|
||
AppCommand::ImportCrashLog(path) => match logs::analyze_crash_file(&path) {
|
||
Ok((label, report)) => {
|
||
self.send(CoreEvent::DiagnosisReady(report));
|
||
self.toast(ToastKind::Info, label);
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Warn, reason),
|
||
},
|
||
AppCommand::AddOfflineAccount(name) => match accounts::add_offline(&self.env, &name) {
|
||
Ok(display) => {
|
||
self.toast(ToastKind::Success, format!("已添加离线账户 {display}"));
|
||
self.refresh_accounts();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
AppCommand::RemoveAccount(key) => match accounts::remove(&self.env, &key) {
|
||
Ok(display) => {
|
||
if self.active_account.as_deref() == Some(key.as_str()) {
|
||
self.active_account = None;
|
||
}
|
||
self.toast(ToastKind::Info, format!("已删除账户 {display}"));
|
||
self.refresh_accounts();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
AppCommand::SwitchAccount(id) => {
|
||
self.active_account = Some(id);
|
||
self.refresh_accounts();
|
||
self.toast(ToastKind::Info, "已切换启动账户");
|
||
}
|
||
AppCommand::StartDeviceLogin => {
|
||
self.login = Some(LoginJob::new());
|
||
let job = self.login.clone().unwrap_or_default();
|
||
self.send(CoreEvent::DeviceLoginChanged(crate::dto::DeviceLoginView {
|
||
phase: crate::dto::DeviceLoginPhase::Requesting,
|
||
message: "正在向微软申请设备码…".to_string(),
|
||
..crate::dto::DeviceLoginView::new()
|
||
}));
|
||
accounts::start_login(self.env.clone(), self.tx.clone(), job);
|
||
}
|
||
AppCommand::CancelDeviceLogin => {
|
||
if let Some(job) = &self.login {
|
||
job.cancel();
|
||
}
|
||
self.login = None;
|
||
self.send(CoreEvent::DeviceLoginChanged(
|
||
crate::dto::DeviceLoginView::new(),
|
||
));
|
||
}
|
||
AppCommand::InstallJava(vendor) => {
|
||
self.java_busy = true;
|
||
self.java_message = format!("正在安装 Java({vendor})…");
|
||
self.refresh_java();
|
||
java::spawn_install(
|
||
self.env.clone(),
|
||
self.tx.clone(),
|
||
unlucky_core::game::java::JdkVendor::parse(&vendor),
|
||
);
|
||
}
|
||
AppCommand::SetSetting { key, value } => match settings::set(&self.env, &key, &value) {
|
||
Ok(()) => {
|
||
self.send(CoreEvent::SettingsChanged(settings::groups(&self.env)));
|
||
self.java_chain.clear();
|
||
self.refresh_java();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
AppCommand::DismissNotice(id) => {
|
||
self.notices.retain(|notice| notice.id != id);
|
||
self.send(CoreEvent::NoticesChanged(self.notices.clone()));
|
||
}
|
||
// AI 对话:agent 的 GUI host 未落地前用脚本回复(不接真实模型)
|
||
other @ (AppCommand::AskAgent(_) | AppCommand::StopAgent | AppCommand::SetYolo(_)) => {
|
||
self.chat.dispatch(other);
|
||
for event in self.chat.drain_events() {
|
||
self.events.push_back(event);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|