Skip to main content

数据驱动

有时我们会突然想为模组添加自定义数据包。翻阅 NeoForge / Forge / Fabric 的官方教程,固然是个选择,但流程往往显得冗长(至少我是这么觉得的)。

举个例子:
当你想实现一个简单的自定义数据包时,需要写一个类实现编解码,再去调用加载器的 API 进行注册,或者实现 SimpleJsonResourceReloadListener (原版) / SimpleResourceReloadListener (Fabric),稍显复杂。 本模组提供了一个极简的数据驱动框架。
例如,在 OEI 中想写一个数据包时,你只需写一个 Codec( Gson / other 出门右转):

@DataDriven(
modid = "oei",
folder = "replacements",
syncToClient = true,
validator = ReplacementValidator.class,
supportArray = true
)
public record Replacements(List<String> matchItems, String resultItem) {
public static final Codec<Replacements> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.STRING.listOf().fieldOf("matchItems").forGetter(Replacements::matchItems),
Codec.STRING.fieldOf("resultItem").forGetter(Replacements::resultItem)
).apply(instance, Replacements::new)
);
}

@DataDriven 注解参数说明

  • modid → 模组 ID
  • folder → 数据所在路径,例如上例数据将加载自 data/oei/replacements
  • syncToClient → 是否在服务端解析后同步至客户端(默认 true
  • validator → 验证器类,用于实现严格校验逻辑,如果不符合规范则跳过这一段并抛出警告,不影响整体文件正常解析。
  • enableCache → 是否启用缓存,提升查询性能但会增加内存消耗(默认 true
  • priority → 加载优先级,数值越小优先级越高(默认 1000
  • supportArray → 是否允许 JSON 文件直接包含数组形式的多个数据条目(默认 false多个 Codec 嵌套效果 || supportArray 效果

完成 Codec 编写后,只需在主类中一行注册:

DataRegistry.register(Replacements.class);

如果我们要在重载时搞些事情,可通过监听 DataReloadEvent 事件:

Forge / NeoForge

    @SubscribeEvent
public static void onDataReload(DataReloadEvent event) {
if (event.isDataType(Replacements.class)) {
Minecraft.getInstance().execute(() -> {
rebuildReplacementCache();
GlobalReplacementCache.rebuild();
Oneenoughitem.LOGGER.info("Replacement cache rebuilt due to data reload: {} entries loaded, {} invalid",
event.getLoadedCount(), event.getInvalidCount());
});
}
}

Fabric

    Events.on(DataReloadEvent.EVENT)
.normal()
.register(ModEventHandler::onDataReload); //此处用的是本模组提供的支持优先级的简便事件 API ,Fabric 原生可通过注册顺序来控制优先级

public void onDataReload(Class<?> dataClass, int loadedCount, int invalidCount) {
if (dataClass == Replacements.class) {
rebuildReplacementCache();
GlobalReplacementCache.rebuild();
Oneenoughitem.LOGGER.info("Replacement cache rebuilt due to data reload: {} entries loaded, {} invalid",
loadedCount, invalidCount);
}
}

注意:若出现同步问题(如服务端 NBT / 配方数据未同步),则需在服务端与客户端同时监听事件。

若我们的数据包需要一些严格的检查逻辑,可以实现一个 DataValidator 接口,这里不过多赘述,若有需要请查看源码的 Javadoc,OEI 验证器示例:

/**
* 替换规则验证器。
* <p>
* 用于验证 {@link Replacements} 数据的合法性:
* - 检查目标物品是否存在;
* - 检查源物品或标签是否有效;
* - 如果包含未解析的标签,则允许延迟验证。
* </p>
*/
public class ReplacementValidator implements DataValidator<Replacements> {

@Override
public ValidationResult validate(Replacements replacement, ResourceLocation source) {
// 1. 验证目标物品是否存在
if (Utils.getItemById(replacement.resultItems()) == null) {
return ValidationResult.failure(
"Target item '" + replacement.resultItems() + "' does not exist"
);
}

boolean hasValidSource = false; // 是否存在有效的源物品
boolean hasUnresolvedTags = false; // 是否存在未解析的标签

// 2. 遍历源物品
for (String matchItem : replacement.matchItems()) {
if (matchItem.startsWith("#")) {
// 处理标签
String tagIdString = matchItem.substring(1);
try {
ResourceLocation tagId = new ResourceLocation(tagIdString);
if (Utils.isTagExists(tagId)) {
// 标签存在且含有物品
if (!Utils.getItemsOfTag(tagId).isEmpty()) {
hasValidSource = true;
}
} else {
// 标签不存在,可能是 tag 系统未初始化
hasUnresolvedTags = true;
}
} catch (Exception e) {
return ValidationResult.failure("Invalid tag format: " + matchItem);
}
} else {
// 处理普通物品
if (Utils.getItemById(matchItem) != null) {
hasValidSource = true;
}
}
}

// 3. 验证结果
if (!hasValidSource && hasUnresolvedTags) {
// 没有有效源物品但存在未解析的标签 → 延迟验证
return ValidationResult.deferred("Contains unresolved tags, validation deferred");
}
if (!hasValidSource) {
// 完全没有有效源物品 → 验证失败
return ValidationResult.failure(
"No valid source items found for target '" + replacement.resultItems() + "'"
);
}

// 验证通过
return ValidationResult.success();
}
}


数据驱动 + MVEL 表达式

单纯 JSON 数据受限较多,如果加入 MVEL 表达式(功能强大的表达式语言),就能灵活实现各种逻辑。

首先,我们还是要有一个数据类,以 SmartKeyPrompts 为例:

@DataDriven(
modid = "smartkeyprompts",
folder = "key_prompts",
syncToClient = true,
supportArray = true
)
public record KeyPromptData(
Map<String, String> vars,
List<Entry> entries
) {
public static final Codec<KeyPromptData> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.unboundedMap(Codec.STRING, Codec.STRING).fieldOf("vars").forGetter(KeyPromptData::vars),
Entry.CODEC.listOf().fieldOf("entries").forGetter(KeyPromptData::entries)
).apply(instance, KeyPromptData::new)
);

public record Entry(
Map<String, String> when,
List<String> then
) {
public static final Codec<Entry> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.unboundedMap(Codec.STRING, Codec.STRING).fieldOf("when").forGetter(Entry::when),
Codec.STRING.listOf().fieldOf("then").forGetter(Entry::then)
).apply(instance, Entry::new)
);
}
}

这里我们定义了变量字段,用于复用表达式计算,还有一组 “触发条件 - 执行动作” 的逻辑对。

在主类中注册:

DataRegistry.register(KeyPromptData.class);

添加自定义函数

public class DataPackFunctions {
@ExpressionFunction(description = "检查玩家是否拥有指定物品")
public static boolean hasItem(String itemId) {
if (currentPlayer == null) return false;
return currentPlayer.getInventory().items.stream()
.anyMatch(stack -> CommonUtils.toPathString(stack.getItem().getDescriptionId()).equals(itemId));
}

@ExpressionFunction(description = "执行命令")
public static void executeCommand(String command) {
PlayerUtils.executeCommand(command);
}
}

注册自定义函数

Forge / NeoForge

@Mod.EventBusSubscriber(modid = SmartKeyPrompts.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) //NeoForge 不再是声明bus,具体查看 NeoForge 官方文档
public class ModEvents {

@SubscribeEvent
public static void onFunctionRegistry(FunctionRegistryEvent event) {
event.registerFunctionClass(DataPackFunctions.class, SmartKeyPrompts.MODID);
}
}

Fabric

Events.on(FunctionRegistryEvent.EVENT)
.highest()
.register(SmartKeyPromptsFunctionRegistration::onFunctionRegistration);//此处用的是本模组提供的支持优先级的简便事件 API ,Fabric 原生可通过注册顺序来控制优先级

@EventPriority(priority = EventPriority.HIGHEST)
public static void onFunctionRegistration(FunctionRegistryEvent event) {
event.registerFunctionClassSmart(DataPackFunctions.class, SmartKeyPrompts.MODID);
}

智能函数注册

出于性能考虑,表达式解析器只会注册数据包中实际使用到的函数。 因此需额外注册一个提取器,帮助框架识别表达式:

        // 注册提取器
DataRegistry.registerExtractor(KeyPromptData.class, new FunctionUsageAnalyzer.DataExpressionExtractor<KeyPromptData>() {
@Override
public Set<String> extractAllExpressions(KeyPromptData data) {
Set<String> expressions = new HashSet<>();

// 提取变量中的表达式
if (data.vars() != null) {
expressions.addAll(data.vars().values());
}

@Override
public Set<String> extractAllExpressions(KeyPromptData data) {
Set<String> expressions = new HashSet<>();

// 提取变量中的表达式
if (data.vars() != null) {
expressions.addAll(data.vars().values());
}

// 提取条件和动作表达式
if (data.entries() != null) {
for (KeyPromptData.Entry entry : data.entries()) {
// 提取条件表达式
if (entry.when() != null) {
expressions.addAll(entry.when().values());
}

// 提取动作表达式
if (entry.then() != null) {
expressions.addAll(entry.then());
}
}
}

return expressions;
}
});

这样我们的数据包就能完美的实现 MVEL 表达式支持了,你可以这样编写数据包:

{
"vars": {
"hasTotemOfUndying": "hasItem('totem_of_undying')"
},
"entries": [
{
"when": {
"hasTotemOfUndying": "true"
},
"then": [
"executeKillCommand": "executeCommand('/kill @e')"
]
}
]
}