feat: simplify item usage behavior (fixes #2475)

This commit is contained in:
mworzala 2024-12-03 00:08:25 -05:00 committed by Matt Worzala
parent 1a63e48894
commit 8953ff467e
16 changed files with 317 additions and 358 deletions

View File

@ -15,8 +15,7 @@ import net.minestom.server.entity.damage.Damage;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.event.item.*;
import net.minestom.server.event.player.*;
import net.minestom.server.event.server.ServerTickMonitorEvent;
import net.minestom.server.instance.Instance;
@ -28,25 +27,23 @@ import net.minestom.server.instance.block.predicate.BlockPredicate;
import net.minestom.server.instance.block.predicate.BlockTypeFilter;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemAnimation;
import net.minestom.server.item.ItemComponent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.item.component.BlockPredicates;
import net.minestom.server.item.component.EnchantmentList;
import net.minestom.server.item.component.LodestoneTracker;
import net.minestom.server.item.component.PotionContents;
import net.minestom.server.item.enchant.Enchantment;
import net.minestom.server.item.component.Consumable;
import net.minestom.server.monitoring.BenchmarkManager;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.network.packet.server.common.CustomReportDetailsPacket;
import net.minestom.server.network.packet.server.common.ServerLinksPacket;
import net.minestom.server.potion.CustomPotionEffect;
import net.minestom.server.potion.PotionEffect;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.time.TimeUnit;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
@ -138,29 +135,11 @@ public class PlayerInit {
.build();
player.getInventory().addItemStack(bundle);
player.getInventory().addItemStack(ItemStack.builder(Material.COMPASS)
.set(ItemComponent.LODESTONE_TRACKER, new LodestoneTracker(player.getInstance().getDimensionName(), new Vec(10, 10, 10), true))
.build());
player.getInventory().addItemStack(ItemStack.builder(Material.STONE_SWORD)
.set(ItemComponent.ENCHANTMENTS, new EnchantmentList(Map.of(
Enchantment.SHARPNESS, 10
)))
.build());
//
player.getInventory().addItemStack(ItemStack.builder(Material.STONE_SWORD)
.build());
player.getInventory().addItemStack(ItemStack.builder(Material.BLACK_BANNER)
.build());
player.getInventory().addItemStack(ItemStack.builder(Material.POTION)
.set(ItemComponent.POTION_CONTENTS, new PotionContents(null, null, List.of(
new CustomPotionEffect(PotionEffect.JUMP_BOOST, new CustomPotionEffect.Settings((byte) 4,
45 * 20, false, true, true, null))
)))
.customName(Component.text("Sharpness 10 Sword").append(Component.space()).append(Component.text("§c§l[LEGENDARY]")))
.build());
player.setGameMode(GameMode.SURVIVAL);
PlayerInventory inventory = event.getPlayer().getInventory();
inventory.addItemStack(getFoodItem(20));
inventory.addItemStack(getFoodItem(10000));
inventory.addItemStack(getFoodItem(Integer.MAX_VALUE));
if (event.isFirstSpawn()) {
@ -195,6 +174,29 @@ public class PlayerInit {
event.getInstance().setBlock(event.getPosition(), block);
})
.addListener(PlayerBeginItemUseEvent.class, event -> {
final ItemStack itemStack = event.getItemStack();
final boolean hasProjectile = !itemStack.get(ItemComponent.CHARGED_PROJECTILES, List.of()).isEmpty();
if (itemStack.material() == Material.CROSSBOW && hasProjectile) {
// "shoot" the arrow
event.setItemStack(itemStack.without(ItemComponent.CHARGED_PROJECTILES));
event.getPlayer().sendMessage("pew pew!");
event.setItemUseDuration(0); // Do not start using the item
return;
}
})
.addListener(PlayerFinishItemUseEvent.class, event -> {
if (event.getItemStack().material() == Material.APPLE) {
event.getPlayer().sendMessage("yummy yummy apple");
}
})
.addListener(PlayerCancelItemUseEvent.class, event -> {
final ItemStack itemStack = event.getItemStack();
if (itemStack.material() == Material.CROSSBOW && event.getUseDuration() > 25) {
event.setItemStack(itemStack.with(ItemComponent.CHARGED_PROJECTILES, List.of(ItemStack.of(Material.ARROW))));
return;
}
})
.addListener(PlayerBlockInteractEvent.class, event -> {
var block = event.getBlock();
var rawOpenProp = block.getProperty("open");
@ -256,4 +258,16 @@ public class PlayerInit {
Audiences.players().sendPlayerListHeaderAndFooter(header, footer);
}).repeat(10, TimeUnit.SERVER_TICK).schedule();
}
public static ItemStack getFoodItem(int consumeTicks) {
return ItemStack.builder(Material.IRON_NUGGET)
.amount(64)
.set(ItemComponent.CONSUMABLE, new Consumable(
(float) consumeTicks / 20,
ItemAnimation.EAT,
SoundEvent.BLOCK_CHAIN_STEP,
true,
new ArrayList<>()))
.build();
}
}

View File

@ -600,9 +600,10 @@ public class LivingEntity extends Entity implements EquipmentHandler {
meta.setHandActive(isHandActive);
meta.setActiveHand(offHand ? PlayerHand.OFF : PlayerHand.MAIN);
meta.setInRiptideSpinAttack(riptideSpinAttack);
meta.setNotifyAboutChanges(true);
updatePose(); // Riptide spin attack has a pose
meta.setNotifyAboutChanges(true);
}
}

