Skip to content

Technical Changes

Player Statistics

Ensures statistics are still marked as dirty and saved properly, even when Bukkit stat saving is disabled.

class ServerStatsCounter {

    @Override
    public void setValue(Player player, Stat<?> stat, int value) {
        // if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot
        if (stat.getType() == Stats.CUSTOM
                && stat.getValue() instanceof final ResourceLocation resourceLocation
                && org.spigotmc.SpigotConfig.forcedStats.get(resourceLocation) != null) {
            return; // Paper - disable saving forced stats
        }

        super.setValue(player, stat, value);
        this.dirty.add(stat);
    }

}

Respawn Anchor

Respawn anchors no longer explode when used in invalid dimensions.

class RespawnAnchorBlock {

    @Override
    protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
        if ((Integer) state.getValue(RespawnAnchorBlock.CHARGE) == 0) {
            return InteractionResult.PASS;
        } else if (!RespawnAnchorBlock.canSetSpawn(world)) {
            /* Mixel disable
            if (!world.isClientSide) {
                this.explode(state, world, pos);
            }
            */

            return InteractionResult.sidedSuccess(world.isClientSide);
        } else {
            if (!world.isClientSide) {
                ServerPlayer entityplayer = (ServerPlayer) player;

                if (entityplayer.getRespawnDimension() != world.dimension() || !pos.equals(entityplayer.getRespawnPosition())) {
                    if (entityplayer.setRespawnPosition(world.dimension(), pos, 0.0F, false, true, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.RESPAWN_ANCHOR)) {
                        world.playSound(null, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, SoundEvents.RESPAWN_ANCHOR_SET_SPAWN, SoundSource.BLOCKS, 1.0F, 1.0F);
                        return InteractionResult.SUCCESS;
                    } else {
                        return InteractionResult.FAIL;
                    }
                }
            }

            return InteractionResult.CONSUME;
        }
    }

}

Guardian Effect Fix

Allows cancelling the elder guardian effect via a custom event before the packet is sent.

class ElderGuardian {

    @Override
    protected void customServerAiStep() {
        super.customServerAiStep();
        if ((this.tickCount + this.getId()) % 1200 == 0) {
            MobEffectInstance effect = new MobEffectInstance(MobEffects.DIG_SLOWDOWN, 6000, 2);
            List<ServerPlayer> effectCancelled = new ArrayList<>();

            List<ServerPlayer> list = MobEffectUtil.addEffectToPlayersAround(
                    (ServerLevel) this.level(),
                    this,
                    this.position(),
                    50.0D,
                    effect,
                    1200,
                    org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK,
                    (player) -> {
                        boolean cancelled = new ElderGuardianAppearanceEvent(
                                (org.bukkit.entity.ElderGuardian) this,
                                player.getBukkitEntity()
                        ).callEvent();

                        if (cancelled) effectCancelled.add(player);
                        return cancelled;
                    }
            );

            list.forEach((entityplayer) -> {
                if (!effectCancelled.contains(entityplayer)) {
                    entityplayer.connection.send(new ClientboundGameEventPacket(
                            ClientboundGameEventPacket.GUARDIAN_ELDER_EFFECT,
                            this.isSilent() ? 0.0F : 1.0F
                    ));
                }
            });
        }

        if (!this.hasRestriction()) {
            this.restrictTo(this.blockPosition(), 16);
        }
    }

}

Advancement Location Fix

Preserves custom advancement x/y positions and prevents them from being overwritten again during loading.

class DisplayInfo {

    public void setLocation(float x, float y) {
        // Mixel Start
        // We do this, because this method gets called and overwrites
        // our set x and y on advancement load
        // this.x = x;
        // this.y = y;
        // Mixel End
    }

}

Block Attached Entity

Item frames and glow item frames are excluded from the normal hanging entity survival removal logic.

class BlockAttachedEntity {

