P2-1:wizard.slint 的 Forge/NeoForge 提示还写着「需要 installer processors:本轮请用 ull install」, 改成「需要 installer processors(首装会外调 java,约 1–3 分钟)」;全仓再扫一遍 「ull install / 显式拒装 / 终端跑」等措辞,user-facing 文案已无残留(余下命中全是 OptiFine/LiteLoader 的正确说明与测试注释)。 P2-2:进度条在处理器依赖库下完那一刻就满格(36/36),随后加载器库的 29 个文件才被计入 → 100% 回落再涨回,且 bytes_total 用 .max() 与文件数对不上。 - Meter 新增 expect(files, bytes):文件数与字节都 saturating_add; - install_loader 在处理器段之前就把加载器库的文件/字节一次预记账(两段之和), 加载器库阶段只剩切阶段文案,不再记账; - Reporter::expect 改为委托 Meter::expect,处理器依赖库沿用同一条曲线; - 回归测试钉住:预记账后 files_total=95、bytes_total=17MB、ratio 不再满格, 处理器库下完仍是中间态,加载器库下完才到 100%。 真机重截 + 识图:D:/tmp/ull-m45-shots/forge-mid-fixed-25s.png —— 同一时刻面板 36/65 文件、3.4 MB/15.3 MB、进度条只填了约一半(旧图 36/36 + 满格)。
81 lines
3.0 KiB
Rust
81 lines
3.0 KiB
Rust
//! 安装进度的记账器(文件计数 / 字节 / 速度 / ETA)。
|
||
//!
|
||
//! `execute` 的文件级回调已经把每个文件的结局给了我们,这里只做聚合;
|
||
//! 速度用相邻两次采样的字节差算(core 的事件只给绝对值)。
|
||
|
||
use std::sync::Arc;
|
||
use std::time::Instant;
|
||
|
||
use unlucky_core::game::install::{FileOutcome, FileStatus, ProgressSink};
|
||
|
||
use crate::dto::{derive_speed, CreateRequest, InstallProgressView, InstallStage};
|
||
|
||
use super::install::loader_label;
|
||
use super::job::{HostTask, JobSender};
|
||
/// 一次安装的上下文(进度回调共享)。
|
||
pub(super) struct Meter {
|
||
pub(super) progress: InstallProgressView,
|
||
previous: Option<(u64, Instant)>,
|
||
}
|
||
|
||
impl Meter {
|
||
pub(super) fn new(request: &CreateRequest) -> Self {
|
||
Self {
|
||
progress: InstallProgressView::start(
|
||
&request.version,
|
||
&request.instance_name(),
|
||
&loader_label(request.loader, &request.loader_version),
|
||
),
|
||
previous: None,
|
||
}
|
||
}
|
||
|
||
/// 换阶段(重置速度采样)。
|
||
pub(super) fn stage(&mut self, stage: InstallStage, message: &str) {
|
||
self.progress.stage = stage;
|
||
self.progress.message = message.to_string();
|
||
self.previous = None;
|
||
}
|
||
|
||
/// 预记账一段待下载的文件(文件数 + 字节)。
|
||
///
|
||
/// 一次安装可能分几段(原版 / 处理器依赖库 / 加载器库):每段开工前都先记账,
|
||
/// 整条进度曲线才是单调的——某一段下完不会先到 100%,再因为下一段的文件被计入而回落。
|
||
pub(super) fn expect(&mut self, files: usize, bytes: u64) {
|
||
let files = u64::try_from(files).unwrap_or(u64::MAX);
|
||
self.progress.files_total = self.progress.files_total.saturating_add(files);
|
||
self.progress.bytes_total = self.progress.bytes_total.saturating_add(bytes);
|
||
}
|
||
|
||
/// 记账一个文件的结局。
|
||
pub(super) fn note(&mut self, outcome: &FileOutcome) {
|
||
self.progress.files_done += 1;
|
||
if outcome.status != FileStatus::Skipped {
|
||
self.progress.bytes_done = self.progress.bytes_done.saturating_add(outcome.bytes);
|
||
}
|
||
let now = Instant::now();
|
||
if let Some(previous) = self.previous {
|
||
if let Some((speed, eta)) = derive_speed(
|
||
previous,
|
||
(self.progress.bytes_done, now),
|
||
self.progress.bytes_total,
|
||
) {
|
||
self.progress.speed_bps = speed;
|
||
self.progress.eta_secs = eta;
|
||
}
|
||
}
|
||
self.previous = Some((self.progress.bytes_done, now));
|
||
}
|
||
}
|
||
|
||
/// 进度 sink(`execute` 的文件级回调)。
|
||
pub(super) fn sink_for(tx: JobSender, meter: Arc<std::sync::Mutex<Meter>>) -> ProgressSink {
|
||
Arc::new(move |outcome: &FileOutcome| {
|
||
let Ok(mut meter) = meter.lock() else {
|
||
return;
|
||
};
|
||
meter.note(outcome);
|
||
let _ = tx.send(HostTask::Install(Box::new(meter.progress.clone())));
|
||
})
|
||
}
|