轻量配置

轻量配置

一个Minecraft的JSON5/TOML配置库

Lite Config

Lite Config 是一个适用于 Fabric 和 NeoForge 上 Minecraft 模组的 JSON5/TOML 配置库。你只需用 @Config 注解一个 Java 类,将其交给构建器,即可获得一个 ConfigHolder,它负责处理文件路径解析、读写操作、损坏文件恢复、副本、校验和生命周期事件。

Lite Config 的功能:

  • 配置数据层: Lite Config 处理配置文件,包括路径、加载、保存、默认值、损坏恢复、原子写入以及 JSON5/TOML 格式。
  • 安全状态管理: 提供经过校验的快照、副本、运行时更新和自定义状态克隆。
  • 异步和只读配置: 根据你的线程和生命周期需求,选择同步、异步或只读的持有器。
  • 重启保护: 阻止本地运行时更新,并延迟需要重启的字段的同步更改。
  • 自定义更新 API: update 和 updateAndSave 返回带有接受状态和校验违规信息的 UpdateResult。
  • 细粒度失败策略: 独立控制读取、写入和更新失败的处理方式,从优雅回退到严格异常。
  • 生命周期和事件监听器: 挂钩配置加载、保存和更新事件,或使用配置级钩子进行规范化和校验。
  • 声明约束: 使用注解限制数字、字符串和集合的范围,并在加载和更新时强制执行,同时通过元数据公开。
  • 配置元数据: 在运行时查询每个字段的路径、类型、默认值、注释、翻译键和约束。
  • 可选同步: 将配置或单个字段标记为同步,Lite Config 会自动将这些值从服务器同步到客户端。
  • 版本控制和迁移: 将修订号写入文件,并逐步升级旧版本,而不是丢失玩家的数值。
  • 可自定义条目: 控制文件路径、字段名称、注释、忽略的字段和其他持久化细节。

不在 Lite Config 范围内:

  • 配置界面: Lite Config 是一个数据层,它本身不渲染 UI。

文档: Wiki

安装

构件已发布到 Maven Central,组名为 com.gmalvestiti.minecraft,每个加载器对应一个构件:

加载器 构件
Fabric liteconfig-fabric
NeoForge liteconfig-neoforge

Maven 版本包含 Lite Config 版本和 Minecraft 构建目标:

Minecraft Lite Config
1.21-1.21.10 1.0.0-1.21
1.21.11 1.0.0-1.21.11
26.1-latest 1.0.0-26.1

下面的示例针对 1.21 构建。

Fabric

repositories {
    mavenCentral()
}

dependencies {
    modImplementation 'com.gmalvestiti.minecraft:liteconfig-fabric:1.0.0-1.21'
}

声明依赖,以便加载器在没有它时拒绝启动:

{
  "depends": {
    "liteconfig": ">=1.0.0-1.21"
  }
}

NeoForge

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.gmalvestiti.minecraft:liteconfig-neoforge:1.0.0-1.21'
}

在 META-INF/neoforge.mods.toml 中声明依赖:

[[dependencies.yourmodid]]
modId = "liteconfig"
type = "required"
versionRange = "[1.0.0,)"
ordering = "NONE"
side = "BOTH"

快速入门

构建器会创建可变或只读的持有器。每个持有器都公开用于调用线程工作的同步方法和由共享配置工作器支持的异步方法:

构建器调用 行为 最佳适用场景
create() 允许同步和异步更改 常规运行时配置
readOnly().create() 拒绝加载和更新 模组只读取但从不更改的配置

声明配置类。将持久化字段初始化为其默认值,并提供公共无参构造函数:

@Config(name = "mymod") // 格式默认为 JSON5
public final class MyModConfig {
    public boolean showHints = true;
    public int hudScale = 2;
}

或使用 TOML:

@Config(name = "mymod", format = ConfigFormat.TOML)
public final class MyModConfig {
    public boolean showHints = true;
    public int hudScale = 2;
}

