Patchly

Patchly

完美的纯净打补丁插件。一款现代化的就地Zima与Hytalor替代方案。

实用

Patchly - 纯补丁插件

面向模组开发者,请使用 curse.maven:patchly-1550889:8406981

一种原地替换的现代化 Zima 和 Hytalor 替代方案

完整文档位于 https://wiki.hytalemodding.dev/mod/patchly

*虽然 Patchly 与另外两个补丁解决方案 HytalorZima 兼容,但不建议同时安装三者。在这种情况下,保证其可靠性。Patchly 拥有另外两个方案的 100% 功能,因此首选迁移到单一补丁解决方案。*

无需从头重写即可对 JSON 资产进行补丁。零依赖。

Patchly 的诞生原因

Hytale 原生的 Parent: super 仅在外部资产层级继承——大多数嵌套的编解码器字段(例如 Item.armor.StatModifiersDamageResistance)使用的是 .append(...) 而非 .appendInherited(...),因此常规的 JSON 重写会替换整个子对象,并静默清除所有未重新声明的部分。

Patchly 的作用

Patchly 会读取已解析的基础资产,并将你的 .patch 文件深度合并到其上,因此你只需编写差异部分。

适用于所有已注册的 AssetPack——包括文件夹、.zip.jar 包。仅需 JSON 的模组制作者可以将 Patchly.jar 放入 mods/ 文件夹中;Java 模组制作者可以通过 Gradle Shadow 将 Patchly 打包到自己的 jar 中。Patchly 会与其他 Patchly 实例协调,确保同一时刻只有一个 Patchly 在运行。这意味着你的模组可以独立运行,也可以与其他 Patchly 模组、patchly.jar 等协同工作。

补丁的工作原理

.patch 文件放置在与你要打补丁的资产相同的路径下,将 .json 扩展名替换为 .patch。要为其他包中的 Armor_Iron_Head.json 打补丁,请在您的包中提供 Server/Item/Items/Armor/Iron/Armor_Iron_Head.patch

Patchly 遍历每个包,解析每个目标的最新版本,将你的 .patch 合并到其上,并将结果写入一个优先级更高的合成覆写包中。

最小补丁

{
  "Armor": {
    "StatModifiers": {
      "Mana": [{ "Amount": 126, "CalculationType": "Additive" }]
    }
  }
}

按字段深度合并。Mana 会被放入父级现有的 StatModifiers 块中;Health 和其他同级别属性保持不变。

数组的替换与追加

默认情况下,数组会被替换。在键名后添加 + 后缀可改为追加:

{
  "BlockType": {
    "Bench": {
      "Categories+": [
        { "Id": "Arcane_Hexcode", "Icon": "...", "Name": "..." }
      ]
    }
  }
}

父级现有的 Categories 条目会保留;此条目会被添加到末尾。

数组的前置

在键名后添加 - 后缀可将你的条目添加到前面而非末尾:

{
  "Children-": [
    { "Id": "Hexcode", "Name": "hexcode.itemcategory.hexcode.name" }
  ]
}

你的条目会按照你编写的顺序放置在父级条目的前面。-+ 互相对称(如果数组不存在则创建,支持 $Match)。它表示前置,而非删除。

仅在键缺失时填充

在键名后添加 ? 后缀,仅在目标未定义该键时写入值。如果键已存在,则基础值胜出,你的值被丢弃:

{
  "Armor": {
    "StatModifiers": {
      "Mana?": [{ "Amount": 200, "CalculationType": "Additive" }]
    }
  }
}

已经拥有 Mana 的物品会保留它;没有的物品则会获得 200。这是基于存在性的判断,因此适用于任何值类型,并且按每个键独立决策。这是让基础值获胜的唯一方式——$Priority 仅用于决定各个补丁之间的顺序,从不与基础值比较。

$Requires - 仅在安装了特定包时应用

单个包:

{
  "$Requires": "Riprod:Hexcode",
  "Armor": { ... }
}

多个包(必须全部存在):

{
  "$Requires": ["Riprod:Hexcode", "Author:SomeOtherPack:^0.5.0"],
  "Armor": { ... }
}

包 ID 会与 AssetPack.getName() 进行匹配(即来自目标模组清单的 Group:Name)。如果缺少任何包,则该补丁会被跳过并记录一条日志。

目前不支持排除包。我们认为这并非必要,如果你希望有此功能,欢迎提交 Pull Request。

$Priority - 解决冲突时选择胜者

整数,默认值为 0。数值越小越先应用,数值越大越后应用 → 在字段冲突中数值大的胜出。平局时由包的加载顺序决定。

{
  "$Priority": 100,
  "Armor": {
    "StatModifiers": {
      "Mana": [{ "Amount": 9999, "CalculationType": "Additive" }]
    }
  }
}

两个模组对同一字段打补丁时都会应用,但 $Priority 更高的那个会在最后写入。较低优先级的 + 追加操作仍然会叠加到较高补丁未触碰的字段上。

保留键

任何以 $ 为前缀的顶层键都是元数据——在合并前会被剥离,不会进入合成的资产中。目前具有语意的只有 $Requires$Priority$Comment(或任何其他 $Foo 格式的键)可自由用于你个人的备注。

使用方法

作为独立工具(资产包)

Patchly-X.Y.Z.jar 放入服务器的 mods/ 文件夹中,与你的资产包放在一起。就这么简单。Patchly 将进行扫描、合并、注册。

你也可以在 package.json 中将其添加为必要依赖,并在 CurseForge 上将其添加为关联项目,以提高可见度!

作为捆绑依赖(Java 模组)

添加 Shadow 插件并依赖 lib jar:

plugins {
    id("hytale-mod") version "0.+"
    id("com.gradleup.shadow") version "8.3.5"
}

// 一个专用配置,使得只有 Patchly 被打包,而不是你的编译依赖
val shaded by configurations.creating

dependencies {
    // 针对 API 编译,并标记为将其打包到最终 jar 中
    shaded(files("deps/Patchly-3.1.1.jar"))
    implementation(files("deps/Patchly-3.1.0.jar"))
}

tasks.shadowJar {
    archiveClassifier.set("")        // shadow jar 就是发布的构件
    mergeServiceFiles()
    configurations = listOf(shaded)  // 仅打包 `shaded` 中的内容
    relocate("com.riprod.patchly", "com.riprod.<你的包ID>.shaded.patchly")
}

tasks.jar { enabled = false }        // 禁用瘦 jar
tasks.build { dependsOn(tasks.shadowJar) }

然后在你的 JavaPlugin 中:

import com.riprod.patchly.PatchManager;

public final class MyPlugin extends JavaPlugin {
    private final PatchManager patchManager;

    public MyPlugin(JavaPluginInit init) {
        super(init);
        patchManager = new PatchManager(this);
    }

    @Override protected void setup() {
        patchManager.install();
    }
}

注意:在使用 ./gradlew runServer 进行测试时,你不能同时将 Patchly.jar 放在 ./mods 文件夹和作为依赖。这是因为开发服务器会扫描你的依赖,找到你的 Patchly.jar 的 manifest.json,并将其也注册为自己的包。如果它同时也存在于 ./mods/ 中,那么它就会被注册两次并导致崩溃。反正你也不需要将 Patchly.jar 放在你的 mods/ 文件夹中,所以直接删掉它就行。

如果在同一个 JVM 中同时安装了独立的 Patchly.jar 和一个捆绑了 Patchly 的模组,每个实例都会投出自己的版本,最新的版本会成为唯一的活跃拥有者;较旧的版本会退出并变为空操作。该决定在启动时最终确定。没有重复工作,没有冲突。

注意事项

  • 热重载适用于文件夹包模组中的 .patch 文件(jar/zip 包在注册时一次性应用;没有实时重载)。
  • 输出位于 MODS_PATH/<群组>_<名称>_PatcherOverrides/ 目录中,每次冷启动时都会被清空。
  • 启动时出现 [AssetModule] Skipping pack at ..._PatcherOverrides: missing or invalid manifest.json 是良性的——合成包是通过编程方式注册的,而非文件系统扫描。