自定义战利品对象
由于战利品表系统较为复杂,其中有多个注册表在协同工作,Mod 开发者都可以借助它们来添加更多行为。
所有与战利品表相关的注册表都遵循类似的模式。要添加一个新的注册项,你通常需要继承某个类或实现某个接口来承载你的功能。然后,你为其定义一个用于序列化的 codec,并像平常一样使用 DeferredRegister 把该 codec 注册到对应的注册表中。这与大多数注册表(例如方块/方块状态、物品/物品堆叠)所采用的“一个基础对象,多个实例”思路一致。
自定义战利品条目类型
要创建自定义的战利品条目类型,可以继承 LootPoolEntryContainer 或它的两个直接子类之一:LootPoolSingletonContainer 或 CompositeEntryBase。作为示例,我们想创建一个返回某实体掉落物的战利品条目类型——这纯粹是为了演示,实际中直接引用另一个战利品表会更为理想。我们先从创建战利品条目类型类开始:
// We extend LootPoolSingletonContainer since we have a "finite" set of drops.
// Some of this code is adapted from NestedLootTable.
public class EntityLootEntry extends LootPoolSingletonContainer {
// A Holder for the entity type we want to roll the other table for.
private final Holder<EntityType<?>> entity;
// It is common practice to have a private constructor and have a static factory method.
// This is because weight, quality, conditions, and functions are supplied by a lambda below.
private EntityLootEntry(Holder<EntityType<?>> entity, int weight, int quality, List<LootItemCondition> conditions, List<LootItemFunction> functions) {
// Pass lambda-provided parameters to super.
super(weight, quality, conditions, functions);
// Set our values.
this.entity = entity;
}
// Static builder method, accepting our custom parameters and combining them with a lambda that supplies the values common to all entry types.
public static LootPoolSingletonContainer.Builder<?> entityLoot(Holder<EntityType<?>> entity) {
// Use the static simpleBuilder() method defined in LootPoolSingletonContainer.
return simpleBuilder((weight, quality, conditions, functions) -> new EntityLootEntry(entity, weight, quality, conditions, functions));
}
// This is where the magic happens. To add an item stack, we generally call #accept on the consumer.
// However, in this case, we let #getRandomItems do that for us.
@Override
public void createItemStack(Consumer<ItemStack> consumer, LootContext context) {
// Get the entity's loot table. If it doesn't exist, an empty loot table will be returned, so null-checking is not necessary.
LootTable table = context.getLevel().reloadableRegistries().getLootTable(entity.value().getDefaultLootTable());
// Use the raw version here, because 原版 does it too. :P
// #getRandomItemsRaw calls consumer#accept for us on the results of the roll.
table.getRandomItemsRaw(context, consumer);
}
}
接下来,我们为战利品条目创建一个 MapCodec:
// This is placed as a constant in EntityLootEntry.
public static final MapCodec<EntityLootEntry> CODEC = RecordCodecBuilder.mapCodec(inst ->
// Add our own fields.
inst.group(
// A value referencing an entity type id.
BuiltInRegistries.ENTITY_TYPE.holderByNameCodec().fieldOf("entity").forGetter(e -> e.entity)
)
// Add common fields: weight, display, conditions, and 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));
最后,在我们的战利品条目类中,必须重写 getType():
public class EntityLootEntry extends LootPoolSingletonContainer {
// other stuff here
@Override
public LootPoolEntryType getType() {
return ENTITY_LOOT.get();
}
}
自定义数值提供器
要创建自定义的数值提供器(number provider),需实现 NumberProvider 接口。作为示例,假设我们想创建一个把所提供数字取反的数值提供器:
// We accept another number provider as our base.
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));
// Return a float value. Use the context and the record parameters as needed.
@Override
public float getFloat(LootContext context) {
return -this.base.getFloat(context);
}
// Return an int value. Use the context and the record parameters as needed.
// Overriding this is optional, the default implementation will round the result of #getFloat.
@Override
public int getInt(LootContext context) {
return -this.base.getInt(context);
}
// Return a set of the loot context params used by this provider. See below for more information.
// Since we have a base value, we just defer to the base.
@Override
public Set<LootContextParam<?>> 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 {
// other stuff here
@Override
public LootNumberProviderType getType() {
return INVERTED_SIGN.get();
}
}
自定义基于等级的数值
要创建自定义的 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));
// Perform our operation.
@Override
public float calculate(int level) {
return -this.base.calculate(level);
}
// Unlike NumberProviders, we don't return the registered type, instead we return the codec directly.
@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);
自定义战利品条件
开始时,我们创建实现 LootItemCondition 的战利品条件类。作为示例,假设我们希望仅当击杀生物的玩家拥有特定经验等级时,条件才通过:
public record HasXpLevelCondition(int level) implements LootItemCondition {
// Add the context we need for this condition. In our case, this will be the xp level the player must have.
public static final MapCodec<HasXpLevelCondition> CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group(
Codec.INT.fieldOf("level").forGetter(HasXpLevelCondition::level)
).apply(inst, HasXpLevelCondition::new));
// Evaluates the condition here. Get the required loot context parameters from the provided LootContext.
// In our case, we want the KILLER_ENTITY to have at least our required level.
@Override
public boolean test(LootContext context) {
Entity entity = context.getParamOrNull(LootContextParams.KILLER_ENTITY);
return entity instanceof Player player && player.experienceLevel >= level;
}
// Tell the game what parameters we expect from the loot context. Used in validation.
@Override
public Set<LootContextParam<?>> 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 {
// other stuff here
@Override
public LootItemConditionType getType() {
return MIN_XP_LEVEL.get();
}
}
自定义战利品函数
开始时,我们创建自己的类来继承 LootItemFunction。 LootItemFunction 继承自 BiFunction<ItemStack, LootContext, ItemStack>,因此我们要做的就是利用现有的物品堆叠和战利品上下文,返回一个新的、经过修改的物品堆叠。不过,几乎所有战利品函数都不直接继承 LootItemFunction,而是继承 LootItemConditionalFunction。该类内置了将战利品条件应用于函数的功能——只有当战利品条件满足时,函数才会被应用。作为示例,我们来给物品施加一个指定等级的随机附魔:
// Code adapted from 原版's EnchantRandomlyFunction class.
// LootItemConditionalFunction is an abstract class, not an interface, so we cannot use a record here.
public class RandomEnchantmentWithLevelFunction extends LootItemConditionalFunction {
// Our context: an optional list of enchantments, and a level.
private final Optional<HolderSet<Enchantment>> enchantments;
private final int level;
// Our codec.
public static final MapCodec<RandomEnchantmentWithLevelFunction> CODEC =
// #commonFields adds the conditions field.
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;
}
// Run our enchantment application logic. Most of this is copied from 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).holders().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 把函数类型注册到注册表:
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 {
// other stuff here
@Override
public LootItemFunctionType getType() {
return RANDOM_ENCHANTMENT_WITH_LEVEL.get();
}
}