    @Override
    public void tick() {
        if (!this.level().isClientSide) {
            this.checkBelowWorld();

            if (this instanceof GlowItemFrame || this instanceof ItemFrame) return;

            if (this.checkInterval++ == this.level().spigotConfig.hangingTickFrequency) {
                this.checkInterval = 0;
                if (!this.isRemoved() && !this.survives()) {
                    BlockState material = this.level().getBlockState(this.blockPosition());
                    HangingBreakEvent.RemoveCause cause = !material.isAir()
                            ? HangingBreakEvent.RemoveCause.OBSTRUCTION
                            : HangingBreakEvent.RemoveCause.PHYSICS;

                    HangingBreakEvent event = new HangingBreakEvent((Hanging) this.getBukkitEntity(), cause);
                    this.level().getCraftServer().getPluginManager().callEvent(event);

                    if (this.isRemoved() || event.isCancelled()) {
                        return;
                    }

                    this.discard(EntityRemoveEvent.Cause.DROP);
                    this.dropItem(null);
                }
            }
        }
    }

}

Thrown Enderpearl

Ender pearls no longer deal damage after teleport.

class ThrownEnderpearl {

    @Override
    protected void onHit(HitResult hitResult) {
        super.onHit(hitResult);

        Level world = this.level();

        if (world instanceof ServerLevel worldserver) {
            if (!this.isRemoved()) {
                Entity entity = this.getOwner();

                if (entity != null && ThrownEnderpearl.isAllowedToTeleportOwner(entity, worldserver)) {
                    if (entity instanceof ServerPlayer entityplayer) {
                        entity.resetFallDistance();
                        entityplayer.resetCurrentImpulseContext();

                        // Mixel start
                        // entity.hurt(this.damageSources().fall().customEventDamager(this), 5.0F);
                        // Mixel end

                        this.playSound(worldserver, this.position());
                    }

                    this.discard(EntityRemoveEvent.Cause.HIT);
                    return;
                }

                this.discard(EntityRemoveEvent.Cause.HIT);
            }
        }
    }

}

SpawnUtil

Sets the creaking home position before the spawn event is fired. This is needed for plot protection logic.

class SpawnUtil {

    public static <T extends Mob> Optional<T> trySpawnMob(...) {
        T mob = (T) entityType.create(level, null, mutableBlockPos, spawnReason, false, false);
        if (mob != null) {
            if (mob.checkSpawnRules(level, spawnReason) && mob.checkSpawnObstruction(level)) {
                if (entityType.equals(net.minecraft.world.entity.EntityType.CREAKING)) {
                    if (mob instanceof net.minecraft.world.entity.monster.creaking.Creaking creaking) {
                        creaking.setHomePos(pos);
                    }
                }

                level.addFreshEntityWithPassengers(mob, reason);
                if (mob.isRemoved()) return Optional.empty();
                mob.playAmbientSound();
                return Optional.of(mob);
            }

            mob.discard(null);
        }

        return Optional.empty();
    }

}

ServerExplosion

Adds custom handling for explosions where the affected block list is still needed even if blocks are not destroyed.

class ServerExplosion {

    public void explode() {
        List<BlockPos> list = this.calculateExplodedPositions();
        this.hurtEntities();

        if (this.interactsWithBlocks()) {
            this.interactWithBlocks(list);
        } else {
            this.interactWithBlocksOnKeep(list);
        }
    }

    private void interactWithBlocksOnKeep(List<BlockPos> blocks) {
        Util.shuffle(blocks, this.level.random);

        Location location = CraftLocation.toBukkit(this.center, this.level.getWorld());
        List<org.bukkit.block.Block> blockList = new ObjectArrayList<>();

        for (int i = blocks.size() - 1; i >= 0; i--) {
            org.bukkit.block.Block bblock = org.bukkit.craftbukkit.block.CraftBlock.at(this.level, blocks.get(i));
            if (!bblock.getType().isAir()) {
                blockList.add(bblock);
            }
        }

        if (this.source != null) {
            CraftEventFactory.callEntityExplodeEvent(this.source, blockList, this.yield, this.getBlockInteraction());
        } else {
            org.bukkit.block.Block block = location.getBlock();
            org.bukkit.block.BlockState blockState = (this.damageSource.causingBlockSnapshot() != null)
                    ? this.damageSource.causingBlockSnapshot()
                    : block.getState();

            CraftEventFactory.callBlockExplodeEvent(block, blockState, blockList, this.yield, this.getBlockInteraction());
        }
    }

}

