package net.minecraft.world.entity.projectile; import com.google.common.collect.Lists; import com.mojang.serialization.Codec; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import java.util.List; import java.util.Objects; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; import net.minecraft.core.component.DataComponents; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; import net.minecraft.network.protocol.game.ClientboundGameEventPacket; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.network.syncher.SynchedEntityData.Builder; import net.minecraft.resources.RegistryOps; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.tags.EntityTypeTags; import net.minecraft.util.Mth; import net.minecraft.util.Unit; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.MoverType; import net.minecraft.world.entity.OminousItemSpawner; import net.minecraft.world.entity.SlotAccess; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.ClipContext.Block; import net.minecraft.world.level.ClipContext.Fluid; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.HitResult.Type; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; public abstract class AbstractArrow extends Projectile { private static final double ARROW_BASE_DAMAGE = 2.0; private static final int SHAKE_TIME = 7; private static final float WATER_INERTIA = 0.6F; private static final float INERTIA = 0.99F; private static final short DEFAULT_LIFE = 0; private static final byte DEFAULT_SHAKE = 0; private static final boolean DEFAULT_IN_GROUND = false; private static final boolean DEFAULT_CRIT = false; private static final byte DEFAULT_PIERCE_LEVEL = 0; private static final EntityDataAccessor ID_FLAGS = SynchedEntityData.defineId(AbstractArrow.class, EntityDataSerializers.BYTE); private static final EntityDataAccessor PIERCE_LEVEL = SynchedEntityData.defineId(AbstractArrow.class, EntityDataSerializers.BYTE); private static final EntityDataAccessor IN_GROUND = SynchedEntityData.defineId(AbstractArrow.class, EntityDataSerializers.BOOLEAN); private static final int FLAG_CRIT = 1; private static final int FLAG_NOPHYSICS = 2; @Nullable private BlockState lastState; protected int inGroundTime; public AbstractArrow.Pickup pickup = AbstractArrow.Pickup.DISALLOWED; public int shakeTime = 0; private int life = 0; private double baseDamage = 2.0; private SoundEvent soundEvent = this.getDefaultHitGroundSoundEvent(); @Nullable private IntOpenHashSet piercingIgnoreEntityIds; @Nullable private List piercedAndKilledEntities; private ItemStack pickupItemStack = this.getDefaultPickupItem(); @Nullable private ItemStack firedFromWeapon = null; protected AbstractArrow(EntityType entityType, Level level) { super(entityType, level); } protected AbstractArrow( EntityType entityType, double x, double y, double z, Level level, ItemStack pickupItemStack, @Nullable ItemStack firedFromWeapon ) { this(entityType, level); this.pickupItemStack = pickupItemStack.copy(); this.applyComponentsFromItemStack(pickupItemStack); Unit unit = pickupItemStack.remove(DataComponents.INTANGIBLE_PROJECTILE); if (unit != null) { this.pickup = AbstractArrow.Pickup.CREATIVE_ONLY; } this.setPos(x, y, z); if (firedFromWeapon != null && level instanceof ServerLevel serverLevel) { if (firedFromWeapon.isEmpty()) { throw new IllegalArgumentException("Invalid weapon firing an arrow"); } this.firedFromWeapon = firedFromWeapon.copy(); int i = EnchantmentHelper.getPiercingCount(serverLevel, firedFromWeapon, this.pickupItemStack); if (i > 0) { this.setPierceLevel((byte)i); } } } protected AbstractArrow( EntityType entityType, LivingEntity owner, Level level, ItemStack pickupItemStack, @Nullable ItemStack firedFromWeapon ) { this(entityType, owner.getX(), owner.getEyeY() - 0.1F, owner.getZ(), level, pickupItemStack, firedFromWeapon); this.setOwner(owner); } public void setSoundEvent(SoundEvent soundEvent) { this.soundEvent = soundEvent; } @Override public boolean shouldRenderAtSqrDistance(double distance) { double d = this.getBoundingBox().getSize() * 10.0; if (Double.isNaN(d)) { d = 1.0; } d *= 64.0 * getViewScale(); return distance < d * d; } @Override protected void defineSynchedData(Builder builder) { builder.define(ID_FLAGS, (byte)0); builder.define(PIERCE_LEVEL, (byte)0); builder.define(IN_GROUND, false); } @Override public void shoot(double x, double y, double z, float velocity, float inaccuracy) { super.shoot(x, y, z, velocity, inaccuracy); this.life = 0; } @Override public void lerpMotion(double x, double y, double z) { super.lerpMotion(x, y, z); this.life = 0; if (this.isInGround() && Mth.lengthSquared(x, y, z) > 0.0) { this.setInGround(false); } } @Override public void onSyncedDataUpdated(EntityDataAccessor dataAccessor) { super.onSyncedDataUpdated(dataAccessor); if (!this.firstTick && this.shakeTime <= 0 && dataAccessor.equals(IN_GROUND) && this.isInGround()) { this.shakeTime = 7; } } @Override public void tick() { boolean bl = !this.isNoPhysics(); Vec3 vec3 = this.getDeltaMovement(); BlockPos blockPos = this.blockPosition(); BlockState blockState = this.level().getBlockState(blockPos); if (!blockState.isAir() && bl) { VoxelShape voxelShape = blockState.getCollisionShape(this.level(), blockPos); if (!voxelShape.isEmpty()) { Vec3 vec32 = this.position(); for (AABB aABB : voxelShape.toAabbs()) { if (aABB.move(blockPos).contains(vec32)) { this.setDeltaMovement(Vec3.ZERO); this.setInGround(true); break; } } } } if (this.shakeTime > 0) { this.shakeTime--; } if (this.isInWaterOrRain() || blockState.is(Blocks.POWDER_SNOW)) { this.clearFire(); } if (this.isInGround() && bl) { if (!this.level().isClientSide()) { if (this.lastState != blockState && this.shouldFall()) { this.startFalling(); } else { this.tickDespawn(); } } this.inGroundTime++; if (this.isAlive()) { this.applyEffectsFromBlocks(); } if (!this.level().isClientSide) { this.setSharedFlagOnFire(this.getRemainingFireTicks() > 0); } } else { this.inGroundTime = 0; Vec3 vec33 = this.position(); if (this.isInWater()) { this.applyInertia(this.getWaterInertia()); this.addBubbleParticles(vec33); } if (this.isCritArrow()) { for (int i = 0; i < 4; i++) { this.level() .addParticle(ParticleTypes.CRIT, vec33.x + vec3.x * i / 4.0, vec33.y + vec3.y * i / 4.0, vec33.z + vec3.z * i / 4.0, -vec3.x, -vec3.y + 0.2, -vec3.z); } } float f; if (!bl) { f = (float)(Mth.atan2(-vec3.x, -vec3.z) * 180.0F / (float)Math.PI); } else { f = (float)(Mth.atan2(vec3.x, vec3.z) * 180.0F / (float)Math.PI); } float g = (float)(Mth.atan2(vec3.y, vec3.horizontalDistance()) * 180.0F / (float)Math.PI); this.setXRot(lerpRotation(this.getXRot(), g)); this.setYRot(lerpRotation(this.getYRot(), f)); if (bl) { BlockHitResult blockHitResult = this.level().clipIncludingBorder(new ClipContext(vec33, vec33.add(vec3), Block.COLLIDER, Fluid.NONE, this)); this.stepMoveAndHit(blockHitResult); } else { this.setPos(vec33.add(vec3)); this.applyEffectsFromBlocks(); } if (!this.isInWater()) { this.applyInertia(0.99F); } if (bl && !this.isInGround()) { this.applyGravity(); } super.tick(); } } private void stepMoveAndHit(BlockHitResult hitResult) { while (this.isAlive()) { Vec3 vec3 = this.position(); EntityHitResult entityHitResult = this.findHitEntity(vec3, hitResult.getLocation()); Vec3 vec32 = ((HitResult)Objects.requireNonNullElse(entityHitResult, hitResult)).getLocation(); this.setPos(vec32); this.applyEffectsFromBlocks(vec3, vec32); if (this.portalProcess != null && this.portalProcess.isInsidePortalThisTick()) { this.handlePortal(); } if (entityHitResult == null) { if (this.isAlive() && hitResult.getType() != Type.MISS) { this.hitTargetOrDeflectSelf(hitResult); this.hasImpulse = true; } break; } else if (this.isAlive() && !this.noPhysics) { ProjectileDeflection projectileDeflection = this.hitTargetOrDeflectSelf(entityHitResult); this.hasImpulse = true; if (this.getPierceLevel() > 0 && projectileDeflection == ProjectileDeflection.NONE) { continue; } break; } } } private void applyInertia(float intertia) { Vec3 vec3 = this.getDeltaMovement(); this.setDeltaMovement(vec3.scale(intertia)); } private void addBubbleParticles(Vec3 pos) { Vec3 vec3 = this.getDeltaMovement(); for (int i = 0; i < 4; i++) { float f = 0.25F; this.level().addParticle(ParticleTypes.BUBBLE, pos.x - vec3.x * 0.25, pos.y - vec3.y * 0.25, pos.z - vec3.z * 0.25, vec3.x, vec3.y, vec3.z); } } @Override protected double getDefaultGravity() { return 0.05; } private boolean shouldFall() { return this.isInGround() && this.level().noCollision(new AABB(this.position(), this.position()).inflate(0.06)); } private void startFalling() { this.setInGround(false); Vec3 vec3 = this.getDeltaMovement(); this.setDeltaMovement(vec3.multiply(this.random.nextFloat() * 0.2F, this.random.nextFloat() * 0.2F, this.random.nextFloat() * 0.2F)); this.life = 0; } protected boolean isInGround() { return this.entityData.get(IN_GROUND); } protected void setInGround(boolean inGround) { this.entityData.set(IN_GROUND, inGround); } @Override public boolean isPushedByFluid() { return !this.isInGround(); } @Override public void move(MoverType type, Vec3 movement) { super.move(type, movement); if (type != MoverType.SELF && this.shouldFall()) { this.startFalling(); } } protected void tickDespawn() { this.life++; if (this.life >= 1200) { this.discard(); } } private void resetPiercedEntities() { if (this.piercedAndKilledEntities != null) { this.piercedAndKilledEntities.clear(); } if (this.piercingIgnoreEntityIds != null) { this.piercingIgnoreEntityIds.clear(); } } @Override protected void onItemBreak(Item item) { this.firedFromWeapon = null; } @Override public void onAboveBubbleColumn(boolean downwards, BlockPos pos) { if (!this.isInGround()) { super.onAboveBubbleColumn(downwards, pos); } } @Override public void onInsideBubbleColumn(boolean downwards) { if (!this.isInGround()) { super.onInsideBubbleColumn(downwards); } } @Override public void push(double x, double y, double z) { if (!this.isInGround()) { super.push(x, y, z); } } @Override protected void onHitEntity(EntityHitResult result) { super.onHitEntity(result); Entity entity = result.getEntity(); float f = (float)this.getDeltaMovement().length(); double d = this.baseDamage; Entity entity2 = this.getOwner(); DamageSource damageSource = this.damageSources().arrow(this, (Entity)(entity2 != null ? entity2 : this)); if (this.getWeaponItem() != null && this.level() instanceof ServerLevel serverLevel) { d = EnchantmentHelper.modifyDamage(serverLevel, this.getWeaponItem(), entity, damageSource, (float)d); } int i = Mth.ceil(Mth.clamp(f * d, 0.0, 2.147483647E9)); if (this.getPierceLevel() > 0) { if (this.piercingIgnoreEntityIds == null) { this.piercingIgnoreEntityIds = new IntOpenHashSet(5); } if (this.piercedAndKilledEntities == null) { this.piercedAndKilledEntities = Lists.newArrayListWithCapacity(5); } if (this.piercingIgnoreEntityIds.size() >= this.getPierceLevel() + 1) { this.discard(); return; } this.piercingIgnoreEntityIds.add(entity.getId()); } if (this.isCritArrow()) { long l = this.random.nextInt(i / 2 + 2); i = (int)Math.min(l + i, 2147483647L); } if (entity2 instanceof LivingEntity livingEntity) { livingEntity.setLastHurtMob(entity); } boolean bl = entity.getType() == EntityType.ENDERMAN; int j = entity.getRemainingFireTicks(); if (this.isOnFire() && !bl) { entity.igniteForSeconds(5.0F); } if (entity.hurtOrSimulate(damageSource, i)) { if (bl) { return; } if (entity instanceof LivingEntity livingEntity2) { if (!this.level().isClientSide && this.getPierceLevel() <= 0) { livingEntity2.setArrowCount(livingEntity2.getArrowCount() + 1); } this.doKnockback(livingEntity2, damageSource); if (this.level() instanceof ServerLevel serverLevel2) { EnchantmentHelper.doPostAttackEffectsWithItemSource(serverLevel2, livingEntity2, damageSource, this.getWeaponItem()); } this.doPostHurtEffects(livingEntity2); if (livingEntity2 instanceof Player && entity2 instanceof ServerPlayer serverPlayer && !this.isSilent() && livingEntity2 != serverPlayer) { serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.PLAY_ARROW_HIT_SOUND, 0.0F)); } if (!entity.isAlive() && this.piercedAndKilledEntities != null) { this.piercedAndKilledEntities.add(livingEntity2); } if (!this.level().isClientSide && entity2 instanceof ServerPlayer serverPlayer) { if (this.piercedAndKilledEntities != null) { CriteriaTriggers.KILLED_BY_ARROW.trigger(serverPlayer, this.piercedAndKilledEntities, this.firedFromWeapon); } else if (!entity.isAlive()) { CriteriaTriggers.KILLED_BY_ARROW.trigger(serverPlayer, List.of(entity), this.firedFromWeapon); } } } this.playSound(this.soundEvent, 1.0F, 1.2F / (this.random.nextFloat() * 0.2F + 0.9F)); if (this.getPierceLevel() <= 0) { this.discard(); } } else { entity.setRemainingFireTicks(j); this.deflect(ProjectileDeflection.REVERSE, entity, this.getOwner(), false); this.setDeltaMovement(this.getDeltaMovement().scale(0.2)); if (this.level() instanceof ServerLevel serverLevel3 && this.getDeltaMovement().lengthSqr() < 1.0E-7) { if (this.pickup == AbstractArrow.Pickup.ALLOWED) { this.spawnAtLocation(serverLevel3, this.getPickupItem(), 0.1F); } this.discard(); } } } protected void doKnockback(LivingEntity entity, DamageSource damageSource) { double d = this.firedFromWeapon != null && this.level() instanceof ServerLevel serverLevel ? EnchantmentHelper.modifyKnockback(serverLevel, this.firedFromWeapon, entity, damageSource, 0.0F) : 0.0F; if (d > 0.0) { double e = Math.max(0.0, 1.0 - entity.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE)); Vec3 vec3 = this.getDeltaMovement().multiply(1.0, 0.0, 1.0).normalize().scale(d * 0.6 * e); if (vec3.lengthSqr() > 0.0) { entity.push(vec3.x, 0.1, vec3.z); } } } @Override protected void onHitBlock(BlockHitResult result) { this.lastState = this.level().getBlockState(result.getBlockPos()); super.onHitBlock(result); ItemStack itemStack = this.getWeaponItem(); if (this.level() instanceof ServerLevel serverLevel && itemStack != null) { this.hitBlockEnchantmentEffects(serverLevel, result, itemStack); } Vec3 vec3 = this.getDeltaMovement(); Vec3 vec32 = new Vec3(Math.signum(vec3.x), Math.signum(vec3.y), Math.signum(vec3.z)); Vec3 vec33 = vec32.scale(0.05F); this.setPos(this.position().subtract(vec33)); this.setDeltaMovement(Vec3.ZERO); this.playSound(this.getHitGroundSoundEvent(), 1.0F, 1.2F / (this.random.nextFloat() * 0.2F + 0.9F)); this.setInGround(true); this.shakeTime = 7; this.setCritArrow(false); this.setPierceLevel((byte)0); this.setSoundEvent(SoundEvents.ARROW_HIT); this.resetPiercedEntities(); } protected void hitBlockEnchantmentEffects(ServerLevel level, BlockHitResult hitResult, ItemStack stack) { Vec3 vec3 = hitResult.getBlockPos().clampLocationWithin(hitResult.getLocation()); EnchantmentHelper.onHitBlock( level, stack, this.getOwner() instanceof LivingEntity livingEntity ? livingEntity : null, this, null, vec3, level.getBlockState(hitResult.getBlockPos()), item -> this.firedFromWeapon = null ); } @Override public ItemStack getWeaponItem() { return this.firedFromWeapon; } /** * The sound made when an entity is hit by this projectile */ protected SoundEvent getDefaultHitGroundSoundEvent() { return SoundEvents.ARROW_HIT; } protected final SoundEvent getHitGroundSoundEvent() { return this.soundEvent; } protected void doPostHurtEffects(LivingEntity target) { } /** * Gets the EntityRayTraceResult representing the entity hit */ @Nullable protected EntityHitResult findHitEntity(Vec3 startVec, Vec3 endVec) { return ProjectileUtil.getEntityHitResult( this.level(), this, startVec, endVec, this.getBoundingBox().expandTowards(this.getDeltaMovement()).inflate(1.0), this::canHitEntity ); } @Override protected boolean canHitEntity(Entity target) { return target instanceof Player && this.getOwner() instanceof Player player && !player.canHarmPlayer((Player)target) ? false : super.canHitEntity(target) && (this.piercingIgnoreEntityIds == null || !this.piercingIgnoreEntityIds.contains(target.getId())); } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); tag.putShort("life", (short)this.life); tag.storeNullable("inBlockState", BlockState.CODEC, registryOps, this.lastState); tag.putByte("shake", (byte)this.shakeTime); tag.putBoolean("inGround", this.isInGround()); tag.store("pickup", AbstractArrow.Pickup.LEGACY_CODEC, this.pickup); tag.putDouble("damage", this.baseDamage); tag.putBoolean("crit", this.isCritArrow()); tag.putByte("PierceLevel", this.getPierceLevel()); tag.store("SoundEvent", BuiltInRegistries.SOUND_EVENT.byNameCodec(), this.soundEvent); tag.store("item", ItemStack.CODEC, registryOps, this.pickupItemStack); tag.storeNullable("weapon", ItemStack.CODEC, registryOps, this.firedFromWeapon); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); this.life = tag.getShortOr("life", (short)0); this.lastState = (BlockState)tag.read("inBlockState", BlockState.CODEC, registryOps).orElse(null); this.shakeTime = tag.getByteOr("shake", (byte)0) & 255; this.setInGround(tag.getBooleanOr("inGround", false)); this.baseDamage = tag.getDoubleOr("damage", 2.0); this.pickup = (AbstractArrow.Pickup)tag.read("pickup", AbstractArrow.Pickup.LEGACY_CODEC).orElse(AbstractArrow.Pickup.DISALLOWED); this.setCritArrow(tag.getBooleanOr("crit", false)); this.setPierceLevel(tag.getByteOr("PierceLevel", (byte)0)); this.soundEvent = (SoundEvent)tag.read("SoundEvent", BuiltInRegistries.SOUND_EVENT.byNameCodec()).orElse(this.getDefaultHitGroundSoundEvent()); this.setPickupItemStack((ItemStack)tag.read("item", ItemStack.CODEC, registryOps).orElse(this.getDefaultPickupItem())); this.firedFromWeapon = (ItemStack)tag.read("weapon", ItemStack.CODEC, registryOps).orElse(null); } @Override public void setOwner(@Nullable Entity owner) { super.setOwner(owner); this.pickup = switch (owner) { case Player player when this.pickup == AbstractArrow.Pickup.DISALLOWED -> AbstractArrow.Pickup.ALLOWED; case OminousItemSpawner ominousItemSpawner -> AbstractArrow.Pickup.DISALLOWED; case null, default -> this.pickup; }; } @Override public void playerTouch(Player player) { if (!this.level().isClientSide && (this.isInGround() || this.isNoPhysics()) && this.shakeTime <= 0) { if (this.tryPickup(player)) { player.take(this, 1); this.discard(); } } } protected boolean tryPickup(Player player) { return switch (this.pickup) { case DISALLOWED -> false; case ALLOWED -> player.getInventory().add(this.getPickupItem()); case CREATIVE_ONLY -> player.hasInfiniteMaterials(); }; } protected ItemStack getPickupItem() { return this.pickupItemStack.copy(); } protected abstract ItemStack getDefaultPickupItem(); @Override protected Entity.MovementEmission getMovementEmission() { return Entity.MovementEmission.NONE; } public ItemStack getPickupItemStackOrigin() { return this.pickupItemStack; } public void setBaseDamage(double baseDamage) { this.baseDamage = baseDamage; } @Override public boolean isAttackable() { return this.getType().is(EntityTypeTags.REDIRECTABLE_PROJECTILE); } /** * Whether the arrow has a stream of critical hit particles flying behind it. */ public void setCritArrow(boolean critArrow) { this.setFlag(1, critArrow); } private void setPierceLevel(byte pierceLevel) { this.entityData.set(PIERCE_LEVEL, pierceLevel); } private void setFlag(int id, boolean value) { byte b = this.entityData.get(ID_FLAGS); if (value) { this.entityData.set(ID_FLAGS, (byte)(b | id)); } else { this.entityData.set(ID_FLAGS, (byte)(b & ~id)); } } protected void setPickupItemStack(ItemStack pickupItemStack) { if (!pickupItemStack.isEmpty()) { this.pickupItemStack = pickupItemStack; } else { this.pickupItemStack = this.getDefaultPickupItem(); } } /** * Whether the arrow has a stream of critical hit particles flying behind it. */ public boolean isCritArrow() { byte b = this.entityData.get(ID_FLAGS); return (b & 1) != 0; } public byte getPierceLevel() { return this.entityData.get(PIERCE_LEVEL); } public void setBaseDamageFromMob(float velocity) { this.setBaseDamage(velocity * 2.0F + this.random.triangle(this.level().getDifficulty().getId() * 0.11, 0.57425)); } protected float getWaterInertia() { return 0.6F; } /** * Sets if this arrow can noClip */ public void setNoPhysics(boolean noPhysics) { this.noPhysics = noPhysics; this.setFlag(2, noPhysics); } /** * Whether the arrow can noClip */ public boolean isNoPhysics() { return !this.level().isClientSide ? this.noPhysics : (this.entityData.get(ID_FLAGS) & 2) != 0; } @Override public boolean isPickable() { return super.isPickable() && !this.isInGround(); } @Override public SlotAccess getSlot(int slot) { return slot == 0 ? SlotAccess.of(this::getPickupItemStackOrigin, this::setPickupItemStack) : super.getSlot(slot); } @Override protected boolean shouldBounceOnWorldBorder() { return true; } public static enum Pickup { DISALLOWED, ALLOWED, CREATIVE_ONLY; public static final Codec LEGACY_CODEC = Codec.BYTE.xmap(AbstractArrow.Pickup::byOrdinal, pickup -> (byte)pickup.ordinal()); public static AbstractArrow.Pickup byOrdinal(int ordinal) { if (ordinal < 0 || ordinal > values().length) { ordinal = 0; } return values()[ordinal]; } } }