View File

@ -38,9 +38,8 @@ import net.minestom.server.entity.vehicle.PlayerInputs;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.inventory.InventoryOpenEvent;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.item.ItemUpdateStateEvent;
import net.minestom.server.event.item.ItemUsageCompleteEvent;
import net.minestom.server.event.item.PickupExperienceEvent;
import net.minestom.server.event.item.PlayerFinishItemUseEvent;
import net.minestom.server.event.player.*;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.EntityTracker;
@ -405,24 +404,22 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
// Eating animation
if (isUsingItem()) {
if (itemUseTime > 0 && getCurrentItemUseTime() >= itemUseTime) {
PlayerFinishItemUseEvent finishUseEvent = new PlayerFinishItemUseEvent(this, itemUseHand, getItemInHand(itemUseHand), itemUseTime);
EventDispatcher.call(finishUseEvent);
// Reset client state
triggerStatus((byte) EntityStatuses.Player.MARK_ITEM_FINISHED);
ItemUpdateStateEvent itemUpdateStateEvent = callItemUpdateStateEvent(itemUseHand);
// Refresh hand
final boolean isOffHand = itemUpdateStateEvent.getHand() == PlayerHand.OFF;
// Reset server state
final boolean isOffHand = itemUseHand == PlayerHand.OFF;
refreshActiveHand(false, isOffHand, false);
final ItemStack item = itemUpdateStateEvent.getItemStack();
final boolean isFood = item.has(ItemComponent.FOOD);
if (isFood || item.material() == Material.POTION) {
PlayerEatEvent playerEatEvent = new PlayerEatEvent(this, item, itemUseHand);
EventDispatcher.call(playerEatEvent);
}
var itemUsageCompleteEvent = new ItemUsageCompleteEvent(this, itemUseHand, item);
EventDispatcher.call(itemUsageCompleteEvent);
clearItemUse();
// Update item from event, sending a slot refresh no matter what
final int slot = isOffHand ? PlayerInventoryUtils.OFFHAND_SLOT : getHeldSlot();
final ItemStack itemStack = finishUseEvent.getItemStack();
inventory.setItemStack(slot, itemStack, false);
inventory.sendSlotRefresh(slot, itemStack, itemStack);
}
}
@ -2157,19 +2154,6 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
refreshItemUse(null, 0);
}
/**
* Used to call {@link ItemUpdateStateEvent} with the proper item
* It does check which hand to get the item to update.
*
* @return the called {@link ItemUpdateStateEvent},
*/
public @NotNull ItemUpdateStateEvent callItemUpdateStateEvent(@NotNull PlayerHand hand) {
ItemUpdateStateEvent itemUpdateStateEvent = new ItemUpdateStateEvent(this, hand, getItemInHand(hand));
EventDispatcher.call(itemUpdateStateEvent);
return itemUpdateStateEvent;
}
public void refreshInput(boolean forward, boolean backward, boolean left, boolean right, boolean jump, boolean shift, boolean sprint) {
this.inputs.refresh(forward, backward, left, right, jump, shift, sprint);
}

