# 架构说明 本文描述 Space Engineers Blueprint Editor 当前代码的主要边界、依赖方向和运行时数据流。构建操作见[开发指南](DEVELOPMENT.md)。 ## 设计约束 项目的核心难点不是 XML 本身,而是在游戏进程外复用《Space Engineers》的 ObjectBuilder、定义和序列化系统。架构由以下约束塑造: - 游戏程序集和内容受游戏安装约束,不能随仓库或应用分发。 - 游戏核心类型仍面向 .NET Framework,桌面界面则使用 .NET 8 和 WinUI 3。 - 部分 VRage/Sandbox 初始化通常依赖完整游戏平台与渲染环境,编辑器只需要其中的定义和序列化能力。 - 蓝图结构层级深、类型多,手写每一种属性编辑器维护成本很高。 - 保存结果必须保持游戏可识别的 ObjectBuilder XML 结构。 ## 系统上下文 ```mermaid flowchart TB subgraph External["外部数据"] Steam["Steam 安装与库清单"] Bin["Space Engineers / Bin64"] Content["Space Engineers / Content"] Blueprints["用户 Blueprint 目录"] end subgraph Product["Space Engineers Blueprint Editor"] UI["WinUI 3 主程序"] Loader["GameAssemblyLoader"] Core["SpaceEngineersCore"] Converter["BlueprintConverter"] Installer["WPF Installer"] end Steam --> Loader Loader --> Bin Bin --> Core Content --> Core Blueprints --> UI UI --> Loader UI --> Core Converter -.->|"独立辅助转换"| Bin Converter -.-> Blueprints Installer -.->|"安装或升级"| UI ``` ## 项目依赖 ```mermaid flowchart LR App["SpaceEngineersBlueprintEditor
net8.0-windows"] --> Loader8["GameAssemblyLoader
net8.0"] App --> Core["SpaceEngineersCore
net48"] Converter["BlueprintConverter
net48"] --> Loader48["GameAssemblyLoader
net48"] Converter --> Core Test["Test console
net8.0"] --> Loader8 Test --> Core Installer["Installer
net8.0-windows"] Core --> Game["游戏程序集
Private=false"] App --> Game Converter --> Game Test --> Game ``` `GameAssemblyLoader` 多目标编译,避免为 .NET 8 主程序和 .NET Framework 转换器维护两套游戏定位代码。`Directory.Build.targets` 为所有声明 `UseSpaceEngineersAssemblies=true` 的项目注入游戏引用。 ## 组件职责 ### WinUI 主程序 `SpaceEngineersBlueprintEditor` 负责用户交互与应用编排: - `Views`:页面布局、文件拖放和少量必须依赖控件事件的桥接逻辑。 - `ViewModels`:页面状态、命令、异步加载和导航参数处理。 - `Model`:面向 UI 的蓝图、属性和游戏定义视图数据。 - `Interface/Services`:集合显示、拖放、标题、树和背景等抽象。 - `Implements/Services`:控件适配和具体事件实现。 - `Utilities`:路径、目录扫描、反射分析、文件操作和游戏帮助方法。 - `Profiles`:主题、导航样式、游戏路径等持久配置。 ViewModel 使用 `CommunityToolkit.Mvvm` 的源生成属性和命令。跨页面导航、消息、加载遮罩和选择状态通过 `ServiceManager` 中的全局或页面服务协调。 ### GameAssemblyLoader 加载器刻意不引用游戏类型,确保可以在游戏程序集尚未加载时先完成定位: 1. `GameInstallationLocator` 枚举首选路径、环境变量、注册表、Steam 库和默认磁盘位置。 2. 候选路径被规范化成游戏根目录并验证关键文件。 3. `GameAssemblyLoader` 保存 `GameRootPath` 与 `GameBinPath`。 4. 注册 `AppDomain.AssemblyResolve`,按简单程序集名从 `Bin64` 加载托管 DLL。 5. 调用 `SetDllDirectory`,支持 Havok 等原生依赖解析。 加载器需要在任何游戏类型首次触发静态加载前初始化,这也是入口代码把相关调用放在辅助方法外层的原因。 ### SpaceEngineersCore 核心层把游戏运行环境裁剪到“能够加载定义和序列化 ObjectBuilder”的程度: - `Initializer` 配置 `MyFileSystem`、VRage 平台、任务调度器、空渲染器、最小 `MySession` 和插件程序集。 - `MyDefinitionManager` 从游戏 `Content` 预加载并加载定义。 - `MyTexts` 按当前 UI Culture 加载游戏本地化文本。 - `SpaceEngineerDefinitions` 通过 Keen 的序列化器读取 ObjectBuilder XML,并以同目录临时文件、原子替换和 `sbcB5` 备份保存。 - `BlueprintEditing` 根据当前定义转换网格尺寸/装甲,并读写网格 Skeleton 形变数据。 - `ImageConverter` 与 `DxtUtil` 处理游戏 DDS 图标到 UI 可用图像的转换。 - `GridConverter` 包含外部网格格式转换定义和导出辅助代码。 - `InnerModel` 提供无窗口环境所需的平台、系统和渲染最小实现。 `InnerModel` 中大量成员故意没有实现:当前初始化路径不会使用它们。一旦新增功能触发这些成员,需要实现最小语义或重新评估是否应该依赖完整游戏环境。 ### BlueprintConverter 该项目提供独立的 `net48` JSON → `.sbc` 转换入口:它把带类型信息的 JSON 反序列化成 `MyObjectBuilder_Definitions`,再用 `XmlSerializer` 生成文件。主界面当前已经直接调用核心层的 `SpaceEngineerDefinitions.Save`,不再把转换器作为正常保存路径。 转换器协议不是面向用户的通用文件格式。JSON 开启了 `TypeNameHandling.Auto`,只应处理编辑器自己生成的受信任输入;保留该项目时应明确它的兼容或迁移用途。 ### Installer 安装器是独立 WPF 应用,负责: - 选择安装目录并展示许可/隐私文本。 - 下载和暂停/继续升级包。 - 解压新安装或升级内容。 - 创建快捷方式并启动主程序。 它不参与蓝图读取和编辑路径。 ## 启动时序 ```mermaid sequenceDiagram participant Entry as App participant Locator as GameInstallationLocator participant Loader as GameAssemblyLoader participant UI as MainWindow/AppShell participant BPM as BlueprintsManager participant Core as Initializer participant Defs as MyDefinitionManager Entry->>Loader: Initialize(保存的游戏路径) Loader->>Locator: FindGameRootPath Locator-->>Loader: 有效游戏根目录或 null Loader->>Loader: 注册 AssemblyResolve 与原生 DLL 目录 Entry->>UI: 创建窗口、注册页面和服务 par 扫描蓝图 Entry->>BPM: LoadBlueprintsAsync BPM-->>Entry: 本地/云端/工坊索引 and 加载游戏定义 Entry->>Loader: EnsureInitialized Entry->>Core: Initialize(Content, UserData) Core->>Defs: PreloadDefinitions / LoadData Defs-->>Core: 定义集合 end Entry->>UI: Activate ``` 蓝图扫描和定义初始化并行执行。依赖定义的页面通过 `Helper.Wait` 等待 `Initializer.IsDefinitionsLoadComplete`;这让首屏更快出现,但也意味着初始化异常必须通过全局消息服务显式反馈。 ## 蓝图读取流程 ```mermaid flowchart LR Dir["蓝图子目录"] --> Scan["BlueprintsManager"] Scan --> Info["BlueprintInfo"] Info --> ViewData["BlueprintInfoViewData"] ViewData --> DetailVM["BlueprintDetailPageViewModel"] DetailVM --> Load["SpaceEngineerDefinitions.Load"] Load --> Definitions["MyObjectBuilder_Definitions"] Definitions --> Ship["第一个 ShipBlueprint"] Ship --> Stats["网格 / DLC / 组件统计"] Ship --> EditorVM["BlueprintEditSubPageViewModel"] ``` `BlueprintsManager` 只建立轻量目录索引;进入详情或编辑器后才反序列化完整 `.sbc`。视图数据同时携带名称、大小、路径和缩略图,供页面之间作为导航参数传递。 ## 属性编辑模型 蓝图类型数量多,编辑器采用反射生成属性树: 1. `SpaceEngineersHelper.AnalyzeBlueprint` 检查对象的公开可写字段和属性。 2. 每个成员包装为 `BlueprintPropertyViewData`,保留名称、类型、值和父节点。 3. 对象和集合节点按需展开,避免一次性构建整棵巨大树。 4. `ShipBlueprintItemTemplateSelector` 根据基础值、枚举、集合、CubeGrid、CubeBlock 等类型选择模板。 5. `SetValue` 通过反射回写父对象;值类型的修改继续向祖先传播,避免只修改装箱副本。 ```mermaid flowchart TD Root["ShipBlueprint / CubeBlock"] --> Reflect["反射公开可写成员"] Reflect --> Node["BlueprintPropertyViewData"] Node --> Kind{"节点类型"} Kind -->|"基础类型或枚举"| Control["输入控件"] Kind -->|"对象"| Lazy["延迟展开子成员"] Kind -->|"IEnumerable"| Items["延迟展开集合项"] Control --> Set["反射写回父对象"] Set --> Bubble["值类型向祖先回写"] ``` 该方案覆盖面广,但缺少针对游戏语义的范围验证。新增专用编辑器时,应优先在类型选择或转换层加入校验,而不是把复杂逻辑塞入通用反射节点。 ## 三维与钣金编辑 编辑器在通用属性树之外提供两条专用路径: ```mermaid flowchart LR Ship["MyObjectBuilder ShipBlueprint"] --> Builder["Blueprint3DSceneBuilder"] Builder --> Json["紧凑场景 JSON"] Json --> WebView["WebView2 + 本地 WebGL 2"] WebView -->|"选择 / action 消息"| VM["BlueprintEditSubPageViewModel"] VM --> Actions["移动 / 旋转 / 复制 / 删除"] Actions --> Undo["三维撤销与重做栈"] VM --> Refresh["重建场景并回传 WebView"] Ship --> Grid["选中的 CubeGrid / CubeBlock"] Grid --> Deform["CubeDeformationEditor"] Deform --> Service["CubeDeformationService"] Service --> Skeleton["CubeGrid.Skeleton BoneInfo"] ``` 三维场景使用定义尺寸、蓝图方向、网格姿态和颜色构造方块包围盒。HTML/JavaScript 资源通过 WebView2 虚拟主机映射从应用 `Assets` 加载,页面与 C# 只交换 JSON 消息,不访问网络。每次操作后重新生成紧凑场景,异步 generation 编号用于丢弃过期结果。 三维撤销栈保存成对的 `Apply`/`Undo` 委托,仅覆盖该视图发起的变换。移动和删除会同步更新方块组坐标,复制会克隆 ObjectBuilder 并清空 `EntityId`。当前占位检测以方块最小坐标为主,不能替代完整多格碰撞检测。 钣金编辑器把 8 个角点和 6 个面中心映射到游戏的 3×3×3 局部 Skeleton 点阵,通过 `SerializableVector3UByte` 编解码 `[-1, 1]` 偏移。中性偏移会删除对应 Bone,避免写入冗余 Skeleton 数据。 ## 保存流程 ```mermaid sequenceDiagram participant User as 用户 participant Editor as WinUI 编辑器 participant Temp as 同目录临时文件 participant Core as SpaceEngineerDefinitions participant Target as 目标 .sbc User->>Editor: 保存或另存为 Editor->>Core: Save(path, definitions) Core->>Temp: Keen SerializeXML alt 目标已存在 Core->>Target: File.Replace(temp, target, target + B5) else 新文件 Core->>Target: File.Move(temp, target) end Core->>Temp: finally 清理残留临时文件 Core-->>Editor: 成功或异常 ``` 临时文件和目标文件位于同一目录,以便使用文件系统原子替换。目标存在时,旧版本成为 `目标路径 + "B5"`;已有备份会被替换。UI 在后台线程执行序列化,并通过消息服务报告异常。 ## 游戏定义与图标 定义页和组件统计都以 `MyDefinitionManager.Static` 为权威来源。`SpaceEngineersHelper` 暴露分类枚举,并把定义图标从游戏路径转换为 PNG 后放入应用缓存。这样 UI 不必直接解码 DDS,也避免每次启动重复转换。 缓存可以从设置页清除。游戏更新、图标路径变化或缓存损坏时,清除缓存会触发重新生成。 ## 主要状态与边界 | 状态 | 所有者 | 生命周期 | | --- | --- | --- | | 游戏根目录 | `SystemProfile.GameRootPath` / `GameAssemblyLoader` | 跨启动持久化;加载器在进程内缓存 | | 蓝图索引 | `BlueprintsManager` 静态列表 | 进程内,可刷新 | | 游戏定义 | `MyDefinitionManager.Static` | 进程级,一次初始化 | | 当前蓝图对象 | 详情/编辑 ViewModel | 页面或编辑标签生命周期 | | 属性树 | TreeView 服务与 `BlueprintPropertyViewData` | 按选择和展开动态重建 | | 定义图标 | 应用缓存目录 | 跨启动,可清除 | | UI 配置 | `SystemProfile` | 跨启动持久化 | ## 已知技术债与扩展方向 - 三维方块使用包围盒近似,移动/复制的占位判断还不是完整的多格碰撞检测。 - 撤销/重做只覆盖三维操作,属性、Skeleton 形变和批量转换尚未进入统一命令系统。 - 网格尺寸和轻/重甲转换依赖当前游戏定义配对;需要更清晰地处理坐标缩放、缺失配对和可能重叠。 - 独立 `BlueprintConverter` 与主界面直接保存形成两条序列化路径,需要明确长期保留或移除策略。 - 外部格式导出尚未连接到生产 UI 流程。 - 组件统计对未知、Mod 或版本不匹配定义需要更清晰的缺失报告。 - 全局静态游戏定义与多个 UI 服务增加了隔离测试难度。 - `async void` 事件路径较多,异常传播和取消控制仍可加强。 - 缺少标准单元、集成与序列化回归测试项目。 新增核心功能时,优先保持依赖方向为“UI → 核心抽象 → 游戏 API”,并把需要游戏安装的集成测试与纯数据转换测试分开。