diff --git a/gradle.properties b/gradle.properties index b6ca6d0930..506ecdcb45 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -pluginVersion=1.219.0 +pluginVersion=1.220.0 diff --git a/nms/java21/src/main/java/io/th0rgal/oraxen/nms/handler/java21/NMSHandler.java b/nms/java21/src/main/java/io/th0rgal/oraxen/nms/handler/java21/NMSHandler.java index c3e31662d5..d361d9fbf3 100644 --- a/nms/java21/src/main/java/io/th0rgal/oraxen/nms/handler/java21/NMSHandler.java +++ b/nms/java21/src/main/java/io/th0rgal/oraxen/nms/handler/java21/NMSHandler.java @@ -33,7 +33,6 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.PositionMoveRotation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; @@ -49,6 +48,7 @@ import net.minecraft.world.item.JukeboxSong; import net.minecraft.world.item.component.Consumable; import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.DeathProtection; import net.minecraft.world.item.consume_effects.*; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.context.DirectionalPlaceContext; @@ -71,10 +71,9 @@ import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.entity.Player; +import org.bukkit.event.Listener; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.components.FoodComponent; -import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; @@ -83,7 +82,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; public class NMSHandler implements io.th0rgal.oraxen.nms.NMSHandler { @@ -249,11 +247,6 @@ public BlockHitResult getPlayerPOVHitResult(Level world, net.minecraft.world.ent return world.clip(new ClipContext(vec3, vec32, ClipContext.Block.OUTLINE, fluidHandling, player)); } - @Override - public void customBlockDefaultTools(Player player) { - - } - private TagNetworkSerialization.NetworkPayload createPayload() { Constructor constructor = Arrays .stream(TagNetworkSerialization.NetworkPayload.class.getDeclaredConstructors()).findFirst() @@ -288,11 +281,6 @@ private Map createTagRegistryMap() { }).collect(HashMap::new, Map::putAll, Map::putAll); } - @Override - public boolean getSupported() { - return true; - } - /** * Sets a component on an item using the DataComponents registry * @@ -449,24 +437,6 @@ private void handleListValue(net.minecraft.nbt.CompoundTag nbt, String key, List } } - @SuppressWarnings("UnstableApiUsage") - @Override - public void foodComponent(ItemBuilder item, ConfigurationSection foodSection) { - FoodComponent foodComponent = new ItemStack(item.getType()).getItemMeta().getFood(); - - // Ensure nutrition is non-negative - int nutrition = Math.max(foodSection.getInt("nutrition"), 0); - foodComponent.setNutrition(nutrition); - - // Ensure saturation is non-negative - float saturation = Math.max((float) foodSection.getDouble("saturation", 0.0), 0f); - foodComponent.setSaturation(saturation); - - foodComponent.setCanAlwaysEat(foodSection.getBoolean("can_always_eat", false)); - - item.setFoodComponent(foodComponent); - } - @SuppressWarnings("UnstableApiUsage") @Override public void consumableComponent(ItemBuilder item, ConfigurationSection section) { @@ -522,6 +492,105 @@ public void consumableComponent(ItemBuilder item, ConfigurationSection section) item.setConsumableComponent(consumable.build()); } + @Override + public void deathProtectionComponent(ItemBuilder item, ConfigurationSection section) { + List effects = parseDeathProtectionEffects(section.getMapList("death_effects")); + item.setDeathProtectionComponent(new DeathProtection(effects)); + } + + private List parseDeathProtectionEffects(List> effectSections) { + List effects = new ArrayList<>(); + + for (Map effectSection : effectSections) { + String type = Optional.ofNullable(effectSection.get("type")) + .map(Object::toString) + .orElse(""); + + switch (type.toLowerCase(Locale.ROOT)) { + case "apply_effects" -> addDeathProtectionStatusEffects(effects, effectSection); + case "remove_effects" -> addDeathProtectionRemoveEffects(effects, effectSection); + case "clear_all_effects" -> effects.add(new ClearAllStatusEffectsConsumeEffect()); + case "teleport_randomly" -> { + float diameter = parseFloatValue(effectSection.get("diameter"), 16f, + "death_protection.teleport_randomly.diameter"); + effects.add(new TeleportRandomlyConsumeEffect(diameter)); + } + case "play_sound" -> { + String soundId = Optional.ofNullable(effectSection.get("sound")) + .map(Object::toString) + .orElse(null); + if (soundId != null) { + SoundEvent soundEvent = getSoundEventFromId(soundId); + if (soundEvent != null) + effects.add(new PlaySoundConsumeEffect(Holder.direct(soundEvent))); + } + } + default -> Logs.logWarning("Invalid death_protection ConsumeEffect-Type " + type); + } + } + + return effects; + } + + private void addDeathProtectionStatusEffects(List effects, Map effectSection) { + if (!(effectSection.get("effects") instanceof Map configuredEffects)) + return; + + float probability = Math.max(0f, Math.min(1f, + parseFloatValue(effectSection.get("probability"), 1f, "death_protection.probability"))); + List statusEffects = new ArrayList<>(); + + for (Map.Entry entry : configuredEffects.entrySet()) { + String effectId = entry.getKey().toString(); + if (!(entry.getValue() instanceof Map rawMap)) { + Logs.logWarning("Invalid death_protection effect data for " + effectId + ": expected map"); + continue; + } + + Map effectData = new HashMap<>(); + for (Map.Entry effectEntry : rawMap.entrySet()) + effectData.put(String.valueOf(effectEntry.getKey()), effectEntry.getValue()); + + getMobEffectOptional(effectId) + .map(BuiltInRegistries.MOB_EFFECT::wrapAsHolder) + .ifPresentOrElse(effect -> { + int duration = Math.max(parseIntegerValue(effectData.get("duration"), 1, "duration", effectId), 0) * 20; + int amplifier = Math.max(parseIntegerValue(effectData.get("amplifier"), 0, "amplifier", effectId), 0); + boolean ambient = Optional.ofNullable(effectData.get("ambient")) + .map(value -> Boolean.parseBoolean(value.toString())) + .orElse(false); + boolean particles = Optional.ofNullable(effectData.get("show_particles")) + .map(value -> Boolean.parseBoolean(value.toString())) + .orElse(true); + boolean icon = Optional.ofNullable(effectData.get("show_icon")) + .map(value -> Boolean.parseBoolean(value.toString())) + .orElse(true); + + statusEffects.add(new MobEffectInstance( + effect, duration, amplifier, ambient, particles, icon)); + }, () -> Logs.logWarning("Invalid potion effect in death_protection: " + effectId)); + } + + if (!statusEffects.isEmpty()) + effects.add(new ApplyStatusEffectsConsumeEffect(statusEffects, probability)); + } + + private void addDeathProtectionRemoveEffects(List effects, Map effectSection) { + if (!(effectSection.get("effects") instanceof List effectIds)) + return; + + List> mobEffects = effectIds.stream() + .map(Object::toString) + .map(this::getMobEffectOptional) + .filter(Optional::isPresent) + .map(Optional::get) + .map(BuiltInRegistries.MOB_EFFECT::wrapAsHolder) + .toList(); + + if (!mobEffects.isEmpty()) + effects.add(new RemoveStatusEffectsConsumeEffect(HolderSet.direct(mobEffects))); + } + private void handleApplyEffects(Consumable.Builder consumable, Map effectSection) { if (!(effectSection.get("effects") instanceof Map effects)) return; @@ -711,6 +780,20 @@ public Object consumableComponent(final ItemStack itemStack) { return null; } + @Override + @Nullable + public Object deathProtectionComponent(final ItemStack itemStack) { + if (itemStack == null) + return null; + try { + net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(itemStack); + return nmsItem.get(DataComponents.DEATH_PROTECTION); + } catch (Exception e) { + Logs.debug(e); + } + return null; + } + @Override public ItemStack consumableComponent(final ItemStack itemStack, @Nullable Object consumable) { if (consumable == null) @@ -725,6 +808,20 @@ public ItemStack consumableComponent(final ItemStack itemStack, @Nullable Object return itemStack; } + @Override + public ItemStack deathProtectionComponent(final ItemStack itemStack, @Nullable Object deathProtection) { + if (!(deathProtection instanceof DeathProtection component)) + return itemStack; + try { + net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(itemStack); + nmsItem.set(DataComponents.DEATH_PROTECTION, component); + return asBukkitCopy(nmsItem); + } catch (Exception e) { + Logs.debug(e); + } + return itemStack; + } + @Override public boolean supportsJukeboxPlaying() { return true; @@ -743,23 +840,8 @@ public void playJukeBoxSong(Location location, ItemStack itemStack) { new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), id); } - @Override - public void stopJukeBox(Location location) { - if (location == null || location.getWorld() == null) return; - ServerLevel level = ((CraftWorld) location.getWorld()).getHandle().getLevel(); - level.levelEvent(null, LevelEvent.SOUND_STOP_JUKEBOX_SONG, - new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), 0); - } - // ============ Backpack Cosmetic Packet Methods ============ - private static final AtomicInteger ENTITY_ID_COUNTER = new AtomicInteger(Integer.MAX_VALUE / 2); - - @Override - public int getNextEntityId() { - return ENTITY_ID_COUNTER.decrementAndGet(); - } - private static EntityType getEntityType(String entityId) { try { Object location = ResourceLocationHelper.parse(entityId); @@ -837,27 +919,6 @@ public void spawnBackpackArmorStand(Player viewer, int entityId, Location locati } } - @Override - public void sendEntityTeleport(Player viewer, int entityId, Location location) { - ServerPlayer serverPlayer = ((CraftPlayer) viewer).getHandle(); - Connection connection = serverPlayer.connection.connection; - - // Create position/rotation data - PositionMoveRotation positionData = new PositionMoveRotation( - new Vec3(location.getX(), location.getY(), location.getZ()), - Vec3.ZERO, // delta movement - location.getYaw(), - location.getPitch() - ); - - ClientboundEntityPositionSyncPacket teleportPacket = new ClientboundEntityPositionSyncPacket( - entityId, - positionData, - false // on ground - ); - connection.send(teleportPacket); - } - @Override public void sendEntityHeadRotation(Player viewer, int entityId, float yaw) { ServerPlayer serverPlayer = ((CraftPlayer) viewer).getHandle(); diff --git a/nms/java25/src/main/java/io/th0rgal/oraxen/nms/handler/java25/NMSHandler.java b/nms/java25/src/main/java/io/th0rgal/oraxen/nms/handler/java25/NMSHandler.java index 6bc4804d11..6f99428c40 100644 --- a/nms/java25/src/main/java/io/th0rgal/oraxen/nms/handler/java25/NMSHandler.java +++ b/nms/java25/src/main/java/io/th0rgal/oraxen/nms/handler/java25/NMSHandler.java @@ -33,7 +33,6 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.PositionMoveRotation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; @@ -49,6 +48,7 @@ import net.minecraft.world.item.JukeboxSong; import net.minecraft.world.item.component.Consumable; import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.DeathProtection; import net.minecraft.world.item.consume_effects.*; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.context.DirectionalPlaceContext; @@ -71,10 +71,9 @@ import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.entity.Player; +import org.bukkit.event.Listener; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.components.FoodComponent; -import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; @@ -83,7 +82,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; public class NMSHandler implements io.th0rgal.oraxen.nms.NMSHandler { @@ -227,11 +225,6 @@ public BlockHitResult getPlayerPOVHitResult(Level world, net.minecraft.world.ent return world.clip(new ClipContext(vec3, vec32, ClipContext.Block.OUTLINE, fluidHandling, player)); } - @Override - public void customBlockDefaultTools(Player player) { - - } - private TagNetworkSerialization.NetworkPayload createPayload() { Constructor constructor = Arrays .stream(TagNetworkSerialization.NetworkPayload.class.getDeclaredConstructors()).findFirst() @@ -265,11 +258,6 @@ private Map createTagRegistryMap() { }).collect(HashMap::new, Map::putAll, Map::putAll); } - @Override - public boolean getSupported() { - return true; - } - /** * Sets a component on an item using the DataComponents registry * @@ -426,24 +414,6 @@ private void handleListValue(net.minecraft.nbt.CompoundTag nbt, String key, List } } - @SuppressWarnings("UnstableApiUsage") - @Override - public void foodComponent(ItemBuilder item, ConfigurationSection foodSection) { - FoodComponent foodComponent = new ItemStack(item.getType()).getItemMeta().getFood(); - - // Ensure nutrition is non-negative - int nutrition = Math.max(foodSection.getInt("nutrition"), 0); - foodComponent.setNutrition(nutrition); - - // Ensure saturation is non-negative - float saturation = Math.max((float) foodSection.getDouble("saturation", 0.0), 0f); - foodComponent.setSaturation(saturation); - - foodComponent.setCanAlwaysEat(foodSection.getBoolean("can_always_eat", false)); - - item.setFoodComponent(foodComponent); - } - @SuppressWarnings("UnstableApiUsage") @Override public void consumableComponent(ItemBuilder item, ConfigurationSection section) { @@ -499,6 +469,105 @@ public void consumableComponent(ItemBuilder item, ConfigurationSection section) item.setConsumableComponent(consumable.build()); } + @Override + public void deathProtectionComponent(ItemBuilder item, ConfigurationSection section) { + List effects = parseDeathProtectionEffects(section.getMapList("death_effects")); + item.setDeathProtectionComponent(new DeathProtection(effects)); + } + + private List parseDeathProtectionEffects(List> effectSections) { + List effects = new ArrayList<>(); + + for (Map effectSection : effectSections) { + String type = Optional.ofNullable(effectSection.get("type")) + .map(Object::toString) + .orElse(""); + + switch (type.toLowerCase(Locale.ROOT)) { + case "apply_effects" -> addDeathProtectionStatusEffects(effects, effectSection); + case "remove_effects" -> addDeathProtectionRemoveEffects(effects, effectSection); + case "clear_all_effects" -> effects.add(new ClearAllStatusEffectsConsumeEffect()); + case "teleport_randomly" -> { + float diameter = parseFloatValue(effectSection.get("diameter"), 16f, + "death_protection.teleport_randomly.diameter"); + effects.add(new TeleportRandomlyConsumeEffect(diameter)); + } + case "play_sound" -> { + String soundId = Optional.ofNullable(effectSection.get("sound")) + .map(Object::toString) + .orElse(null); + if (soundId != null) { + SoundEvent soundEvent = getSoundEventFromId(soundId); + if (soundEvent != null) + effects.add(new PlaySoundConsumeEffect(Holder.direct(soundEvent))); + } + } + default -> Logs.logWarning("Invalid death_protection ConsumeEffect-Type " + type); + } + } + + return effects; + } + + private void addDeathProtectionStatusEffects(List effects, Map effectSection) { + if (!(effectSection.get("effects") instanceof Map configuredEffects)) + return; + + float probability = Math.max(0f, Math.min(1f, + parseFloatValue(effectSection.get("probability"), 1f, "death_protection.probability"))); + List statusEffects = new ArrayList<>(); + + for (Map.Entry entry : configuredEffects.entrySet()) { + String effectId = entry.getKey().toString(); + if (!(entry.getValue() instanceof Map rawMap)) { + Logs.logWarning("Invalid death_protection effect data for " + effectId + ": expected map"); + continue; + } + + Map effectData = new HashMap<>(); + for (Map.Entry effectEntry : rawMap.entrySet()) + effectData.put(String.valueOf(effectEntry.getKey()), effectEntry.getValue()); + + getMobEffectOptional(effectId) + .map(BuiltInRegistries.MOB_EFFECT::wrapAsHolder) + .ifPresentOrElse(effect -> { + int duration = Math.max(parseIntegerValue(effectData.get("duration"), 1, "duration", effectId), 0) * 20; + int amplifier = Math.max(parseIntegerValue(effectData.get("amplifier"), 0, "amplifier", effectId), 0); + boolean ambient = Optional.ofNullable(effectData.get("ambient")) + .map(value -> Boolean.parseBoolean(value.toString())) + .orElse(false); + boolean particles = Optional.ofNullable(effectData.get("show_particles")) + .map(value -> Boolean.parseBoolean(value.toString())) + .orElse(true); + boolean icon = Optional.ofNullable(effectData.get("show_icon")) + .map(value -> Boolean.parseBoolean(value.toString())) + .orElse(true); + + statusEffects.add(new MobEffectInstance( + effect, duration, amplifier, ambient, particles, icon)); + }, () -> Logs.logWarning("Invalid potion effect in death_protection: " + effectId)); + } + + if (!statusEffects.isEmpty()) + effects.add(new ApplyStatusEffectsConsumeEffect(statusEffects, probability)); + } + + private void addDeathProtectionRemoveEffects(List effects, Map effectSection) { + if (!(effectSection.get("effects") instanceof List effectIds)) + return; + + List> mobEffects = effectIds.stream() + .map(Object::toString) + .map(this::getMobEffectOptional) + .filter(Optional::isPresent) + .map(Optional::get) + .map(BuiltInRegistries.MOB_EFFECT::wrapAsHolder) + .toList(); + + if (!mobEffects.isEmpty()) + effects.add(new RemoveStatusEffectsConsumeEffect(HolderSet.direct(mobEffects))); + } + private void handleApplyEffects(Consumable.Builder consumable, Map effectSection) { if (!(effectSection.get("effects") instanceof Map effects)) return; @@ -688,6 +757,20 @@ public Object consumableComponent(final ItemStack itemStack) { return null; } + @Override + @Nullable + public Object deathProtectionComponent(final ItemStack itemStack) { + if (itemStack == null) + return null; + try { + net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(itemStack); + return nmsItem.get(DataComponents.DEATH_PROTECTION); + } catch (Exception e) { + Logs.debug(e); + } + return null; + } + @Override public ItemStack consumableComponent(final ItemStack itemStack, @Nullable Object consumable) { if (consumable == null) @@ -702,6 +785,20 @@ public ItemStack consumableComponent(final ItemStack itemStack, @Nullable Object return itemStack; } + @Override + public ItemStack deathProtectionComponent(final ItemStack itemStack, @Nullable Object deathProtection) { + if (!(deathProtection instanceof DeathProtection component)) + return itemStack; + try { + net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(itemStack); + nmsItem.set(DataComponents.DEATH_PROTECTION, component); + return asBukkitCopy(nmsItem); + } catch (Exception e) { + Logs.debug(e); + } + return itemStack; + } + @Override public boolean supportsJukeboxPlaying() { return true; @@ -720,23 +817,8 @@ public void playJukeBoxSong(Location location, ItemStack itemStack) { new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), id); } - @Override - public void stopJukeBox(Location location) { - if (location == null || location.getWorld() == null) return; - ServerLevel level = ((CraftWorld) location.getWorld()).getHandle().getLevel(); - level.levelEvent(null, LevelEvent.SOUND_STOP_JUKEBOX_SONG, - new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), 0); - } - // ============ Backpack Cosmetic Packet Methods ============ - private static final AtomicInteger ENTITY_ID_COUNTER = new AtomicInteger(Integer.MAX_VALUE / 2); - - @Override - public int getNextEntityId() { - return ENTITY_ID_COUNTER.decrementAndGet(); - } - private static EntityType getEntityType(String entityId) { try { Object location = ResourceLocationHelper.parse(entityId); @@ -814,27 +896,6 @@ public void spawnBackpackArmorStand(Player viewer, int entityId, Location locati } } - @Override - public void sendEntityTeleport(Player viewer, int entityId, Location location) { - ServerPlayer serverPlayer = ((CraftPlayer) viewer).getHandle(); - Connection connection = serverPlayer.connection.connection; - - // Create position/rotation data - PositionMoveRotation positionData = new PositionMoveRotation( - new Vec3(location.getX(), location.getY(), location.getZ()), - Vec3.ZERO, // delta movement - location.getYaw(), - location.getPitch() - ); - - ClientboundEntityPositionSyncPacket teleportPacket = new ClientboundEntityPositionSyncPacket( - entityId, - positionData, - false // on ground - ); - connection.send(teleportPacket); - } - @Override public void sendEntityHeadRotation(Player viewer, int entityId, float yaw) { ServerPlayer serverPlayer = ((CraftPlayer) viewer).getHandle(); diff --git a/src/main/java/io/th0rgal/oraxen/OraxenPlugin.java b/src/main/java/io/th0rgal/oraxen/OraxenPlugin.java index bdc4eaeebe..5f975da463 100644 --- a/src/main/java/io/th0rgal/oraxen/OraxenPlugin.java +++ b/src/main/java/io/th0rgal/oraxen/OraxenPlugin.java @@ -19,6 +19,7 @@ import io.th0rgal.oraxen.hud.HudManager; import io.th0rgal.oraxen.items.ItemUpdater; import io.th0rgal.oraxen.mechanics.MechanicsManager; +import io.th0rgal.oraxen.mechanics.provided.gameplay.CustomBlockPickItemListener; import io.th0rgal.oraxen.mechanics.provided.gameplay.furniture.FurnitureFactory; import io.th0rgal.oraxen.nms.NMSHandlers; import io.th0rgal.oraxen.pack.dispatch.PackLoadingManager; @@ -134,6 +135,9 @@ public void onEnable() { if (CustomBlockMiningListener.isSupported()) { Bukkit.getPluginManager().registerEvents(new CustomBlockMiningListener(), this); } + if (VersionUtil.atOrAbove("1.21.5")) { + Bukkit.getPluginManager().registerEvents(new CustomBlockPickItemListener(), this); + } NMSHandlers.setup(); // Auto-update Paper config for block updates (noteblock, tripwire, chorus) diff --git a/src/main/java/io/th0rgal/oraxen/commands/RecipesCommand.java b/src/main/java/io/th0rgal/oraxen/commands/RecipesCommand.java index eab54d0d2f..f3369e9ffa 100644 --- a/src/main/java/io/th0rgal/oraxen/commands/RecipesCommand.java +++ b/src/main/java/io/th0rgal/oraxen/commands/RecipesCommand.java @@ -58,6 +58,9 @@ private OraxenCommand getBuilderCommand() { .withSubcommand(getCampfireBuilderCommand()) .withSubcommand(getSmokingBuilderCommand()) .withSubcommand(getStonecuttingBuilderCommand()) + .withSubcommand(getSmithingBuilderCommand()) + .withSubcommand(getAnvilBuilderCommand()) + .withSubcommand(getGrindstoneBuilderCommand()) .executes((sender, args) -> { if (sender instanceof Player player) { final RecipeBuilder recipe = RecipeBuilder.get(player.getUniqueId()); @@ -182,6 +185,50 @@ private OraxenCommand getStonecuttingBuilderCommand() { }); } + private OraxenCommand getSmithingBuilderCommand() { + return new OraxenCommand("smithing") + .withPermission("oraxen.command.recipes.builder") + .executes((sender, args) -> { + if (sender instanceof Player player) { + final RecipeBuilder recipe = RecipeBuilder.get(player.getUniqueId()); + (recipe != null ? recipe : new SmithingBuilder(player)).open(); + } else + Message.NOT_PLAYER.send(sender); + }); + } + + private OraxenCommand getAnvilBuilderCommand() { + return new OraxenCommand("anvil") + .withPermission("oraxen.command.recipes.builder") + .withArguments(new IntegerArgument("experience_cost")) + .executes((sender, args) -> { + if (sender instanceof Player player) { + RecipeBuilder recipe = RecipeBuilder.get(player.getUniqueId()); + recipe = recipe != null ? recipe : new AnvilBuilder(player); + if (recipe instanceof AnvilBuilder anvil) + anvil.setExperienceCost((Integer) args.get("experience_cost")); + recipe.open(); + } else + Message.NOT_PLAYER.send(sender); + }); + } + + private OraxenCommand getGrindstoneBuilderCommand() { + return new OraxenCommand("grindstone") + .withPermission("oraxen.command.recipes.builder") + .withArguments(new IntegerArgument("experience")) + .executes((sender, args) -> { + if (sender instanceof Player player) { + RecipeBuilder recipe = RecipeBuilder.get(player.getUniqueId()); + recipe = recipe != null ? recipe : new GrindstoneBuilder(player); + if (recipe instanceof GrindstoneBuilder grindstone) + grindstone.setExperience((Integer) args.get("experience")); + recipe.open(); + } else + Message.NOT_PLAYER.send(sender); + }); + } + private OraxenCommand getSaveCommand() { return new OraxenCommand("save") .withPermission("oraxen.command.recipes.builder") diff --git a/src/main/java/io/th0rgal/oraxen/items/ItemBuilder.java b/src/main/java/io/th0rgal/oraxen/items/ItemBuilder.java index bc18865d18..24f7b5ff14 100644 --- a/src/main/java/io/th0rgal/oraxen/items/ItemBuilder.java +++ b/src/main/java/io/th0rgal/oraxen/items/ItemBuilder.java @@ -132,6 +132,8 @@ public class ItemBuilder { // 1.21.2+ properties @Nullable + private Object deathProtectionComponent; + @Nullable private EquippableComponent equippableComponent; @Nullable private Boolean isGlider; @@ -619,6 +621,20 @@ public ItemBuilder setConsumableComponent(@Nullable V consumableComponent) { return this; } + public boolean hasDeathProtectionComponent() { + return VersionUtil.atOrAbove("1.21.2") && deathProtectionComponent != null; + } + + @Nullable + public Object getDeathProtectionComponent() { + return deathProtectionComponent; + } + + public ItemBuilder setDeathProtectionComponent(@Nullable V deathProtectionComponent) { + this.deathProtectionComponent = deathProtectionComponent; + return this; + } + public boolean hasToolComponent() { return VersionUtil.atOrAbove("1.20.5") && toolComponent != null; } @@ -863,6 +879,7 @@ public ItemBuilder clone() { ItemBuilder clonedBuilder = new ItemBuilder(itemStack.clone()); clonedBuilder.genericComponents.putAll(genericComponents); clonedBuilder.paintingVariant = paintingVariant; + clonedBuilder.deathProtectionComponent = deathProtectionComponent; clonedBuilder.attributeEntries.clear(); clonedBuilder.attributeEntries.addAll(attributeEntries); if (legacyAttributeModifiers != null) { @@ -899,6 +916,7 @@ public synchronized ItemBuilder regen() { // Build into a local and publish once so concurrent readers never see // an intermediate stack. ItemStack built = applyConsumableComponent(itemStack); + built = applyDeathProtectionComponent(built); built = applyPaintingVariantComponent(built); built = applyGenericComponents(built); finalItemStack = built; @@ -1098,6 +1116,10 @@ private ItemStack applyConsumableComponent(ItemStack itemStack) { return NMSHandlers.getHandler().consumableComponent(itemStack, consumableComponent); } + private ItemStack applyDeathProtectionComponent(ItemStack itemStack) { + return NMSHandlers.getHandler().deathProtectionComponent(itemStack, deathProtectionComponent); + } + private ItemStack applyGenericComponents(ItemStack itemStack) { if (genericComponents.isEmpty()) return itemStack; return NMSHandlers.getHandler().applyGenericComponents(itemStack, genericComponents); diff --git a/src/main/java/io/th0rgal/oraxen/items/ItemComponents.java b/src/main/java/io/th0rgal/oraxen/items/ItemComponents.java index 1194327b06..1fc36ffff8 100644 --- a/src/main/java/io/th0rgal/oraxen/items/ItemComponents.java +++ b/src/main/java/io/th0rgal/oraxen/items/ItemComponents.java @@ -3,7 +3,6 @@ import io.th0rgal.oraxen.api.OraxenItems; import io.th0rgal.oraxen.compatibilities.provided.ecoitems.WrappedEcoItem; import io.th0rgal.oraxen.compatibilities.provided.mythiccrucible.WrappedCrucibleItem; -import io.th0rgal.oraxen.nms.NMSHandler; import io.th0rgal.oraxen.nms.NMSHandlers; import io.th0rgal.oraxen.utils.AdventureUtils; import io.th0rgal.oraxen.utils.OraxenYaml; @@ -20,6 +19,7 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.components.EquippableComponent; +import org.bukkit.inventory.meta.components.FoodComponent; import org.bukkit.inventory.meta.components.JukeboxPlayableComponent; import org.bukkit.inventory.meta.components.ToolComponent; import org.bukkit.inventory.meta.components.UseCooldownComponent; @@ -104,9 +104,8 @@ private void handleLegacyComponents(final ItemBuilder item, final ConfigurationS if (OraxenYaml.contains(components, "enchantment_glint_override")) item.setEnchantmentGlintOverride(OraxenYaml.getBoolean(components, "enchantment_glint_override")); - final NMSHandler nmsHandler = NMSHandlers.getHandler(); Optional.ofNullable(OraxenYaml.getConfigurationSection(components, "food")) - .ifPresent(food -> nmsHandler.foodComponent(item, food)); + .ifPresent(food -> parseFoodComponent(item, food)); Optional.ofNullable(OraxenYaml.getConfigurationSection(components, "tool")) .ifPresent(toolSection -> parseToolComponent(item, toolSection)); @@ -149,6 +148,10 @@ private void handleLegacyComponents(final ItemBuilder item, final ConfigurationS if (!VersionUtil.atOrAbove("1.21.2")) return; + + Optional.ofNullable(OraxenYaml.getConfigurationSection(components, "death_protection")) + .ifPresent(deathProtection -> NMSHandlers.getHandler().deathProtectionComponent(item, deathProtection)); + Optional.ofNullable(OraxenYaml.getConfigurationSection(components, "equippable")) .ifPresent(equippable -> parseEquippableComponent(item, equippable)); @@ -179,7 +182,16 @@ private void handleLegacyComponents(final ItemBuilder item, final ConfigurationS .ifPresent(item::setItemModel); Optional.ofNullable(OraxenYaml.getConfigurationSection(components, "consumable")) - .ifPresent(consumableSection -> nmsHandler.consumableComponent(item, consumableSection)); + .ifPresent(consumableSection -> NMSHandlers.getHandler().consumableComponent(item, consumableSection)); + } + + @SuppressWarnings("UnstableApiUsage") + private void parseFoodComponent(final ItemBuilder item, final ConfigurationSection foodSection) { + final FoodComponent foodComponent = new ItemStack(item.getType()).getItemMeta().getFood(); + foodComponent.setNutrition(Math.max(foodSection.getInt("nutrition"), 0)); + foodComponent.setSaturation(Math.max((float) foodSection.getDouble("saturation", 0.0), 0f)); + foodComponent.setCanAlwaysEat(foodSection.getBoolean("can_always_eat", false)); + item.setFoodComponent(foodComponent); } private boolean isLegacyComponent(final String key) { @@ -193,6 +205,7 @@ private boolean isLegacyComponent(final String key) { normalizedKey.equals("tool") || normalizedKey.equals("painting_variant") || normalizedKey.equals("jukebox_playable") || + normalizedKey.equals("death_protection") || normalizedKey.equals("equippable") || normalizedKey.equals("use_cooldown") || normalizedKey.equals("use_remainder") || diff --git a/src/main/java/io/th0rgal/oraxen/items/ItemUpdater.java b/src/main/java/io/th0rgal/oraxen/items/ItemUpdater.java index 14a24888a6..50a75048f8 100644 --- a/src/main/java/io/th0rgal/oraxen/items/ItemUpdater.java +++ b/src/main/java/io/th0rgal/oraxen/items/ItemUpdater.java @@ -527,6 +527,10 @@ public static ItemStack updateItem(ItemStack oldItem) { ItemStack newItem = nmsHandler.copyItemNBTTags(oldItem, newItemBuilder.build()); newItem.setAmount(oldItem.getAmount()); + Object deathProtectionComponent = VersionUtil.atOrAbove("1.21.2") + ? Optional.ofNullable(nmsHandler.deathProtectionComponent(newItem)) + .orElseGet(() -> nmsHandler.deathProtectionComponent(oldItem)) + : null; ItemUtils.editItemMeta(newItem, itemMeta -> { ItemMeta oldMeta = oldItem.getItemMeta(); @@ -681,7 +685,9 @@ public static ItemStack updateItem(ItemStack oldItem) { nmsHandler.consumableComponent(newItem, Optional.ofNullable(nmsHandler.consumableComponent(newItem)) .orElse(nmsHandler.consumableComponent(oldItem))); - return newItem; + return deathProtectionComponent != null + ? nmsHandler.deathProtectionComponent(newItem, deathProtectionComponent) + : newItem; } } diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticFactory.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticFactory.java index 8c2c06d46d..20c578a0e6 100644 --- a/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticFactory.java +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticFactory.java @@ -18,6 +18,11 @@ public class BackpackCosmeticFactory extends MechanicFactory { private static BackpackCosmeticFactory instance; + private static final String armorStandEnabledKey = "armor_stand_enabled"; + private static final String armorStandRangeKey = "armor_stand_range"; + private final BackpackCosmeticListener listener; + private final boolean armorStandEnabled; + private final int armorStandRange; @ConfigProperty(type = PropertyType.STRING, description = "Equipment slot that triggers backpack display", defaultValue = "CHEST") public static final String PROP_SLOT = "slot"; @@ -49,22 +54,24 @@ public class BackpackCosmeticFactory extends MechanicFactory { public BackpackCosmeticFactory(ConfigurationSection section) { super(section); instance = this; + armorStandEnabled = section.getBoolean(armorStandEnabledKey, true); + armorStandRange = Math.max(1, section.getInt(armorStandRangeKey, 128)); - BackpackCosmeticListener listener = new BackpackCosmeticListener(this); + listener = new BackpackCosmeticListener(this); MechanicsManager.registerListeners(OraxenPlugin.get(), getMechanicID(), listener, listener.createMountListener(OraxenPlugin.get())); - // Register tasks with MechanicsManager for proper cleanup on reload BackpackCosmeticManager manager = BackpackCosmeticManager.getInstance(); - // Fast position update task (every tick = 50ms) for smooth backpack following SchedulerUtil.ScheduledTask positionTask = SchedulerUtil.runTaskTimer(1L, 1L, manager::updateAllBackpackPositions); MechanicsManager.registerTask(getMechanicID(), positionTask); - // Viewer refresh task (every 20 ticks = 1 second) for adding/removing viewers SchedulerUtil.ScheduledTask refreshTask = SchedulerUtil.runTaskTimer(20L, 20L, manager::refreshAllViewers); MechanicsManager.registerTask(getMechanicID(), refreshTask); + SchedulerUtil.ScheduledTask armorStandTask = SchedulerUtil.runTaskLater(1L, listener::startExistingArmorStandViewerTasks); + MechanicsManager.registerTask(getMechanicID(), armorStandTask); + if (Settings.DEBUG.toBool()) { io.th0rgal.oraxen.utils.logs.Logs.logSuccess("BackpackCosmeticFactory initialized"); } @@ -74,6 +81,19 @@ public static BackpackCosmeticFactory getInstance() { return instance; } + public boolean isArmorStandEnabled() { + return armorStandEnabled; + } + + public int getArmorStandRange() { + return armorStandRange; + } + + @Override + public void onUnregister() { + listener.cleanupArmorStandDisplays(); + } + @Override public Mechanic parse(ConfigurationSection section) { BackpackCosmeticMechanic mechanic = new BackpackCosmeticMechanic(this, section); diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticListener.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticListener.java index 4a5abe0c21..37c3b56775 100644 --- a/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticListener.java +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticListener.java @@ -2,15 +2,20 @@ import io.th0rgal.oraxen.api.OraxenItems; import io.th0rgal.oraxen.mechanics.Mechanic; +import io.th0rgal.oraxen.mechanics.MechanicsManager; import io.th0rgal.oraxen.utils.SchedulerUtil; import io.th0rgal.oraxen.utils.VersionUtil; import org.bukkit.Bukkit; import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.entity.EntityPickupItemEvent; import org.bukkit.event.entity.EntityToggleGlideEvent; import org.bukkit.event.entity.PlayerDeathEvent; @@ -22,6 +27,7 @@ import org.bukkit.plugin.java.JavaPlugin; import java.lang.reflect.Method; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -37,6 +43,8 @@ public class BackpackCosmeticListener implements Listener { private final BackpackCosmeticManager manager; private final Set hiddenForMovement = ConcurrentHashMap.newKeySet(); private final Map hiddenMovementMechanics = new ConcurrentHashMap<>(); + private final Map armorStandDisplays = new ConcurrentHashMap<>(); + private final Map armorStandViewerTasks = new ConcurrentHashMap<>(); // Movement thresholds to reduce unnecessary updates // Without mount packets, we need more frequent updates for smooth following @@ -53,14 +61,29 @@ public BackpackCosmeticListener(BackpackCosmeticFactory factory) { public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - // Check if player has backpack item equipped - SchedulerUtil.runForEntityLater(player, 5L, () -> checkAndUpdateBackpack(player)); + SchedulerUtil.runForEntityLater(player, 5L, () -> { + checkAndUpdateBackpack(player); + startArmorStandViewerTask(player); + refreshArmorStandDisplays(player); + }); } @EventHandler(priority = EventPriority.MONITOR) public void onPlayerQuit(PlayerQuitEvent event) { - clearMovementHidden(event.getPlayer().getUniqueId()); - manager.hideBackpack(event.getPlayer()); + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + clearMovementHidden(playerId); + manager.hideBackpack(player); + + SchedulerUtil.ScheduledTask task = armorStandViewerTasks.remove(playerId); + if (task != null) task.cancel(); + for (Map.Entry entry : armorStandDisplays.entrySet()) { + BackpackCosmeticManager.BackpackData data = entry.getValue(); + data.getViewers().remove(playerId); + if (data.getViewers().isEmpty()) { + armorStandDisplays.remove(entry.getKey(), data); + } + } } @EventHandler(priority = EventPriority.MONITOR) @@ -137,6 +160,23 @@ public void onEntityToggleGlide(EntityToggleGlideEvent event) { SchedulerUtil.runForEntityLater(player, 1L, () -> checkAndUpdateBackpack(player)); } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onArmorStandManipulate(PlayerArmorStandManipulateEvent event) { + ArmorStand stand = event.getRightClicked(); + Player player = event.getPlayer(); + if (!factory.isArmorStandEnabled() || event.getSlot() != EquipmentSlot.CHEST) return; + + SchedulerUtil.runForEntityLater(stand, 1L, () -> checkArmorStandDisplay(stand)); + SchedulerUtil.runForEntityLater(player, 2L, () -> refreshArmorStandDisplays(player)); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onArmorStandDeath(EntityDeathEvent event) { + if (event.getEntity() instanceof ArmorStand stand) { + removeArmorStandDisplay(stand.getUniqueId()); + } + } + /** * Creates the mount/dismount listener for backpack resyncs. * The mount events moved from org.spigotmc to org.bukkit.event.entity in 1.20.5; @@ -162,11 +202,13 @@ public class MountListener implements Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onEntityMount(org.bukkit.event.entity.EntityMountEvent event) { handleMountChange(event.getEntity()); + handleArmorStandMountChange(event.getMount()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onEntityDismount(org.bukkit.event.entity.EntityDismountEvent event) { handleMountChange(event.getEntity()); + handleArmorStandMountChange(event.getDismounted()); } } @@ -176,11 +218,22 @@ public void onEntityDismount(org.bukkit.event.entity.EntityDismountEvent event) private void registerLegacyMountEvent(JavaPlugin plugin, Listener listener, String eventClassName) { try { Class eventClass = Class.forName(eventClassName).asSubclass(Event.class); - Method getter = eventClass.getMethod("getEntity"); + Method entityGetter = eventClass.getMethod("getEntity"); + String vehicleGetterName = eventClassName.endsWith("EntityMountEvent") ? "getMount" : "getDismounted"; + Method vehicleGetter; + try { + vehicleGetter = eventClass.getMethod(vehicleGetterName); + } catch (NoSuchMethodException ignored) { + vehicleGetter = null; + } + Method finalVehicleGetter = vehicleGetter; Bukkit.getPluginManager().registerEvent(eventClass, listener, EventPriority.MONITOR, (l, event) -> { if (!eventClass.isInstance(event)) return; try { - if (getter.invoke(event) instanceof org.bukkit.entity.Entity entity) handleMountChange(entity); + if (entityGetter.invoke(event) instanceof Entity entity) handleMountChange(entity); + if (finalVehicleGetter != null && finalVehicleGetter.invoke(event) instanceof Entity vehicle) { + handleArmorStandMountChange(vehicle); + } } catch (ReflectiveOperationException ignored) { } }, plugin, true); @@ -189,15 +242,39 @@ private void registerLegacyMountEvent(JavaPlugin plugin, Listener listener, Stri } } - private void handleMountChange(org.bukkit.entity.Entity mounted) { + private void handleMountChange(Entity mounted) { if (!(mounted instanceof Player player)) return; if (!manager.hasBackpack(player)) return; scheduleBackpackMountResync(player); } - // Schedules two resyncs because mount/dismount packets can arrive out of order with the - // passenger-list updates the client uses; the second pass is a safety net for that race. + private void handleArmorStandMountChange(Entity vehicle) { + if (!factory.isArmorStandEnabled() || !(vehicle instanceof ArmorStand stand)) return; + + BackpackCosmeticManager.BackpackData data = armorStandDisplays.get(stand.getUniqueId()); + if (data == null) return; + + SchedulerUtil.runForEntity(stand, () -> { + if (armorStandDisplays.get(stand.getUniqueId()) != data || !stand.isValid()) return; + + int[] passengerIds = manager.getMergedPassengerIds(stand, data.getEntityId()); + int vehicleId = stand.getEntityId(); + float yaw = stand.getYaw(); + for (UUID viewerId : data.getViewers()) { + Player viewer = Bukkit.getPlayer(viewerId); + if (viewer == null) continue; + + SchedulerUtil.runForEntity(viewer, () -> { + if (viewer.isOnline() && data.getViewers().contains(viewerId) + && armorStandDisplays.get(stand.getUniqueId()) == data) { + manager.sendBackpackMount(viewer, data, vehicleId, yaw, passengerIds, false); + } + }); + } + }); + } + private void scheduleBackpackMountResync(Player player) { manager.requestResync(player); SchedulerUtil.runForEntityLater(player, 1L, () -> manager.resyncBackpackMount(player)); @@ -434,4 +511,131 @@ private boolean isArmorItem(ItemStack item) { typeName.endsWith("_BOOTS") || typeName.equals("ELYTRA"); } + + void startExistingArmorStandViewerTasks() { + if (!factory.isArmorStandEnabled()) return; + + for (Player player : Bukkit.getOnlinePlayers()) { + startArmorStandViewerTask(player); + } + } + + private void startArmorStandViewerTask(Player viewer) { + if (!factory.isArmorStandEnabled()) return; + + UUID viewerId = viewer.getUniqueId(); + if (armorStandViewerTasks.containsKey(viewerId)) return; + + SchedulerUtil.ScheduledTask task = SchedulerUtil.runForEntityTimer(viewer, 1L, 20L, + () -> refreshArmorStandDisplays(viewer), + () -> armorStandViewerTasks.remove(viewerId)); + if (task == null) return; + + SchedulerUtil.ScheduledTask previous = armorStandViewerTasks.putIfAbsent(viewerId, task); + if (previous == null) { + MechanicsManager.registerTask(factory.getMechanicID(), task); + } else { + task.cancel(); + } + } + + private void refreshArmorStandDisplays(Player viewer) { + if (!factory.isArmorStandEnabled() || !viewer.isOnline()) return; + + int armorStandRange = factory.getArmorStandRange(); + Set nearbyStandIds = new HashSet<>(); + for (Entity entity : viewer.getNearbyEntities( + armorStandRange, armorStandRange, armorStandRange)) { + if (entity instanceof ArmorStand stand) { + nearbyStandIds.add(stand.getUniqueId()); + refreshArmorStandForViewer(viewer, stand); + } + } + + for (Map.Entry entry : armorStandDisplays.entrySet()) { + if (!nearbyStandIds.contains(entry.getKey())) { + removeArmorStandViewer(viewer, entry.getKey(), entry.getValue()); + } + } + } + + private void refreshArmorStandForViewer(Player viewer, ArmorStand stand) { + SchedulerUtil.runForEntity(stand, () -> { + if (!stand.isValid()) { + removeArmorStandDisplay(stand.getUniqueId()); + return; + } + + ItemStack chestItem = stand.getEquipment() == null ? null : stand.getEquipment().getChestplate(); + BackpackCosmeticMechanic mechanic = getBackpackMechanic(chestItem); + if (mechanic == null) { + removeArmorStandDisplay(stand.getUniqueId()); + return; + } + + BackpackCosmeticManager.BackpackData data = ensureArmorStandDisplay(stand, mechanic, chestItem); + Location location = stand.getLocation().clone(); + int vehicleId = stand.getEntityId(); + float yaw = stand.getYaw(); + int[] passengerIds = manager.getMergedPassengerIds(stand, data.getEntityId()); + + SchedulerUtil.runForEntity(viewer, () -> { + if (armorStandDisplays.get(stand.getUniqueId()) != data) return; + manager.updateBackpackViewer(viewer, data, location, vehicleId, yaw, passengerIds); + if (data.getViewers().isEmpty()) armorStandDisplays.remove(stand.getUniqueId(), data); + }); + }, () -> removeArmorStandDisplay(stand.getUniqueId())); + } + + private BackpackCosmeticManager.BackpackData ensureArmorStandDisplay(ArmorStand stand, + BackpackCosmeticMechanic mechanic, + ItemStack displayItem) { + UUID standId = stand.getUniqueId(); + BackpackCosmeticManager.BackpackData data = armorStandDisplays.get(standId); + if (data != null && data.getMechanic() == mechanic && displayItem.isSimilar(data.getDisplayItem())) { + return data; + } + + removeArmorStandDisplay(standId); + data = manager.createBackpackData(mechanic, displayItem.clone()); + armorStandDisplays.put(standId, data); + return data; + } + + private void removeArmorStandViewer(Player viewer, UUID standId, BackpackCosmeticManager.BackpackData data) { + manager.removeBackpackViewer(viewer, data); + if (data.getViewers().isEmpty()) armorStandDisplays.remove(standId, data); + } + + private void checkArmorStandDisplay(ArmorStand stand) { + if (!stand.isValid()) { + removeArmorStandDisplay(stand.getUniqueId()); + return; + } + + ItemStack chestItem = stand.getEquipment() == null ? null : stand.getEquipment().getChestplate(); + BackpackCosmeticMechanic mechanic = getBackpackMechanic(chestItem); + if (mechanic == null) { + removeArmorStandDisplay(stand.getUniqueId()); + return; + } + + ensureArmorStandDisplay(stand, mechanic, chestItem); + } + + private void removeArmorStandDisplay(UUID standId) { + BackpackCosmeticManager.BackpackData data = armorStandDisplays.remove(standId); + if (data == null) return; + manager.scheduleBackpackDestroyForViewers(data); + } + + void cleanupArmorStandDisplays() { + for (SchedulerUtil.ScheduledTask task : armorStandViewerTasks.values()) { + task.cancel(); + } + armorStandViewerTasks.clear(); + + armorStandDisplays.values().forEach(manager::scheduleBackpackDestroyForViewers); + armorStandDisplays.clear(); + } } diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticManager.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticManager.java index 1fee41fdaf..529fb8aa1c 100644 --- a/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticManager.java +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/cosmetic/backpack/BackpackCosmeticManager.java @@ -42,10 +42,7 @@ public void showBackpack(Player player, BackpackCosmeticMechanic mechanic, ItemS // Remove existing backpack first hideBackpack(player); - // Generate a unique entity ID for the armor stand - int entityId = NMSHandlers.getHandler().getNextEntityId(); - - BackpackData data = new BackpackData(entityId, mechanic, displayItem); + BackpackData data = createBackpackData(mechanic, displayItem); activeBackpacks.put(playerId, data); // Spawn the backpack for all nearby players @@ -294,7 +291,11 @@ private void sendBackpackMountPacket(Player viewer, Player owner, int[] passenge NMSHandlers.getHandler().sendMountPacket(viewer, owner.getEntityId(), passengerIds); } - private int[] getMergedPassengerIds(Player owner, int backpackEntityId) { + BackpackData createBackpackData(BackpackCosmeticMechanic mechanic, ItemStack displayItem) { + return new BackpackData(Bukkit.getUnsafe().nextEntityId(), mechanic, displayItem); + } + + int[] getMergedPassengerIds(Entity owner, int backpackEntityId) { Set passengerIds = new LinkedHashSet<>(); for (Entity passenger : owner.getPassengers()) { passengerIds.add(passenger.getEntityId()); @@ -304,6 +305,42 @@ private int[] getMergedPassengerIds(Player owner, int backpackEntityId) { return passengerIds.stream().mapToInt(Integer::intValue).toArray(); } + void updateBackpackViewer(Player viewer, BackpackData data, Location location, + int vehicleId, float yaw, int[] passengerIds) { + if (!viewer.isOnline() || !isWithinViewDistance(viewer, location, data.getMechanic().getViewDistance())) { + removeBackpackViewer(viewer, data); + return; + } + + boolean spawned = data.getViewers().add(viewer.getUniqueId()); + if (spawned) { + NMSHandlers.getHandler().spawnBackpackArmorStand( + viewer, data.getEntityId(), location, data.getDisplayItem(), data.getMechanic().isSmallArmorStand()); + } + + sendBackpackMount(viewer, data, vehicleId, yaw, passengerIds, spawned); + } + + void sendBackpackMount(Player viewer, BackpackData data, int vehicleId, + float yaw, int[] passengerIds, boolean resync) { + NMSHandlers.getHandler().sendMountPacket(viewer, vehicleId, passengerIds); + NMSHandlers.getHandler().sendEntityHeadRotation(viewer, data.getEntityId(), yaw); + + if (resync) { + SchedulerUtil.runForEntityLater(viewer, 1L, () -> { + if (viewer.isOnline() && data.getViewers().contains(viewer.getUniqueId())) { + NMSHandlers.getHandler().sendMountPacket(viewer, vehicleId, passengerIds); + } + }); + } + } + + void removeBackpackViewer(Player viewer, BackpackData data) { + if (data.getViewers().remove(viewer.getUniqueId())) { + NMSHandlers.getHandler().sendEntityDestroy(viewer, data.getEntityId()); + } + } + private void destroyBackpackForViewers(BackpackData data) { for (UUID viewerId : data.getViewers()) { Player viewer = Bukkit.getPlayer(viewerId); @@ -314,9 +351,24 @@ private void destroyBackpackForViewers(BackpackData data) { data.getViewers().clear(); } + void scheduleBackpackDestroyForViewers(BackpackData data) { + for (UUID viewerId : data.getViewers()) { + Player viewer = Bukkit.getPlayer(viewerId); + if (viewer != null && viewer.isOnline()) { + SchedulerUtil.runForEntity(viewer, + () -> NMSHandlers.getHandler().sendEntityDestroy(viewer, data.getEntityId())); + } + } + data.getViewers().clear(); + } + private boolean isWithinViewDistance(Player viewer, Player target, int viewDistance) { + return isWithinViewDistance(viewer, target.getLocation(), viewDistance); + } + + private boolean isWithinViewDistance(Player viewer, Location target, int viewDistance) { if (!viewer.getWorld().equals(target.getWorld())) return false; - return viewer.getLocation().distanceSquared(target.getLocation()) <= viewDistance * viewDistance; + return viewer.getLocation().distanceSquared(target) <= (double) viewDistance * viewDistance; } /** diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/CustomBlockPickItemListener.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/CustomBlockPickItemListener.java new file mode 100644 index 0000000000..8b508e9ba5 --- /dev/null +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/CustomBlockPickItemListener.java @@ -0,0 +1,67 @@ +package io.th0rgal.oraxen.mechanics.provided.gameplay; + +import io.papermc.paper.event.player.PlayerPickBlockEvent; +import io.th0rgal.oraxen.api.OraxenBlocks; +import io.th0rgal.oraxen.api.OraxenItems; +import io.th0rgal.oraxen.items.ItemBuilder; +import io.th0rgal.oraxen.mechanics.provided.gameplay.chorusblock.ChorusBlockMechanic; +import io.th0rgal.oraxen.mechanics.provided.gameplay.noteblock.NoteBlockMechanic; +import io.th0rgal.oraxen.mechanics.provided.gameplay.shaped.ShapedBlockMechanic; +import io.th0rgal.oraxen.mechanics.provided.gameplay.stringblock.StringBlockMechanic; +import io.th0rgal.oraxen.utils.inventories.PickItemUtils; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.inventory.ItemStack; + +public class CustomBlockPickItemListener implements Listener { + + @EventHandler(ignoreCancelled = true) + public void onPickBlock(PlayerPickBlockEvent event) { + ItemBuilder itemBuilder = getPickedItem(event.getBlock()); + if (itemBuilder == null) return; + + ItemStack item = itemBuilder.build(); + if (item == null || item.getType().isAir()) return; + + event.setCancelled(true); + PickItemUtils.pickItem(event.getPlayer(), item); + } + + private ItemBuilder getPickedItem(Block block) { + String itemId = switch (block.getType()) { + case NOTE_BLOCK -> getNoteBlockItemId(block); + case TRIPWIRE -> getStringBlockItemId(block); + case CHORUS_PLANT -> getChorusBlockItemId(block); + default -> getShapedBlockItemId(block); + }; + return itemId == null ? null : OraxenItems.getItemById(itemId); + } + + private String getNoteBlockItemId(Block block) { + NoteBlockMechanic mechanic = OraxenBlocks.getNoteBlockMechanic(block); + if (mechanic == null) return null; + if (mechanic.isDirectional() && !mechanic.getDirectional().isParentBlock()) + return mechanic.getDirectional().getParentBlock(); + return mechanic.getItemID(); + } + + private String getStringBlockItemId(Block block) { + StringBlockMechanic mechanic = OraxenBlocks.getStringMechanic(block); + if (mechanic != null) return mechanic.getItemID(); + + StringBlockMechanic mechanicBelow = OraxenBlocks.getStringMechanic(block.getRelative(BlockFace.DOWN)); + return mechanicBelow != null && mechanicBelow.isTall() ? mechanicBelow.getItemID() : null; + } + + private String getChorusBlockItemId(Block block) { + ChorusBlockMechanic mechanic = OraxenBlocks.getChorusMechanic(block); + return mechanic == null ? null : mechanic.getItemID(); + } + + private String getShapedBlockItemId(Block block) { + ShapedBlockMechanic mechanic = OraxenBlocks.getShapedMechanic(block); + return mechanic == null ? null : mechanic.getItemID(); + } +} diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/BlockLocation.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/BlockLocation.java index 19d66038ee..ed214ad9b7 100644 --- a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/BlockLocation.java +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/BlockLocation.java @@ -5,6 +5,7 @@ import org.bukkit.Utility; import org.bukkit.World; import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.persistence.PersistentDataType; import org.jetbrains.annotations.NotNull; @@ -12,7 +13,12 @@ import java.util.Map; public class BlockLocation implements ConfigurationSerializable { - public static PersistentDataType dataType = new ConfigurationSerializableDataType<>(BlockLocation.class); + + static { + ConfigurationSerialization.registerClass(BlockLocation.class); + } + + public static final PersistentDataType dataType = new ConfigurationSerializableDataType<>(BlockLocation.class); private int x; private int y; diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/FurnitureFactory.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/FurnitureFactory.java index 526e84189f..1e68da974d 100644 --- a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/FurnitureFactory.java +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/FurnitureFactory.java @@ -14,6 +14,7 @@ import io.th0rgal.oraxen.mechanics.provided.gameplay.furniture.text.FurnitureTextPacketBridge; import io.th0rgal.oraxen.mechanics.provided.gameplay.furniture.text.FurnitureTextRegistry; import io.th0rgal.oraxen.utils.SchedulerUtil; +import io.th0rgal.oraxen.utils.VersionUtil; import io.th0rgal.oraxen.utils.blocksounds.BlockSounds; import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; @@ -49,6 +50,8 @@ public FurnitureFactory(ConfigurationSection section) { new JukeboxListener(), new FurnitureTextLoadListener() ); + if (VersionUtil.atOrAbove("1.21.5")) + MechanicsManager.registerListeners(OraxenPlugin.get(), getMechanicID(), new FurniturePickItemListener()); evolvingFurnitures = false; instance = this; FurniturePacketDispatcher.init(); diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/FurniturePickItemListener.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/FurniturePickItemListener.java new file mode 100644 index 0000000000..bb4b3a087b --- /dev/null +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/FurniturePickItemListener.java @@ -0,0 +1,34 @@ +package io.th0rgal.oraxen.mechanics.provided.gameplay.furniture; + +import io.papermc.paper.event.player.PlayerPickBlockEvent; +import io.papermc.paper.event.player.PlayerPickEntityEvent; +import io.papermc.paper.event.player.PlayerPickItemEvent; +import io.th0rgal.oraxen.api.OraxenFurniture; +import io.th0rgal.oraxen.api.OraxenItems; +import io.th0rgal.oraxen.items.ItemBuilder; +import io.th0rgal.oraxen.utils.inventories.PickItemUtils; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +public class FurniturePickItemListener implements Listener { + + @EventHandler(ignoreCancelled = true) + public void onPickBlock(PlayerPickBlockEvent event) { + handlePick(event, OraxenFurniture.getFurnitureMechanic(event.getBlock())); + } + + @EventHandler(ignoreCancelled = true) + public void onPickEntity(PlayerPickEntityEvent event) { + handlePick(event, OraxenFurniture.getFurnitureMechanic(event.getEntity())); + } + + private void handlePick(PlayerPickItemEvent event, FurnitureMechanic mechanic) { + if (mechanic == null) return; + + event.setCancelled(true); + ItemBuilder itemBuilder = OraxenItems.getItemById(mechanic.getItemID()); + if (itemBuilder == null) return; + + PickItemUtils.pickItem(event.getPlayer(), itemBuilder.build()); + } +} diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextEntry.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextEntry.java index fdc99975d6..f767f7f6d8 100644 --- a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextEntry.java +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextEntry.java @@ -1,6 +1,6 @@ package io.th0rgal.oraxen.mechanics.provided.gameplay.furniture.text; -import io.th0rgal.oraxen.nms.NMSHandlers; +import org.bukkit.Bukkit; import org.bukkit.Location; import java.util.Arrays; @@ -8,7 +8,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; /** * Runtime registration entry for a placed furniture base that has one or more @@ -16,8 +15,6 @@ */ public final class FurnitureTextEntry { - private static final AtomicInteger FALLBACK_VIRTUAL_ID = new AtomicInteger(Integer.MAX_VALUE / 2); - private final UUID baseUuid; private final int baseEntityId; private volatile Location baseLocation; @@ -95,8 +92,6 @@ private static int refreshInterval(FurnitureTextDefinition definition) { } private static int nextVirtualEntityId() { - int entityId = NMSHandlers.getHandler().getNextEntityId(); - if (entityId != -1) return entityId; - return FALLBACK_VIRTUAL_ID.decrementAndGet(); + return Bukkit.getUnsafe().nextEntityId(); } } diff --git a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextPacketListener.java b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextPacketListener.java index ccbd93d9cc..4e115c9b5c 100644 --- a/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextPacketListener.java +++ b/src/main/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/text/FurnitureTextPacketListener.java @@ -13,7 +13,6 @@ import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; -import io.th0rgal.oraxen.nms.NMSHandlers; import io.th0rgal.oraxen.utils.VersionUtil; import io.th0rgal.oraxen.utils.SchedulerUtil; import net.kyori.adventure.text.Component; @@ -97,11 +96,6 @@ void sendTextMetadata(FurnitureTextEntry entry, Player viewer, boolean ignoreRan for (int i = 0; i < entry.size(); i++) { FurnitureTextDefinition def = entry.getDefinitions().get(i); Component text = def.renderComponent(viewer); - if (NMSHandlers.getHandler().sendTextDisplayMetadata(viewer, entry.virtualEntityId(i), text, - def.getScale(), billboardByte(def), def.getViewRange(), def.getLineWidth(), - def.getBackgroundArgb(), def.getTextOpacity(), textFlags(def))) { - continue; - } PacketEvents.getAPI().getPlayerManager().sendPacket(viewer, new WrapperPlayServerEntityMetadata(entry.virtualEntityId(i), buildMetadata(def, viewer, text))); } @@ -134,13 +128,6 @@ private void sendTextEntry(FurnitureTextEntry entry, Player viewer, User user, V ); Component text = def.renderComponent(viewer); - org.bukkit.Location textLocation = new org.bukkit.Location(baseLocation.getWorld(), textPos.x, textPos.y, textPos.z, yaw, pitch); - if (viewer != null && NMSHandlers.getHandler().spawnTextDisplay(viewer, virtualId, virtualUuid, textLocation, - text, def.getScale(), billboardByte(def), def.getViewRange(), def.getLineWidth(), - def.getBackgroundArgb(), def.getTextOpacity(), textFlags(def))) { - continue; - } - WrapperPlayServerEntityMetadata textMeta = new WrapperPlayServerEntityMetadata( virtualId, buildMetadata(def, viewer, text) @@ -213,11 +200,6 @@ void refresh(long tick) { FurnitureTextDefinition def = entry.getDefinitions().get(i); if (!entry.shouldRefresh(def, tick)) continue; Component text = def.renderComponent(viewer); - if (NMSHandlers.getHandler().sendTextDisplayMetadata(viewer, entry.virtualEntityId(i), text, - def.getScale(), billboardByte(def), def.getViewRange(), def.getLineWidth(), - def.getBackgroundArgb(), def.getTextOpacity(), textFlags(def))) { - continue; - } PacketEvents.getAPI().getPlayerManager().sendPacket(viewer, new WrapperPlayServerEntityMetadata(entry.virtualEntityId(i), buildMetadata(def, viewer, text))); } diff --git a/src/main/java/io/th0rgal/oraxen/nms/NMSHandler.java b/src/main/java/io/th0rgal/oraxen/nms/NMSHandler.java index 1e9549023c..48370824d2 100644 --- a/src/main/java/io/th0rgal/oraxen/nms/NMSHandler.java +++ b/src/main/java/io/th0rgal/oraxen/nms/NMSHandler.java @@ -1,7 +1,6 @@ package io.th0rgal.oraxen.nms; import io.th0rgal.oraxen.items.ItemBuilder; -import net.kyori.adventure.text.Component; import org.bukkit.Location; import org.bukkit.block.data.BlockData; import org.bukkit.configuration.ConfigurationSection; @@ -11,11 +10,9 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.joml.Vector3f; import java.util.Map; import java.util.Set; -import java.util.UUID; public interface NMSHandler { @@ -53,15 +50,6 @@ default Listener packDispatchListener() { @Nullable BlockData correctBlockStates(Player player, EquipmentSlot slot, ItemStack itemStack); - /** - * Removes mineable/axe tag from noteblocks for custom blocks - */ - void customBlockDefaultTools(Player player); - - default void foodComponent(ItemBuilder itemBuilder, ConfigurationSection foodSection) { - - } - default Object consumableComponent(ItemStack itemStack) { return itemStack; } @@ -74,36 +62,32 @@ default void consumableComponent(ItemBuilder itemBuilder, ConfigurationSection c } + default Object deathProtectionComponent(ItemStack itemStack) { + return null; + } + + default ItemStack deathProtectionComponent(ItemStack itemStack, Object deathProtectionComponent) { + return itemStack; + } + + default void deathProtectionComponent(ItemBuilder itemBuilder, ConfigurationSection deathProtectionSection) { + + } + default boolean supportsJukeboxPlaying() { return false; } default void playJukeBoxSong(Location location, ItemStack itemStack) { } - default void stopJukeBox(Location location) { - } - // Backpack cosmetic packet methods - /** - * Get the next available entity ID for packet-based entities - */ - default int getNextEntityId() { - return -1; - } - /** * Spawn an invisible armor stand for backpack display */ default void spawnBackpackArmorStand(Player viewer, int entityId, Location location, ItemStack displayItem, boolean small) { } - /** - * Send entity teleport packet - */ - default void sendEntityTeleport(Player viewer, int entityId, Location location) { - } - /** * Send entity head rotation packet */ @@ -116,18 +100,6 @@ default void sendEntityHeadRotation(Player viewer, int entityId, float yaw) { default void sendEntityDestroy(Player viewer, int... entityIds) { } - default boolean spawnTextDisplay(Player viewer, int entityId, UUID uuid, Location location, Component text, - Vector3f scale, byte billboard, float viewRange, int lineWidth, - int backgroundArgb, byte textOpacity, byte flags) { - return false; - } - - default boolean sendTextDisplayMetadata(Player viewer, int entityId, Component text, - Vector3f scale, byte billboard, float viewRange, int lineWidth, - int backgroundArgb, byte textOpacity, byte flags) { - return false; - } - /** * Send mount/ride packet (make entity ride another) */ @@ -148,10 +120,6 @@ default void sendMountPacket(Player viewer, int vehicleId, int... passengerIds) "map", "map_scale_direction", "map_to_lock", "Decorations", "SkullOwner", "Effects", "BlockEntityTag", "BlockStateTag"); - default boolean getSupported() { - return false; - } - /** * Sets a component on an item using the DataComponents registry. * The parsed component is stored in ItemBuilder's generic components map @@ -174,10 +142,6 @@ default boolean getSupported() { */ ItemStack applyGenericComponents(ItemStack itemStack, Map components); - default @NotNull ItemStack paintingVariantComponent(@NotNull ItemStack itemStack, @NotNull String paintingVariant) { - return itemStack; - } - class EmptyNMSHandler implements NMSHandler { @Override @@ -207,28 +171,33 @@ public BlockData correctBlockStates(Player player, EquipmentSlot slot, ItemStack } @Override - public void customBlockDefaultTools(Player player) { - + public void consumableComponent(ItemBuilder item, ConfigurationSection section) { } @Override - public void foodComponent(ItemBuilder item, ConfigurationSection foodSection) { + public Object consumableComponent(ItemStack itemStack) { + return null; } @Override - public void consumableComponent(ItemBuilder item, ConfigurationSection section) { + public ItemStack consumableComponent(ItemStack itemStack, Object consumable) { + return itemStack; } @Override - public Object consumableComponent(ItemStack itemStack) { + public Object deathProtectionComponent(ItemStack itemStack) { return null; } @Override - public ItemStack consumableComponent(ItemStack itemStack, Object consumable) { + public ItemStack deathProtectionComponent(ItemStack itemStack, Object deathProtection) { return itemStack; } + @Override + public void deathProtectionComponent(ItemBuilder item, ConfigurationSection section) { + } + @Override public boolean setComponent(ItemBuilder item, String componentKey, Object component) { return false; diff --git a/src/main/java/io/th0rgal/oraxen/nms/NMSHandlers.java b/src/main/java/io/th0rgal/oraxen/nms/NMSHandlers.java index 6cd2069873..927fa6ed06 100644 --- a/src/main/java/io/th0rgal/oraxen/nms/NMSHandlers.java +++ b/src/main/java/io/th0rgal/oraxen/nms/NMSHandlers.java @@ -56,7 +56,6 @@ public static void setup() { Logs.logSuccess("Version " + version + " has been detected."); Logs.logInfo("Oraxen will use " + handlerClass + "."); } - Bukkit.getPluginManager().registerEvents(new NMSListeners(), OraxenPlugin.get()); Listener packDispatchListener = handler.packDispatchListener(); if (packDispatchListener != null) { Bukkit.getPluginManager().registerEvents(packDispatchListener, OraxenPlugin.get()); diff --git a/src/main/java/io/th0rgal/oraxen/nms/NMSListeners.java b/src/main/java/io/th0rgal/oraxen/nms/NMSListeners.java deleted file mode 100644 index 9b24fb5cf9..0000000000 --- a/src/main/java/io/th0rgal/oraxen/nms/NMSListeners.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.th0rgal.oraxen.nms; - -import io.th0rgal.oraxen.mechanics.provided.gameplay.noteblock.NoteBlockMechanicFactory; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; - -public class NMSListeners implements Listener { - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - Player player = event.getPlayer(); - - if (NoteBlockMechanicFactory.isEnabled() && NoteBlockMechanicFactory.getInstance().removeMineableTag()) - NMSHandlers.getHandler().customBlockDefaultTools(player); - } -} diff --git a/src/main/java/io/th0rgal/oraxen/recipes/builders/AnvilBuilder.java b/src/main/java/io/th0rgal/oraxen/recipes/builders/AnvilBuilder.java new file mode 100644 index 0000000000..6636a491ad --- /dev/null +++ b/src/main/java/io/th0rgal/oraxen/recipes/builders/AnvilBuilder.java @@ -0,0 +1,23 @@ +package io.th0rgal.oraxen.recipes.builders; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; + +public class AnvilBuilder extends WorkstationBuilder { + + public AnvilBuilder(Player player) { + super(player, "anvil", "experience_cost"); + } + + @Override + Inventory createInventory(Player player, Component inventoryTitle) { + return Bukkit.createInventory(player, InventoryType.ANVIL, inventoryTitle); + } + + public void setExperienceCost(int experienceCost) { + setValue(experienceCost); + } +} diff --git a/src/main/java/io/th0rgal/oraxen/recipes/builders/GrindstoneBuilder.java b/src/main/java/io/th0rgal/oraxen/recipes/builders/GrindstoneBuilder.java new file mode 100644 index 0000000000..b6b8410cf9 --- /dev/null +++ b/src/main/java/io/th0rgal/oraxen/recipes/builders/GrindstoneBuilder.java @@ -0,0 +1,28 @@ +package io.th0rgal.oraxen.recipes.builders; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; + +public class GrindstoneBuilder extends WorkstationBuilder { + + public GrindstoneBuilder(Player player) { + super(player, "grindstone", "experience"); + } + + @Override + Inventory createInventory(Player player, Component inventoryTitle) { + // Paper opens custom grindstone inventories through a separate native GrindstoneMenu. + // Its visible slots are not backed by the Inventory returned here and vanilla grindstone + // slot predicates would also reject otherwise valid custom recipe ingredients. A hopper + // gives us unrestricted, directly-backed authoring slots; slots 0-2 map to base, + // addition and result, while the builder listener keeps the remaining slots unused. + return Bukkit.createInventory(player, InventoryType.HOPPER, inventoryTitle); + } + + public void setExperience(int experience) { + setValue(experience); + } +} diff --git a/src/main/java/io/th0rgal/oraxen/recipes/builders/RecipeBuilder.java b/src/main/java/io/th0rgal/oraxen/recipes/builders/RecipeBuilder.java index c5f9a54b02..36ee1b4736 100644 --- a/src/main/java/io/th0rgal/oraxen/recipes/builders/RecipeBuilder.java +++ b/src/main/java/io/th0rgal/oraxen/recipes/builders/RecipeBuilder.java @@ -33,7 +33,10 @@ public abstract class RecipeBuilder { protected RecipeBuilder(Player player, String builderName) { this.player = player; this.builderName = builderName; - this.inventoryTitle = player.getName() + " " + builderName + " builder"; + this.inventoryTitle = switch (builderName) { + case "shaped", "shapeless" -> "Recipe builder"; + default -> Character.toUpperCase(builderName.charAt(0)) + builderName.substring(1) + " builder"; + }; UUID playerId = player.getUniqueId(); RecipeBuilder existingBuilder = MAP.get(playerId); inventory = existingBuilder != null && existingBuilder.builderName.equals(builderName) @@ -96,6 +99,10 @@ public String getInventoryTitle() { return inventoryTitle; } + public boolean matchesInventory(Inventory inventory) { + return this.inventory == inventory; + } + public Player getPlayer() { return player; } diff --git a/src/main/java/io/th0rgal/oraxen/recipes/builders/SmithingBuilder.java b/src/main/java/io/th0rgal/oraxen/recipes/builders/SmithingBuilder.java new file mode 100644 index 0000000000..0f0f7b45d6 --- /dev/null +++ b/src/main/java/io/th0rgal/oraxen/recipes/builders/SmithingBuilder.java @@ -0,0 +1,49 @@ +package io.th0rgal.oraxen.recipes.builders; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class SmithingBuilder extends RecipeBuilder { + + public SmithingBuilder(Player player) { + super(player, "smithing"); + } + + @Override + Inventory createInventory(Player player, Component inventoryTitle) { + return Bukkit.createInventory(player, InventoryType.SMITHING, inventoryTitle); + } + + @Override + public void saveRecipe(String name) { + saveRecipe(name, null); + } + + @Override + public void saveRecipe(String name, String permission) { + ItemStack[] content = getInventory().getContents(); + ConfigurationSection newCraftSection = getConfig().createSection(name); + + setSingleIngredient(newCraftSection.createSection("template"), content[0]); + setSingleIngredient(newCraftSection.createSection("base"), content[1]); + setSingleIngredient(newCraftSection.createSection("addition"), content[2]); + setSerializedItem(newCraftSection.createSection("result"), content[3]); + + if (permission != null && !permission.isEmpty()) + newCraftSection.set("permission", permission); + + saveConfig(); + close(); + } + + private void setSingleIngredient(ConfigurationSection section, ItemStack itemStack) { + ItemStack singleItem = itemStack.clone(); + singleItem.setAmount(1); + setSerializedItem(section, singleItem); + } +} diff --git a/src/main/java/io/th0rgal/oraxen/recipes/builders/WorkstationBuilder.java b/src/main/java/io/th0rgal/oraxen/recipes/builders/WorkstationBuilder.java new file mode 100644 index 0000000000..0fb104f51c --- /dev/null +++ b/src/main/java/io/th0rgal/oraxen/recipes/builders/WorkstationBuilder.java @@ -0,0 +1,44 @@ +package io.th0rgal.oraxen.recipes.builders; + +import io.th0rgal.oraxen.utils.ItemUtils; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public abstract class WorkstationBuilder extends RecipeBuilder { + + private final String valueKey; + private int value; + + protected WorkstationBuilder(Player player, String builderName, String valueKey) { + super(player, builderName); + this.valueKey = valueKey; + } + + @Override + public void saveRecipe(String name) { + saveRecipe(name, null); + } + + @Override + public void saveRecipe(String name, String permission) { + ItemStack[] content = getInventory().getContents(); + ConfigurationSection newCraftSection = getConfig().createSection(name); + + setSerializedItem(newCraftSection.createSection("base"), content[0]); + if (!ItemUtils.isEmpty(content[1])) + setSerializedItem(newCraftSection.createSection("addition"), content[1]); + setSerializedItem(newCraftSection.createSection("result"), content[2]); + newCraftSection.set(valueKey, value); + + if (permission != null && !permission.isEmpty()) + newCraftSection.set("permission", permission); + + saveConfig(); + close(); + } + + protected void setValue(int value) { + this.value = Math.max(0, value); + } +} diff --git a/src/main/java/io/th0rgal/oraxen/recipes/listeners/CustomWorkstationEvents.java b/src/main/java/io/th0rgal/oraxen/recipes/listeners/CustomWorkstationEvents.java index a98cb03dbb..911f11ba0c 100644 --- a/src/main/java/io/th0rgal/oraxen/recipes/listeners/CustomWorkstationEvents.java +++ b/src/main/java/io/th0rgal/oraxen/recipes/listeners/CustomWorkstationEvents.java @@ -2,6 +2,8 @@ import io.th0rgal.oraxen.recipes.CustomWorkstationRecipe; import io.th0rgal.oraxen.recipes.CustomWorkstationRegistry; +import io.th0rgal.oraxen.recipes.builders.AnvilBuilder; +import io.th0rgal.oraxen.recipes.builders.GrindstoneBuilder; import io.th0rgal.oraxen.utils.InventoryUtils; import org.bukkit.GameMode; import org.bukkit.Material; @@ -31,6 +33,11 @@ public class CustomWorkstationEvents implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void prepareAnvil(PrepareAnvilEvent event) { + if (RecipesBuilderEvents.isBuilderInventory(event, AnvilBuilder.class)) { + event.setResult(null); + return; + } + Player player = InventoryUtils.playerFromView(event); CustomWorkstationRecipe recipe = CustomWorkstationRegistry.match(CustomWorkstationRecipe.Type.ANVIL, event.getInventory().getItem(0), event.getInventory().getItem(1)); @@ -45,6 +52,11 @@ public void prepareAnvil(PrepareAnvilEvent event) { @EventHandler(priority = EventPriority.HIGHEST) public void prepareGrindstone(PrepareGrindstoneEvent event) { + if (RecipesBuilderEvents.isBuilderInventory(event, GrindstoneBuilder.class)) { + event.setResult(null); + return; + } + Player player = InventoryUtils.playerFromView(event); Match match = grindstoneRecipe(event.getInventory().getItem(0), event.getInventory().getItem(1)); if (match == null) return; diff --git a/src/main/java/io/th0rgal/oraxen/recipes/listeners/RecipesBuilderEvents.java b/src/main/java/io/th0rgal/oraxen/recipes/listeners/RecipesBuilderEvents.java index cc963a4439..f72694c60f 100644 --- a/src/main/java/io/th0rgal/oraxen/recipes/listeners/RecipesBuilderEvents.java +++ b/src/main/java/io/th0rgal/oraxen/recipes/listeners/RecipesBuilderEvents.java @@ -1,13 +1,19 @@ package io.th0rgal.oraxen.recipes.listeners; import io.th0rgal.oraxen.recipes.builders.RecipeBuilder; +import io.th0rgal.oraxen.recipes.builders.GrindstoneBuilder; +import io.th0rgal.oraxen.recipes.builders.WorkstationBuilder; import io.th0rgal.oraxen.utils.InventoryUtils; import org.bukkit.Material; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.ItemStack; @@ -16,10 +22,35 @@ public class RecipesBuilderEvents implements Listener { + static boolean isBuilderInventory(InventoryEvent event, Class builderType) { + Player player = InventoryUtils.playerFromView(event); + RecipeBuilder recipeBuilder = player == null ? null : RecipeBuilder.get(player.getUniqueId()); + return builderType.isInstance(recipeBuilder) + && recipeBuilder.matchesInventory(event.getInventory()); + } + @EventHandler(priority = EventPriority.HIGH) public void setCursor(InventoryClickEvent event) { - String recipeBuilderTitle = Optional.ofNullable(RecipeBuilder.get(event.getWhoClicked().getUniqueId())).map(RecipeBuilder::getInventoryTitle).orElse(null); - if (!InventoryUtils.getTitleFromView(event).equals(recipeBuilderTitle) || event.getSlotType() != InventoryType.SlotType.RESULT) return; + RecipeBuilder recipeBuilder = RecipeBuilder.get(event.getWhoClicked().getUniqueId()); + if (recipeBuilder == null || !recipeBuilder.matchesInventory(event.getInventory())) return; + + // Paper delegates shift-clicks in custom anvil menus to a separate native AnvilMenu. + // Prevent that path for workstation builders. Grindstone authoring uses a generic hopper + // inventory, where disabling quick-move also keeps items out of the two unused slots. + if (recipeBuilder instanceof WorkstationBuilder + && event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY) { + event.setCancelled(true); + return; + } + + if (recipeBuilder instanceof GrindstoneBuilder && isUnusedGrindstoneSlot(event.getRawSlot(), event.getInventory().getSize())) { + event.setCancelled(true); + return; + } + + boolean resultSlot = event.getSlotType() == InventoryType.SlotType.RESULT + || recipeBuilder instanceof GrindstoneBuilder && event.getRawSlot() == 2; + if (!resultSlot) return; event.setCancelled(true); ItemStack currentResult = Optional.ofNullable(event.getCurrentItem()).orElse(new ItemStack(Material.AIR)).clone(); @@ -28,10 +59,24 @@ public void setCursor(InventoryClickEvent event) { event.getView().setCursor(currentResult); } + @EventHandler(priority = EventPriority.HIGH) + public void restrictGrindstoneDrag(InventoryDragEvent event) { + RecipeBuilder recipeBuilder = RecipeBuilder.get(event.getWhoClicked().getUniqueId()); + if (!(recipeBuilder instanceof GrindstoneBuilder) || !recipeBuilder.matchesInventory(event.getInventory())) return; + + int inventorySize = event.getInventory().getSize(); + if (event.getRawSlots().stream().anyMatch(slot -> isUnusedGrindstoneSlot(slot, inventorySize))) + event.setCancelled(true); + } + + private boolean isUnusedGrindstoneSlot(int rawSlot, int inventorySize) { + return rawSlot >= 3 && rawSlot < inventorySize; + } + @EventHandler(priority = EventPriority.HIGH) public void onInventoryClosed(InventoryCloseEvent event) { RecipeBuilder recipeBuilder = RecipeBuilder.get(event.getPlayer().getUniqueId()); - if (recipeBuilder == null || !InventoryUtils.getTitleFromView(event).equals(recipeBuilder.getInventoryTitle())) + if (recipeBuilder == null || !recipeBuilder.matchesInventory(event.getInventory())) return; recipeBuilder.setInventory(event.getInventory()); diff --git a/src/main/java/io/th0rgal/oraxen/recipes/listeners/SmithingRecipeEvents.java b/src/main/java/io/th0rgal/oraxen/recipes/listeners/SmithingRecipeEvents.java index e37203acf0..5f783d731d 100644 --- a/src/main/java/io/th0rgal/oraxen/recipes/listeners/SmithingRecipeEvents.java +++ b/src/main/java/io/th0rgal/oraxen/recipes/listeners/SmithingRecipeEvents.java @@ -3,6 +3,7 @@ import io.th0rgal.oraxen.api.OraxenItems; import io.th0rgal.oraxen.mechanics.provided.misc.misc.MiscMechanic; import io.th0rgal.oraxen.mechanics.provided.misc.misc.MiscMechanicFactory; +import io.th0rgal.oraxen.recipes.builders.SmithingBuilder; import io.th0rgal.oraxen.utils.InventoryUtils; import io.th0rgal.oraxen.utils.ItemUtils; import org.bukkit.Bukkit; @@ -22,6 +23,11 @@ public class SmithingRecipeEvents implements Listener { @EventHandler public void onSmithingRecipe(PrepareSmithingEvent event) { + if (RecipesBuilderEvents.isBuilderInventory(event, SmithingBuilder.class)) { + event.setResult(null); + return; + } + SmithingInventory inventory = event.getInventory(); ItemStack template = inventory.getInputTemplate(); ItemStack material = inventory.getInputMineral(); diff --git a/src/main/java/io/th0rgal/oraxen/utils/MusicDiscHelpers.java b/src/main/java/io/th0rgal/oraxen/utils/MusicDiscHelpers.java index 23a922a776..5a83314061 100644 --- a/src/main/java/io/th0rgal/oraxen/utils/MusicDiscHelpers.java +++ b/src/main/java/io/th0rgal/oraxen/utils/MusicDiscHelpers.java @@ -11,6 +11,7 @@ import net.kyori.adventure.sound.Sound; import net.kyori.adventure.sound.SoundStop; import org.bukkit.Bukkit; +import org.bukkit.Effect; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -79,7 +80,7 @@ public static ItemStack stopJukeboxAt(Entity entity, float volume, float pitch) if(record == null) return null; pdc.remove(MUSIC_DISC_KEY); if(ItemUtils.isMusicDisc(record) && volume == 1F && pitch == 1F && NMSHandlers.getHandler().supportsJukeboxPlaying()) { - NMSHandlers.getHandler().stopJukeBox(entity.getLocation()); + entity.getWorld().playEffect(entity.getLocation(), Effect.SOUND_STOP_JUKEBOX_SONG, 0); } else { var song = MusicDiscHelpers.getSong(record); if (song == null) return record; diff --git a/src/main/java/io/th0rgal/oraxen/utils/inventories/PickItemUtils.java b/src/main/java/io/th0rgal/oraxen/utils/inventories/PickItemUtils.java new file mode 100644 index 0000000000..a55855c477 --- /dev/null +++ b/src/main/java/io/th0rgal/oraxen/utils/inventories/PickItemUtils.java @@ -0,0 +1,78 @@ +package io.th0rgal.oraxen.utils.inventories; + +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +public final class PickItemUtils { + + private PickItemUtils() { + } + + public static void pickItem(Player player, ItemStack item) { + if (item == null || item.getType().isAir()) return; + + PlayerInventory inventory = player.getInventory(); + int sourceSlot = findMatchingSlot(inventory, item); + int targetSlot = isHotbarSlot(sourceSlot) ? sourceSlot : findSuitableHotbarSlot(inventory); + if (sourceSlot >= 0) { + if (sourceSlot != targetSlot) { + ItemStack targetItem = inventory.getItem(targetSlot); + inventory.setItem(targetSlot, inventory.getItem(sourceSlot)); + inventory.setItem(sourceSlot, targetItem); + } + inventory.setHeldItemSlot(targetSlot); + return; + } + + if (player.getGameMode() != GameMode.CREATIVE) return; + + ItemStack targetItem = inventory.getItem(targetSlot); + if (targetItem != null && !targetItem.getType().isAir()) { + int emptySlot = findEmptySlot(inventory, targetSlot); + if (emptySlot >= 0) inventory.setItem(emptySlot, targetItem); + } + + inventory.setItem(targetSlot, item); + inventory.setHeldItemSlot(targetSlot); + } + + static int findSuitableHotbarSlot(PlayerInventory inventory) { + int selectedSlot = inventory.getHeldItemSlot(); + for (int offset = 0; offset < 9; offset++) { + int slot = (selectedSlot + offset) % 9; + ItemStack candidate = inventory.getItem(slot); + if (candidate == null || candidate.getType().isAir()) return slot; + } + + for (int offset = 0; offset < 9; offset++) { + int slot = (selectedSlot + offset) % 9; + ItemStack candidate = inventory.getItem(slot); + if (candidate == null || candidate.getEnchantments().isEmpty()) return slot; + } + + return selectedSlot; + } + + private static boolean isHotbarSlot(int slot) { + return slot >= 0 && slot < 9; + } + + private static int findMatchingSlot(PlayerInventory inventory, ItemStack item) { + for (int slot = 0; slot < 36; slot++) { + ItemStack candidate = inventory.getItem(slot); + if (candidate != null && candidate.isSimilar(item)) return slot; + } + return -1; + } + + private static int findEmptySlot(PlayerInventory inventory, int excludedSlot) { + for (int slot = 0; slot < 36; slot++) { + if (slot == excludedSlot) continue; + ItemStack candidate = inventory.getItem(slot); + if (candidate == null || candidate.getType().isAir()) return slot; + } + return -1; + } +} diff --git a/src/main/java/io/th0rgal/oraxen/utils/schema/SchemaGenerator.java b/src/main/java/io/th0rgal/oraxen/utils/schema/SchemaGenerator.java index 866fb451fe..d842b2922a 100644 --- a/src/main/java/io/th0rgal/oraxen/utils/schema/SchemaGenerator.java +++ b/src/main/java/io/th0rgal/oraxen/utils/schema/SchemaGenerator.java @@ -572,6 +572,23 @@ private static JsonObject generateComponents() { consumable.add("properties", consumeProps); components.add("consumable", consumable); + // death_protection (1.21.2+) + JsonObject deathProtection = new JsonObject(); + deathProtection.addProperty("type", "object"); + deathProtection.addProperty("minecraftVersion", "1.21.2+"); + deathProtection.addProperty("description", "Prevents death and applies configured death effects"); + JsonObject deathProtectionProps = new JsonObject(); + JsonObject deathEffects = new JsonObject(); + deathEffects.addProperty("type", "array"); + deathEffects.addProperty("description", + "Death effects: apply_effects, remove_effects, clear_all_effects, teleport_randomly, and play_sound"); + JsonObject deathEffect = new JsonObject(); + deathEffect.addProperty("type", "object"); + deathEffects.add("items", deathEffect); + deathProtectionProps.add("death_effects", deathEffects); + deathProtection.add("properties", deathProtectionProps); + components.add("death_protection", deathProtection); + // equippable (1.21.2+) JsonObject equippable = new JsonObject(); equippable.addProperty("type", "object"); diff --git a/src/main/resources/languages/english.yml b/src/main/resources/languages/english.yml index ed828ef36e..e45685b4d8 100644 --- a/src/main/resources/languages/english.yml +++ b/src/main/resources/languages/english.yml @@ -69,7 +69,7 @@ logs: command: recipe: - no_builder: "<#fa4943>Please create a recipe first!" + no_builder: "<#fa4943>Please create a recipe first." no_furnace: "<#fa4943>This option is only available for Furnace Recipes!" no_name: "<#fa4943>Please specify a name for the recipe!" no_recipes: "<#fa4943>There are no recipes to show!" diff --git a/src/main/resources/mechanics.yml b/src/main/resources/mechanics.yml index 02c5e76bd2..3e0af624c0 100644 --- a/src/main/resources/mechanics.yml +++ b/src/main/resources/mechanics.yml @@ -94,6 +94,8 @@ aura: # Cosmetic backpacks displayed on player's back using packet-based armor stands backpack_cosmetic: enabled: true + armor_stand_enabled: true + armor_stand_range: 128 hat: enabled: true diff --git a/src/test/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/BlockLocationTest.java b/src/test/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/BlockLocationTest.java new file mode 100644 index 0000000000..00cddb6442 --- /dev/null +++ b/src/test/java/io/th0rgal/oraxen/mechanics/provided/gameplay/furniture/BlockLocationTest.java @@ -0,0 +1,21 @@ +package io.th0rgal.oraxen.mechanics.provided.gameplay.furniture; + +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +class BlockLocationTest { + + @Test + void persistentDataTypeRoundTripsBlockLocation() { + BlockLocation location = new BlockLocation(3, -2, 7); + PersistentDataAdapterContext context = mock(PersistentDataAdapterContext.class); + + byte[] primitive = BlockLocation.dataType.toPrimitive(location, context); + BlockLocation deserialized = BlockLocation.dataType.fromPrimitive(primitive, context); + + assertEquals(location, deserialized); + } +} diff --git a/src/test/java/io/th0rgal/oraxen/utils/inventories/PickItemUtilsTest.java b/src/test/java/io/th0rgal/oraxen/utils/inventories/PickItemUtilsTest.java new file mode 100644 index 0000000000..06f8a7ea9c --- /dev/null +++ b/src/test/java/io/th0rgal/oraxen/utils/inventories/PickItemUtilsTest.java @@ -0,0 +1,74 @@ +package io.th0rgal.oraxen.utils.inventories; + +import org.bukkit.entity.Player; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PickItemUtilsTest { + + @Test + void selectsMatchingHotbarItemWithoutMovingIt() { + ItemStack pickedItem = item(); + ItemStack[] slots = new ItemStack[36]; + slots[2] = pickedItem; + + PlayerInventory inventory = inventory(slots, 7); + Player player = mock(Player.class); + when(player.getInventory()).thenReturn(inventory); + + PickItemUtils.pickItem(player, pickedItem); + + verify(inventory).setHeldItemSlot(2); + verify(inventory, never()).setItem(anyInt(), nullable(ItemStack.class)); + } + + @Test + void movesMatchingStorageItemToSuitableHotbarSlot() { + ItemStack pickedItem = item(); + ItemStack selectedItem = item(); + ItemStack[] slots = new ItemStack[36]; + slots[4] = selectedItem; + slots[20] = pickedItem; + + PlayerInventory inventory = inventory(slots, 4); + Player player = mock(Player.class); + when(player.getInventory()).thenReturn(inventory); + + PickItemUtils.pickItem(player, pickedItem); + + verify(inventory).setItem(5, pickedItem); + verify(inventory).setItem(20, null); + verify(inventory).setHeldItemSlot(5); + } + + private static ItemStack item() { + Material material = mock(Material.class); + when(material.isAir()).thenReturn(false); + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(material); + when(item.isSimilar(item)).thenReturn(true); + when(item.getEnchantments()).thenReturn(java.util.Map.of()); + return item; + } + + private static PlayerInventory inventory(ItemStack[] slots, int selectedSlot) { + PlayerInventory inventory = mock(PlayerInventory.class); + when(inventory.getHeldItemSlot()).thenReturn(selectedSlot); + when(inventory.getItem(anyInt())).thenAnswer(invocation -> slots[invocation.getArgument(0)]); + doAnswer(invocation -> { + slots[invocation.getArgument(0)] = invocation.getArgument(1); + return null; + }).when(inventory).setItem(anyInt(), nullable(ItemStack.class)); + return inventory; + } +}