/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.network;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.lanternpowered.server.text.translation.TranslationHelper.t;
import com.flowpowered.math.vector.Vector3d;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoop;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.CodecException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.timeout.TimeoutException;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import org.lanternpowered.server.LanternServer;
import org.lanternpowered.server.config.user.ban.BanConfig;
import org.lanternpowered.server.config.user.ban.BanEntry;
import org.lanternpowered.server.config.world.WorldConfig;
import org.lanternpowered.server.data.io.PlayerIO;
import org.lanternpowered.server.entity.LanternEntity;
import org.lanternpowered.server.entity.living.player.LanternPlayer;
import org.lanternpowered.server.entity.living.player.tab.GlobalTabList;
import org.lanternpowered.server.game.Lantern;
import org.lanternpowered.server.network.entity.EntityProtocolManager;
import org.lanternpowered.server.network.entity.EntityProtocolTypes;
import org.lanternpowered.server.network.message.AsyncHelper;
import org.lanternpowered.server.network.message.BulkMessage;
import org.lanternpowered.server.network.message.HandlerMessage;
import org.lanternpowered.server.network.message.Message;
import org.lanternpowered.server.network.message.MessageRegistration;
import org.lanternpowered.server.network.message.NullMessage;
import org.lanternpowered.server.network.message.handler.Handler;
import org.lanternpowered.server.network.protocol.Protocol;
import org.lanternpowered.server.network.protocol.ProtocolState;
import org.lanternpowered.server.network.vanilla.message.type.connection.MessageInOutKeepAlive;
import org.lanternpowered.server.network.vanilla.message.type.connection.MessageOutDisconnect;
import org.lanternpowered.server.profile.LanternGameProfile;
import org.lanternpowered.server.text.LanternTexts;
import org.lanternpowered.server.world.LanternWorld;
import org.lanternpowered.server.world.LanternWorldProperties;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.key.Keys;
import org.spongepowered.api.entity.Transform;
import org.spongepowered.api.entity.living.player.gamemode.GameModes;
import org.spongepowered.api.event.SpongeEventFactory;
import org.spongepowered.api.event.cause.Cause;
import org.spongepowered.api.event.message.MessageEvent;
import org.spongepowered.api.event.network.ClientConnectionEvent;
import org.spongepowered.api.network.PlayerConnection;
import org.spongepowered.api.profile.GameProfile;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.channel.MessageChannel;
import org.spongepowered.api.util.ban.Ban;
import org.spongepowered.api.world.World;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
public final class NetworkSession extends SimpleChannelInboundHandler<Message> implements PlayerConnection {
/**
* The read timeout in seconds.
*/
public static final int READ_TIMEOUT_SECONDS = 10;
public static final String ENCRYPTION = "encryption";
public static final String LEGACY_PING = "legacy-ping";
public static final String COMPRESSION = "compression";
public static final String FRAMING = "framing";
public static final String CODECS = "codecs";
public static final String PROCESSOR = "processor";
public static final String HANDLER = "handler";
/**
* The attribute key for the FML (Forge Mod Loader) marker.
*/
public static final AttributeKey<Boolean> FML_MARKER = AttributeKey.valueOf("fml-marker");
/**
* The formatter that is used to format the date used in the ban disconnect message.
*/
private static final DateTimeFormatter BAN_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd 'at' HH:mm:ss z");
private final NetworkManager networkManager;
private final LanternServer server;
private final Channel channel;
/**
* A shared random.
*/
private final Random random = new Random();
/**
* The network context that is used by the handlers.
*/
private final NetworkContext networkContext = new NetworkContext() {
@Override
public NetworkSession getSession() {
return NetworkSession.this;
}
@Override
public Channel getChannel() {
return channel;
}
};
/**
* The game profile of the player that owns this connection.
*/
@Nullable private LanternGameProfile gameProfile;
/**
* The player that owns this connection.
*/
@Nullable private LanternPlayer player;
/**
* The reason that caused the channel to disconnect.
*/
@Nullable private volatile Text disconnectReason;
/**
* A queue of incoming messages that must be handled on
* the synchronous thread.
*/
private final Queue<HandlerMessage> messageQueue = new ConcurrentLinkedDeque<>();
/**
* The virtual host address.
*/
@Nullable private InetSocketAddress virtualHostAddress;
/**
* The protocol state that is currently active.
*/
private volatile ProtocolState protocolState = ProtocolState.HANDSHAKE;
/**
* A list with all the registered channels.
*/
private final Set<String> registeredChannels = new HashSet<>();
/**
* A list with all the installed client mods.
*/
private final Set<String> installedMods = new HashSet<>();
/**
* The latency of the connection.
*/
private volatile int latency;
/**
* The id that was used in the keep alive message.
*/
private int keepAliveId = -1;
/**
* The time that the last the keep alive message was send.
*/
private long keepAliveTime;
/**
* The protocol version.
*/
private int protocolVersion = -1;
public NetworkSession(Channel channel, LanternServer server, NetworkManager networkManager) {
this.networkManager = networkManager;
this.channel = channel;
this.server = server;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// Send a keep alive message to the client every 40 ticks (2 seconds),
// doing this also in the event loop to keep it separate from the main
// thread.
this.channel.eventLoop().scheduleAtFixedRate(() -> {
final ProtocolState protocolState = this.protocolState;
if (protocolState == ProtocolState.PLAY || protocolState == ProtocolState.FORGE_HANDSHAKE) {
this.keepAliveId = this.random.nextInt();
this.keepAliveTime = System.currentTimeMillis();
send(new MessageInOutKeepAlive(this.keepAliveId));
}
}, 0, 2, TimeUnit.SECONDS);
}
private void handleKeepAlive(MessageInOutKeepAlive message) {
if (this.keepAliveId == message.getId()) {
// Calculate the latency
this.latency = (int) ((this.latency * 3 + (System.currentTimeMillis() - this.keepAliveTime) / 1000L) / 4);
if (this.gameProfile != null) {
// Update the global tab list
GlobalTabList.getInstance().get(this.gameProfile).ifPresent(entry -> entry.setLatency(this.latency));
}
} else {
Lantern.getLogger().debug("A keep alive message {} didn't receive a response in time, "
+ "the next message is already been send.", this.keepAliveId);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message message) throws Exception {
this.messageReceived(message);
}
/**
* Handles the inbound {@link Message} with the specified {@link Handler}.
*
* @param handler The handler
* @param message The message
*/
@SuppressWarnings("unchecked")
private void handleMessage(Handler handler, Message message) {
try {
handler.handle(this.networkContext, message);
} catch (Throwable throwable) {
Lantern.getLogger().error("Error while handling {}", message, throwable);
}
}
/**
* Called when the server received a message from the client.
*
* @param message The message
*/
@SuppressWarnings("unchecked")
public void messageReceived(Message message) {
if (message instanceof MessageInOutKeepAlive) { // Special case
handleKeepAlive((MessageInOutKeepAlive) message);
} else if (message == NullMessage.INSTANCE) {
// Ignore
} else if (message instanceof BulkMessage) {
((BulkMessage) message).getMessages().forEach(this::messageReceived);
} else if (message instanceof HandlerMessage) {
final HandlerMessage handlerMessage = (HandlerMessage) message;
if (AsyncHelper.isAsyncMessage(handlerMessage.getMessage()) ||
AsyncHelper.isAsyncHandler(handlerMessage.getHandler())) {
handleMessage(handlerMessage.getHandler(), handlerMessage.getMessage());
} else {
this.messageQueue.add(handlerMessage);
}
} else {
final Class<? extends Message> messageClass = message.getClass();
final MessageRegistration registration = getProtocol().inbound().findByMessageType(messageClass).orElse(null);
if (registration == null) {
throw new DecoderException("Failed to find a message registration for " + messageClass.getName() + "!");
}
registration.getHandler().ifPresent(handler -> {
final Handler handler1 = (Handler) handler;
if (AsyncHelper.isAsyncMessage(message) || AsyncHelper.isAsyncHandler(handler1)) {
handleMessage(handler1, message);
} else {
this.messageQueue.add(new HandlerMessage(message, handler1));
}
});
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
this.networkManager.onActive(this);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
this.networkManager.onInactive(this);
// The player probably just left the server
if (this.disconnectReason == null) {
if (this.channel.isOpen()) {
this.disconnectReason = t("disconnect.endOfStream");
} else {
this.disconnectReason = t("disconnect.leftServer");
}
}
// The player was able to spawn before the connection closed
if (this.player != null) {
leavePlayer();
//noinspection ConstantConditions
Lantern.getLogger().debug("{} ({}) disconnected. Reason: {}", this.gameProfile.getName().orElse("Unknown"),
this.channel.remoteAddress(), LanternTexts.toLegacy(this.disconnectReason));
} else if (getProtocolState() != ProtocolState.STATUS) { // Ignore the status requests
// The player left before he was able to connect
//noinspection ConstantConditions
Lantern.getLogger().debug("A player{} failed to join from {}. Reason: {}", this.gameProfile == null ? "" :
" (" + this.gameProfile.getName().orElse("Unknown") + ')',
this.channel.remoteAddress(), LanternTexts.toLegacy(this.disconnectReason));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Pipeline error, just log it
if (cause instanceof CodecException) {
Lantern.getLogger().error("A netty pipeline error occurred", cause);
} else {
// Use the debug level, don't spam the server with errors
// caused by client disconnection, ...
Lantern.getLogger().debug("A netty connection error occurred", cause);
if (cause instanceof TimeoutException) {
closeChannel(t("disconnect.timeout"));
} else {
closeChannel(t("disconnect.genericReason", "Internal Exception: " + cause));
}
}
}
@Override
public LanternPlayer getPlayer() {
if (this.player == null) {
throw new IllegalStateException("The player is not yet available.");
}
return this.player;
}
@Override
public int getLatency() {
return this.latency;
}
@Override
public InetSocketAddress getAddress() {
return (InetSocketAddress) this.channel.remoteAddress();
}
@Override
public InetSocketAddress getVirtualHost() {
return this.virtualHostAddress == null ? getAddress() : this.virtualHostAddress;
}
/**
* Sets the virtual host address.
*
* @param address The virtual host address
*/
public void setVirtualHost(@Nullable InetSocketAddress address) {
this.virtualHostAddress = address;
}
/**
* Gets the protocol version.
*
* @return The protocol version
*/
public int getProtocolVersion() {
return this.protocolVersion;
}
/**
* Sets the protocol version.
*
* @param protocolVersion The protocol version
*/
public void setProtocolVersion(int protocolVersion) {
if (this.protocolVersion != -1) {
throw new IllegalStateException("The protocol version may only be set once.");
}
this.protocolVersion = protocolVersion;
}
/**
* Gets the {@link GameProfile}.
*
* @return The game profile
*/
public LanternGameProfile getGameProfile() {
if (this.gameProfile == null) {
throw new IllegalStateException("The game profile is not yet available.");
}
return this.gameProfile;
}
/**
* Sets the {@link GameProfile}.
*
* @param gameProfile The game profile
*/
public void setGameProfile(GameProfile gameProfile) {
if (this.gameProfile != null) {
throw new IllegalStateException("The game profile may only be set once.");
}
this.gameProfile = (LanternGameProfile) gameProfile;
}
/**
* Pulses the session. This should be called
* from the main thread.
*/
public void pulse() {
HandlerMessage entry;
while ((entry = this.messageQueue.poll()) != null) {
handleMessage(entry.getHandler(), entry.getMessage());
}
}
/**
* Gets a list with all the installed client mods.
*
* @return The installed mods
*/
public Set<String> getInstalledMods() {
return this.installedMods;
}
/**
* Gets the registered channels.
*
* @return The registered channels
*/
public Set<String> getRegisteredChannels() {
return this.registeredChannels;
}
/**
* Gets the {@link LanternServer}.
*
* @return The server
*/
public LanternServer getServer() {
return this.server;
}
/**
* Gets the {@link Channel} of this session.
*
* @return The channel
*/
public Channel getChannel() {
return this.channel;
}
/**
* Gets the protocol associated with the current type.
*
* @return The protocol
*/
public Protocol getProtocol() {
return this.protocolState.getProtocol();
}
/**
* Gets the current protocol state.
*
* @return The protocol state
*/
public ProtocolState getProtocolState() {
return this.protocolState;
}
/**
* Sets the current protocol state.
*
* @param state The protocol state
*/
public void setProtocolState(ProtocolState state) {
this.protocolState = state;
}
/**
* Closes the channel with a specific disconnect reason, this doesn't
* send a disconnect message to the client, it just closes the connection.
*
* @param reason The reason
*/
private void closeChannel(Text reason) {
this.disconnectReason = checkNotNull(reason, "reason");
this.channel.close();
}
/**
* Sends a {@link Message} and returns the {@link ChannelFuture}.
*
* @param message The message
* @return The channel future
*/
public ChannelFuture sendWithFuture(Message message) {
checkNotNull(message, "message");
if (!this.channel.isActive()) {
return this.channel.newPromise();
}
// Write the message and add a exception handler
return this.channel.writeAndFlush(message).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
}
/**
* Sends a array of {@link Message}s and returns the {@link ChannelFuture}.
*
* @param messages The messages
* @return The channel future
*/
public ChannelFuture sendWithFuture(Message... messages) {
checkNotNull(messages, "messages");
checkArgument(messages.length != 0, "messages cannot be empty");
final ChannelPromise promise = this.channel.newPromise();
if (!this.channel.isActive()) {
return promise;
}
promise.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
// Don't bother checking if we are in the event loop,
// there is only one message.
if (messages.length == 1) {
this.channel.writeAndFlush(messages[0], promise);
} else {
final EventLoop eventLoop = this.channel.eventLoop();
final ChannelPromise voidPromise = this.channel.voidPromise();
if (eventLoop.inEventLoop()) {
final int last = messages.length - 1;
for (int i = 0; i < last; i++) {
ReferenceCountUtil.retain(messages[i]);
this.channel.writeAndFlush(messages[i], voidPromise);
}
ReferenceCountUtil.retain(messages[last]);
this.channel.writeAndFlush(messages[last], promise);
} else {
// If there are more then one message, combine them inside the
// event loop to reduce overhead of wakeup calls and object creation
// Create a copy of the list, to avoid concurrent modifications
final List<Message> messages0 = ImmutableList.copyOf(messages);
messages0.forEach(ReferenceCountUtil::retain);
eventLoop.submit(() -> {
final Iterator<Message> it0 = messages0.iterator();
do {
final Message message0 = it0.next();
// Only use a normal channel promise for the last message
this.channel.writeAndFlush(message0, it0.hasNext() ? voidPromise : promise);
} while (it0.hasNext());
});
}
}
return promise;
}
/**
* Sends a iterable of {@link Message}s.
*
* @param messages The messages
*/
public ChannelFuture sendWithFuture(Iterable<Message> messages) {
checkNotNull(messages, "messages");
final Iterator<Message> it = messages.iterator();
checkArgument(it.hasNext(), "messages cannot be empty");
final ChannelPromise promise = this.channel.newPromise();
if (!this.channel.isActive()) {
return promise;
}
promise.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
Message message = it.next();
// Don't bother checking if we are in the event loop,
// there is only one message.
if (!it.hasNext()) {
this.channel.writeAndFlush(message, promise);
} else {
final EventLoop eventLoop = this.channel.eventLoop();
final ChannelPromise voidPromise = this.channel.voidPromise();
if (eventLoop.inEventLoop()) {
while (true) {
final boolean next = it.hasNext();
// Only use a normal channel promise for the last message
this.channel.writeAndFlush(message, next ? voidPromise : promise);
if (!next) {
break;
}
message = it.next();
ReferenceCountUtil.retain(message);
}
} else {
// If there are more then one message, combine them inside the
// event loop to reduce overhead of wakeup calls and object creation
// Create a copy of the list, to avoid concurrent modifications
final List<Message> messages0 = ImmutableList.copyOf(messages);
messages0.forEach(ReferenceCountUtil::retain);
eventLoop.submit(() -> {
final Iterator<Message> it0 = messages0.iterator();
do {
final Message message0 = it0.next();
// Only use a normal channel promise for the last message
this.channel.writeAndFlush(message0, it0.hasNext() ? voidPromise : promise);
} while (it0.hasNext());
});
}
}
return promise;
}
/**
* Sends a {@link Message}.
*
* @param message The message
*/
public void send(Message message) {
checkNotNull(message, "message");
if (!this.channel.isActive()) {
return;
}
ReferenceCountUtil.retain(message);
// Thrown exceptions will be delegated through the exceptionCaught method
this.channel.writeAndFlush(message, this.channel.voidPromise());
}
/**
* Sends a array of {@link Message}s.
*
* @param messages The messages
*/
public void send(Message... messages) {
checkNotNull(messages, "messages");
checkArgument(messages.length != 0, "messages cannot be empty");
if (!this.channel.isActive()) {
return;
}
final ChannelPromise voidPromise = this.channel.voidPromise();
if (messages.length == 1) {
this.channel.writeAndFlush(messages[0], voidPromise);
} else {
final EventLoop eventLoop = this.channel.eventLoop();
if (eventLoop.inEventLoop()) {
for (Message message : messages) {
ReferenceCountUtil.retain(message);
this.channel.writeAndFlush(message, voidPromise);
}
} else {
// If there are more then one message, combine them inside the
// event loop to reduce overhead of wakeup calls and object creation
// Create a copy of the list, to avoid concurrent modifications
final List<Message> messages0 = ImmutableList.copyOf(messages);
messages0.forEach(ReferenceCountUtil::retain);
eventLoop.submit(() -> {
for (Message message0 : messages0) {
this.channel.writeAndFlush(message0, voidPromise);
}
});
}
}
}
/**
* Sends a iterable of {@link Message}s.
*
* @param messages The messages
*/
public void send(Iterable<Message> messages) {
checkNotNull(messages, "messages");
final Iterator<Message> it = messages.iterator();
checkArgument(it.hasNext(), "messages cannot be empty");
Message message = it.next();
// Don't bother checking if we are in the event loop,
// there is only one message.
final ChannelPromise voidPromise = this.channel.voidPromise();
if (!it.hasNext()) {
this.channel.writeAndFlush(message, voidPromise);
} else {
final EventLoop eventLoop = this.channel.eventLoop();
if (eventLoop.inEventLoop()) {
for (Message message0 : messages) {
this.channel.writeAndFlush(message0, voidPromise);
}
} else {
// If there are more then one message, combine them inside the
// event loop to reduce overhead of wakeup calls and object creation
// Create a copy of the list, to avoid concurrent modifications
final List<Message> messages0 = ImmutableList.copyOf(messages);
eventLoop.submit(() -> {
for (Message message0 : messages0) {
this.channel.writeAndFlush(message0, voidPromise);
}
});
}
}
}
/**
* Disconnects the session with a unknown reason.
*/
public void disconnect() {
this.disconnect(Text.of("Unknown reason."));
}
/**
* Disconnects the session with a specific reason.
*
* @param reason The reason
*/
public void disconnect(Text reason) {
checkNotNull(reason, "reason");
if (this.disconnectReason != null) {
return;
}
this.disconnectReason = reason;
if (this.channel.isActive() && (this.protocolState == ProtocolState.PLAY ||
this.protocolState == ProtocolState.LOGIN || this.protocolState == ProtocolState.FORGE_HANDSHAKE)) {
sendWithFuture(new MessageOutDisconnect(reason)).addListener(ChannelFutureListener.CLOSE);
} else {
this.channel.close();
}
}
/**
* Is called when the {@link LanternPlayer} leaves the
* server and needs to be cleaned up.
*/
private void leavePlayer() {
if (this.player == null) {
throw new IllegalStateException("The player must first be available!");
}
final LanternWorld world = this.player.getWorld();
//noinspection ConstantConditions
if (world != null) {
final MessageChannel messageChannel = this.player.getMessageChannel();
final Text quitMessage = t("multiplayer.player.left", this.player.getName());
final ClientConnectionEvent.Disconnect event = SpongeEventFactory.createClientConnectionEventDisconnect(
Cause.source(this.player).build(), messageChannel, Optional.of(messageChannel),
new MessageEvent.MessageFormatter(quitMessage), this.player, false);
Sponge.getEventManager().post(event);
if (!event.isMessageCancelled()) {
event.getChannel().ifPresent(channel -> channel.send(this.player, event.getMessage()));
}
// Save the player data
try {
PlayerIO.save(Lantern.getGame().getSavesDirectory(), this.player);
} catch (IOException e) {
//noinspection ConstantConditions
Lantern.getLogger().warn("An error occurred while saving the player data of {} ({})", this.gameProfile.getName().get(),
this.gameProfile.getUniqueId(), e);
}
this.player.getContainerSession().setRawOpenContainer(null, null);
this.player.remove(LanternEntity.RemoveState.DESTROYED);
this.player.setWorld(null);
EntityProtocolManager.releaseEntityId(this.player.getNetworkId());
}
}
/**
* Initializes the {@link LanternPlayer} instance
* and spawns it in a world if permitted to join
* the server.
*/
public void initPlayer() {
if (this.gameProfile == null) {
throw new IllegalStateException("The game profile must first be available!");
}
this.player = new LanternPlayer(this.gameProfile, this);
this.player.setNetworkId(EntityProtocolManager.acquireEntityId());
this.player.setEntityProtocolType(EntityProtocolTypes.PLAYER);
try {
PlayerIO.load(Lantern.getGame().getSavesDirectory(), this.player);
} catch (IOException e) {
Lantern.getLogger().warn("An error occurred while loading the player data", e);
}
LanternWorld world = this.player.getWorld();
//noinspection ConstantConditions
if (world == null) {
LanternWorldProperties worldProperties = this.player.getTempWorld();
boolean fixSpawnLocation = false;
if (worldProperties == null) {
Lantern.getLogger().warn("The player [{}] attempted to login in a non-existent world, this is not possible "
+ "so we have moved them to the default's world spawn point.", this.gameProfile.getName().get());
worldProperties = (LanternWorldProperties) Lantern.getServer().getDefaultWorld().get();
fixSpawnLocation = true;
} else if (!worldProperties.isEnabled()) {
Lantern.getLogger().warn("The player [{}] attempted to login in a unloaded and not-enabled world [{}], this is not possible "
+ "so we have moved them to the default's world spawn point.", this.gameProfile.getName().get(),
worldProperties.getWorldName());
worldProperties = (LanternWorldProperties) Lantern.getServer().getDefaultWorld().get();
fixSpawnLocation = true;
}
final Optional<World> optWorld = Lantern.getWorldManager().loadWorld(worldProperties);
// Use the raw method to avoid triggering any network messages
this.player.setRawWorld((LanternWorld) optWorld.get());
this.player.setTempWorld(null);
if (fixSpawnLocation) {
// TODO: Use a proper spawn position
this.player.setRawPosition(new Vector3d(0, 100, 0));
this.player.setRawRotation(new Vector3d(0, 0, 0));
}
}
// The kick reason
Text kickReason = null;
final BanConfig banConfig = Lantern.getGame().getBanConfig();
// Check whether the player is banned and kick if necessary
Optional<BanEntry> optBanEntry = banConfig.getEntryByProfile(gameProfile);
if (!optBanEntry.isPresent()) {
final SocketAddress address = getChannel().remoteAddress();
if (address instanceof InetSocketAddress) {
optBanEntry = banConfig.getEntryByIp(((InetSocketAddress) address).getAddress());
}
}
if (optBanEntry.isPresent()) {
final BanEntry banEntry = optBanEntry.get();
final Optional<Instant> optExpirationDate = banEntry.getExpirationDate();
final Optional<Text> optReason = banEntry.getReason();
// Generate the kick message
Text.Builder builder = Text.builder();
if (banEntry instanceof Ban.Profile) {
builder.append(Text.of("You are banned from this server!"));
} else {
builder.append(Text.of("Your IP address is banned from this server!"));
}
// There is optionally a reason
optReason.ifPresent(reason -> builder.append(Text.of("\nReason: ", reason)));
// And a expiration date if present
optExpirationDate.ifPresent(expirationDate ->
builder.append(Text.of("\nYour ban will be removed on ", BAN_TIME_FORMATTER.format(expirationDate))));
kickReason = builder.build();
// Check for white-list
} else if (Lantern.getGame().getGlobalConfig().isWhitelistEnabled() &&
!Lantern.getGame().getWhitelistConfig().isWhitelisted(this.gameProfile) &&
!Lantern.getGame().getOpsConfig().getEntryByProfile(this.gameProfile).isPresent()) {
kickReason = Text.of("You are not white-listed on this server!");
// Check whether the server is full
} else if (Lantern.getServer().getOnlinePlayers().size() >= Lantern.getServer().getMaxPlayers()) {
kickReason = Text.of("The server is full!");
}
final MessageEvent.MessageFormatter messageFormatter = new MessageEvent.MessageFormatter(
kickReason != null ? kickReason : t("disconnect.notAllowedToJoin"));
final Cause cause = Cause.source(this.player).build();
final Transform<World> fromTransform = this.player.getTransform();
final ClientConnectionEvent.Login loginEvent = SpongeEventFactory.createClientConnectionEventLogin(cause,
fromTransform, fromTransform, this, messageFormatter, this.gameProfile, this.player, false);
if (kickReason != null) {
loginEvent.setCancelled(true);
}
Sponge.getEventManager().post(loginEvent);
if (loginEvent.isCancelled()) {
disconnect(loginEvent.isMessageCancelled() ? t("disconnect.disconnected") : loginEvent.getMessage());
return;
}
Lantern.getLogger().debug("The player {} successfully to joined from {}.",
this.gameProfile.getName().orElse("Unknown"), this.channel.remoteAddress());
// Update the first join and last played data
final Instant lastJoined = Instant.now();
this.player.offer(Keys.LAST_DATE_PLAYED, lastJoined);
if (!this.player.get(Keys.FIRST_DATE_PLAYED).isPresent()) {
this.player.offer(Keys.FIRST_DATE_PLAYED, lastJoined);
}
final Transform<World> toTransform = loginEvent.getToTransform();
world = (LanternWorld) toTransform.getExtent();
final WorldConfig config = world.getProperties().getConfig();
// Update the game mode if necessary
if (config.isGameModeForced() || this.player.get(Keys.GAME_MODE).get().equals(GameModes.NOT_SET)) {
this.player.offer(Keys.GAME_MODE, config.getGameMode());
}
// Reset the raw world
this.player.setRawWorld(null);
// Set the transform, this will trigger the initial
// network messages to be send
this.player.setTransform(toTransform);
final MessageChannel messageChannel = this.player.getMessageChannel();
final Text joinMessage = t("multiplayer.player.joined", this.player.getName());
final ClientConnectionEvent.Join joinEvent = SpongeEventFactory.createClientConnectionEventJoin(cause, messageChannel,
Optional.of(messageChannel), new MessageEvent.MessageFormatter(joinMessage), this.player, false);
Sponge.getEventManager().post(joinEvent);
if (!joinEvent.isMessageCancelled()) {
joinEvent.getChannel().ifPresent(channel -> channel.send(this.player, joinEvent.getMessage()));
}
this.server.getDefaultResourcePack().ifPresent(this.player::sendResourcePack);
this.player.resetIdleTimeoutCounter();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.omitNullValues()
.add("address", this.getAddress())
.add("virtualHost", this.virtualHostAddress)
.add("profile", this.gameProfile)
.add("protocolVersion", this.protocolVersion)
.add("protocolState", this.protocolState)
.toString();
}
}