CraftMetaAxolotlBucket

Fixes variant loading for axolotl buckets.

class CraftMetaAxolotlBucket {

    CraftMetaAxolotlBucket(DataComponentPatch tag, Set<DataComponentType<?>> extraHandledDcts) {
        super(tag, extraHandledDcts);

        if (variant == null) {
            Optional<? extends net.minecraft.world.entity.animal.axolotl.Axolotl.Variant> optional = tag.get(DataComponents.AXOLOTL_VARIANT);
            if (optional != null && optional.isPresent()) {
                net.minecraft.world.entity.animal.axolotl.Axolotl.Variant variant = optional.get();
                this.variant = variant.getId();
            }
        }
    }

}

CraftMetaTropicalFishBucket

Fixes variant loading for tropical fish buckets.

class CraftMetaTropicalFishBucket {

    CraftMetaTropicalFishBucket(DataComponentPatch tag, Set<DataComponentType<?>> extraHandledDcts) {
        super(tag, extraHandledDcts);

        if (variant == null) {
            final Optional<? extends net.minecraft.world.item.DyeColor> optionalPatternColor =
                    tag.get(DataComponents.TROPICAL_FISH_PATTERN_COLOR);
            final Optional<? extends net.minecraft.world.item.DyeColor> optionalBaseDyeColor =
                    tag.get(DataComponents.TROPICAL_FISH_BASE_COLOR);
            final Optional<? extends net.minecraft.world.entity.animal.TropicalFish.Pattern> optionalPattern =
                    tag.get(DataComponents.TROPICAL_FISH_PATTERN);

            if (optionalPatternColor != null && optionalPatternColor.isPresent()
                    && optionalBaseDyeColor != null && optionalBaseDyeColor.isPresent()
                    && optionalPattern != null && optionalPattern.isPresent()) {
                net.minecraft.world.item.DyeColor patternColor = optionalPatternColor.get();
                net.minecraft.world.item.DyeColor baseDyeColor = optionalBaseDyeColor.get();
                net.minecraft.world.entity.animal.TropicalFish.Pattern pattern = optionalPattern.get();

                org.bukkit.DyeColor bukkitPatternColor = org.bukkit.DyeColor.valueOf(patternColor.name());
                org.bukkit.DyeColor bukkitBaseColor = org.bukkit.DyeColor.valueOf(baseDyeColor.name());

                this.variant = bukkitPatternColor.getWoolData() << 24
                        | bukkitBaseColor.getWoolData() << 16
                        | pattern.getPackedId();
            }
        }
    }

}

Item Merge Logic

Items always merge into the current entity instead of depending on stack size. This was changed from 1.21.2 to 1.21.8 and players noticed it - destroyed some farms.

class ItemEntity {

    void tryToMerge(ItemEntity itemEntity) {
        ItemStack item = this.getItem();
        ItemStack item1 = itemEntity.getItem();
        if (Objects.equals(this.target, itemEntity.target) && areMergable(item, item1)) {
            if (item1.getCount() < item.getCount()) {
                merge(this, item, itemEntity, item1);
            } else {
               merge(itemEntity, item1, this, item);
            }
        }
    }
}
class ItemEntity {

    void tryToMerge(ItemEntity other) {
        ItemStack itemstack = this.getItem();
        ItemStack itemstack1 = other.getItem();

        if (Objects.equals(this.target, other.target) && ItemEntity.areMergable(itemstack, itemstack1)) {
            if (true || itemstack1.getCount() < itemstack.getCount()) { // Spigot
                ItemEntity.merge(this, itemstack, other, itemstack1);
            } else {
                ItemEntity.merge(other, itemstack1, this, itemstack);
            }
        }
    }

}