Merge branch 'master' into event-api

This commit is contained in:
TheMode 2021-05-11 04:53:03 +02:00
commit 920a36399f
48 changed files with 2071 additions and 682 deletions

View File

@ -5,7 +5,7 @@ plugins {
id 'java' id 'java'
id 'maven-publish' id 'maven-publish'
id 'net.ltgt.apt' version '0.10' id 'net.ltgt.apt' version '0.10'
id 'org.jetbrains.kotlin.jvm' version '1.4.21' id 'org.jetbrains.kotlin.jvm' version '1.5.0'
id 'checkstyle' id 'checkstyle'
} }
@ -141,7 +141,7 @@ dependencies {
// Noise library for terrain generation // Noise library for terrain generation
// https://jitpack.io/#Articdive/Jnoise // https://jitpack.io/#Articdive/Jnoise
api 'com.github.Articdive:Jnoise:1.0.0' api 'com.github.Articdive:Jnoise:2.1.0'
// https://mvnrepository.com/artifact/org.rocksdb/rocksdbjni // https://mvnrepository.com/artifact/org.rocksdb/rocksdbjni
api 'org.rocksdb:rocksdbjni:6.16.4' api 'org.rocksdb:rocksdbjni:6.16.4'
@ -152,7 +152,7 @@ dependencies {
api 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.0' api 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.0'
// https://mvnrepository.com/artifact/org.jline/jline // https://mvnrepository.com/artifact/org.jline/jline
implementation group: 'org.jline', name: 'jline', version: '3.19.0' implementation group: 'org.jline', name: 'jline', version: '3.20.0'
// Guava 21.0+ required for Mixin, but Authlib imports 17.0 // Guava 21.0+ required for Mixin, but Authlib imports 17.0
api 'com.google.guava:guava:30.1-jre' api 'com.google.guava:guava:30.1-jre'

View File

@ -1,5 +1,5 @@
asmVersion=9.0 asmVersion=9.0
mixinVersion=0.8.1 mixinVersion=0.8.1
hephaistosVersion=v1.1.8 hephaistosVersion=v1.1.8
kotlinVersion=1.4.21 kotlinVersion=1.5.0
adventureVersion=4.7.0 adventureVersion=4.7.0

View File

@ -78,19 +78,20 @@ public final class MinecraftServer {
public static final String THREAD_NAME_TICK = "Ms-Tick"; public static final String THREAD_NAME_TICK = "Ms-Tick";
public static final String THREAD_NAME_BLOCK_BATCH = "Ms-BlockBatchPool"; public static final String THREAD_NAME_BLOCK_BATCH = "Ms-BlockBatchPool";
public static final int THREAD_COUNT_BLOCK_BATCH = 4; public static final int THREAD_COUNT_BLOCK_BATCH = getThreadCount("minestom.block-thread-count",
Runtime.getRuntime().availableProcessors() / 2);
public static final String THREAD_NAME_SCHEDULER = "Ms-SchedulerPool"; public static final String THREAD_NAME_SCHEDULER = "Ms-SchedulerPool";
public static final int THREAD_COUNT_SCHEDULER = 1; public static final int THREAD_COUNT_SCHEDULER = getThreadCount("minestom.scheduler-thread-count",
Runtime.getRuntime().availableProcessors() / 2);
public static final String THREAD_NAME_PARALLEL_CHUNK_SAVING = "Ms-ParallelChunkSaving"; public static final String THREAD_NAME_PARALLEL_CHUNK_SAVING = "Ms-ParallelChunkSaving";
public static final int THREAD_COUNT_PARALLEL_CHUNK_SAVING = 4; public static final int THREAD_COUNT_PARALLEL_CHUNK_SAVING = getThreadCount("minestom.save-thread-count", 2);
// Config // Config
// Can be modified at performance cost when increased // Can be modified at performance cost when increased
public static final int TICK_PER_SECOND = 20; public static final int TICK_PER_SECOND = Integer.getInteger("minestom.tps", 20);
private static final int MS_TO_SEC = 1000; public static final int TICK_MS = 1000 / TICK_PER_SECOND;
public static final int TICK_MS = MS_TO_SEC / TICK_PER_SECOND;
// Network monitoring // Network monitoring
private static int rateLimit = 300; private static int rateLimit = 300;
@ -823,4 +824,8 @@ public final class MinecraftServer {
"You cannot access the manager before MinecraftServer#init, " + "You cannot access the manager before MinecraftServer#init, " +
"if you are developing an extension be sure to retrieve them at least after Extension#preInitialize");*/ "if you are developing an extension be sure to retrieve them at least after Extension#preInitialize");*/
} }
private static int getThreadCount(@NotNull String property, int count) {
return Integer.getInteger(property, Math.min(1, count));
}
} }

View File

