Skip to content

Instantly share code, notes, and snippets.

@jordoncodes
Last active May 5, 2025 13:56
Show Gist options
  • Save jordoncodes/b8db6426fcd9cdba044fb39ccfbb7c77 to your computer and use it in GitHub Desktop.
Save jordoncodes/b8db6426fcd9cdba044fb39ccfbb7c77 to your computer and use it in GitHub Desktop.
(Minecraft) Tutorial: Custom Block Breaking Speed (custom mining speed) using Spigot API & ProtocolLib.

Custom Mining Speed

Hello, this is a little bit advanced tutorial (using packets) about how to do custom mining speed. This will show you the basics of how to do it.

You may also notice that this has the effect of being able to stop midway thru mining then resume.

Dependencies:

  • ProtocolLib
  • Spigot API (this was tested on 1.19.2)

Make the mining listener and mining manager classes, don't forget to register the listener.

MiningManager steps:

  1. In the Mining Listener, make a HashMap<UUID, Long>. Name it nextPhase. This will store the time that the next mining phase can happen (the next block break animation stage).

You also want a HashMap<Location, Integer>, name it blockStages. This will store the current block stage for each block.

You also want the ProtocolManager (from ProtocolLib).

Code:

private final HashMap<UUID, Long> nextPhase = new HashMap<UUID, Long>();
private final HashMap<Location, Integer> blockStages = new HashMap<>(); // this could be changed to a Cache, so the blockStages **don't last forever**
private final ProtocolManager manager = ProtocolLibrary.getProtocolManager();
  1. Create methods to update the cooldown between the mining phases. updatePhaseCooldown checks if the player has a phase, and if so and the cooldown is over, remove it.

nextPhase just re-adds the player into the phase cooldown map, making there be a delay of 400ms between phases in this example.

Code:

 public long getNextPhase(Player player) {
    return nextPhase.get(player.getUniqueId());
}

/**
* @return true if the player can go to the next phase, false if they can't
*/
public boolean updatePhaseCooldown(Player player) {
    List<UUID> toRemove = new ArrayList<>();
    nextPhase.forEach((uuid, phase) -> {
        if (phase <= System.currentTimeMillis()) {
            toRemove.add(uuid);
        }
    });
    toRemove.forEach(nextPhase::remove);
    if (nextPhase.containsKey(player.getUniqueId())) return false;
    nextPhase(player);
    return true;
}

public void nextPhase(Player player) {
    nextPhase.put(player.getUniqueId(), System.currentTimeMillis() + 400); // 400 milliseconds between phases
}

/**
* does both updatePhaseCooldown and nextPhase
* @return true if the phase has been updated
*/
public boolean updateAndNextPhase(Player player) {
    if (updatePhaseCooldown(player)) {
        nextPhase(player);
        return true;
    }
    return false;
}
  1. Create a sendBlockDamage method, Paper API's sendBlockDamage method doesn't work with what we're doing and there's no alternatives in Spigot, so we're going to use packets:

Code:

public void sendBlockDamage(Player player, Location location, float progress) {
    int locationId = location.getBlockX() + location.getBlockY() + location.getBlockZ();
    PacketContainer packet = manager.createPacket(PacketType.Play.Server.BLOCK_BREAK_ANIMATION);
    // set entity ID to the location so the block break animation doesn't break (this is why we need packets).
    packet.getIntegers().write(0, locationId); 
    packet.getBlockPositionModifier().write(0, new BlockPosition(location.toVector())); // set the block location
    packet.getIntegers().write(1, getBlockStage(location)); // set the damage to blockStage
    try {
        manager.sendServerPacket(player, packet);
    } catch (InvocationTargetException e) {
        e.printStackTrace(); // the packet was unable to send.
    }
}
  1. We will also create methods for block stages in the MiningManager. getBlockStage(Location) which simply gets from the map and setBlockStage(Location,int) which puts the stage into the map. and removeBlockStage(Location) that simply removes it.
public int getBlockStage(Location loc) {
    return blockStages.getOrDefault(loc, 0);
}

public void setBlockStage(Location loc, int stage) {
    blockStages.remove(loc);
    blockStages.put(loc, stage);
}

public void removeBlockStage(Location loc) {
    blockStages.remove(loc);
}

You're finally done with your MiningManager. Now onto the MiningListener:

  1. Create a constructor to take in the MiningManager.
public MiningManager miningManager;


public MiningListener(MiningManager manager) {
    this.miningManager = manager;
}
  1. Create an eventhandler and call it onMine. This will listen for a PlayerAnimationEvent (this may cause some issues, as the arm swings for other things, too i.e. placing blocks, but its minor and this avoids using more packets).

if you wanted to avoid the aforementioned issues, you could listen to a PacketType.Play.Client.BLOCK_DIG using ProtocolLib. PacketWrapper might help you do this. It will get called when a player starts mining, finishes mining, and cancels their current mining, and you'd use that instead of arm swing and keep running it until they cancel it or you break the block. Personally, I don't want more packets, so this will do for what we're doing here.

Here's all the code for it. Reading the comments and ^ will help understnading it.

@EventHandler
public void onMine(PlayerAnimationEvent e) {
    UUID uuid = e.getPlayer().getUniqueId();
    Player player = e.getPlayer();
    if (!e.getAnimationType().equals(PlayerAnimationType.ARM_SWING)) return;
    if (!player.getGameMode().equals(GameMode.SURVIVAL)) return; // require survival mode

    // get the block the player is looking at.
    // you may notice that 3 sometimes isn't enough, you might want to increase this to 4.
    Block block = player.getTargetBlockExact(3, FluidCollisionMode.NEVER);
    if (block == null) return;
    if (block.getType().equals(Material.AIR)) return; // make sure the block isn't air
    miningManager.updateAndNextPhase(player); // update the mining phase.

    // send the block stage before updating mining. This will attempt to prevent placing blocks making destruction animations.
    int blockStage = miningManager.getBlockStage(block.getLocation());
    miningManager.sendBlockDamage(player, block.getLocation(), blockStage); // send the block damage packet
    blockStage = ((blockStage+1) % 10); // increment the block stage, if it's already 10, set it back to 0.
    miningManager.setBlockStage(block.getLocation(), blockStage);
    if (blockStage == 0) {
        miningManager.removeBlockStage(block.getLocation()); // remove the block stage
        block.breakNaturally(player.getInventory().getItemInMainHand()); // break the block
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment