mirror of
https://github.com/Minestom/Minestom.git
synced 2025-03-12 14:49:11 +01:00
Merge branch 'master' into event-api
This commit is contained in:
commit
920a36399f
@ -5,7 +5,7 @@ plugins {
|
||||
id 'java'
|
||||
id 'maven-publish'
|
||||
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'
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ dependencies {
|
||||
|
||||
// Noise library for terrain generation
|
||||
// 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
|
||||
api 'org.rocksdb:rocksdbjni:6.16.4'
|
||||
@ -152,7 +152,7 @@ dependencies {
|
||||
api 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.0'
|
||||
|
||||
// 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
|
||||
api 'com.google.guava:guava:30.1-jre'
|
||||
|
@ -1,5 +1,5 @@
|
||||
asmVersion=9.0
|
||||
mixinVersion=0.8.1
|
||||
hephaistosVersion=v1.1.8
|
||||
kotlinVersion=1.4.21
|
||||
kotlinVersion=1.5.0
|
||||
adventureVersion=4.7.0
|
@ -78,19 +78,20 @@ public final class MinecraftServer {
|
||||
public static final String THREAD_NAME_TICK = "Ms-Tick";
|
||||
|
||||
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 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 int THREAD_COUNT_PARALLEL_CHUNK_SAVING = 4;
|
||||
public static final int THREAD_COUNT_PARALLEL_CHUNK_SAVING = getThreadCount("minestom.save-thread-count", 2);
|
||||
|
||||
// Config
|
||||
// Can be modified at performance cost when increased
|
||||
public static final int TICK_PER_SECOND = 20;
|
||||
private static final int MS_TO_SEC = 1000;
|
||||
public static final int TICK_MS = MS_TO_SEC / TICK_PER_SECOND;
|
||||
public static final int TICK_PER_SECOND = Integer.getInteger("minestom.tps", 20);
|
||||
public static final int TICK_MS = 1000 / TICK_PER_SECOND;
|
||||
|
||||
// Network monitoring
|
||||
private static int rateLimit = 300;
|
||||
@ -823,4 +824,8 @@ public final class MinecraftServer {
|
||||
"You cannot access the manager before MinecraftServer#init, " +
|
||||
"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));
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
package net.minestom.server.command.builder;
|
||||
|
||||
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.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentDynamicStringArray;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentDynamicWord;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.command.builder.arguments.*;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.SuggestionType;
|
||||
import net.minestom.server.command.builder.condition.CommandCondition;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@ -15,6 +14,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
@ -330,6 +331,85 @@ public class Command {
|
||||
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) {
|
||||
for (String commandName : command.getNames()) {
|
||||
if (commandName.equals(name)) {
|
||||
@ -339,4 +419,33 @@ public class Command {
|
||||
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<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
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<>();
|
||||
|
||||
static {
|
||||
ARGUMENT_FUNCTION_MAP.put("Literal", ArgumentLiteral::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Boolean", ArgumentBoolean::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Integer", ArgumentInteger::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Double", ArgumentDouble::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Float", ArgumentFloat::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("String", ArgumentString::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Word", ArgumentWord::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("StringArray", ArgumentStringArray::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Command", ArgumentCommand::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("literal", ArgumentLiteral::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("boolean", ArgumentBoolean::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("integer", ArgumentInteger::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("double", ArgumentDouble::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("float", ArgumentFloat::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("string", ArgumentString::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("word", ArgumentWord::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("stringarray", ArgumentStringArray::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("command", ArgumentCommand::new);
|
||||
// TODO enum
|
||||
ARGUMENT_FUNCTION_MAP.put("Color", ArgumentColor::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Time", ArgumentTime::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Enchantment", ArgumentEnchantment::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Particle", ArgumentParticle::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("ResourceLocation", ArgumentResourceLocation::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Potion", ArgumentPotionEffect::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("EntityType", ArgumentEntityType::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("BlockState", ArgumentBlockState::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("IntRange", ArgumentIntRange::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("FloatRange", ArgumentFloatRange::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("color", ArgumentColor::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("time", ArgumentTime::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("enchantment", ArgumentEnchantment::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("particle", ArgumentParticle::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("resourceLocation", ArgumentResourceLocation::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("potion", ArgumentPotionEffect::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("entityType", ArgumentEntityType::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("blockState", ArgumentBlockState::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("intrange", ArgumentIntRange::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("floatrange", ArgumentFloatRange::new);
|
||||
|
||||
ARGUMENT_FUNCTION_MAP.put("Entity", s -> new ArgumentEntity(s).singleEntity(true));
|
||||
ARGUMENT_FUNCTION_MAP.put("Entities", ArgumentEntity::new);
|
||||
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("entity", s -> new ArgumentEntity(s).singleEntity(true));
|
||||
ARGUMENT_FUNCTION_MAP.put("entities", ArgumentEntity::new);
|
||||
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("ItemStack", ArgumentItemStack::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("Component", ArgumentComponent::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("UUID", ArgumentUUID::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("NBT", ArgumentNbtTag::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("NbtCompound", ArgumentNbtCompoundTag::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("RelativeBlockPosition", ArgumentRelativeBlockPosition::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("RelativeVec3", ArgumentRelativeVec3::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("RelativeVec2", ArgumentRelativeVec2::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("itemstack", ArgumentItemStack::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("component", ArgumentComponent::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("uuid", ArgumentUUID::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("nbt", ArgumentNbtTag::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("nbtcompound", ArgumentNbtCompoundTag::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("relativeblockposition", ArgumentRelativeBlockPosition::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("relativevec3", ArgumentRelativeVec3::new);
|
||||
ARGUMENT_FUNCTION_MAP.put("relativevec2", ArgumentRelativeVec2::new);
|
||||
}
|
||||
|
||||
@Beta
|
||||
@NotNull
|
||||
public static Argument<?>[] generate(@NotNull String format) {
|
||||
public static @NotNull Argument<?>[] generate(@NotNull String format) {
|
||||
List<Argument<?>> result = new ArrayList<>();
|
||||
|
||||
// 0 = no state
|
||||
@ -92,7 +92,7 @@ public class ArgumentParser {
|
||||
} else if (c == '<') {
|
||||
// Retrieve argument type
|
||||
final String argument = builder.toString();
|
||||
argumentFunction = ARGUMENT_FUNCTION_MAP.get(argument);
|
||||
argumentFunction = ARGUMENT_FUNCTION_MAP.get(argument.toLowerCase(Locale.ROOT));
|
||||
if (argumentFunction == null) {
|
||||
throw new IllegalArgumentException("error invalid argument name: " + argument);
|
||||
}
|
||||
|
@ -267,7 +267,6 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
|
||||
|
||||
final ChunkCallback endCallback = (chunk) -> {
|
||||
refreshPosition(teleportPosition);
|
||||
refreshView(teleportPosition.getYaw(), teleportPosition.getPitch());
|
||||
|
||||
synchronizePosition();
|
||||
|
||||
@ -493,48 +492,9 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
|
||||
}
|
||||
}
|
||||
|
||||
sendPositionUpdate(false);
|
||||
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
|
||||
{
|
||||
|
||||
@ -591,7 +551,7 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
|
||||
}
|
||||
|
||||
// Apply the position if changed
|
||||
if (!newPosition.isSimilar(position)) {
|
||||
if (!finalVelocityPosition.isSimilar(position)) {
|
||||
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.
|
||||
*
|
||||
@ -1313,13 +1349,10 @@ public class Entity implements Viewable, Tickable, EventHandler, DataContainer,
|
||||
* @param y new position Y
|
||||
* @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.setY(y);
|
||||
position.setZ(z);
|
||||
lastSyncedPosition.setX(x);
|
||||
lastSyncedPosition.setY(y);
|
||||
lastSyncedPosition.setZ(z);
|
||||
|
||||
if (hasPassenger()) {
|
||||
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 #refreshView(float, float)
|
||||
* @see #sendPositionUpdate(boolean)
|
||||
*/
|
||||
public void refreshPosition(@NotNull Position position) {
|
||||
refreshPosition(position.getX(), position.getY(), position.getZ());
|
||||
@ApiStatus.Internal
|
||||
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 pitch the pitch
|
||||
*/
|
||||
public void refreshView(float yaw, float pitch) {
|
||||
this.lastPosition.setYaw(position.getYaw());
|
||||
this.lastPosition.setPitch(position.getPitch());
|
||||
private void refreshView(final float yaw, final float pitch) {
|
||||
lastPosition.setYaw(position.getYaw());
|
||||
lastPosition.setPitch(position.getPitch());
|
||||
position.setYaw(yaw);
|
||||
position.setPitch(pitch);
|
||||
this.lastSyncedPosition.setYaw(yaw);
|
||||
this.lastSyncedPosition.setPitch(pitch);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,6 +66,7 @@ import net.minestom.server.utils.*;
|
||||
import net.minestom.server.utils.chunk.ChunkCallback;
|
||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||
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.inventory.PlayerInventoryUtils;
|
||||
import net.minestom.server.utils.time.Cooldown;
|
||||
@ -89,12 +90,13 @@ import java.util.function.UnaryOperator;
|
||||
* <p>
|
||||
* 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 boolean answerKeepAlive;
|
||||
|
||||
private String username;
|
||||
private Component usernameComponent;
|
||||
protected final PlayerConnection playerConnection;
|
||||
// All the entities that this player can see
|
||||
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
|
||||
private final Set<Player> targetBreakers = Collections.singleton(this);
|
||||
|
||||
// Position synchronization with viewers
|
||||
private final Position lastSyncedPlayerPosition;
|
||||
|
||||
// Experience orb pickup
|
||||
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) {
|
||||
super(EntityType.PLAYER, uuid);
|
||||
this.username = username;
|
||||
this.usernameComponent = Component.text(username);
|
||||
this.playerConnection = playerConnection;
|
||||
|
||||
setBoundingBox(0.6f, 1.8f, 0.6f);
|
||||
|
||||
setRespawnPoint(new Position(0, 0, 0));
|
||||
this.lastSyncedPlayerPosition = new Position();
|
||||
|
||||
this.settings = new PlayerSettings();
|
||||
this.inventory = new PlayerInventory(this);
|
||||
@ -421,50 +420,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
|
||||
// Tick event
|
||||
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
|
||||
@ -653,19 +608,13 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
sendDimension(instanceDimensionType);
|
||||
}
|
||||
|
||||
// Load all the required chunks
|
||||
final long[] visibleChunks = ChunkUtils.getChunksInRange(spawnPosition, getChunkRange());
|
||||
// Only load the spawning chunk to speed up login, remaining chunks are loaded in #spawnPlayer
|
||||
final long[] visibleChunks = ChunkUtils.getChunksInRange(spawnPosition, 0);
|
||||
|
||||
final ChunkCallback endCallback = chunk -> {
|
||||
// This is the last chunk to be loaded , spawn player
|
||||
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));
|
||||
final ChunkCallback endCallback =
|
||||
chunk -> spawnPlayer(instance, spawnPosition, firstSpawn, dimensionChange, true);
|
||||
|
||||
ChunkUtils.optionalLoadAll(instance, visibleChunks, null, endCallback);
|
||||
} else {
|
||||
// The player already has the good version of all the chunks.
|
||||
// We just need to refresh his entity viewing list and add him to the instance
|
||||
@ -694,23 +643,20 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
*
|
||||
* @param spawnPosition the position to teleport the player
|
||||
* @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,
|
||||
boolean firstSpawn, boolean updateChunks, boolean dimensionChange) {
|
||||
// Clear previous instance elements
|
||||
boolean firstSpawn, boolean dimensionChange, boolean updateChunks) {
|
||||
if (!firstSpawn) {
|
||||
// Player instance changed, clear current viewable collections
|
||||
this.viewableChunks.forEach(chunk -> chunk.removeViewer(this));
|
||||
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
|
||||
}
|
||||
|
||||
super.setInstance(instance, spawnPosition);
|
||||
this.lastSyncedPlayerPosition.set(position);
|
||||
|
||||
if (!position.isSimilar(spawnPosition) && !firstSpawn) {
|
||||
// Player changed instance at a different position
|
||||
teleport(spawnPosition);
|
||||
} else if (updateChunks) {
|
||||
// Send newly visible chunks to player once spawned in the instance
|
||||
if (updateChunks) {
|
||||
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
|
||||
public String getUsername() {
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1347,6 +1307,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
*/
|
||||
public void setUsernameField(@NotNull String username) {
|
||||
this.username = username;
|
||||
this.usernameComponent = Component.text(username);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
protected void refreshAfterTeleport() {
|
||||
getInventory().update();
|
||||
|
||||
sendPacketsToViewers(getEntityType().getSpawnType().getSpawnPacket(this));
|
||||
|
||||
@ -1480,6 +1440,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
sendPacketToViewersAndSelf(getPropertiesPacket());
|
||||
sendPacketToViewersAndSelf(getEquipmentsPacket());
|
||||
|
||||
getInventory().update();
|
||||
|
||||
{
|
||||
// Send new chunks
|
||||
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[] newChunks = ArrayUtils.getDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks);
|
||||
|
||||
// Update client render distance
|
||||
updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
|
||||
|
||||
// Unload old chunks
|
||||
for (int index : oldChunks) {
|
||||
final long chunkIndex = lastVisibleChunks[index];
|
||||
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
|
||||
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
|
||||
|
||||
// TODO prevent the client from getting lag spikes when re-loading large chunks
|
||||
// Probably by having a distinction between visible and loaded (cache) chunks
|
||||
/*UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
|
||||
final UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
|
||||
unloadChunkPacket.chunkX = chunkX;
|
||||
unloadChunkPacket.chunkZ = chunkZ;
|
||||
playerConnection.sendPacket(unloadChunkPacket);*/
|
||||
playerConnection.sendPacket(unloadChunkPacket);
|
||||
|
||||
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
|
||||
if (chunk != null)
|
||||
chunk.removeViewer(this);
|
||||
}
|
||||
|
||||
// Update client render distance
|
||||
updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
|
||||
|
||||
// Load new chunks
|
||||
for (int index : newChunks) {
|
||||
final long chunkIndex = updatedVisibleChunks[index];
|
||||
@ -2007,6 +1967,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
playerConnection.sendPacket(updateViewPositionPacket);
|
||||
}
|
||||
|
||||
public int getNextTeleportId() {
|
||||
return teleportId.getAndIncrement();
|
||||
}
|
||||
|
||||
public int getLastSentTeleportId() {
|
||||
return teleportId.get();
|
||||
}
|
||||
@ -2397,17 +2361,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
* based on which one is the lowest
|
||||
*/
|
||||
public int getChunkRange() {
|
||||
final int playerRange = getSettings().viewDistance;
|
||||
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);
|
||||
}
|
||||
return Math.min(getSettings().viewDistance, MinecraftServer.getChunkViewDistance());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2639,6 +2593,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
private byte displayedSkinParts;
|
||||
private MainHand mainHand;
|
||||
|
||||
public PlayerSettings() {
|
||||
viewDistance = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* The player game language.
|
||||
*
|
||||
|
@ -1,29 +1,59 @@
|
||||
package net.minestom.server.event.server;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.event.CancellableEvent;
|
||||
import net.minestom.server.event.Event;
|
||||
import net.minestom.server.network.player.PlayerConnection;
|
||||
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.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Called when a {@link PlayerConnection} sends a status packet,
|
||||
* usually to display information on the server list.
|
||||
*/
|
||||
public class ServerListPingEvent extends Event implements CancellableEvent {
|
||||
private boolean cancelled = false;
|
||||
|
||||
private final ResponseData responseData;
|
||||
private final PlayerConnection connection;
|
||||
private final ServerListPingType type;
|
||||
|
||||
private boolean cancelled = false;
|
||||
private ResponseData responseData;
|
||||
|
||||
public ServerListPingEvent(ResponseData responseData, PlayerConnection connection) {
|
||||
this.responseData = responseData;
|
||||
this.connection = connection;
|
||||
/**
|
||||
* Creates a new server list ping event with no player 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
|
||||
*/
|
||||
@ -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.
|
||||
*/
|
||||
public @NotNull PlayerConnection getConnection() {
|
||||
public @Nullable PlayerConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ping type that the client is pinging with.
|
||||
*
|
||||
* @return the ping type
|
||||
*/
|
||||
public @NotNull ServerListPingType getPingType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
*/
|
||||
@ -57,5 +104,4 @@ public class ServerListPingEvent extends Event implements CancellableEvent {
|
||||
public void setCancelled(boolean cancel) {
|
||||
this.cancelled = cancel;
|
||||
}
|
||||
|
||||
}
|
||||
|
140
src/main/java/net/minestom/server/extras/lan/OpenToLAN.java
Normal file
140
src/main/java/net/minestom/server/extras/lan/OpenToLAN.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
224
src/main/java/net/minestom/server/extras/query/Query.java
Normal file
224
src/main/java/net/minestom/server/extras/query/Query.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -417,22 +417,18 @@ public abstract class Chunk implements Viewable, Tickable, DataContainer {
|
||||
UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime());
|
||||
updateLightPacket.chunkX = getChunkX();
|
||||
updateLightPacket.chunkZ = getChunkZ();
|
||||
updateLightPacket.skyLightMask = 0x3FFF0;
|
||||
updateLightPacket.blockLightMask = 0x3F;
|
||||
updateLightPacket.emptySkyLightMask = 0x0F;
|
||||
updateLightPacket.emptyBlockLightMask = 0x3FFC0;
|
||||
updateLightPacket.skyLightMask = 0b111111111111111111;
|
||||
updateLightPacket.emptySkyLightMask = 0b000000000000000000;
|
||||
updateLightPacket.blockLightMask = 0b000000000000000000;
|
||||
updateLightPacket.emptyBlockLightMask = 0b111111111111111111;
|
||||
byte[] bytes = new byte[2048];
|
||||
Arrays.fill(bytes, (byte) 0xFF);
|
||||
List<byte[]> temp = new ArrayList<>(14);
|
||||
List<byte[]> temp2 = new ArrayList<>(6);
|
||||
for (int i = 0; i < 14; ++i) {
|
||||
final List<byte[]> temp = new ArrayList<>(18);
|
||||
for (int i = 0; i < 18; ++i) {
|
||||
temp.add(bytes);
|
||||
}
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
temp2.add(bytes);
|
||||
}
|
||||
updateLightPacket.skyLight = temp;
|
||||
updateLightPacket.blockLight = temp2;
|
||||
updateLightPacket.blockLight = new ArrayList<>(0);
|
||||
|
||||
return updateLightPacket;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import net.minestom.server.world.biomes.Biome;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@ -59,7 +60,7 @@ public class DynamicChunk extends Chunk {
|
||||
|
||||
private long lastChangeTime;
|
||||
|
||||
private ChunkDataPacket cachedPacket;
|
||||
private SoftReference<ChunkDataPacket> cachedPacket = new SoftReference<>(null);
|
||||
private long cachedPacketTime;
|
||||
|
||||
public DynamicChunk(@NotNull Instance instance, @Nullable Biome[] biomes, int chunkX, int chunkZ,
|
||||
@ -386,22 +387,22 @@ public class DynamicChunk extends Chunk {
|
||||
@NotNull
|
||||
@Override
|
||||
protected ChunkDataPacket createFreshPacket() {
|
||||
if (cachedPacket != null && cachedPacketTime == getLastChangeTime()) {
|
||||
return cachedPacket;
|
||||
ChunkDataPacket packet = cachedPacket.get();
|
||||
if (packet != null && cachedPacketTime == getLastChangeTime()) {
|
||||
return packet;
|
||||
}
|
||||
ChunkDataPacket fullDataPacket = new ChunkDataPacket(getIdentifier(), getLastChangeTime());
|
||||
fullDataPacket.biomes = biomes;
|
||||
fullDataPacket.chunkX = chunkX;
|
||||
fullDataPacket.chunkZ = chunkZ;
|
||||
fullDataPacket.paletteStorage = blockPalette.clone();
|
||||
fullDataPacket.customBlockPaletteStorage = customBlockPalette.clone();
|
||||
fullDataPacket.blockEntities = blockEntities.clone();
|
||||
fullDataPacket.blocksData = blocksData.clone();
|
||||
packet = new ChunkDataPacket(getIdentifier(), getLastChangeTime());
|
||||
packet.biomes = biomes;
|
||||
packet.chunkX = chunkX;
|
||||
packet.chunkZ = chunkZ;
|
||||
packet.paletteStorage = blockPalette.clone();
|
||||
packet.customBlockPaletteStorage = customBlockPalette.clone();
|
||||
packet.blockEntities = blockEntities.clone();
|
||||
packet.blocksData = blocksData.clone();
|
||||
|
||||
this.cachedPacketTime = getLastChangeTime();
|
||||
this.cachedPacket = fullDataPacket;
|
||||
|
||||
return fullDataPacket;
|
||||
this.cachedPacket = new SoftReference<>(packet);
|
||||
return packet;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
@ -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.
|
||||
*/
|
||||
protected void UNSAFE_unloadChunks() {
|
||||
if (scheduledChunksToRemove.isEmpty()) {
|
||||
// Fast exit
|
||||
return;
|
||||
}
|
||||
synchronized (scheduledChunksToRemove) {
|
||||
for (Chunk chunk : scheduledChunksToRemove) {
|
||||
final int chunkX = chunk.getChunkX();
|
||||
|
@ -272,38 +272,40 @@ public class PlayerInventory extends AbstractInventory implements EquipmentHandl
|
||||
|
||||
@Override
|
||||
public boolean leftClick(@NotNull Player player, int slot) {
|
||||
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
|
||||
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())
|
||||
sendSlotRefresh((short) slot, clicked);
|
||||
|
||||
setItemStack(slot, OFFSET, clickResult.getClicked());
|
||||
setItemStack(convertedSlot, clickResult.getClicked());
|
||||
setCursorItem(clickResult.getCursor());
|
||||
|
||||
if (!clickResult.isCancel())
|
||||
callClickEvent(player, null, slot, ClickType.LEFT_CLICK, clicked, cursor);
|
||||
callClickEvent(player, null, convertedSlot, ClickType.LEFT_CLICK, clicked, cursor);
|
||||
|
||||
return !clickResult.isCancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rightClick(@NotNull Player player, int slot) {
|
||||
final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET);
|
||||
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())
|
||||
sendSlotRefresh((short) slot, clicked);
|
||||
|
||||
setItemStack(slot, OFFSET, clickResult.getClicked());
|
||||
setItemStack(convertedSlot, clickResult.getClicked());
|
||||
setCursorItem(clickResult.getCursor());
|
||||
|
||||
if (!clickResult.isCancel())
|
||||
callClickEvent(player, null, slot, ClickType.RIGHT_CLICK, clicked, cursor);
|
||||
callClickEvent(player, null, convertedSlot, ClickType.RIGHT_CLICK, clicked, cursor);
|
||||
|
||||
return !clickResult.isCancel();
|
||||
}
|
||||
|
@ -84,7 +84,16 @@ public class ItemTag<T> {
|
||||
|
||||
public static @NotNull ItemTag<NBT> NBT(@NotNull String 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())));
|
||||
}
|
||||
|
||||
|
@ -99,13 +99,15 @@ public class BlockPlacementListener {
|
||||
blockPosition.add(offsetX, offsetY, offsetZ);
|
||||
|
||||
if (!canPlaceBlock) {
|
||||
//Send a block change with AIR as block to keep the client in sync,
|
||||
//using refreshChunk results in the client not being in sync
|
||||
//after rapid invalid block placements
|
||||
BlockChangePacket blockChangePacket = new BlockChangePacket();
|
||||
blockChangePacket.blockPosition = blockPosition;
|
||||
blockChangePacket.blockStateId = Block.AIR.getBlockId();
|
||||
player.getPlayerConnection().sendPacket(blockChangePacket);
|
||||
if (useMaterial.isBlock()) {
|
||||
//Send a block change with AIR as block to keep the client in sync,
|
||||
//using refreshChunk results in the client not being in sync
|
||||
//after rapid invalid block placements
|
||||
BlockChangePacket blockChangePacket = new BlockChangePacket();
|
||||
blockChangePacket.blockPosition = blockPosition;
|
||||
blockChangePacket.blockStateId = Block.AIR.getBlockId();
|
||||
player.getPlayerConnection().sendPacket(blockChangePacket);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -87,8 +87,7 @@ public class PlayerPositionListener {
|
||||
player.teleport(newPosition);
|
||||
}
|
||||
// Change the internal data
|
||||
player.refreshPosition(newPosition.getX(), newPosition.getY(), newPosition.getZ());
|
||||
player.refreshView(newPosition.getYaw(), newPosition.getPitch());
|
||||
player.refreshPosition(newPosition);
|
||||
player.refreshOnGround(onGround);
|
||||
} else {
|
||||
player.teleport(player.getPosition());
|
||||
|
@ -23,10 +23,8 @@ public class PlayerVehicleListener {
|
||||
if (vehicle == null)
|
||||
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.refreshView(packet.yaw, packet.pitch);
|
||||
vehicle.askSynchronization();
|
||||
|
||||
// This packet causes weird screen distortion
|
||||
/*VehicleMovePacket vehicleMovePacket = new VehicleMovePacket();
|
||||
|
@ -76,8 +76,8 @@ public final class BenchmarkManager {
|
||||
|
||||
stop = false;
|
||||
|
||||
}, MinecraftServer.THREAD_NAME_BENCHMARK, 0L);
|
||||
|
||||
}, MinecraftServer.THREAD_NAME_BENCHMARK);
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
|
||||
this.enabled = true;
|
||||
|
@ -13,8 +13,6 @@ import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.ServerSocketChannel;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
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.IOUringEventLoopGroup;
|
||||
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.netty.channel.ClientChannel;
|
||||
import net.minestom.server.network.netty.codec.*;
|
||||
import net.minestom.server.ping.ResponseDataConsumer;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -30,22 +27,15 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public final class NettyServer {
|
||||
|
||||
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,
|
||||
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 ENCRYPT_HANDLER_NAME = "encrypt"; // Write
|
||||
@ -63,7 +53,6 @@ public final class NettyServer {
|
||||
private boolean initialized = false;
|
||||
|
||||
private final PacketProcessor packetProcessor;
|
||||
private final GlobalChannelTrafficShapingHandler globalTrafficHandler;
|
||||
|
||||
private EventLoopGroup boss, worker;
|
||||
private ServerBootstrap bootstrap;
|
||||
@ -73,27 +62,14 @@ public final class NettyServer {
|
||||
private String address;
|
||||
private int port;
|
||||
|
||||
/**
|
||||
* Scheduler used by {@code globalTrafficHandler}.
|
||||
*/
|
||||
private final ScheduledExecutorService trafficScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public NettyServer(@NotNull 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...
|
||||
* <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() {
|
||||
Check.stateCondition(initialized, "Netty server has already been initialized!");
|
||||
@ -146,14 +122,11 @@ public final class NettyServer {
|
||||
ChannelConfig config = ch.config();
|
||||
config.setOption(ChannelOption.TCP_NODELAY, 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);
|
||||
|
||||
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)
|
||||
// Removed from the pipeline later in LegacyPingHandler if unnecessary (>1.6)
|
||||
pipeline.addLast(LEGACY_PING_HANDLER_NAME, new LegacyPingHandler());
|
||||
@ -185,18 +158,6 @@ public final class NettyServer {
|
||||
this.address = address;
|
||||
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
|
||||
try {
|
||||
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.
|
||||
* <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.
|
||||
* Stops the server.
|
||||
*/
|
||||
public void stop() {
|
||||
this.worker.shutdownGracefully();
|
||||
this.boss.shutdownGracefully();
|
||||
|
||||
this.trafficScheduler.shutdown();
|
||||
this.globalTrafficHandler.release();
|
||||
try {
|
||||
this.boss.shutdownGracefully().sync();
|
||||
this.worker.shutdownGracefully().sync();
|
||||
this.serverChannel.closeFuture().sync();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,22 +6,23 @@ import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.event.server.ServerListPingEvent;
|
||||
import net.minestom.server.ping.ServerListPingType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
// Copied from original minecraft :(
|
||||
public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private ByteBuf buf;
|
||||
|
||||
@Override
|
||||
public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object object) {
|
||||
ByteBuf buf = (ByteBuf) object;
|
||||
final ByteBuf buf = (ByteBuf) object;
|
||||
|
||||
if (this.buf != null) {
|
||||
try {
|
||||
readLegacy1_6(ctx, buf);
|
||||
handle1_6(ctx, buf);
|
||||
} finally {
|
||||
buf.release();
|
||||
}
|
||||
@ -38,19 +39,17 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
switch (length) {
|
||||
case 0:
|
||||
this.writeResponse(ctx, this.createResponse(formatResponse(-2)));
|
||||
if (trySendResponse(ServerListPingType.LEGACY_UNVERSIONED, ctx)) return;
|
||||
break;
|
||||
case 1:
|
||||
if (buf.readUnsignedByte() != 1) {
|
||||
return;
|
||||
}
|
||||
if (buf.readUnsignedByte() != 1) return;
|
||||
|
||||
this.writeResponse(ctx, this.createResponse(formatResponse(-1)));
|
||||
if (trySendResponse(ServerListPingType.LEGACY_VERSIONED, ctx)) return;
|
||||
break;
|
||||
default:
|
||||
if (buf.readUnsignedByte() != 0x01 || buf.readUnsignedByte() != 0xFA) return;
|
||||
|
||||
readLegacy1_6(ctx, buf);
|
||||
handle1_6(ctx, buf);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -66,19 +65,7 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private static String readLegacyString(ByteBuf buf) {
|
||||
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) {
|
||||
private void handle1_6(ChannelHandlerContext ctx, ByteBuf part) {
|
||||
ByteBuf buf = this.buf;
|
||||
|
||||
if (buf == null) {
|
||||
@ -127,27 +114,7 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
this.buf = null;
|
||||
|
||||
this.writeResponse(ctx, this.createResponse(formatResponse(protocolVersion)));
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
trySendResponse(ServerListPingType.LEGACY_VERSIONED, 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) {
|
||||
ByteBuf response = Unpooled.buffer();
|
||||
response.writeByte(255);
|
||||
|
||||
final char[] chars = s.toCharArray();
|
||||
|
||||
response.writeShort(chars.length);
|
||||
|
||||
for (char c : chars) {
|
||||
response.writeChar(c);
|
||||
private static String readLegacyString(ByteBuf buf) {
|
||||
int size = buf.readShort() * Character.BYTES;
|
||||
if (!buf.isReadable(size)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response;
|
||||
final String result = buf.toString(buf.readerIndex(), size, StandardCharsets.UTF_16BE);
|
||||
buf.skipBytes(size);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
package net.minestom.server.network.packet.client.status;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.event.server.ServerListPingEvent;
|
||||
import net.minestom.server.network.packet.client.ClientPreplayPacket;
|
||||
import net.minestom.server.network.packet.server.handshake.ResponsePacket;
|
||||
import net.minestom.server.network.player.PlayerConnection;
|
||||
import net.minestom.server.ping.ResponseData;
|
||||
import net.minestom.server.ping.ResponseDataConsumer;
|
||||
import net.minestom.server.ping.ServerListPingType;
|
||||
import net.minestom.server.utils.binary.BinaryReader;
|
||||
import net.minestom.server.utils.binary.BinaryWriter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -16,30 +14,14 @@ public class StatusRequestPacket implements ClientPreplayPacket {
|
||||
|
||||
@Override
|
||||
public void process(@NotNull PlayerConnection connection) {
|
||||
ResponseDataConsumer consumer = MinecraftServer.getResponseDataConsumer();
|
||||
ResponseData responseData = new ResponseData();
|
||||
|
||||
// Fill default params
|
||||
responseData.setVersion(MinecraftServer.VERSION_NAME);
|
||||
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);
|
||||
});
|
||||
final ServerListPingType pingVersion = ServerListPingType.fromModernProtocolVersion(connection.getProtocolVersion());
|
||||
final ServerListPingEvent statusRequestEvent = new ServerListPingEvent(connection, pingVersion);
|
||||
MinecraftServer.getGlobalEventHandler().callCancellableEvent(ServerListPingEvent.class, statusRequestEvent, () -> {
|
||||
final ResponsePacket responsePacket = new ResponsePacket();
|
||||
responsePacket.jsonResponse = pingVersion.getPingResponse(statusRequestEvent.getResponseData());
|
||||
|
||||
connection.sendPacket(responsePacket);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.minestom.server.network.packet.server.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
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.ServerPacketIdentifier;
|
||||
import net.minestom.server.utils.BlockPosition;
|
||||
import net.minestom.server.utils.BufUtils;
|
||||
import net.minestom.server.utils.Utils;
|
||||
import net.minestom.server.utils.binary.BinaryReader;
|
||||
import net.minestom.server.utils.binary.BinaryWriter;
|
||||
@ -82,7 +82,7 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
|
||||
writer.writeBoolean(fullChunk);
|
||||
|
||||
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++) {
|
||||
if (fullChunk || (sections.length == CHUNK_SECTION_COUNT && sections[i] != 0)) {
|
||||
final Section section = paletteStorage.getSections()[i];
|
||||
|
@ -62,7 +62,7 @@ public class NettyPlayerConnection extends PlayerConnection {
|
||||
private PlayerSkin bungeeSkin;
|
||||
|
||||
private final Object tickBufferLock = new Object();
|
||||
private volatile ByteBuf tickBuffer = BufUtils.getBuffer(true);
|
||||
private volatile ByteBuf tickBuffer = BufUtils.direct();
|
||||
|
||||
public NettyPlayerConnection(@NotNull SocketChannel channel) {
|
||||
super();
|
||||
|
@ -1,44 +1,53 @@
|
||||
package net.minestom.server.ping;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
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.plain.PlainComponentSerializer;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private final List<PingPlayer> pingPlayers;
|
||||
private static final Component DEFAULT_DESCRIPTION = Component.text("Minestom Server");
|
||||
|
||||
private final List<NamedAndIdentified> entries;
|
||||
|
||||
private String version;
|
||||
private int protocol;
|
||||
private int maxPlayer;
|
||||
private int online;
|
||||
private Component description;
|
||||
|
||||
private String favicon;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link 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.
|
||||
*
|
||||
* @param name The name for the response data.
|
||||
* @deprecated Use {@link #setVersion(String)}
|
||||
* @deprecated This is named incorrectly, use {@link #setVersion(String)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public void setName(String name) {
|
||||
@ -121,7 +130,9 @@ public class ResponseData {
|
||||
* Adds some players to the response.
|
||||
*
|
||||
* @param players the players
|
||||
* @deprecated Use {@link #addEntries(Collection)}}
|
||||
*/
|
||||
@Deprecated
|
||||
public void addPlayer(Iterable<Player> players) {
|
||||
for (Player player : players) {
|
||||
this.addPlayer(player);
|
||||
@ -132,9 +143,11 @@ public class ResponseData {
|
||||
* Adds a player to the response.
|
||||
*
|
||||
* @param player the player
|
||||
* @deprecated Use {@link #addEntry(NamedAndIdentified)}
|
||||
*/
|
||||
@Deprecated
|
||||
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 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) {
|
||||
PingPlayer pingPlayer = PingPlayer.of(name, uuid);
|
||||
this.pingPlayers.add(pingPlayer);
|
||||
this.addEntry(NamedAndIdentified.of(name, uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,27 +168,35 @@ public class ResponseData {
|
||||
* {@link UUID#randomUUID()} is used as the player's UUID.
|
||||
*
|
||||
* @param name The name of the player.
|
||||
* @deprecated Use {@link #addEntry(NamedAndIdentified)} with {@link NamedAndIdentified#named(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public void addPlayer(String name) {
|
||||
PingPlayer pingPlayer = PingPlayer.of(name, UUID.randomUUID());
|
||||
this.pingPlayers.add(pingPlayer);
|
||||
this.addEntry(NamedAndIdentified.named(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @deprecated Use {@link #clearEntries()}
|
||||
*/
|
||||
@Deprecated
|
||||
public void clearPlayers() {
|
||||
this.pingPlayers.clear();
|
||||
this.clearEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 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() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* @return The converted response data as a json tree.
|
||||
* @deprecated Use {@link ServerListPingType#getPingResponse(ResponseData)}
|
||||
*/
|
||||
@NotNull
|
||||
public JsonObject build() {
|
||||
// version
|
||||
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;
|
||||
@Deprecated
|
||||
public @NotNull JsonObject build() {
|
||||
return ServerListPingType.getModernPingResponse(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a player line in the server list hover.
|
||||
* @deprecated See {@link NamedAndIdentified}
|
||||
*/
|
||||
@Deprecated
|
||||
public static class PingPlayer {
|
||||
|
||||
private static @NotNull PingPlayer of(@NotNull String name, @NotNull UUID uuid) {
|
||||
|
157
src/main/java/net/minestom/server/ping/ServerListPingType.java
Normal file
157
src/main/java/net/minestom/server/ping/ServerListPingType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ import java.nio.file.Paths;
|
||||
*/
|
||||
public class FileStorageSystem implements StorageSystem {
|
||||
|
||||
private Options options;
|
||||
|
||||
static {
|
||||
RocksDB.loadLibrary();
|
||||
}
|
||||
@ -32,11 +34,11 @@ public class FileStorageSystem implements StorageSystem {
|
||||
|
||||
@Override
|
||||
public void open(@NotNull String location, @NotNull StorageOptions storageOptions) {
|
||||
Options options = new Options().setCreateIfMissing(true);
|
||||
options = new Options().setCreateIfMissing(true);
|
||||
|
||||
if (storageOptions.hasCompression()) {
|
||||
options.setCompressionType(CompressionType.ZSTD_COMPRESSION);
|
||||
options.setCompressionOptions(new CompressionOptions().setLevel(1));
|
||||
options.setCompressionOptions(new CompressionOptions().setLevel(4));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -77,6 +79,9 @@ public class FileStorageSystem implements StorageSystem {
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
if (options != null)
|
||||
this.options.close();
|
||||
|
||||
this.rocksDB.closeE();
|
||||
} catch (RocksDBException e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
|
@ -22,26 +22,31 @@ public class MinestomTerminal {
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static void start() {
|
||||
try {
|
||||
terminal = TerminalBuilder.terminal();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
LineReader reader = LineReaderBuilder.builder()
|
||||
.terminal(terminal)
|
||||
.build();
|
||||
running = true;
|
||||
while (running) {
|
||||
String command;
|
||||
final Thread thread = new Thread(null, () -> {
|
||||
try {
|
||||
command = reader.readLine(PROMPT);
|
||||
COMMAND_MANAGER.execute(COMMAND_MANAGER.getConsoleSender(), command);
|
||||
} catch (UserInterruptException e) {
|
||||
// Ignore
|
||||
} catch (EndOfFileException e) {
|
||||
return;
|
||||
terminal = TerminalBuilder.terminal();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
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
|
||||
|
@ -4,8 +4,6 @@ import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
@ -50,10 +48,7 @@ public class TickThread extends Thread {
|
||||
private volatile boolean stop;
|
||||
private TickThread tickThread;
|
||||
|
||||
private volatile boolean inTick;
|
||||
private final AtomicReference<CountDownLatch> countDownLatch = new AtomicReference<>();
|
||||
|
||||
private final Queue<Runnable> queue = new ConcurrentLinkedQueue<>();
|
||||
private final AtomicReference<TickContext> tickContext = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
@ -62,42 +57,38 @@ public class TickThread extends Thread {
|
||||
LockSupport.park(tickThread);
|
||||
if (stop)
|
||||
break;
|
||||
CountDownLatch localCountDownLatch = this.countDownLatch.get();
|
||||
|
||||
// The latch is necessary to control the tick rates
|
||||
if (localCountDownLatch == null) {
|
||||
TickContext localContext = this.tickContext.get();
|
||||
// The context is necessary to control the tick rates
|
||||
if (localContext == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.inTick = true;
|
||||
// Execute tick
|
||||
localContext.runnable.run();
|
||||
|
||||
// Execute all pending runnable
|
||||
Runnable runnable;
|
||||
while ((runnable = queue.poll()) != null) {
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
localCountDownLatch.countDown();
|
||||
this.countDownLatch.compareAndSet(localCountDownLatch, null);
|
||||
|
||||
// Wait for the next notify (game tick)
|
||||
this.inTick = false;
|
||||
localContext.countDownLatch.countDown();
|
||||
this.tickContext.compareAndSet(localContext, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void startTick(@NotNull CountDownLatch countDownLatch, @NotNull Runnable runnable) {
|
||||
this.countDownLatch.set(countDownLatch);
|
||||
this.queue.add(runnable);
|
||||
protected void startTick(@NotNull CountDownLatch countDownLatch, @NotNull Runnable runnable) {
|
||||
this.tickContext.set(new TickContext(countDownLatch, runnable));
|
||||
LockSupport.unpark(tickThread);
|
||||
}
|
||||
|
||||
public boolean isInTick() {
|
||||
return inTick;
|
||||
}
|
||||
|
||||
private void setLinkedThread(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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,28 +7,7 @@ public class BufUtils {
|
||||
|
||||
private static final PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
|
||||
|
||||
public static ByteBuf getBuffer() {
|
||||
return alloc.heapBuffer();
|
||||
public static ByteBuf direct() {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
29
src/main/java/net/minestom/server/utils/NetworkUtils.java
Normal file
29
src/main/java/net/minestom/server/utils/NetworkUtils.java
Normal 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;
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package net.minestom.server.utils;
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.audience.ForwardingAudience;
|
||||
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.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
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 PacketUtils() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,7 +51,7 @@ public final class PacketUtils {
|
||||
* </ol>
|
||||
*
|
||||
* @param audience the audience
|
||||
* @param packet the packet
|
||||
* @param packet the packet
|
||||
*/
|
||||
@SuppressWarnings("OverrideOnly") // we need to access the audiences inside ForwardingAudience
|
||||
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
|
||||
boolean needsTranslating = false;
|
||||
|
||||
if (AdventureSerializer.AUTOMATIC_COMPONENT_TRANSLATION && packet instanceof ComponentHoldingServerPacket) {
|
||||
needsTranslating = AdventureSerializer.areAnyTranslatable(((ComponentHoldingServerPacket) packet).components());
|
||||
}
|
||||
@ -96,21 +92,17 @@ public final class PacketUtils {
|
||||
// Send grouped packet...
|
||||
final boolean success = PACKET_LISTENER_MANAGER.processServerPacket(packet, players);
|
||||
if (success) {
|
||||
final ByteBuf finalBuffer = createFramedPacket(packet, true);
|
||||
final ByteBuf finalBuffer = createFramedPacket(packet);
|
||||
final FramedPacket framedPacket = new FramedPacket(finalBuffer);
|
||||
|
||||
// Send packet to all players
|
||||
for (Player player : players) {
|
||||
|
||||
if (!player.isOnline())
|
||||
continue;
|
||||
|
||||
// Verify if the player should receive the packet
|
||||
if (playerValidator != null && !playerValidator.isValid(player))
|
||||
continue;
|
||||
|
||||
finalBuffer.retain();
|
||||
|
||||
final PlayerConnection playerConnection = player.getPlayerConnection();
|
||||
if (playerConnection instanceof NettyPlayerConnection) {
|
||||
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) playerConnection;
|
||||
@ -118,15 +110,12 @@ public final class PacketUtils {
|
||||
} else {
|
||||
playerConnection.sendPacket(packet);
|
||||
}
|
||||
|
||||
finalBuffer.release();
|
||||
}
|
||||
finalBuffer.release(); // Release last reference
|
||||
}
|
||||
} else {
|
||||
// Write the same packet for each individual players
|
||||
for (Player player : players) {
|
||||
|
||||
// Verify if the player should receive the packet
|
||||
if (playerValidator != null && !playerValidator.isValid(player))
|
||||
continue;
|
||||
@ -154,7 +143,7 @@ public final class PacketUtils {
|
||||
* @param packet the packet to write into {@code buf}
|
||||
*/
|
||||
public static void writePacket(@NotNull ByteBuf buf, @NotNull ServerPacket packet) {
|
||||
Utils.writeVarIntBuf(buf, packet.getId());
|
||||
Utils.writeVarInt(buf, packet.getId());
|
||||
writePacketPayload(buf, packet);
|
||||
}
|
||||
|
||||
@ -184,14 +173,13 @@ public final class PacketUtils {
|
||||
public static void frameBuffer(@NotNull ByteBuf packetBuffer, @NotNull ByteBuf frameTarget) {
|
||||
final int packetSize = packetBuffer.readableBytes();
|
||||
final int headerSize = Utils.getVarIntSize(packetSize);
|
||||
|
||||
if (headerSize > 3) {
|
||||
throw new IllegalStateException("Unable to fit " + headerSize + " into 3");
|
||||
}
|
||||
|
||||
frameTarget.ensureWritable(packetSize + headerSize);
|
||||
|
||||
Utils.writeVarIntBuf(frameTarget, packetSize);
|
||||
Utils.writeVarInt(frameTarget, 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) {
|
||||
final int packetLength = packetBuffer.readableBytes();
|
||||
final boolean compression = packetLength > MinecraftServer.getCompressionThreshold();
|
||||
Utils.writeVarIntBuf(compressionTarget, compression ? packetLength : 0);
|
||||
Utils.writeVarInt(compressionTarget, compression ? packetLength : 0);
|
||||
if (compression) {
|
||||
compress(compressor, packetBuffer, compressionTarget);
|
||||
} else {
|
||||
@ -226,54 +214,35 @@ public final class PacketUtils {
|
||||
public static void writeFramedPacket(@NotNull ByteBuf buffer,
|
||||
@NotNull ServerPacket serverPacket) {
|
||||
final int compressionThreshold = MinecraftServer.getCompressionThreshold();
|
||||
final boolean compression = compressionThreshold > 0;
|
||||
|
||||
if (compression) {
|
||||
// Dummy var-int
|
||||
final int packetLengthIndex = Utils.writeEmptyVarIntHeader(buffer);
|
||||
final int dataLengthIndex = Utils.writeEmptyVarIntHeader(buffer);
|
||||
// Index of the var-int containing the complete packet length
|
||||
final int packetLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
|
||||
final int startIndex = buffer.writerIndex(); // Index where the content starts (after length)
|
||||
if (compressionThreshold > 0) {
|
||||
// Index of the uncompressed payload length
|
||||
final int dataLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
|
||||
|
||||
// Write packet
|
||||
final int contentIndex = buffer.writerIndex();
|
||||
writePacket(buffer, serverPacket);
|
||||
final int afterIndex = buffer.writerIndex();
|
||||
final int packetSize = (afterIndex - dataLengthIndex) - Utils.VARINT_HEADER_SIZE;
|
||||
final int packetSize = buffer.writerIndex() - contentIndex;
|
||||
|
||||
if (packetSize >= compressionThreshold) {
|
||||
// Packet large enough
|
||||
|
||||
final VelocityCompressor compressor = COMPRESSOR.get();
|
||||
// Compress id + payload
|
||||
final int uncompressedLength = packetSize >= compressionThreshold ? packetSize : 0;
|
||||
Utils.write3BytesVarInt(buffer, dataLengthIndex, uncompressedLength);
|
||||
if (uncompressedLength > 0) {
|
||||
// Packet large enough, compress
|
||||
ByteBuf uncompressedCopy = buffer.copy(contentIndex, packetSize);
|
||||
buffer.writerIndex(contentIndex);
|
||||
compress(compressor, uncompressedCopy, buffer);
|
||||
compress(COMPRESSOR.get(), uncompressedCopy, buffer);
|
||||
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 {
|
||||
// No compression
|
||||
|
||||
// Write dummy var-int
|
||||
final int index = Utils.writeEmptyVarIntHeader(buffer);
|
||||
|
||||
// Write packet id + payload
|
||||
// No compression, write packet id + payload
|
||||
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>
|
||||
* 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.
|
||||
*
|
||||
* @param serverPacket the server packet to write
|
||||
*/
|
||||
@NotNull
|
||||
public static ByteBuf createFramedPacket(@NotNull ServerPacket serverPacket, boolean directBuffer) {
|
||||
ByteBuf packetBuf = directBuffer ? BufUtils.getBuffer(true) : Unpooled.buffer();
|
||||
public static @NotNull ByteBuf createFramedPacket(@NotNull ServerPacket serverPacket) {
|
||||
ByteBuf packetBuf = BufUtils.direct();
|
||||
writeFramedPacket(packetBuf, serverPacket);
|
||||
return packetBuf;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,11 +10,7 @@ import java.util.UUID;
|
||||
|
||||
public final class Utils {
|
||||
|
||||
// Do NOT modify
|
||||
public static final int VARINT_HEADER_SIZE = 3;
|
||||
|
||||
private Utils() {
|
||||
|
||||
}
|
||||
|
||||
public static int getVarIntSize(int input) {
|
||||
@ -25,70 +21,52 @@ public final class Utils {
|
||||
? 4 : 5;
|
||||
}
|
||||
|
||||
public static void writeVarIntBuf(ByteBuf buffer, int value) {
|
||||
do {
|
||||
byte temp = (byte) (value & 0b01111111);
|
||||
value >>>= 7;
|
||||
if (value != 0) {
|
||||
temp |= 0b10000000;
|
||||
}
|
||||
buffer.writeByte(temp);
|
||||
} while (value != 0);
|
||||
public static void writeVarInt(@NotNull ByteBuf buf, int value) {
|
||||
// Took from velocity
|
||||
if ((value & (0xFFFFFFFF << 7)) == 0) {
|
||||
buf.writeByte(value);
|
||||
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
|
||||
int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
|
||||
buf.writeShort(w);
|
||||
} else if ((value & (0xFFFFFFFF << 21)) == 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) {
|
||||
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) {
|
||||
public static void write3BytesVarInt(@NotNull ByteBuf buffer, int startIndex, int value) {
|
||||
final int indexCache = buffer.writerIndex();
|
||||
buffer.writerIndex(startIndex);
|
||||
|
||||
for (int i = 0; i < VARINT_HEADER_SIZE; i++) {
|
||||
byte temp = (byte) (value & 0b01111111);
|
||||
value >>>= 7;
|
||||
if (value != 0 || i != VARINT_HEADER_SIZE - 1) {
|
||||
temp |= 0b10000000;
|
||||
}
|
||||
|
||||
buffer.writeByte(temp);
|
||||
}
|
||||
|
||||
final int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
|
||||
buffer.writeMedium(w);
|
||||
buffer.writerIndex(indexCache);
|
||||
}
|
||||
|
||||
public static int writeEmptyVarIntHeader(@NotNull ByteBuf buffer) {
|
||||
public static int writeEmpty3BytesVarInt(@NotNull ByteBuf buffer) {
|
||||
final int index = buffer.writerIndex();
|
||||
buffer.writeMedium(0);
|
||||
return index;
|
||||
}
|
||||
|
||||
public static int readVarInt(ByteBuf buffer) {
|
||||
int numRead = 0;
|
||||
int result = 0;
|
||||
byte read;
|
||||
do {
|
||||
read = buffer.readByte();
|
||||
int value = (read & 0b01111111);
|
||||
result |= (value << (7 * numRead));
|
||||
|
||||
numRead++;
|
||||
if (numRead > 5) {
|
||||
throw new RuntimeException("VarInt is too big");
|
||||
public static int readVarInt(ByteBuf buf) {
|
||||
int i = 0;
|
||||
final int maxRead = Math.min(5, buf.readableBytes());
|
||||
for (int j = 0; j < maxRead; j++) {
|
||||
final int k = buf.readByte();
|
||||
i |= (k & 0x7F) << j * 7;
|
||||
if ((k & 0x80) != 128) {
|
||||
return i;
|
||||
}
|
||||
} while ((read & 0b10000000) != 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
throw new RuntimeException("VarInt is too big");
|
||||
}
|
||||
|
||||
public static long readVarLong(ByteBuf buffer) {
|
||||
public static long readVarLong(@NotNull ByteBuf buffer) {
|
||||
int numRead = 0;
|
||||
long result = 0;
|
||||
byte read;
|
||||
@ -156,14 +134,14 @@ public final class Utils {
|
||||
if (bitsPerEntry < 9) {
|
||||
// Palette has to exist
|
||||
final Short2ShortLinkedOpenHashMap paletteBlockMap = section.getPaletteBlockMap();
|
||||
writeVarIntBuf(buffer, paletteBlockMap.size());
|
||||
writeVarInt(buffer, paletteBlockMap.size());
|
||||
for (short paletteValue : paletteBlockMap.values()) {
|
||||
writeVarIntBuf(buffer, paletteValue);
|
||||
writeVarInt(buffer, paletteValue);
|
||||
}
|
||||
}
|
||||
|
||||
final long[] blocks = section.getBlocks();
|
||||
writeVarIntBuf(buffer, blocks.length);
|
||||
writeVarInt(buffer, blocks.length);
|
||||
for (long datum : blocks) {
|
||||
buffer.writeLong(datum);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import org.jglrxavpok.hephaistos.nbt.NBTWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
@ -151,7 +152,7 @@ public class BinaryWriter extends OutputStream {
|
||||
* @param i the int to write
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Simply a writeSizedString with message.toString()
|
||||
|
@ -63,7 +63,7 @@ public interface CacheablePacket {
|
||||
|
||||
if (shouldUpdate) {
|
||||
// Buffer freed by guava cache #removalListener
|
||||
final ByteBuf buffer = PacketUtils.createFramedPacket(serverPacket, true);
|
||||
final ByteBuf buffer = PacketUtils.createFramedPacket(serverPacket);
|
||||
timedBuffer = new TimedBuffer(buffer, timestamp);
|
||||
temporaryCache.cache(identifier, timedBuffer);
|
||||
}
|
||||
|
@ -161,18 +161,42 @@ public final class ChunkUtils {
|
||||
* @param range how far should it retrieves chunk
|
||||
* @return an array containing chunks index
|
||||
*/
|
||||
@NotNull
|
||||
public static long[] getChunksInRange(@NotNull Position position, int range) {
|
||||
range = range * 2;
|
||||
long[] visibleChunks = new long[MathUtils.square(range + 1)];
|
||||
final int startLoop = -(range / 2);
|
||||
final int endLoop = range / 2 + 1;
|
||||
int counter = 0;
|
||||
for (int x = startLoop; x < endLoop; x++) {
|
||||
for (int z = startLoop; z < endLoop; z++) {
|
||||
final int chunkX = getChunkCoordinate(position.getX() + Chunk.CHUNK_SIZE_X * x);
|
||||
final int chunkZ = getChunkCoordinate(position.getZ() + Chunk.CHUNK_SIZE_Z * z);
|
||||
visibleChunks[counter++] = getChunkIndex(chunkX, chunkZ);
|
||||
public static @NotNull long[] getChunksInRange(@NotNull Position position, int range) {
|
||||
long[] visibleChunks = new long[MathUtils.square(range * 2 + 1)];
|
||||
int xDistance = 0;
|
||||
int xDirection = 1;
|
||||
int zDistance = 0;
|
||||
int zDirection = -1;
|
||||
int len = 1;
|
||||
int corner = 0;
|
||||
|
||||
for (int i = 0; i < visibleChunks.length; i++) {
|
||||
final int chunkX = getChunkCoordinate(xDistance * Chunk.CHUNK_SIZE_X + position.getX());
|
||||
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;
|
||||
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -7,21 +7,22 @@ import demo.commands.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.CommandManager;
|
||||
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.instance.block.BlockManager;
|
||||
import net.minestom.server.instance.block.rule.vanilla.RedstonePlacementRule;
|
||||
import net.minestom.server.ping.ResponseData;
|
||||
import net.minestom.server.storage.StorageManager;
|
||||
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.UpdateOption;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
@ -66,37 +67,32 @@ public class Main {
|
||||
|
||||
MinecraftServer.getGlobalEventHandler().addEventCallback(ServerListPingEvent.class, event -> {
|
||||
ResponseData responseData = event.getResponseData();
|
||||
responseData.setMaxPlayer(0);
|
||||
responseData.setOnline(MinecraftServer.getConnectionManager().getOnlinePlayers().size());
|
||||
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.addEntry(NamedAndIdentified.named("The first line is separated from the others"));
|
||||
responseData.addEntry(NamedAndIdentified.named("Could be a name, or a message"));
|
||||
|
||||
responseData.addPlayer("Connection Info:");
|
||||
String ip = event.getConnection().getServerAddress();
|
||||
responseData.addPlayer((char)0x00a7 + "8- " + (char)0x00a7 +"7IP: " + (char)0x00a7 + "e" + (ip != null ? ip : "???"));
|
||||
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());
|
||||
// on modern versions, you can obtain the player connection directly from the event
|
||||
if (event.getConnection() != null) {
|
||||
responseData.addEntry(NamedAndIdentified.named("IP test: " + event.getConnection().getRemoteAddress().toString()));
|
||||
|
||||
// Check if client supports RGB color
|
||||
if (event.getConnection().getProtocolVersion() >= 713) { // Snapshot 20w17a
|
||||
responseData.setDescription(Component.text("You can do ")
|
||||
.append(Component.text("RGB", TextColor.color(0x66b3ff)))
|
||||
.append(Component.text(" color here")));
|
||||
} else {
|
||||
responseData.setDescription(Component.text("You can do ")
|
||||
.append(Component.text("RGB", NamedTextColor.nearestTo(TextColor.color(0x66b3ff))))
|
||||
.append(Component.text(" color here,"))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("if you are on 1.16 or up"))
|
||||
);
|
||||
responseData.addEntry(NamedAndIdentified.named("Connection Info:"));
|
||||
String ip = event.getConnection().getServerAddress();
|
||||
responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
|
||||
.append(Component.text(" IP: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(ip != null ? ip : "???", NamedTextColor.YELLOW))));
|
||||
responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
|
||||
.append(Component.text(" PORT: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(event.getConnection().getServerPort()))));
|
||||
responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
|
||||
.append(Component.text(" VERSION: ", NamedTextColor.GRAY))
|
||||
.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();
|
||||
@ -108,8 +104,10 @@ public class Main {
|
||||
|
||||
//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);
|
||||
//Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,17 +3,22 @@ package demo.commands;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
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.minecraft.registry.ArgumentEntityType;
|
||||
import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeVec3;
|
||||
import net.minestom.server.command.builder.condition.Conditions;
|
||||
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;
|
||||
|
||||
public class SummonCommand extends Command {
|
||||
|
||||
private final ArgumentEntityType entity;
|
||||
private final ArgumentRelativeVec3 pos;
|
||||
private final ArgumentEnum<EntityClass> entityClass;
|
||||
|
||||
public SummonCommand() {
|
||||
super("summon");
|
||||
@ -21,12 +26,36 @@ public class SummonCommand extends Command {
|
||||
|
||||
entity = ArgumentType.EntityType("entity type");
|
||||
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) {
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ public class TestCommand extends Command {
|
||||
super("testcmd");
|
||||
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) {
|
||||
|
Loading…
Reference in New Issue
Block a user