Skip to content

Instantly share code, notes, and snippets.

@InfiniteCoder01
Last active March 9, 2026 17:11
Show Gist options
  • Select an option

  • Save InfiniteCoder01/701e6f871d46fc21b1875a9fbea96893 to your computer and use it in GitHub Desktop.

Select an option

Save InfiniteCoder01/701e6f871d46fc21b1875a9fbea96893 to your computer and use it in GitHub Desktop.
Fixing minestom sounds and other sync issues. Not accurate, but good enough for me. Licenced under CC0 (https://creativecommons.org/publicdomain/zero/1.0/), so feel free to use any of this code wherever you want!
/*
Registers global event node named "player-actions", forwarding many sounds to other players (by default, other players don't even hear footsteps!),
implementing some basic features like applying item attributes, a gamemode switcher, etc.
To use it, just call `PlayerActions.init();` somewhere after MinecraftServer has been initialized.
*/
package events;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import net.kyori.adventure.sound.Sound;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.AdventurePacketConvertor;
import net.minestom.server.component.DataComponents;
import net.minestom.server.coordinate.*;
import net.minestom.server.entity.EntityPose;
import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.entity.EquipmentSlotGroup;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.attribute.AttributeModifier;
import net.minestom.server.event.EventFilter;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.item.EntityEquipEvent;
import net.minestom.server.event.player.*;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler.PlayerPlacement;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.component.AttributeList;
import net.minestom.server.item.component.EnchantmentList;
import net.minestom.server.item.enchant.AttributeEffect;
import net.minestom.server.network.packet.client.play.ClientPlayerActionPacket;
import net.minestom.server.network.packet.server.play.BlockBreakAnimationPacket;
import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.PacketSendingUtils;
import net.minestom.server.utils.block.BlockBreakCalculation;
public class PlayerActions {
private static final Map<Player, PlayerData> playerData = new ConcurrentHashMap<>();
private static class PlayerData {
boolean lastOnGround = false;
boolean lastInLiquid = false;
Task diggingSound = null;
Task diggingProgressTask = null;
double diggingProgress = 0.0;
double distanceUntilNextSound = 0.0;
double fallHighestY = 0.0;
}
public static void init() {
final var events = MinecraftServer.getGlobalEventHandler().addChild(EventNode.type("player-actions", EventFilter.PLAYER));
events.addListener(PlayerDisconnectEvent.class, event -> {
final var data = playerData.remove(event.getPlayer());
if (data != null) clearDigging(data);
});
events.addListener(PlayerMoveEvent.class, event -> {
final var player = event.getPlayer();
final var data = playerData.computeIfAbsent(player, p -> new PlayerData());
final var instance = event.getInstance();
final var inLiquid = instance.getBlock(player.getPosition()).registry().isLiquid();
final var inWater = instance.getBlock(player.getPosition()) == Block.WATER;
/// Swim mode
final var head = player.getPosition().add(0.0, player.getBoundingBox().height(), 0.0);
final var swimChange = player.isSprinting() && instance.getBlock(head).registry().isLiquid();
if (swimChange || event.getNewPosition().y() > player.getPreviousPosition().y())
player.getPlayerMeta().setSwimming(swimChange);
// Walk & Swim sounds
final var movedThisTick = event.getNewPosition().distance(player.getPreviousPosition());
data.distanceUntilNextSound -= Math.min(movedThisTick, MinecraftServer.TICK_MS / 250.0);
if (player.getPose() == EntityPose.SWIMMING && data.distanceUntilNextSound <= 0.0) {
if (inWater) {
final var sound = SoundEvent.ENTITY_PLAYER_SWIM;
play(player, player.getPosition(), sound, 0.05f, 1.0f);
}
data.distanceUntilNextSound = 2.0;
} else if (player.isOnGround() && movedThisTick > 0.0 && data.distanceUntilNextSound <= 0.0) {
final var block = event.getInstance().getBlock(event.getNewPosition().sub(0.0, 0.1, 0.0));
final var sound = block.registry().getBlockSoundType().stepSound();
play(player, player.getPosition(), sound, 0.15f, 1.0f);
data.distanceUntilNextSound = 0.9;
} else if (!player.isOnGround()) data.distanceUntilNextSound = 0.0; // sprintjumping
// Fall & Splash
final var fallDistance = data.fallHighestY - event.getNewPosition().y();
if (inLiquid) {
if (inWater && !data.lastInLiquid)
play(player, player.getPosition(), SoundEvent.ENTITY_PLAYER_SPLASH, 0.2f, 1.0f);
data.fallHighestY = event.getNewPosition().y();
} else if (player.isOnGround() && !data.lastOnGround) {
SoundEvent sound = null;
if (fallDistance > 4.0)
sound = fallDistance > 15.0 ? SoundEvent.ENTITY_PLAYER_BIG_FALL : SoundEvent.ENTITY_PLAYER_SMALL_FALL;
else {
final var block = event.getInstance().getBlock(event.getNewPosition().sub(0.0, 0.1, 0.0));
sound = block.registry().getBlockSoundType().fallSound();
}
if (fallDistance > 1.0)
play(player, player.getPosition(), sound, 1.0f, 1.0f);
}
if (!player.isOnGround()) {
data.fallHighestY = Math.max(data.fallHighestY, event.getNewPosition().y());
} else {
data.fallHighestY = event.getNewPosition().y();
}
// Update last state
data.lastOnGround = player.isOnGround();
data.lastInLiquid = inLiquid;
});
// Digging
events.addListener(PlayerPacketEvent.class, event -> {
final var player = event.getPlayer();
final var data = playerData.computeIfAbsent(player, p -> new PlayerData());
if (event.getPacket() instanceof ClientPlayerActionPacket packet) {
if (packet.status() == ClientPlayerActionPacket.Status.STARTED_DIGGING) {
final var block = event.getInstance().getBlock(packet.blockPosition());
if (block.isAir()) return;
startDigging(player, block, packet.blockPosition().asBlockVec());
} else if (packet.status() == ClientPlayerActionPacket.Status.CANCELLED_DIGGING) {
PacketSendingUtils.sendGroupedPacket(
player.getInstance().getChunkAt(packet.blockPosition()).getViewers(),
new BlockBreakAnimationPacket(player.getEntityId(), packet.blockPosition(), (byte)-1),
(p) -> p != player
);
clearDigging(data);
} else if (packet.status() == ClientPlayerActionPacket.Status.FINISHED_DIGGING) {
clearDigging(data);
}
}
});
events.addListener(PlayerBlockBreakEvent.class, event -> {
final var player = event.getPlayer();
final var data = playerData.get(player);
if (data != null) clearDigging(data); // TODO: Anticheat (actually, there is a comment in minestom's PlayerListener about it, so might as well do a PR)
});
// Block placement sound
events.addListener(PlayerBlockPlaceEvent.class, event -> {
final var player = event.getPlayer();
final var block = event.getBlock();
if (block.isAir()) return;
final var sound = block.registry().getBlockSoundType().placeSound();
play(player, event.getBlockPosition().asPos().add(0.5), sound, 1.0f, 1.0f);
});
// Block pickup (TODO: survival & adventure)
events.addListener(PlayerPickBlockEvent.class, event -> {
final var player = event.getPlayer();
final var block = event.getBlock();
if (block.isAir()) return;
if (player.getGameMode() == GameMode.CREATIVE)
player.setEquipment(EquipmentSlot.MAIN_HAND, ItemStack.of(block.registry().material()));
});
// Game mode switching
events.addListener(PlayerGameModeRequestEvent.class, event -> {
final var player = event.getPlayer();
if (player.getPermissionLevel() >= 2) {
player.setGameMode(event.getRequestedGameMode());
}
});
// Block replacing
events.addListener(PlayerBlockPlaceEvent.class, event -> {
final var instance = event.getInstance();
final var support = event.getBlockPosition().relative(event.getBlockFace().getOppositeFace());
final var supportBlock = instance.getBlock(support);
if (supportBlock.registry().isReplaceable() && !event.getBlock().registry().isReplaceable()) {
instance.placeBlock(new PlayerPlacement(
event.getBlock(),
supportBlock,
instance,
support,
event.getPlayer(),
event.getHand(),
event.getBlockFace().getOppositeFace(),
(float)support.x(),
(float)support.y(),
(float)support.z()
));
event.setBlock(instance.getBlock(event.getBlockPosition()));
event.setDoBlockUpdates(false);
}
});
// Player teleports to entity in specator mode using hotbar
events.addListener(PlayerSpectateEvent.class, event -> {
final var player = event.getPlayer();
final var target = event.getTarget();
if (player.getInstance() != target.getInstance())
player.setInstance(target.getInstance(), target.getPosition());
else player.teleport(target.getPosition());
});
// Make spectators invisible (FIXME: buggy with invisibility effect)
events.addListener(PlayerGameModeChangeEvent.class, event -> {
event.getPlayer().setInvisible(event.getNewGameMode() == GameMode.SPECTATOR);
});
// Attributes (should work with and without MinestomPvP)
events.addListener(EntityEquipEvent.class, event -> {
if (event.getEntity() instanceof LivingEntity entity)
updateEquipment(entity, entity.getEquipment(event.getSlot()), event.getEquippedItem(), event.getSlot());
});
// Required because changing held slot doesn't trigger equip event
events.addListener(PlayerChangeHeldSlotEvent.class, event -> updateEquipment(
event.getPlayer(),
event.getItemInOldSlot(),
event.getItemInNewSlot(),
EquipmentSlot.MAIN_HAND
));
}
private static void startDigging(Player player, Block block, BlockVec pos) {
final var data = playerData.computeIfAbsent(player, p -> new PlayerData());
clearDigging(data);
final var sound = block.registry().getBlockSoundType().hitSound();
data.diggingSound = MinecraftServer.getSchedulerManager()
.buildTask(() -> play(player, pos.asPos().add(0.5, 0.5, 0.5), sound, 0.5f, 0.5f))
.repeat(Duration.ofMillis(250))
.schedule();
final var breakTicks = BlockBreakCalculation.breakTicks(block, player);
final var damagePerTick = breakTicks < 0 ? 0.0 : 1.0 / breakTicks;
data.diggingProgress = 0.0;
data.diggingProgressTask = MinecraftServer.getSchedulerManager()
.buildTask(new Runnable() {
@Override
public void run() {
data.diggingProgress += damagePerTick;
PacketSendingUtils.sendGroupedPacket(
player.getInstance().getChunkAt(pos).getViewers(),
new BlockBreakAnimationPacket(player.getEntityId(), pos, (byte)(data.diggingProgress * 10 - 1)),
(p) -> p != player
);
}
})
.repeat(TaskSchedule.tick(1))
.schedule();
}
private static void clearDigging(PlayerData data) {
if (data.diggingSound != null) {
data.diggingSound.cancel();
data.diggingSound = null;
}
if (data.diggingProgressTask != null) {
data.diggingProgressTask.cancel();
data.diggingProgressTask = null;
}
data.diggingProgress = 0.0;
}
private static void updateEquipment(LivingEntity entity, ItemStack oldItem, ItemStack newItem, EquipmentSlot slot) {
// Remove old modifiers
for (final var modifier : getItemAttrs(oldItem)
.modifiers()) {
if (!modifier.slot().contains(slot)) continue;
entity.getAttribute(modifier.attribute()).removeModifier(modifier.modifier());
}
/// Apply new modifiers
for (final var modifier : getItemAttrs(newItem)
.modifiers()) {
if (!modifier.slot().contains(slot)) continue;
entity.getAttribute(modifier.attribute()).addModifier(modifier.modifier());
}
}
private static AttributeList getItemAttrs(ItemStack item) {
// Applies patches from enchantments
var attrs = item.get(DataComponents.ATTRIBUTE_MODIFIERS, AttributeList.EMPTY);
var slotGroup = EquipmentSlotGroup.ANY;
final var equippable = item.get(DataComponents.EQUIPPABLE);
if (equippable != null) {
for (final var variant : EquipmentSlotGroup.values()) {
if (variant.equipmentSlots().equals(List.of(equippable.slot())))
slotGroup = variant;
}
}
final var enchantments = item.get(DataComponents.ENCHANTMENTS, EnchantmentList.EMPTY).enchantments();
final var registry = MinecraftServer.getEnchantmentRegistry();
for (final var key : enchantments.keySet()) {
final var enchantment = registry.get(key);
final var level = enchantments.get(key);
for (final var effectEntry : enchantment.effects().entrySet()) {
if (!(effectEntry.value() instanceof List effects)) continue; // Yeah, idk how to properly check it. `.component()` returns item_model for some reason.
for (final var effectRaw : effects) {
if (!(effectRaw instanceof AttributeEffect effect)) continue;
attrs = attrs.with(new AttributeList.Modifier(
effect.attribute(),
new AttributeModifier(
effect.id(),
effect.amount().calc(level),
effect.operation()
), slotGroup
));
}
}
}
return attrs;
}
private static void play(Player player, Point pos, SoundEvent sound, float volume, float pitch) {
if (sound == null) return;
// Doing this instead of playSoundExcept, because playSoundExcept sends the packet to ALL players,
// whereas this function only sends the packet to players viewing the chunk (if the chunk is loaded)
final var instance = player.getInstance();
final var chunk = instance.getChunkAt(pos);
final var packet = AdventurePacketConvertor.createSoundPacket(
Sound.sound(sound, Sound.Source.PLAYER, volume, pitch),
pos.x(), pos.y(), pos.z());
PacketSendingUtils.sendGroupedPacket(
chunk == null ? instance.getPlayers() : chunk.getViewers(), packet, (p) -> {
return p != player;
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment