Solved Stop block cracking animation and breaking.

Discussion in 'Plugin Development' started by Sirisian, May 21, 2013.

Thread Status:
Not open for further replies.
  1. Offline

    Sirisian

    My goal is to allow blocks to be unbrakeable or to have an arbitrary amount of health so the cracking animation is played over a long period and the block doesn't pop after a few seconds of hitting it.

    As an example pretend you give a grass block 1 minute to break then the cracking animation would play out over 1 minute slowly and only at the end would the block break. Or if grass was made invulnerable then as you hit it no cracking animation would play at all and the block wouldn't break and then reappear.

    So to do this I believe I have to use:
    Code:
    @EventHandler
    public void onPlayerInteract(PlayerInteractEvent event)
    {
        if (event.getAction() == Action.LEFT_CLICK_BLOCK)
        {
            event.setCancelled(true);
            Block block = event.getClickedBlock();
            Player player = event.getPlayer();
            int damage = -1;
            Packet55BlockBreakAnimation packet = new Packet55BlockBreakAnimation(0, block.getX(), block.getY(), block.getZ(), damage);
            int dimension = ((CraftWorld) player.getWorld()).getHandle().dimension;
            ((CraftServer)event.getPlayer().getServer()).getHandle().sendPacketNearby(block.getX(), block.getY(), block.getZ(), 120, dimension, packet);
        }
    }
    However, with that naive code the animation still plays like normal even after being set to the first frame of the animation with the Packet55BlockBreakAnimation method. Also the block breaks and then immediately reappears making a break sound after the normal period of time for breaking a grass block. I've been trying a few ideas like sending a block change event to the player but it has no effect, or how I'm doing it has no effect. I'm sure someone has written something like this already so any advice or code sample would be appreciated.

    I get that I'll need to use the scheduler to do the breaking animation over time though. Right now the goal is to stop the cracking animation from playing when I left click on a block and to stop if from breaking in the normal time.
     
  2. Offline

    number1_Master

    Messaged you about it! For all those wondering:

    What you want to do is on the BlockDamageEvent, cancel it. Then, start a scheduler that checks every x ticks if the player is still looking at the same block. If not, end the scheduler. If so, continue with the block animation packet and keep track of how many ticks went by. Remember, 20 ticks in a second. When done, break the block however you would like.

    I used this as an inspiration: https://gist.github.com/aadnk/3788593

    Obviously, it is a little outdated so I changed it up. To animate a block, you would use the following:
    CODE (open)
    Code:java
    1. Packet55BlockBreakAnimation packet = newPacket55BlockBreakAnimation(0, b.getX(), b.getY(), b.getZ(), DAMAGE);
    2. int dimension = ((CraftWorld) p.getWorld()).getHandle().dimension;
    3. ((CraftServer) p.getServer()).getHandle().sendPacketNearby(b.getX(), b.getY(), b.getZ(), 120, dimension, packet);

    Where variable b is a block and variable p is a player. A good way to calculate the damage is to use the modules operator. For example:
    Every 10 ticks the scheduler is executed. So, if I want to increase the damage every 2 seconds, I would have to make a counter and see if the counter % 4 == 0. If it is, that means 2 seconds has gone by (4 * 10 = 40 / 20 = 2). The damage is then the counter / 4. As long as the damage is less than 10 but greater than 0, I would animate the block.
    CODE (open)
    Code:java
    1. if(counter % 4 == 0)
    2. {
    3. damage = counter / 4;
    4. if(damage <= 10 && damage >= 0) // ANIMATE BLOCK
    5. }
     
    1SmallVille1 and AngryNerd like this.
  3. Offline

    Sirisian

    I already implemented that part using his code before, but my issue is as I'm mining the grass block the cracks are still forming on the block at their old rate when I hold down. Now if I click once then the cracks form correctly over time. I'm already canceling the BlockDamageEvent which isn't the problem. The block still pops for instance when I hit a rose causing me to see through it. This happens to the grass block also.

    My goal is that at no point should the block change or make a breaking sound on the client until the time is up and cracks shouldn't form on the client-side at a different rate when they mine the block.

    Ideally it would be nice if I could mark the block they are mining as invulnerable so they can hit it and only the cracks I'm sending will show up as they hold down.

    Is there a way to change the durability of all blocks perhaps on the client-side to 10000? I don't see a method like that.

    Code:
    package org.civcraft.citadel.listeners;
     
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.block.BlockDamageEvent;
    import org.bukkit.plugin.Plugin;
     
    public class BlockDamageListener implements Listener
    {
        private Plugin plugin;
     
        public BlockDamageListener(Plugin plugin)
        {
            this.plugin = plugin;
        }
     
        @EventHandler
        public void onBlockDamage(BlockDamageEvent event)
        {
            plugin.getLogger().info("onBlockDamage");
            event.setCancelled(true);
        }
    }
    Code:
    package org.civcraft.citadel.listeners;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import net.minecraft.server.v1_5_R3.Packet55BlockBreakAnimation;
    
    import org.bukkit.Server;
    import org.bukkit.block.Block;
    import org.bukkit.craftbukkit.v1_5_R3.CraftServer;
    import org.bukkit.craftbukkit.v1_5_R3.CraftWorld;
    import org.bukkit.entity.Player;
    import org.bukkit.event.Event.Result;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.block.Action;
    import org.bukkit.event.player.PlayerInteractEvent;
    import org.bukkit.plugin.Plugin;
    import org.bukkit.scheduler.BukkitScheduler;
    
    public class PlayerInteractListener implements Listener
    {
        private Map<String, TaskedRunnable> playerClicking = new HashMap<String, TaskedRunnable>();
        private Plugin plugin;
        private Server server;
        private BukkitScheduler scheduler;
        
        public PlayerInteractListener(Plugin plugin)
        {
            this.plugin = plugin;
            this.server = plugin.getServer();
            this.scheduler = server.getScheduler();
        }
    
        @EventHandler
        public void onPlayerInteract(PlayerInteractEvent event)
        {
            plugin.getLogger().info("onPlayerInteract");
            Player player = event.getPlayer();
            event.setUseItemInHand(Result.DENY);
            int dimension = ((CraftWorld) player.getWorld()).getHandle().dimension;
            float breakTimeInSeconds = 5;
    
            if (event.getAction() == Action.LEFT_CLICK_BLOCK)
            {
                Block block = event.getClickedBlock();
    
                TaskedRunnable previous = playerClicking.get(player.getName());
                // If there is no previously targeted block or if the currently targeted block isn't the same as the previous then destroying the new block
                if (previous == null || !previous.getBlock().getLocation().equals(block.getLocation()))
                {
                    // The player has switched targets so the previous block is no longer being destroyed
                    if (previous != null)
                    {
                        previous.cancel();
                    }
    
                    TaskedRunnable tasked = new TaskedRunnable(player, block, dimension, (int)(breakTimeInSeconds * 20));
                    int taskID = scheduler.scheduleSyncRepeatingTask(plugin, tasked, 1L, 1L);
                    // Save the task ID
                    tasked.setTaskID(taskID);
                    playerClicking.put(player.getName(), tasked);
                }
            }
        }
        
        public void cancelClicking(Player player)
        {
            TaskedRunnable previous = playerClicking.get(player.getName());
            if (previous != null)
            {
                previous.cancel();
            }
        }
    
        private class TaskedRunnable implements Runnable
        {
            private int counter = 0;
            private int taskID;
            private Player player;
            private Block block;
            private int breakTime;
            private int dimension;
    
            public TaskedRunnable(Player player, Block block, int dimension, int breakTime)
            {
                this.player = player;
                this.block = block;
                this.dimension = dimension;
                this.breakTime = breakTime;
            }
     
            public void setTaskID(int taskID)
            {
                this.taskID = taskID;
            }
    
            public Block getBlock()
            {
                return block;
            }
    
            @Override
            public void run()
            {
                Block newTarget = player.getTargetBlock(null, 5);
                // If the player isn't looking at the block cancel
                if (newTarget == null || !block.getLocation().equals(newTarget.getLocation()))
                {
                    cancel();
                    return;
                }
    
                // Damage is a value 0 to 9 inclusive representing the 10 different damage textures that can be applied to a block
                int damage = (int)(counter / (float)breakTime * 10);
    
                // Send the damage animation state once for each increment
                if (damage != (counter == 0 ? -1 : (int)((counter - 1) / (float)breakTime * 10)))
                {
                    plugin.getLogger().info("Damage: " + damage);
                    sendBlockBreak(damage);
                }
                counter++;
                if (counter == breakTime)
                {
                    sendBlockBreak(10);
                    block.breakNaturally();
                    scheduler.cancelTask(taskID);
                }
            }
    
            private void sendBlockBreak(int damage)
            {
                Packet55BlockBreakAnimation breakBlockPacket = new Packet55BlockBreakAnimation(0, block.getX(), block.getY(), block.getZ(), damage);
                ((CraftServer) server).getHandle().sendPacketNearby(block.getX(), block.getY(), block.getZ(), 120, dimension, breakBlockPacket);
            }
    
            public void cancel()
            {
                playerClicking.remove(player.getName());
                scheduler.cancelTask(taskID);
                sendBlockBreak(10);
            }
        }
    }
    
    Using ProtocolLib I've successfully solved another issue. Originally when you clicked on a block it would only stop destroying it when you looked away. Using a packet listener on the server you can detect when the player cancels attacking the block.

    Code:
    protocolManager.addPacketListener(new PacketAdapter(this, ConnectionSide.CLIENT_SIDE, ListenerPriority.NORMAL, Packets.Client.BLOCK_DIG)
    {
        @Override
        public void onPacketReceiving(PacketEvent event)
        {
            if (event.getPacketID() == Packets.Client.BLOCK_DIG)
            {
                Player player = event.getPlayer();
                if (event.getPacket().getIntegers().read(4) == 1)
                {
                    getLogger().info("Cancel left click.");
                    playerInteractListender.cancelClicking(player);
                }
            }
        }
    });
    This doesn't solve the issue if the block is destroyed on the client-side though. It'll send the server 2 in the BLOCK_DIG indicating the player on the client-side destroyed it, which actually makes things more complicated. It's possible to release just as the block is destroyed and it'll send only 2 and the block will reform and continue to break until you look away since the BLOCK_DIG 1 packet isn't sent since according to the client you finished destroying the block and didn't cancel even though the server cancelled that block destruction.

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 1, 2016
  4. Offline

    number1_Master

    Call the scheduler on the BlockDamageEvent, not the PlayerInteractEvent :)
     
  5. Offline

    Sirisian

    That would do the same thing? They're both called exactly once when a block is mined. Are you guessing or have you done this before? Both the PlayerInteractEvent and BlockDamageEvent are called when a block begins getting mined exactly once. I already called the setCancelled in the onBlockDamage which doesn't prevent the block from being cracked. All it does it prevent it from being destroyed on the server-side. It still breaks and reappears on the client-side.

    Code:
    package org.civcraft.citadel.listeners;
     
    import java.util.HashMap;
    import java.util.Map;
     
    import net.minecraft.server.v1_5_R3.Packet55BlockBreakAnimation;
     
    import org.bukkit.Server;
    import org.bukkit.block.Block;
    import org.bukkit.craftbukkit.v1_5_R3.CraftServer;
    import org.bukkit.craftbukkit.v1_5_R3.CraftWorld;
    import org.bukkit.entity.Player;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.block.BlockDamageEvent;
    import org.bukkit.plugin.Plugin;
    import org.bukkit.scheduler.BukkitScheduler;
     
    public class BlockDamageListener implements Listener
    {
        private Map<String, TaskedRunnable> playerClicking = new HashMap<String, TaskedRunnable>();
        private Plugin plugin;
        private Server server;
        private BukkitScheduler scheduler;
     
        public BlockDamageListener(Plugin plugin)
        {
            this.plugin = plugin;
            this.server = plugin.getServer();
            this.scheduler = server.getScheduler();
        }
     
        @EventHandler
        public void onBlockDamage(BlockDamageEvent event)
        {
            plugin.getLogger().info("onBlockDamage");
            event.setCancelled(true);
            Player player = event.getPlayer();
     
            int dimension = ((CraftWorld)player.getWorld()).getHandle().dimension;
            float breakTimeInSeconds = 5;
     
            Block block = event.getBlock();
     
            TaskedRunnable previous = playerClicking.get(player.getName());
            // If there is no previously targeted block or if the currently targeted block isn't the same as the previous then destroying the new block
            if (previous == null || !previous.getBlock().getLocation().equals(block.getLocation()))
            {
                // The player has switched targets so the previous block is no longer being destroyed
                if (previous != null)
                {
                    previous.cancel();
                }
     
                TaskedRunnable tasked = new TaskedRunnable(player, block, dimension, (int)(breakTimeInSeconds * 20));
                int taskID = scheduler.scheduleSyncRepeatingTask(plugin, tasked, 1L, 1L);
                // Save the task ID
                tasked.setTaskID(taskID);
                playerClicking.put(player.getName(), tasked);
            }
        }
           
        public void cancelClicking(Player player)
        {
            TaskedRunnable previous = playerClicking.get(player.getName());
            if (previous != null)
            {
                previous.cancel();
            }
        }
     
        private class TaskedRunnable implements Runnable
        {
            private int counter = 0;
            private int taskID;
            private Player player;
            private Block block;
            private int breakTime;
            private int dimension;
     
            public TaskedRunnable(Player player, Block block, int dimension, int breakTime)
            {
                this.player = player;
                this.block = block;
                this.dimension = dimension;
                this.breakTime = breakTime;
            }
     
            public void setTaskID(int taskID)
            {
                this.taskID = taskID;
            }
     
            public Block getBlock()
            {
                return block;
            }
     
            @Override
            public void run()
            {
                Block newTarget = player.getTargetBlock(null, 5);
                // If the player isn't looking at the block cancel
                if (newTarget == null || !block.getLocation().equals(newTarget.getLocation()))
                {
                    cancel();
                    return;
                }
     
                // Damage is a value 0 to 9 inclusive representing the 10 different damage textures that can be applied to a block
                int damage = (int)(counter / (float)breakTime * 10);
     
                // Send the damage animation state once for each increment
                if (damage != (counter == 0 ? -1 : (int)((counter - 1) / (float)breakTime * 10)))
                {
                    plugin.getLogger().info("Damage: " + damage);
                    sendBlockBreak(damage);
                }
                counter++;
                if (counter == breakTime)
                {
                    sendBlockBreak(10);
                    block.breakNaturally();
                    scheduler.cancelTask(taskID);
                }
            }
     
            private void sendBlockBreak(int damage)
            {
                Packet55BlockBreakAnimation breakBlockPacket = new Packet55BlockBreakAnimation(0, block.getX(), block.getY(), block.getZ(), damage);
                ((CraftServer) server).getHandle().sendPacketNearby(block.getX(), block.getY(), block.getZ(), 120, dimension, breakBlockPacket);
            }
     
            public void cancel()
            {
                playerClicking.remove(player.getName());
                scheduler.cancelTask(taskID);
                sendBlockBreak(10);
            }
        }
    }
    
     
  6. Offline

    number1_Master

    The BlockDamageEvent is called if the PlayerInteractEvent is confirmed as a block. The PlayerInteractEvent will almost always be called whenever a player clicks (interacts).
    By canceling the BlockDamageEvent, you also cancel the block from being dropped, HOWEVER, as I mentioned before, keep track of how much damage the block has in the scheduler. When the damage is 10, manually drop the block.
     
  7. Offline

    Sirisian

    I fixed this. If anyone else ever wants to implement this then they need to implement a MinecraftForge client-side plugin. Fairly simple idea. The below makes all blocks invulnerable. I still have a lot of testing to see if the delay is tolerable, but this should work for my use case.

    With the above code and configurations I was able to set it up so I could define on the server-side how long it takes blocks to break and define all of the properties for them based on their enchantments.

    Code:
    package org.civcraft.citadel;
    
    import net.minecraft.block.Block;
    import cpw.mods.fml.common.Mod;
    import cpw.mods.fml.common.Mod.Init;
    import cpw.mods.fml.common.Mod.Instance;
    import cpw.mods.fml.common.Mod.PostInit;
    import cpw.mods.fml.common.Mod.PreInit;
    import cpw.mods.fml.common.event.FMLInitializationEvent;
    import cpw.mods.fml.common.event.FMLPostInitializationEvent;
    import cpw.mods.fml.common.event.FMLPreInitializationEvent;
    import cpw.mods.fml.common.network.NetworkMod;
    
    @Mod(modid="Citadel", name="Citadel", version="0.0.1")
    @NetworkMod(clientSideRequired=true, serverSideRequired=false)
    public class Citadel
    {
        @Instance("Citadel")
        public static Citadel instance;
    
        @PreInit
        public void preInit(FMLPreInitializationEvent event)
        {
        }
    
        @Init
        public void load(FMLInitializationEvent event)
        {
            for(Block block : Block.blocksList)
            {
                if (block != null)
                {
                    block.setBlockUnbreakable();
                }
            }
        }
    
        @PostInit
        public void postInit(FMLPostInitializationEvent event)
        {
        }
    }
     
Thread Status:
Not open for further replies.

Share This Page