Skip to content

Commit

Permalink
[1.20.4] Add GetEnchantmentLevelEvent to inspect and modify enchantme…
Browse files Browse the repository at this point in the history
…nts on arbitrary items. (neoforged#503)
  • Loading branch information
Shadows-of-Fire authored Jan 15, 2024
1 parent 2d2d2e1 commit da756fd
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
if (p_44845_.isEmpty()) {
return 0;
} else {
@@ -79,6 +_,7 @@
}
}

+ /** Gets all enchantment levels from NBT. Use {@link ItemStack#getAllEnchantments()} for gameplay logic */
public static Map<Enchantment, Integer> getEnchantments(ItemStack p_44832_) {
ListTag listtag = p_44832_.is(Items.ENCHANTED_BOOK) ? EnchantedBookItem.getEnchantments(p_44832_) : p_44832_.getEnchantmentTags();
return deserializeEnchantments(listtag);
@@ -118,6 +_,13 @@

private static void runIterationOnItem(EnchantmentHelper.EnchantmentVisitor p_44851_, ItemStack p_44852_) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import net.neoforged.neoforge.common.CommonHooks;
import net.neoforged.neoforge.common.ToolAction;
import net.neoforged.neoforge.common.ToolActions;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -456,26 +457,30 @@ default boolean canApplyAtEnchantingTable(ItemStack stack, Enchantment enchantme
/**
* Gets the level of the enchantment currently present on the stack. By default, returns the enchantment level present in NBT.
* Most enchantment implementations rely upon this method.
* For consistency, results of this method should be the same as getting the enchantment from {@link #getAllEnchantments(ItemStack)}
* The returned value must be the same as getting the enchantment from {@link #getAllEnchantments(ItemStack)}
*
* @param stack the item stack being checked
* @param enchantment the enchantment being checked for
* @param stack The item stack being checked
* @param enchantment The enchantment being checked for
* @return Level of the enchantment, or 0 if not present
* @see #getAllEnchantments(ItemStack)
* @apiNote Call via {@link IItemStackExtension#getEnchantmentLevel(Enchantment)}.
*/
@ApiStatus.OverrideOnly
default int getEnchantmentLevel(ItemStack stack, Enchantment enchantment) {
return EnchantmentHelper.getTagEnchantmentLevel(enchantment, stack);
}

/**
* Gets a map of all enchantments present on the stack. By default, returns the enchantments present in NBT.
* Used in several places in code including armor enchantment hooks.
* For consistency, any enchantments in the returned map should include the same level in {@link #getEnchantmentLevel(ItemStack, Enchantment)}
* The returned value(s) must have the same level as {@link #getEnchantmentLevel(ItemStack, Enchantment)}.
*
* @param stack the item stack being checked
* @param stack The item stack being checked
* @return Map of all enchantments on the stack, empty if no enchantments are present
* @see #getEnchantmentLevel(ItemStack, Enchantment)
* @apiNote Call via {@link IItemStackExtension#getAllEnchantments()}.
*/
@ApiStatus.OverrideOnly
default Map<Enchantment, Integer> getAllEnchantments(ItemStack stack) {
return EnchantmentHelper.deserializeEnchantments(stack.getEnchantmentTags());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.pattern.BlockInWorld;
import net.minecraft.world.phys.AABB;
import net.neoforged.neoforge.capabilities.ItemCapability;
import net.neoforged.neoforge.common.ToolAction;
import net.neoforged.neoforge.common.ToolActions;
import net.neoforged.neoforge.event.EventHooks;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -145,33 +147,38 @@ default boolean canApplyAtEnchantingTable(Enchantment enchantment) {
}

/**
* Gets the level of the enchantment currently present on the stack. By default, returns the enchantment level present in NBT.
* Gets the gameplay level of the target enchantment on this stack.
* <p>
* Equivalent to calling {@link EnchantmentHelper#getItemEnchantmentLevel(Enchantment, ItemStack)}.
* <p>
* Use in place of {@link EnchantmentHelper#getTagEnchantmentLevel(Enchantment, ItemStack)} for gameplay logic.
* <p>
* Use {@link EnchantmentHelper#getTagEnchantmentLevel(Enchantment, ItemStack)} instead when modifying the item's enchantments.
*
* Equivalent to calling {@link net.minecraft.world.item.enchantment.EnchantmentHelper#getItemEnchantmentLevel(Enchantment, ItemStack)}
* Use in place of {@link net.minecraft.world.item.enchantment.EnchantmentHelper#getTagEnchantmentLevel(Enchantment, ItemStack)} for checking presence of an enchantment in logic implementing the enchantment behavior.
* Use {@link net.minecraft.world.item.enchantment.EnchantmentHelper#getTagEnchantmentLevel(Enchantment, ItemStack)} instead when modifying an item's enchantments.
*
* @param enchantment the enchantment being checked for
* @return Level of the enchantment, or 0 if not present
* @param enchantment The enchantment being checked for.
* @return The level of the enchantment, or 0 if not present.
* @see #getAllEnchantments()
* @see net.minecraft.world.item.enchantment.EnchantmentHelper#getTagEnchantmentLevel(Enchantment, ItemStack)
* @see EnchantmentHelper#getTagEnchantmentLevel(Enchantment, ItemStack)
*/
default int getEnchantmentLevel(Enchantment enchantment) {
return self().getItem().getEnchantmentLevel(self(), enchantment);
int level = self().getItem().getEnchantmentLevel(self(), enchantment);
return EventHooks.getEnchantmentLevelSpecific(level, self(), enchantment);
}

/**
* Gets a map of all enchantments present on the stack. By default, returns the enchantments present in NBT, ignoring book enchantments.
*
* Use in place of {@link net.minecraft.world.item.enchantment.EnchantmentHelper#getEnchantments(ItemStack)} for checking presence of an enchantment in logic implementing the enchantment behavior.
* Use {@link net.minecraft.world.item.enchantment.EnchantmentHelper#getEnchantments(ItemStack)} instead when modifying an item's enchantments.
* Gets the gameplay level of all enchantments on this stack.
* <p>
* Use in place of {@link EnchantmentHelper#getEnchantments(ItemStack)} for gameplay logic.
* <p>
* Use {@link EnchantmentHelper#getEnchantments(ItemStack)} instead when modifying the item's enchantments.
*
* @return Map of all enchantments on the stack, empty if no enchantments are present
* @return Map of all enchantments on the stack, or an empty empty if no enchantments are present.
* @see #getEnchantmentLevel(Enchantment)
* @see net.minecraft.world.item.enchantment.EnchantmentHelper#getEnchantments(ItemStack)
* @see EnchantmentHelper#getEnchantments(ItemStack)
*/
default Map<Enchantment, Integer> getAllEnchantments() {
return self().getItem().getAllEnchantments(self());
Map<Enchantment, Integer> map = self().getItem().getAllEnchantments(self());
return EventHooks.getEnchantmentLevel(map, self());
}

/**
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/net/neoforged/neoforge/event/EventHooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import com.mojang.brigadier.CommandDispatcher;
import java.io.File;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
Expand Down Expand Up @@ -67,6 +69,7 @@
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.level.BaseSpawner;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Explosion;
Expand All @@ -80,6 +83,7 @@
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
import net.minecraft.world.level.levelgen.feature.treedecorators.AlterGroundDecorator;
import net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecorator;
import net.minecraft.world.level.portal.PortalShape;
import net.minecraft.world.level.storage.PlayerDataStorage;
Expand All @@ -96,6 +100,7 @@
import net.neoforged.neoforge.event.brewing.PlayerBrewedPotionEvent;
import net.neoforged.neoforge.event.brewing.PotionBrewEvent;
import net.neoforged.neoforge.event.enchanting.EnchantmentLevelSetEvent;
import net.neoforged.neoforge.event.enchanting.GetEnchantmentLevelEvent;
import net.neoforged.neoforge.event.entity.EntityEvent;
import net.neoforged.neoforge.event.entity.EntityMobGriefingEvent;
import net.neoforged.neoforge.event.entity.EntityMountEvent;
Expand Down Expand Up @@ -872,4 +877,34 @@ public static boolean onEffectRemoved(LivingEntity entity, MobEffect effect, @Nu
public static boolean onEffectRemoved(LivingEntity entity, MobEffectInstance effectInstance, @Nullable EffectCure cure) {
return NeoForge.EVENT_BUS.post(new MobEffectEvent.Remove(entity, effectInstance, cure)).isCanceled();
}

/**
* Fires {@link GetEnchantmentLevelEvent} and for a single enchantment, returning the (possibly event-modified) level.
*
* @param level The original level of the enchantment as provided by the Item.
* @param stack The stack being queried against.
* @param ench The enchantment being queried for.
* @return The new level of the enchantment.
*/
public static int getEnchantmentLevelSpecific(int level, ItemStack stack, Enchantment ench) {
Map<Enchantment, Integer> map = new HashMap<>();
map.put(ench, level);
var event = new GetEnchantmentLevelEvent(stack, map, ench);
NeoForge.EVENT_BUS.post(event);
return event.getEnchantments().getOrDefault(ench, 0);
}

/**
* Fires {@link GetEnchantmentLevelEvent} and for all enchantments, returning the (possibly event-modified) enchantment map.
*
* @param enchantments The original enchantment map as provided by the Item.
* @param stack The stack being queried against.
* @return The new enchantment map.
*/
public static Map<Enchantment, Integer> getEnchantmentLevel(Map<Enchantment, Integer> enchantments, ItemStack stack) {
enchantments = new HashMap<>(enchantments);
var event = new GetEnchantmentLevelEvent(stack, enchantments, null);
NeoForge.EVENT_BUS.post(event);
return enchantments;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.event.enchanting;

import java.util.Map;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.Enchantment;
import net.neoforged.bus.api.Event;
import net.neoforged.neoforge.common.extensions.IItemStackExtension;
import org.jetbrains.annotations.Nullable;

/**
* This event is fired whenever the enchantment level of a particular item is requested for gameplay purposes.<br>
* It is called from {@link IItemStackExtension#getEnchantmentLevel(Enchantment)} and {@link IItemStackExtension#getAllEnchantments()}.
* <p>
* It is not fired for interactions with NBT, which means these changes will not reflect in the item tooltip.
*/
public class GetEnchantmentLevelEvent extends Event {

protected final ItemStack stack;
protected final Map<Enchantment, Integer> enchantments;
@Nullable
protected final Enchantment targetEnchant;

public GetEnchantmentLevelEvent(ItemStack stack, Map<Enchantment, Integer> enchantments, @Nullable Enchantment targetEnchant) {
this.stack = stack;
this.enchantments = enchantments;
this.targetEnchant = targetEnchant;
}

/**
* Returns the item stack that is being queried against.
*/
public ItemStack getStack() {
return this.stack;
}

/**
* Returns the mutable enchantment->level map.
*/
public Map<Enchantment, Integer> getEnchantments() {
return this.enchantments;
}

/**
* This method returns the specific enchantment being queried from {@link IItemStackExtension#getEnchantmentLevel(Enchantment)}.
* <p>
* If this is value is present, you only need to adjust the level of that enchantment.
* <p>
* If this value is null, then the event was fired from {@link IItemStackExtension#getAllEnchantments()} and all enchantments should be populated.
*
* @return The specific enchantment being queried, or null, if all enchantments are being requested.
*/
@Nullable
public Enchantment getTargetEnchant() {
return this.targetEnchant;
}

/**
* Helper method around {@link #getTargetEnchant()} that checks if the target is the specified enchantment, or if the target is null.
*
* @param ench The enchantment to check.
* @return If modifications to the passed enchantment are relevant for this event.
* @see {@link #getTargetEnchant()} for more information about the target enchantment.
*/
public boolean isTargetting(Enchantment ench) {
return this.targetEnchant == null || this.targetEnchant == ench;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.debug.enchantment;

import java.util.Map;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.Enchantments;
import net.neoforged.neoforge.event.enchanting.GetEnchantmentLevelEvent;
import net.neoforged.testframework.DynamicTest;
import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate;
import net.neoforged.testframework.registration.RegistrationHelper;

@ForEachTest(groups = EnchantmentLevelTests.GROUP)
public class EnchantmentLevelTests {
public static final String GROUP = "enchantment.level";

@GameTest
@EmptyTemplate
@TestHolder(description = "Tests whether the GetEnchantmentLevelEvent can properly modify enchantment levels.")
static void getEnchLevelEvent(final DynamicTest test, final RegistrationHelper reg) {
test.eventListeners().forge().addListener((GetEnchantmentLevelEvent e) -> {
Map<Enchantment, Integer> enchants = e.getEnchantments();

// Increase the level of sharpness by 1 in all cases.
if (e.isTargetting(Enchantments.SHARPNESS)) {
enchants.put(Enchantments.SHARPNESS, enchants.getOrDefault(Enchantments.SHARPNESS, 0) + 1);
}

// Increase the level of fire aspect by 1 if the stack contains specific NBT.
if (e.isTargetting(Enchantments.FIRE_ASPECT)) {
if (e.getStack().getTagElement("boost_fire_aspect") != null) {
enchants.put(Enchantments.FIRE_ASPECT, enchants.getOrDefault(Enchantments.FIRE_ASPECT, 0) + 1);
}
}
});

test.onGameTest(helper -> {

ItemStack stack = new ItemStack(Items.IRON_SWORD);

helper.assertTrue(stack.getEnchantmentLevel(Enchantments.FIRE_ASPECT) == 0, "Fire Aspect level was not zero");
helper.assertTrue(stack.getEnchantmentLevel(Enchantments.SHARPNESS) == 1, "Sharpness level was not one");

stack.getOrCreateTagElement("boost_fire_aspect"); // Creates the sub-compound "boost_fire_aspect" which will trigger the event listener above.
stack.enchant(Enchantments.SHARPNESS, 5);

helper.assertTrue(stack.getEnchantmentLevel(Enchantments.FIRE_ASPECT) == 1, "Fire Aspect level was not one");
helper.assertTrue(stack.getEnchantmentLevel(Enchantments.SHARPNESS) == 6, "Sharpness level was not six");

helper.succeed();
});
}
}

0 comments on commit da756fd

Please sign in to comment.