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