自定义掉落对象(Custom Loot Objects)
由于掉落表(loot table)系统的复杂性,内部涉及多个 注册表,这些注册表都可以被模组开发者用来扩展行为。
所有与掉落表相关的注册表(registry)都遵循类似的模式。要添加新的注册表条目,通常需要扩展某个类或实现某个接口,将你的功能逻辑写入其中。接着,需要为其定义一个 编解码器(codec) 用于序列化,并像平常一样使用 DeferredRegister 注册该编解码器到相应的注册表。这种模式与大多数注册表(比如方块/blockstates 和物品/item stacks)采用的“一个基础对象,多个实例”的方式一致。
自定义掉落条目类型(Custom Loot Entry Types)
要创建自定义的掉落条目类型,需要扩展 LootPoolEntryContainer,或者它的两个直接子类之一:LootPoolSingletonContainer 或 CompositeEntryBase。为了举例说明,我们将创建一个掉落条目类型,该类型会返回一个 实体 的掉落物——这只是为了演示,实际开发中更理想的做法是直接引用另一个掉落表。首先,我们来创建自己的掉落条目类型类:
// 我们继承自 LootPoolSingletonContainer,因为我们有一个“有限”的掉落集合。
// 部分代码参考了 NestedLootTable。
public class EntityLootEntry extends LootPoolSingletonContainer {
// 用于保存我们要为其抽取其他掉落表的实体类型(entity type)的 Holder。
private final Holder<EntityType<?>> entity;
// 通常做法是使用私有构造函数,并提供一个静态工厂方法。
// 这是因为 weight、quality、conditions 和 functions 会在下面通过 lambda 传入。
private EntityLootEntry(Holder<EntityType<?>> entity, int weight, int quality, List<LootItemCondition> conditions, List<LootItemFunction> functions) {
// 将 lambda 提供的参数传递给父类。
super(weight, quality, conditions, functions);
// 设置我们的字段值。
this.entity = entity;
}
// 静态构建器方法,接收自定义参数,并结合一个 lambda 提供所有条目类型通用的参数。
public static LootPoolSingletonContainer.Builder<?> entityLoot(Holder<EntityType<?>> entity) {
// 使用 LootPoolSingletonContainer 中定义的静态 simpleBuilder() 方法。
return simpleBuilder((weight, quality, conditions, functions) -> new EntityLootEntry(entity, weight, quality, conditions, functions));
}
// 核心逻辑在这里。通常我们通过调用 consumer 的 #accept 方法来添加物品堆(item stack)。
// 但在这里,我们让 #getRandomItems 方法帮我们完成这件事。
@Override
public void createItemStack(Consumer<ItemStack> consumer, LootContext context) {
// 获取实体的掉落表(loot table)。如果不存在,则会返回一个空掉落表,所以无需进行 null 检查。
LootTable table = context.getLevel().reloadableRegistries().getLootTable(entity.value().getDefaultLootTable());
// 这里使用原始版本,因为原版也是这样做的 :P
// #getRandomItemsRaw 会自动为我们调用 consumer#accept,处理 roll 的结果。
table.getRandomItemsRaw(context, consumer);
}
}
接下来,我们为我们的掉落条目(loot entry)创建一个 MapCodec:
// 这个常量定义在 EntityLootEntry 内部。
public static final MapCodec<EntityLootEntry> CODEC = RecordCodecBuilder.mapCodec(inst ->
// 添加我们自己的字段。
inst.group(
// 一个引用实体类型 id 的值。
BuiltInRegistries.ENTITY_TYPE.holderByNameCodec().fieldOf("entity").forGetter(e -> e.entity)
)
// 添加通用字段:weight、display、conditions 和 functions。
.and(singletonFields(inst))
.apply(inst, EntityLootEntry::new)
);
然后我们在 注册表 中使用这个 codec:
public static final DeferredRegister<LootPoolEntryType> LOOT_POOL_ENTRY_TYPES =
DeferredRegister.create(Registries.LOOT_POOL_ENTRY_TYPE, ExampleMod.MOD_ID);
public static final Supplier<LootPoolEntryType> ENTITY_LOOT =
LOOT_POOL_ENTRY_TYPES.register("entity_loot", () -> new LootPoolEntryType(EntityLootEntry.CODEC));
最后,在我们的掉落条目类(loot entry class)中,必须重写 getType() 方法:
public class EntityLootEntry extends LootPoolSingletonContainer {
// 这里是其他内容
@Override
public LootPoolEntryType getType() {
return ENTITY_LOOT.get();
}
}
自定义数值提供器(Custom Number Providers)
要创建一个自定义数值提供器(number provider),需要实现 NumberProvider 接口。举个例子,假设我们想要创建一个会改变传入数值符号的数值提供器:
// 我们接受另一个数值提供器作为基础。
public record InvertedSignProvider(NumberProvider base) implements NumberProvider {
public static final MapCodec<InvertedSignProvider> CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group(
NumberProviders.CODEC.fieldOf("base").forGetter(InvertedSignProvider::base)
).apply(inst, InvertedSignProvider::new));
// 返回一个 float 值。根据需要使用 context 和 record 的参数。
@Override
public float getFloat(LootContext context) {
return -this.base.getFloat(context);
}
// 返回一个 int 值。根据需要使用 context 和 record 的参数。
// 重写这个方法是可选的,默认实现会对 #getFloat 的结果进行四舍五入。
@Override
public int getInt(LootContext context) {
return -this.base.getInt(context);
}
// 返回该提供器所用到的掉落上下文参数集合。详见下文说明。
// 由于我们有一个基础值,这里直接调用 base 的方法即可。
@Override
public Set<ContextKey<?>> getReferencedContextParams() {
return this.base.getReferencedContextParams();
}
}
与自定义掉落条目类型类似,我们需要在 注册表 中注册对应的 codec:
public static final DeferredRegister<LootNumberProviderType> LOOT_NUMBER_PROVIDER_TYPES =
DeferredRegister.create(Registries.LOOT_NUMBER_PROVIDER_TYPE, ExampleMod.MOD_ID);
public static final Supplier<LootNumberProviderType> INVERTED_SIGN =
LOOT_NUMBER_PROVIDER_TYPES.register("inverted_sign", () -> new LootNumberProviderType(InvertedSignProvider.CODEC));
同样地,在我们的数值提供器类中,也必须重写 getType() 方法:
public record InvertedSignProvider(NumberProvider base) implements NumberProvider {
// 这里是其他内容
@Override
public LootNumberProviderType getType() {
return INVERTED_SIGN.get();
}
}
基于等级的自定义数值(Custom Level-Based Values)
可以通过在一个 record 中实现 LevelBasedValue 接口来自定义 LevelBasedValue。同样地,为了举例,假设我们希望反转另一个 LevelBasedValue 的输出结果:
public record InvertedSignLevelBasedValue(LevelBasedValue base) implements LevelBaseValue {
public static final MapCodec<InvertedLevelBasedValue> CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group(
LevelBasedValue.CODEC.fieldOf("base").forGetter(InvertedLevelBasedValue::base)
).apply(inst, InvertedLevelBasedValue::new));
// 执行我们的操作。
@Override
public float calculate(int level) {
return -this.base.calculate(level);
}
// 与 NumberProviders 不同,我们不返回已注册的类型,而是直接返回 codec。
@Override
public MapCodec<InvertedLevelBasedValue> codec() {
return CODEC;
}
}
然后,我们可以在 注册表 中直接使用这个 codec,如下所示:
public static final DeferredRegister<MapCodec<? extends LevelBasedValue>> LEVEL_BASED_VALUES =
DeferredRegister.create(Registries.ENCHANTMENT_LEVEL_BASED_VALUE_TYPE, ExampleMod.MOD_ID);
public static final Supplier<MapCodec<? extends LevelBasedValue>> INVERTED_SIGN =
LEVEL_BASED_VALUES.register("inverted_sign", () -> InvertedSignLevelBasedValue.CODEC);
自定义掉落条件(Custom Loot Conditions)
首先,我们需要创建一个实现 LootItemCondition 接口的掉落物品条件类。举个例子,假设我们只希望在击杀生物的玩家拥有特定经验等级时条件才成立:
public record HasXpLevelCondition(int level) implements LootItemCondition {
// 添加本条件所需的上下文。在本例中,就是玩家必须拥有的经验等级。
public static final MapCodec<HasXpLevelCondition> CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group(
Codec.INT.fieldOf("level").forGetter(HasXpLevelCondition::level)
).apply(inst, HasXpLevelCondition::new));
// 在这里判断条件是否成立。从提供的 LootContext 中获取所需的掉落上下文参数。
// 本例中,我们希望 KILLER_ENTITY 至少拥有我们要求的等级。
@Override
public boolean test(LootContext context) {
@Nullable
Entity entity = context.getOptionalParameter(LootContextParams.KILLER_ENTITY);
return entity instanceof Player player && player.experienceLevel >= level;
}
// 告诉游戏我们希望从掉落上下文中获取哪些参数,用于校验。
@Override
public Set<ContextKey<?>> getReferencedContextParams() {
return ImmutableSet.of(LootContextParams.KILLER_ENTITY);
}
}
我们可以使用该条件的 codec 将条件类型注册到 注册表 中:
public static final DeferredRegister<LootItemConditionType> LOOT_CONDITION_TYPES =
DeferredRegister.create(Registries.LOOT_CONDITION_TYPE, ExampleMod.MOD_ID);
public static final Supplier<LootItemConditionType> MIN_XP_LEVEL =
LOOT_CONDITION_TYPES.register("min_xp_level", () -> new LootItemConditionType(HasXpLevelCondition.CODEC));
完成上述步骤后,我们需要在自定义条件中重写 #getType 方法,并返回已注册的类型:
public record HasXpLevelCondition(int level) implements LootItemCondition {
// 这里省略其他内容
@Override
public LootItemConditionType getType() {
return MIN_XP_LEVEL.get();
}
}
自定义战利品函数(Custom Loot Functions)
首先,我们需要创建一个继承自 LootItemFunction 的自定义类。LootItemFunction 继承自 BiFunction<ItemStack, LootContext, ItemStack>,也就是说,我们需要使用已有的物品堆(item stack)和战利品上下文(loot context),返回一个新的、已修改的物品堆。不过,几乎所有的战利品函数都不是直接继承自 LootItemFunction,而是继承自 LootItemConditionalFunction。这个类内置了应用战利品条件(loot condition)的功能——只有当战利品条件满足时,函数才会被应用。为了举例,我们将实现一个给物品随机附魔且指定等级的函数:
// 代码改编自原版的 EnchantRandomlyFunction 类。
// LootItemConditionalFunction 是一个抽象类,不是接口,因此这里不能使用 record。
public class RandomEnchantmentWithLevelFunction extends LootItemConditionalFunction {
// 我们的上下文:一个可选的附魔列表,以及一个等级。
private final Optional<HolderSet<Enchantment>> enchantments;
private final int level;
// 我们的 codec。
public static final MapCodec<RandomEnchantmentWithLevelFunction> CODEC =
// #commonFields 会添加 conditions 字段。
RecordCodecBuilder.mapCodec(inst -> commonFields(inst).and(inst.group(
RegistryCodecs.homogeneousList(Registries.ENCHANTMENT).optionalFieldOf("enchantments").forGetter(e -> e.enchantments),
Codec.INT.fieldOf("level").forGetter(e -> e.level)
).apply(inst, RandomEnchantmentWithLevelFunction::new));
public RandomEnchantmentWithLevelFunction(List<LootItemCondition> conditions, Optional<HolderSet<Enchantment>> enchantments, int level) {
super(conditions);
this.enchantments = enchantments;
this.level = level;
}
// 执行我们的附魔应用逻辑。大部分内容复制自 EnchantRandomlyFunction#run。
@Override
public ItemStack run(ItemStack stack, LootContext context) {
RandomSource random = context.getRandom();
List<Holder<Enchantment>> stream = this.enchantments
.map(HolderSet::stream)
.orElseGet(() -> context.getLevel().registryAccess().registryOrThrow(Registries.ENCHANTMENT).listElements().map(Function.identity()))
.filter(e -> e.value().canEnchant(stack))
.toList();
Optional<Holder<Enchantment>> optional = Util.getRandomSafe(list, random);
if (optional.isEmpty()) {
LOGGER.warn("Couldn't find a compatible enchantment for {}", stack);
} else {
if (stack.is(Items.BOOK)) {
stack = new ItemStack(Items.ENCHANTED_BOOK);
}
stack.enchant(enchantment, Mth.nextInt(random, enchantment.value().getMinLevel(), enchantment.value().getMaxLevel()));
}
return stack;
}
}
然后我们可以使用该函数的 codec,将其类型 注册 到注册表(Registry)中:
public static final DeferredRegister<LootItemFunctionType<?>> LOOT_FUNCTION_TYPES =
DeferredRegister.create(Registries.LOOT_FUNCTION_TYPE, ExampleMod.MOD_ID);
public static final Supplier<LootItemFunctionType<RandomEnchantmentWithLevelFunction>> RANDOM_ENCHANTMENT_WITH_LEVEL =
LOOT_FUNCTION_TYPES.register("random_enchantment_with_level", () -> new LootItemFunctionType(RandomEnchantmentWithLevelFunction.CODEC));
完成上述操作后,我们还需要在我们的条件类中重写 #getType 方法,并返回刚刚注册的类型:
public class RandomEnchantmentWithLevelFunction extends LootItemConditionalFunction {
// 这里省略了其他内容
@Override
public LootItemFunctionType<?> getType() {
// 返回 此函数的类型
return RANDOM_ENCHANTMENT_WITH_LEVEL.get();
}
}