@ -1,11 +1,10 @@
package net.minestom.server.command.builder; package net.minestom.server.command.builder;
import com.google.common.annotations.Beta; import com.google.common.annotations.Beta;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import net.minestom.server.command.CommandSender; import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.*;
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.arguments.minecraft.SuggestionType;
import net.minestom.server.command.builder.condition.CommandCondition; import net.minestom.server.command.builder.condition.CommandCondition;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -15,6 +14,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.*; import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -330,6 +331,85 @@ public class Command {
return syntaxes; return syntaxes;
} }
@Beta
public @NotNull String getSyntaxesTree() {
Node commandNode = new Node();
commandNode.names.addAll(Arrays.asList(getNames()));
// current node, literal = returned node
BiFunction<Node, Set<String>, Node> findNode = (currentNode, literals) -> {
for (Node node : currentNode.nodes) {
final var names = node.names;
// Verify if at least one literal is shared
final boolean shared = names.stream().anyMatch(literals::contains);
if (shared) {
names.addAll(literals);
return node;
}
}
// Create a new node
Node node = new Node();
node.names.addAll(literals);
currentNode.nodes.add(node);
return node;
};
BiConsumer<CommandSyntax, Node> syntaxProcessor = (syntax, node) -> {
List<String> arguments = new ArrayList<>();
BiConsumer<Node, List<String>> addArguments = (n, args) -> {
if (!args.isEmpty()) {
n.arguments.add(args);
}
};
// true if all following arguments are not part of
// the branching plant (literals)
boolean branched = false;
for (Argument<?> argument : syntax.getArguments()) {
if (!branched) {
if (argument instanceof ArgumentLiteral) {
final String literal = argument.getId();
addArguments.accept(node, arguments);
arguments = new ArrayList<>();
node = findNode.apply(node, Collections.singleton(literal));
continue;
} else if (argument instanceof ArgumentWord) {
ArgumentWord argumentWord = (ArgumentWord) argument;
if (argumentWord.hasRestrictions()) {
addArguments.accept(node, arguments);
arguments = new ArrayList<>();
node = findNode.apply(node, Set.of(argumentWord.getRestrictions()));
continue;
}
}
}
branched = true;
arguments.add(argument.toString());
}
addArguments.accept(node, arguments);
};
// Subcommands
this.subcommands.forEach(command -> {
final Node node = findNode.apply(commandNode, Set.of(command.getNames()));
command.getSyntaxes().forEach(syntax -> syntaxProcessor.accept(syntax, node));
});
// Syntaxes
this.syntaxes.forEach(syntax -> syntaxProcessor.accept(syntax, commandNode));
JsonObject jsonObject = new JsonObject();
processNode(commandNode, jsonObject);
return jsonObject.toString();
}
public static boolean isValidName(@NotNull Command command, @NotNull String name) { public static boolean isValidName(@NotNull Command command, @NotNull String name) {
for (String commandName : command.getNames()) { for (String commandName : command.getNames()) {
if (commandName.equals(name)) { if (commandName.equals(name)) {
@ -339,4 +419,33 @@ public class Command {
return false; return false;
} }
private void processNode(@NotNull Node node, @NotNull JsonObject jsonObject) {
BiConsumer<String, Consumer<JsonArray>> processor = (s, consumer) -> {
JsonArray array = new JsonArray();
consumer.accept(array);
if (array.size() != 0) {
jsonObject.add(s, array);
}
};
// Names
processor.accept("names", array -> node.names.forEach(array::add));
// Nodes
processor.accept("nodes", array ->
node.nodes.forEach(n -> {
JsonObject nodeObject = new JsonObject();
processNode(n, nodeObject);
array.add(nodeObject);
}));
// Arguments
processor.accept("arguments", array ->
node.arguments.forEach(arguments ->
array.add(String.join(StringUtils.SPACE, arguments))));
}
private static final class Node {
private final Set<String> names = new HashSet<>();
private final Set<Node> nodes = new HashSet<>();
private final List<List<String>> arguments = new ArrayList<>();
}
} }

View File

@ -17,6 +17,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
@ -26,45 +27,44 @@ public class ArgumentParser {
private static final Map<String, Function<String, Argument<?>>> ARGUMENT_FUNCTION_MAP = new ConcurrentHashMap<>(); private static final Map<String, Function<String, Argument<?>>> ARGUMENT_FUNCTION_MAP = new ConcurrentHashMap<>();
static { static {
ARGUMENT_FUNCTION_MAP.put("Literal", ArgumentLiteral::new); ARGUMENT_FUNCTION_MAP.put("literal", ArgumentLiteral::new);
ARGUMENT_FUNCTION_MAP.put("Boolean", ArgumentBoolean::new); ARGUMENT_FUNCTION_MAP.put("boolean", ArgumentBoolean::new);
ARGUMENT_FUNCTION_MAP.put("Integer", ArgumentInteger::new); ARGUMENT_FUNCTION_MAP.put("integer", ArgumentInteger::new);
ARGUMENT_FUNCTION_MAP.put("Double", ArgumentDouble::new); ARGUMENT_FUNCTION_MAP.put("double", ArgumentDouble::new);
ARGUMENT_FUNCTION_MAP.put("Float", ArgumentFloat::new); ARGUMENT_FUNCTION_MAP.put("float", ArgumentFloat::new);
ARGUMENT_FUNCTION_MAP.put("String", ArgumentString::new); ARGUMENT_FUNCTION_MAP.put("string", ArgumentString::new);
ARGUMENT_FUNCTION_MAP.put("Word", ArgumentWord::new); ARGUMENT_FUNCTION_MAP.put("word", ArgumentWord::new);
ARGUMENT_FUNCTION_MAP.put("StringArray", ArgumentStringArray::new); ARGUMENT_FUNCTION_MAP.put("stringarray", ArgumentStringArray::new);
ARGUMENT_FUNCTION_MAP.put("Command", ArgumentCommand::new); ARGUMENT_FUNCTION_MAP.put("command", ArgumentCommand::new);
// TODO enum // TODO enum
ARGUMENT_FUNCTION_MAP.put("Color", ArgumentColor::new); ARGUMENT_FUNCTION_MAP.put("color", ArgumentColor::new);
ARGUMENT_FUNCTION_MAP.put("Time", ArgumentTime::new); ARGUMENT_FUNCTION_MAP.put("time", ArgumentTime::new);
ARGUMENT_FUNCTION_MAP.put("Enchantment", ArgumentEnchantment::new); ARGUMENT_FUNCTION_MAP.put("enchantment", ArgumentEnchantment::new);
ARGUMENT_FUNCTION_MAP.put("Particle", ArgumentParticle::new); ARGUMENT_FUNCTION_MAP.put("particle", ArgumentParticle::new);
ARGUMENT_FUNCTION_MAP.put("ResourceLocation", ArgumentResourceLocation::new); ARGUMENT_FUNCTION_MAP.put("resourceLocation", ArgumentResourceLocation::new);
ARGUMENT_FUNCTION_MAP.put("Potion", ArgumentPotionEffect::new); ARGUMENT_FUNCTION_MAP.put("potion", ArgumentPotionEffect::new);
ARGUMENT_FUNCTION_MAP.put("EntityType", ArgumentEntityType::new); ARGUMENT_FUNCTION_MAP.put("entityType", ArgumentEntityType::new);
ARGUMENT_FUNCTION_MAP.put("BlockState", ArgumentBlockState::new); ARGUMENT_FUNCTION_MAP.put("blockState", ArgumentBlockState::new);
ARGUMENT_FUNCTION_MAP.put("IntRange", ArgumentIntRange::new); ARGUMENT_FUNCTION_MAP.put("intrange", ArgumentIntRange::new);
ARGUMENT_FUNCTION_MAP.put("FloatRange", ArgumentFloatRange::new); ARGUMENT_FUNCTION_MAP.put("floatrange", ArgumentFloatRange::new);
ARGUMENT_FUNCTION_MAP.put("Entity", s -> new ArgumentEntity(s).singleEntity(true)); ARGUMENT_FUNCTION_MAP.put("entity", s -> new ArgumentEntity(s).singleEntity(true));
ARGUMENT_FUNCTION_MAP.put("Entities", ArgumentEntity::new); ARGUMENT_FUNCTION_MAP.put("entities", ArgumentEntity::new);
ARGUMENT_FUNCTION_MAP.put("Player", s -> new ArgumentEntity(s).singleEntity(true).onlyPlayers(true)); ARGUMENT_FUNCTION_MAP.put("player", s -> new ArgumentEntity(s).singleEntity(true).onlyPlayers(true));
ARGUMENT_FUNCTION_MAP.put("Players", s -> new ArgumentEntity(s).onlyPlayers(true)); ARGUMENT_FUNCTION_MAP.put("players", s -> new ArgumentEntity(s).onlyPlayers(true));
ARGUMENT_FUNCTION_MAP.put("ItemStack", ArgumentItemStack::new); ARGUMENT_FUNCTION_MAP.put("itemstack", ArgumentItemStack::new);
ARGUMENT_FUNCTION_MAP.put("Component", ArgumentComponent::new); ARGUMENT_FUNCTION_MAP.put("component", ArgumentComponent::new);
ARGUMENT_FUNCTION_MAP.put("UUID", ArgumentUUID::new); ARGUMENT_FUNCTION_MAP.put("uuid", ArgumentUUID::new);
ARGUMENT_FUNCTION_MAP.put("NBT", ArgumentNbtTag::new); ARGUMENT_FUNCTION_MAP.put("nbt", ArgumentNbtTag::new);
ARGUMENT_FUNCTION_MAP.put("NbtCompound", ArgumentNbtCompoundTag::new); ARGUMENT_FUNCTION_MAP.put("nbtcompound", ArgumentNbtCompoundTag::new);
ARGUMENT_FUNCTION_MAP.put("RelativeBlockPosition", ArgumentRelativeBlockPosition::new); ARGUMENT_FUNCTION_MAP.put("relativeblockposition", ArgumentRelativeBlockPosition::new);
ARGUMENT_FUNCTION_MAP.put("RelativeVec3", ArgumentRelativeVec3::new); ARGUMENT_FUNCTION_MAP.put("relativevec3", ArgumentRelativeVec3::new);
ARGUMENT_FUNCTION_MAP.put("RelativeVec2", ArgumentRelativeVec2::new); ARGUMENT_FUNCTION_MAP.put("relativevec2", ArgumentRelativeVec2::new);
} }
@Beta @Beta
@NotNull public static @NotNull Argument<?>[] generate(@NotNull String format) {
public static Argument<?>[] generate(@NotNull String format) {
List<Argument<?>> result = new ArrayList<>(); List<Argument<?>> result = new ArrayList<>();
// 0 = no state // 0 = no state
@ -92,7 +92,7 @@ public class ArgumentParser {
} else if (c == '<') { } else if (c == '<') {
// Retrieve argument type // Retrieve argument type
final String argument = builder.toString(); final String argument = builder.toString();
argumentFunction = ARGUMENT_FUNCTION_MAP.get(argument); argumentFunction = ARGUMENT_FUNCTION_MAP.get(argument.toLowerCase(Locale.ROOT));
if (argumentFunction == null) { if (argumentFunction == null) {
throw new IllegalArgumentException("error invalid argument name: " + argument); throw new IllegalArgumentException("error invalid argument name: " + argument);
} }

View File

@ -267,7 +267,6 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
final ChunkCallback endCallback = (chunk) -> { final ChunkCallback endCallback = (chunk) -> {
refreshPosition(teleportPosition); refreshPosition(teleportPosition);
refreshView(teleportPosition.getYaw(), teleportPosition.getPitch());
synchronizePosition(); synchronizePosition();
@ -493,48 +492,9 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
} }
} }
sendPositionUpdate(false);
final boolean isNettyClient = PlayerUtils.isNettyClient(this); final boolean isNettyClient = PlayerUtils.isNettyClient(this);
// Synchronization with updated fields in #getPosition()
{
final boolean positionChange = !position.isSimilar(lastSyncedPosition);
final boolean viewChange = !position.hasSimilarView(lastSyncedPosition);
final double distance = positionChange ? position.getDistance(lastSyncedPosition) : 0;
if (distance >= 8 || (positionChange && isNettyClient)) {
// Teleport has the priority over everything else
teleport(position);
} else if (positionChange && viewChange) {
EntityPositionAndRotationPacket positionAndRotationPacket =
EntityPositionAndRotationPacket.getPacket(getEntityId(),
position, lastSyncedPosition, isOnGround());
sendPacketToViewersAndSelf(positionAndRotationPacket);
refreshPosition(position.clone());
// Fix head rotation
EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId();
entityHeadLookPacket.yaw = position.getYaw();
sendPacketToViewersAndSelf(entityHeadLookPacket);
} else if (positionChange) {
EntityPositionPacket entityPositionPacket = EntityPositionPacket.getPacket(getEntityId(),
position, lastSyncedPosition, isOnGround());
sendPacketToViewersAndSelf(entityPositionPacket);
refreshPosition(position.clone());
} else if (viewChange) {
// Yaw/Pitch
setView(position);
}
}
// Entity tick // Entity tick
{ {
@ -591,7 +551,7 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
} }
// Apply the position if changed // Apply the position if changed
if (!newPosition.isSimilar(position)) { if (!finalVelocityPosition.isSimilar(position)) {
refreshPosition(finalVelocityPosition); refreshPosition(finalVelocityPosition);
} }
@ -713,6 +673,82 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
} }
} }
/**
* Sends the correct packets to update the entity's position, should be called
* every tick. The movement is checked inside the method!
* <p>
* The following packets are sent to viewers (check are performed in this order):
* <ol>
* <li>{@link EntityTeleportPacket} if {@code distanceX > 8 || distanceY > 8 || distanceZ > 8}
* <i>(performed using {@link #synchronizePosition()})</i></li>
* <li>{@link EntityPositionAndRotationPacket} if {@code positionChange && viewChange}</li>
* <li>{@link EntityPositionPacket} if {@code positionChange}</li>
* <li>{@link EntityRotationPacket} and {@link EntityHeadLookPacket} if {@code viewChange}</li>
* </ol>
* In case of a player's position and/or view change an additional {@link PlayerPositionAndLookPacket}
* is sent to self.
*
* @param clientSide {@code true} if the client triggered this action
*/
protected void sendPositionUpdate(final boolean clientSide) {
final boolean viewChange = !position.hasSimilarView(lastSyncedPosition);
final double distanceX = Math.abs(position.getX()-lastSyncedPosition.getX());
final double distanceY = Math.abs(position.getY()-lastSyncedPosition.getY());
final double distanceZ = Math.abs(position.getZ()-lastSyncedPosition.getZ());
final boolean positionChange = (distanceX+distanceY+distanceZ) > 0;
if (distanceX > 8 || distanceY > 8 || distanceZ > 8) {
synchronizePosition();
// #synchronizePosition sets sync fields, it's safe to return
return;
} else if (positionChange && viewChange) {
EntityPositionAndRotationPacket positionAndRotationPacket = EntityPositionAndRotationPacket
.getPacket(getEntityId(), position, lastSyncedPosition, isOnGround());
sendPacketToViewers(positionAndRotationPacket);
// Fix head rotation
final EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId();
entityHeadLookPacket.yaw = position.getYaw();
sendPacketToViewersAndSelf(entityHeadLookPacket);
} else if (positionChange) {
final EntityPositionPacket entityPositionPacket = EntityPositionPacket
.getPacket(getEntityId(), position, lastSyncedPosition, onGround);
sendPacketToViewers(entityPositionPacket);
} else if (viewChange) {
final EntityRotationPacket entityRotationPacket = new EntityRotationPacket();
entityRotationPacket.entityId = getEntityId();
entityRotationPacket.yaw = position.getYaw();
entityRotationPacket.pitch = position.getPitch();
entityRotationPacket.onGround = onGround;
final EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId();
entityHeadLookPacket.yaw = position.getYaw();
if (clientSide) {
sendPacketToViewers(entityHeadLookPacket);
sendPacketToViewers(entityRotationPacket);
} else {
sendPacketToViewersAndSelf(entityHeadLookPacket);
sendPacketToViewersAndSelf(entityRotationPacket);
}
} else {
// Nothing changed, return
return;
}
if (PlayerUtils.isNettyClient(this) && !clientSide) {
final PlayerPositionAndLookPacket playerPositionAndLookPacket = new PlayerPositionAndLookPacket();
playerPositionAndLookPacket.flags = 0b111;
playerPositionAndLookPacket.position = position.clone().subtract(lastSyncedPosition.getX(), lastSyncedPosition.getY(), lastSyncedPosition.getZ());
playerPositionAndLookPacket.teleportId = ((Player)this).getNextTeleportId();
((Player) this).getPlayerConnection().sendPacket(playerPositionAndLookPacket);
}
lastSyncedPosition.set(position);
}
/** /**
* Gets the number of ticks this entity has been active for. * Gets the number of ticks this entity has been active for.
* *
@ -1313,13 +1349,10 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
* @param y new position Y * @param y new position Y
* @param z new position Z * @param z new position Z
*/ */
public void refreshPosition(double x, double y, double z) { private void refreshPosition(double x, double y, double z) {
position.setX(x); position.setX(x);
position.setY(y); position.setY(y);
position.setZ(z); position.setZ(z);
lastSyncedPosition.setX(x);
lastSyncedPosition.setY(y);
lastSyncedPosition.setZ(z);
if (hasPassenger()) { if (hasPassenger()) {
for (Entity passenger : getPassengers()) { for (Entity passenger : getPassengers()) {
@ -1358,11 +1391,20 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
} }
/** /**
* @param position the new position * Updates internal fields and sends updates
*
* @param position the new position
*
* @see #refreshPosition(double, double, double) * @see #refreshPosition(double, double, double)
* @see #refreshView(float, float)
* @see #sendPositionUpdate(boolean)
*/ */
public void refreshPosition(@NotNull Position position) { @ApiStatus.Internal
refreshPosition(position.getX(), position.getY(), position.getZ()); public void refreshPosition(@NotNull final Position position) {
if (!position.isSimilar(this.position))
refreshPosition(position.getX(), position.getY(), position.getZ());
refreshView(position.getYaw(), position.getPitch());
sendPositionUpdate(true);
} }
/** /**
@ -1373,13 +1415,11 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
* @param yaw the yaw * @param yaw the yaw
* @param pitch the pitch * @param pitch the pitch
*/ */
public void refreshView(float yaw, float pitch) { private void refreshView(final float yaw, final float pitch) {
this.lastPosition.setYaw(position.getYaw()); lastPosition.setYaw(position.getYaw());
this.lastPosition.setPitch(position.getPitch()); lastPosition.setPitch(position.getPitch());
position.setYaw(yaw); position.setYaw(yaw);
position.setPitch(pitch); position.setPitch(pitch);
this.lastSyncedPosition.setYaw(yaw);
this.lastSyncedPosition.setPitch(pitch);
} }
/** /**

View File

@ -66,6 +66,7 @@ import net.minestom.server.utils.*;
import net.minestom.server.utils.chunk.ChunkCallback; import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.entity.EntityUtils;
import net.minestom.server.utils.identity.NamedAndIdentified;
import net.minestom.server.utils.instance.InstanceUtils; import net.minestom.server.utils.instance.InstanceUtils;
import net.minestom.server.utils.inventory.PlayerInventoryUtils; import net.minestom.server.utils.inventory.PlayerInventoryUtils;
import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.Cooldown;
@ -89,12 +90,13 @@ import java.util.function.UnaryOperator;
* <p> * <p>
* You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}. * You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}.
*/ */
public class Player extends LivingEntity implements CommandSender, Localizable, HoverEventSource<ShowEntity>, Identified { public class Player extends LivingEntity implements CommandSender, Localizable, HoverEventSource<ShowEntity>, Identified, NamedAndIdentified {
private long lastKeepAlive; private long lastKeepAlive;
private boolean answerKeepAlive; private boolean answerKeepAlive;
private String username; private String username;
private Component usernameComponent;
protected final PlayerConnection playerConnection; protected final PlayerConnection playerConnection;
// All the entities that this player can see // All the entities that this player can see
protected final Set<Entity> viewableEntities = ConcurrentHashMap.newKeySet(); protected final Set<Entity> viewableEntities = ConcurrentHashMap.newKeySet();
@ -148,9 +150,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Only used if multi player breaking is disabled, contains only this player // Only used if multi player breaking is disabled, contains only this player
private final Set<Player> targetBreakers = Collections.singleton(this); private final Set<Player> targetBreakers = Collections.singleton(this);
// Position synchronization with viewers
private final Position lastSyncedPlayerPosition;
// Experience orb pickup // Experience orb pickup
protected Cooldown experiencePickupCooldown = new Cooldown(new UpdateOption(10, TimeUnit.TICK)); protected Cooldown experiencePickupCooldown = new Cooldown(new UpdateOption(10, TimeUnit.TICK));
@ -182,12 +181,12 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
public Player(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection playerConnection) { public Player(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection playerConnection) {
super(EntityType.PLAYER, uuid); super(EntityType.PLAYER, uuid);
this.username = username; this.username = username;
this.usernameComponent = Component.text(username);
this.playerConnection = playerConnection; this.playerConnection = playerConnection;
setBoundingBox(0.6f, 1.8f, 0.6f); setBoundingBox(0.6f, 1.8f, 0.6f);
setRespawnPoint(new Position(0, 0, 0)); setRespawnPoint(new Position(0, 0, 0));
this.lastSyncedPlayerPosition = new Position();
this.settings = new PlayerSettings(); this.settings = new PlayerSettings();
this.inventory = new PlayerInventory(this); this.inventory = new PlayerInventory(this);
@ -421,50 +420,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Tick event // Tick event
callEvent(PlayerTickEvent.class, playerTickEvent); callEvent(PlayerTickEvent.class, playerTickEvent);
// Multiplayer sync
if (!viewers.isEmpty()) {
final boolean positionChanged = !position.isSimilar(lastSyncedPlayerPosition);
final boolean viewChanged = !position.hasSimilarView(lastSyncedPlayerPosition);
if (positionChanged || viewChanged) {
// Player moved since last time
ServerPacket updatePacket;
ServerPacket optionalUpdatePacket = null;
if (positionChanged && viewChanged) {
updatePacket = EntityPositionAndRotationPacket.getPacket(getEntityId(),
position, lastSyncedPlayerPosition, onGround);
} else if (positionChanged) {
updatePacket = EntityPositionPacket.getPacket(getEntityId(),
position, lastSyncedPlayerPosition, onGround);
} else {
// View changed
updatePacket = EntityRotationPacket.getPacket(getEntityId(),
position.getYaw(), position.getPitch(), onGround);
}
if (viewChanged) {
// Yaw from the rotation packet seems to be ignored, which is why this is required
EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId();
entityHeadLookPacket.yaw = position.getYaw();
optionalUpdatePacket = entityHeadLookPacket;
}
// Send the update packet
if (optionalUpdatePacket != null) {
sendPacketsToViewers(updatePacket, optionalUpdatePacket);
} else {
sendPacketToViewers(updatePacket);
}
}
// Update sync data
lastSyncedPlayerPosition.set(position);
}
} }
@Override @Override
@ -653,19 +608,13 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
sendDimension(instanceDimensionType); sendDimension(instanceDimensionType);
} }
// Load all the required chunks // Only load the spawning chunk to speed up login, remaining chunks are loaded in #spawnPlayer
final long[] visibleChunks = ChunkUtils.getChunksInRange(spawnPosition, getChunkRange()); final long[] visibleChunks = ChunkUtils.getChunksInRange(spawnPosition, 0);
final ChunkCallback endCallback = chunk -> { final ChunkCallback endCallback =
// This is the last chunk to be loaded , spawn player chunk -> spawnPlayer(instance, spawnPosition, firstSpawn, dimensionChange, true);
spawnPlayer(instance, spawnPosition, firstSpawn, true, dimensionChange);
};
// Chunk 0;0 always needs to be loaded
instance.loadChunk(0, 0, chunk ->
// Load all the required chunks
ChunkUtils.optionalLoadAll(instance, visibleChunks, null, endCallback));
ChunkUtils.optionalLoadAll(instance, visibleChunks, null, endCallback);
} else { } else {
// The player already has the good version of all the chunks. // 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 // We just need to refresh his entity viewing list and add him to the instance
@ -694,23 +643,20 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* *
* @param spawnPosition the position to teleport the player * @param spawnPosition the position to teleport the player
* @param firstSpawn true if this is the player first spawn * @param firstSpawn true if this is the player first spawn
* @param updateChunks true if chunks should be refreshed, false if the new instance shares the same
* chunks
*/ */
private void spawnPlayer(@NotNull Instance instance, @NotNull Position spawnPosition, private void spawnPlayer(@NotNull Instance instance, @NotNull Position spawnPosition,
boolean firstSpawn, boolean updateChunks, boolean dimensionChange) { boolean firstSpawn, boolean dimensionChange, boolean updateChunks) {
// Clear previous instance elements
if (!firstSpawn) { if (!firstSpawn) {
// Player instance changed, clear current viewable collections
this.viewableChunks.forEach(chunk -> chunk.removeViewer(this)); this.viewableChunks.forEach(chunk -> chunk.removeViewer(this));
this.viewableEntities.forEach(entity -> entity.removeViewer(this)); this.viewableEntities.forEach(entity -> entity.removeViewer(this));
} }
super.setInstance(instance, spawnPosition); super.setInstance(instance, spawnPosition);
this.lastSyncedPlayerPosition.set(position);
if (!position.isSimilar(spawnPosition) && !firstSpawn) { if (updateChunks) {
// Player changed instance at a different position
teleport(spawnPosition);
} else if (updateChunks) {
// Send newly visible chunks to player once spawned in the instance
refreshVisibleChunks(); refreshVisibleChunks();
} }
@ -1330,12 +1276,26 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
} }
/** /**
* Gets the player username. * Gets the player's name as a component. This will either return the display name
* (if set) or a component holding the username.
* *
* @return the player username * @return the name
*/ */
@NotNull @Override
public String getUsername() { public @NotNull Component getName() {
if (this.displayName != null) {
return this.displayName;
} else {
return this.usernameComponent;
}
}
/**
* Gets the player's username.
*
* @return the player's username
*/
public @NotNull String getUsername() {
return username; return username;
} }
@ -1347,6 +1307,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
*/ */
public void setUsernameField(@NotNull String username) { public void setUsernameField(@NotNull String username) {
this.username = username; this.username = username;
this.usernameComponent = Component.text(username);
} }
private void sendChangeGameStatePacket(@NotNull ChangeGameStatePacket.Reason reason, float value) { private void sendChangeGameStatePacket(@NotNull ChangeGameStatePacket.Reason reason, float value) {
@ -1470,7 +1431,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* and send data to his new viewers. * and send data to his new viewers.
*/ */
protected void refreshAfterTeleport() { protected void refreshAfterTeleport() {
getInventory().update();
sendPacketsToViewers(getEntityType().getSpawnType().getSpawnPacket(this)); sendPacketsToViewers(getEntityType().getSpawnType().getSpawnPacket(this));
@ -1480,6 +1440,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
sendPacketToViewersAndSelf(getPropertiesPacket()); sendPacketToViewersAndSelf(getPropertiesPacket());
sendPacketToViewersAndSelf(getEquipmentsPacket()); sendPacketToViewersAndSelf(getEquipmentsPacket());
getInventory().update();
{ {
// Send new chunks // Send new chunks
final BlockPosition pos = position.toBlockPosition(); final BlockPosition pos = position.toBlockPosition();
@ -1585,27 +1547,25 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
final int[] oldChunks = ArrayUtils.getDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks); final int[] oldChunks = ArrayUtils.getDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks);
final int[] newChunks = ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks); final int[] newChunks = ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks);
// Update client render distance
updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
// Unload old chunks // Unload old chunks
for (int index : oldChunks) { for (int index : oldChunks) {
final long chunkIndex = lastVisibleChunks[index]; final long chunkIndex = lastVisibleChunks[index];
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex); final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex); final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
// TODO prevent the client from getting lag spikes when re-loading large chunks final UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
// Probably by having a distinction between visible and loaded (cache) chunks
/*UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
unloadChunkPacket.chunkX = chunkX; unloadChunkPacket.chunkX = chunkX;
unloadChunkPacket.chunkZ = chunkZ; unloadChunkPacket.chunkZ = chunkZ;
playerConnection.sendPacket(unloadChunkPacket);*/ playerConnection.sendPacket(unloadChunkPacket);
final Chunk chunk = instance.getChunk(chunkX, chunkZ); final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk != null) if (chunk != null)
chunk.removeViewer(this); chunk.removeViewer(this);
} }
// Update client render distance
updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
// Load new chunks // Load new chunks
for (int index : newChunks) { for (int index : newChunks) {
final long chunkIndex = updatedVisibleChunks[index]; final long chunkIndex = updatedVisibleChunks[index];
@ -2007,6 +1967,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
playerConnection.sendPacket(updateViewPositionPacket); playerConnection.sendPacket(updateViewPositionPacket);
} }
public int getNextTeleportId() {
return teleportId.getAndIncrement();
}
public int getLastSentTeleportId() { public int getLastSentTeleportId() {
return teleportId.get(); return teleportId.get();
} }
@ -2397,17 +2361,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* based on which one is the lowest * based on which one is the lowest
*/ */
public int getChunkRange() { public int getChunkRange() {
final int playerRange = getSettings().viewDistance; return Math.min(getSettings().viewDistance, MinecraftServer.getChunkViewDistance());
if (playerRange < 1) {
// Didn't receive settings packet yet (is the case on login)
// In this case we send an arbitrary number of chunks
// Will be updated in PlayerSettings#refresh.
// Non-compliant clients might also be stuck with this view
return 7;
} else {
final int serverRange = MinecraftServer.getChunkViewDistance();
return Math.min(playerRange, serverRange);
}
} }
/** /**
@ -2639,6 +2593,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
private byte displayedSkinParts; private byte displayedSkinParts;
private MainHand mainHand; private MainHand mainHand;
public PlayerSettings() {
viewDistance = 2;
}
/** /**
* The player game language. * The player game language.
* *

View File

@ -1,29 +1,59 @@
package net.minestom.server.event.server; package net.minestom.server.event.server;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.CancellableEvent; import net.minestom.server.event.CancellableEvent;
import net.minestom.server.event.Event; import net.minestom.server.event.Event;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.ping.ResponseData; import net.minestom.server.ping.ResponseData;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.ping.ServerListPingType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/** /**
* Called when a {@link PlayerConnection} sends a status packet, * Called when a {@link PlayerConnection} sends a status packet,
* usually to display information on the server list. * usually to display information on the server list.
*/ */
public class ServerListPingEvent extends Event implements CancellableEvent { public class ServerListPingEvent extends Event implements CancellableEvent {
private boolean cancelled = false;
private final ResponseData responseData;
private final PlayerConnection connection; private final PlayerConnection connection;
private final ServerListPingType type;
private boolean cancelled = false;
private ResponseData responseData;
public ServerListPingEvent(ResponseData responseData, PlayerConnection connection) { /**
this.responseData = responseData; * Creates a new server list ping event with no player connection.
this.connection = connection; *
* @param type the ping type to respond with
*/
public ServerListPingEvent(@NotNull ServerListPingType type) {
this(null, type);
} }
/** /**
* ResponseData being returned. * Creates a new server list ping event.
*
* @param connection the player connection, if the ping type is modern
* @param type the ping type to respond with
*/
public ServerListPingEvent(@Nullable PlayerConnection connection, @NotNull ServerListPingType type) {
//noinspection deprecation we need to continue doing this until the consumer is removed - todo remove
ResponseDataConsumer consumer = MinecraftServer.getResponseDataConsumer();
this.responseData = new ResponseData();
if (consumer != null) {
consumer.accept(connection, responseData);
}
this.connection = connection;
this.type = type;
}
/**
* Gets the response data that is sent to the client.
* This is mutable and can be modified to change what is returned.
* *
* @return the response data being returned * @return the response data being returned
*/ */
@ -32,16 +62,32 @@ public class ServerListPingEvent extends Event implements CancellableEvent {
} }
/** /**
* PlayerConnection of received packet. * Sets the response data, overwriting the exiting data.
* *
* Note that the player has not joined the server at this time. * @param responseData the new data
*/
public void setResponseData(@NotNull ResponseData responseData) {
this.responseData = Objects.requireNonNull(responseData);
}
/**
* PlayerConnection of received packet. Note that the player has not joined the server
* at this time. This will <b>only</b> be non-null for modern server list pings.
* *
* @return the playerConnection. * @return the playerConnection.
*/ */
public @NotNull PlayerConnection getConnection() { public @Nullable PlayerConnection getConnection() {
return connection; return connection;
} }
/**
* Gets the ping type that the client is pinging with.
*
* @return the ping type
*/
public @NotNull ServerListPingType getPingType() {
return type;
}
@Override @Override
public boolean isCancelled() { public boolean isCancelled() {
@ -49,7 +95,8 @@ public class ServerListPingEvent extends Event implements CancellableEvent {
} }
/** /**
* Cancelling this event will cause you server to appear offline in the vanilla server list. * Cancelling this event will cause the server to appear offline in the vanilla server list.
* Note that this will have no effect if the ping version is {@link ServerListPingType#OPEN_TO_LAN}.
* *
* @param cancel true if the event should be cancelled, false otherwise * @param cancel true if the event should be cancelled, false otherwise
*/ */
@ -57,5 +104,4 @@ public class ServerListPingEvent extends Event implements CancellableEvent {
public void setCancelled(boolean cancel) { public void setCancelled(boolean cancel) {
this.cancelled = cancel; this.cancelled = cancel;
} }
} }

View File

@ -0,0 +1,140 @@
package net.minestom.server.extras.lan;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.timer.Task;
import net.minestom.server.utils.NetworkUtils;
import net.minestom.server.utils.time.Cooldown;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import static net.minestom.server.ping.ServerListPingType.OPEN_TO_LAN;
/**
* Utility class to manage opening the server to LAN. Note that this <b>doesn't</b> actually
* open your server to LAN if it isn't already visible to anyone on your local network.
* Instead it simply sends the packets needed to trick the Minecraft client into thinking
* that this is a single-player world that has been opened to LANfor it to be displayed on
* the bottom of the server list.
* @see <a href="https://wiki.vg/Server_List_Ping#Ping_via_LAN_.28Open_to_LAN_in_Singleplayer.29">wiki.vg</a>
*/
public class OpenToLAN {
private static final InetSocketAddress PING_ADDRESS = new InetSocketAddress("224.0.2.60", 4445);
private static final Logger LOGGER = LoggerFactory.getLogger(OpenToLAN.class);
private static volatile Cooldown eventCooldown;
private static volatile DatagramSocket socket = null;
private static volatile DatagramPacket packet = null;
private static volatile Task task = null;
private OpenToLAN() { }
/**
* Opens the server to LAN with the default config.
*
* @return {@code true} if it was opened successfully, {@code false} otherwise
*/
public static boolean open() {
return open(new OpenToLANConfig());
}
/**
* Opens the server to LAN.
*
* @param config the configuration
* @return {@code true} if it was opened successfully, {@code false} otherwise
*/
public static boolean open(@NotNull OpenToLANConfig config) {
Objects.requireNonNull(config, "config");
if (socket != null) {
return false;
} else {
int port = config.port;
if (port == 0) {
try {
port = NetworkUtils.getFreePort();
} catch (IOException e) {
LOGGER.warn("Could not find an open port!", e);
return false;
}
}
try {
socket = new DatagramSocket(port);
} catch (SocketException e) {
LOGGER.warn("Could not bind to the port!", e);
return false;
}
eventCooldown = new Cooldown(config.delayBetweenEvent);
task = MinecraftServer.getSchedulerManager().buildTask(OpenToLAN::ping)
.repeat(config.delayBetweenPings.getValue(), config.delayBetweenPings.getTimeUnit())
.schedule();
return true;
}
}
/**
* Closes the server to LAN.
*
* @return {@code true} if it was closed, {@code false} if it was already closed
*/
public static boolean close() {
if (socket == null) {
return false;
} else {
task.cancel();
socket.close();
task = null;
socket = null;
return true;
}
}
/**
* Checks if the server is currently opened to LAN.
*
* @return {@code true} if it is, {@code false} otherwise
*/
public static boolean isOpen() {
return socket != null;
}
/**
* Performs the ping.
*/
private static void ping() {
if (MinecraftServer.getNettyServer().getPort() != 0) {
if (packet == null || eventCooldown.isReady(System.currentTimeMillis())) {
final ServerListPingEvent event = new ServerListPingEvent(OPEN_TO_LAN);
MinecraftServer.getGlobalEventHandler().callEvent(ServerListPingEvent.class, event);
final byte[] data = OPEN_TO_LAN.getPingResponse(event.getResponseData()).getBytes(StandardCharsets.UTF_8);
packet = new DatagramPacket(data, data.length, PING_ADDRESS);
eventCooldown.refreshLastUpdate(System.currentTimeMillis());
}
try {
socket.send(packet);
} catch (IOException e) {
LOGGER.warn("Could not send Open to LAN packet!", e);
}
}
}
}

View File

@ -0,0 +1,65 @@
package net.minestom.server.extras.lan;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* Configuration for opening the server to LAN.
*
* @see OpenToLAN#open(OpenToLANConfig)
*/
public class OpenToLANConfig {
int port;
UpdateOption delayBetweenPings, delayBetweenEvent;
/**
* Creates a new config with the port set to random and the delay between pings set
* to 1.5 seconds and the delay between event calls set to 30 seconds.
*/
public OpenToLANConfig() {
this.port = 0;
this.delayBetweenPings = new UpdateOption(1500, TimeUnit.MILLISECOND);
this.delayBetweenEvent = new UpdateOption(30, TimeUnit.SECOND);
}
/**
* Sets the port used to send pings from. Use {@code 0} to pick a random free port.
*
* @param port the port
* @return {@code this}, for chaining
*/
@Contract("_ -> this")
public @NotNull OpenToLANConfig port(int port) {
this.port = port;
return this;
}
/**
* Sets the delay between outgoing pings.
*
* @param delay the delay
* @return {@code this}, for chaining
*/
@Contract("_ -> this")
public @NotNull OpenToLANConfig pingDelay(@NotNull UpdateOption delay) {
this.delayBetweenPings = Objects.requireNonNull(delay, "delay");
return this;
}
/**
* Sets the delay between calls of {@link ServerListPingEvent}.
*
* @param delay the delay
* @return {@code this}, for chaining
*/
@Contract("_ -> this")
public @NotNull OpenToLANConfig eventCallDelay(@NotNull UpdateOption delay) {
this.delayBetweenEvent = Objects.requireNonNull(delay, "delay");
return this;
}
}

View File

@ -0,0 +1,224 @@
package net.minestom.server.extras.query;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extras.query.event.BasicQueryEvent;
import net.minestom.server.extras.query.event.FullQueryEvent;
import net.minestom.server.timer.Task;
import net.minestom.server.utils.NetworkUtils;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.binary.Writeable;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Random;
/**
* Utility class to manage responses to the GameSpy4 Query Protocol.
* @see <a href="https://wiki.vg/Query">wiki.vg</a>
*/
public class Query {
public static final Charset CHARSET = StandardCharsets.ISO_8859_1;
private static final Logger LOGGER = LoggerFactory.getLogger(Query.class);
private static final Random RANDOM = new Random();
private static final Int2ObjectMap<SocketAddress> CHALLENGE_TOKENS = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());
private static volatile boolean started;
private static volatile DatagramSocket socket;
private static volatile Thread thread;
private static volatile Task task;
private Query() { }
/**
* Starts the query system, responding to queries on a random port, logging if it could not be started.
*
* @return the port
* @throws IllegalArgumentException if the system was already running
*/
public static int start() {
if (socket != null) {
throw new IllegalArgumentException("System is already running");
} else {
int port;
try {
port = NetworkUtils.getFreePort();
} catch (IOException e) {
LOGGER.warn("Could not find an open port!", e);
return -1;
}
start(port);
return port;
}
}
/**
* Starts the query system, responding to queries on a given port, logging if it could not be started.
*
* @param port the port
* @return {@code true} if the query system started successfully, {@code false} otherwise
*/
public static boolean start(int port) {
if (socket != null) {
return false;
} else {
try {
socket = new DatagramSocket(port);
} catch (SocketException e) {
LOGGER.warn("Could not open the query port!", e);
return false;
}
thread = new Thread(Query::run);
thread.start();
started = true;
task = MinecraftServer.getSchedulerManager()
.buildTask(CHALLENGE_TOKENS::clear)
.repeat(30, TimeUnit.SECOND)
.schedule();
return true;
}
}
/**
* Stops the query system.
*
* @return {@code true} if the query system was stopped, {@code false} if it was not running
*/
public boolean stop() {
if (!started) {
return false;
} else {
started = false;
thread = null;
socket.close();
socket = null;
task.cancel();
CHALLENGE_TOKENS.clear();
return true;
}
}
/**
* Checks if the query system has been started.
*
* @return {@code true} if it has been started, {@code false} otherwise
*/
public boolean isStarted() {
return started;
}
private static void run() {
final byte[] buffer = new byte[16];
while (started) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// try and receive the packet
try {
socket.receive(packet);
} catch (IOException e) {
if (!started) {
LOGGER.error("An error occurred whilst receiving a query packet.", e);
continue;
} else {
return;
}
}
// get the contents
ByteBuf data = Unpooled.wrappedBuffer(packet.getData());
// check the magic field
if (data.readUnsignedShort() != 0xFEFD) {
continue;
}
// now check the query type
byte type = data.readByte();
if (type == 9) { // handshake
int sessionID = data.readInt();
int challengeToken = RANDOM.nextInt();
CHALLENGE_TOKENS.put(challengeToken, packet.getSocketAddress());
// send the response
BinaryWriter response = new BinaryWriter(32);
response.writeByte((byte) 9);
response.writeInt(sessionID);
response.writeNullTerminatedString(String.valueOf(challengeToken), CHARSET);
try {
byte[] responseData = response.toByteArray();
socket.send(new DatagramPacket(responseData, responseData.length, packet.getSocketAddress()));
} catch (IOException e) {
if (!started) {
LOGGER.error("An error occurred whilst sending a query handshake packet.", e);
} else {
return;
}
}
} else if (type == 0) { // stat
int sessionID = data.readInt();
int challengeToken = data.readInt();
SocketAddress sender = packet.getSocketAddress();
if (CHALLENGE_TOKENS.containsKey(challengeToken) && CHALLENGE_TOKENS.get(challengeToken).equals(sender)) {
int remaining = data.readableBytes();
if (remaining == 0) { // basic
BasicQueryEvent event = new BasicQueryEvent(sender, sessionID);
MinecraftServer.getGlobalEventHandler().callCancellableEvent(BasicQueryEvent.class, event,
() -> sendResponse(event.getQueryResponse(), sessionID, sender));
} else if (remaining == 5) { // full
FullQueryEvent event = new FullQueryEvent(sender, sessionID);
MinecraftServer.getGlobalEventHandler().callCancellableEvent(FullQueryEvent.class, event,
() -> sendResponse(event.getQueryResponse(), sessionID, sender));
}
}
}
}
}
private static void sendResponse(@NotNull Writeable queryResponse, int sessionID, @NotNull SocketAddress sender) {
// header
BinaryWriter response = new BinaryWriter();
response.writeByte((byte) 0);
response.writeInt(sessionID);
// payload
queryResponse.write(response);
// send!
byte[] responseData = response.toByteArray();
try {
socket.send(new DatagramPacket(responseData, responseData.length, sender));
} catch (IOException e) {
if (!started) {
LOGGER.error("An error occurred whilst sending a query handshake packet.", e);
}
}
}
}

View File

@ -0,0 +1,22 @@
package net.minestom.server.extras.query.event;
import net.minestom.server.extras.query.response.BasicQueryResponse;
import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress;
/**
* An event called when a basic query is received and ready to be responded to.
*/
public class BasicQueryEvent extends QueryEvent<BasicQueryResponse> {
/**
* Creates a new basic query event.
*
* @param sessionID the session ID
* @param sender the sender
*/
public BasicQueryEvent(@NotNull SocketAddress sender, int sessionID) {
super(sender, sessionID, new BasicQueryResponse());
}
}

View File

@ -0,0 +1,22 @@
package net.minestom.server.extras.query.event;
import net.minestom.server.extras.query.response.FullQueryResponse;
import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress;
/**
* An event called when a full query is received and ready to be responded to.
*/
public class FullQueryEvent extends QueryEvent<FullQueryResponse> {
/**
* Creates a new full query event.
*
* @param sender the sender
* @param sessionID the sessionID
*/
public FullQueryEvent(@NotNull SocketAddress sender, int sessionID) {
super(sender, sessionID, new FullQueryResponse());
}
}

View File

@ -0,0 +1,83 @@
package net.minestom.server.extras.query.event;
import net.minestom.server.event.CancellableEvent;
import net.minestom.server.event.Event;
import net.minestom.server.utils.binary.Writeable;
import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress;
import java.util.Objects;
/**
* An event called when a query is received and ready to be responded to.
*
* @param <T> the type of the response
*/
public abstract class QueryEvent<T extends Writeable> extends Event implements CancellableEvent {
private final SocketAddress sender;
private final int sessionID;
private T response;
private boolean cancelled;
/**
* Creates a new query event.
*
* @param sender the sender
* @param sessionID the session ID of the query sender
* @param response the initial response
*/
public QueryEvent(@NotNull SocketAddress sender, int sessionID, @NotNull T response) {
this.sender = sender;
this.sessionID = sessionID;
this.response = response;
this.cancelled = false;
}
/**
* Gets the query response that will be sent back to the sender.
* This can be mutated.
*
* @return the response
*/
public T getQueryResponse() {
return this.response;
}
/**
* Sets the query response that will be sent back to the sender.
*
* @param response the response
*/
public void setQueryResponse(@NotNull T response) {
this.response = Objects.requireNonNull(response, "response");
}
/**
* Gets the socket address of the initiator of the query.
*
* @return the initiator
*/
public @NotNull SocketAddress getSender() {
return this.sender;
}
/**
* Gets the Session ID of the initiator of the query.
*
* @return the session ID
*/
public int getSessionID() {
return this.sessionID;
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@ -0,0 +1,148 @@
package net.minestom.server.extras.query.response;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extras.query.Query;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.binary.Writeable;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* A basic query response containing a fixed set of responses.
*/
public class BasicQueryResponse implements Writeable {
private String motd, gametype, map, numPlayers, maxPlayers;
/**
* Creates a new basic query response with pre-filled default values.
*/
public BasicQueryResponse() {
this.motd = "A Minestom Server";
this.gametype = "SMP";
this.map = "world";
this.numPlayers = String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size());
this.maxPlayers = String.valueOf(Integer.parseInt(this.numPlayers) + 1);
}
/**
* Gets the MoTD.
*
* @return the motd
*/
public @NotNull String getMotd() {
return this.motd;
}
/**
* Sets the MoTD.
*
* @param motd the motd
*/
public void setMotd(@NotNull String motd) {
this.motd = Objects.requireNonNull(motd, "motd");
}
/**
* Gets the gametype.
*
* @return the gametype
*/
public @NotNull String getGametype() {
return this.gametype;
}
/**
* Sets the gametype.
*
* @param gametype the gametype
*/
public void setGametype(@NotNull String gametype) {
this.gametype = Objects.requireNonNull(gametype, "gametype");
}
/**
* Gets the map.
*
* @return the map
*/
public @NotNull String getMap() {
return this.map;
}
/**
* Sets the map.
*
* @param map the map
*/
public void setMap(@NotNull String map) {
this.map = Objects.requireNonNull(map, "map");
}
/**
* Gets the number of players.
*
* @return the number of players
*/
public @NotNull String getNumPlayers() {
return this.numPlayers;
}
/**
* Sets the number of players.
*
* @param numPlayers the number of players
*/
public void setNumPlayers(@NotNull String numPlayers) {
this.numPlayers = Objects.requireNonNull(numPlayers, "numPlayers");
}
/**
* Sets the number of players.
* This method is just an overload for {@link #setNumPlayers(String)}.
*
* @param numPlayers the number of players
*/
public void setNumPlayers(int numPlayers) {
this.setNumPlayers(String.valueOf(numPlayers));
}
/**
* Gets the max number of players.
*
* @return the max number of players
*/
public @NotNull String getMaxPlayers() {
return this.maxPlayers;
}
/**
* Sets the max number of players.
*
* @param maxPlayers the max number of players
*/
public void setMaxPlayers(@NotNull String maxPlayers) {
this.maxPlayers = Objects.requireNonNull(maxPlayers, "maxPlayers");
}
/**
* Sets the max number of players.
* This method is just an overload for {@link #setMaxPlayers(String)}
*
* @param maxPlayers the max number of players
*/
public void setMaxPlayers(int maxPlayers) {
this.setMaxPlayers(String.valueOf(maxPlayers));
}
@Override
public void write(@NotNull BinaryWriter writer) {
writer.writeNullTerminatedString(this.motd, Query.CHARSET);
writer.writeNullTerminatedString(this.gametype, Query.CHARSET);
writer.writeNullTerminatedString(this.map, Query.CHARSET);
writer.writeNullTerminatedString(this.numPlayers, Query.CHARSET);
writer.writeNullTerminatedString(this.maxPlayers, Query.CHARSET);
writer.getBuffer().writeShortLE(MinecraftServer.getNettyServer().getPort());
writer.writeNullTerminatedString(Objects.requireNonNullElse(MinecraftServer.getNettyServer().getAddress(), ""), Query.CHARSET);
}
}

View File

@ -0,0 +1,161 @@
package net.minestom.server.extras.query.response;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extensions.Extension;
import net.minestom.server.extras.query.Query;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.binary.Writeable;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.Collectors;
/**
* A full query response containing a dynamic set of responses.
*/
public class FullQueryResponse implements Writeable {
private static final PlainComponentSerializer PLAIN = PlainComponentSerializer.plain();
private static final byte[] PADDING_10 = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
PADDING_11 = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
private Map<String, String> kv;
private List<String> players;
/**
* Creates a new full query response with default values set.
*/
public FullQueryResponse() {
this.kv = new HashMap<>();
// populate defaults
for (QueryKey key : QueryKey.VALUES) {
this.kv.put(key.getKey(), key.getValue());
}
this.players = MinecraftServer.getConnectionManager().getOnlinePlayers()
.stream()
.map(player -> PLAIN.serialize(player.getName()))
.collect(Collectors.toList());
}
/**
* Puts a key-value mapping into the response.
*
* @param key the key
* @param value the value
*/
public void put(@NotNull QueryKey key, @NotNull String value) {
this.put(key.getKey(), value);
}
/**
* Puts a key-value mapping into the response.
*
* @param key the key
* @param value the value
*/
public void put(@NotNull String key, @NotNull String value) {
this.kv.put(key, value);
}
/**
* Gets the map containing the key-value mappings.
*
* @return the map
*/
public @NotNull Map<String, String> getKeyValuesMap() {
return this.kv;
}
/**
* Sets the map containing the key-value mappings.
*
* @param map the map
*/
public void setKeyValuesMap(@NotNull Map<String, String> map) {
this.kv = Objects.requireNonNull(map, "map");
}
/**
* Adds some players to the response.
*
* @param players the players
*/
public void addPlayers(@NotNull String @NotNull... players) {
Collections.addAll(this.players, players);
}
/**
* Adds some players to the response.
*
* @param players the players
*/
public void addPlayers(@NotNull Collection<String> players) {
this.players.addAll(players);
}
/**
* Gets the list of players.
*
* @return the list
*/
public @NotNull List<String> getPlayers() {
return this.players;
}
/**
* Sets the list of players.
*
* @param players the players
*/
public void setPlayers(@NotNull List<String> players) {
this.players = Objects.requireNonNull(players, "players");
}
/**
* Generates the default plugins value. That being the server name and version followed
* by the name and version for each extension.
*
* @return the string result
*/
public static String generatePluginsValue() {
StringBuilder builder = new StringBuilder(MinecraftServer.getBrandName())
.append(' ')
.append(MinecraftServer.VERSION_NAME);
if (!MinecraftServer.getExtensionManager().getExtensions().isEmpty()) {
for (Extension extension : MinecraftServer.getExtensionManager().getExtensions()) {
builder.append(extension.getOrigin().getName())
.append(' ')
.append(extension.getOrigin().getVersion())
.append("; ");
}
builder.delete(builder.length() - 2, builder.length());
}
return builder.toString();
}
@Override
public void write(@NotNull BinaryWriter writer) {
writer.writeBytes(PADDING_11);
// key-values
for (Map.Entry<String, String> entry : this.kv.entrySet()) {
writer.writeNullTerminatedString(entry.getKey(), Query.CHARSET);
writer.writeNullTerminatedString(entry.getValue(), Query.CHARSET);
}
writer.writeNullTerminatedString("", Query.CHARSET);
writer.writeBytes(PADDING_10);
// players
for (String player : this.players) {
writer.writeNullTerminatedString(player, Query.CHARSET);
}
writer.writeNullTerminatedString("", Query.CHARSET);
}
}

View File

@ -0,0 +1,57 @@
package net.minestom.server.extras.query.response;
import net.minestom.server.MinecraftServer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Supplier;
/**
* An enum of default query keys.
*/
public enum QueryKey {
HOSTNAME(() -> "A Minestom Server"),
GAME_TYPE(() -> "SMP"),
GAME_ID("game_id", () -> "MINECRAFT"),
VERSION(() -> MinecraftServer.VERSION_NAME),
PLUGINS(FullQueryResponse::generatePluginsValue),
MAP(() -> "world"),
NUM_PLAYERS("numplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size())),
MAX_PLAYERS("maxplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size() + 1)),
HOST_PORT("hostport", () -> String.valueOf(MinecraftServer.getNettyServer().getPort())),
HOST_IP("hostip", () -> Objects.requireNonNullElse(MinecraftServer.getNettyServer().getAddress(), "localhost"));
static QueryKey[] VALUES = QueryKey.values();
private final String key;
private final Supplier<String> value;
QueryKey(@NotNull Supplier<String> value) {
this(null, value);
}
QueryKey(@Nullable String key, @NotNull Supplier<String> value) {
this.key = Objects.requireNonNullElse(key, this.name().toLowerCase(Locale.ROOT).replace('_', ' '));
this.value = value;
}
/**
* Gets the key of this query key.
*
* @return the key
*/
public @NotNull String getKey() {
return this.key;
}
/**
* Gets the value of this query key.
*
* @return the value
*/
public @NotNull String getValue() {
return this.value.get();
}
}

View File

@ -417,22 +417,18 @@ public abstract class Chunk implements Viewable, Tickable, DataContainer {
UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime()); UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime());
updateLightPacket.chunkX = getChunkX(); updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ(); updateLightPacket.chunkZ = getChunkZ();
updateLightPacket.skyLightMask = 0x3FFF0; updateLightPacket.skyLightMask = 0b111111111111111111;
updateLightPacket.blockLightMask = 0x3F; updateLightPacket.emptySkyLightMask = 0b000000000000000000;
updateLightPacket.emptySkyLightMask = 0x0F; updateLightPacket.blockLightMask = 0b000000000000000000;
updateLightPacket.emptyBlockLightMask = 0x3FFC0; updateLightPacket.emptyBlockLightMask = 0b111111111111111111;
byte[] bytes = new byte[2048]; byte[] bytes = new byte[2048];
Arrays.fill(bytes, (byte) 0xFF); Arrays.fill(bytes, (byte) 0xFF);
List<byte[]> temp = new ArrayList<>(14); final List<byte[]> temp = new ArrayList<>(18);
List<byte[]> temp2 = new ArrayList<>(6); for (int i = 0; i < 18; ++i) {
for (int i = 0; i < 14; ++i) {
temp.add(bytes); temp.add(bytes);
} }
for (int i = 0; i < 6; ++i) {
temp2.add(bytes);
}
updateLightPacket.skyLight = temp; updateLightPacket.skyLight = temp;
updateLightPacket.blockLight = temp2; updateLightPacket.blockLight = new ArrayList<>(0);
return updateLightPacket; return updateLightPacket;
} }

View File

@ -26,6 +26,7 @@ import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
import java.util.Set; import java.util.Set;
/** /**
@ -59,7 +60,7 @@ public class DynamicChunk extends Chunk {
private long lastChangeTime; private long lastChangeTime;
private ChunkDataPacket cachedPacket; private SoftReference<ChunkDataPacket> cachedPacket = new SoftReference<>(null);
private long cachedPacketTime; private long cachedPacketTime;
public DynamicChunk(@NotNull Instance instance, @Nullable Biome[] biomes, int chunkX, int chunkZ, public DynamicChunk(@NotNull Instance instance, @Nullable Biome[] biomes, int chunkX, int chunkZ,
@ -386,22 +387,22 @@ public class DynamicChunk extends Chunk {
@NotNull @NotNull
@Override @Override
protected ChunkDataPacket createFreshPacket() { protected ChunkDataPacket createFreshPacket() {
if (cachedPacket != null && cachedPacketTime == getLastChangeTime()) { ChunkDataPacket packet = cachedPacket.get();
return cachedPacket; if (packet != null && cachedPacketTime == getLastChangeTime()) {
return packet;
} }
ChunkDataPacket fullDataPacket = new ChunkDataPacket(getIdentifier(), getLastChangeTime()); packet = new ChunkDataPacket(getIdentifier(), getLastChangeTime());
fullDataPacket.biomes = biomes; packet.biomes = biomes;
fullDataPacket.chunkX = chunkX; packet.chunkX = chunkX;
fullDataPacket.chunkZ = chunkZ; packet.chunkZ = chunkZ;
fullDataPacket.paletteStorage = blockPalette.clone(); packet.paletteStorage = blockPalette.clone();
fullDataPacket.customBlockPaletteStorage = customBlockPalette.clone(); packet.customBlockPaletteStorage = customBlockPalette.clone();
fullDataPacket.blockEntities = blockEntities.clone(); packet.blockEntities = blockEntities.clone();
fullDataPacket.blocksData = blocksData.clone(); packet.blocksData = blocksData.clone();
this.cachedPacketTime = getLastChangeTime(); this.cachedPacketTime = getLastChangeTime();
this.cachedPacket = fullDataPacket; this.cachedPacket = new SoftReference<>(packet);
return packet;
return fullDataPacket;
} }
@NotNull @NotNull

View File

@ -793,6 +793,10 @@ public class InstanceContainer extends Instance {
* Unsafe because it has to be done on the same thread as the instance/chunks tick update. * Unsafe because it has to be done on the same thread as the instance/chunks tick update.
*/ */
protected void UNSAFE_unloadChunks() { protected void UNSAFE_unloadChunks() {
if (scheduledChunksToRemove.isEmpty()) {
// Fast exit
return;
}
synchronized (scheduledChunksToRemove) { synchronized (scheduledChunksToRemove) {
for (Chunk chunk : scheduledChunksToRemove) { for (Chunk chunk : scheduledChunksToRemove) {
final int chunkX = chunk.getChunkX(); final int chunkX = chunk.getChunkX();

View File

@ -272,38 +272,40 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
@Override @Override
public boolean leftClick(@NotNull Player player, int slot) { public boolean leftClick(@NotNull Player player, int slot) {
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
final ItemStack cursor = getCursorItem(); final ItemStack cursor = getCursorItem();
final ItemStack clicked = getItemStack(convertPlayerInventorySlot(slot, OFFSET)); final ItemStack clicked = getItemStack(convertedSlot);
final InventoryClickResult clickResult = clickProcessor.leftClick(null, player, slot, clicked, cursor); final InventoryClickResult clickResult = clickProcessor.leftClick(null, player, convertedSlot, clicked, cursor);
if (clickResult.doRefresh()) if (clickResult.doRefresh())
sendSlotRefresh((short) slot, clicked); sendSlotRefresh((short) slot, clicked);
setItemStack(slot, OFFSET, clickResult.getClicked()); setItemStack(convertedSlot, clickResult.getClicked());
setCursorItem(clickResult.getCursor()); setCursorItem(clickResult.getCursor());
if (!clickResult.isCancel()) if (!clickResult.isCancel())
callClickEvent(player, null, slot, ClickType.LEFT_CLICK, clicked, cursor); callClickEvent(player, null, convertedSlot, ClickType.LEFT_CLICK, clicked, cursor);
return !clickResult.isCancel(); return !clickResult.isCancel();
} }
@Override @Override
public boolean rightClick(@NotNull Player player, int slot) { public boolean rightClick(@NotNull Player player, int slot) {
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
final ItemStack cursor = getCursorItem(); final ItemStack cursor = getCursorItem();
final ItemStack clicked = getItemStack(slot, OFFSET); final ItemStack clicked = getItemStack(convertedSlot);
final InventoryClickResult clickResult = clickProcessor.rightClick(null, player, slot, clicked, cursor); final InventoryClickResult clickResult = clickProcessor.rightClick(null, player, convertedSlot, clicked, cursor);
if (clickResult.doRefresh()) if (clickResult.doRefresh())
sendSlotRefresh((short) slot, clicked); sendSlotRefresh((short) slot, clicked);
setItemStack(slot, OFFSET, clickResult.getClicked()); setItemStack(convertedSlot, clickResult.getClicked());
setCursorItem(clickResult.getCursor()); setCursorItem(clickResult.getCursor());
if (!clickResult.isCancel()) if (!clickResult.isCancel())
callClickEvent(player, null, slot, ClickType.RIGHT_CLICK, clicked, cursor); callClickEvent(player, null, convertedSlot, ClickType.RIGHT_CLICK, clicked, cursor);
return !clickResult.isCancel(); return !clickResult.isCancel();
} }

View File

@ -84,7 +84,16 @@ public class ItemTag<T> {
public static @NotNull ItemTag<NBT> NBT(@NotNull String key) { public static @NotNull ItemTag<NBT> NBT(@NotNull String key) {
return new ItemTag<>(key, return new ItemTag<>(key,
nbt -> nbt.get(key).deepClone(), nbt -> {
var currentNBT = nbt.get(key);
// Avoid a NPE when cloning a null variable.
if (currentNBT == null) {
return null;
}
return currentNBT.deepClone();
},
((nbt, value) -> nbt.set(key, value.deepClone()))); ((nbt, value) -> nbt.set(key, value.deepClone())));
} }

View File

@ -99,13 +99,15 @@ public class BlockPlacementListener {
blockPosition.add(offsetX, offsetY, offsetZ); blockPosition.add(offsetX, offsetY, offsetZ);
if (!canPlaceBlock) { if (!canPlaceBlock) {
//Send a block change with AIR as block to keep the client in sync, if (useMaterial.isBlock()) {
//using refreshChunk results in the client not being in sync //Send a block change with AIR as block to keep the client in sync,
//after rapid invalid block placements //using refreshChunk results in the client not being in sync
BlockChangePacket blockChangePacket = new BlockChangePacket(); //after rapid invalid block placements
blockChangePacket.blockPosition = blockPosition; BlockChangePacket blockChangePacket = new BlockChangePacket();
blockChangePacket.blockStateId = Block.AIR.getBlockId(); blockChangePacket.blockPosition = blockPosition;
player.getPlayerConnection().sendPacket(blockChangePacket); blockChangePacket.blockStateId = Block.AIR.getBlockId();
player.getPlayerConnection().sendPacket(blockChangePacket);
}
return; return;
} }

View File

@ -87,8 +87,7 @@ public class PlayerPositionListener {
player.teleport(newPosition); player.teleport(newPosition);
} }
// Change the internal data // Change the internal data
player.refreshPosition(newPosition.getX(), newPosition.getY(), newPosition.getZ()); player.refreshPosition(newPosition);
player.refreshView(newPosition.getYaw(), newPosition.getPitch());
player.refreshOnGround(onGround); player.refreshOnGround(onGround);
} else { } else {
player.teleport(player.getPosition()); player.teleport(player.getPosition());

View File

@ -23,10 +23,8 @@ public class PlayerVehicleListener {
if (vehicle == null) if (vehicle == null)
return; return;
final Position newPosition = new Position((float) packet.x, (float) packet.y, (float) packet.z); final Position newPosition = new Position((float) packet.x, (float) packet.y, (float) packet.z, packet.yaw, packet.pitch);
vehicle.refreshPosition(newPosition); vehicle.refreshPosition(newPosition);
vehicle.refreshView(packet.yaw, packet.pitch);
vehicle.askSynchronization();
// This packet causes weird screen distortion // This packet causes weird screen distortion
/*VehicleMovePacket vehicleMovePacket = new VehicleMovePacket(); /*VehicleMovePacket vehicleMovePacket = new VehicleMovePacket();

View File

@ -76,8 +76,8 @@ public final class BenchmarkManager {
stop = false; stop = false;
}, MinecraftServer.THREAD_NAME_BENCHMARK, 0L); }, MinecraftServer.THREAD_NAME_BENCHMARK);
thread.setDaemon(true);
thread.start(); thread.start();
this.enabled = true; this.enabled = true;

View File

@ -13,8 +13,6 @@ import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.traffic.GlobalChannelTrafficShapingHandler;
import io.netty.handler.traffic.TrafficCounter;
import io.netty.incubator.channel.uring.IOUring; import io.netty.incubator.channel.uring.IOUring;
import io.netty.incubator.channel.uring.IOUringEventLoopGroup; import io.netty.incubator.channel.uring.IOUringEventLoopGroup;
import io.netty.incubator.channel.uring.IOUringServerSocketChannel; import io.netty.incubator.channel.uring.IOUringServerSocketChannel;
@ -22,7 +20,6 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.network.PacketProcessor; import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.channel.ClientChannel; import net.minestom.server.network.netty.channel.ClientChannel;
import net.minestom.server.network.netty.codec.*; import net.minestom.server.network.netty.codec.*;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -30,22 +27,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public final class NettyServer { public final class NettyServer {
public static final Logger LOGGER = LoggerFactory.getLogger(NettyServer.class); public static final Logger LOGGER = LoggerFactory.getLogger(NettyServer.class);
public static final int BUFFER_SIZE = Integer.getInteger("minestom.channel-buffer-size", 65535);
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
1 << 21); 1 << 21);
private static final long DEFAULT_COMPRESSED_CHANNEL_WRITE_LIMIT = 600_000L;
private static final long DEFAULT_COMPRESSED_CHANNEL_READ_LIMIT = 100_000L;
private static final long DEFAULT_UNCOMPRESSED_CHANNEL_WRITE_LIMIT = 15_000_000L;
private static final long DEFAULT_UNCOMPRESSED_CHANNEL_READ_LIMIT = 1_000_000L;
public static final String TRAFFIC_LIMITER_HANDLER_NAME = "traffic-limiter"; // Read/write
public static final String LEGACY_PING_HANDLER_NAME = "legacy-ping"; // Read public static final String LEGACY_PING_HANDLER_NAME = "legacy-ping"; // Read
public static final String ENCRYPT_HANDLER_NAME = "encrypt"; // Write public static final String ENCRYPT_HANDLER_NAME = "encrypt"; // Write
@ -63,7 +53,6 @@ public final class NettyServer {
private boolean initialized = false; private boolean initialized = false;
private final PacketProcessor packetProcessor; private final PacketProcessor packetProcessor;
private final GlobalChannelTrafficShapingHandler globalTrafficHandler;
private EventLoopGroup boss, worker; private EventLoopGroup boss, worker;
private ServerBootstrap bootstrap; private ServerBootstrap bootstrap;
@ -73,27 +62,14 @@ public final class NettyServer {
private String address; private String address;
private int port; private int port;
/**
* Scheduler used by {@code globalTrafficHandler}.
*/
private final ScheduledExecutorService trafficScheduler = Executors.newScheduledThreadPool(1);
public NettyServer(@NotNull PacketProcessor packetProcessor) { public NettyServer(@NotNull PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor; this.packetProcessor = packetProcessor;
this.globalTrafficHandler = new GlobalChannelTrafficShapingHandler(trafficScheduler, 1000) {
@Override
protected void doAccounting(TrafficCounter counter) {
// TODO proper monitoring API
//System.out.println("data " + counter.getRealWriteThroughput() / 1e6);
}
};
} }
/** /**
* Inits the server by choosing which transport layer to use, number of threads, pipeline order, etc... * Inits the server by choosing which transport layer to use, number of threads, pipeline order, etc...
* <p> * <p>
* Called just before {@link #start(String, int)} in {@link MinecraftServer#start(String, int, ResponseDataConsumer)}. * Called just before {@link #start(String, int)}.
*/ */
public void init() { public void init() {
Check.stateCondition(initialized, "Netty server has already been initialized!"); Check.stateCondition(initialized, "Netty server has already been initialized!");
@ -146,14 +122,11 @@ public final class NettyServer {
ChannelConfig config = ch.config(); ChannelConfig config = ch.config();
config.setOption(ChannelOption.TCP_NODELAY, true); config.setOption(ChannelOption.TCP_NODELAY, true);
config.setOption(ChannelOption.SO_KEEPALIVE, true); config.setOption(ChannelOption.SO_KEEPALIVE, true);
config.setOption(ChannelOption.SO_SNDBUF, 262_144); config.setOption(ChannelOption.SO_SNDBUF, BUFFER_SIZE);
config.setAllocator(ByteBufAllocator.DEFAULT); config.setAllocator(ByteBufAllocator.DEFAULT);
ChannelPipeline pipeline = ch.pipeline(); ChannelPipeline pipeline = ch.pipeline();
// TODO enable when properly implemented (dynamic limit based on the number of clients)
//pipeline.addLast(TRAFFIC_LIMITER_HANDLER_NAME, globalTrafficHandler);
// First check should verify if the packet is a legacy ping (from 1.6 version and earlier) // First check should verify if the packet is a legacy ping (from 1.6 version and earlier)
// Removed from the pipeline later in LegacyPingHandler if unnecessary (>1.6) // Removed from the pipeline later in LegacyPingHandler if unnecessary (>1.6)
pipeline.addLast(LEGACY_PING_HANDLER_NAME, new LegacyPingHandler()); pipeline.addLast(LEGACY_PING_HANDLER_NAME, new LegacyPingHandler());
@ -185,18 +158,6 @@ public final class NettyServer {
this.address = address; this.address = address;
this.port = port; this.port = port;
// Setup traffic limiter
{
final boolean compression = MinecraftServer.getCompressionThreshold() != 0;
if (compression) {
this.globalTrafficHandler.setWriteChannelLimit(DEFAULT_COMPRESSED_CHANNEL_WRITE_LIMIT);
this.globalTrafficHandler.setReadChannelLimit(DEFAULT_COMPRESSED_CHANNEL_READ_LIMIT);
} else {
this.globalTrafficHandler.setWriteChannelLimit(DEFAULT_UNCOMPRESSED_CHANNEL_WRITE_LIMIT);
this.globalTrafficHandler.setReadChannelLimit(DEFAULT_UNCOMPRESSED_CHANNEL_READ_LIMIT);
}
}
// Bind address // Bind address
try { try {
ChannelFuture cf = bootstrap.bind(new InetSocketAddress(address, port)).sync(); ChannelFuture cf = bootstrap.bind(new InetSocketAddress(address, port)).sync();
@ -231,25 +192,15 @@ public final class NettyServer {
} }
/** /**
* Gets the traffic handler, used to control channel and global bandwidth. * Stops the server.
* <p>
* The object can be modified as specified by Netty documentation.
*
* @return the global traffic handler
*/
@NotNull
public GlobalChannelTrafficShapingHandler getGlobalTrafficHandler() {
return globalTrafficHandler;
}
/**
* Stops the server and the various services.
*/ */
public void stop() { public void stop() {
this.worker.shutdownGracefully(); try {
this.boss.shutdownGracefully(); this.boss.shutdownGracefully().sync();
this.worker.shutdownGracefully().sync();
this.trafficScheduler.shutdown(); this.serverChannel.closeFuture().sync();
this.globalTrafficHandler.release(); } catch (InterruptedException e) {
e.printStackTrace();
}
} }
} }

View File

@ -6,22 +6,23 @@ import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.ping.ServerListPingType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
// Copied from original minecraft :(
public class LegacyPingHandler extends ChannelInboundHandlerAdapter { public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf; private ByteBuf buf;
@Override @Override
public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object object) { public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object object) {
ByteBuf buf = (ByteBuf) object; final ByteBuf buf = (ByteBuf) object;
if (this.buf != null) { if (this.buf != null) {
try { try {
readLegacy1_6(ctx, buf); handle1_6(ctx, buf);
} finally { } finally {
buf.release(); buf.release();
} }
@ -38,19 +39,17 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
switch (length) { switch (length) {
case 0: case 0:
this.writeResponse(ctx, this.createResponse(formatResponse(-2))); if (trySendResponse(ServerListPingType.LEGACY_UNVERSIONED, ctx)) return;
break; break;
case 1: case 1:
if (buf.readUnsignedByte() != 1) { if (buf.readUnsignedByte() != 1) return;
return;
}
this.writeResponse(ctx, this.createResponse(formatResponse(-1))); if (trySendResponse(ServerListPingType.LEGACY_VERSIONED, ctx)) return;
break; break;
default: default:
if (buf.readUnsignedByte() != 0x01 || buf.readUnsignedByte() != 0xFA) return; if (buf.readUnsignedByte() != 0x01 || buf.readUnsignedByte() != 0xFA) return;
readLegacy1_6(ctx, buf); handle1_6(ctx, buf);
break; break;
} }
@ -66,19 +65,7 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
} }
} }
private static String readLegacyString(ByteBuf buf) { private void handle1_6(ChannelHandlerContext ctx, ByteBuf part) {
int size = buf.readShort() * Character.BYTES;
if (!buf.isReadable(size)) {
return null;
}
final String result = buf.toString(buf.readerIndex(), size, StandardCharsets.UTF_16BE);
buf.skipBytes(size);
return result;
}
private void readLegacy1_6(ChannelHandlerContext ctx, ByteBuf part) {
ByteBuf buf = this.buf; ByteBuf buf = this.buf;
if (buf == null) { if (buf == null) {
@ -127,27 +114,7 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
this.buf = null; this.buf = null;
this.writeResponse(ctx, this.createResponse(formatResponse(protocolVersion))); trySendResponse(ServerListPingType.LEGACY_VERSIONED, ctx);
}
private String formatResponse(int playerProtocol) {
final String motd = MinecraftServer.getBrandName();
final String version = MinecraftServer.VERSION_NAME;
final int online = MinecraftServer.getConnectionManager().getOnlinePlayers().size();
final int max = 0;
final int protocol = MinecraftServer.PROTOCOL_VERSION;
if (playerProtocol == -2) {
return String.format(
"%s\u00a7%d\u00a7%d",
motd, online, max
);
}
return String.format(
"\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d",
protocol, version, motd, online, max
);
} }
private void removeHandler(ChannelHandlerContext ctx) { private void removeHandler(ChannelHandlerContext ctx) {
@ -167,22 +134,51 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
} }
} }
private void writeResponse(ChannelHandlerContext ctx, ByteBuf buf) { /**
ctx.pipeline().firstContext().writeAndFlush(buf).addListener(ChannelFutureListener.CLOSE); * Calls a {@link ServerListPingEvent} and sends the response, if the event was not cancelled.
*
* @param version the version
* @param ctx the context
* @return {@code true} if the response was cancelled, {@code false} otherwise
*/
private static boolean trySendResponse(@NotNull ServerListPingType version, @NotNull ChannelHandlerContext ctx) {
final ServerListPingEvent event = new ServerListPingEvent(version);
MinecraftServer.getGlobalEventHandler().callEvent(ServerListPingEvent.class, event);
if (event.isCancelled()) {
return true;
} else {
// get the response string
String s = version.getPingResponse(event.getResponseData());
// create the buffer
ByteBuf response = Unpooled.buffer();
response.writeByte(255);
final char[] chars = s.toCharArray();
response.writeShort(chars.length);
for (char c : chars) {
response.writeChar(c);
}
// write the buffer
ctx.pipeline().firstContext().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
return false;
}
} }
private ByteBuf createResponse(String s) { private static String readLegacyString(ByteBuf buf) {
ByteBuf response = Unpooled.buffer(); int size = buf.readShort() * Character.BYTES;
response.writeByte(255); if (!buf.isReadable(size)) {
return null;
final char[] chars = s.toCharArray();
response.writeShort(chars.length);
for (char c : chars) {
response.writeChar(c);
} }
return response; final String result = buf.toString(buf.readerIndex(), size, StandardCharsets.UTF_16BE);
buf.skipBytes(size);
return result;
} }
} }

View File

@ -1,13 +1,11 @@
package net.minestom.server.network.packet.client.status; package net.minestom.server.network.packet.client.status;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.event.server.ServerListPingEvent; import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.network.packet.client.ClientPreplayPacket; import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.packet.server.handshake.ResponsePacket; import net.minestom.server.network.packet.server.handshake.ResponsePacket;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.ping.ResponseData; import net.minestom.server.ping.ServerListPingType;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.utils.binary.BinaryReader; import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -16,30 +14,14 @@ public class StatusRequestPacket implements ClientPreplayPacket {
@Override @Override
public void process(@NotNull PlayerConnection connection) { public void process(@NotNull PlayerConnection connection) {
ResponseDataConsumer consumer = MinecraftServer.getResponseDataConsumer(); final ServerListPingType pingVersion = ServerListPingType.fromModernProtocolVersion(connection.getProtocolVersion());
ResponseData responseData = new ResponseData(); final ServerListPingEvent statusRequestEvent = new ServerListPingEvent(connection, pingVersion);
MinecraftServer.getGlobalEventHandler().callCancellableEvent(ServerListPingEvent.class, statusRequestEvent, () -> {
// Fill default params final ResponsePacket responsePacket = new ResponsePacket();
responseData.setVersion(MinecraftServer.VERSION_NAME); responsePacket.jsonResponse = pingVersion.getPingResponse(statusRequestEvent.getResponseData());
responseData.setProtocol(MinecraftServer.PROTOCOL_VERSION);
responseData.setMaxPlayer(0);
responseData.setOnline(0);
responseData.setDescription(Component.text("Minestom Server"));
responseData.setFavicon("");
if (consumer != null)
consumer.accept(connection, responseData);
// Call event
ServerListPingEvent statusRequestEvent = new ServerListPingEvent(responseData, connection);
MinecraftServer.getGlobalEventHandler().callCancellableEvent(ServerListPingEvent.class, statusRequestEvent,
() -> {
ResponsePacket responsePacket = new ResponsePacket();
responsePacket.jsonResponse = responseData.build().toString();
connection.sendPacket(responsePacket);
});
connection.sendPacket(responsePacket);
});
} }
@Override @Override

View File

@ -1,6 +1,7 @@
package net.minestom.server.network.packet.server.play; package net.minestom.server.network.packet.server.play;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
@ -13,7 +14,6 @@ import net.minestom.server.instance.palette.Section;
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.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.BufUtils;
import net.minestom.server.utils.Utils; import net.minestom.server.utils.Utils;
import net.minestom.server.utils.binary.BinaryReader; import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.BinaryWriter;
@ -82,7 +82,7 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
writer.writeBoolean(fullChunk); writer.writeBoolean(fullChunk);
int mask = 0; int mask = 0;
ByteBuf blocks = BufUtils.getBuffer(MAX_BUFFER_SIZE); ByteBuf blocks = Unpooled.buffer(MAX_BUFFER_SIZE);
for (byte i = 0; i < CHUNK_SECTION_COUNT; i++) { for (byte i = 0; i < CHUNK_SECTION_COUNT; i++) {
if (fullChunk || (sections.length == CHUNK_SECTION_COUNT && sections[i] != 0)) { if (fullChunk || (sections.length == CHUNK_SECTION_COUNT && sections[i] != 0)) {
final Section section = paletteStorage.getSections()[i]; final Section section = paletteStorage.getSections()[i];

View File

@ -62,7 +62,7 @@ public class NettyPlayerConnection extends PlayerConnection {
private PlayerSkin bungeeSkin; private PlayerSkin bungeeSkin;
private final Object tickBufferLock = new Object(); private final Object tickBufferLock = new Object();
private volatile ByteBuf tickBuffer = BufUtils.getBuffer(true); private volatile ByteBuf tickBuffer = BufUtils.direct();
public NettyPlayerConnection(@NotNull SocketChannel channel) { public NettyPlayerConnection(@NotNull SocketChannel channel) {
super(); super();

View File

@ -1,44 +1,53 @@
package net.minestom.server.ping; package net.minestom.server.ping;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.utils.identity.NamedAndIdentified;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
import java.util.UUID;
/** /**
* Represents the data sent to the player when refreshing the server list. * Represents the data sent to the player when responding to a ping event.
* *
* <p>Edited by listening to the {@link net.minestom.server.event.server.ServerListPingEvent}. * @see ServerListPingEvent
*/ */
public class ResponseData { public class ResponseData {
private final List<PingPlayer> pingPlayers; private static final Component DEFAULT_DESCRIPTION = Component.text("Minestom Server");
private final List<NamedAndIdentified> entries;
private String version; private String version;
private int protocol; private int protocol;
private int maxPlayer; private int maxPlayer;
private int online; private int online;
private Component description; private Component description;
private String favicon; private String favicon;
/** /**
* Constructs a new {@link ResponseData}. * Constructs a new {@link ResponseData}.
*/ */
public ResponseData() { public ResponseData() {
this.pingPlayers = new ArrayList<>(); this.entries = new ArrayList<>();
this.version = MinecraftServer.VERSION_NAME;
this.protocol = MinecraftServer.PROTOCOL_VERSION;
this.online = MinecraftServer.getConnectionManager().getOnlinePlayers().size();
this.maxPlayer = this.online + 1;
this.description = DEFAULT_DESCRIPTION;
this.favicon = "";
} }
/** /**
* Sets the name for the response. * Sets the name for the response.
* *
* @param name The name for the response data. * @param name The name for the response data.
* @deprecated Use {@link #setVersion(String)} * @deprecated This is named incorrectly, use {@link #setVersion(String)} instead
*/ */
@Deprecated @Deprecated
public void setName(String name) { public void setName(String name) {
@ -121,7 +130,9 @@ public class ResponseData {
* Adds some players to the response. * Adds some players to the response.
* *
* @param players the players * @param players the players
* @deprecated Use {@link #addEntries(Collection)}}
*/ */
@Deprecated
public void addPlayer(Iterable<Player> players) { public void addPlayer(Iterable<Player> players) {
for (Player player : players) { for (Player player : players) {
this.addPlayer(player); this.addPlayer(player);
@ -132,9 +143,11 @@ public class ResponseData {
* Adds a player to the response. * Adds a player to the response.
* *
* @param player the player * @param player the player
* @deprecated Use {@link #addEntry(NamedAndIdentified)}
*/ */
@Deprecated
public void addPlayer(Player player) { public void addPlayer(Player player) {
this.addPlayer(player.getUsername(), player.getUuid()); this.addEntry(player);
} }
/** /**
@ -142,10 +155,11 @@ public class ResponseData {
* *
* @param name The name of the player. * @param name The name of the player.
* @param uuid The unique identifier of the player. * @param uuid The unique identifier of the player.
* @deprecated Use {@link #addEntry(NamedAndIdentified)} with {@link NamedAndIdentified#of(String, UUID)}
*/ */
@Deprecated
public void addPlayer(String name, UUID uuid) { public void addPlayer(String name, UUID uuid) {
PingPlayer pingPlayer = PingPlayer.of(name, uuid); this.addEntry(NamedAndIdentified.of(name, uuid));
this.pingPlayers.add(pingPlayer);
} }
/** /**
@ -154,27 +168,35 @@ public class ResponseData {
* {@link UUID#randomUUID()} is used as the player's UUID. * {@link UUID#randomUUID()} is used as the player's UUID.
* *
* @param name The name of the player. * @param name The name of the player.
* @deprecated Use {@link #addEntry(NamedAndIdentified)} with {@link NamedAndIdentified#named(String)}
*/ */
@Deprecated
public void addPlayer(String name) { public void addPlayer(String name) {
PingPlayer pingPlayer = PingPlayer.of(name, UUID.randomUUID()); this.addEntry(NamedAndIdentified.named(name));
this.pingPlayers.add(pingPlayer);
} }
/** /**
* Removes all of the ping players from this {@link #pingPlayers}. The {@link #pingPlayers} list * Removes all of the ping players from this {@link #entries}. The {@link #entries} list
* will be empty this call returns. * will be empty this call returns.
*
* @deprecated Use {@link #clearEntries()}
*/ */
@Deprecated
public void clearPlayers() { public void clearPlayers() {
this.pingPlayers.clear(); this.clearEntries();
} }
/** /**
* Get the list of the response players. * Get the list of the response players.
* *
* @return the list of the response players. * @return the list of the response players.
* @deprecated Use {@link #getEntries()}. This return value is now unmodifiable and this operation is incredibly costly.
*/ */
@Deprecated(forRemoval = true) // to throw an error for people using it - this method is *horrible*
public List<PingPlayer> getPlayers() { public List<PingPlayer> getPlayers() {
return pingPlayers; return this.entries.stream()
.map(entry -> PingPlayer.of(PlainComponentSerializer.plain().serialize(entry.getName()), entry.getUuid()))
.collect(Collectors.toUnmodifiableList());
} }
/** /**
@ -226,48 +248,68 @@ public class ResponseData {
return favicon; return favicon;
} }
/**
* Adds an entry to the response data sample. This can be a player or a custom object.
*
* @param entry the entry
* @see NamedAndIdentified
*/
public void addEntry(@NotNull NamedAndIdentified entry) {
this.entries.add(entry);
}
/**
* Adds a series of entries to the response data sample. These can be players or a custom object.
*
* @param entries the entries
* @see NamedAndIdentified
*/
public void addEntries(@NotNull NamedAndIdentified... entries) {
this.addEntries(Arrays.asList(entries));
}
/**
* Adds a series of entries to the response data sample. These can be players or a custom object.
*
* @param entries the entries
* @see NamedAndIdentified
*/
public void addEntries(@NotNull Collection<? extends NamedAndIdentified> entries) {
this.entries.addAll(entries);
}
/**
* Clears the entries.
*/
public void clearEntries() {
this.entries.clear();
}
/**
* Gets a modifiable collection of the current entries.
*
* @return the entries
*/
public @NotNull Collection<NamedAndIdentified> getEntries() {
return this.entries;
}
/** /**
* Converts the response data into a {@link JsonObject}. * Converts the response data into a {@link JsonObject}.
* *
* @return The converted response data as a json tree. * @return The converted response data as a json tree.
* @deprecated Use {@link ServerListPingType#getPingResponse(ResponseData)}
*/ */
@NotNull @Deprecated
public JsonObject build() { public @NotNull JsonObject build() {
// version return ServerListPingType.getModernPingResponse(this, true);
final JsonObject versionObject = new JsonObject();
versionObject.addProperty("name", this.version);
versionObject.addProperty("protocol", this.protocol);
// players info
final JsonObject playersObject = new JsonObject();
playersObject.addProperty("max", this.maxPlayer);
playersObject.addProperty("online", this.online);
// individual players
final JsonArray sampleArray = new JsonArray();
for (PingPlayer pingPlayer : this.pingPlayers) {
JsonObject pingPlayerObject = new JsonObject();
pingPlayerObject.addProperty("name", pingPlayer.name);
pingPlayerObject.addProperty("id", pingPlayer.uuid.toString());
sampleArray.add(pingPlayerObject);
}
playersObject.add("sample", sampleArray);
final JsonObject descriptionObject = GsonComponentSerializer.gson().serializer()
.toJsonTree(this.description).getAsJsonObject();
final JsonObject jsonObject = new JsonObject();
jsonObject.add("version", versionObject);
jsonObject.add("players", playersObject);
jsonObject.add("description", descriptionObject);
jsonObject.addProperty("favicon", this.favicon);
return jsonObject;
} }
/** /**
* Represents a player line in the server list hover. * Represents a player line in the server list hover.
* @deprecated See {@link NamedAndIdentified}
*/ */
@Deprecated
public static class PingPlayer { public static class PingPlayer {
private static @NotNull PingPlayer of(@NotNull String name, @NotNull UUID uuid) { private static @NotNull PingPlayer of(@NotNull String name, @NotNull UUID uuid) {

View File

@ -0,0 +1,157 @@
package net.minestom.server.ping;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.extras.lan.OpenToLAN;
import net.minestom.server.utils.identity.NamedAndIdentified;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
/**
* An enum containing the different types of server list ping responses.
*
* @see <a href="https://wiki.vg/Server_List_Ping">https://wiki.vg/Server_List_Ping</a>
* @see ServerListPingEvent
*/
public enum ServerListPingType {
/**
* The client is on version 1.16 or higher and supports full RGB with JSON text formatting.
*/
MODERN_FULL_RGB(data -> getModernPingResponse(data, true).toString()),
/**
* The client is on version 1.7 or higher and doesn't support full RGB but does support JSON text formatting.
*/
MODERN_NAMED_COLORS(data -> getModernPingResponse(data, false).toString()),
/**
* The client is on version 1.6 and supports a description, the player count and the version information.
*/
LEGACY_VERSIONED(data -> getLegacyPingResponse(data, true)),
/**
* The client is on version 1.5 or lower and supports a description and the player count.
*/
LEGACY_UNVERSIONED(data -> getLegacyPingResponse(data, false)),
/**
* The ping that is sent when {@link OpenToLAN} is enabled and sending packets.
* Only the description formatted as a legacy string is sent.
* Ping events with this ping version are <b>not</b> cancellable.
*/
OPEN_TO_LAN(ServerListPingType::getOpenToLANPing);
private final Function<ResponseData, String> pingResponseCreator;
ServerListPingType(@NotNull Function<ResponseData, String> pingResponseCreator) {
this.pingResponseCreator = pingResponseCreator;
}
/**
* Gets the ping response for this version.
*
* @param responseData the response data
* @return the response
*/
public @NotNull String getPingResponse(@NotNull ResponseData responseData) {
return this.pingResponseCreator.apply(responseData);
}
private static final String LAN_PING_FORMAT = "[MOTD]%s[/MOTD][AD]%s[/AD]";
private static final GsonComponentSerializer FULL_RGB = GsonComponentSerializer.gson(),
NAMED_RGB = GsonComponentSerializer.colorDownsamplingGson();
private static final LegacyComponentSerializer SECTION = LegacyComponentSerializer.legacySection();
/**
* Creates a ping sent when the server is sending {@link OpenToLAN} packets.
*
* @param data the response data
* @return the ping
* @see OpenToLAN
*/
public static @NotNull String getOpenToLANPing(@NotNull ResponseData data) {
return String.format(LAN_PING_FORMAT, SECTION.serialize(data.getDescription()), MinecraftServer.getNettyServer().getPort());
}
/**
* Creates a legacy ping response for client versions below the Netty rewrite (1.6-).
*
* @param data the response data
* @param supportsVersions if the client supports recieving the versions of the server
* @return the response
*/
public static @NotNull String getLegacyPingResponse(@NotNull ResponseData data, boolean supportsVersions) {
final String motd = SECTION.serialize(data.getDescription());
if (supportsVersions) {
return String.format("\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d",
data.getProtocol(), data.getVersion(), motd, data.getOnline(), data.getMaxPlayer());
} else {
return String.format("%s\u00a7%d\u00a7%d", motd, data.getOnline(), data.getMaxPlayer());
}
}
/**
* Creates a modern ping response for client versions above the Netty rewrite (1.7+).
*
* @param data the response data
* @param supportsFullRgb if the client supports full RGB
* @return the response
*/
public static @NotNull JsonObject getModernPingResponse(@NotNull ResponseData data, boolean supportsFullRgb) {
// version
final JsonObject versionObject = new JsonObject();
versionObject.addProperty("name", data.getVersion());
versionObject.addProperty("protocol", data.getProtocol());
// players info
final JsonObject playersObject = new JsonObject();
playersObject.addProperty("max", data.getMaxPlayer());
playersObject.addProperty("online", data.getOnline());
// individual players
final JsonArray sampleArray = new JsonArray();
for (NamedAndIdentified entry : data.getEntries()) {
JsonObject playerObject = new JsonObject();
playerObject.addProperty("name", SECTION.serialize(entry.getName()));
playerObject.addProperty("id", entry.getUuid().toString());
sampleArray.add(playerObject);
}
playersObject.add("sample", sampleArray);
final JsonObject jsonObject = new JsonObject();
jsonObject.add("version", versionObject);
jsonObject.add("players", playersObject);
jsonObject.addProperty("favicon", data.getFavicon());
// description
if (supportsFullRgb) {
jsonObject.add("description", FULL_RGB.serializeToTree(data.getDescription()));
} else {
jsonObject.add("description", NAMED_RGB.serializeToTree(data.getDescription()));
}
return jsonObject;
}
/**
* Gets the server list ping version from the protocol version.
* This only works for modern ping responses since the Netty rewrite.
*
* @param version the protocol version
* @return the corresponding server list ping version
*/
public static @NotNull ServerListPingType fromModernProtocolVersion(int version) {
if (version >= 713) {
return MODERN_FULL_RGB;
} else {
return MODERN_NAMED_COLORS;
}
}
}

View File

@ -19,6 +19,8 @@ import java.nio.file.Paths;
*/ */
public class FileStorageSystem implements StorageSystem { public class FileStorageSystem implements StorageSystem {
private Options options;
static { static {
RocksDB.loadLibrary(); RocksDB.loadLibrary();
} }
@ -32,11 +34,11 @@ public class FileStorageSystem implements StorageSystem {
@Override @Override
public void open(@NotNull String location, @NotNull StorageOptions storageOptions) { public void open(@NotNull String location, @NotNull StorageOptions storageOptions) {
Options options = new Options().setCreateIfMissing(true); options = new Options().setCreateIfMissing(true);
if (storageOptions.hasCompression()) { if (storageOptions.hasCompression()) {
options.setCompressionType(CompressionType.ZSTD_COMPRESSION); options.setCompressionType(CompressionType.ZSTD_COMPRESSION);
options.setCompressionOptions(new CompressionOptions().setLevel(1)); options.setCompressionOptions(new CompressionOptions().setLevel(4));
} }
try { try {
@ -77,6 +79,9 @@ public class FileStorageSystem implements StorageSystem {
@Override @Override
public void close() { public void close() {
try { try {
if (options != null)
this.options.close();
this.rocksDB.closeE(); this.rocksDB.closeE();
} catch (RocksDBException e) { } catch (RocksDBException e) {
MinecraftServer.getExceptionManager().handleException(e); MinecraftServer.getExceptionManager().handleException(e);

View File

@ -22,26 +22,31 @@ public class MinestomTerminal {
@ApiStatus.Internal @ApiStatus.Internal
public static void start() { public static void start() {
try { final Thread thread = new Thread(null, () -> {
terminal = TerminalBuilder.terminal();
} catch (IOException e) {
e.printStackTrace();
}
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.build();
running = true;
while (running) {
String command;
try { try {
command = reader.readLine(PROMPT); terminal = TerminalBuilder.terminal();
COMMAND_MANAGER.execute(COMMAND_MANAGER.getConsoleSender(), command); } catch (IOException e) {
} catch (UserInterruptException e) { e.printStackTrace();
// Ignore
} catch (EndOfFileException e) {
return;
} }
} LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.build();
running = true;
while (running) {
String command;
try {
command = reader.readLine(PROMPT);
COMMAND_MANAGER.execute(COMMAND_MANAGER.getConsoleSender(), command);
} catch (UserInterruptException e) {
// Ignore
} catch (EndOfFileException e) {
return;
}
}
}, "Jline");
thread.setDaemon(true);
thread.start();
} }
@ApiStatus.Internal @ApiStatus.Internal

View File

@ -4,8 +4,6 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.LockSupport;
@ -50,10 +48,7 @@ public class TickThread extends Thread {
private volatile boolean stop; private volatile boolean stop;
private TickThread tickThread; private TickThread tickThread;
private volatile boolean inTick; private final AtomicReference<TickContext> tickContext = new AtomicReference<>();
private final AtomicReference<CountDownLatch> countDownLatch = new AtomicReference<>();
private final Queue<Runnable> queue = new ConcurrentLinkedQueue<>();
@Override @Override
public void run() { public void run() {
@ -62,42 +57,38 @@ public class TickThread extends Thread {
LockSupport.park(tickThread); LockSupport.park(tickThread);
if (stop) if (stop)
break; break;
CountDownLatch localCountDownLatch = this.countDownLatch.get(); TickContext localContext = this.tickContext.get();
// The context is necessary to control the tick rates
// The latch is necessary to control the tick rates if (localContext == null) {
if (localCountDownLatch == null) {
continue; continue;
} }
this.inTick = true; // Execute tick
localContext.runnable.run();
// Execute all pending runnable localContext.countDownLatch.countDown();
Runnable runnable; this.tickContext.compareAndSet(localContext, null);
while ((runnable = queue.poll()) != null) {
runnable.run();
}
localCountDownLatch.countDown();
this.countDownLatch.compareAndSet(localCountDownLatch, null);
// Wait for the next notify (game tick)
this.inTick = false;
} }
} }
public void startTick(@NotNull CountDownLatch countDownLatch, @NotNull Runnable runnable) { protected void startTick(@NotNull CountDownLatch countDownLatch, @NotNull Runnable runnable) {
this.countDownLatch.set(countDownLatch); this.tickContext.set(new TickContext(countDownLatch, runnable));
this.queue.add(runnable);
LockSupport.unpark(tickThread); LockSupport.unpark(tickThread);
} }
public boolean isInTick() {
return inTick;
}
private void setLinkedThread(TickThread tickThread) { private void setLinkedThread(TickThread tickThread) {
this.tickThread = tickThread; this.tickThread = tickThread;
} }
} }
private static class TickContext {
private final CountDownLatch countDownLatch;
private final Runnable runnable;
private TickContext(@NotNull CountDownLatch countDownLatch, @NotNull Runnable runnable) {
this.countDownLatch = countDownLatch;
this.runnable = runnable;
}
}
} }

View File

@ -7,28 +7,7 @@ public class BufUtils {
private static final PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; private static final PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
public static ByteBuf getBuffer() { public static ByteBuf direct() {
return alloc.heapBuffer(); return alloc.ioBuffer();
} }
public static ByteBuf getBuffer(boolean io) {
return io ? alloc.ioBuffer() : alloc.heapBuffer();
}
public static ByteBuf getBuffer(int initialCapacity) {
return alloc.heapBuffer(initialCapacity);
}
public static ByteBuf getBuffer(boolean io, int initialCapacity) {
return io ? alloc.ioBuffer(initialCapacity) : alloc.heapBuffer(initialCapacity);
}
public static ByteBuf getBuffer(int initialCapacity, int maxCapacity) {
return alloc.heapBuffer(initialCapacity, maxCapacity);
}
public static ByteBuf getBuffer(boolean io, int initialCapacity, int maxCapacity) {
return io ? alloc.ioBuffer(initialCapacity, maxCapacity) : alloc.heapBuffer(initialCapacity, maxCapacity);
}
} }

View File

@ -0,0 +1,29 @@
package net.minestom.server.utils;
import java.io.IOException;
import java.net.ServerSocket;
/**
* Network related utilities.
*/
public class NetworkUtils {
private NetworkUtils() { }
/**
* Gets a free port.
*
* @return the port
* @throws IOException if a port could not be found
*/
public static int getFreePort() throws IOException {
int port;
final ServerSocket socket = new ServerSocket(0);
port = socket.getLocalPort();
socket.close();
return port;
}
}

View File

@ -3,7 +3,6 @@ package net.minestom.server.utils;
import com.velocitypowered.natives.compression.VelocityCompressor; import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.util.Natives; import com.velocitypowered.natives.util.Natives;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.audience.ForwardingAudience;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
@ -21,7 +20,6 @@ import net.minestom.server.utils.callback.validator.PlayerValidator;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.Collection; import java.util.Collection;
import java.util.zip.DataFormatException; import java.util.zip.DataFormatException;
@ -35,7 +33,6 @@ public final class PacketUtils {
private static final ThreadLocal<VelocityCompressor> COMPRESSOR = ThreadLocal.withInitial(() -> Natives.compress.get().create(4)); private static final ThreadLocal<VelocityCompressor> COMPRESSOR = ThreadLocal.withInitial(() -> Natives.compress.get().create(4));
private PacketUtils() { private PacketUtils() {
} }
/** /**
@ -54,7 +51,7 @@ public final class PacketUtils {
* </ol> * </ol>
* *
* @param audience the audience * @param audience the audience
* @param packet the packet * @param packet the packet
*/ */
@SuppressWarnings("OverrideOnly") // we need to access the audiences inside ForwardingAudience @SuppressWarnings("OverrideOnly") // we need to access the audiences inside ForwardingAudience
public static void sendPacket(@NotNull Audience audience, @NotNull ServerPacket packet) { public static void sendPacket(@NotNull Audience audience, @NotNull ServerPacket packet) {
@ -87,7 +84,6 @@ public final class PacketUtils {
// work out if the packet needs to be sent individually due to server-side translating // work out if the packet needs to be sent individually due to server-side translating
boolean needsTranslating = false; boolean needsTranslating = false;
if (AdventureSerializer.AUTOMATIC_COMPONENT_TRANSLATION && packet instanceof ComponentHoldingServerPacket) { if (AdventureSerializer.AUTOMATIC_COMPONENT_TRANSLATION && packet instanceof ComponentHoldingServerPacket) {
needsTranslating = AdventureSerializer.areAnyTranslatable(((ComponentHoldingServerPacket) packet).components()); needsTranslating = AdventureSerializer.areAnyTranslatable(((ComponentHoldingServerPacket) packet).components());
} }
@ -96,21 +92,17 @@ public final class PacketUtils {
// Send grouped packet... // Send grouped packet...
final boolean success = PACKET_LISTENER_MANAGER.processServerPacket(packet, players); final boolean success = PACKET_LISTENER_MANAGER.processServerPacket(packet, players);
if (success) { if (success) {
final ByteBuf finalBuffer = createFramedPacket(packet, true); final ByteBuf finalBuffer = createFramedPacket(packet);
final FramedPacket framedPacket = new FramedPacket(finalBuffer); final FramedPacket framedPacket = new FramedPacket(finalBuffer);
// Send packet to all players // Send packet to all players
for (Player player : players) { for (Player player : players) {
if (!player.isOnline()) if (!player.isOnline())
continue; continue;
// Verify if the player should receive the packet // Verify if the player should receive the packet
if (playerValidator != null && !playerValidator.isValid(player)) if (playerValidator != null && !playerValidator.isValid(player))
continue; continue;
finalBuffer.retain();
final PlayerConnection playerConnection = player.getPlayerConnection(); final PlayerConnection playerConnection = player.getPlayerConnection();
if (playerConnection instanceof NettyPlayerConnection) { if (playerConnection instanceof NettyPlayerConnection) {
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) playerConnection; final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) playerConnection;
@ -118,15 +110,12 @@ public final class PacketUtils {
} else { } else {
playerConnection.sendPacket(packet); playerConnection.sendPacket(packet);
} }
finalBuffer.release();
} }
finalBuffer.release(); // Release last reference finalBuffer.release(); // Release last reference
} }
} else { } else {
// Write the same packet for each individual players // Write the same packet for each individual players
for (Player player : players) { for (Player player : players) {
// Verify if the player should receive the packet // Verify if the player should receive the packet
if (playerValidator != null && !playerValidator.isValid(player)) if (playerValidator != null && !playerValidator.isValid(player))
continue; continue;
@ -154,7 +143,7 @@ public final class PacketUtils {
* @param packet the packet to write into {@code buf} * @param packet the packet to write into {@code buf}
*/ */
public static void writePacket(@NotNull ByteBuf buf, @NotNull ServerPacket packet) { public static void writePacket(@NotNull ByteBuf buf, @NotNull ServerPacket packet) {
Utils.writeVarIntBuf(buf, packet.getId()); Utils.writeVarInt(buf, packet.getId());
writePacketPayload(buf, packet); writePacketPayload(buf, packet);
} }
@ -184,14 +173,13 @@ public final class PacketUtils {
public static void frameBuffer(@NotNull ByteBuf packetBuffer, @NotNull ByteBuf frameTarget) { public static void frameBuffer(@NotNull ByteBuf packetBuffer, @NotNull ByteBuf frameTarget) {
final int packetSize = packetBuffer.readableBytes(); final int packetSize = packetBuffer.readableBytes();
final int headerSize = Utils.getVarIntSize(packetSize); final int headerSize = Utils.getVarIntSize(packetSize);
if (headerSize > 3) { if (headerSize > 3) {
throw new IllegalStateException("Unable to fit " + headerSize + " into 3"); throw new IllegalStateException("Unable to fit " + headerSize + " into 3");
} }
frameTarget.ensureWritable(packetSize + headerSize); frameTarget.ensureWritable(packetSize + headerSize);
Utils.writeVarIntBuf(frameTarget, packetSize); Utils.writeVarInt(frameTarget, packetSize);
frameTarget.writeBytes(packetBuffer, packetBuffer.readerIndex(), packetSize); frameTarget.writeBytes(packetBuffer, packetBuffer.readerIndex(), packetSize);
} }
@ -207,7 +195,7 @@ public final class PacketUtils {
public static void compressBuffer(@NotNull VelocityCompressor compressor, @NotNull ByteBuf packetBuffer, @NotNull ByteBuf compressionTarget) { public static void compressBuffer(@NotNull VelocityCompressor compressor, @NotNull ByteBuf packetBuffer, @NotNull ByteBuf compressionTarget) {
final int packetLength = packetBuffer.readableBytes(); final int packetLength = packetBuffer.readableBytes();
final boolean compression = packetLength > MinecraftServer.getCompressionThreshold(); final boolean compression = packetLength > MinecraftServer.getCompressionThreshold();
Utils.writeVarIntBuf(compressionTarget, compression ? packetLength : 0); Utils.writeVarInt(compressionTarget, compression ? packetLength : 0);
if (compression) { if (compression) {
compress(compressor, packetBuffer, compressionTarget); compress(compressor, packetBuffer, compressionTarget);
} else { } else {
@ -226,54 +214,35 @@ public final class PacketUtils {
public static void writeFramedPacket(@NotNull ByteBuf buffer, public static void writeFramedPacket(@NotNull ByteBuf buffer,
@NotNull ServerPacket serverPacket) { @NotNull ServerPacket serverPacket) {
final int compressionThreshold = MinecraftServer.getCompressionThreshold(); final int compressionThreshold = MinecraftServer.getCompressionThreshold();
final boolean compression = compressionThreshold > 0;
if (compression) { // Index of the var-int containing the complete packet length
// Dummy var-int final int packetLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
final int packetLengthIndex = Utils.writeEmptyVarIntHeader(buffer); final int startIndex = buffer.writerIndex(); // Index where the content starts (after length)
final int dataLengthIndex = Utils.writeEmptyVarIntHeader(buffer); if (compressionThreshold > 0) {
// Index of the uncompressed payload length
final int dataLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
// Write packet // Write packet
final int contentIndex = buffer.writerIndex(); final int contentIndex = buffer.writerIndex();
writePacket(buffer, serverPacket); writePacket(buffer, serverPacket);
final int afterIndex = buffer.writerIndex(); final int packetSize = buffer.writerIndex() - contentIndex;
final int packetSize = (afterIndex - dataLengthIndex) - Utils.VARINT_HEADER_SIZE;
if (packetSize >= compressionThreshold) { final int uncompressedLength = packetSize >= compressionThreshold ? packetSize : 0;
// Packet large enough Utils.write3BytesVarInt(buffer, dataLengthIndex, uncompressedLength);
if (uncompressedLength > 0) {
final VelocityCompressor compressor = COMPRESSOR.get(); // Packet large enough, compress
// Compress id + payload
ByteBuf uncompressedCopy = buffer.copy(contentIndex, packetSize); ByteBuf uncompressedCopy = buffer.copy(contentIndex, packetSize);
buffer.writerIndex(contentIndex); buffer.writerIndex(contentIndex);
compress(compressor, uncompressedCopy, buffer); compress(COMPRESSOR.get(), uncompressedCopy, buffer);
uncompressedCopy.release(); uncompressedCopy.release();
final int totalPacketLength = buffer.writerIndex() - contentIndex + Utils.VARINT_HEADER_SIZE;
// Update header values
Utils.overrideVarIntHeader(buffer, packetLengthIndex, totalPacketLength);
Utils.overrideVarIntHeader(buffer, dataLengthIndex, packetSize);
} else {
// Packet too small, just override header values
final int totalPacketLength = packetSize + Utils.VARINT_HEADER_SIZE;
Utils.overrideVarIntHeader(buffer, packetLengthIndex, totalPacketLength);
Utils.overrideVarIntHeader(buffer, dataLengthIndex, 0); // -> Uncompressed
} }
} else { } else {
// No compression // No compression, write packet id + payload
// Write dummy var-int
final int index = Utils.writeEmptyVarIntHeader(buffer);
// Write packet id + payload
writePacket(buffer, serverPacket); writePacket(buffer, serverPacket);
// Rewrite dummy var-int to packet length
final int afterIndex = buffer.writerIndex();
final int packetSize = (afterIndex - index) - Utils.VARINT_HEADER_SIZE;
Utils.overrideVarIntHeader(buffer, index, packetSize);
} }
// Total length
final int totalPacketLength = buffer.writerIndex() - startIndex;
Utils.write3BytesVarInt(buffer, packetLengthIndex, totalPacketLength);
} }
/** /**
@ -282,14 +251,10 @@ public final class PacketUtils {
* <p> * <p>
* Can be used if you want to store a raw buffer and send it later without the additional writing cost. * Can be used if you want to store a raw buffer and send it later without the additional writing cost.
* Compression is applied if {@link MinecraftServer#getCompressionThreshold()} is greater than 0. * Compression is applied if {@link MinecraftServer#getCompressionThreshold()} is greater than 0.
*
* @param serverPacket the server packet to write
*/ */
@NotNull public static @NotNull ByteBuf createFramedPacket(@NotNull ServerPacket serverPacket) {
public static ByteBuf createFramedPacket(@NotNull ServerPacket serverPacket, boolean directBuffer) { ByteBuf packetBuf = BufUtils.direct();
ByteBuf packetBuf = directBuffer ? BufUtils.getBuffer(true) : Unpooled.buffer();
writeFramedPacket(packetBuf, serverPacket); writeFramedPacket(packetBuf, serverPacket);
return packetBuf; return packetBuf;
} }
} }

View File

@ -10,11 +10,7 @@ import java.util.UUID;
public final class Utils { public final class Utils {
// Do NOT modify
public static final int VARINT_HEADER_SIZE = 3;
private Utils() { private Utils() {
} }
public static int getVarIntSize(int input) { public static int getVarIntSize(int input) {
@ -25,70 +21,52 @@ public final class Utils {
? 4 : 5; ? 4 : 5;
} }
public static void writeVarIntBuf(ByteBuf buffer, int value) { public static void writeVarInt(@NotNull ByteBuf buf, int value) {
do { // Took from velocity
byte temp = (byte) (value & 0b01111111); if ((value & (0xFFFFFFFF << 7)) == 0) {
value >>>= 7; buf.writeByte(value);
if (value != 0) { } else if ((value & (0xFFFFFFFF << 14)) == 0) {
temp |= 0b10000000; int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
} buf.writeShort(w);
buffer.writeByte(temp); } else if ((value & (0xFFFFFFFF << 21)) == 0) {
} while (value != 0); int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
buf.writeMedium(w);
} else {
int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
| ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80);
buf.writeInt(w);
buf.writeByte(value >>> 28);
}
} }
public static void writeVarInt(BinaryWriter writer, int value) { public static void write3BytesVarInt(@NotNull ByteBuf buffer, int startIndex, int value) {
do {
byte temp = (byte) (value & 0b01111111);
value >>>= 7;
if (value != 0) {
temp |= 0b10000000;
}
writer.writeByte(temp);
} while (value != 0);
}
public static void overrideVarIntHeader(@NotNull ByteBuf buffer, int startIndex, int value) {
final int indexCache = buffer.writerIndex(); final int indexCache = buffer.writerIndex();
buffer.writerIndex(startIndex); buffer.writerIndex(startIndex);
final int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
for (int i = 0; i < VARINT_HEADER_SIZE; i++) { buffer.writeMedium(w);
byte temp = (byte) (value & 0b01111111);
value >>>= 7;
if (value != 0 || i != VARINT_HEADER_SIZE - 1) {
temp |= 0b10000000;
}
buffer.writeByte(temp);
}
buffer.writerIndex(indexCache); buffer.writerIndex(indexCache);
} }
public static int writeEmptyVarIntHeader(@NotNull ByteBuf buffer) { public static int writeEmpty3BytesVarInt(@NotNull ByteBuf buffer) {
final int index = buffer.writerIndex(); final int index = buffer.writerIndex();
buffer.writeMedium(0); buffer.writeMedium(0);
return index; return index;
} }
public static int readVarInt(ByteBuf buffer) { public static int readVarInt(ByteBuf buf) {
int numRead = 0; int i = 0;
int result = 0; final int maxRead = Math.min(5, buf.readableBytes());
byte read; for (int j = 0; j < maxRead; j++) {
do { final int k = buf.readByte();
read = buffer.readByte(); i |= (k & 0x7F) << j * 7;
int value = (read & 0b01111111); if ((k & 0x80) != 128) {
result |= (value << (7 * numRead)); return i;
numRead++;
if (numRead > 5) {
throw new RuntimeException("VarInt is too big");
} }
} while ((read & 0b10000000) != 0); }
throw new RuntimeException("VarInt is too big");
return result;
} }
public static long readVarLong(ByteBuf buffer) { public static long readVarLong(@NotNull ByteBuf buffer) {
int numRead = 0; int numRead = 0;
long result = 0; long result = 0;
byte read; byte read;
@ -156,14 +134,14 @@ public final class Utils {
if (bitsPerEntry < 9) { if (bitsPerEntry < 9) {
// Palette has to exist // Palette has to exist
final Short2ShortLinkedOpenHashMap paletteBlockMap = section.getPaletteBlockMap(); final Short2ShortLinkedOpenHashMap paletteBlockMap = section.getPaletteBlockMap();
writeVarIntBuf(buffer, paletteBlockMap.size()); writeVarInt(buffer, paletteBlockMap.size());
for (short paletteValue : paletteBlockMap.values()) { for (short paletteValue : paletteBlockMap.values()) {
writeVarIntBuf(buffer, paletteValue); writeVarInt(buffer, paletteValue);
} }
} }
final long[] blocks = section.getBlocks(); final long[] blocks = section.getBlocks();
writeVarIntBuf(buffer, blocks.length); writeVarInt(buffer, blocks.length);
for (long datum : blocks) { for (long datum : blocks) {
buffer.writeLong(datum); buffer.writeLong(datum);
} }

View File

@ -17,6 +17,7 @@ import org.jglrxavpok.hephaistos.nbt.NBTWriter;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -151,7 +152,7 @@ public class BinaryWriter extends OutputStream {
* @param i the int to write * @param i the int to write
*/ */
public void writeVarInt(int i) { public void writeVarInt(int i) {
Utils.writeVarInt(this, i); Utils.writeVarInt(buffer, i);
} }
/** /**
@ -176,6 +177,17 @@ public class BinaryWriter extends OutputStream {
buffer.writeCharSequence(string, StandardCharsets.UTF_8); buffer.writeCharSequence(string, StandardCharsets.UTF_8);
} }
/**
* Writes a null terminated string to the buffer. This method adds the null character
* to the end of the string before writing.
*
* @param string the string to write
* @param charset the charset to encode in
*/
public void writeNullTerminatedString(@NotNull String string, @NotNull Charset charset) {
buffer.writeCharSequence(string + '\0', charset);
}
/** /**
* Writes a JsonMessage to the buffer. * Writes a JsonMessage to the buffer.
* Simply a writeSizedString with message.toString() * Simply a writeSizedString with message.toString()

View File

@ -63,7 +63,7 @@ public interface CacheablePacket {
if (shouldUpdate) { if (shouldUpdate) {
// Buffer freed by guava cache #removalListener // Buffer freed by guava cache #removalListener
final ByteBuf buffer = PacketUtils.createFramedPacket(serverPacket, true); final ByteBuf buffer = PacketUtils.createFramedPacket(serverPacket);
timedBuffer = new TimedBuffer(buffer, timestamp); timedBuffer = new TimedBuffer(buffer, timestamp);
temporaryCache.cache(identifier, timedBuffer); temporaryCache.cache(identifier, timedBuffer);
} }

View File

@ -161,18 +161,42 @@ public final class ChunkUtils {
* @param range how far should it retrieves chunk * @param range how far should it retrieves chunk
* @return an array containing chunks index * @return an array containing chunks index
*/ */
@NotNull public static @NotNull long[] getChunksInRange(@NotNull Position position, int range) {
public static long[] getChunksInRange(@NotNull Position position, int range) { long[] visibleChunks = new long[MathUtils.square(range * 2 + 1)];
range = range * 2; int xDistance = 0;
long[] visibleChunks = new long[MathUtils.square(range + 1)]; int xDirection = 1;
final int startLoop = -(range / 2); int zDistance = 0;
final int endLoop = range / 2 + 1; int zDirection = -1;
int counter = 0; int len = 1;
for (int x = startLoop; x < endLoop; x++) { int corner = 0;
for (int z = startLoop; z < endLoop; z++) {
final int chunkX = getChunkCoordinate(position.getX() + Chunk.CHUNK_SIZE_X * x); for (int i = 0; i < visibleChunks.length; i++) {
final int chunkZ = getChunkCoordinate(position.getZ() + Chunk.CHUNK_SIZE_Z * z); final int chunkX = getChunkCoordinate(xDistance * Chunk.CHUNK_SIZE_X + position.getX());
visibleChunks[counter++] = getChunkIndex(chunkX, chunkZ); final int chunkZ = getChunkCoordinate(zDistance * Chunk.CHUNK_SIZE_Z + position.getZ());
visibleChunks[i] = getChunkIndex(chunkX, chunkZ);
if (corner % 2 == 0) {
// step on X axis
xDistance += xDirection;
if (Math.abs(xDistance) == len) {
// hit corner
corner++;
xDirection = -xDirection;
}
} else {
// step on Z axis
zDistance += zDirection;
if (Math.abs(zDistance) == len) {
// hit corner
corner++;
zDirection = -zDirection;
if (corner % 4 == 0) {
len++;
}
}
} }
} }
return visibleChunks; return visibleChunks;

View File

@ -0,0 +1,87 @@
package net.minestom.server.utils.identity;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* An object with a {@link Component} name and a {@link UUID} identity.
*/
public interface NamedAndIdentified {
/**
* Creates a {@link NamedAndIdentified} instance with an empty name and a random UUID.
*
* @return the named and identified instance
*/
static @NotNull NamedAndIdentified empty() {
return of(Component.empty(), UUID.randomUUID());
}
/**
* Creates a {@link NamedAndIdentified} instance with a given name and a random UUID.
*
* @param name the name
* @return the named and identified instance
*/
static @NotNull NamedAndIdentified named(@NotNull String name) {
return of(name, UUID.randomUUID());
}
/**
* Creates a {@link NamedAndIdentified} instance with a given name and a random UUID.
*
* @param name the name
* @return the named and identified instance
*/
static @NotNull NamedAndIdentified named(@NotNull Component name) {
return of(name, UUID.randomUUID());
}
/**
* Creates a {@link NamedAndIdentified} instance with an empty name and a given UUID.
*
* @param uuid the uuid
* @return the named and identified instance
*/
static @NotNull NamedAndIdentified identified(@NotNull UUID uuid) {
return of(Component.empty(), uuid);
}
/**
* Creates a {@link NamedAndIdentified} instance with a given name and UUID.
*
* @param name the name
* @param uuid the uuid
* @return the named and identified instance
*/
static @NotNull NamedAndIdentified of(@NotNull String name, @NotNull UUID uuid) {
return new NamedAndIdentifiedImpl(name, uuid);
}
/**
* Creates a {@link NamedAndIdentified} instance with a given name and UUID.
*
* @param name the name
* @param uuid the uuid
* @return the named and identified instance
*/
static @NotNull NamedAndIdentified of(@NotNull Component name, @NotNull UUID uuid) {
return new NamedAndIdentifiedImpl(name, uuid);
}
/**
* Gets the name of this object.
*
* @return the name
*/
@NotNull Component getName();
/**
* Gets the UUID of this object.
*
* @return the uuid
*/
@NotNull UUID getUuid();
}

View File

@ -0,0 +1,68 @@
package net.minestom.server.utils.identity;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.UUID;
/**
* Simple implementation of {@link NamedAndIdentified}.
* @see #of(String, UUID)
* @see #of(Component, UUID)
*/
class NamedAndIdentifiedImpl implements NamedAndIdentified {
private final Component name;
private final UUID uuid;
/**
* Creates a new named and identified implementation.
*
* @param name the name
* @param uuid the uuid
* @see NamedAndIdentified#of(String, UUID)
*/
NamedAndIdentifiedImpl(@NotNull String name, @NotNull UUID uuid) {
this(Component.text(name), uuid);
}
/**
* Creates a new named and identified implementation.
*
* @param name the name
* @param uuid the uuid
* @see NamedAndIdentified#of(Component, UUID)
*/
NamedAndIdentifiedImpl(@NotNull Component name, @NotNull UUID uuid) {
this.name = Objects.requireNonNull(name, "name cannot be null");
this.uuid = Objects.requireNonNull(uuid, "uuid cannot be null");
}
@Override
public @NotNull Component getName() {
return this.name;
}
@Override
public @NotNull UUID getUuid() {
return this.uuid;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof NamedAndIdentified)) return false;
NamedAndIdentified that = (NamedAndIdentified) o;
return this.uuid.equals(that.getUuid());
}
@Override
public int hashCode() {
return Objects.hash(this.uuid);
}
@Override
public String toString() {
return String.format("NamedAndIdentifiedImpl{name='%s', uuid=%s}", this.name, this.uuid);
}
}

View File

@ -7,21 +7,22 @@ import demo.commands.*;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandManager; import net.minestom.server.command.CommandManager;
import net.minestom.server.event.server.ServerListPingEvent; import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.extras.lan.OpenToLAN;
import net.minestom.server.extras.lan.OpenToLANConfig;
import net.minestom.server.extras.optifine.OptifineSupport; import net.minestom.server.extras.optifine.OptifineSupport;
import net.minestom.server.instance.block.BlockManager; import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.instance.block.rule.vanilla.RedstonePlacementRule; import net.minestom.server.instance.block.rule.vanilla.RedstonePlacementRule;
import net.minestom.server.ping.ResponseData; import net.minestom.server.ping.ResponseData;
import net.minestom.server.storage.StorageManager; import net.minestom.server.storage.StorageManager;
import net.minestom.server.storage.systems.FileStorageSystem; import net.minestom.server.storage.systems.FileStorageSystem;
import net.minestom.server.utils.identity.NamedAndIdentified;
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.UUID;
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) {
@ -66,37 +67,32 @@ public class Main {
MinecraftServer.getGlobalEventHandler().addEventCallback(ServerListPingEvent.class, event -> { MinecraftServer.getGlobalEventHandler().addEventCallback(ServerListPingEvent.class, event -> {
ResponseData responseData = event.getResponseData(); ResponseData responseData = event.getResponseData();
responseData.setMaxPlayer(0); responseData.addEntry(NamedAndIdentified.named("The first line is separated from the others"));
responseData.setOnline(MinecraftServer.getConnectionManager().getOnlinePlayers().size()); responseData.addEntry(NamedAndIdentified.named("Could be a name, or a message"));
responseData.addPlayer("The first line is separated from the others", UUID.randomUUID());
responseData.addPlayer("Could be a name, or a message", UUID.randomUUID());
responseData.addPlayer("IP test: " + event.getConnection().getRemoteAddress().toString(), UUID.randomUUID());
responseData.addPlayer("Use " + (char)0x00a7 + "7section characters", UUID.randomUUID());
responseData.addPlayer((char)0x00a7 + "7" + (char)0x00a7 + "ofor formatting" + (char)0x00a7 + "r: (" + (char)0x00a7 + "6char" + (char)0x00a7 + "r)" + (char)0x00a7 + "90x00a7", UUID.randomUUID());
responseData.addPlayer("Connection Info:"); // on modern versions, you can obtain the player connection directly from the event
String ip = event.getConnection().getServerAddress(); if (event.getConnection() != null) {
responseData.addPlayer((char)0x00a7 + "8- " + (char)0x00a7 +"7IP: " + (char)0x00a7 + "e" + (ip != null ? ip : "???")); responseData.addEntry(NamedAndIdentified.named("IP test: " + event.getConnection().getRemoteAddress().toString()));
responseData.addPlayer((char)0x00a7 + "8- " + (char)0x00a7 +"7PORT: " + (char)0x00a7 + "e" + event.getConnection().getServerPort());
responseData.addPlayer((char)0x00a7 + "8- " + (char)0x00a7 +"7VERSION: " + (char)0x00a7 + "e" + event.getConnection().getProtocolVersion());
// Check if client supports RGB color responseData.addEntry(NamedAndIdentified.named("Connection Info:"));
if (event.getConnection().getProtocolVersion() >= 713) { // Snapshot 20w17a String ip = event.getConnection().getServerAddress();
responseData.setDescription(Component.text("You can do ") responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
.append(Component.text("RGB", TextColor.color(0x66b3ff))) .append(Component.text(" IP: ", NamedTextColor.GRAY))
.append(Component.text(" color here"))); .append(Component.text(ip != null ? ip : "???", NamedTextColor.YELLOW))));
} else { responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
responseData.setDescription(Component.text("You can do ") .append(Component.text(" PORT: ", NamedTextColor.GRAY))
.append(Component.text("RGB", NamedTextColor.nearestTo(TextColor.color(0x66b3ff)))) .append(Component.text(event.getConnection().getServerPort()))));
.append(Component.text(" color here,")) responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
.append(Component.newline()) .append(Component.text(" VERSION: ", NamedTextColor.GRAY))
.append(Component.text("if you are on 1.16 or up")) .append(Component.text(event.getConnection().getProtocolVersion()))));
);
} }
// components will be converted the legacy section sign format so they are displayed in the client
responseData.addEntry(NamedAndIdentified.named(Component.text("You can use ").append(Component.text("styling too!", NamedTextColor.RED, TextDecoration.BOLD))));
// the data will be automatically converted to the correct format on response, so you can do RGB and it'll be downsampled!
// on legacy versions, colors will be converted to the section format so it'll work there too
responseData.setDescription(Component.text("This is a Minestom Server", TextColor.color(0x66b3ff)));
}); });
PlayerInit.init(); PlayerInit.init();
@ -108,8 +104,10 @@ public class Main {
//MojangAuth.init(); //MojangAuth.init();
// useful for testing - we don't need to worry about event calls so just set this to a long time
OpenToLAN.open(new OpenToLANConfig().eventCallDelay(new UpdateOption(1, TimeUnit.DAY)));
minecraftServer.start("0.0.0.0", 25565); minecraftServer.start("0.0.0.0", 25565);
//Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly)); //Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly));
} }
} }

View File

@ -3,17 +3,22 @@ package demo.commands;
import net.minestom.server.command.CommandSender; import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext; import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentEnum;
import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.arguments.minecraft.registry.ArgumentEntityType; import net.minestom.server.command.builder.arguments.minecraft.registry.ArgumentEntityType;
import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec3; import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec3;
import net.minestom.server.command.builder.condition.Conditions; import net.minestom.server.command.builder.condition.Conditions;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.LivingEntity;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class SummonCommand extends Command { public class SummonCommand extends Command {
private final ArgumentEntityType entity; private final ArgumentEntityType entity;
private final ArgumentRelativeVec3 pos; private final ArgumentRelativeVec3 pos;
private final ArgumentEnum<EntityClass> entityClass;
public SummonCommand() { public SummonCommand() {
super("summon"); super("summon");
@ -21,12 +26,36 @@ public class SummonCommand extends Command {
entity = ArgumentType.EntityType("entity type"); entity = ArgumentType.EntityType("entity type");
pos = ArgumentType.RelativeVec3("pos"); pos = ArgumentType.RelativeVec3("pos");
addSyntax(this::execute, entity, pos); entityClass = ArgumentType.Enum("class", EntityClass.class);
entityClass.setFormat(ArgumentEnum.Format.LOWER_CASED);
entityClass.setDefaultValue(EntityClass.CREATURE);
addSyntax(this::execute, entity, pos, entityClass);
setDefaultExecutor((sender, context) -> sender.sendMessage("Usage: /summon <type> <x> <y> <z> <class>"));
} }
private void execute(@NotNull CommandSender commandSender, @NotNull CommandContext commandContext) { private void execute(@NotNull CommandSender commandSender, @NotNull CommandContext commandContext) {
final Entity entity = new Entity(commandContext.get(this.entity)); final Entity entity = commandContext.get(entityClass).instantiate(commandContext.get(this.entity));
//noinspection ConstantConditions - One couldn't possibly execute a command without being in an instance //noinspection ConstantConditions - One couldn't possibly execute a command without being in an instance
entity.setInstance(commandSender.asPlayer().getInstance(), commandContext.get(pos).from(commandSender.asPlayer()).toPosition()); entity.setInstance(commandSender.asPlayer().getInstance(), commandContext.get(pos).from(commandSender.asPlayer()).toPosition());
} }
@SuppressWarnings("unused")
enum EntityClass {
BASE(Entity::new),
LIVING(LivingEntity::new),
CREATURE(EntityCreature::new);
private final EntityFactory factory;
EntityClass(EntityFactory factory) {
this.factory = factory;
}
public Entity instantiate(EntityType type) {
return factory.newInstance(type);
}
}
interface EntityFactory {
Entity newInstance(EntityType type);
}
} }

View File

@ -11,7 +11,7 @@ public class TestCommand extends Command {
super("testcmd"); super("testcmd");
setDefaultExecutor(this::usage); setDefaultExecutor(this::usage);
addSyntax((sender, context) -> System.out.println("executed"), "test get"); addSyntax((sender, context) -> System.out.println("executed"), "test Get integer<number>");
} }
private void usage(CommandSender sender, CommandContext context) { private void usage(CommandSender sender, CommandContext context) {