Conflicts:
	src/main/java/net/minestom/server/benchmark/BenchmarkManager.java
This commit is contained in:
Eoghanmc22 2020-11-25 11:29:50 -05:00
commit 296d645400
34 changed files with 367 additions and 238 deletions

5
.github/README.md vendored
View File

@ -5,10 +5,9 @@
[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![discord-banner](https://discordapp.com/api/guilds/706185253441634317/widget.png?style=banner2)](https://discord.gg/pkFRvqB)
Minestom is an alternative to the popular Minecraft server API called Bukkit.
Minestom is a complete rewrite of Minecraft server software, open-source and without any code from Mojang.
The main difference is that our implementation of the Notchian server does not contain any features by default!
The main difference compared to it is that our implementation of the Notchian server does not contain any features by default!
However, we have a complete API which allows you to make anything possible with current spigot plugins.
This is a developer API not meant to be used by the end-users. Replacing Spigot/Paper with this will **not** work since we do not implement the Bukkit API.

View File

@ -143,7 +143,7 @@ dependencies {
api "org.spongepowered:mixin:${mixinVersion}"
// Path finding
api 'com.github.MadMartian:hydrazine-path-finding:1.4.2'
api 'com.github.MadMartian:hydrazine-path-finding:1.5.0'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

View File

@ -242,7 +242,7 @@ public enum EntityType {
}
public static EntityType fromId(int id) {
if(id >= 0 && id < values().length) {
if (id >= 0 && id < values().length) {
return values()[id];
}
return PIG;

View File

@ -36,7 +36,7 @@ public enum Fluid {
}
public static Fluid fromId(int id) {
if(id >= 0 && id < values().length) {
if (id >= 0 && id < values().length) {
return values()[id];
}
return EMPTY;

View File

@ -102,7 +102,7 @@ public enum Enchantment {
}
public static Enchantment fromId(int id) {
if(id >= 0 && id < values().length) {
if (id >= 0 && id < values().length) {
return values()[id];
}
return null;

View File

@ -170,7 +170,7 @@ public enum Particle {
}
public static Particle fromId(int id) {
if(id >= 0 && id < values().length) {
if (id >= 0 && id < values().length) {
return values()[id];
}
return null;

View File

@ -82,7 +82,7 @@ public enum PotionEffect {
}
public int getId() {
return ordinal();
return ordinal() + 1;
}
public String getNamespaceID() {
@ -90,8 +90,8 @@ public enum PotionEffect {
}
public static PotionEffect fromId(int id) {
if(id >= 0 && id < values().length) {
return values()[id];
if (id >= 0 && id < values().length + 1) {
return values()[id - 1];
}
return null;
}

View File

@ -112,7 +112,7 @@ public enum PotionType {
}
public static PotionType fromId(int id) {
if(id >= 0 && id < values().length) {
if (id >= 0 && id < values().length) {
return values()[id];
}
return EMPTY;

View File

@ -2010,7 +2010,7 @@ public enum Sound {
}
public static Sound fromId(int id) {
if(id >= 0 && id < values().length) {
if (id >= 0 && id < values().length) {
return values()[id];
}
return null;

View File

@ -174,7 +174,7 @@ public enum StatisticType {
}
public static StatisticType fromId(int id) {
if(id >= 0 && id < values().length) {
if (id >= 0 && id < values().length) {
return values()[id];
}
return null;

View File

@ -20,13 +20,23 @@ public abstract class BasicEnumGenerator extends MinestomEnumGenerator<BasicEnum
private final boolean linear;
private NamespaceID defaultEntry;
protected BasicEnumGenerator(File targetFolder) throws IOException {
this(targetFolder, true);
/**
* True if the enum is linear and start by 1 instead of 0
*/
private boolean incrementOrdinal;
protected BasicEnumGenerator(File targetFolder, boolean linear, boolean incrementOrdinal) throws IOException {
this.linear = linear;
this.incrementOrdinal = incrementOrdinal;
generateTo(targetFolder);
}
protected BasicEnumGenerator(File targetFolder, boolean linear) throws IOException {
this.linear = linear;
generateTo(targetFolder);
this(targetFolder, linear, false);
}
protected BasicEnumGenerator(File targetFolder) throws IOException {
this(targetFolder, true);
}
@Override
@ -59,17 +69,19 @@ public abstract class BasicEnumGenerator extends MinestomEnumGenerator<BasicEnum
ParameterSpec idParam = ParameterSpec.builder(TypeName.INT, "id").build();
ParameterSpec[] signature = new ParameterSpec[]{idParam};
if (linear) {
final String ordinalIncrementCondition = incrementOrdinal ? " + 1" : "";
final String ordinalIncrementIndex = incrementOrdinal ? " - 1" : "";
generator.addStaticMethod("fromId", signature, className, code -> {
code.beginControlFlow("if($N >= 0 && $N < values().length)", idParam, idParam)
.addStatement("return values()[$N]", idParam)
code.beginControlFlow("if ($N >= 0 && $N < values().length" + ordinalIncrementCondition + ")", idParam, idParam)
.addStatement("return values()[$N" + ordinalIncrementIndex + "]", idParam)
.endControlFlow()
.addStatement("return " + (defaultEntry == null ? "null" : identifier(defaultEntry)));
}
);
} else {
generator.addStaticMethod("fromId", signature, className, code -> {
code.beginControlFlow("for($T o : values())")
.beginControlFlow("if(o.getId() == id)")
code.beginControlFlow("for ($T o : values())")
.beginControlFlow("if (o.getId() == id)")
.addStatement("return o")
.endControlFlow()
.endControlFlow()
@ -94,7 +106,7 @@ public abstract class BasicEnumGenerator extends MinestomEnumGenerator<BasicEnum
ClassName registriesClass = ClassName.get(Registries.class);
if (linear) {
generator.setParams(ParameterSpec.builder(ClassName.get(String.class), "namespaceID").build());
generator.addMethod("getId", new ParameterSpec[0], TypeName.INT, code -> code.addStatement("return ordinal()"));
generator.addMethod("getId", new ParameterSpec[0], TypeName.INT, code -> code.addStatement("return ordinal()" + (incrementOrdinal ? " + 1" : "")));
} else {
generator.setParams(ParameterSpec.builder(ClassName.get(String.class), "namespaceID").build(), ParameterSpec.builder(TypeName.INT, "id").build());
generator.addMethod("getId", new ParameterSpec[0], TypeName.INT, code -> code.addStatement("return $N", "id"));

View File

@ -1,7 +1,6 @@
package net.minestom.codegen.potions;
import net.minestom.codegen.BasicEnumGenerator;
import net.minestom.codegen.stats.StatsEnumGenerator;
import net.minestom.server.registry.ResourceGatherer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,7 +41,7 @@ public class PotionEffectEnumGenerator extends BasicEnumGenerator {
}
private PotionEffectEnumGenerator(File targetFolder) throws IOException {
super(targetFolder);
super(targetFolder, true, true);
}
@Override

View File

@ -99,11 +99,14 @@ public final class BenchmarkManager {
return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
}
@NotNull
public Map<String, ThreadResult> getResultMap() {
return Collections.unmodifiableMap(resultMap);
}
@NotNull
public String getCpuMonitoringMessage() {
Check.stateCondition(!enabled, "CPU monitoring is only possible when the benchmark manager is enabled.");
StringBuilder benchmarkMessage = new StringBuilder();
for (Map.Entry<String, ThreadResult> resultEntry : resultMap.entrySet()) {
final String name = resultEntry.getKey();

View File

@ -580,11 +580,13 @@ public final class CommandManager {
} else if (argument instanceof ArgumentDynamicWord) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(nodes, argument, executable, true);
final SuggestionType suggestionType = ((ArgumentDynamicWord) argument).getSuggestionType();
argumentNode.parser = "brigadier:string";
argumentNode.properties = packetWriter -> {
packetWriter.writeVarInt(0); // Single word
};
argumentNode.suggestionsType = "minecraft:ask_server";
argumentNode.suggestionsType = suggestionType.getIdentifier();
} else if (argument instanceof ArgumentString) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(nodes, argument, executable, false);

View File

@ -5,6 +5,7 @@ import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.arguments.ArgumentDynamicStringArray;
import net.minestom.server.command.builder.arguments.ArgumentDynamicWord;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.arguments.minecraft.SuggestionType;
import net.minestom.server.command.builder.condition.CommandCondition;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -187,7 +188,8 @@ public class Command {
/**
* Allows for tab auto completion, this is called everytime the player press a key in the chat
* when in a dynamic argument ({@link ArgumentDynamicWord} and {@link ArgumentDynamicStringArray}).
* when in a dynamic argument ({@link ArgumentDynamicWord} (when {@link SuggestionType#ASK_SERVER} is used)
* and {@link ArgumentDynamicStringArray}).
*
* @param text the whole player's text
* @return the array containing all the suggestion for the current arg (split " "), can be null

View File

@ -37,9 +37,9 @@ public class CommandDispatcher {
this.commands.add(command);
}
public void unregister(Command command) {
public void unregister(@NotNull Command command) {
commandMap.remove(command.getName().toLowerCase());
for(String alias : command.getAliases()) {
for (String alias : command.getAliases()) {
this.commandMap.remove(alias.toLowerCase());
}
commands.remove(command);
@ -248,7 +248,6 @@ public class CommandDispatcher {
// Get closest valid syntax
if (!syntaxesSuggestions.isEmpty()) {
final int max = syntaxesSuggestions.firstKey(); // number of correct arguments
// Check if at least 1 argument of the syntax is correct
if (max > 0) {
// Get the data of the closest syntax

View File

@ -1,22 +1,27 @@
package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.builder.arguments.minecraft.SuggestionType;
import net.minestom.server.utils.callback.validator.StringValidator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Same as {@link ArgumentWord} with the exception
* that this argument can trigger {@link net.minestom.server.command.builder.Command#onDynamicWrite(String)}.
* that this argument can trigger {@link net.minestom.server.command.builder.Command#onDynamicWrite(String)}
* when the suggestion type is {@link SuggestionType#ASK_SERVER}, or any other suggestions available in the enum.
*/
public class ArgumentDynamicWord extends Argument<String> {
public static final int SPACE_ERROR = 1;
public static final int RESTRICTION_ERROR = 2;
private SuggestionType suggestionType;
private StringValidator dynamicRestriction;
public ArgumentDynamicWord(String id) {
public ArgumentDynamicWord(@NotNull String id, @NotNull SuggestionType suggestionType) {
super(id);
this.suggestionType = suggestionType;
}
@Override
@ -46,6 +51,18 @@ public class ArgumentDynamicWord extends Argument<String> {
return SUCCESS;
}
/**
* Gets the suggestion type of this argument.
* <p>
* Suggestions are only suggestion, this means that the end value could not be the expected one.
*
* @return this argument suggestion type
*/
@NotNull
public SuggestionType getSuggestionType() {
return suggestionType;
}
/**
* Sets the dynamic restriction of this dynamic argument.
* <p>

View File

@ -49,8 +49,12 @@ public class ArgumentType {
return new ArgumentWord(id);
}
public static ArgumentDynamicWord DynamicWord(@NotNull String id, @NotNull SuggestionType suggestionType) {
return new ArgumentDynamicWord(id, suggestionType);
}
public static ArgumentDynamicWord DynamicWord(@NotNull String id) {
return new ArgumentDynamicWord(id);
return DynamicWord(id, SuggestionType.ASK_SERVER);
}
public static ArgumentStringArray StringArray(@NotNull String id) {

View File

@ -0,0 +1,22 @@
package net.minestom.server.command.builder.arguments.minecraft;
import org.jetbrains.annotations.NotNull;
public enum SuggestionType {
ASK_SERVER("minecraft:ask_server"),
ALL_RECIPES("minecraft:all_recipes"),
AVAILABLE_SOUNDS("minecraft:available_sounds"),
SUMMONABLE_ENTITIES("minecraft:summonable_entities");
private String identifier;
SuggestionType(@NotNull String identifier) {
this.identifier = identifier;
}
@NotNull
public String getIdentifier() {
return identifier;
}
}

View File

@ -6,7 +6,6 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents a {@link Data} object which can be serialized and read back.
@ -17,9 +16,6 @@ public interface SerializableData extends Data {
DataManager DATA_MANAGER = MinecraftServer.getDataManager();
@Override
<T> void set(@NotNull String key, @Nullable T value, @Nullable Class<T> type);
/**
* Serializes the data into an array of bytes.
* <p>

View File

@ -1122,34 +1122,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
this.eyeHeight = eyeHeight;
}
/**
* Gets if this entity is in the same chunk as the specified position.
*
* @param position the checked position chunk
* @return true if the entity is in the same chunk as {@code position}
*/
public boolean sameChunk(@NotNull Position position) {
Check.notNull(position, "Position cannot be null");
final Position pos = getPosition();
final int chunkX1 = ChunkUtils.getChunkCoordinate((int) Math.floor(pos.getX()));
final int chunkZ1 = ChunkUtils.getChunkCoordinate((int) Math.floor(pos.getZ()));
final int chunkX2 = ChunkUtils.getChunkCoordinate((int) Math.floor(position.getX()));
final int chunkZ2 = ChunkUtils.getChunkCoordinate((int) Math.floor(position.getZ()));
return chunkX1 == chunkX2 && chunkZ1 == chunkZ2;
}
/**
* Gets if the entity is in the same chunk as another.
*
* @param entity the entity to check
* @return true if both entities are in the same chunk, false otherwise
*/
public boolean sameChunk(@NotNull Entity entity) {
return sameChunk(entity.getPosition());
}
/**
* Removes the entity from the server immediately.
* <p>
@ -1175,17 +1147,16 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
/**
* Triggers {@link #remove()} after the specified time.
*
* @param delay the time before removing the entity
* @param delay the time before removing the entity,
* 0 to cancel the removing
* @param timeUnit the unit of the delay
*/
public void scheduleRemove(long delay, @NotNull TimeUnit timeUnit) {
delay = timeUnit.toMilliseconds(delay);
if (delay == 0) { // Cancel the scheduled remove
this.scheduledRemoveTime = 0;
return;
}
this.scheduledRemoveTime = System.currentTimeMillis() + delay;
this.scheduledRemoveTime = System.currentTimeMillis() + timeUnit.toMilliseconds(delay);
}
/**

View File

@ -66,6 +66,7 @@ public final class EntityManager {
waitingPlayer.init();
// Spawn the player at Player#getRespawnPoint during the next instance tick
spawningInstance.scheduleNextTick(waitingPlayer::setInstance);
}
}

View File

@ -678,40 +678,40 @@ public class Player extends LivingEntity implements CommandSender {
* <p>
* Be aware that because chunk operations are expensive,
* it is possible for this method to be non-blocking when retrieving chunks is required.
* <p>
* When this method is called for the first time (during player login), the player will be teleport at {@link #getRespawnPoint()}.
*
* @param instance the new instance of the player
* @param instance the new player instance
* @param spawnPosition the new position of the player,
* can be null or {@link #getPosition()} if you do not want to teleport the player
*/
@Override
public void setInstance(@NotNull Instance instance) {
public void setInstance(@NotNull Instance instance, @Nullable Position spawnPosition) {
Check.notNull(instance, "instance cannot be null!");
Check.argCondition(this.instance == instance, "Instance should be different than the current one");
final boolean firstSpawn = this.instance == null; // TODO: Handle player reconnections, must be false in that case too
// true if the chunks need to be sent to the client, can be false if the instances share the same chunks (eg SharedInstance)
final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance);
if (needWorldRefresh) {
final boolean firstSpawn = this.instance == null; // TODO: Handle player reconnections, must be false in that case too
// Remove all previous viewable chunks (from the previous instance)
for (Chunk viewableChunk : viewableChunks) {
viewableChunk.removeViewer(this);
}
// Send the new dimension
if (this.instance != null) {
final DimensionType instanceDimensionType = instance.getDimensionType();
if (dimensionType != instanceDimensionType)
sendDimension(instanceDimensionType);
}
// Load all the required chunks
final Position pos = firstSpawn ? getRespawnPoint() : position;
final long[] visibleChunks = ChunkUtils.getChunksInRange(pos, getChunkRange());
final long[] visibleChunks = ChunkUtils.getChunksInRange(spawnPosition, getChunkRange());
final ChunkCallback eachCallback = chunk -> {
if (chunk != null) {
final int chunkX = ChunkUtils.getChunkCoordinate((int) pos.getX());
final int chunkZ = ChunkUtils.getChunkCoordinate((int) pos.getZ());
final int chunkX = ChunkUtils.getChunkCoordinate((int) spawnPosition.getX());
final int chunkZ = ChunkUtils.getChunkCoordinate((int) spawnPosition.getZ());
if (chunk.getChunkX() == chunkX &&
chunk.getChunkZ() == chunkZ) {
updateViewPosition(chunkX, chunkZ);
@ -721,7 +721,7 @@ public class Player extends LivingEntity implements CommandSender {
final ChunkCallback endCallback = chunk -> {
// This is the last chunk to be loaded , spawn player
spawnPlayer(instance, firstSpawn);
spawnPlayer(instance, spawnPosition, firstSpawn);
};
// Chunk 0;0 always needs to be loaded
@ -732,26 +732,42 @@ public class Player extends LivingEntity implements CommandSender {
} else {
// The player already has the good version of all the chunks.
// We just need to refresh his entity viewing list and add him to the instance
spawnPlayer(instance, false);
spawnPlayer(instance, spawnPosition, false);
}
}
/**
* Changes the player instance without changing its position (defaulted to {@link #getRespawnPoint()}
* if the player is not in any instance.
*
* @param instance the new player instance
* @see #setInstance(Instance, Position)
*/
@Override
public void setInstance(@NotNull Instance instance) {
setInstance(instance, this.instance != null ? getPosition() : getRespawnPoint());
}
/**
* Used to spawn the player once the client has all the required chunks.
* <p>
* Does add the player to {@code instance}, remove all viewable entities and call {@link PlayerSpawnEvent}.
* <p>
* UNSAFE: only called with {@link #setInstance(Instance)}.
* UNSAFE: only called with {@link #setInstance(Instance, Position)}.
*
* @param firstSpawn true if this is the player first spawn
* @param spawnPosition the position to teleport the player
* @param firstSpawn true if this is the player first spawn
*/
private void spawnPlayer(@NotNull Instance instance, boolean firstSpawn) {
private void spawnPlayer(@NotNull Instance instance, @Nullable Position spawnPosition, boolean firstSpawn) {
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
super.setInstance(instance);
if (firstSpawn) {
teleport(getRespawnPoint());
if (spawnPosition != null && !position.isSimilar(spawnPosition)) {
teleport(spawnPosition,
position.inSameChunk(spawnPosition) ? () -> refreshVisibleChunks(getChunk()) : null);
} else {
refreshVisibleChunks(getChunk());
}
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
@ -1510,7 +1526,7 @@ public class Player extends LivingEntity implements CommandSender {
// New chunks indexes
final long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(newChunk.toPosition(), getChunkRange());
// Find the difference between the two arrays¬
// Find the difference between the two arrays
final int[] oldChunks = ArrayUtils.getDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks);
final int[] newChunks = ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks);

View File

@ -1,36 +0,0 @@
package net.minestom.server.entity.task;
import net.minestom.server.entity.LivingEntity;
public abstract class EntityTask {
/**
* Whether the task should begin executing for this entity.
*
* @param entity the entity in question.
* @return true if the task should start, false otherwise.
*/
public abstract boolean shouldStart(LivingEntity entity);
/**
* Invoked when this task is about to start for this entity.
*
* @param entity the entity in question.
*/
public abstract void start(LivingEntity entity);
/**
* Invoked when this task is being ended for this entity.
*
* @param entity the entity in question.
*/
public abstract void end(LivingEntity entity);
/**
* Invoked each tick when this task is being executed for this entity.
*
* @param entity the entity in question.
*/
public abstract void execute(LivingEntity entity);
}

View File

@ -8,6 +8,7 @@ import net.minestom.server.entity.Player;
import net.minestom.server.entity.pathfinding.PFColumnarSpace;
import net.minestom.server.event.player.PlayerChunkLoadEvent;
import net.minestom.server.event.player.PlayerChunkUnloadEvent;
import net.minestom.server.instance.batch.BatchOption;
import net.minestom.server.instance.batch.BlockBatch;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block;
@ -186,7 +187,7 @@ public abstract class Chunk implements Viewable, DataContainer {
* @param z the block Z
* @param data the new data, can be null
*/
public abstract void setBlockData(int x, int y, int z, Data data);
public abstract void setBlockData(int x, int y, int z, @Nullable Data data);
/**
* Gets all the block entities in this chunk.
@ -240,15 +241,22 @@ public abstract class Chunk implements Viewable, DataContainer {
/**
* Creates a copy of this chunk, including blocks state id, custom block id, biomes, update data.
* <p>
* The instance and chunk position (X/Z) can be modified using the given arguments.
* The chunk position (X/Z) can be modified using the given arguments.
*
* @param chunkX the new chunk X
* @param chunkZ the new chunk Z
* @param chunkX the chunk X of the copy
* @param chunkZ the chunk Z of the copy
* @return a copy of this chunk with a potentially new instance and position
*/
@NotNull
public abstract Chunk copy(int chunkX, int chunkZ);
/**
* Resets the chunk, this means clearing all the data making it empty.
* <p>
* Used for {@link BatchOption#isFullChunk()}.
*/
public abstract void reset();
/**
* Gets the {@link CustomBlock} at a position.
*
@ -314,6 +322,7 @@ public abstract class Chunk implements Viewable, DataContainer {
*
* @return the position of this chunk
*/
@NotNull
public Position toPosition() {
return new Position(CHUNK_SIZE_Z * getChunkX(), 0, CHUNK_SIZE_Z * getChunkZ());
}
@ -385,6 +394,37 @@ public abstract class Chunk implements Viewable, DataContainer {
return fullDataPacket;
}
/**
* Gets the light packet of this chunk.
*
* @return the light packet
*/
@NotNull
public UpdateLightPacket getLightPacket() {
// TODO do not hardcode light
UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime());
updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ();
updateLightPacket.skyLightMask = 0x3FFF0;
updateLightPacket.blockLightMask = 0x3F;
updateLightPacket.emptySkyLightMask = 0x0F;
updateLightPacket.emptyBlockLightMask = 0x3FFC0;
byte[] bytes = new byte[2048];
Arrays.fill(bytes, (byte) 0xFF);
List<byte[]> temp = new ArrayList<>(14);
List<byte[]> temp2 = new ArrayList<>(6);
for (int i = 0; i < 14; ++i) {
temp.add(bytes);
}
for (int i = 0; i < 6; ++i) {
temp2.add(bytes);
}
updateLightPacket.skyLight = temp;
updateLightPacket.blockLight = temp2;
return updateLightPacket;
}
/**
* Used to verify if the chunk should still be kept in memory.
*
@ -468,7 +508,7 @@ public abstract class Chunk implements Viewable, DataContainer {
*
* @param player the player
*/
protected synchronized void sendChunk(@NotNull Player player) {
public synchronized void sendChunk(@NotNull Player player) {
// Only send loaded chunk
if (!isLoaded())
return;
@ -478,30 +518,16 @@ public abstract class Chunk implements Viewable, DataContainer {
// Retrieve & send the buffer to the connection
playerConnection.sendPacket(getFreshFullDataPacket());
// TODO do not hardcode light
{
UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime());
updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ();
updateLightPacket.skyLightMask = 0x3FFF0;
updateLightPacket.blockLightMask = 0x3F;
updateLightPacket.emptySkyLightMask = 0x0F;
updateLightPacket.emptyBlockLightMask = 0x3FFC0;
byte[] bytes = new byte[2048];
Arrays.fill(bytes, (byte) 0xFF);
List<byte[]> temp = new ArrayList<>(14);
List<byte[]> temp2 = new ArrayList<>(6);
for (int i = 0; i < 14; ++i) {
temp.add(bytes);
}
for (int i = 0; i < 6; ++i) {
temp2.add(bytes);
}
updateLightPacket.skyLight = temp;
updateLightPacket.blockLight = temp2;
playerConnection.sendPacket(getLightPacket());
}
playerConnection.sendPacket(updateLightPacket);
public synchronized void sendChunk() {
if (!isLoaded()) {
return;
}
sendPacketToViewers(getFreshFullDataPacket());
sendPacketToViewers(getLightPacket());
}
/**

View File

@ -31,7 +31,7 @@ import java.util.Set;
/**
* Represents a {@link Chunk} which store each individual block in memory.
* <p>
* WARNING: not thread-safe
* WARNING: not thread-safe.
*/
public class DynamicChunk extends Chunk {
@ -408,6 +408,17 @@ public class DynamicChunk extends Chunk {
return dynamicChunk;
}
@Override
public void reset() {
this.blockPalette.clear();
this.customBlockPalette.clear();
this.blocksData.clear();
this.updatableBlocks.clear();
this.updatableBlocksLastUpdate.clear();
this.blockEntities.clear();
}
private short getBlockAt(@NotNull PaletteStorage paletteStorage, int x, int y, int z) {
return paletteStorage.getBlockAt(x, y, z);
}

View File

@ -1,6 +1,7 @@
package net.minestom.server.instance;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.UpdateManager;
@ -81,7 +82,8 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
protected final Set<ObjectEntity> objectEntities = new CopyOnWriteArraySet<>();
protected final Set<ExperienceOrb> experienceOrbs = new CopyOnWriteArraySet<>();
// Entities per chunk
protected final Long2ObjectMap<Set<Entity>> chunkEntities = new Long2ObjectOpenHashMap<>();
protected final Long2ObjectMap<Set<Entity>> chunkEntities = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
private Object entitiesLock = new Object(); // Lock used to prevent the entities Set and Map to be subject to race condition
// the uuid of this instance
protected UUID uniqueId;
@ -921,7 +923,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
"The chunk " + chunk + " is not loaded, you can make it automatic by using Instance#enableAutoChunkLoad(true)");
Check.argCondition(!chunk.isLoaded(), "Chunk " + chunk + " has been unloaded previously");
final long chunkIndex = ChunkUtils.getChunkIndex(chunk.getChunkX(), chunk.getChunkZ());
synchronized (chunkEntities) {
synchronized (entitiesLock) {
Set<Entity> entities = getEntitiesInChunk(chunkIndex);
entities.add(entity);
this.chunkEntities.put(chunkIndex, entities);
@ -948,7 +950,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
* @param chunk the chunk where the entity will be removed
*/
public void removeEntityFromChunk(@NotNull Entity entity, @NotNull Chunk chunk) {
synchronized (chunkEntities) {
synchronized (entitiesLock) {
final long chunkIndex = ChunkUtils.getChunkIndex(chunk.getChunkX(), chunk.getChunkZ());
Set<Entity> entities = getEntitiesInChunk(chunkIndex);
entities.remove(entity);
@ -973,9 +975,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
@NotNull
private Set<Entity> getEntitiesInChunk(long index) {
synchronized (chunkEntities) {
return chunkEntities.computeIfAbsent(index, i -> new CopyOnWriteArraySet<>());
}
return chunkEntities.computeIfAbsent(index, i -> new CopyOnWriteArraySet<>());
}
/**

View File

@ -1,6 +1,7 @@
package net.minestom.server.instance;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.data.Data;
@ -57,7 +58,7 @@ public class InstanceContainer extends Instance {
// the chunk generator used, can be null
private ChunkGenerator chunkGenerator;
// (chunk index -> chunk) map, contains all the chunks in the instance
private final Long2ObjectMap<Chunk> chunks = new Long2ObjectOpenHashMap<>();
private final Long2ObjectMap<Chunk> chunks = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
// contains all the chunks to remove during the next instance tick
protected final Set<Chunk> scheduledChunksToRemove = new HashSet<>();
@ -312,7 +313,7 @@ public class InstanceContainer extends Instance {
*
* @param blockPosition the position of the modified block
*/
private void executeNeighboursBlockPlacementRule(BlockPosition blockPosition) {
private void executeNeighboursBlockPlacementRule(@NotNull BlockPosition blockPosition) {
for (int offsetX = -1; offsetX < 2; offsetX++) {
for (int offsetY = -1; offsetY < 2; offsetY++) {
for (int offsetZ = -1; offsetZ < 2; offsetZ++) {
@ -451,10 +452,8 @@ public class InstanceContainer extends Instance {
@Override
public Chunk getChunk(int chunkX, int chunkZ) {
final long index = ChunkUtils.getChunkIndex(chunkX, chunkZ);
synchronized (chunks) {
final Chunk chunk = chunks.get(index);
return ChunkUtils.isLoaded(chunk) ? chunk : null;
}
final Chunk chunk = chunks.get(index);
return ChunkUtils.isLoaded(chunk) ? chunk : null;
}
/**
@ -480,7 +479,7 @@ public class InstanceContainer extends Instance {
}
/**
* Save the instance without callback.
* Saves the instance without callback.
*
* @see #saveInstance(Runnable)
*/
@ -495,10 +494,8 @@ public class InstanceContainer extends Instance {
@Override
public void saveChunksToStorage(@Nullable Runnable callback) {
synchronized (chunks) {
Collection<Chunk> chunksCollection = chunks.values();
this.chunkLoader.saveChunks(chunksCollection, callback);
}
Collection<Chunk> chunksCollection = chunks.values();
this.chunkLoader.saveChunks(chunksCollection, callback);
}
@Override
@ -645,16 +642,14 @@ public class InstanceContainer extends Instance {
copiedInstance.srcInstance = this;
copiedInstance.lastBlockChangeTime = lastBlockChangeTime;
synchronized (chunks) {
for (Chunk chunk : chunks.values()) {
final int chunkX = chunk.getChunkX();
final int chunkZ = chunk.getChunkZ();
for (Chunk chunk : chunks.values()) {
final int chunkX = chunk.getChunkX();
final int chunkZ = chunk.getChunkZ();
final Chunk copiedChunk = chunk.copy(chunkX, chunkZ);
final Chunk copiedChunk = chunk.copy(chunkX, chunkZ);
copiedInstance.cacheChunk(copiedChunk);
UPDATE_MANAGER.signalChunkLoad(copiedInstance, chunkX, chunkZ);
}
copiedInstance.cacheChunk(copiedChunk);
UPDATE_MANAGER.signalChunkLoad(copiedInstance, chunkX, chunkZ);
}
return copiedInstance;
@ -701,9 +696,7 @@ public class InstanceContainer extends Instance {
*/
public void cacheChunk(@NotNull Chunk chunk) {
final long index = ChunkUtils.getChunkIndex(chunk.getChunkX(), chunk.getChunkZ());
synchronized (chunks) {
this.chunks.put(index, chunk);
}
this.chunks.put(index, chunk);
}
@Override
@ -833,12 +826,8 @@ public class InstanceContainer extends Instance {
});
// Clear cache
synchronized (chunks) {
this.chunks.remove(index);
}
synchronized (chunkEntities) {
this.chunkEntities.remove(index);
}
this.chunks.remove(index);
this.chunkEntities.remove(index);
chunk.unload();

View File

@ -0,0 +1,33 @@
package net.minestom.server.instance.batch;
import org.jetbrains.annotations.NotNull;
public class BatchOption {
private boolean fullChunk = false;
public BatchOption() {
}
/**
* Gets if the batch is responsible for composing the whole chunk.
* <p>
* Having it to true means that the batch will clear the chunk data before placing the blocks.
*
* @return true if the batch is responsible for all the chunk
*/
public boolean isFullChunk() {
return fullChunk;
}
/**
* @param fullChunk true to make this batch composes the whole chunk
* @return 'this' for chaining
* @see #isFullChunk()
*/
@NotNull
public BatchOption setFullChunk(boolean fullChunk) {
this.fullChunk = fullChunk;
return this;
}
}

View File

@ -6,6 +6,8 @@ import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.utils.block.CustomBlockUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
@ -23,33 +25,39 @@ import java.util.concurrent.atomic.AtomicInteger;
public class BlockBatch implements InstanceBatch {
private final InstanceContainer instance;
private final BatchOption batchOption;
private final Map<Chunk, List<BlockData>> data = new HashMap<>();
public BlockBatch(InstanceContainer instance) {
public BlockBatch(@NotNull InstanceContainer instance, @NotNull BatchOption batchOption) {
this.instance = instance;
this.batchOption = batchOption;
}
public BlockBatch(@NotNull InstanceContainer instance) {
this(instance, new BatchOption());
}
@Override
public synchronized void setBlockStateId(int x, int y, int z, short blockStateId, Data data) {
public synchronized void setBlockStateId(int x, int y, int z, short blockStateId, @Nullable Data data) {
final Chunk chunk = this.instance.getChunkAt(x, z);
addBlockData(chunk, x, y, z, blockStateId, (short) 0, data);
}
@Override
public void setCustomBlock(int x, int y, int z, short customBlockId, Data data) {
public void setCustomBlock(int x, int y, int z, short customBlockId, @Nullable Data data) {
final Chunk chunk = this.instance.getChunkAt(x, z);
final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId);
addBlockData(chunk, x, y, z, customBlock.getDefaultBlockStateId(), customBlockId, data);
}
@Override
public synchronized void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, Data data) {
public synchronized void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
final Chunk chunk = this.instance.getChunkAt(x, z);
addBlockData(chunk, x, y, z, blockStateId, customBlockId, data);
}
private void addBlockData(Chunk chunk, int x, int y, int z, short blockStateId, short customBlockId, Data data) {
private void addBlockData(@NotNull Chunk chunk, int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
List<BlockData> blocksData = this.data.get(chunk);
if (blocksData == null)
blocksData = new ArrayList<>();
@ -67,7 +75,7 @@ public class BlockBatch implements InstanceBatch {
this.data.put(chunk, blocksData);
}
public void flush(Runnable callback) {
public void flush(@Nullable Runnable callback) {
synchronized (data) {
AtomicInteger counter = new AtomicInteger();
for (Map.Entry<Chunk, List<BlockData>> entry : data.entrySet()) {
@ -78,12 +86,20 @@ public class BlockBatch implements InstanceBatch {
if (!chunk.isLoaded())
return;
if (batchOption.isFullChunk()) {
chunk.reset();
}
for (BlockData data : dataList) {
data.apply(chunk);
}
// Refresh chunk for viewers
chunk.sendChunkUpdate();
if (batchOption.isFullChunk()) {
chunk.sendChunk();
} else {
chunk.sendChunkUpdate();
}
final boolean isLast = counter.incrementAndGet() == data.size();

View File

@ -22,7 +22,7 @@ import java.util.List;
* use a {@link BlockBatch} instead otherwise.
* Can be created using {@link Instance#createChunkBatch(Chunk)}.
* <p>
* Use chunk coordinate (0-15) instead of world's.
* Uses chunk coordinate (0-15) instead of world's.
*
* @see InstanceBatch
*/
@ -30,6 +30,7 @@ public class ChunkBatch implements InstanceBatch {
private final InstanceContainer instance;
private final Chunk chunk;
private final BatchOption batchOption;
private final boolean generationBatch;
@ -42,9 +43,11 @@ public class ChunkBatch implements InstanceBatch {
private Int2ObjectMap<Data> blockDataMap;
public ChunkBatch(@NotNull InstanceContainer instance, @NotNull Chunk chunk,
@NotNull BatchOption batchOption,
boolean generationBatch) {
this.instance = instance;
this.chunk = chunk;
this.batchOption = batchOption;
this.generationBatch = generationBatch;
if (!generationBatch) {
@ -53,6 +56,11 @@ public class ChunkBatch implements InstanceBatch {
}
}
public ChunkBatch(@NotNull InstanceContainer instance, @NotNull Chunk chunk,
boolean generationBatch) {
this(instance, chunk, new BatchOption(), generationBatch);
}
@Override
public void setBlockStateId(int x, int y, int z, short blockStateId, @Nullable Data data) {
addBlockData((byte) x, y, (byte) z, blockStateId, (short) 0, data);
@ -120,6 +128,10 @@ public class ChunkBatch implements InstanceBatch {
final List<ChunkPopulator> populators = chunkGenerator.getPopulators();
final boolean hasPopulator = populators != null && !populators.isEmpty();
if (batchOption.isFullChunk()) {
this.chunk.reset();
}
chunkGenerator.generateChunkData(this, chunk.getChunkX(), chunk.getChunkZ());
if (hasPopulator) {
@ -128,16 +140,7 @@ public class ChunkBatch implements InstanceBatch {
}
}
// Refresh chunk for viewers
this.chunk.sendChunkUpdate();
this.instance.refreshLastBlockChangeTime();
// Safe callback
instance.scheduleNextTick(inst -> {
OptionalCallback.execute(callback, chunk);
});
updateChunk(callback, true);
}
});
}
@ -197,18 +200,7 @@ public class ChunkBatch implements InstanceBatch {
}
}
// Refresh chunk for viewers
chunk.sendChunkUpdate();
this.instance.refreshLastBlockChangeTime();
if (callback != null) {
if (safeCallback) {
this.instance.scheduleNextTick(inst -> callback.accept(chunk));
} else {
callback.accept(chunk);
}
}
updateChunk(callback, safeCallback);
}
}
@ -234,8 +226,26 @@ public class ChunkBatch implements InstanceBatch {
ChunkUtils.blockIndexToChunkPositionY(index),
ChunkUtils.blockIndexToChunkPositionZ(index),
blockId, customBlockId, data, CustomBlockUtils.hasUpdate(customBlockId));
}
private void updateChunk(@Nullable ChunkCallback callback, boolean safeCallback) {
// Refresh chunk for viewers
if (batchOption.isFullChunk()) {
chunk.sendChunk();
} else {
chunk.sendChunkUpdate();
}
this.instance.refreshLastBlockChangeTime();
if (callback != null) {
if (safeCallback) {
this.instance.scheduleNextTick(inst -> callback.accept(chunk));
} else {
callback.accept(chunk);
}
}
}
}

View File

@ -45,12 +45,12 @@ public class PaletteStorage {
private int valuesPerLong;
private boolean hasPalette;
private long[][] sectionBlocks = new long[CHUNK_SECTION_COUNT][0];
private long[][] sectionBlocks;
// chunk section - palette index = block id
private Short2ShortLinkedOpenHashMap[] paletteBlockMaps = new Short2ShortLinkedOpenHashMap[CHUNK_SECTION_COUNT];
private Short2ShortLinkedOpenHashMap[] paletteBlockMaps;
// chunk section - block id = palette index
private Short2ShortOpenHashMap[] blockPaletteMaps = new Short2ShortOpenHashMap[CHUNK_SECTION_COUNT];
private Short2ShortOpenHashMap[] blockPaletteMaps;
/**
* Creates a new palette storage.
@ -72,6 +72,15 @@ public class PaletteStorage {
this.valuesPerLong = Long.SIZE / bitsPerEntry;
this.hasPalette = bitsPerEntry <= PALETTE_MAXIMUM_BITS;
init();
}
private void init() {
this.sectionBlocks = new long[CHUNK_SECTION_COUNT][0];
this.paletteBlockMaps = new Short2ShortLinkedOpenHashMap[CHUNK_SECTION_COUNT];
this.blockPaletteMaps = new Short2ShortOpenHashMap[CHUNK_SECTION_COUNT];
}
public void setBlockAt(int x, int y, int z, short blockId) {
@ -139,6 +148,14 @@ public class PaletteStorage {
}
}
/**
* Clears all the data in the palette and data array.
*/
public void clear() {
init();
}
@NotNull
public PaletteStorage copy() {
PaletteStorage paletteStorage = new PaletteStorage(bitsPerEntry, bitsIncrement);
paletteStorage.sectionBlocks = sectionBlocks.clone();

View File

@ -1,5 +1,8 @@
package net.minestom.server.utils;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
@ -72,8 +75,8 @@ public class Position {
/**
* Gets the distance between 2 positions.
* In cases where performance matters, {@link #getDistanceSquared(Position)} should be used
* as it does not perform the expensive Math.sqrt method.
* In cases where performance matters, {@link #getDistanceSquared(Position)} should be used
* as it does not perform the expensive Math.sqrt method.
*
* @param position the second position
* @return the distance between {@code this} and {@code position}
@ -86,15 +89,15 @@ public class Position {
/**
* Gets the square distance to another position.
*
* @param position the second position
* @return the squared distance between {@code this} and {@code position}
*/
public float getDistanceSquared(Position position) {
return MathUtils.square(getX() - position.getX()) +
MathUtils.square(getY() - position.getY()) +
MathUtils.square(getZ() - position.getZ());
}
*
* @param position the second position
* @return the squared distance between {@code this} and {@code position}
*/
public float getDistanceSquared(Position position) {
return MathUtils.square(getX() - position.getX()) +
MathUtils.square(getY() - position.getY()) +
MathUtils.square(getZ() - position.getZ());
}
/**
* Gets a unit-vector pointing in the direction that this Location is
@ -209,7 +212,7 @@ public class Position {
* @param position the position to compare
* @return true if the two positions are similar
*/
public boolean isSimilar(Position position) {
public boolean isSimilar(@NotNull Position position) {
return Float.compare(position.x, x) == 0 &&
Float.compare(position.y, y) == 0 &&
Float.compare(position.z, z) == 0;
@ -221,11 +224,27 @@ public class Position {
* @param position the position to compare
* @return true if the two positions have the same view
*/
public boolean hasSimilarView(Position position) {
public boolean hasSimilarView(@NotNull Position position) {
return Float.compare(position.yaw, yaw) == 0 &&
Float.compare(position.pitch, pitch) == 0;
}
/**
* Gets if two positions are in the same chunk.
*
* @param position the checked position chunk
* @return true if 'this' is in the same chunk as {@code position}
*/
public boolean inSameChunk(@NotNull Position position) {
final int chunkX1 = ChunkUtils.getChunkCoordinate((int) Math.floor(getX()));
final int chunkZ1 = ChunkUtils.getChunkCoordinate((int) Math.floor(getZ()));
final int chunkX2 = ChunkUtils.getChunkCoordinate((int) Math.floor(position.getX()));
final int chunkZ2 = ChunkUtils.getChunkCoordinate((int) Math.floor(position.getZ()));
return chunkX1 == chunkX2 && chunkZ1 == chunkZ2;
}
@Override
public int hashCode() {
return Objects.hash(x, y, z, yaw, pitch);

View File

@ -1,11 +1,12 @@
package demo.commands;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Arguments;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.entity.Player;
import net.minestom.server.command.builder.arguments.minecraft.SuggestionType;
public class TestCommand extends Command {
@ -13,20 +14,20 @@ public class TestCommand extends Command {
super("testcmd");
setDefaultExecutor(this::usage);
Argument test = ArgumentType.ItemStack("test");
Argument test = ArgumentType.DynamicWord("view_distance", SuggestionType.SUMMONABLE_ENTITIES);
test.setCallback((source, value, error) -> {
System.out.println("ERROR " + error);
});
setDefaultExecutor((source, args) -> {
System.gc();
source.sendMessage("Explicit GC executed!");
//System.gc();
//source.sendMessage("Explicit GC executed!");
MinecraftServer.setChunkViewDistance(2);
});
addSyntax((source, args) -> {
Player player = (Player) source;
System.out.println("ARG 1");
}, test);
}