XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8

架构说明

本文描述 Space Engineers Blueprint Editor 当前代码的主要边界、依赖方向和运行时数据流。构建操作见开发指南

设计约束

项目的核心难点不是 XML 本身,而是在游戏进程外复用《Space Engineers》的 ObjectBuilder、定义和序列化系统。架构由以下约束塑造:

  • 游戏程序集和内容受游戏安装约束,不能随仓库或应用分发。
  • 游戏核心类型仍面向 .NET Framework,桌面界面则使用 .NET 8 和 WinUI 3。
  • 部分 VRage/Sandbox 初始化通常依赖完整游戏平台与渲染环境,编辑器只需要其中的定义和序列化能力。
  • 蓝图结构层级深、类型多,手写每一种属性编辑器维护成本很高。
  • 保存结果必须保持游戏可识别的 ObjectBuilder XML 结构。

系统上下文

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

项目依赖

flowchart LR
    App["SpaceEngineersBlueprintEditor<br/>net8.0-windows"] --> Loader8["GameAssemblyLoader<br/>net8.0"]
    App --> Core["SpaceEngineersCore<br/>net48"]
    Converter["BlueprintConverter<br/>net48"] --> Loader48["GameAssemblyLoader<br/>net48"]
    Converter --> Core
    Test["Test console<br/>net8.0"] --> Loader8
    Test --> Core
    Installer["Installer<br/>net8.0-windows"]
    Core --> Game["游戏程序集<br/>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 保存 GameRootPathGameBinPath
  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 形变数据。
  • ImageConverterDxtUtil 处理游戏 DDS 图标到 UI 可用图像的转换。
  • GridConverter 包含外部网格格式转换定义和导出辅助代码。
  • InnerModel 提供无窗口环境所需的平台、系统和渲染最小实现。

InnerModel 中大量成员故意没有实现:当前初始化路径不会使用它们。一旦新增功能触发这些成员,需要实现最小语义或重新评估是否应该依赖完整游戏环境。

BlueprintConverter

该项目提供独立的 net48 JSON → .sbc 转换入口:它把带类型信息的 JSON 反序列化成 MyObjectBuilder_Definitions,再用 XmlSerializer 生成文件。主界面当前已经直接调用核心层的 SpaceEngineerDefinitions.Save,不再把转换器作为正常保存路径。

转换器协议不是面向用户的通用文件格式。JSON 开启了 TypeNameHandling.Auto,只应处理编辑器自己生成的受信任输入;保留该项目时应明确它的兼容或迁移用途。

Installer

安装器是独立 WPF 应用,负责:

  • 选择安装目录并展示许可/隐私文本。
  • 下载和暂停/继续升级包。
  • 解压新安装或升级内容。
  • 创建快捷方式并启动主程序。

它不参与蓝图读取和编辑路径。

启动时序

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;这让首屏更快出现,但也意味着初始化异常必须通过全局消息服务显式反馈。

蓝图读取流程

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 通过反射回写父对象;值类型的修改继续向祖先传播,避免只修改装箱副本。
flowchart TD
    Root["ShipBlueprint / CubeBlock"] --> Reflect["反射公开可写成员"]
    Reflect --> Node["BlueprintPropertyViewData"]
    Node --> Kind{"节点类型"}
    Kind -->|"基础类型或枚举"| Control["输入控件"]
    Kind -->|"对象"| Lazy["延迟展开子成员"]
    Kind -->|"IEnumerable"| Items["延迟展开集合项"]
    Control --> Set["反射写回父对象"]
    Set --> Bubble["值类型向祖先回写"]

该方案覆盖面广,但缺少针对游戏语义的范围验证。新增专用编辑器时,应优先在类型选择或转换层加入校验,而不是把复杂逻辑塞入通用反射节点。

三维与钣金编辑

编辑器在通用属性树之外提供两条专用路径:

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 数据。

保存流程

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”,并把需要游戏安装的集成测试与纯数据转换测试分开。