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.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);
}
});

View File

@ -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;
}
}

View File

@ -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<Player> breakers) {
return 2;
}
@Override
public boolean enableCustomBreakDelay() {
return true;
}
@Override
public boolean enableMultiPlayerBreaking() {
return true;
}
@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.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<Player> breakers) {
return 1;
}
@Override
public boolean enableCustomBreakDelay() {
return true;
}
@Override

View File

@ -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;
}

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.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<Player> 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<Player> 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"
* <a href="https://wiki.vg/Protocol#Player_Abilities_.28clientbound.29">see</a>
* <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
*/
@ -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<Player> 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<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
*/
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)
* <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
*/

View File

@ -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;

View File

@ -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) {

View File

@ -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)

View File

@ -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.
* <p>
@ -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<Instance, InstanceBreakData> 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<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() {
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<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.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;

View File

@ -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);

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.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);

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.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

View File

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

View File

@ -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) {

View File

@ -7,7 +7,7 @@ import java.util.Objects;
/**
* Represents a namespaced ID
* https://minecraft.gamepedia.com/Namespaced_ID
*
* <p>
* 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 <pre>namespaceID</pre> 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;
}
}

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.
*
* <p>
* Contains {@link Biome#PLAINS} by default but can be removed.
*/
public class BiomeManager {
private final List<Biome> biomes = new LinkedList<>();
private final List<Biome> 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<Biome> 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<Biome> 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<NBTCompound> 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<NBTCompound> biomesList = new NBTList<>(NBTTypes.TAG_Compound);
for (Biome biome : this.biomes) {
biomesList.add(biome.toNbt());
}
biomes.set("value", biomesList);
return biomes;
}
}