mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-31 21:48:08 +01:00
Add Query system
This commit is contained in:
parent
c92829e3cf
commit
94ecb8de7b
217
src/main/java/net/minestom/server/extras/query/Query.java
Normal file
217
src/main/java/net/minestom/server/extras/query/Query.java
Normal file
@ -0,0 +1,217 @@
|
||||
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.response.QueryResponse;
|
||||
import net.minestom.server.timer.Task;
|
||||
import net.minestom.server.utils.InetAddressWithPort;
|
||||
import net.minestom.server.utils.NetworkUtils;
|
||||
import net.minestom.server.utils.binary.BinaryWriter;
|
||||
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.SocketException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Utility class to manage responses to the UT3 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 volatile boolean started;
|
||||
|
||||
private static volatile DatagramSocket socket;
|
||||
private static volatile Thread thread;
|
||||
|
||||
private static final Int2ObjectMap<InetAddressWithPort> CHALLENGE_TOKENS = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());
|
||||
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, new InetAddressWithPort(packet.getAddress(), packet.getPort()));
|
||||
|
||||
// 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.getAddress(), packet.getPort()));
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("An error occurred whilst sending a query handshake packet.", e);
|
||||
}
|
||||
} else if (type == 0) { // stat
|
||||
int sessionID = data.readInt();
|
||||
int challengeToken = data.readInt();
|
||||
InetAddressWithPort sender = new InetAddressWithPort(packet.getAddress(), packet.getPort());
|
||||
|
||||
if (CHALLENGE_TOKENS.containsKey(challengeToken) && CHALLENGE_TOKENS.get(challengeToken).equals(sender)) {
|
||||
int remaining = data.readableBytes();
|
||||
|
||||
if (remaining == 0) { // basic
|
||||
BasicQueryEvent event = new BasicQueryEvent(new InetAddressWithPort(packet.getAddress(), packet.getPort()));
|
||||
MinecraftServer.getGlobalEventHandler().callCancellableEvent(BasicQueryEvent.class, event,
|
||||
() -> sendResponse(event.getQueryResponse(), sessionID, sender));
|
||||
} else if (remaining == 8) { // full
|
||||
BasicQueryEvent event = new BasicQueryEvent(new InetAddressWithPort(packet.getAddress(), packet.getPort()));
|
||||
MinecraftServer.getGlobalEventHandler().callCancellableEvent(BasicQueryEvent.class, event,
|
||||
() -> sendResponse(event.getQueryResponse(), sessionID, sender));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendResponse(@NotNull QueryResponse queryResponse, int sessionID, @NotNull InetAddressWithPort 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.getInetAddress(), sender.getPort()));
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("An error occurred whilst sending a query handshake packet.", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.minestom.server.extras.query.event;
|
||||
|
||||
import net.minestom.server.extras.query.response.BasicQueryResponse;
|
||||
import net.minestom.server.utils.InetAddressWithPort;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* 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 sender the sender
|
||||
*/
|
||||
public BasicQueryEvent(@NotNull InetAddressWithPort sender) {
|
||||
super(sender, new BasicQueryResponse());
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.minestom.server.extras.query.event;
|
||||
|
||||
import net.minestom.server.extras.query.response.FullQueryResponse;
|
||||
import net.minestom.server.utils.InetAddressWithPort;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public FullQueryEvent(@NotNull InetAddressWithPort sender) {
|
||||
super(sender, new FullQueryResponse());
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package net.minestom.server.extras.query.event;
|
||||
|
||||
import net.minestom.server.event.CancellableEvent;
|
||||
import net.minestom.server.event.Event;
|
||||
import net.minestom.server.extras.query.response.QueryResponse;
|
||||
import net.minestom.server.utils.InetAddressWithPort;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
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 QueryResponse> extends Event implements CancellableEvent {
|
||||
private final InetAddressWithPort sender;
|
||||
|
||||
private T response;
|
||||
private boolean cancelled;
|
||||
|
||||
/**
|
||||
* Creates a new query event.
|
||||
*
|
||||
* @param sender the sender
|
||||
* @param response the initial response
|
||||
*/
|
||||
public QueryEvent(@NotNull InetAddressWithPort sender, @NotNull T response) {
|
||||
this.sender = sender;
|
||||
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 IP address and port of the initiator of the query.
|
||||
*
|
||||
* @return the initiator
|
||||
*/
|
||||
public @NotNull InetAddressWithPort getSender() {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCancelled(boolean cancel) {
|
||||
this.cancelled = cancel;
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A basic query response containing a fixed set of responses.
|
||||
*/
|
||||
public class BasicQueryResponse implements QueryResponse {
|
||||
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,160 @@
|
||||
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 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 QueryResponse {
|
||||
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_10);
|
||||
|
||||
// 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_11);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.minestom.server.extras.query.response;
|
||||
|
||||
import net.minestom.server.utils.binary.BinaryWriter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A query response.
|
||||
*/
|
||||
public interface QueryResponse {
|
||||
|
||||
/**
|
||||
* Writes the query response to a writer.
|
||||
*
|
||||
* @param writer the writer to write the response to
|
||||
*/
|
||||
void write(@NotNull BinaryWriter writer);
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package net.minestom.server.utils;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A utility class to hold an {@link InetAddress} and a port.
|
||||
*/
|
||||
public class InetAddressWithPort {
|
||||
private final InetAddress inetAddress;
|
||||
private final int port;
|
||||
|
||||
/**
|
||||
* Creates a new {@link InetAddressWithPort}.
|
||||
*
|
||||
* @param inetAddress the inet address
|
||||
* @param port the port
|
||||
*/
|
||||
public InetAddressWithPort(@NotNull InetAddress inetAddress, int port) {
|
||||
Validate.inclusiveBetween(1, 65535, port, "port must be a valid port");
|
||||
|
||||
this.inetAddress = Objects.requireNonNull(inetAddress, "inetAddress");
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the inet address.
|
||||
*
|
||||
* @return the inet address
|
||||
*/
|
||||
public @NotNull InetAddress getInetAddress() {
|
||||
return this.inetAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the port.
|
||||
*
|
||||
* @return the port
|
||||
*/
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
InetAddressWithPort that = (InetAddressWithPort) o;
|
||||
return port == that.port && Objects.equals(inetAddress, that.inetAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(inetAddress, port);
|
||||
}
|
||||
}
|
@ -3,6 +3,9 @@ package net.minestom.server.utils.binary;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.adventure.AdventureSerializer;
|
||||
@ -176,6 +179,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()
|
||||
|
@ -14,6 +14,7 @@ import net.minestom.server.event.server.ServerListPingEvent;
|
||||
import net.minestom.server.extras.lan.OpenToLAN;
|
||||
import net.minestom.server.extras.lan.OpenToLANConfig;
|
||||
import net.minestom.server.extras.optifine.OptifineSupport;
|
||||
import net.minestom.server.extras.query.Query;
|
||||
import net.minestom.server.instance.block.BlockManager;
|
||||
import net.minestom.server.instance.block.rule.vanilla.RedstonePlacementRule;
|
||||
import net.minestom.server.ping.ResponseData;
|
||||
@ -110,6 +111,8 @@ public class Main {
|
||||
|
||||
minecraftServer.start("0.0.0.0", 25565);
|
||||
//Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly));
|
||||
|
||||
Query.start(25566);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user