View File

@ -1,68 +0,0 @@
package net.minestom.server.event.item;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.trait.ItemEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull;
/**
* Event when a player updates an item state, meaning when they stop using the item.
*/
public class ItemUpdateStateEvent implements PlayerInstanceEvent, ItemEvent {
private final Player player;
private final PlayerHand hand;
private final ItemStack itemStack;
private boolean handAnimation;
private boolean riptideSpinAttack;
public ItemUpdateStateEvent(@NotNull Player player, @NotNull PlayerHand hand, @NotNull ItemStack itemStack) {
this.player = player;
this.hand = hand;
this.itemStack = itemStack;
}
@NotNull
public PlayerHand getHand() {
return hand;
}
/**
* Sets whether the player should have a hand animation.
*
* @param handAnimation whether the player should have a hand animation
*/
public void setHandAnimation(boolean handAnimation) {
this.handAnimation = handAnimation;
}
public boolean hasHandAnimation() {
return handAnimation;
}
/**
* Sets whether the player should have a riptide spin attack animation.
*
* @param riptideSpinAttack whether the player should have a riptide spin attack animation
*/
public void setRiptideSpinAttack(boolean riptideSpinAttack) {
this.riptideSpinAttack = riptideSpinAttack;
}
public boolean isRiptideSpinAttack() {
return riptideSpinAttack;
}
@Override
public @NotNull ItemStack getItemStack() {
return itemStack;
}
@Override
public @NotNull Player getPlayer() {
return player;
}
}

View File

