diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java index 6ef86d61e..3841e293b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java @@ -77,8 +77,10 @@ public class ChunkToLodBuilder implements AutoCloseable { return; } - else if (!MC.playerExists()) + else if (MC == null || !MC.playerExists()) { + // TODO handle server side properly + // MC hasn't finished loading (or is currently unloaded) // can be uncommented if tasks aren't being cleared correctly diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/LodClient.java b/core/src/main/java/com/seibel/distanthorizons/core/network/LodClient.java deleted file mode 100644 index 9bab5d702..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/LodClient.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.seibel.distanthorizons.core.network; - -import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -//import net.minecraft.client.Minecraft; - -public class LodClient { -// public static final LodClient INSTANCE = new LodClient(); -// -// private final Minecraft client; -// private final NetworkHandler networkHandler; -// -// private LodClient() { -// client = Minecraft.getInstance(); -// this.networkHandler = new NetworkHandler(); -// } - - public void connect() { - - } - - public void disconnect() { - - } -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/LodServer.java b/core/src/main/java/com/seibel/distanthorizons/core/network/LodServer.java deleted file mode 100644 index 0866ecaa7..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/LodServer.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.seibel.distanthorizons.core.network; - -import com.seibel.distanthorizons.core.network.protocol.*; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -import io.netty.handler.codec.LengthFieldPrepender; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; -import org.jetbrains.annotations.NotNull; - -public class LodServer { - // TODO move to config of some sort - static final int PORT = 25049; - - public LodServer(/* initial settings */) { - - } - - public void start() throws InterruptedException { - EventLoopGroup bossGroup = new NioEventLoopGroup(1); - EventLoopGroup workerGroup = new NioEventLoopGroup(); - try { - ServerBootstrap b = new ServerBootstrap(); - b.group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .handler(new LoggingHandler(LogLevel.INFO)) - .childHandler(getInitializer()); - - b.bind(PORT).sync().channel().closeFuture().sync(); - } finally { - bossGroup.shutdownGracefully(); - workerGroup.shutdownGracefully(); - } - } - - public void stop() { - - } - - private ChannelInitializer getInitializer() { - return new ChannelInitializer() { - @Override - public void initChannel(@NotNull SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); - - MessageRegistry messageRegistry = new MessageRegistry(); - - // Encoding - pipeline.addLast(new LengthFieldBasedFrameDecoder(Short.MAX_VALUE, 0, Short.BYTES, 0, Short.BYTES)); - pipeline.addLast(new MessageDecoder(messageRegistry)); - - // Encoder - pipeline.addLast(new LengthFieldPrepender(Short.BYTES)); - pipeline.addLast(new MessageEncoder(messageRegistry)); - - pipeline.addLast(new MessageHandler(MessageHandlerSide.SERVER)); - } - }; - } -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/NetworkClient.java b/core/src/main/java/com/seibel/distanthorizons/core/network/NetworkClient.java new file mode 100644 index 000000000..6ed422cc6 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/NetworkClient.java @@ -0,0 +1,77 @@ +package com.seibel.distanthorizons.core.network; + +import com.seibel.distanthorizons.core.network.messages.HelloMessage; +import com.seibel.distanthorizons.core.util.NetworkUtil; +import com.seibel.distanthorizons.core.network.protocol.MessageHandlerSide; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class NetworkClient implements Closeable { + private enum State { + OPEN, + RECONNECT, + CLOSED + } + + private static final int FAILURE_RECONNECT_DELAY_SEC = 5; + + // TODO move to config of some sort + private final String host; + private final int port; + private State state = State.OPEN; + + private final Bootstrap clientBootstrap; + private final EventLoopGroup workerGroup = new NioEventLoopGroup(); + private Channel channel; + + public NetworkClient(String host, int port) { + this.host = host; + this.port = port; + + clientBootstrap = new Bootstrap() + .group(workerGroup) + .channel(NioSocketChannel.class) + .option(ChannelOption.SO_KEEPALIVE, true) + .handler(NetworkUtil.getChannelInitializer(MessageHandlerSide.CLIENT)); + connect(); + } + + private void connect() { + ChannelFuture connectFuture = clientBootstrap.connect(host, port); + connectFuture.addListener((ChannelFuture channelFuture) -> { + if (!channelFuture.isSuccess()) return; + + channel = channelFuture.channel(); + channel.writeAndFlush(new HelloMessage()); + }); + + channel = connectFuture.channel(); + channel.closeFuture().addListener((ChannelFuture channelFuture) -> { + if (state == State.CLOSED) return; + + workerGroup.schedule(this::connect, state == State.RECONNECT ? 0 : FAILURE_RECONNECT_DELAY_SEC, TimeUnit.SECONDS); + state = State.OPEN; + }); + } + + /** Kills the current connection, triggering auto-reconnection immediately. */ + public void reconnect() { + state = State.RECONNECT; + channel.disconnect(); + } + + @Override + public void close() throws IOException { + state = State.CLOSED; + workerGroup.shutdownGracefully(); + } +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/NetworkServer.java b/core/src/main/java/com/seibel/distanthorizons/core/network/NetworkServer.java new file mode 100644 index 000000000..3149d7e1a --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/NetworkServer.java @@ -0,0 +1,45 @@ +package com.seibel.distanthorizons.core.network; + +import com.seibel.distanthorizons.core.network.protocol.*; +import com.seibel.distanthorizons.core.util.NetworkUtil; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +import java.io.Closeable; +import java.io.IOException; + +public class NetworkServer implements Closeable { + // TODO move to config of some sort + private final int port; + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + public NetworkServer(int port) { + this.port = port; + + ServerBootstrap b = new ServerBootstrap() + .group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(NetworkUtil.getChannelInitializer(MessageHandlerSide.SERVER)); + + b.bind(port) + .addListener((ChannelFuture channelFuture) -> { + if (!channelFuture.isSuccess()) + throw new RuntimeException("Failed to bind: " + channelFuture); + }) + .channel().closeFuture().addListener(future -> close()); + } + + @Override + public void close() throws IOException { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/HelloMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/HelloMessage.java index d5b64398a..c790e72a9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/HelloMessage.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/HelloMessage.java @@ -1,12 +1,17 @@ package com.seibel.distanthorizons.core.network.messages; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.coreapi.ModInfo; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; // This message is critical to maintain backwards compatibility // as it's used to receive version before everything else. public class HelloMessage extends Message { + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + public int version = ModInfo.PROTOCOL_VERSION; @Override @@ -21,6 +26,11 @@ public class HelloMessage extends Message { @Override public void handle_Server(ChannelHandlerContext ctx) { - // TODO Adjust message handling to client's version + LOGGER.log(Level.INFO, "Client version: " + version); + } + + @Override + public void handle_Client(ChannelHandlerContext ctx) { + LOGGER.log(Level.INFO, "Server version: " + version); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageDecoder.java b/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageDecoder.java index de2b4c7d1..82b653c56 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageDecoder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageDecoder.java @@ -8,15 +8,9 @@ import io.netty.handler.codec.ByteToMessageDecoder; import java.util.List; public class MessageDecoder extends ByteToMessageDecoder { - private final MessageRegistry messageRegistry; - - public MessageDecoder(MessageRegistry messageRegistry) { - this.messageRegistry = messageRegistry; - } - @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { - Message message = messageRegistry.createMessage(in.readShort()); + Message message = MessageRegistry.INSTANCE.createMessage(in.readShort()); message.decode(ctx, in); out.add(message); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageEncoder.java b/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageEncoder.java index 43603e25a..5b491ccc7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageEncoder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageEncoder.java @@ -6,15 +6,9 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; public class MessageEncoder extends MessageToByteEncoder { - private final MessageRegistry messageRegistry; - - public MessageEncoder(MessageRegistry messageRegistry) { - this.messageRegistry = messageRegistry; - } - @Override protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws IllegalArgumentException { - out.writeShort(messageRegistry.getMessageId(msg)); + out.writeShort(MessageRegistry.INSTANCE.getMessageId(msg)); msg.encode(ctx, out); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageRegistry.java b/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageRegistry.java index f6f702461..e02ba0b20 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageRegistry.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageRegistry.java @@ -6,24 +6,27 @@ import com.seibel.distanthorizons.core.network.messages.Message; import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; -import java.util.stream.Collectors; public class MessageRegistry { - private final Map> idToConstructor = new HashMap>() {{ + public static final MessageRegistry INSTANCE = new MessageRegistry() {{ // Note: Make sure IDs are unique // Also note: Removing IDs will break backwards compatibility - put(1, HelloMessage::new); + registerMessage(1, HelloMessage.class, HelloMessage::new); }}; - private final Map, Integer> classToId = idToConstructor.entrySet().stream() - .collect(Collectors.toMap( - e -> e.getValue().getClass(), - Map.Entry::getKey - )); + private final Map> idToSupplier = new HashMap<>(); + private final Map, Integer> classToId = new HashMap<>(); + + private MessageRegistry() { } + + public void registerMessage(int id, Class clazz, Supplier supplier) { + idToSupplier.put(id, (Supplier) supplier); + classToId.put(clazz, id); + } public Message createMessage(int id) throws IllegalArgumentException { try { - return idToConstructor.get(id).get(); + return idToSupplier.get(id).get(); } catch (NullPointerException e) { throw new IllegalArgumentException("Invalid message ID"); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/NetworkUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/NetworkUtil.java new file mode 100644 index 000000000..a67d68bb1 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/NetworkUtil.java @@ -0,0 +1,30 @@ +package com.seibel.distanthorizons.core.util; + +import com.seibel.distanthorizons.core.network.protocol.*; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import org.jetbrains.annotations.NotNull; + +public class NetworkUtil { + public static ChannelInitializer getChannelInitializer(MessageHandlerSide side) { + return new ChannelInitializer() { + @Override + public void initChannel(@NotNull SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // Encoding + pipeline.addLast(new LengthFieldBasedFrameDecoder(Short.MAX_VALUE, 0, Short.BYTES, 0, Short.BYTES)); + pipeline.addLast(new MessageDecoder()); + + // Encoder + pipeline.addLast(new LengthFieldPrepender(Short.BYTES)); + pipeline.addLast(new MessageEncoder()); + + pipeline.addLast(new MessageHandler(side)); + } + }; + } +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java index 08b12a129..90247a8e8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java @@ -3,6 +3,7 @@ package com.seibel.distanthorizons.core.world; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.DhClientLevel; +import com.seibel.distanthorizons.core.network.NetworkClient; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.EventLoop; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; @@ -17,6 +18,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld { private final HashMap levels; public final ClientOnlySaveStructure saveStructure; + private final NetworkClient networkClient; // TODO why does this executor have 2 threads? public ExecutorService dhTickerThread = ThreadUtil.makeSingleThreadPool("DH Client World Ticker Thread", 2); @@ -27,8 +29,11 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld public DhClientWorld() { super(EWorldEnvironment.Client_Only); - this.saveStructure = new ClientOnlySaveStructure(); + + this.saveStructure = new ClientOnlySaveStructure(); this.levels = new HashMap<>(); + this.networkClient = new NetworkClient("127.0.0.1", 25049); + LOGGER.info("Started DhWorld of type "+this.environment); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java index aa0574629..c32dcbe3d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java @@ -3,6 +3,7 @@ package com.seibel.distanthorizons.core.world; import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure; import com.seibel.distanthorizons.core.level.DhServerLevel; import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.network.NetworkServer; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; @@ -15,7 +16,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld { private final HashMap levels; public final LocalSaveStructure saveStructure; - + private final NetworkServer networkServer; public DhServerWorld() @@ -24,6 +25,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld this.saveStructure = new LocalSaveStructure(); this.levels = new HashMap<>(); + this.networkServer = new NetworkServer(25049); LOGGER.info("Started "+DhServerWorld.class.getSimpleName()+" of type "+this.environment); }