Implement OpenToLAN system

This commit is contained in:
Kieran Wallbanks 2021-04-23 13:38:50 +01:00
parent 009f7cb1da
commit 01fe452783
5 changed files with 228 additions and 3 deletions

View File

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

View File

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

View File

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

View File

@ -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 <b>not</b> cancellable.
*/
OPEN_TO_LAN(ServerListPingVersion::getOpenToLANPing);
private final Function<ResponseData, String> 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-).
*

View File

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