Merge branch 'distant-horizons-core-main'

This commit is contained in:
James Seibel
2023-07-11 18:50:47 -05:00
27 changed files with 942 additions and 13 deletions
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.world.AbstractDhWorld;
@@ -36,9 +37,6 @@ import org.apache.logging.log4j.Logger;
/**
* This holds the methods that should be called by the host mod loader (Fabric,
* Forge, etc.). Specifically server events.
*
* @author James Seibel
* @version 2022-9-16
*/
public class ServerApi
{
@@ -160,4 +158,23 @@ public class ServerApi
}
}
public void serverPlayerJoinEvent(IServerPlayerWrapper player)
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{
LOGGER.debug("Waiting for player to connect: " + player.getUUID());
((DhServerWorld) serverWorld).addPlayer(player);
}
}
public void serverPlayerDisconnectEvent(IServerPlayerWrapper player)
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{
LOGGER.debug("Removing player from connect wait list: " + player.getUUID());
((DhServerWorld) serverWorld).removePlayer(player);
}
}
}
@@ -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
@@ -0,0 +1,154 @@
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.CloseMessage;
import com.seibel.distanthorizons.core.network.messages.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.NetworkChannelInitializer;
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 org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
public class NetworkClient extends NetworkEventSource implements AutoCloseable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private enum EConnectionState
{
OPEN,
RECONNECT,
RECONNECT_FORCE,
CLOSE_WAIT,
CLOSED
}
private static final int FAILURE_RECONNECT_DELAY_SEC = 5;
private static final int FAILURE_RECONNECT_ATTEMPTS = 5;
// TODO move to payload of some sort
private final InetSocketAddress address;
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
private final Bootstrap clientBootstrap = new Bootstrap()
.group(this.workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new NetworkChannelInitializer(this.messageHandler));
private EConnectionState connectionState;
private Channel channel;
private int reconnectAttempts = FAILURE_RECONNECT_ATTEMPTS;
public NetworkClient(String host, int port)
{
this.address = new InetSocketAddress(host, port);
this.registerHandlers();
this.connect();
}
private void registerHandlers()
{
this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
{
LOGGER.info("Connected to server: "+channelContext.channel().remoteAddress());
});
this.registerHandler(CloseReasonMessage.class, (closeReasonMessage, channelContext) ->
{
LOGGER.info(closeReasonMessage.reason);
this.connectionState = EConnectionState.CLOSE_WAIT;
});
this.registerHandler(CloseMessage.class, (closeMessage, channelContext) ->
{
LOGGER.info("Disconnected from server: "+channelContext.channel().remoteAddress());
if (this.connectionState == EConnectionState.CLOSE_WAIT)
{
this.close();
}
});
}
private void connect()
{
LOGGER.info("Connecting to server: "+this.address);
this.connectionState = EConnectionState.OPEN;
ChannelFuture connectFuture = this.clientBootstrap.connect(this.address);
connectFuture.addListener((ChannelFuture channelFuture) ->
{
if (!channelFuture.isSuccess())
{
LOGGER.warn("Connection failed: "+channelFuture.cause());
return;
}
this.channel.writeAndFlush(new HelloMessage());
});
this.channel = connectFuture.channel();
this. channel.closeFuture().addListener((ChannelFuture channelFuture) ->
{
switch (this.connectionState)
{
case OPEN:
this.reconnectAttempts--;
LOGGER.info("Reconnection attempts left: ["+this.reconnectAttempts+"] of ["+FAILURE_RECONNECT_ATTEMPTS+"].");
if (this.reconnectAttempts == 0)
{
this.connectionState = EConnectionState.CLOSE_WAIT;
return;
}
this.connectionState = EConnectionState.RECONNECT;
this.workerGroup.schedule(this::connect, FAILURE_RECONNECT_DELAY_SEC, TimeUnit.SECONDS);
break;
case RECONNECT_FORCE:
LOGGER.info("Reconnecting forcefully.");
this.reconnectAttempts = FAILURE_RECONNECT_ATTEMPTS;
this.connectionState = EConnectionState.RECONNECT;
this.workerGroup.schedule(this::connect, 0, TimeUnit.SECONDS);
break;
}
});
}
/** Kills the current connection, triggering auto-reconnection immediately. */
public void reconnect()
{
this.connectionState = EConnectionState.RECONNECT_FORCE;
this.channel.disconnect();
}
@Override
public void close()
{
if (this.closeReason != null)
{
LOGGER.error(this.closeReason);
}
if (this.connectionState == EConnectionState.CLOSED)
{
return;
}
this.connectionState = EConnectionState.CLOSED;
this.workerGroup.shutdownGracefully().syncUninterruptibly();
this.channel.close().syncUninterruptibly();
}
}
@@ -0,0 +1,63 @@
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.AckMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.MessageHandler;
import com.seibel.distanthorizons.coreapi.ModInfo;
import io.netty.channel.ChannelHandlerContext;
import org.apache.logging.log4j.Logger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public abstract class NetworkEventSource implements AutoCloseable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
protected final MessageHandler messageHandler = new MessageHandler();
protected String closeReason = null;
public NetworkEventSource()
{
this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
{
if (helloMessage.version != ModInfo.PROTOCOL_VERSION)
{
try
{
String closeReason = "Ignoring message from channel ["+channelContext.name()+"], due to version mismatch. Expected version: ["+ModInfo.PROTOCOL_VERSION+"], received version: ["+helloMessage.version+"].";
LOGGER.info(closeReason);
this.close(closeReason);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
});
}
public <T extends INetworkMessage> void registerHandler(Class<T> clazz, BiConsumer<T, ChannelHandlerContext> handler) { this.messageHandler.registerHandler(clazz, handler); }
public <T extends INetworkMessage> void registerAckHandler(Class<T> clazz, Consumer<ChannelHandlerContext> handler)
{
this.messageHandler.registerHandler(AckMessage.class, (ackMessage, channelContext) ->
{
if (ackMessage.messageType == clazz)
{
handler.accept(channelContext);
}
});
}
public void close(String reason) throws Exception
{
this.closeReason = reason;
this.close();
}
}
@@ -0,0 +1,103 @@
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.CloseMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.NetworkChannelInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
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 org.apache.logging.log4j.Logger;
public class NetworkServer extends NetworkEventSource implements AutoCloseable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
// TODO move to the config
private final int port;
private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;
private boolean isClosed = false;
public NetworkServer(int port)
{
this.port = port;
LOGGER.info("Starting server on port "+port);
this.registerHandlers();
this.bind();
}
private void registerHandlers()
{
this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
{
LOGGER.info("Client connected: "+channelContext.channel().remoteAddress());
channelContext.channel().writeAndFlush(new HelloMessage());
});
this.registerHandler(CloseMessage.class, (closeMessage, channelContext) ->
{
LOGGER.info("Client disconnected: "+channelContext.channel().remoteAddress());
});
}
private void bind()
{
ServerBootstrap bootstrap = new ServerBootstrap()
.group(this.bossGroup, this.workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new NetworkChannelInitializer(this.messageHandler));
ChannelFuture bindFuture = bootstrap.bind(this.port);
bindFuture.addListener((ChannelFuture channelFuture) ->
{
if (!channelFuture.isSuccess())
{
throw new RuntimeException("Failed to bind: " + channelFuture.cause());
}
LOGGER.info("Server is started on port "+this.port);
});
this.channel = bindFuture.channel();
this.channel.closeFuture().addListener(future -> this.close());
}
public void disconnectClient(ChannelHandlerContext ctx, String reason)
{
ctx.channel().config().setAutoRead(false);
ctx.writeAndFlush(new CloseReasonMessage(reason))
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void close()
{
if (this.closeReason != null)
{
LOGGER.error(this.closeReason);
}
if (this.isClosed)
{
return;
}
this.isClosed = true;
LOGGER.info("Shutting down the network server.");
this.workerGroup.shutdownGracefully().syncUninterruptibly();
this.bossGroup.shutdownGracefully().syncUninterruptibly();
LOGGER.info("Network server has been closed.");
}
}
@@ -0,0 +1,26 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.MessageRegistry;
import io.netty.buffer.ByteBuf;
/**
* Simple empty response message.
* This message is not sent automatically.
*/
public class AckMessage implements INetworkMessage
{
public Class<? extends INetworkMessage> messageType;
public AckMessage() { }
public AckMessage(Class<? extends INetworkMessage> messageType) { this.messageType = messageType; }
@Override
public void encode(ByteBuf out) { out.writeInt(MessageRegistry.INSTANCE.getMessageId(this.messageType)); }
@Override
public void decode(ByteBuf in) { this.messageType = MessageRegistry.INSTANCE.getMessageClassById(in.readInt()); }
}
@@ -0,0 +1,19 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import io.netty.buffer.ByteBuf;
/**
* This is not a "real" message, and only used to indicate a disconnection.
* To send a "disconnect reason" message, use {@link CloseReasonMessage}.
*/
public class CloseMessage implements INetworkMessage
{
@Override
public void encode(ByteBuf out) { throw new UnsupportedOperationException("CloseMessage is not a real message, and must not be sent."); }
@Override
public void decode(ByteBuf in) { throw new UnsupportedOperationException("CloseMessage is not a real message, and must not be received."); }
}
@@ -0,0 +1,22 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
import io.netty.buffer.ByteBuf;
public class CloseReasonMessage implements INetworkMessage
{
public String reason;
public CloseReasonMessage() { }
public CloseReasonMessage(String reason) { this.reason = reason; }
@Override
public void encode(ByteBuf out) { INetworkObject.encodeString(this.reason, out); }
@Override
public void decode(ByteBuf in) { this.reason = INetworkObject.decodeString(in); }
}
@@ -0,0 +1,19 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
import io.netty.buffer.ByteBuf;
public class HelloMessage implements INetworkMessage
{
public int version = ModInfo.PROTOCOL_VERSION;
@Override
public void encode(ByteBuf out) { out.writeInt(this.version); }
@Override
public void decode(ByteBuf in) { this.version = in.readInt(); }
}
@@ -0,0 +1,27 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import io.netty.buffer.ByteBuf;
import java.util.UUID;
public class PlayerUUIDMessage implements INetworkMessage
{
public UUID playerUUID;
public PlayerUUIDMessage() { }
public PlayerUUIDMessage(UUID playerUUID) { this.playerUUID = playerUUID; }
@Override
public void encode(ByteBuf out)
{
out.writeLong(this.playerUUID.getMostSignificantBits());
out.writeLong(this.playerUUID.getLeastSignificantBits());
}
@Override
public void decode(ByteBuf in) { this.playerUUID = new UUID(in.readLong(), in.readLong()); }
}
@@ -0,0 +1,23 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.objects.RemotePlayer;
import io.netty.buffer.ByteBuf;
public class RemotePlayerConfigMessage implements INetworkMessage
{
public RemotePlayer.Payload payload;
public RemotePlayerConfigMessage() { }
public RemotePlayerConfigMessage(RemotePlayer.Payload payload) { this.payload = payload; }
@Override
public void encode(ByteBuf out) { this.payload.encode(out); }
@Override
public void decode(ByteBuf in) { this.payload = INetworkObject.decode(new RemotePlayer.Payload(), in); }
}
@@ -0,0 +1,21 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import io.netty.buffer.ByteBuf;
public class RequestChunksMessage implements INetworkMessage
{
@Override
public void encode(ByteBuf out)
{
}
@Override
public void decode(ByteBuf in)
{
}
}
@@ -0,0 +1,34 @@
package com.seibel.distanthorizons.core.network.objects;
import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
public class RemotePlayer
{
public IServerPlayerWrapper serverPlayer;
public Payload payload;
public ChannelHandlerContext channelContext;
public RemotePlayer(IServerPlayerWrapper serverPlayer) { this.serverPlayer = serverPlayer; }
public static class Payload implements INetworkObject
{
// TODO Replace this example with useful fields,
// this should include any information the server needs to know about the connected client
public int renderDistance;
@Override
public void encode(ByteBuf out) { out.writeInt(this.renderDistance); }
@Override
public void decode(ByteBuf in) { this.renderDistance = in.readInt(); }
}
}
@@ -0,0 +1,11 @@
package com.seibel.distanthorizons.core.network.protocol;
/**
* CLIENT, <br>
* SERVER, <br>
*/
public enum EMessageHandlerSide
{
CLIENT,
SERVER
}
@@ -0,0 +1,8 @@
package com.seibel.distanthorizons.core.network.protocol;
/** For now this is only used for constraining listeners */
public interface INetworkMessage extends INetworkObject
{
}
@@ -0,0 +1,31 @@
package com.seibel.distanthorizons.core.network.protocol;
import io.netty.buffer.ByteBuf;
import java.nio.charset.StandardCharsets;
public interface INetworkObject
{
void encode(ByteBuf out);
void decode(ByteBuf in);
static <T extends INetworkObject> T decode(T obj, ByteBuf inputByteBuf)
{
obj.decode(inputByteBuf);
return obj;
}
static void encodeString(String inputString, ByteBuf outputByteBuf)
{
outputByteBuf.writeShort(inputString.length());
outputByteBuf.writeBytes(inputString.getBytes(StandardCharsets.UTF_8));
}
static String decodeString(ByteBuf inputByteBuf)
{
int length = inputByteBuf.readShort();
return inputByteBuf.readBytes(length).toString(StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,18 @@
package com.seibel.distanthorizons.core.network.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class MessageDecoder extends ByteToMessageDecoder
{
@Override
protected void decode(ChannelHandlerContext channelContext, ByteBuf inputByteBuf, List<Object> outputDecodedObjectList)
{
INetworkMessage message = MessageRegistry.INSTANCE.createMessage(inputByteBuf.readShort());
outputDecodedObjectList.add(INetworkObject.decode(message, inputByteBuf));
}
}
@@ -0,0 +1,16 @@
package com.seibel.distanthorizons.core.network.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MessageEncoder extends MessageToByteEncoder<INetworkMessage>
{
@Override
protected void encode(ChannelHandlerContext channelContext, INetworkMessage message, ByteBuf outputByteBuf) throws IllegalArgumentException
{
outputByteBuf.writeShort(MessageRegistry.INSTANCE.getMessageId(message));
message.encode(outputByteBuf);
}
}
@@ -0,0 +1,53 @@
package com.seibel.distanthorizons.core.network.protocol;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.CloseMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
@ChannelHandler.Sharable
public class MessageHandler extends SimpleChannelInboundHandler<INetworkMessage>
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private final Map<Class<? extends INetworkMessage>, List<BiConsumer<INetworkMessage, ChannelHandlerContext>>> handlers = new HashMap<>();
public <T extends INetworkMessage> void registerHandler(Class<T> handlerClass, BiConsumer<T, ChannelHandlerContext> handlerImplementation)
{
this.handlers.computeIfAbsent(handlerClass, missingHandlerClass -> new LinkedList<>())
.add((BiConsumer<INetworkMessage, ChannelHandlerContext>) handlerImplementation);
}
@Override
protected void channelRead0(ChannelHandlerContext channelContext, INetworkMessage message)
{
LOGGER.trace("Received message: "+message.getClass().getSimpleName());
List<BiConsumer<INetworkMessage, ChannelHandlerContext>> handlerList = this.handlers.get(message.getClass());
if (handlerList == null)
{
LOGGER.warn("Unhandled message type: "+message.getClass().getSimpleName());
return;
}
for (BiConsumer<INetworkMessage, ChannelHandlerContext> handler : handlerList)
{
handler.accept(message, channelContext);
}
}
@Override
public void channelInactive(@NotNull ChannelHandlerContext channelContext) { this.channelRead0(channelContext, new CloseMessage()); }
}
@@ -0,0 +1,62 @@
package com.seibel.distanthorizons.core.network.protocol;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.seibel.distanthorizons.core.network.messages.*;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class MessageRegistry
{
public static final MessageRegistry INSTANCE = new MessageRegistry();
private final Map<Integer, Supplier<? extends INetworkMessage>> idToSupplier = new HashMap<>();
private final BiMap<Class<? extends INetworkMessage>, Integer> classToId = HashBiMap.create();
private MessageRegistry()
{
// Note: Messages must have parameterless constructors
// Keep messages below intact so client/server can disconnect if version does not match
this.registerMessage(HelloMessage.class, HelloMessage::new);
this.registerMessage(CloseReasonMessage.class, CloseReasonMessage::new);
// Define your messages after this line
this.registerMessage(AckMessage.class, AckMessage::new);
this.registerMessage(PlayerUUIDMessage.class, PlayerUUIDMessage::new);
this.registerMessage(RemotePlayerConfigMessage.class, RemotePlayerConfigMessage::new);
this.registerMessage(RequestChunksMessage.class, RequestChunksMessage::new);
}
public <T extends INetworkMessage> void registerMessage(Class<T> clazz, Supplier<T> supplier)
{
int id = this.idToSupplier.size() + 1;
this.idToSupplier.put(id, supplier);
this.classToId.put(clazz, id);
}
public Class<? extends INetworkMessage> getMessageClassById(int messageId) { return this.classToId.inverse().get(messageId); }
public INetworkMessage createMessage(int messageId) throws IllegalArgumentException
{
try
{
return this.idToSupplier.get(messageId).get();
}
catch (NullPointerException e)
{
throw new IllegalArgumentException("Invalid message ID");
}
}
public int getMessageId(INetworkMessage message) { return this.getMessageId(message.getClass()); }
public int getMessageId(Class<? extends INetworkMessage> messageClass) { return this.classToId.get(messageClass); }
}
@@ -0,0 +1,37 @@
package com.seibel.distanthorizons.core.network.protocol;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import org.jetbrains.annotations.NotNull;
/** used when creating a network channel */
public class NetworkChannelInitializer extends ChannelInitializer<SocketChannel>
{
private final MessageHandler messageHandler;
public NetworkChannelInitializer(MessageHandler messageHandler) { this.messageHandler = messageHandler; }
@Override
public void initChannel(@NotNull SocketChannel socketChannel)
{
ChannelPipeline pipeline = socketChannel.pipeline();
// Encoder
pipeline.addLast(new LengthFieldPrepender(Short.BYTES));
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new NetworkOutboundExceptionRouter());
// Decoder
pipeline.addLast(new LengthFieldBasedFrameDecoder(Short.MAX_VALUE, 0, Short.BYTES, 0, Short.BYTES));
pipeline.addLast(new MessageDecoder());
// Handler
pipeline.addLast(this.messageHandler);
pipeline.addLast(new NetworkExceptionHandler());
}
}
@@ -0,0 +1,19 @@
package com.seibel.distanthorizons.core.network.protocol;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.apache.logging.log4j.Logger;
public class NetworkExceptionHandler extends ChannelInboundHandlerAdapter
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
@Override
public void exceptionCaught(ChannelHandlerContext channelContext, Throwable cause)
{
LOGGER.error("Exception caught in channel: ["+channelContext.name()+"].", cause);
channelContext.close();
}
}
@@ -0,0 +1,17 @@
package com.seibel.distanthorizons.core.network.protocol;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
public class NetworkOutboundExceptionRouter extends ChannelOutboundHandlerAdapter
{
@Override
public void write(ChannelHandlerContext channelContext, Object messageObj, ChannelPromise promise) throws Exception
{
promise.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
super.write(channelContext, messageObj, promise);
}
}
@@ -1,10 +1,17 @@
package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
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.network.messages.*;
import com.seibel.distanthorizons.core.network.messages.PlayerUUIDMessage;
import com.seibel.distanthorizons.core.network.messages.RemotePlayerConfigMessage;
import com.seibel.distanthorizons.core.network.objects.RemotePlayer;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.EventLoop;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
@@ -15,9 +22,13 @@ import java.util.concurrent.ExecutorService;
public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
{
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private final ConcurrentHashMap<IClientLevelWrapper, DhClientLevel> 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);
public EventLoop eventLoop = new EventLoop(this.dhTickerThread, this::_clientTick);
@@ -27,12 +38,35 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
public DhClientWorld()
{
super(EWorldEnvironment.Client_Only);
this.saveStructure = new ClientOnlySaveStructure();
this.levels = new ConcurrentHashMap<>();
this.saveStructure = new ClientOnlySaveStructure();
this.levels = new ConcurrentHashMap<>();
// TODO server specific configs
this.networkClient = new NetworkClient(MC_CLIENT.getCurrentServerIp(), 25049);
registerNetworkHandlers();
LOGGER.info("Started DhWorld of type "+this.environment);
}
private void registerNetworkHandlers() {
networkClient.registerHandler(HelloMessage.class, (msg, ctx) -> {
ctx.writeAndFlush(new PlayerUUIDMessage(MC_CLIENT.getPlayerUUID()));
});
// TODO Proper payload handling
networkClient.registerAckHandler(PlayerUUIDMessage.class, ctx -> {
ctx.writeAndFlush(new RemotePlayerConfigMessage(new RemotePlayer.Payload()));
});
networkClient.registerHandler(RemotePlayerConfigMessage.class, (msg, ctx) -> {
});
networkClient.registerAckHandler(RemotePlayerConfigMessage.class, ctx -> {
// TODO Actually request chunks
ctx.writeAndFlush(new RequestChunksMessage());
});
}
@Override
public DhClientLevel getOrLoadLevel(ILevelWrapper wrapper)
@@ -100,6 +134,8 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
@Override
public void close()
{
this.networkClient.close();
this.saveAndFlush().join();
for (DhClientLevel dhClientLevel : this.levels.values())
{
@@ -1,14 +1,23 @@
package com.seibel.distanthorizons.core.world;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
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.network.messages.*;
import com.seibel.distanthorizons.core.network.messages.RequestChunksMessage;
import com.seibel.distanthorizons.core.network.objects.RemotePlayer;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import io.netty.channel.ChannelHandlerContext;
import java.io.File;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
@@ -16,6 +25,10 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
private final HashMap<IServerLevelWrapper, DhServerLevel> levels;
public final LocalSaveStructure saveStructure;
private final NetworkServer networkServer;
private final HashMap<UUID, RemotePlayer> playersByUUID;
private final BiMap<ChannelHandlerContext, RemotePlayer> playersByConnection;
public DhServerWorld()
@@ -24,11 +37,76 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
this.saveStructure = new LocalSaveStructure();
this.levels = new HashMap<>();
// TODO move to global payload once server specific configs are implemented
this.networkServer = new NetworkServer(25049);
this.playersByUUID = new HashMap<>();
this.playersByConnection = HashBiMap.create();
this.registerNetworkHandlers();
LOGGER.info("Started "+DhServerWorld.class.getSimpleName()+" of type "+this.environment);
}
private void registerNetworkHandlers()
{
this.networkServer.registerHandler(CloseMessage.class, (closeMessage, channelContext) ->
{
RemotePlayer dhPlayer = this.playersByConnection.remove(channelContext);
if (dhPlayer != null)
{
dhPlayer.channelContext = null;
}
});
this.networkServer.registerHandler(PlayerUUIDMessage.class, (playerUUIDMessage, channelContext) ->
{
RemotePlayer dhPlayer = this.playersByUUID.get(playerUUIDMessage.playerUUID);
if (dhPlayer == null)
{
this.networkServer.disconnectClient(channelContext, "Player is not logged in.");
return;
}
if (dhPlayer.channelContext != null)
{
this.networkServer.disconnectClient(channelContext, "Another connection is already in use.");
return;
}
dhPlayer.channelContext = channelContext;
this.playersByConnection.put(channelContext, dhPlayer);
channelContext.writeAndFlush(new AckMessage(PlayerUUIDMessage.class));
});
this.networkServer.registerHandler(RemotePlayerConfigMessage.class, (dhRemotePlayerConfigMessage, channelContext) ->
{
// TODO Take notice of received payload and possibly echo back a constrained version
channelContext.writeAndFlush(new AckMessage(RemotePlayerConfigMessage.class));
});
this.networkServer.registerHandler(RequestChunksMessage.class, (msg, ctx) ->
{
LOGGER.info("RequestChunksMessage");
// hasReceivedChunkRequest should be false somewhere ???
// to avoid sending updates until client says at least something about its state
});
}
public void addPlayer(IServerPlayerWrapper serverPlayer)
{
this.playersByUUID.put(serverPlayer.getUUID(), new RemotePlayer(serverPlayer));
}
public void removePlayer(IServerPlayerWrapper serverPlayer)
{
RemotePlayer dhPlayer = this.playersByUUID.remove(serverPlayer.getUUID());
ChannelHandlerContext channelContext = this.playersByConnection.inverse().remove(dhPlayer);
if (channelContext != null)
{
this.networkServer.disconnectClient(channelContext, "You are being disconnected.");
}
}
@Override
public DhServerLevel getOrLoadLevel(ILevelWrapper wrapper)
@@ -38,11 +116,11 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
return null;
}
return this.levels.computeIfAbsent((IServerLevelWrapper) wrapper, (w) ->
return this.levels.computeIfAbsent((IServerLevelWrapper) wrapper, (serverLevelWrapper) ->
{
File levelFile = this.saveStructure.getLevelFolder(wrapper);
LodUtil.assertTrue(levelFile != null);
return new DhServerLevel(this.saveStructure, w);
return new DhServerLevel(this.saveStructure, serverLevelWrapper);
});
}
@@ -88,6 +166,8 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
@Override
public void close()
{
this.networkServer.close();
for (DhServerLevel level : this.levels.values())
{
LOGGER.info("Unloading level " + level.getLevelWrapper().getDimensionType().getDimensionName());
@@ -98,6 +178,4 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
LOGGER.info("Closed DhWorld of type "+this.environment);
}
}
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.minecraft;
import java.io.File;
import java.util.ArrayList;
import java.util.UUID;
import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
@@ -70,11 +71,13 @@ public interface IMinecraftClientWrapper extends IBindable
//=============//
boolean playerExists();
UUID getPlayerUUID();
DhBlockPos getPlayerBlockPos();
DhChunkPos getPlayerChunkPos();
/**
* Returns the level the client is currently in. <br>
* Returns null if the client isn't in a level.
@@ -0,0 +1,10 @@
package com.seibel.distanthorizons.core.wrapperInterfaces.misc;
import com.seibel.distanthorizons.api.interfaces.IDhApiUnsafeWrapper;
import java.util.UUID;
public interface IServerPlayerWrapper extends IDhApiUnsafeWrapper
{
UUID getUUID();
}