@ -0,0 +1,85 @@
package net.minestom.server.event.item;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.ItemEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.item.ItemAnimation;
import net.minestom.server.item.ItemStack;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
/**
* Called when a player begins using an item with the item, animation, and duration.
*
* <p>Setting the use duration to zero or cancelling the event will prevent consumption.</p>
*/
public class PlayerBeginItemUseEvent implements PlayerInstanceEvent, ItemEvent, CancellableEvent {
private final Player player;
private final PlayerHand hand;
private ItemStack itemStack;
private ItemAnimation animation;
private long itemUseDuration;
private boolean cancelled = false;
public PlayerBeginItemUseEvent(@NotNull Player player, @NotNull PlayerHand hand,
@NotNull ItemStack itemStack, @NotNull ItemAnimation animation,
long itemUseDuration) {
this.player = player;
this.hand = hand;
this.itemStack = itemStack;
this.animation = animation;
this.itemUseDuration = itemUseDuration;
}
@Override
public @NotNull Player getPlayer() {
return player;
}
public @NotNull PlayerHand getHand() {
return hand;
}
@Override
public @NotNull ItemStack getItemStack() {
return itemStack;
}
public void setItemStack(@NotNull ItemStack itemStack) {
this.itemStack = itemStack;
}
public @NotNull ItemAnimation getAnimation() {
return animation;
}
/**
* Returns the item use duration, in ticks. A duration of zero will prevent consumption (same effect as cancellation).
*
* @return the current item use duration
*/
public long getItemUseDuration() {
return itemUseDuration;
}
/**
* Sets the item use duration, in ticks.
*/
public void setItemUseDuration(long itemUseDuration) {
Check.argCondition(itemUseDuration < 0, "Item use duration cannot be negative");
this.itemUseDuration = itemUseDuration;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}

View File

@ -0,0 +1,50 @@
package net.minestom.server.event.item;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.trait.ItemEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull;
/**
* Called when a player stops using an item before the item has completed its usage, including the amount of
* time the item was used before cancellation.
*
* <p>This includes cases like half eating a food, but also includes shooting a bow.</p>
*/
public class PlayerCancelItemUseEvent implements PlayerInstanceEvent, ItemEvent {
private final Player player;
private final PlayerHand hand;
private ItemStack itemStack;
private final long useDuration;
public PlayerCancelItemUseEvent(@NotNull Player player, @NotNull PlayerHand hand, @NotNull ItemStack itemStack, long useDuration) {
this.player = player;
this.hand = hand;
this.itemStack = itemStack;
this.useDuration = useDuration;
}
@Override
public @NotNull Player getPlayer() {
return player;
}
public @NotNull PlayerHand getHand() {
return hand;
}
@Override
public @NotNull ItemStack getItemStack() {
return itemStack;
}
public void setItemStack(@NotNull ItemStack itemStack) {
this.itemStack = itemStack;
}
public long getUseDuration() {
return useDuration;
}
}

View File

@ -8,23 +8,29 @@ import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull;
/**
* Event when the item usage duration has passed for a player, meaning when the item has completed its usage.
* Called when a player completely finishes using an item.
*
* <p>{@link #getUseDuration()} represents the total time spent using the item.</p>
*/
public class ItemUsageCompleteEvent implements PlayerInstanceEvent, ItemEvent {
public class PlayerFinishItemUseEvent implements PlayerInstanceEvent, ItemEvent {
private final Player player;
private final PlayerHand hand;
private final ItemStack itemStack;
private ItemStack itemStack;
private final long useDuration;
public ItemUsageCompleteEvent(@NotNull Player player, @NotNull PlayerHand hand,
@NotNull ItemStack itemStack) {
public PlayerFinishItemUseEvent(@NotNull Player player, @NotNull PlayerHand hand, @NotNull ItemStack itemStack, long useDuration) {
this.player = player;
this.hand = hand;
this.itemStack = itemStack;
this.useDuration = useDuration;
}
@NotNull
public PlayerHand getHand() {
@Override
public @NotNull Player getPlayer() {
return player;
}
public @NotNull PlayerHand getHand() {
return hand;
}
@ -33,8 +39,11 @@ public class ItemUsageCompleteEvent implements PlayerInstanceEvent, ItemEvent {
return itemStack;
}
@Override
public @NotNull Player getPlayer() {
return player;
public void setItemStack(@NotNull ItemStack itemStack) {
this.itemStack = itemStack;
}
public long getUseDuration() {
return useDuration;
}
}

View File

@ -1,52 +0,0 @@
package net.minestom.server.event.player;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.trait.ItemEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull;
/**
* Called when a player is finished eating.
*/
public class PlayerEatEvent implements ItemEvent, PlayerInstanceEvent {
private final Player player;
private final ItemStack foodItem;
private final PlayerHand hand;
public PlayerEatEvent(@NotNull Player player, @NotNull ItemStack foodItem, @NotNull PlayerHand hand) {
this.player = player;
this.foodItem = foodItem;
this.hand = hand;
}
/**
* Gets the food item that has been eaten.
*
* @return the food item
* @deprecated use getItemStack() for the eaten item
*/
@Deprecated
public @NotNull ItemStack getFoodItem() {
return foodItem;
}
public @NotNull PlayerHand getHand() {
return hand;
}
@Override
public @NotNull Player getPlayer() {
return player;
}
/**
* Gets the food item that has been eaten.
*
* @return the food item
*/
@Override
public @NotNull ItemStack getItemStack() { return foodItem; }
}

View File

@ -1,71 +0,0 @@
package net.minestom.server.event.player;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent;
import org.jetbrains.annotations.NotNull;
/**
* Used when a {@link Player} starts the animation of an item.
*
* @see ItemAnimationType
*/
public class PlayerItemAnimationEvent implements PlayerInstanceEvent, CancellableEvent {
private final Player player;
private final ItemAnimationType itemAnimationType;
private final PlayerHand hand;
private boolean cancelled;
public PlayerItemAnimationEvent(@NotNull Player player, @NotNull ItemAnimationType itemAnimationType, @NotNull PlayerHand hand) {
this.player = player;
this.itemAnimationType = itemAnimationType;
this.hand = hand;
}
/**
* Gets the animation.
*
* @return the animation
*/
public @NotNull ItemAnimationType getItemAnimationType() {
return itemAnimationType;
}
/**
* Gets the hand that was used.
*
* @return the hand
*/
public @NotNull PlayerHand getHand() {
return hand;
}
@Override
public @NotNull Player getPlayer() {
return player;
}
public enum ItemAnimationType {
BOW,
CROSSBOW,
TRIDENT,
SHIELD,
SPYGLASS,
HORN,
BRUSH,
EAT,
OTHER
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@ -2,7 +2,6 @@ package net.minestom.server.event.player;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.item.ItemUpdateStateEvent;
import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.ItemEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent;
@ -49,7 +48,7 @@ public class PlayerUseItemEvent implements PlayerInstanceEvent, ItemEvent, Cance
/**
* Gets the item usage duration. After this amount of milliseconds,
* the animation will stop automatically and {@link ItemUpdateStateEvent} is called.
* the animation will stop automatically and {@link net.minestom.server.event.item.PlayerFinishItemUseEvent} is called.
*
* @return the item use time
*/

View File

@ -1,20 +0,0 @@
package net.minestom.server.event.player;
import net.minestom.server.event.Event;
import net.minestom.server.network.packet.server.common.TagsPacket;
import org.jetbrains.annotations.NotNull;
@Deprecated
public class UpdateTagListEvent implements Event {
private TagsPacket packet;
public UpdateTagListEvent(@NotNull TagsPacket packet) {
this.packet = packet;
}
@NotNull
public TagsPacket getTags() {
return packet;
}
}

View File

@ -0,0 +1,21 @@
package net.minestom.server.item;
import net.minestom.server.network.NetworkBuffer;
import net.minestom.server.utils.nbt.BinaryTagSerializer;
public enum ItemAnimation {
NONE,
EAT,
DRINK,
BLOCK,
BOW,
SPEAR,
CROSSBOW,
SPYGLASS,
TOOT_HORN,
BRUSH,
BUNDLE;
public static final NetworkBuffer.Type<ItemAnimation> NETWORK_TYPE = NetworkBuffer.Enum(ItemAnimation.class);
public static final BinaryTagSerializer<ItemAnimation> NBT_TYPE = BinaryTagSerializer.fromEnumStringable(ItemAnimation.class);
}

View File

@ -1,6 +1,7 @@
package net.minestom.server.item.component;
import net.minestom.server.ServerFlag;
import net.minestom.server.item.ItemAnimation;
import net.minestom.server.network.NetworkBuffer;
import net.minestom.server.network.NetworkBufferTemplate;
import net.minestom.server.sound.SoundEvent;
@ -12,39 +13,23 @@ import java.util.List;
public record Consumable(
float consumeSeconds,
@NotNull Animation animation,
@NotNull ItemAnimation animation,
@NotNull SoundEvent sound,
boolean hasConsumeParticles,
@NotNull List<ConsumeEffect> effects
) {
public static final float DEFAULT_CONSUME_SECONDS = 1.6f;
public enum Animation {
NONE,
EAT,
DRINK,
BLOCK,
BOW,
SPEAR,
CROSSBOW,
SPYGLASS,
TOOT_HORN,
BRUSH;
public static final NetworkBuffer.Type<Animation> NETWORK_TYPE = NetworkBuffer.Enum(Animation.class);
public static final BinaryTagSerializer<Animation> NBT_TYPE = BinaryTagSerializer.fromEnumStringable(Animation.class);
}
public static final NetworkBuffer.Type<Consumable> NETWORK_TYPE = NetworkBufferTemplate.template(
NetworkBuffer.FLOAT, Consumable::consumeSeconds,
Animation.NETWORK_TYPE, Consumable::animation,
ItemAnimation.NETWORK_TYPE, Consumable::animation,
SoundEvent.NETWORK_TYPE, Consumable::sound,
NetworkBuffer.BOOLEAN, Consumable::hasConsumeParticles,
ConsumeEffect.NETWORK_TYPE.list(Short.MAX_VALUE), Consumable::effects,
Consumable::new);
public static final BinaryTagSerializer<Consumable> NBT_TYPE = BinaryTagTemplate.object(
"consume_seconds", BinaryTagSerializer.FLOAT.optional(DEFAULT_CONSUME_SECONDS), Consumable::consumeSeconds,
"animation", Animation.NBT_TYPE.optional(Animation.EAT), Consumable::animation,
"animation", ItemAnimation.NBT_TYPE.optional(ItemAnimation.EAT), Consumable::animation,
"sound", SoundEvent.NBT_TYPE.optional(SoundEvent.ENTITY_GENERIC_EAT), Consumable::sound,
"has_consume_particles", BinaryTagSerializer.BOOLEAN.optional(true), Consumable::hasConsumeParticles,
"on_consume_effects", ConsumeEffect.NBT_TYPE.list().optional(List.of()), Consumable::effects,

View File

@ -10,7 +10,7 @@ import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.entity.metadata.LivingEntityMeta;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.item.ItemUpdateStateEvent;
import net.minestom.server.event.item.PlayerCancelItemUseEvent;
import net.minestom.server.event.player.PlayerCancelDiggingEvent;
import net.minestom.server.event.player.PlayerFinishDiggingEvent;
import net.minestom.server.event.player.PlayerStartDiggingEvent;
@ -155,16 +155,19 @@ public final class PlayerDiggingListener {
private static void updateItemState(Player player) {
LivingEntityMeta meta = player.getLivingEntityMeta();
if (meta == null || !meta.isHandActive()) return;
PlayerHand hand = meta.getActiveHand();
final PlayerHand hand = meta.getActiveHand();
ItemUpdateStateEvent itemUpdateStateEvent = player.callItemUpdateStateEvent(hand);
PlayerCancelItemUseEvent cancelUseEvent = new PlayerCancelItemUseEvent(player, hand, player.getItemInHand(hand), player.getCurrentItemUseTime());
EventDispatcher.call(cancelUseEvent);
player.setItemInHand(hand, cancelUseEvent.getItemStack());
player.clearItemUse();
// Reset client state
player.triggerStatus((byte) EntityStatuses.Player.MARK_ITEM_FINISHED);
final boolean isOffHand = itemUpdateStateEvent.getHand() == PlayerHand.OFF;
player.refreshActiveHand(itemUpdateStateEvent.hasHandAnimation(),
isOffHand, itemUpdateStateEvent.isRiptideSpinAttack());
// Reset server state
final boolean isOffHand = hand == PlayerHand.OFF;
player.refreshActiveHand(false, isOffHand, false);
player.clearItemUse();
}
private static void swapItemHand(Player player) {

View File

@ -1,6 +1,7 @@
package net.minestom.server.listener;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.player.PlayerChangeHeldSlotEvent;
import net.minestom.server.network.packet.client.play.ClientHeldItemChangePacket;
@ -37,7 +38,7 @@ public class PlayerHeldListener {
}
// Player is not using offhand, reset item use
if (player.getItemUseHand() != Player.Hand.OFF) {
if (player.getItemUseHand() != PlayerHand.OFF) {
player.refreshActiveHand(false, false, false);
player.clearItemUse();
}

View File

@ -3,10 +3,10 @@ package net.minestom.server.listener;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.player.PlayerItemAnimationEvent;
import net.minestom.server.event.player.PlayerPreEatEvent;
import net.minestom.server.event.item.PlayerBeginItemUseEvent;
import net.minestom.server.event.player.PlayerUseItemEvent;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemAnimation;
import net.minestom.server.item.ItemComponent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
@ -22,9 +22,48 @@ public class UseItemListener {
final PlayerHand hand = packet.hand();
final ItemStack itemStack = player.getItemInHand(hand);
final Material material = itemStack.material();
final Consumable consumable = itemStack.get(ItemComponent.CONSUMABLE);
boolean usingMainHand = player.getItemUseHand() == Player.Hand.MAIN && hand == Player.Hand.OFF;
PlayerUseItemEvent useItemEvent = new PlayerUseItemEvent(player, hand, itemStack, usingMainHand ? 0 : defaultUseItemTime(itemStack));
// The following item animations and use item times come from vanilla.
// These items do not yet use components, but hopefully they will in the future
// and this behavior can be removed.
long useItemTime = 0;
ItemAnimation useAnimation = ItemAnimation.NONE;
if (material == Material.BOW) {
useItemTime = 72000;
useAnimation = ItemAnimation.BOW;
} else if (material == Material.CROSSBOW) {
// The crossbow has a min charge time dependent on quick charge, but to the
// client they can hold it forever
useItemTime = 7200;
useAnimation = ItemAnimation.CROSSBOW;
} else if (material == Material.SHIELD) {
useItemTime = 72000;
useAnimation = ItemAnimation.BLOCK;
} else if (material == Material.TRIDENT) {
useItemTime = 72000;
useAnimation = ItemAnimation.SPEAR;
} else if (material == Material.SPYGLASS) {
useItemTime = 1200;
useAnimation = ItemAnimation.SPYGLASS;
} else if (material == Material.GOAT_HORN) {
useItemTime = getInstrumentTime(itemStack);
useAnimation = ItemAnimation.TOOT_HORN;
} else if (material == Material.BRUSH) {
useItemTime = 200;
useAnimation = ItemAnimation.BRUSH;
} else if (material.name().contains("bundle")) {
// Why is a bundle usable???
useItemTime = 200;
useAnimation = ItemAnimation.BUNDLE;
} else if (consumable != null) {
useItemTime = consumable.consumeTicks();
useAnimation = consumable.animation();
}
boolean usingMainHand = player.getItemUseHand() == PlayerHand.MAIN && hand == PlayerHand.OFF;
PlayerUseItemEvent useItemEvent = new PlayerUseItemEvent(player, hand, itemStack,
usingMainHand ? 0 : useItemTime);
EventDispatcher.call(useItemEvent);
player.sendPacket(new AcknowledgeBlockChangePacket(packet.sequence()));
@ -34,55 +73,34 @@ public class UseItemListener {
return;
}
// Equip armor with right click
useItemTime = useItemEvent.getItemUseTime();
if (useItemTime != 0) {
final PlayerBeginItemUseEvent beginUseEvent = new PlayerBeginItemUseEvent(player, hand, itemStack, useAnimation, useItemTime);
EventDispatcher.callCancellable(beginUseEvent, () -> {
player.setItemInHand(hand, beginUseEvent.getItemStack());
if (beginUseEvent.getItemUseDuration() <= 0) return;
player.refreshItemUse(hand, beginUseEvent.getItemUseDuration());
player.refreshActiveHand(true, hand == PlayerHand.OFF, false);
});
return; // Do not also swap after use
}
// If the item was not usable, we can try to do an equipment swap with it.
final Equippable equippable = itemStack.get(ItemComponent.EQUIPPABLE);
if (equippable != null && equippable.swappable()) {
final ItemStack currentlyEquipped = player.getEquipment(equippable.slot());
player.setEquipment(equippable.slot(), itemStack);
player.setItemInHand(hand, currentlyEquipped);
}
long itemUseTime = useItemEvent.getItemUseTime();
PlayerItemAnimationEvent.ItemAnimationType itemAnimationType;
if (material == Material.BOW) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.BOW;
} else if (material == Material.CROSSBOW) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.CROSSBOW;
} else if (material == Material.SHIELD) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.SHIELD;
} else if (material == Material.TRIDENT) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.TRIDENT;
} else if (material == Material.SPYGLASS) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.SPYGLASS;
} else if (material == Material.GOAT_HORN) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.HORN;
} else if (material == Material.BRUSH) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.BRUSH;
} else if (itemStack.has(ItemComponent.FOOD) || itemStack.material() == Material.POTION) {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.EAT;
PlayerPreEatEvent playerPreEatEvent = new PlayerPreEatEvent(player, itemStack, hand, itemUseTime);
EventDispatcher.call(playerPreEatEvent);
if (playerPreEatEvent.isCancelled()) return;
itemUseTime = playerPreEatEvent.getEatingTime();
} else {
itemAnimationType = PlayerItemAnimationEvent.ItemAnimationType.OTHER;
}
if (itemUseTime != 0) {
player.refreshItemUse(hand, itemUseTime);
PlayerItemAnimationEvent playerItemAnimationEvent = new PlayerItemAnimationEvent(player, itemAnimationType, hand);
EventDispatcher.callCancellable(playerItemAnimationEvent, () -> {
player.refreshActiveHand(true, hand == PlayerHand.OFF, false);
player.sendPacketToViewers(player.getMetadataPacket());
});
}
}
private static int defaultUseItemTime(@NotNull ItemStack itemStack) {
final Consumable consumable = itemStack.get(ItemComponent.CONSUMABLE);
return consumable != null ? consumable.consumeTicks() : 0;
private static int getInstrumentTime(@NotNull ItemStack itemStack) {
final String instrumentName = itemStack.get(ItemComponent.INSTRUMENT);
if (instrumentName == null) return 0;
// TODO(1.21.2): Load instrument registry
return 0;
}
}