在模组初始化期间创建一次持有器,并在模组的整个生命周期中保留它。create() 会解析文件路径、校验模型、加载任何现有文件(如果不存在则写入默认值),并校验加载的状态——持有器在返回后立即可读:

public final class MyMod implements ModInitializer {

    public static final ConfigHolder<MyModConfig> CONFIG = LiteConfig.holder(MyModConfig.class)
        .modId("mymod")
        .create();
}

通过 data() 读取,通过 update / updateAndSave 修改:

if (MyMod.CONFIG.data().showHints) {
    // ...
}

MyMod.CONFIG.updateAndSave(config -> config.hudScale = 3);

这将写入 config/mymod.json5:

{
  "showHints": true,
  "hudScale": 3
}

完整配置示例

@Config(
    name = "mymodfile",
    path = "mymoddir1/mymoddir2",
    format = ConfigFormat.JSON5,
    comment = "MyMod settings.",
    version = 3,
    stateCloner = MyModConfigCloner.class,
    readFailurePolicy = FailurePolicy.FALLBACK,
    writeFailurePolicy = FailurePolicy.STRICT,
    updateFailurePolicy = FailurePolicy.FALLBACK
)
public final class MyModConfig implements ConfigExtension {

    @Entry(
        name = "hud_scale",
        comment = "Scale of the HUD, from 1 to 4.",
        translationKey = "mymod.config.hud_scale",
        sync = true,
        callback = "onHudScaleChanged"
    )
    @Range(min = 1, max = 4)
    public int hudScale = 2;

    @Entry(comment = "Rendering backend. Applied after the next restart.", restart = true)
    public Renderer renderer = Renderer.DEFAULT;

    @Entry(comment = {"Profile used by server rules.", "Must be lowercase, alphanumeric, or underscore."})
    @Pattern("[a-z0-9_]+")
    @Length(max = 16)
    public String profileName = "default";

    @Entry(comment = "Server-owned spawn range.", sync = true)
    public IntRange spawnRange = new IntRange(1, 12);

    public Display display = new Display();

    @Ignore
    public Map<String, String> runtimeCache = new HashMap<>();

    @Override
    public void afterLoad() {
        if (profileName != null) {
            profileName = profileName.strip().toLowerCase(Locale.ROOT);
        }
    }

    @Override
    public void beforeSave() {
        if (display != null && display.hiddenHints != null) {
            display.hiddenHints.sort(String::compareTo);
        }
    }

    @Override
    public void validate(List<Violation> violations) {
        if (spawnRange == null || spawnRange.minimum() > spawnRange.maximum()) {
            violations.add(Violation.of(
                "spawn-range.order",
                "spawnRange minimum must not exceed its maximum"
            ));
        }
    }

    private void onHudScaleChanged(Integer oldValue, Integer newValue, boolean fromSync) {
        System.out.printf("HUD scale: %d -> %d (from server: %s)%n",
            oldValue, newValue, fromSync);
    }

    @Migration(from = 1)
    static void toVersion2(ConfigData data) {
        data.rename("hudScale", "hud_scale");
    }

    @Migration(from = 2)
    static void toVersion3(ConfigData data) {
        if (!data.has("spawnRange")) {
            data.set("spawnRange.minimum", 1)
                .set("spawnRange.maximum", 12);
        }
    }

    public static final class Display {
        @Entry(comment = "Show contextual hints.")
        public boolean showHints = true;

        @Length(max = 32)
        public List<String> hiddenHints = new ArrayList<>();
    }

    public enum Renderer {
        DEFAULT,
        COMPATIBILITY
    }
}

// 自定义类型
public record IntRange(int minimum, int maximum) {
    public static final Codec<IntRange> CODEC = RecordCodecBuilder.create(instance ->
        instance.group(
            Codec.INT.fieldOf("minimum").forGetter(IntRange::minimum),
            Codec.INT.fieldOf("maximum").forGetter(IntRange::maximum)
        ).apply(instance, IntRange::new)
    );

