diff --git a/src/main/java/net/minestom/server/event/server/ServerListPingEvent.java b/src/main/java/net/minestom/server/event/server/ServerListPingEvent.java index 8a2ce23b8..346a94379 100644 --- a/src/main/java/net/minestom/server/event/server/ServerListPingEvent.java +++ b/src/main/java/net/minestom/server/event/server/ServerListPingEvent.java @@ -39,7 +39,7 @@ public class ServerListPingEvent extends Event implements CancellableEvent { * @param version the ping version to respond with */ public ServerListPingEvent(@Nullable PlayerConnection connection, @NotNull ServerListPingVersion version) { - //noinspection deprecation we need to continue doing this until the consumer is removed + //noinspection deprecation we need to continue doing this until the consumer is removed - todo remove ResponseDataConsumer consumer = MinecraftServer.getResponseDataConsumer(); this.responseData = new ResponseData(); @@ -97,7 +97,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 ServerListPingVersion#OPEN_TO_LAN}. * * @param cancel true if the event should be cancelled, false otherwise */ diff --git a/src/main/java/net/minestom/server/extras/lan/OpenToLAN.java b/src/main/java/net/minestom/server/extras/lan/OpenToLAN.java new file mode 100644 index 000000000..6e37919b2 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/lan/OpenToLAN.java @@ -0,0 +1,135 @@ +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.time.Cooldown; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.*; +import java.nio.charset.StandardCharsets; + +import static net.minestom.server.ping.ServerListPingVersion.OPEN_TO_LAN; + +/** + * Utility class to manage opening the server to LAN. Note that this doesn't 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 wiki.vg + */ +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 open 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 open successfully, {@code false} otherwise + */ + public static boolean open(@NotNull OpenToLANConfig config) { + if (socket != null) { + return false; + } else { + int port = config.port; + + if (port == 0) { + try { + final ServerSocket socket = new ServerSocket(0); + port = socket.getLocalPort(); + socket.close(); + } 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); + } + } + } +} diff --git a/src/main/java/net/minestom/server/extras/lan/OpenToLANConfig.java b/src/main/java/net/minestom/server/extras/lan/OpenToLANConfig.java new file mode 100644 index 000000000..9549621ed --- /dev/null +++ b/src/main/java/net/minestom/server/extras/lan/OpenToLANConfig.java @@ -0,0 +1,64 @@ +package net.minestom.server.extras.lan; + +import java.util.Objects; + +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; + +/** + * 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 setPort(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 setDelayBetweenPings(@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 setDelayBetweenEventCalls(@NotNull UpdateOption delay) { + this.delayBetweenEvent = Objects.requireNonNull(delay, "delay"); + return this; + } +} diff --git a/src/main/java/net/minestom/server/ping/ServerListPingVersion.java b/src/main/java/net/minestom/server/ping/ServerListPingVersion.java index 23f0657b4..88fc5ec1f 100644 --- a/src/main/java/net/minestom/server/ping/ServerListPingVersion.java +++ b/src/main/java/net/minestom/server/ping/ServerListPingVersion.java @@ -5,6 +5,7 @@ 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.extras.lan.OpenToLAN; import net.minestom.server.utils.identity.NamedAndIdentified; import org.jetbrains.annotations.NotNull; @@ -34,7 +35,14 @@ public enum ServerListPingVersion { /** * The client is on version 1.5 or lower and supports a description and the player count. */ - LEGACY(data -> getLegacyPingResponse(data, false)); + LEGACY(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 not cancellable. + */ + OPEN_TO_LAN(ServerListPingVersion::getOpenToLANPing); private final Function pingResponseCreator; @@ -52,10 +60,22 @@ public enum ServerListPingVersion { 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-). * diff --git a/src/test/java/demo/Main.java b/src/test/java/demo/Main.java index 844574b21..29f4621b0 100644 --- a/src/test/java/demo/Main.java +++ b/src/test/java/demo/Main.java @@ -11,6 +11,8 @@ 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; @@ -103,6 +105,9 @@ 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().setDelayBetweenEventCalls(new UpdateOption(1, TimeUnit.DAY))); + minecraftServer.start("0.0.0.0", 25565); //Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly)); }