Merge remote-tracking branch 'upstream/master' into position-cleanup

This commit is contained in:
Németh Noel 2021-05-05 18:26:57 +02:00
commit 2f1ada9a9f
21 changed files with 1651 additions and 184 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,21 +7,22 @@ import demo.commands.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.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));
}
}