# Mod 格式扩展 ## 分层 - `internal/mod` 定义中立包模型、格式注册表和独立的 `Reader` / `Writer` 接口,不依赖 Wails、归档服务或版本库。 - `Package` 保存名称和操作集合。`replace` 携带明确编码的文件内容,`merge-list` / `merge-strings` 携带键值条目,`delete` 表达整文件删除。 - `services.ExportService` 将选中文件、相对 HEAD 的工作区改动或提交前后状态适配为包模型;负责依赖补齐、预检、修订校验和目录发布。 - 前端四类导出入口只指定来源,不包含格式专属的目录或编码规则。 ## 添加格式 向注册表注册稳定 ID、显示名称、支持的操作以及读取器或写入器。读取和写入能力根据实际接口计算,不使用空实现冒充支持。 写入器先通过 `Validate` 检查包是否可表达,`Files` 描述实际输出路径及对应的包条目,再通过 `Write` 写入隔离的临时目录。`Files` 中没有 `EntryPath` 的文件为必需的生成元数据,不可移除。写入器不负责目录选择、覆盖确认或修改归档。 导入通过 `Reader.Detect(fs.FS)` 识别格式,`Reader.Read(fs.FS)` 返回同一 `Package`,再由服务层执行导入预览和归档暂存事务。外部文件路径不能直接成为写入路径;导入器需要校验包内路径和操作,并复用现有暂存、索引重建及版本变更记录流程。当前已注册 110USextend 和 115CN-mod,现有“导入”弹窗中的“Mod”选项提供入口。实现 `ArchiveWriter` 的格式直接发布单个 ZIP,其余格式保留目录发布。 ## 110USextend 导出结果是父目录下的 `-<版本号>/`,包含 `pack.json` 和实际用到的 `pvf/`、`merge/`。版本号可填写,默认或留空为 `1.0`,不强制 semver;最多 128 字节,不允许控制字符和首尾空格。优先级 `100`、启用,`types` 记录整文件替换项的 PVF 类型。文本使用 UTF-8,无 BOM。 普通脚本和文本整份放入 `pvf/<归档路径>`;`.lst`、`.str` 使用 `merge/<归档路径>`。版本导出只包含新增、修改的表条目(新增表以空表为基线);选中文件、文件集或未启用版本控制的内存改动没有准确基线,跳过显式选择的整表,不全量导出。列表按编号匹配,文字表按第一个 `>` 分割并保留空值及文字中的空格;忽略有缩进的 `//` 注释和空行。 该格式不支持删除,预检会提示跳过的文件或表条目删除。只有删除或没有可应用条目的包不能导出。无法解析的文字表行跳过;无法解析或读取的依赖表、无法准确提取改动的合并表及未能补齐的依赖也跳过,记录文件路径、行号或原因到 `[pvfine:export]` 日志,并在确认弹窗汇总警告。非法目标路径、不能表达的输出内容仍阻止导出,不以改名绕过安全校验。 依赖补齐默认关闭。开启后,从同一来源状态补齐脚本引用的文字表条目、表号映射及配置列表中匹配的登记项;显式内容优先,重复项去重。历史版本使用该提交快照,不读取当前工作区内容。不补齐任意脚本、sprite、sounds、npk 资源。 直接导出保留既有行为:版本新增或修改输出后状态,删除输出前状态。导出不会应用编辑器草稿、创建版本或保存 PVF。 未启用版本控制的“当前内存改动”从归档 overlay 枚举所有现存修改和新增文件;mod 模式跳过无版本基线的 lst/str,直接导出仍保留整文件;已删除文件不在该范围中。 所有确认弹窗列出完整输出树,勾选文件可控制最终输出范围。排除作用于已冻结的预览,不重新计算依赖,也不会自动补回被排除的依赖文件。已有预览刷新或切换模式时保留相同输出路径的排除,新弹窗重置;选空时不可导出。mod 元数据根据最终保留条目重新生成。 未选择合并来源时,只改名称或版本号会复用同一有效内容快照,不重新解码文件、计算差异或扫描依赖。快照过期时间不延长,归档、工作区或版本会话变化后不可复用。日志分别记录锁等待、来源读取、差异提取、依赖补齐和总耗时;普通脚本不读取不必要的修改前内容,无文字表引用时不读取表号映射或文字表。 ## 导出时合并已有 MOD 导出为 mod 时,可在依赖开关后选择一个相同格式的已有 MOD。110USextend 选择目录;115CN-mod 可选择目录、ZIP 或 `.dnfmod`。普通文件同路径冲突时使用本次内容;同类型的 lst/str 合并操作按键合并,同键使用本次值,保留已有 MOD 的其余文件及键。选择已有 MOD 后,自动读取包内名称及版本号;纯数字分段版本递增最后一段(如 `1.0 → 1.1`、`1.2.3 → 1.2.4`),保留末段前导零,其他版本保留原值并提示手动修改。名称和版本号仍可编辑,ID 使用本次输入。输出目录名为 `-<版本号>/`,ZIP 为 `-<版本号>.zip`,不覆盖已有 MOD。版本号留空时包名采用格式默认版本;名称与版本号组合必须是合法文件名。 已有内容经过读取、合并和格式校验后进入同一预览树,可以取消勾选。每次准备预览都会重新读取合并来源;执行时使用冻结内容,不再读取源 MOD。切换格式或重新打开弹窗会清除来源;取消文件选择保留原来源。115CN 的全部组件合并为基础内容;互斥组件或组件目标冲突会阻止合并。 当前仅合并 PVF 内容,不复制 NPK/BK2 等客户端资源、包级依赖/冲突和其他包元数据。 ## Mod 导入 工具栏“导入”中选择“Mod”和导入格式,再选择单个 mod 目录。格式选项由 `ModImportService.Formats()` 从注册表加载,只列出拥有 `Reader` 的格式;服务按显式传入的格式 ID 调用对应的 `Detect` / `Read`,目录与所选格式不匹配时拒绝预览,不回退到其他读取器。后续格式注册读取器后即可出现在导入选项中。110USextend 目录需包含 `pack.json`。当前支持读取本工具导出的 110USextend 包;`pvf/` 中每个替换文件必须在 `pack.json.types` 中声明类型(1 为脚本、3 为文本),声明的文件必须存在。仅处理 `pvf/` 和 `merge/`,不导入 sprite、sounds、npk 等游戏资源,不支持删除操作。 预览显示所有文件的新增/更新状态、整文件替换或表合并方式;展开表文件显示每个传入键的新增/覆盖状态、新值和原值。整文件替换使用包中声明的类型;`.lst` 按编号合并,`.str` 按第一个 `>` 拆分后按文字键合并,同键覆盖并保留无关条目。文字表保留空值、值中的空格和 `>`,以及当前表中未改动的行和注释。归档中不存在的合并表会新建;已有合并表类型必须匹配。 读取器拒绝非法路径、大小写路径冲突、文件/目录冲突、符号链接、非普通文件、敏感配置路径、无效编码、重复传入合并键及无法解析的合并行。包内文本必须是无 BOM 的 UTF-8;合并行忽略空行和有缩进的 `//` 注释。现有目标表无法准确解析时阻止导入,避免丢失条目。 `ModImportService` 在隔离归档上预检并冻结合并后的文件内容,确认时不重新读取目录。预览有效期为 10 分钟;归档、工作区修订或版本会话发生变化后必须重新预览。取消或重新选择会释放旧预览。确认导入复用文件导入暂存事务、索引更新及版本记录,预检或暂存失败不安装部分文件。启用版本控制时记录一组“导入 Mod”变更,可撤销;不会自动提交版本或保存 PVF。编辑器草稿仍由既有归档变更刷新流程处理。 ## 115CN-mod 格式 ID 为 `115CN-mod`。配置使用 UTF-8 `mod.json`,要求 `schema: 1`、`client: "115CN"`、`kind: "resource"`。读取目录、`.zip`、`.dnfmod`,配置可在根目录或唯一的一层包目录内;也兼容旧 ZIP 的 GB18030 中文包目录。RAR、7z、JAR 不接受。不解压到游戏目录,不执行包内程序或脚本。 PVF 操作通过 `component` 关联组件,`source` 相对包根目录,`path` 为小写 PVF 路径: - `add`:仅新增,目标存在时整批预检失败。 - `replace`:整份替换,目标不存在时新增。 - `merge`:仅支持 `.lst` 按编号合并、`.str` 按文字键合并。同键覆盖,其他条目保留。 - `type`:示例包使用的可选字段,1 为脚本、3 为文本。省略时 `.str` / `.nut` 推断为 3,其他为 1;合并表必须匹配对应类型。 - `patch`:提供的文档片段和示例包未包含标签补丁字段协议,当前明确拒绝,不猜测、忽略或降级成整文件替换。需补充登录器 `ModExamples/README.md` 中的完整协议后接入。 导入预览按 `defaultSelected` 初始化组件,勾选后从冻结的包内容重新预检。`requires` 必须同时选中,`exclusiveGroup` 同组最多选一项;未知组件、循环依赖、所选组件的重复目标及文件/目录冲突拒绝。全不选时禁止执行。切换不延长原预览的 10 分钟期限,源 ZIP 变化不影响已冻结的内容,归档修订变化后必须重新预览。现有 Unicode 文字表的尾部 NUL 终止符原样保留,其他无法解析的行仍阻止合并。 `files` 中的客户端资源仅校验声明的组件、路径、size 与 sha256 字段格式,并在预览警告中报告;不读取或校验资源正文摘要、不写入 PVF、不安装或备份 NPK/BK2。包级 `requires / conflicts` 提示用户在登录器核对,不模拟 MOD 管理器的安装、排序、关闭或恢复状态。资源安装、已有视频恢复和依赖管理仍由登录器负责。 导出复用现有来源、精确表差异和依赖补齐,生成 `<名称>.zip`,根目录为 `mod.json` 和 `pvf/`,单个默认组件 `base`。新增独立 MOD ID 输入框:初次生成唯一 ID,发布同一 MOD 的更新需填回原 ID;ID 只能为 1–64 个小写字母、数字、`-`、`_`。名称/版本/ID 更新复用有效内容快照。筛选文件后同步重建清单,不携带被排除的载荷。ZIP 写完后原子发布,拒绝覆盖已有同名包。MCP `export_prepare` 选择此格式时同样必须提供 `modId`。 ZIP 拒绝路径穿越、大小写路径冲突、符号链接、特殊文件和敏感配置路径;声明文件读取也检查父目录链接。限制 ZIP 最多 20,000 项、单项 64 MiB、总展开大小 512 MiB,清单 4 MiB。目录包同样限制 PVF 单文件和累计读取大小。不会读取未登记的 PVF 载荷。 ## 验证 运行 `go test ./...`、前端 `npm test` 和 `npm run build`。真实归档的新增导出验证需要设置 `PVF_TESTFILE`;示例包和游戏目录不作为可写测试目标。 115CN 示例读取回归:设置 `PVF_MOD_TESTFILE` 为 ZIP 路径,运行 `go test ./internal/mod -run '^TestCN115RealMOD$' -v`。真实归档预检另外设置 `PVF_MOD_ARCHIVE` 为测试 PVF,运行 `go test ./services -run '^TestCN115RealMODPreflight$' -v -timeout 15m`。两者均只读源文件,不自动执行或保存 PVF。