发布说明
v1.4.0 — 飞船管理、渐进式模组选择与 UI 性能(2026-09-18)
本版本为实例和存档新增了完整的飞船(.craft)管理器,支持拖放导入;使推荐/建议对话框变为渐进式且按模块处理;通过懒加载加速实例详情页;并让模组列表按实例持久化其视图状态。
飞船管理(实例与存档)
新的飞船管理器可为任意实例和任意存档列出、查看并导入 .craft 文件,布局仿照存档管理器的标签页放置方式。
- 实例飞船管理器(
shiptabpage.{h,cpp},新增)—— 一个QTabWidget,含 VAB / SPH 标签页,列出.craft文件(自动排除.loadmeta、.craft.original等);每行显示飞船名称(去掉.craft)和游戏版本(从version = X.Y.Z解析,缺失时显示“未知”)。 - 飞船详情(列表内第二页)—— 点击一行会打开名称(
ship = XXX)、游戏版本和只读描述(description = XXX),右侧显示来自Ships/@thumbs/VAB|SPH的匹配缩略图(缺失时使用火箭 SVG 回退图)。 - 解析(
instancemanager_ships.cpp)——listCraftFiles(按文件名排序,仅.craft)、loadCraftInfo(只读ship/version/description,名称回退到文件名)、getShipThumbPath(png/jpg)、moveCraftToTrash。 - 入口点—— 实例详情侧边栏新增“飞船管理”第 5 个标签页(
showSection(5),rocket.svg);存档详情页新增“飞船管理”第 3 个标签页,通过可设置的基路径(setShipsBase)复用同一个ShipTabPage,该路径会自动定位到instanceRoot/saves/saveName/Ships/VAB|SPH。 - 导入飞船—— 列表工具栏新增“导入飞船”按钮(
download.svg);可通过QFileDialog(多选)导入,或直接将.craft拖放到 VAB/SPH 标签页;导入到当前标签页的文件夹;名称冲突不区分大小写,会弹出“覆盖飞船”确认(是 = 覆盖 / 否 = 跳过 / 取消 = 中止剩余操作),并保留目标原有大小写。 - 删除到回收站—— 每行现在都有垃圾桶按钮(存档列表中旧的装饰性箭头也一并替换);删除操作在 Windows 上将
.craft/ 存档文件夹移至系统回收站(SHFileOperation(FO_DELETE + FOF_ALLOWUNDO)),其他平台则永久删除。 - 翻译/测试—— 新的 ShipTabPage 字符串已全部翻译为 en_US(0 个未完成);
test_launcher新增TestShips(顶层键解析、嵌套键不会覆盖、名称回退、列表过滤、缩略图定位)。导入/回收站流程没有自动化测试(它们依赖模态对话框和实例路径,符合项目惯例)。
渐进式、按模块的推荐/建议对话框
现在安装模块时,会逐个模块解析并询问其推荐/建议,而不是将所有内容聚合成一个巨大列表。
- 收集(
relationshipresolver.{h,cpp}、ckan.{h,cpp})—— 新增collectOptionalFor(parent, curInstallSet, wantRecommends),仅收集单个父模块自身的 Recommends/Suggests 候选项,跳过已安装/已选择/冲突项,且不自动级联。旧的全量聚合不再用于弹窗。 - 安装流程(
installservice.cppresolveInstallSet)—— 两阶段渐进式解析:- 阶段 1(推荐)—— 对每个显式安装的模块,一次显示一个推荐对话框;将选择合并到安装集,并将新添加的模块加入队列(FIFO),直到没有新模块出现。
- 阶段 2(建议)—— 对每个显式模块显示一个建议对话框;如果新选择的模块的推荐未在阶段 1 显示,则先询问这些推荐,再询问其建议。
- 每个模块只安装一次(按标识符去重);没有候选项的模块静默跳过;每个对话框标题标明其来源父模块。
- 选择对话框(
moddecision.{h,cpp})——askOptionalModules新增了一个 Trak-a-dock 风格的“全选 / 全不选”切换按钮,会根据当前状态翻转,并新增parentName上下文标题。取消按钮已移除——唯一退出方式是“安装所选”;放弃安装会返回模组管理。 - 测试——
test_libckan新增collectOptionalNoCascade(按模块、不级联、无交叉污染)和collectOptionalSkipsInstalledAndSelected。全部通过。
模组管理 UI(分隔条与按实例状态)
- 可拖动的列表/详情分隔条(
modstabpage.cpp、configmanager.{h,cpp})—— 一个垂直QSplitter(modSplitter)位于模组表格和 4 个详情标签页(元数据/文件/关系/版本)之间;拖动可调整列表高度(列表保持在上方,详情标签页下移)。初始为 3:2 分割,两个窗格均不可折叠;底部操作栏保持原位。顶部窗格高度持久化到HKSPL.json(modSplitterTopHeight,全局),以 250ms 防抖保存;在首次showEvent中按分隔条的实际高度精确应用。 - 按实例列表状态(
configmanager.{h,cpp}、modstabpage.cpp)—— 每个实例的模组列表在实例切换/重启后恢复精确视图:搜索文本、状态过滤(全部/已安装/可升级/未安装)、标签过滤、详情标签页索引、排序列+顺序、垂直滚动位置和所选模块。变更时保存(300ms 防抖;切换实例前刷新)到专用modListViewState区段,以实例 id 为键。在异步列表加载后恢复(restoreListStateAfterLoad),并用m_restorePending标志区分首次进入加载与同实例刷新。
实例详情页——懒加载
进入实例管理不再预先加载所有内容;每个次级标签页仅在进入时获取其数据。
instancedetailpage.cpp的showSection——模组管理标签页触发prepareMods()(异步索引 + DLL 扫描),存档标签页触发loadSaves(),飞船标签页触发loadShips()。页面打开在“游戏设置”上,前期不执行繁重工作。- 模组标签页(
modstabpage.{h,cpp})——setInstance仅记录实例,setTabActive(true)在每次进入时重新准备模组。 - 存档标签页(
savestabpage.{h,cpp})——loadSaves()在QtConcurrent::run后台线程中运行,遍历目录并逐文件解析persistent.sfs,通过QFutureWatcher填充列表(带有“正在加载存档...”占位符);删除后刷新复用同一异步路径。 - 飞船标签页(
shiptabpage.{h,cpp})——loadShips()在后台线程中解析 VAB + SPH,并通过 watcher 填充;行构建被拆分为addShipRow。 - 线程安全——
InstanceManager::listSaves/loadSaveInfo/listCraftFiles/loadCraftInfo是纯文件读取,没有共享可变状态,可安全地在主线程之外调用。
崩溃日志对话框——上下文与一键日志打包
- 上下文窗口(
playerloganalyzer.{h,cpp})——analyzePlayerLog新增context字段;extractPlayerLogContext取关键错误行(崩溃标记Caught fatal signal/ OOM),并从该处向上回溯最多 40 行(kContextLeadingLines),再取到日志末尾。 - 对话框(
mainwindow.cpp的maybeShowCrashAnalysis)—— 一个只读、可选择的QPlainTextEdit(最小高度 240px)现在会在消息框中粘贴错误上下文,并新增“保存日志”按钮。 - 一键打包(
mainwindow.{h,cpp}的packLogToZip)—— 将整个Player.log流式写入 zip(使用 miniz 回调,绝不把数百 MB 的日志加载到内存),保存到启动器旁边,命名为<cleaned-instance>-<id8>_<yyMMdd_HHmmss>.zip;非法文件名字符替换为下划线。成功时显示完整路径;失败时发出警告,且不会留下不完整的 zip。 - 测试——
test_launcher新增三个extractPlayerLogContext用例(≥40 行回溯起点、短日志头部起点、无崩溃返回空)。
UI 润色
- 次级侧边栏对齐—— 实例详情和存档详情的次级菜单现在作为左列贯穿整个窗口高度,宽度统一为 220px,与主页侧边栏一致(返回/标题栏移入内容区域顶部)。
技术栈
- 框架:Qt 6(Widgets、Svg、Network、Concurrent)
- 语言:C++17
- 构建:CMake ≥ 3.16;Qt 6.11
- 测试:
test_libckan+test_launcher(全部通过)