mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-31 21:48:08 +01:00
Merge remote-tracking branch 'upstream/master' into position-cleanup
This commit is contained in:
commit
2f1ada9a9f
@ -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<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.player.PlayerUtils;
|
||||
@ -90,12 +91,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();
|
||||
@ -180,6 +182,7 @@ 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);
|
||||
@ -1275,12 +1278,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;
|
||||
}
|
||||
|
||||
@ -1292,6 +1309,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) {
|
||||
@ -1415,7 +1433,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));
|
||||
|
||||
@ -1425,6 +1442,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
|
||||
sendPacketToViewersAndSelf(getPropertiesPacket());
|
||||
sendPacketToViewersAndSelf(getEquipmentsPacket());
|
||||
|
||||
getInventory().update();
|
||||
|
||||
{
|
||||
// Send new chunks
|
||||
final BlockPosition pos = position.toBlockPosition();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,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;
|
||||
}
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -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()
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user