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);
}
}
}
}