Enchantment API ()

* ALLOW_ENCHANTING event

* Intrinsic enchantments + testmod

also fixed a bug in the testmod that prevented the custom damage handler from ever working

* Item-based override mechanism

* Replaces part of the use cases of the event with a convenient method to override in FabricItem.
* Updated and tested the testmod.

* javadoc

* Move event logic to FabricItemStack

* oops

* Simplify mixin

* Replace ActionResult with TriState

* Use TriState in testmod

* requests

* Clarify jdoc

* Ship without intrinsic enchantments at first

* Checkstyle

* Checkstyle

---------

Co-authored-by: modmuss50 <modmuss50@gmail.com>
This commit is contained in:
Syst3ms 2024-04-11 00:02:11 +02:00 committed by GitHub
parent 6793dde117
commit 8f5205a8a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 390 additions and 14 deletions
fabric-item-api-v1/src
fabric-object-builder-api-v1/src/test/java/net/fabricmc/fabric/test/object/builder

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.item.v1;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.item.ItemStack;
import net.minecraft.util.math.random.Random;
/*
* There is one context for each vanilla call to Enchantment#isAcceptableItem. The reason why RANDOM_ENCHANTMENT
* feels like a kitchen sink is because it corresponds to the one in EnchantmentHelper, which is shared across multiple
* uses.
*
* This also gets in the way of adding further context (nullable Player and BlockPos have been suggested
* in the past). It's not impossible to do so, but a probably a bit more brittle.
*/
/**
* An enum that describes the various contexts in which the game checks whether an enchantment can be applied to an item.
*/
public enum EnchantingContext {
/**
* When generating a random enchantment for the item. This includes the enchanting table, random
* mob equipment, and the {@code enchant_with_levels} loot function.
*
* @see EnchantmentHelper#generateEnchantments(Random, ItemStack, int, boolean)
*/
RANDOM_ENCHANTMENT,
/**
* When trying to apply an enchantment in an anvil.
*/
ANVIL,
/**
* When using the {@code /enchant} command.
*/
ENCHANT_COMMAND,
/**
* When randomly enchanting an item using the {@code enchant_randomly} loot function without a list of enchantments
* to choose from.
*/
LOOT_RANDOM_ENCHANTMENT
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.item.v1;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.item.ItemStack;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.fabricmc.fabric.api.util.TriState;
/**
* Events relating to enchantments, allowing for finer control of what enchantments can apply to different items.
*/
public final class EnchantmentEvents {
private EnchantmentEvents() { }
/**
* An event that allows overriding whether an {@link Enchantment} can be applied to an {@link ItemStack}.
*
* <p>This should only be used to modify the behavior of <em>external</em> items with regards to <em>external</em> enchantments,
* where 'external' means either vanilla or from another mod. For instance, a mod might allow enchanting a pickaxe
* with Sharpness (and only Sharpness) under certain specific conditions.</p>
*
* <p>To modify the behavior of your own modded <em>enchantments</em>, use {@link Enchantment#isAcceptableItem(ItemStack)} instead.
* To modify the behavior of your own modded <em>items</em>, use {@link FabricItem#canBeEnchantedWith(ItemStack, Enchantment, EnchantingContext)} instead.
* Note that this event triggers <em>before</em> {@link FabricItem#canBeEnchantedWith(ItemStack, Enchantment, EnchantingContext)},
* and that method will only be called if no listeners override it.</p>
*
* <p>Note that allowing an enchantment using this event does not guarantee the item will receive that enchantment,
* only that it isn't forbidden from doing so.</p>
*
* @see AllowEnchanting#allowEnchanting(Enchantment, ItemStack, EnchantingContext)
* @see Enchantment#isAcceptableItem(ItemStack)
* @see FabricItem#canBeEnchantedWith(ItemStack, Enchantment, EnchantingContext)
*/
public static final Event<AllowEnchanting> ALLOW_ENCHANTING = EventFactory.createArrayBacked(
AllowEnchanting.class,
callbacks -> (enchantment, target, context) -> {
for (AllowEnchanting callback : callbacks) {
TriState result = callback.allowEnchanting(enchantment, target, context);
if (result != TriState.DEFAULT) {
return result;
}
}
return TriState.DEFAULT;
}
);
@FunctionalInterface
public interface AllowEnchanting {
/**
* Checks whether an {@link Enchantment} should be applied to a given {@link ItemStack}.
*
* @param enchantment the enchantment that may be applied
* @param target the target item
* @param enchantingContext the enchanting context in which this check is made
* @return {@link TriState#TRUE} if the enchantment may be applied, {@link TriState#FALSE} if it
* may not, {@link TriState#DEFAULT} to fall back to other callbacks/vanilla behavior
* @see EnchantingContext
*/
TriState allowEnchanting(
Enchantment enchantment,
ItemStack target,
EnchantingContext enchantingContext
);
}
}

View file

@ -17,6 +17,7 @@
package net.fabricmc.fabric.api.item.v1;
import net.minecraft.component.type.AttributeModifiersComponent;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
@ -107,6 +108,26 @@ public interface FabricItem {
return ((Item) this).hasRecipeRemainder() ? ((Item) this).getRecipeRemainder().getDefaultStack() : ItemStack.EMPTY;
}
/**
* Determines if the item is allowed to receive an {@link Enchantment}. This can be used to manually override what
* enchantments a modded item is able to receive.
*
* <p>For example, one might want a modded item to be able to receive Unbreaking, but not Mending, which cannot be
* achieved with the vanilla tag system alone. Alternatively, one might want to do the same thing with enchantments
* from other mods, which don't have a similar tag system in general.</p>
*
* <p>Note that this method is only called <em>after</em> the {@link EnchantmentEvents#ALLOW_ENCHANTING} event, and
* only if none of the listeners to that event override the result.</p>
*
* @param stack the current stack
* @param enchantment the enchantment to check
* @param context the context in which the enchantment is being checked
* @return whether the enchantment is allowed to apply to the stack
*/
default boolean canBeEnchantedWith(ItemStack stack, Enchantment enchantment, EnchantingContext context) {
return enchantment.isAcceptableItem(stack);
}
/**
* Fabric-provided extensions for {@link Item.Settings}.
* This interface is automatically implemented on all item settings via Mixin and interface injection.

View file

@ -16,9 +16,12 @@
package net.fabricmc.fabric.api.item.v1;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.fabricmc.fabric.api.util.TriState;
/*
* Fabric-provided extensions for {@link ItemStack}.
* This interface is automatically implemented on all item stacks via Mixin and interface injection.
@ -36,4 +39,24 @@ public interface FabricItemStack {
default ItemStack getRecipeRemainder() {
return ((ItemStack) this).getItem().getRecipeRemainder((ItemStack) this);
}
/**
* Determines whether this {@link ItemStack} can be enchanted with the given {@link Enchantment}.
*
* <p>When checking whether an enchantment can be applied to an {@link ItemStack}, use this method instead of
* {@link Enchantment#isAcceptableItem(ItemStack)}</p>
*
* @param enchantment the enchantment to check
* @param context the context in which the enchantment is being checked
* @return whether the enchantment is allowed to apply to the stack
* @see FabricItem#canBeEnchantedWith(ItemStack, Enchantment, EnchantingContext)
*/
default boolean canBeEnchantedWith(Enchantment enchantment, EnchantingContext context) {
TriState result = EnchantmentEvents.ALLOW_ENCHANTING.invoker().allowEnchanting(
enchantment,
(ItemStack) this,
context
);
return result.orElseGet(() -> ((ItemStack) this).getItem().canBeEnchantedWith((ItemStack) this, enchantment, context));
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.item;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.AnvilScreenHandler;
import net.minecraft.screen.ForgingScreenHandler;
import net.minecraft.screen.ScreenHandlerContext;
import net.minecraft.screen.ScreenHandlerType;
import net.fabricmc.fabric.api.item.v1.EnchantingContext;
@Mixin(AnvilScreenHandler.class)
abstract class AnvilScreenHandlerMixin extends ForgingScreenHandler {
AnvilScreenHandlerMixin(@Nullable ScreenHandlerType<?> type, int syncId, PlayerInventory playerInventory, ScreenHandlerContext context) {
super(type, syncId, playerInventory, context);
}
@Redirect(
method = "updateResult",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/enchantment/Enchantment;isAcceptableItem(Lnet/minecraft/item/ItemStack;)Z"
)
)
private boolean callAllowEnchantingEvent(Enchantment instance, ItemStack stack) {
return stack.canBeEnchantedWith(instance, EnchantingContext.ANVIL);
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.item;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.item.ItemStack;
import net.minecraft.server.command.EnchantCommand;
import net.fabricmc.fabric.api.item.v1.EnchantingContext;
@Mixin(EnchantCommand.class)
abstract class EnchantCommandMixin {
@Redirect(
method = "execute",
at = @At(value = "INVOKE", target = "Lnet/minecraft/enchantment/Enchantment;isAcceptableItem(Lnet/minecraft/item/ItemStack;)Z")
)
private static boolean callAllowEnchantingEvent(Enchantment instance, ItemStack stack) {
return stack.canBeEnchantedWith(instance, EnchantingContext.ENCHANT_COMMAND);
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.item;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.item.ItemStack;
import net.minecraft.loot.function.EnchantRandomlyLootFunction;
import net.fabricmc.fabric.api.item.v1.EnchantingContext;
@Mixin(EnchantRandomlyLootFunction.class)
abstract class EnchantRandomlyLootFunctionMixin {
@Redirect(
method = "method_53327",
at = @At(value = "INVOKE", target = "Lnet/minecraft/enchantment/Enchantment;isAcceptableItem(Lnet/minecraft/item/ItemStack;)Z")
)
private static boolean callAllowEnchantingEvent(Enchantment instance, ItemStack stack) {
return stack.canBeEnchantedWith(instance, EnchantingContext.LOOT_RANDOM_ENCHANTMENT);
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.item;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.item.ItemStack;
import net.fabricmc.fabric.api.item.v1.EnchantingContext;
@Mixin(EnchantmentHelper.class)
abstract class EnchantmentHelperMixin {
@Redirect(
method = "getPossibleEntries",
at = @At(value = "INVOKE", target = "Lnet/minecraft/enchantment/Enchantment;isAcceptableItem(Lnet/minecraft/item/ItemStack;)Z")
)
private static boolean useCustomEnchantingChecks(Enchantment instance, ItemStack stack) {
return stack.canBeEnchantedWith(instance, EnchantingContext.RANDOM_ENCHANTMENT);
}
}

View file

@ -4,7 +4,11 @@
"compatibilityLevel": "JAVA_17",
"mixins": [
"AbstractFurnaceBlockEntityMixin",
"AnvilScreenHandlerMixin",
"BrewingStandBlockEntityMixin",
"EnchantCommandMixin",
"EnchantmentHelperMixin",
"EnchantRandomlyLootFunctionMixin",
"ItemMixin",
"ItemSettingsMixin",
"ItemStackMixin",

View file

@ -17,8 +17,12 @@
package net.fabricmc.fabric.test.item;
import net.minecraft.component.DataComponentType;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.item.PickaxeItem;
import net.minecraft.item.ToolMaterials;
import net.minecraft.network.codec.PacketCodecs;
@ -30,21 +34,14 @@ import net.minecraft.util.dynamic.Codecs;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.item.v1.CustomDamageHandler;
import net.fabricmc.fabric.api.item.v1.EnchantingContext;
import net.fabricmc.fabric.api.item.v1.EnchantmentEvents;
import net.fabricmc.fabric.api.registry.FuelRegistry;
import net.fabricmc.fabric.api.util.TriState;
public class CustomDamageTest implements ModInitializer {
public static final Item WEIRD_PICK = new WeirdPick();
public static final DataComponentType<Integer> WEIRD = Registry.register(Registries.DATA_COMPONENT_TYPE, new Identifier("fabric-item-api-v1-testmod", "weird"),
DataComponentType.<Integer>builder().codec(Codecs.NONNEGATIVE_INT).packetCodec(PacketCodecs.VAR_INT).build());
@Override
public void onInitialize() {
Registry.register(Registries.ITEM, new Identifier("fabric-item-api-v1-testmod", "weird_pickaxe"), WEIRD_PICK);
FuelRegistry.INSTANCE.add(WEIRD_PICK, 200);
// TODO 1.20.5
// FabricBrewingRecipeRegistry.registerPotionRecipe(Potions.WATER, Ingredient.ofItems(WEIRD_PICK), Potions.AWKWARD);
}
public static final CustomDamageHandler WEIRD_DAMAGE_HANDLER = (stack, amount, entity, slot, breakCallback) -> {
// If sneaking, apply all damage to vanilla. Otherwise, increment a tag on the stack by one and don't apply any damage
if (entity.isSneaking()) {
@ -54,6 +51,25 @@ public class CustomDamageTest implements ModInitializer {
return 0;
}
};
// Do this static init *after* the damage handler otherwise it's still null while inside the constructor
public static final Item WEIRD_PICK = new WeirdPick();
@Override
public void onInitialize() {
Registry.register(Registries.ITEM, new Identifier("fabric-item-api-v1-testmod", "weird_pickaxe"), WEIRD_PICK);
FuelRegistry.INSTANCE.add(WEIRD_PICK, 200);
// TODO 1.20.5
// FabricBrewingRecipeRegistry.registerPotionRecipe(Potions.WATER, Ingredient.ofItems(WEIRD_PICK), Potions.AWKWARD);
EnchantmentEvents.ALLOW_ENCHANTING.register(((enchantment, target, enchantingContext) -> {
if (target.isOf(Items.DIAMOND_PICKAXE)
&& enchantment == Enchantments.SHARPNESS
&& EnchantmentHelper.hasSilkTouch(target)) {
return TriState.TRUE;
}
return TriState.DEFAULT;
}));
}
public static class WeirdPick extends PickaxeItem {
protected WeirdPick() {
@ -77,5 +93,11 @@ public class CustomDamageTest implements ModInitializer {
return ItemStack.EMPTY;
}
@Override
public boolean canBeEnchantedWith(ItemStack stack, Enchantment enchantment, EnchantingContext context) {
return context == EnchantingContext.ANVIL && enchantment == Enchantments.FIRE_ASPECT
|| enchantment != Enchantments.FORTUNE && super.canBeEnchantedWith(stack, enchantment, context);
}
}
}

View file

@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"fabric-item-api-v1-testmod:weird_pickaxe"
]
}

View file

@ -20,10 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import net.minecraft.entity.SpawnLocation;
import net.minecraft.entity.SpawnLocationTypes;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@ -33,6 +29,7 @@ import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.SpawnGroup;
import net.minecraft.entity.SpawnLocationTypes;
import net.minecraft.entity.SpawnRestriction;
import net.minecraft.entity.attribute.DefaultAttributeContainer;
import net.minecraft.entity.attribute.DefaultAttributeRegistry;