Rewrite of the CustomBlock break delay system + support for multi player digging

This commit is contained in:
themode 2020-08-20 02:42:27 +02:00
parent b8c30d9b58
commit 5b394e5bf7
18 changed files with 475 additions and 164 deletions

View File

@ -30,13 +30,12 @@ import net.minestom.server.item.metadata.MapMeta;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
import net.minestom.server.ping.ResponseDataConsumer; import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.scoreboard.Sidebar; 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.BlockPosition;
import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.Position; import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector; import net.minestom.server.utils.Vector;
import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.world.DimensionType;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -48,11 +47,11 @@ public class PlayerInit {
private static volatile Inventory inventory; private static volatile Inventory inventory;
static { 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(); ChunkGeneratorDemo chunkGeneratorDemo = new ChunkGeneratorDemo();
NoiseTestGenerator noiseTestGenerator = new NoiseTestGenerator(); NoiseTestGenerator noiseTestGenerator = new NoiseTestGenerator();
instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(storageFolder); //instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(storageFolder);
//instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(DimensionType.OVERWORLD); instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(DimensionType.OVERWORLD);
instanceContainer.enableAutoChunkLoad(true); instanceContainer.enableAutoChunkLoad(true);
//instanceContainer.setChunkDecider((x,y) -> (pos) -> pos.getY()>40?(short)0:(short)1); //instanceContainer.setChunkDecider((x,y) -> (pos) -> pos.getY()>40?(short)0:(short)1);
instanceContainer.setChunkGenerator(noiseTestGenerator); instanceContainer.setChunkGenerator(noiseTestGenerator);
@ -272,7 +271,7 @@ public class PlayerInit {
}); });
player.addEventCallback(PlayerSpawnEvent.class, event -> { player.addEventCallback(PlayerSpawnEvent.class, event -> {
player.setGameMode(GameMode.CREATIVE); player.setGameMode(GameMode.SURVIVAL);
player.teleport(new Position(0, 41f, 0)); player.teleport(new Position(0, 41f, 0));
//player.setHeldItemSlot((byte) 5); //player.setHeldItemSlot((byte) 5);
@ -390,7 +389,7 @@ public class PlayerInit {
// Unload the chunk (save memory) if it has no remaining viewer // Unload the chunk (save memory) if it has no remaining viewer
if (chunk.getViewers().isEmpty()) { if (chunk.getViewers().isEmpty()) {
player.getInstance().unloadChunk(chunk); //player.getInstance().unloadChunk(chunk);
} }
}); });

View File

@ -20,8 +20,8 @@ public class BurningTorchBlock extends CustomBlock {
@Override @Override
public void handleContact(Instance instance, BlockPosition position, Entity touching) { public void handleContact(Instance instance, BlockPosition position, Entity touching) {
System.out.println("touching "+touching); System.out.println("touching " + touching);
if(touching instanceof LivingEntity) { if (touching instanceof LivingEntity) {
((LivingEntity) touching).damage(DamageType.GRAVITY, 0.1f); ((LivingEntity) touching).damage(DamageType.GRAVITY, 0.1f);
} }
} }
@ -45,9 +45,4 @@ public class BurningTorchBlock extends CustomBlock {
public short getCustomBlockId() { public short getCustomBlockId() {
return 3; return 3;
} }
@Override
public int getBreakDelay(Player player, BlockPosition position) {
return -1;
}
} }

View File

@ -7,6 +7,8 @@ import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
import java.util.Set;
public class StoneBlock extends CustomBlock { public class StoneBlock extends CustomBlock {
public StoneBlock() { public StoneBlock() {
@ -39,8 +41,18 @@ public class StoneBlock extends CustomBlock {
} }
@Override @Override
public int getBreakDelay(Player player, BlockPosition position) { public int getBreakDelay(Player player, BlockPosition position, byte stage, Set<Player> breakers) {
return 750; return 2;
}
@Override
public boolean enableCustomBreakDelay() {
return true;
}
@Override
public boolean enableMultiPlayerBreaking() {
return true;
} }
@Override @Override

View File

@ -9,6 +9,8 @@ import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption; import net.minestom.server.utils.time.UpdateOption;
import java.util.Set;
public class UpdatableBlockDemo extends CustomBlock { public class UpdatableBlockDemo extends CustomBlock {
private static final UpdateOption UPDATE_OPTION = new UpdateOption(20, TimeUnit.TICK); private static final UpdateOption UPDATE_OPTION = new UpdateOption(20, TimeUnit.TICK);
@ -43,8 +45,13 @@ public class UpdatableBlockDemo extends CustomBlock {
} }
@Override @Override
public int getBreakDelay(Player player, BlockPosition position) { public int getBreakDelay(Player player, BlockPosition position, byte stage, Set<Player> breakers) {
return 500; return 1;
}
@Override
public boolean enableCustomBreakDelay() {
return true;
} }
@Override @Override

View File

@ -67,7 +67,9 @@ public class SimpleCommand implements CommandProcessor {
NotificationCenter.send(notification, player); NotificationCenter.send(notification, player);
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; return true;
} }

View File

@ -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.packet.server.play.*;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.permission.Permission; import net.minestom.server.permission.Permission;
import net.minestom.server.potion.PotionType;
import net.minestom.server.recipe.Recipe; import net.minestom.server.recipe.Recipe;
import net.minestom.server.recipe.RecipeManager; import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.resourcepack.ResourcePack; import net.minestom.server.resourcepack.ResourcePack;
@ -102,9 +103,10 @@ public class Player extends LivingEntity implements CommandSender {
// CustomBlock break delay // CustomBlock break delay
private CustomBlock targetCustomBlock; private CustomBlock targetCustomBlock;
private BlockPosition targetBlockPosition; private BlockPosition targetBlockPosition;
private long targetBlockTime; private long targetBreakDelay; // The last break delay requested
private byte targetLastStage; private long targetBlockLastStageChangeTime; // Time at which the block stage last changed
private int blockBreakTime; private byte targetStage; // The current stage of the target block, only if multi player breaking is disabled
private final Set<Player> targetBreakers = new HashSet<>(1); // Only used if multi player breaking is disabled, contains only this player
private BelowNameTag belowNameTag; private BelowNameTag belowNameTag;
@ -159,6 +161,9 @@ public class Player extends LivingEntity implements CommandSender {
this.levelType = LevelType.FLAT; this.levelType = LevelType.FLAT;
refreshPosition(0, 0, 0); refreshPosition(0, 0, 0);
// Used to cache the breaker for single custom block breaking
this.targetBreakers.add(this);
// FakePlayer init its connection there // FakePlayer init its connection there
playerConnectionInit(); playerConnectionInit();
@ -300,17 +305,39 @@ public class Player extends LivingEntity implements CommandSender {
// Target block stage // Target block stage
if (targetCustomBlock != null) { if (targetCustomBlock != null) {
final byte animationCount = 10; final boolean processStage = (time - targetBlockLastStageChangeTime) >= targetBreakDelay;
final long since = time - targetBlockTime; if (processStage) {
byte stage = (byte) (since / (blockBreakTime / animationCount)); // Should increment the target block stage
stage = MathUtils.setBetween(stage, (byte) -1, animationCount); if (targetCustomBlock.enableMultiPlayerBreaking()) {
if (stage != targetLastStage) { // Let the custom block object manages the breaking
sendBlockBreakAnimation(targetBlockPosition, stage); final boolean canContinue = this.targetCustomBlock.processStage(instance, targetBlockPosition, this);
} if (canContinue) {
this.targetLastStage = stage; final Set<Player> breakers = targetCustomBlock.getBreakers(instance, targetBlockPosition);
if (stage > 9) { refreshBreakDelay(breakers);
instance.breakBlock(this, targetBlockPosition); this.targetBlockLastStageChangeTime = time;
resetTargetBlock(); } 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); 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 @Override
public void sendMessage(String message) { public void sendMessage(String message) {
sendMessage(ColoredText.of(message)); sendMessage(ColoredText.of(message));
@ -1690,7 +1699,7 @@ public class Player extends LivingEntity implements CommandSender {
* Change the player ability "Creative Mode" * Change the player ability "Creative Mode"
* <a href="https://wiki.vg/Protocol#Player_Abilities_.28clientbound.29">see</a> * <a href="https://wiki.vg/Protocol#Player_Abilities_.28clientbound.29">see</a>
* <p> * <p>
* 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 * @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 targetCustomBlock the custom block to dig
* @param targetBlockPosition the custom block position * @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<Player> breakers) {
this.targetCustomBlock = targetCustomBlock; this.targetCustomBlock = targetCustomBlock;
this.targetBlockPosition = targetBlockPosition; 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<Player> 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 * If the currently mined block (or if there isn't any) is not a CustomBlock, nothing append
*/ */
public void resetTargetBlock() { public void resetTargetBlock() {
if (targetBlockPosition != null) { if (targetCustomBlock != null) {
sendBlockBreakAnimation(targetBlockPosition, (byte) -1); // Clear the break animation targetCustomBlock.stopDigging(instance, targetBlockPosition, this);
this.targetCustomBlock = null; this.targetCustomBlock = null;
this.targetBlockPosition = null; this.targetBlockPosition = null;
this.targetBlockTime = 0; this.targetBreakDelay = 0;
this.targetBlockLastStageChangeTime = 0;
this.targetStage = 0;
// Remove effect // Remove effect
RemoveEntityEffectPacket removeEntityEffectPacket = new RemoveEntityEffectPacket(); RemoveEntityEffectPacket removeEntityEffectPacket = new RemoveEntityEffectPacket();
removeEntityEffectPacket.entityId = getEntityId(); removeEntityEffectPacket.entityId = getEntityId();
removeEntityEffectPacket.effectId = 4; removeEntityEffectPacket.effect = PotionType.AWKWARD;
getPlayerConnection().sendPacket(removeEntityEffectPacket); 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 * @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() { protected PlayerInfoPacket getRemovePlayerToList() {
PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.REMOVE_PLAYER); 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 * Send all the related packet to have the player sent to another with related data
* (create player, spawn position, velocity, metadata, equipments, passengers, team) * (create player, spawn position, velocity, metadata, equipments, passengers, team)
* <p> * <p>
* 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 * @param connection the connection to show the player to
*/ */

View File

@ -23,6 +23,14 @@ public class PlayerVehicleInformation {
return unmount; 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) { public void refresh(float sideways, float forward, boolean jump, boolean unmount) {
this.sideways = sideways; this.sideways = sideways;
this.forward = forward; this.forward = forward;

View File

@ -116,8 +116,17 @@ public class InstanceContainer extends Instance {
setAlreadyChanged(blockPosition, blockStateId); setAlreadyChanged(blockPosition, blockStateId);
final int index = ChunkUtils.getBlockIndex(x, y, z); final int index = ChunkUtils.getBlockIndex(x, y, z);
// Call the destroy listener if previous block was a custom block final CustomBlock previousBlock = chunk.getCustomBlock(index);
callBlockDestroy(chunk, index, blockPosition); 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 // Change id based on neighbors
blockStateId = executeBlockPlacementRule(blockStateId, blockPosition); blockStateId = executeBlockPlacementRule(blockStateId, blockPosition);
@ -170,13 +179,10 @@ public class InstanceContainer extends Instance {
} }
} }
private void callBlockDestroy(Chunk chunk, int index, BlockPosition blockPosition) { private void callBlockDestroy(Chunk chunk, int index, CustomBlock previousBlock, BlockPosition blockPosition) {
final CustomBlock previousBlock = chunk.getCustomBlock(index); final Data previousData = chunk.getData(index);
if (previousBlock != null) { previousBlock.onDestroy(this, blockPosition, previousData);
final Data previousData = chunk.getData(index); chunk.UNSAFE_removeCustomBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ());
previousBlock.onDestroy(this, blockPosition, previousData);
chunk.UNSAFE_removeCustomBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ());
}
} }
private void callBlockPlace(Chunk chunk, int index, BlockPosition blockPosition) { private void callBlockPlace(Chunk chunk, int index, BlockPosition blockPosition) {

View File

@ -23,13 +23,18 @@ public class MinestomBasicChunkLoader implements IChunkLoader {
LOGGER.warn("No folder to save chunk!"); LOGGER.warn("No folder to save chunk!");
return; return;
} }
final int chunkX = chunk.getChunkX(); final int chunkX = chunk.getChunkX();
final int chunkZ = chunk.getChunkZ(); final int chunkZ = chunk.getChunkZ();
final String key = getChunkKey(chunkX, chunkZ); final String key = getChunkKey(chunkX, chunkZ);
final byte[] data = chunk.getSerializedData(); final byte[] data = chunk.getSerializedData();
if (data == null) if (data == null) {
if (callback != null)
callback.run();
return; return;
}
storageFolder.set(key, data); storageFolder.set(key, data);
if (callback != null) if (callback != null)

View File

@ -1,16 +1,28 @@
package net.minestom.server.instance.block; 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.data.Data;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.gamedata.loottables.LootTable; import net.minestom.server.gamedata.loottables.LootTable;
import net.minestom.server.gamedata.loottables.LootTableManager; import net.minestom.server.gamedata.loottables.LootTableManager;
import net.minestom.server.instance.BlockModifier; import net.minestom.server.instance.BlockModifier;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance; 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.BlockPosition;
import net.minestom.server.utils.time.UpdateOption; import net.minestom.server.utils.time.UpdateOption;
import net.minestom.server.utils.validate.Check;
import org.jglrxavpok.hephaistos.nbt.NBTCompound; 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. * Represent the handler of a custom block type.
* <p> * <p>
@ -20,10 +32,17 @@ import org.jglrxavpok.hephaistos.nbt.NBTCompound;
*/ */
public abstract class CustomBlock { public abstract class CustomBlock {
public static final byte MAX_STAGE = 10;
/** /**
* TODO * Instance -> break data
* - option to set the global as "global breaking" meaning that multiple players mining the same block will break it faster (accumulation) * Used to store block break stage data when {@link #enableMultiPlayerBreaking()} is enabled
*/ */
private final Map<Instance, InstanceBreakData> instanceBreakDataMap = new HashMap<>();
public int getBreakEntityId(Player firstBreaker) {
return firstBreaker.getEntityId() + 1;
}
private final short blockStateId; private final short blockStateId;
private final String identifier; private final String identifier;
@ -106,17 +125,45 @@ public abstract class CustomBlock {
public abstract short getCustomBlockId(); public abstract short getCustomBlockId();
/** /**
* Called at digging start to check for custom breaking time * Called when the player requests the next stage break delay
* Can be set to < 0 to be cancelled, in this case vanilla time will be used
* *
* @param player the player who is trying to break the block * @param player the player who is trying to break the block
* @param position the block position * @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<Player> 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
* <p>
* 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() { public boolean hasUpdate() {
final UpdateOption updateOption = getUpdateOption(); final UpdateOption updateOption = getUpdateOption();
@ -239,4 +286,189 @@ public abstract class CustomBlock {
public LootTable getLootTable(LootTableManager tableManager) { public LootTable getLootTable(LootTableManager tableManager) {
return null; 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<Player> breakers = null;
if (enableMultiPlayerBreaking()) {
// Multi player breaking enabled, get the breakers and cache some values
InstanceBreakData instanceBreakData = instanceBreakDataMap.computeIfAbsent(instance, i -> new InstanceBreakData());
Map<BlockPosition, Set<Player>> breakersMap = instanceBreakData.breakersMap;
breakers = breakersMap.computeIfAbsent(blockPosition, pos -> new HashSet<>(1));
breakers.add(player);
Object2ByteMap<BlockPosition> 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<BlockPosition> 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<Player> 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<BlockPosition> 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<Player> 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<BlockPosition, Set<Player>> breakersMap = new HashMap<>();
// Contains the current break stage of a block
private final Object2ByteMap<BlockPosition> breakStageMap = new Object2ByteOpenHashMap<>();
// Contains the entity id used by the block break packet
private final Object2IntMap<BlockPosition> breakIdMap = new Object2IntOpenHashMap<>();
private void clear(BlockPosition blockPosition) {
this.breakersMap.remove(blockPosition);
this.breakStageMap.removeByte(blockPosition);
this.breakIdMap.removeInt(blockPosition);
}
}
} }

View File

@ -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.client.play.ClientPlayerDiggingPacket;
import net.minestom.server.network.packet.server.play.AcknowledgePlayerDiggingPacket; import net.minestom.server.network.packet.server.play.AcknowledgePlayerDiggingPacket;
import net.minestom.server.network.packet.server.play.EntityEffectPacket; import net.minestom.server.network.packet.server.play.EntityEffectPacket;
import net.minestom.server.potion.PotionType;
import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
public class PlayerDiggingListener { public class PlayerDiggingListener {
@ -43,17 +44,17 @@ public class PlayerDiggingListener {
} else { } else {
final CustomBlock customBlock = instance.getCustomBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ()); final CustomBlock customBlock = instance.getCustomBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ());
if (customBlock != null) { if (customBlock != null) {
int breakTime = customBlock.getBreakDelay(player, blockPosition);
// Custom block has a custom break time, allow for digging event // Custom block has a custom break time, allow for digging event
PlayerStartDiggingEvent playerStartDiggingEvent = new PlayerStartDiggingEvent(player, blockPosition, customBlock); PlayerStartDiggingEvent playerStartDiggingEvent = new PlayerStartDiggingEvent(player, blockPosition, customBlock);
player.callEvent(PlayerStartDiggingEvent.class, playerStartDiggingEvent); player.callEvent(PlayerStartDiggingEvent.class, playerStartDiggingEvent);
if (!playerStartDiggingEvent.isCancelled()) { if (!playerStartDiggingEvent.isCancelled()) {
// Start digging the block // Start digging the block
if (breakTime >= 0) { if (customBlock.enableCustomBreakDelay()) {
player.setTargetBlock(customBlock, blockPosition, breakTime); customBlock.startDigging(instance, blockPosition, player);
addEffect(player); addEffect(player);
} }
sendAcknowledgePacket(player, blockPosition, customBlock.getBlockStateId(), sendAcknowledgePacket(player, blockPosition, customBlock.getBlockStateId(),
ClientPlayerDiggingPacket.Status.STARTED_DIGGING, true); ClientPlayerDiggingPacket.Status.STARTED_DIGGING, true);
} else { } else {
@ -140,7 +141,7 @@ public class PlayerDiggingListener {
private static void addEffect(Player player) { private static void addEffect(Player player) {
EntityEffectPacket entityEffectPacket = new EntityEffectPacket(); EntityEffectPacket entityEffectPacket = new EntityEffectPacket();
entityEffectPacket.entityId = player.getEntityId(); entityEffectPacket.entityId = player.getEntityId();
entityEffectPacket.effectId = 4; entityEffectPacket.effect = PotionType.AWKWARD;
entityEffectPacket.amplifier = -1; entityEffectPacket.amplifier = -1;
entityEffectPacket.duration = 0; entityEffectPacket.duration = 0;
entityEffectPacket.flags = 0; entityEffectPacket.flags = 0;

View File

@ -11,6 +11,16 @@ public class BlockBreakAnimationPacket implements ServerPacket {
public BlockPosition blockPosition; public BlockPosition blockPosition;
public byte destroyStage; public byte destroyStage;
public BlockBreakAnimationPacket() {
}
public BlockBreakAnimationPacket(int entityId, BlockPosition blockPosition, byte destroyStage) {
this.entityId = entityId;
this.blockPosition = blockPosition;
this.destroyStage = destroyStage;
}
@Override @Override
public void write(BinaryWriter writer) { public void write(BinaryWriter writer) {
writer.writeVarInt(entityId); writer.writeVarInt(entityId);

View File

@ -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.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier; import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.potion.PotionType;
import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.BinaryWriter;
public class EntityEffectPacket implements ServerPacket { public class EntityEffectPacket implements ServerPacket {
public int entityId; public int entityId;
public byte effectId; public PotionType effect;
public byte amplifier; public byte amplifier;
public int duration; public int duration;
public byte flags; public byte flags;
@ -15,7 +16,7 @@ public class EntityEffectPacket implements ServerPacket {
@Override @Override
public void write(BinaryWriter writer) { public void write(BinaryWriter writer) {
writer.writeVarInt(entityId); writer.writeVarInt(entityId);
writer.writeByte(effectId); writer.writeByte((byte) effect.getId());
writer.writeByte(amplifier); writer.writeByte(amplifier);
writer.writeVarInt(duration); writer.writeVarInt(duration);
writer.writeByte(flags); writer.writeByte(flags);

View File

@ -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.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier; import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.potion.PotionType;
import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.BinaryWriter;
public class RemoveEntityEffectPacket implements ServerPacket { public class RemoveEntityEffectPacket implements ServerPacket {
public int entityId; public int entityId;
public byte effectId; public PotionType effect;
@Override @Override
public void write(BinaryWriter writer) { public void write(BinaryWriter writer) {
writer.writeVarInt(entityId); writer.writeVarInt(entityId);
writer.writeByte(effectId); writer.writeByte((byte) effect.getId());
} }
@Override @Override

View File

@ -46,7 +46,6 @@ public class ChunkReader {
Data data = null; Data data = null;
{ {
final boolean hasData = binaryReader.readBoolean(); final boolean hasData = binaryReader.readBoolean();
// Data deserializer // Data deserializer
if (hasData) { if (hasData) {
data = DataReader.readData(binaryReader); data = DataReader.readData(binaryReader);

View File

@ -33,19 +33,23 @@ public class DataReader {
break; break;
} }
final String className; // Get the class type
final Class type;
{ {
final byte[] typeCache = reader.readBytes(typeLength); 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(); final String name = reader.readSizedString();
// Get the data
final Object value = DATA_MANAGER.getDataType(type).decode(reader); final Object value = DATA_MANAGER.getDataType(type).decode(reader);
// Set the data
data.set(name, value, type); data.set(name, value, type);
} }
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {

View File

@ -7,7 +7,7 @@ import java.util.Objects;
/** /**
* Represents a namespaced ID * Represents a namespaced ID
* https://minecraft.gamepedia.com/Namespaced_ID * https://minecraft.gamepedia.com/Namespaced_ID
* * <p>
* TODO: Implement validity conditions * TODO: Implement validity conditions
*/ */
public class NamespaceID implements CharSequence { 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". * Extracts the domain from the namespace ID. "minecraft:stone" would return "minecraft".
* If no ':' character is found, "minecraft" is returned. * If no ':' character is found, "minecraft" is returned.
*
* @param namespaceID * @param namespaceID
* @return the domain of the namespace ID * @return the domain of the namespace ID
*/ */
public static String getDomain(String namespaceID) { public static String getDomain(String namespaceID) {
int index = namespaceID.indexOf(':'); final int index = namespaceID.indexOf(':');
if(index < 0) if (index < 0)
return "minecraft"; return "minecraft";
return namespaceID.substring(0, index); 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". * Extracts the path from the namespace ID. "minecraft:blocks/stone" would return "blocks/stone".
* If no ':' character is found, the <pre>namespaceID</pre> is returned. * If no ':' character is found, the <pre>namespaceID</pre> is returned.
*
* @param namespaceID * @param namespaceID
* @return the path of the namespace ID * @return the path of the namespace ID
*/ */
public static String getPath(String namespaceID) { public static String getPath(String namespaceID) {
int index = namespaceID.indexOf(':'); final int index = namespaceID.indexOf(':');
if(index < 0) if (index < 0)
return namespaceID; return namespaceID;
return namespaceID.substring(index+1); return namespaceID.substring(index + 1);
} }
static int hash(String domain, String path) { static int hash(String domain, String path) {
@ -48,7 +50,7 @@ public class NamespaceID implements CharSequence {
} }
public static NamespaceID from(String domain, String path) { 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)); return cache.computeIfAbsent(hash, _unused -> new NamespaceID(domain, path));
} }
@ -57,13 +59,13 @@ public class NamespaceID implements CharSequence {
} }
private NamespaceID(String path) { private NamespaceID(String path) {
int index = path.indexOf(':'); final int index = path.indexOf(':');
if(index < 0) { if (index < 0) {
this.domain = "minecraft"; this.domain = "minecraft";
this.path = path; this.path = path;
} else { } else {
this.domain = path.substring(0, index); this.domain = path.substring(0, index);
this.path = path.substring(index+1); this.path = path.substring(index + 1);
} }
this.full = toString(); this.full = toString();
} }
@ -113,7 +115,7 @@ public class NamespaceID implements CharSequence {
@Override @Override
public String toString() { public String toString() {
return domain+":"+path; return domain + ":" + path;
} }
} }

View File

@ -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. * Allows servers to register custom dimensions. Also used during player joining to send the list of all existing dimensions.
* * <p>
* Contains {@link Biome#PLAINS} by default but can be removed. * Contains {@link Biome#PLAINS} by default but can be removed.
*/ */
public class BiomeManager { public class BiomeManager {
private final List<Biome> biomes = new LinkedList<>(); private final List<Biome> biomes = new LinkedList<>();
public BiomeManager() { public BiomeManager() {
addBiome(Biome.PLAINS); addBiome(Biome.PLAINS);
} }
/** /**
* Add a new biome. This does NOT send the new list to players. * Add a new biome. This does NOT send the new list to players.
* @param biome *
*/ * @param biome the biome to add
public void addBiome(Biome biome) { */
biomes.add(biome); public void addBiome(Biome biome) {
} biomes.add(biome);
}
/** /**
* Removes a biome. This does NOT send the new list to players. * 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 * @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); public boolean removeBiome(Biome biome) {
} return biomes.remove(biome);
}
/** /**
* Returns an immutable copy of the biomes already registered * Returns an immutable copy of the biomes already registered
* @return *
*/ * @return an immutable copy of the biomes already registered
public List<Biome> unmodifiableList() { */
return Collections.unmodifiableList(biomes); public List<Biome> unmodifiableList() {
} return Collections.unmodifiableList(biomes);
}
public Biome getById(int id) { // TODO optimize for fast get
Biome biome = null; public Biome getById(int id) {
for (final Biome biomeT : biomes) { Biome biome = null;
if (biomeT.getId() == id) { for (final Biome biomeT : biomes) {
biome = biomeT; if (biomeT.getId() == id) {
break; biome = biomeT;
} break;
} }
return biome; }
} return biome;
}
public Biome getByName(NamespaceID namespaceID) { public Biome getByName(NamespaceID namespaceID) {
Biome biome = null; Biome biome = null;
for (final Biome biomeT : biomes) { for (final Biome biomeT : biomes) {
if (biomeT.getName().equals(namespaceID)) { if (biomeT.getName().equals(namespaceID)) {
biome = biomeT; biome = biomeT;
break; break;
} }
} }
return biome; return biome;
} }
public NBTCompound toNBT() { public NBTCompound toNBT() {
NBTCompound biomes = new NBTCompound(); NBTCompound biomes = new NBTCompound();
biomes.setString("type", "minecraft:worldgen/biome"); biomes.setString("type", "minecraft:worldgen/biome");
NBTList<NBTCompound> biomesList = new NBTList<>(NBTTypes.TAG_Compound); NBTList<NBTCompound> biomesList = new NBTList<>(NBTTypes.TAG_Compound);
for (Biome biome : this.biomes) { for (Biome biome : this.biomes) {
biomesList.add(biome.toNbt()); biomesList.add(biome.toNbt());
} }
biomes.set("value", biomesList); biomes.set("value", biomesList);
return biomes; return biomes;
} }
} }