diff --git a/src/main/java/fr/themode/demo/PlayerInit.java b/src/main/java/fr/themode/demo/PlayerInit.java index 2419afa47..f68f94578 100644 --- a/src/main/java/fr/themode/demo/PlayerInit.java +++ b/src/main/java/fr/themode/demo/PlayerInit.java @@ -30,13 +30,12 @@ import net.minestom.server.item.metadata.MapMeta; import net.minestom.server.network.ConnectionManager; import net.minestom.server.ping.ResponseDataConsumer; import net.minestom.server.scoreboard.Sidebar; -import net.minestom.server.storage.StorageFolder; -import net.minestom.server.storage.StorageOptions; import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.Position; import net.minestom.server.utils.Vector; import net.minestom.server.utils.time.TimeUnit; +import net.minestom.server.world.DimensionType; import java.util.Map; import java.util.UUID; @@ -48,11 +47,11 @@ public class PlayerInit { private static volatile Inventory inventory; static { - StorageFolder storageFolder = MinecraftServer.getStorageManager().getFolder("instance_data", new StorageOptions().setCompression(true)); + //StorageFolder storageFolder = MinecraftServer.getStorageManager().getFolder("instance_data", new StorageOptions().setCompression(true)); ChunkGeneratorDemo chunkGeneratorDemo = new ChunkGeneratorDemo(); NoiseTestGenerator noiseTestGenerator = new NoiseTestGenerator(); - instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(storageFolder); - //instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(DimensionType.OVERWORLD); + //instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(storageFolder); + instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(DimensionType.OVERWORLD); instanceContainer.enableAutoChunkLoad(true); //instanceContainer.setChunkDecider((x,y) -> (pos) -> pos.getY()>40?(short)0:(short)1); instanceContainer.setChunkGenerator(noiseTestGenerator); @@ -272,7 +271,7 @@ public class PlayerInit { }); player.addEventCallback(PlayerSpawnEvent.class, event -> { - player.setGameMode(GameMode.CREATIVE); + player.setGameMode(GameMode.SURVIVAL); player.teleport(new Position(0, 41f, 0)); //player.setHeldItemSlot((byte) 5); @@ -390,7 +389,7 @@ public class PlayerInit { // Unload the chunk (save memory) if it has no remaining viewer if (chunk.getViewers().isEmpty()) { - player.getInstance().unloadChunk(chunk); + //player.getInstance().unloadChunk(chunk); } }); diff --git a/src/main/java/fr/themode/demo/blocks/BurningTorchBlock.java b/src/main/java/fr/themode/demo/blocks/BurningTorchBlock.java index b6d9ebc54..cb90f2e40 100644 --- a/src/main/java/fr/themode/demo/blocks/BurningTorchBlock.java +++ b/src/main/java/fr/themode/demo/blocks/BurningTorchBlock.java @@ -20,8 +20,8 @@ public class BurningTorchBlock extends CustomBlock { @Override public void handleContact(Instance instance, BlockPosition position, Entity touching) { - System.out.println("touching "+touching); - if(touching instanceof LivingEntity) { + System.out.println("touching " + touching); + if (touching instanceof LivingEntity) { ((LivingEntity) touching).damage(DamageType.GRAVITY, 0.1f); } } @@ -45,9 +45,4 @@ public class BurningTorchBlock extends CustomBlock { public short getCustomBlockId() { return 3; } - - @Override - public int getBreakDelay(Player player, BlockPosition position) { - return -1; - } } diff --git a/src/main/java/fr/themode/demo/blocks/StoneBlock.java b/src/main/java/fr/themode/demo/blocks/StoneBlock.java index 153fc02ec..87ee9a692 100644 --- a/src/main/java/fr/themode/demo/blocks/StoneBlock.java +++ b/src/main/java/fr/themode/demo/blocks/StoneBlock.java @@ -7,6 +7,8 @@ import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.utils.BlockPosition; +import java.util.Set; + public class StoneBlock extends CustomBlock { public StoneBlock() { @@ -39,8 +41,18 @@ public class StoneBlock extends CustomBlock { } @Override - public int getBreakDelay(Player player, BlockPosition position) { - return 750; + public int getBreakDelay(Player player, BlockPosition position, byte stage, Set breakers) { + return 2; + } + + @Override + public boolean enableCustomBreakDelay() { + return true; + } + + @Override + public boolean enableMultiPlayerBreaking() { + return true; } @Override diff --git a/src/main/java/fr/themode/demo/blocks/UpdatableBlockDemo.java b/src/main/java/fr/themode/demo/blocks/UpdatableBlockDemo.java index 362aa8292..ac434a6e4 100644 --- a/src/main/java/fr/themode/demo/blocks/UpdatableBlockDemo.java +++ b/src/main/java/fr/themode/demo/blocks/UpdatableBlockDemo.java @@ -9,6 +9,8 @@ import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.UpdateOption; +import java.util.Set; + public class UpdatableBlockDemo extends CustomBlock { private static final UpdateOption UPDATE_OPTION = new UpdateOption(20, TimeUnit.TICK); @@ -43,8 +45,13 @@ public class UpdatableBlockDemo extends CustomBlock { } @Override - public int getBreakDelay(Player player, BlockPosition position) { - return 500; + public int getBreakDelay(Player player, BlockPosition position, byte stage, Set breakers) { + return 1; + } + + @Override + public boolean enableCustomBreakDelay() { + return true; } @Override diff --git a/src/main/java/fr/themode/demo/commands/SimpleCommand.java b/src/main/java/fr/themode/demo/commands/SimpleCommand.java index dc9a27f7b..2ebc9bc85 100644 --- a/src/main/java/fr/themode/demo/commands/SimpleCommand.java +++ b/src/main/java/fr/themode/demo/commands/SimpleCommand.java @@ -67,7 +67,9 @@ public class SimpleCommand implements CommandProcessor { NotificationCenter.send(notification, player); NotificationCenter.send(notification, player); - player.getInstance().saveChunksToStorageFolder(() -> System.out.println("end save")); + System.gc(); + + //player.getInstance().saveChunksToStorageFolder(() -> System.out.println("end save")); return true; } diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index b399a8a2f..44d340650 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -30,6 +30,7 @@ import net.minestom.server.network.packet.server.login.JoinGamePacket; import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.permission.Permission; +import net.minestom.server.potion.PotionType; import net.minestom.server.recipe.Recipe; import net.minestom.server.recipe.RecipeManager; import net.minestom.server.resourcepack.ResourcePack; @@ -102,9 +103,10 @@ public class Player extends LivingEntity implements CommandSender { // CustomBlock break delay private CustomBlock targetCustomBlock; private BlockPosition targetBlockPosition; - private long targetBlockTime; - private byte targetLastStage; - private int blockBreakTime; + private long targetBreakDelay; // The last break delay requested + private long targetBlockLastStageChangeTime; // Time at which the block stage last changed + private byte targetStage; // The current stage of the target block, only if multi player breaking is disabled + private final Set targetBreakers = new HashSet<>(1); // Only used if multi player breaking is disabled, contains only this player private BelowNameTag belowNameTag; @@ -159,6 +161,9 @@ public class Player extends LivingEntity implements CommandSender { this.levelType = LevelType.FLAT; refreshPosition(0, 0, 0); + // Used to cache the breaker for single custom block breaking + this.targetBreakers.add(this); + // FakePlayer init its connection there playerConnectionInit(); @@ -300,17 +305,39 @@ public class Player extends LivingEntity implements CommandSender { // Target block stage if (targetCustomBlock != null) { - final byte animationCount = 10; - final long since = time - targetBlockTime; - byte stage = (byte) (since / (blockBreakTime / animationCount)); - stage = MathUtils.setBetween(stage, (byte) -1, animationCount); - if (stage != targetLastStage) { - sendBlockBreakAnimation(targetBlockPosition, stage); - } - this.targetLastStage = stage; - if (stage > 9) { - instance.breakBlock(this, targetBlockPosition); - resetTargetBlock(); + final boolean processStage = (time - targetBlockLastStageChangeTime) >= targetBreakDelay; + if (processStage) { + // Should increment the target block stage + if (targetCustomBlock.enableMultiPlayerBreaking()) { + // Let the custom block object manages the breaking + final boolean canContinue = this.targetCustomBlock.processStage(instance, targetBlockPosition, this); + if (canContinue) { + final Set breakers = targetCustomBlock.getBreakers(instance, targetBlockPosition); + refreshBreakDelay(breakers); + this.targetBlockLastStageChangeTime = time; + } else { + resetTargetBlock(); + } + } else { + // Let the player object manages the breaking + // The custom block doesn't support multi player breaking + if (targetStage + 1 >= CustomBlock.MAX_STAGE) { + // Break the block + instance.breakBlock(this, targetBlockPosition); + resetTargetBlock(); + } else { + // Send the new block break animation packet and refresh data + + final Chunk chunk = instance.getChunkAt(targetBlockPosition); + final int entityId = targetCustomBlock.getBreakEntityId(this); + final BlockBreakAnimationPacket blockBreakAnimationPacket = new BlockBreakAnimationPacket(entityId, targetBlockPosition, targetStage); + chunk.sendPacketToViewers(blockBreakAnimationPacket); + + refreshBreakDelay(targetBreakers); + this.targetBlockLastStageChangeTime = time; + this.targetStage++; + } + } } } @@ -630,24 +657,6 @@ public class Player extends LivingEntity implements CommandSender { sendPluginMessage(channel, data); } - /** - * Send a {@link BlockBreakAnimationPacket} packet to the player and his viewers - * Setting {@code destroyStage} to -1 resets the break animation - * - * @param blockPosition the position of the block - * @param destroyStage the destroy stage - * @throws IllegalArgumentException if {@code destroyStage} is not between -1 and 10 - */ - public void sendBlockBreakAnimation(BlockPosition blockPosition, byte destroyStage) { - Check.argCondition(!MathUtils.isBetween(destroyStage, -1, 10), - "The destroy stage has to be between -1 and 10"); - BlockBreakAnimationPacket breakAnimationPacket = new BlockBreakAnimationPacket(); - breakAnimationPacket.entityId = getEntityId() + 1; - breakAnimationPacket.blockPosition = blockPosition; - breakAnimationPacket.destroyStage = destroyStage; - sendPacketToViewersAndSelf(breakAnimationPacket); - } - @Override public void sendMessage(String message) { sendMessage(ColoredText.of(message)); @@ -1690,7 +1699,7 @@ public class Player extends LivingEntity implements CommandSender { * Change the player ability "Creative Mode" * see *

- * WARNING: this has nothing to do with {@link CustomBlock#getBreakDelay(Player, BlockPosition)} + * WARNING: this has nothing to do with {@link CustomBlock#getBreakDelay(Player, BlockPosition, byte, Set)} * * @param instantBreak true to allow instant break */ @@ -1870,13 +1879,25 @@ public class Player extends LivingEntity implements CommandSender { * * @param targetCustomBlock the custom block to dig * @param targetBlockPosition the custom block position - * @param breakTime the time it will take to break the block in milliseconds + * @param breakers the breakers of the block, can be null if {@code this} is the only breaker */ - public void setTargetBlock(CustomBlock targetCustomBlock, BlockPosition targetBlockPosition, int breakTime) { + public void setTargetBlock(CustomBlock targetCustomBlock, BlockPosition targetBlockPosition, Set breakers) { this.targetCustomBlock = targetCustomBlock; this.targetBlockPosition = targetBlockPosition; - this.targetBlockTime = targetBlockPosition == null ? 0 : System.currentTimeMillis(); - this.blockBreakTime = breakTime; + + refreshBreakDelay(breakers); + } + + /** + * Refresh the break delay for the next block break stage + * + * @param breakers the list of breakers, can be null if {@code this} is the only breaker + */ + private void refreshBreakDelay(Set breakers) { + breakers = breakers == null ? targetBreakers : breakers; + final byte stage = targetCustomBlock.getBreakStage(instance, targetBlockPosition); + final int breakDelay = targetCustomBlock.getBreakDelay(this, targetBlockPosition, stage, breakers); + this.targetBreakDelay = breakDelay * MinecraftServer.TICK_MS; } /** @@ -1884,17 +1905,19 @@ public class Player extends LivingEntity implements CommandSender { * If the currently mined block (or if there isn't any) is not a CustomBlock, nothing append */ public void resetTargetBlock() { - if (targetBlockPosition != null) { - sendBlockBreakAnimation(targetBlockPosition, (byte) -1); // Clear the break animation + if (targetCustomBlock != null) { + targetCustomBlock.stopDigging(instance, targetBlockPosition, this); this.targetCustomBlock = null; this.targetBlockPosition = null; - this.targetBlockTime = 0; + this.targetBreakDelay = 0; + this.targetBlockLastStageChangeTime = 0; + this.targetStage = 0; // Remove effect RemoveEntityEffectPacket removeEntityEffectPacket = new RemoveEntityEffectPacket(); removeEntityEffectPacket.entityId = getEntityId(); - removeEntityEffectPacket.effectId = 4; - getPlayerConnection().sendPacket(removeEntityEffectPacket); + removeEntityEffectPacket.effect = PotionType.AWKWARD; + playerConnection.sendPacket(removeEntityEffectPacket); } } @@ -1927,7 +1950,7 @@ public class Player extends LivingEntity implements CommandSender { } /** - * Get the packet to add the player from tab-list + * Get the packet to add the player from the tab-list * * @return a {@link PlayerInfoPacket} to add the player */ @@ -1950,9 +1973,9 @@ public class Player extends LivingEntity implements CommandSender { } /** - * Get the packet to remove the player from tab-list + * Get the packet to remove the player from the tab-list * - * @return a {@link PlayerInfoPacket} to add the player + * @return a {@link PlayerInfoPacket} to remove the player */ protected PlayerInfoPacket getRemovePlayerToList() { PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.REMOVE_PLAYER); @@ -1968,7 +1991,7 @@ public class Player extends LivingEntity implements CommandSender { * Send all the related packet to have the player sent to another with related data * (create player, spawn position, velocity, metadata, equipments, passengers, team) *

- * WARNING: this does not sync the player, please use {@link #addViewer(Player)} + * WARNING: this alone does not sync the player, please use {@link #addViewer(Player)} * * @param connection the connection to show the player to */ diff --git a/src/main/java/net/minestom/server/entity/vehicle/PlayerVehicleInformation.java b/src/main/java/net/minestom/server/entity/vehicle/PlayerVehicleInformation.java index a23775cf7..4bce56318 100644 --- a/src/main/java/net/minestom/server/entity/vehicle/PlayerVehicleInformation.java +++ b/src/main/java/net/minestom/server/entity/vehicle/PlayerVehicleInformation.java @@ -23,6 +23,14 @@ public class PlayerVehicleInformation { return unmount; } + /** + * Refresh internal data + * + * @param sideways the new sideways value + * @param forward the new forward value + * @param jump the new jump value + * @param unmount the new unmount value + */ public void refresh(float sideways, float forward, boolean jump, boolean unmount) { this.sideways = sideways; this.forward = forward; diff --git a/src/main/java/net/minestom/server/instance/InstanceContainer.java b/src/main/java/net/minestom/server/instance/InstanceContainer.java index 2da77fda8..5f5707d89 100644 --- a/src/main/java/net/minestom/server/instance/InstanceContainer.java +++ b/src/main/java/net/minestom/server/instance/InstanceContainer.java @@ -116,8 +116,17 @@ public class InstanceContainer extends Instance { setAlreadyChanged(blockPosition, blockStateId); final int index = ChunkUtils.getBlockIndex(x, y, z); - // Call the destroy listener if previous block was a custom block - callBlockDestroy(chunk, index, blockPosition); + final CustomBlock previousBlock = chunk.getCustomBlock(index); + if (previousBlock != null) { + // Previous block was a custom block + + // Call the destroy listener + callBlockDestroy(chunk, index, previousBlock, blockPosition); + + // Remove digging information for the previous custom block + previousBlock.removeDiggingInformation(this, blockPosition); + } + // Change id based on neighbors blockStateId = executeBlockPlacementRule(blockStateId, blockPosition); @@ -170,13 +179,10 @@ public class InstanceContainer extends Instance { } } - private void callBlockDestroy(Chunk chunk, int index, BlockPosition blockPosition) { - final CustomBlock previousBlock = chunk.getCustomBlock(index); - if (previousBlock != null) { - final Data previousData = chunk.getData(index); - previousBlock.onDestroy(this, blockPosition, previousData); - chunk.UNSAFE_removeCustomBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ()); - } + private void callBlockDestroy(Chunk chunk, int index, CustomBlock previousBlock, BlockPosition blockPosition) { + final Data previousData = chunk.getData(index); + previousBlock.onDestroy(this, blockPosition, previousData); + chunk.UNSAFE_removeCustomBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ()); } private void callBlockPlace(Chunk chunk, int index, BlockPosition blockPosition) { diff --git a/src/main/java/net/minestom/server/instance/MinestomBasicChunkLoader.java b/src/main/java/net/minestom/server/instance/MinestomBasicChunkLoader.java index 33d2962cf..5b525d6ab 100644 --- a/src/main/java/net/minestom/server/instance/MinestomBasicChunkLoader.java +++ b/src/main/java/net/minestom/server/instance/MinestomBasicChunkLoader.java @@ -23,13 +23,18 @@ public class MinestomBasicChunkLoader implements IChunkLoader { LOGGER.warn("No folder to save chunk!"); return; } + final int chunkX = chunk.getChunkX(); final int chunkZ = chunk.getChunkZ(); final String key = getChunkKey(chunkX, chunkZ); final byte[] data = chunk.getSerializedData(); - if (data == null) + if (data == null) { + if (callback != null) + callback.run(); return; + } + storageFolder.set(key, data); if (callback != null) diff --git a/src/main/java/net/minestom/server/instance/block/CustomBlock.java b/src/main/java/net/minestom/server/instance/block/CustomBlock.java index 6d43fd100..5d82b1634 100644 --- a/src/main/java/net/minestom/server/instance/block/CustomBlock.java +++ b/src/main/java/net/minestom/server/instance/block/CustomBlock.java @@ -1,16 +1,28 @@ package net.minestom.server.instance.block; +import it.unimi.dsi.fastutil.objects.Object2ByteMap; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.minestom.server.data.Data; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.gamedata.loottables.LootTable; import net.minestom.server.gamedata.loottables.LootTableManager; import net.minestom.server.instance.BlockModifier; +import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; +import net.minestom.server.network.packet.server.play.BlockBreakAnimationPacket; import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.time.UpdateOption; +import net.minestom.server.utils.validate.Check; import org.jglrxavpok.hephaistos.nbt.NBTCompound; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + /** * Represent the handler of a custom block type. *

@@ -20,10 +32,17 @@ import org.jglrxavpok.hephaistos.nbt.NBTCompound; */ public abstract class CustomBlock { + public static final byte MAX_STAGE = 10; + /** - * TODO - * - option to set the global as "global breaking" meaning that multiple players mining the same block will break it faster (accumulation) + * Instance -> break data + * Used to store block break stage data when {@link #enableMultiPlayerBreaking()} is enabled */ + private final Map instanceBreakDataMap = new HashMap<>(); + + public int getBreakEntityId(Player firstBreaker) { + return firstBreaker.getEntityId() + 1; + } private final short blockStateId; private final String identifier; @@ -106,17 +125,45 @@ public abstract class CustomBlock { public abstract short getCustomBlockId(); /** - * Called at digging start to check for custom breaking time - * Can be set to < 0 to be cancelled, in this case vanilla time will be used + * Called when the player requests the next stage break delay * * @param player the player who is trying to break the block * @param position the block position - * @return the time in ms to break it + * @param stage the current break stage of the block (0-10) + * @param breakers the list containing all the players currently digging this block + * @return the time in tick to pass to the next state, 0 to instant break it. + * @see #enableCustomBreakDelay() to enable/disable it */ - public abstract int getBreakDelay(Player player, BlockPosition position); + public int getBreakDelay(Player player, BlockPosition position, byte stage, Set breakers) { + return 0; + } /** - * @return true if {@link #getUpdateOption()} is not null, false otherwise + * Used to enable the custom break delay from {@link #getBreakDelay(Player, BlockPosition, byte, Set)} + * Disabling it would result in having vanilla time + * + * @return true to enable custom break delay + */ + public boolean enableCustomBreakDelay() { + return false; + } + + /** + * Get if this block breaking time can be reduced by having multiple players + * digging it + *

+ * WARNING: this should be constant, do not change this value halfway + * + * @return true to enable the multi-player breaking feature + */ + public boolean enableMultiPlayerBreaking() { + return false; + } + + /** + * Get if this {@link CustomBlock} requires any tick update + * + * @return true if {@link #getUpdateOption()} is not null and the value is positive */ public boolean hasUpdate() { final UpdateOption updateOption = getUpdateOption(); @@ -239,4 +286,189 @@ public abstract class CustomBlock { public LootTable getLootTable(LootTableManager tableManager) { return null; } + + // BLOCK BREAK METHODS + + /** + * Called when a player start digging this custom block, + * process all necessary data if {@link #enableMultiPlayerBreaking()} is enabled + * + * @param instance the instance of the block + * @param blockPosition the position of the block + * @param player the player who started digging + */ + public void startDigging(Instance instance, BlockPosition blockPosition, Player player) { + // Stay null if multi player breaking is disabled + Set breakers = null; + + if (enableMultiPlayerBreaking()) { + // Multi player breaking enabled, get the breakers and cache some values + InstanceBreakData instanceBreakData = instanceBreakDataMap.computeIfAbsent(instance, i -> new InstanceBreakData()); + + Map> breakersMap = instanceBreakData.breakersMap; + breakers = breakersMap.computeIfAbsent(blockPosition, pos -> new HashSet<>(1)); + breakers.add(player); + + Object2ByteMap breakStageMap = instanceBreakData.breakStageMap; + // Set the block stage to 0, use the previous one if any + if (!breakStageMap.containsKey(blockPosition)) { + breakStageMap.put(blockPosition, (byte) 0); + } + + Object2IntMap breakIdMap = instanceBreakData.breakIdMap; + // Set the entity id used for the packet, otherwise use the previous one + if (!breakIdMap.containsKey(blockPosition)) { + breakIdMap.put(blockPosition, getBreakEntityId(player)); + } + } + + // Set the player target block + player.setTargetBlock(this, blockPosition, breakers); + } + + /** + * Called when a player stop digging a block, + * does remove the block break animation if he was the only breaker + * + * @param instance the instance of the block + * @param blockPosition the position of the block + * @param player the player who stopped digging + */ + public void stopDigging(Instance instance, BlockPosition blockPosition, Player player) { + if (enableMultiPlayerBreaking()) { + // Remove cache data + if (instanceBreakDataMap.containsKey(instance)) { + InstanceBreakData instanceBreakData = instanceBreakDataMap.get(instance); + + Set breakers = instanceBreakData.breakersMap.get(blockPosition); + if (breakers != null) { + breakers.remove(player); + if (breakers.isEmpty()) { + // No remaining breakers + + // Get the entity id assigned to the block break + final int entityId = instanceBreakData.breakIdMap.getInt(blockPosition); + + final Chunk chunk = instance.getChunkAt(blockPosition); + chunk.sendPacketToViewers(new BlockBreakAnimationPacket(entityId, blockPosition, (byte) -1)); + + // Clear cache + removeDiggingInformation(instance, blockPosition); + } + } + + } + } else { + // Stop the breaking animation for the specific player id + final Chunk chunk = instance.getChunkAt(blockPosition); + final int entityId = getBreakEntityId(player); + chunk.sendPacketToViewers(new BlockBreakAnimationPacket(entityId, blockPosition, (byte) -1)); + } + } + + /** + * Process one stage on the block, break it if it excess {@link #MAX_STAGE}, + * only if {@link #enableMultiPlayerBreaking()} is enabled + * + * @param instance the instance of the block + * @param blockPosition the position of the block + * @param player the player who processed one stage on the block + * @return true if the block can continue being digged + * @throws IllegalStateException if {@link #enableMultiPlayerBreaking()} is disabled + */ + public synchronized boolean processStage(Instance instance, BlockPosition blockPosition, Player player) { + Check.stateCondition(!enableMultiPlayerBreaking(), + "CustomBlock#processState requires having the multi player breaking feature enabled"); + + if (instanceBreakDataMap.containsKey(instance)) { + InstanceBreakData instanceBreakData = instanceBreakDataMap.get(instance); + Object2ByteMap breakStageMap = instanceBreakData.breakStageMap; + byte stage = breakStageMap.getByte(blockPosition); + if (stage + 1 >= MAX_STAGE) { + instance.breakBlock(player, blockPosition); + return false; + } else { + + // Get the entity id assigned to the block break + final int entityId = instanceBreakData.breakIdMap.getInt(blockPosition); + + // Send the block break animation + final Chunk chunk = instance.getChunkAt(blockPosition); + chunk.sendPacketToViewers(new BlockBreakAnimationPacket(entityId, blockPosition, stage)); + + // Refresh the stage + breakStageMap.put(blockPosition, ++stage); + return true; + } + } + return false; + } + + public void removeDiggingInformation(Instance instance, BlockPosition blockPosition) { + if (!enableMultiPlayerBreaking()) { + return; + } + + if (instanceBreakDataMap.containsKey(instance)) { + InstanceBreakData instanceBreakData = instanceBreakDataMap.get(instance); + // Remove the block position from all maps + instanceBreakData.clear(blockPosition); + } + } + + /** + * Get all the breakers of a block, only if {@link #enableMultiPlayerBreaking()} is enabled + * + * @param instance the instance of the block + * @param blockPosition the position of the block + * @return the {@link Set} of breakers of a block + * @throws IllegalStateException if {@link #enableMultiPlayerBreaking()} is disabled + */ + public Set getBreakers(Instance instance, BlockPosition blockPosition) { + Check.stateCondition(!enableMultiPlayerBreaking(), + "CustomBlock#getBreakers requires having the multi player breaking feature enabled"); + + if (instanceBreakDataMap.containsKey(instance)) { + InstanceBreakData instanceBreakData = instanceBreakDataMap.get(instance); + return instanceBreakData.breakersMap.get(blockPosition); + } + return null; + } + + /** + * Get the block break stage at a position, only work if {@link #enableMultiPlayerBreaking()} is enabled + * + * @param instance the instance of the custom block + * @param blockPosition the position of the custom block + * @return the break stage at the position. Can also be 0 when nonexistent + */ + public byte getBreakStage(Instance instance, BlockPosition blockPosition) { + Check.stateCondition(!enableMultiPlayerBreaking(), + "CustomBlock#getBreakStage requires having the multi player breaking feature enabled"); + + if (!instanceBreakDataMap.containsKey(instance)) + return 0; + final InstanceBreakData instanceBreakData = instanceBreakDataMap.get(instance); + return instanceBreakData.breakStageMap.getByte(blockPosition); + } + + /** + * Class used to store block break stage + * Only used if multi player breaking is enabled + */ + private class InstanceBreakData { + // Contains all the breakers of a block + private final Map> breakersMap = new HashMap<>(); + // Contains the current break stage of a block + private final Object2ByteMap breakStageMap = new Object2ByteOpenHashMap<>(); + // Contains the entity id used by the block break packet + private final Object2IntMap breakIdMap = new Object2IntOpenHashMap<>(); + + private void clear(BlockPosition blockPosition) { + this.breakersMap.remove(blockPosition); + this.breakStageMap.removeByte(blockPosition); + this.breakIdMap.removeInt(blockPosition); + } + + } } diff --git a/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java b/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java index 50dbdeb7f..4cb69fbcf 100644 --- a/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java +++ b/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java @@ -13,6 +13,7 @@ import net.minestom.server.item.StackingRule; import net.minestom.server.network.packet.client.play.ClientPlayerDiggingPacket; import net.minestom.server.network.packet.server.play.AcknowledgePlayerDiggingPacket; import net.minestom.server.network.packet.server.play.EntityEffectPacket; +import net.minestom.server.potion.PotionType; import net.minestom.server.utils.BlockPosition; public class PlayerDiggingListener { @@ -43,17 +44,17 @@ public class PlayerDiggingListener { } else { final CustomBlock customBlock = instance.getCustomBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ()); if (customBlock != null) { - int breakTime = customBlock.getBreakDelay(player, blockPosition); - // Custom block has a custom break time, allow for digging event PlayerStartDiggingEvent playerStartDiggingEvent = new PlayerStartDiggingEvent(player, blockPosition, customBlock); player.callEvent(PlayerStartDiggingEvent.class, playerStartDiggingEvent); if (!playerStartDiggingEvent.isCancelled()) { + // Start digging the block - if (breakTime >= 0) { - player.setTargetBlock(customBlock, blockPosition, breakTime); + if (customBlock.enableCustomBreakDelay()) { + customBlock.startDigging(instance, blockPosition, player); addEffect(player); } + sendAcknowledgePacket(player, blockPosition, customBlock.getBlockStateId(), ClientPlayerDiggingPacket.Status.STARTED_DIGGING, true); } else { @@ -140,7 +141,7 @@ public class PlayerDiggingListener { private static void addEffect(Player player) { EntityEffectPacket entityEffectPacket = new EntityEffectPacket(); entityEffectPacket.entityId = player.getEntityId(); - entityEffectPacket.effectId = 4; + entityEffectPacket.effect = PotionType.AWKWARD; entityEffectPacket.amplifier = -1; entityEffectPacket.duration = 0; entityEffectPacket.flags = 0; diff --git a/src/main/java/net/minestom/server/network/packet/server/play/BlockBreakAnimationPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/BlockBreakAnimationPacket.java index ec02cb23b..b35efc440 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/BlockBreakAnimationPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/BlockBreakAnimationPacket.java @@ -11,6 +11,16 @@ public class BlockBreakAnimationPacket implements ServerPacket { public BlockPosition blockPosition; public byte destroyStage; + public BlockBreakAnimationPacket() { + + } + + public BlockBreakAnimationPacket(int entityId, BlockPosition blockPosition, byte destroyStage) { + this.entityId = entityId; + this.blockPosition = blockPosition; + this.destroyStage = destroyStage; + } + @Override public void write(BinaryWriter writer) { writer.writeVarInt(entityId); diff --git a/src/main/java/net/minestom/server/network/packet/server/play/EntityEffectPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/EntityEffectPacket.java index 6b7db8448..c7899e5e9 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/EntityEffectPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/EntityEffectPacket.java @@ -2,12 +2,13 @@ package net.minestom.server.network.packet.server.play; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; +import net.minestom.server.potion.PotionType; import net.minestom.server.utils.binary.BinaryWriter; public class EntityEffectPacket implements ServerPacket { public int entityId; - public byte effectId; + public PotionType effect; public byte amplifier; public int duration; public byte flags; @@ -15,7 +16,7 @@ public class EntityEffectPacket implements ServerPacket { @Override public void write(BinaryWriter writer) { writer.writeVarInt(entityId); - writer.writeByte(effectId); + writer.writeByte((byte) effect.getId()); writer.writeByte(amplifier); writer.writeVarInt(duration); writer.writeByte(flags); diff --git a/src/main/java/net/minestom/server/network/packet/server/play/RemoveEntityEffectPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/RemoveEntityEffectPacket.java index f7a661fd7..309430c0b 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/RemoveEntityEffectPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/RemoveEntityEffectPacket.java @@ -2,17 +2,18 @@ package net.minestom.server.network.packet.server.play; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; +import net.minestom.server.potion.PotionType; import net.minestom.server.utils.binary.BinaryWriter; public class RemoveEntityEffectPacket implements ServerPacket { public int entityId; - public byte effectId; + public PotionType effect; @Override public void write(BinaryWriter writer) { writer.writeVarInt(entityId); - writer.writeByte(effectId); + writer.writeByte((byte) effect.getId()); } @Override diff --git a/src/main/java/net/minestom/server/reader/ChunkReader.java b/src/main/java/net/minestom/server/reader/ChunkReader.java index e52c9699d..3b8f30347 100644 --- a/src/main/java/net/minestom/server/reader/ChunkReader.java +++ b/src/main/java/net/minestom/server/reader/ChunkReader.java @@ -46,7 +46,6 @@ public class ChunkReader { Data data = null; { final boolean hasData = binaryReader.readBoolean(); - // Data deserializer if (hasData) { data = DataReader.readData(binaryReader); diff --git a/src/main/java/net/minestom/server/reader/DataReader.java b/src/main/java/net/minestom/server/reader/DataReader.java index 5441b7d0a..ba9bfd88f 100644 --- a/src/main/java/net/minestom/server/reader/DataReader.java +++ b/src/main/java/net/minestom/server/reader/DataReader.java @@ -33,19 +33,23 @@ public class DataReader { break; } - final String className; + // Get the class type + final Class type; { final byte[] typeCache = reader.readBytes(typeLength); - className = new String(typeCache); + final String className = new String(typeCache); + + type = Class.forName(className); } - final Class type = Class.forName(className); - + // Get the key final String name = reader.readSizedString(); + // Get the data final Object value = DATA_MANAGER.getDataType(type).decode(reader); + // Set the data data.set(name, value, type); } } catch (ClassNotFoundException e) { diff --git a/src/main/java/net/minestom/server/utils/NamespaceID.java b/src/main/java/net/minestom/server/utils/NamespaceID.java index fd0ead4ce..bb74b8e8b 100644 --- a/src/main/java/net/minestom/server/utils/NamespaceID.java +++ b/src/main/java/net/minestom/server/utils/NamespaceID.java @@ -7,7 +7,7 @@ import java.util.Objects; /** * Represents a namespaced ID * https://minecraft.gamepedia.com/Namespaced_ID - * + *

* TODO: Implement validity conditions */ public class NamespaceID implements CharSequence { @@ -20,12 +20,13 @@ public class NamespaceID implements CharSequence { /** * Extracts the domain from the namespace ID. "minecraft:stone" would return "minecraft". * If no ':' character is found, "minecraft" is returned. + * * @param namespaceID * @return the domain of the namespace ID */ public static String getDomain(String namespaceID) { - int index = namespaceID.indexOf(':'); - if(index < 0) + final int index = namespaceID.indexOf(':'); + if (index < 0) return "minecraft"; return namespaceID.substring(0, index); } @@ -33,14 +34,15 @@ public class NamespaceID implements CharSequence { /** * Extracts the path from the namespace ID. "minecraft:blocks/stone" would return "blocks/stone". * If no ':' character is found, the

namespaceID
is returned. + * * @param namespaceID * @return the path of the namespace ID */ public static String getPath(String namespaceID) { - int index = namespaceID.indexOf(':'); - if(index < 0) + final int index = namespaceID.indexOf(':'); + if (index < 0) return namespaceID; - return namespaceID.substring(index+1); + return namespaceID.substring(index + 1); } static int hash(String domain, String path) { @@ -48,7 +50,7 @@ public class NamespaceID implements CharSequence { } public static NamespaceID from(String domain, String path) { - int hash = hash(domain, path); + final int hash = hash(domain, path); return cache.computeIfAbsent(hash, _unused -> new NamespaceID(domain, path)); } @@ -57,13 +59,13 @@ public class NamespaceID implements CharSequence { } private NamespaceID(String path) { - int index = path.indexOf(':'); - if(index < 0) { + final int index = path.indexOf(':'); + if (index < 0) { this.domain = "minecraft"; this.path = path; } else { this.domain = path.substring(0, index); - this.path = path.substring(index+1); + this.path = path.substring(index + 1); } this.full = toString(); } @@ -113,7 +115,7 @@ public class NamespaceID implements CharSequence { @Override public String toString() { - return domain+":"+path; + return domain + ":" + path; } } diff --git a/src/main/java/net/minestom/server/world/biomes/BiomeManager.java b/src/main/java/net/minestom/server/world/biomes/BiomeManager.java index ab734e010..982c53e0d 100644 --- a/src/main/java/net/minestom/server/world/biomes/BiomeManager.java +++ b/src/main/java/net/minestom/server/world/biomes/BiomeManager.java @@ -11,72 +11,76 @@ import java.util.List; /** * Allows servers to register custom dimensions. Also used during player joining to send the list of all existing dimensions. - * + *

* Contains {@link Biome#PLAINS} by default but can be removed. */ public class BiomeManager { - private final List biomes = new LinkedList<>(); + private final List biomes = new LinkedList<>(); - public BiomeManager() { - addBiome(Biome.PLAINS); - } + public BiomeManager() { + addBiome(Biome.PLAINS); + } - /** - * Add a new biome. This does NOT send the new list to players. - * @param biome - */ - public void addBiome(Biome biome) { - biomes.add(biome); - } + /** + * Add a new biome. This does NOT send the new list to players. + * + * @param biome the biome to add + */ + public void addBiome(Biome biome) { + biomes.add(biome); + } - /** - * Removes a biome. This does NOT send the new list to players. - * @param biome - * @return if the biome type was removed, false if it was not present before - */ - public boolean removeBiome(Biome biome) { - return biomes.remove(biome); - } + /** + * Removes a biome. This does NOT send the new list to players. + * + * @param biome the biome to remove + * @return true if the biome type was removed, false if it was not present before + */ + public boolean removeBiome(Biome biome) { + return biomes.remove(biome); + } - /** - * Returns an immutable copy of the biomes already registered - * @return - */ - public List unmodifiableList() { - return Collections.unmodifiableList(biomes); - } + /** + * Returns an immutable copy of the biomes already registered + * + * @return an immutable copy of the biomes already registered + */ + public List unmodifiableList() { + return Collections.unmodifiableList(biomes); + } - public Biome getById(int id) { - Biome biome = null; - for (final Biome biomeT : biomes) { - if (biomeT.getId() == id) { - biome = biomeT; - break; - } - } - return biome; - } + // TODO optimize for fast get + public Biome getById(int id) { + Biome biome = null; + for (final Biome biomeT : biomes) { + if (biomeT.getId() == id) { + biome = biomeT; + break; + } + } + return biome; + } - public Biome getByName(NamespaceID namespaceID) { - Biome biome = null; - for (final Biome biomeT : biomes) { - if (biomeT.getName().equals(namespaceID)) { - biome = biomeT; - break; - } - } - return biome; - } + public Biome getByName(NamespaceID namespaceID) { + Biome biome = null; + for (final Biome biomeT : biomes) { + if (biomeT.getName().equals(namespaceID)) { + biome = biomeT; + break; + } + } + return biome; + } - public NBTCompound toNBT() { - NBTCompound biomes = new NBTCompound(); - biomes.setString("type", "minecraft:worldgen/biome"); - NBTList biomesList = new NBTList<>(NBTTypes.TAG_Compound); - for (Biome biome : this.biomes) { - biomesList.add(biome.toNbt()); - } - biomes.set("value", biomesList); - return biomes; - } + public NBTCompound toNBT() { + NBTCompound biomes = new NBTCompound(); + biomes.setString("type", "minecraft:worldgen/biome"); + NBTList biomesList = new NBTList<>(NBTTypes.TAG_Compound); + for (Biome biome : this.biomes) { + biomesList.add(biome.toNbt()); + } + biomes.set("value", biomesList); + return biomes; + } }