    public static final StreamCodec<ByteBuf, IntRange> STREAM_CODEC =
        StreamCodec.composite(
            ByteBufCodecs.VAR_INT, IntRange::minimum,
            ByteBufCodecs.VAR_INT, IntRange::maximum,
            IntRange::new
        );
}

// 自定义配置状态克隆器
public static final class Cloner implements StateCloner<MyModConfig> {
    @Override
    public MyModConfig copy(MyModConfig source) {
        MyModConfig copy = new MyModConfig();
        copy.hudScale = source.hudScale;
        copy.renderer = source.renderer;
        copy.profileName = source.profileName;
        copy.spawnRange = source.spawnRange;
        copy.display = copyDisplay(source.display);
        copy.runtimeCache = source.runtimeCache == null
            ? new HashMap<>()
            : new HashMap<>(source.runtimeCache);
        return copy;
    }

    private static Display copyDisplay(Display source) {
        if (source == null) {
            return null;
        }
        Display copy = new Display();
        copy.showHints = source.showHints;
        copy.hiddenHints = source.hiddenHints == null
            ? null
            : new ArrayList<>(source.hiddenHints);
        return copy;
    }
}

在创建持有器时注册自定义编解码器。注册使用共享的进程级注册表,不绑定到该持有器或配置。Codec 控制 JSON5/TOML 持久化和状态复制;如果它拒绝某个值,Lite Config 会在该用途上回退到反射序列化。StreamCodec 控制同步。Lite Config 的加载器入口点处理数据包、握手、批处理和服务器广播。

public final class MyMod implements ModInitializer {

    public static ConfigHolder<MyModConfig> config = LiteConfig.holder(MyModConfig.class, codecs -> codecs
            .registerCodec(IntRange.class, IntRange.CODEC)
            .registerStreamCodec(IntRange.class, IntRange.STREAM_CODEC))
        .modId("mymod")
        .onLoad(ConfigSide.SERVER, state -> System.out.println("Loaded profile " + state.profileName))
        .onUpdate(ConfigSide.BOTH, state -> System.out.println("HUD scale is now " + state.hudScale))
        .onSave(ConfigSide.SERVER, state -> System.out.println("Saved MyMod config"))
        .create();

    @Override
    public void onInitialize() {
        // 快速共享读取。将返回的对象视为只读。
        int currentScale = config.data().hudScale;

        // 由该调用者拥有的稳定深拷贝。
        MyModConfig snapshot = config.copy();
        snapshot.display.hiddenHints.add("crafting");

        // 经过校验的内存中更新。
        UpdateResult result = config.update(state -> {
            state.hudScale = 3;
            state.display.hiddenHints = new ArrayList<>(
                snapshot.display.hiddenHints
            );
        });
        if (!result.accepted()) {
            result.violations().forEach(violation ->
                System.err.println(violation.id() + ": " + violation.message()));
        }

        // 序列化更新并保存。接受后,同步值由服务器广播。
        config.updateAndSaveAsync(state ->
            state.spawnRange = new IntRange(2, 24)
        ).thenAccept(update ->
            System.out.println("Saved: " + update.accepted()));

        // 适用于界面、命令或生成帮助的结构化元数据。
        config.metadata().flatten().forEach(property ->
            System.out.println(property.path() + " -> " + property.type().getSimpleName()));
    }
}

第一次 create() 会加载 config/mymod/server.json5,按顺序迁移旧版本,校验结果,并写回被接受的状态。没有 configVersion 的文件从版本 1 开始。只有 hud_scale 和 spawnRange 会被同步,因为它们通过 sync = true 选择加入;其他值保持本地化。当所有持久化叶子字段都由服务器管理时,改用 @Config(sync = true)。