org.lanternpowered.server.LanternServer.java Source code

Java tutorial

Introduction

Here is the source code for org.lanternpowered.server.LanternServer.java

Source

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

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import org.lanternpowered.server.advancement.AdvancementTrees;
import org.lanternpowered.server.config.GlobalConfig;
import org.lanternpowered.server.console.ConsoleManager;
import org.lanternpowered.server.console.LanternConsoleSource;
import org.lanternpowered.server.entity.living.player.LanternPlayer;
import org.lanternpowered.server.game.LanternGame;
import org.lanternpowered.server.game.version.LanternMinecraftVersion;
import org.lanternpowered.server.network.NetworkManager;
import org.lanternpowered.server.network.ProxyType;
import org.lanternpowered.server.network.protocol.ProtocolState;
import org.lanternpowered.server.network.query.QueryServer;
import org.lanternpowered.server.network.rcon.RconServer;
import org.lanternpowered.server.network.status.LanternFavicon;
import org.lanternpowered.server.plugin.InternalPluginsInfo;
import org.lanternpowered.server.service.CloseableService;
import org.lanternpowered.server.service.LanternServiceManager;
import org.lanternpowered.server.text.LanternTexts;
import org.lanternpowered.server.util.SecurityHelper;
import org.lanternpowered.server.util.ShutdownMonitorThread;
import org.lanternpowered.server.world.LanternWorldManager;
import org.lanternpowered.server.world.chunk.LanternChunkLayout;
import org.slf4j.Logger;
import org.spongepowered.api.Server;
import org.spongepowered.api.command.source.ConsoleSource;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.SpongeEventFactory;
import org.spongepowered.api.event.cause.Cause;
import org.spongepowered.api.network.status.Favicon;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.profile.GameProfileCache;
import org.spongepowered.api.profile.GameProfileManager;
import org.spongepowered.api.resourcepack.ResourcePack;
import org.spongepowered.api.resourcepack.ResourcePacks;
import org.spongepowered.api.scoreboard.Scoreboard;
import org.spongepowered.api.service.ProviderRegistration;
import org.spongepowered.api.service.ServiceManager;
import org.spongepowered.api.service.SimpleServiceManager;
import org.spongepowered.api.service.rcon.RconService;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.channel.MessageChannel;
import org.spongepowered.api.world.ChunkTicketManager;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.WorldArchetype;
import org.spongepowered.api.world.storage.ChunkLayout;
import org.spongepowered.api.world.storage.WorldProperties;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.Nullable;

@Singleton
public final class LanternServer implements Server {

    // The executor service for the server ticks
    private final ScheduledExecutorService executor = Executors
            .newSingleThreadScheduledExecutor(runnable -> this.mainThread = new Thread(runnable, "server"));

    @SuppressWarnings("NullableProblems")
    private Thread mainThread;

    // The world manager
    @Inject
    private LanternWorldManager worldManager;

    // The network manager
    @Inject
    private NetworkManager networkManager;

    // The logger
    @Inject
    private Logger logger;

    // The implementation plugin container
    @Inject
    @Named(InternalPluginsInfo.Implementation.IDENTIFIER)
    private PluginContainer pluginContainer;

    // The game instance
    @Inject
    private LanternGame game;

    // The console manager
    @Inject
    private ConsoleManager consoleManager;

    // The rcon server/service
    @Nullable
    private RconServer rconServer;

    // The query server
    @Nullable
    private QueryServer queryServer;

    // The key pair used for authentication
    private final KeyPair keyPair = SecurityHelper.generateKeyPair();

    // The broadcast channel
    private volatile MessageChannel broadcastChannel = MessageChannel.TO_ALL;

    // The maximum amount of players that can join
    private int maxPlayers;

    // The amount of ticks the server is running
    private final AtomicInteger runningTimeTicks = new AtomicInteger(0);

    // All the players by their name
    private final Map<String, LanternPlayer> playersByName = Maps.newConcurrentMap();

    // A unmodifiable collection with all the players
    private final Collection<LanternPlayer> unmodifiablePlayers = Collections
            .unmodifiableCollection(this.playersByName.values());

    // All the players by their uniqueId
    private final Map<UUID, LanternPlayer> playersByUUID = Maps.newConcurrentMap();

    @Nullable
    private ResourcePack resourcePack;
    @Nullable
    private Favicon favicon;
    private boolean onlineMode;
    private boolean whitelist;

    private volatile boolean shuttingDown;

    @Inject
    private LanternServer() {
    }

    /**
     * Gets the {@link LanternGame} instance.
     *
     * @return The game
     */
    public LanternGame getGame() {
        return this.game;
    }

    void initialize() {
        // First initialize the console manager, but don't start to read anything yet
        this.consoleManager.init();

        this.logger.info("Starting Lantern Server {}",
                firstNonNull(InternalPluginsInfo.Implementation.VERSION, ""));
        this.logger.info("   for  Minecraft {} with protocol version {}", LanternMinecraftVersion.CURRENT.getName(),
                LanternMinecraftVersion.CURRENT.getProtocol());
        this.logger.info("   with SpongeAPI {}", firstNonNull(InternalPluginsInfo.Api.VERSION, ""));

        try {
            this.game.initialize();
        } catch (IOException e) {
            throw new IllegalStateException("Failed to Pre Initialize the Game.", e);
        }
    }

    void start() throws IOException {
        final GlobalConfig globalConfig = this.game.getGlobalConfig();
        // Enable the query server if needed
        if (globalConfig.isQueryEnabled()) {
            this.queryServer = new QueryServer(this.game, globalConfig.getShowPluginsToQuery());
        }
        // Enable the rcon server if needed
        if (globalConfig.isRconEnabled()) {
            this.rconServer = new RconServer(globalConfig.getRconPassword());
            this.game.getServiceManager().setProvider(this.pluginContainer, RconService.class, this.rconServer);
        }
        if (globalConfig.getProxyType() == ProxyType.NONE && !globalConfig.isOnlineMode()) {
            this.logger.warn("It is not recommend to run the server in offline mode, this allows people to");
            this.logger.warn("choose any username they want. The server does will use the account attached");
            this.logger.warn("to the username, it doesn't care if it's in offline mode, this will only");
            this.logger.warn("disable the authentication and allow non registered usernames to be used.");
        }

        this.consoleManager.start();

        try {
            bind();
        } catch (BindException e) {
            // descriptive bind error messages
            this.logger.error("The server could not bind to the requested address.");
            if (e.getMessage().startsWith("Cannot assign requested address")) {
                this.logger.error("The 'server.ip' in your global.conf file may not be valid.");
                this.logger.error("Unless you are sure you need it, try removing it.");
                this.logger.error(e.toString());
            } else if (e.getMessage().startsWith("Address already in use")) {
                this.logger.error("The address was already in use. Check that no server is");
                this.logger.error("already running on that port. If needed, try killing all");
                this.logger.error("Java processes using Task Manager or similar.");
                this.logger.error(e.toString());
            } else {
                this.logger.error("An unknown bind error has occurred.", e);
            }
            System.exit(1);
            return;
        }
        bindQuery();
        bindRcon();

        this.logger.info("Ready for connections.");
        this.worldManager.init();

        final Cause gameCause = Cause.source(this.game).build();

        this.game.postGameStateChange(SpongeEventFactory.createGameAboutToStartServerEvent(gameCause));
        this.game.postGameStateChange(SpongeEventFactory.createGameStartingServerEvent(gameCause));

        final GlobalConfig config = this.game.getGlobalConfig();
        this.maxPlayers = config.getMaxPlayers();
        this.onlineMode = config.isOnlineMode();

        final Path faviconPath = Paths.get(config.getFavicon());
        if (Files.exists(faviconPath)) {
            try {
                this.favicon = LanternFavicon.load(faviconPath);
            } catch (IOException e) {
                this.logger.error("Failed to load the favicon", e);
            }
        }

        final String resourcePackPath = config.getDefaultResourcePack();
        if (!resourcePackPath.isEmpty()) {
            try {
                this.resourcePack = ResourcePacks.fromUri(URI.create(resourcePackPath));
            } catch (FileNotFoundException e) {
                this.logger.warn("Couldn't find a valid resource pack at the location: {}", resourcePackPath, e);
            }
        }

        this.executor.scheduleAtFixedRate(() -> {
            try {
                pulse();
            } catch (Exception e) {
                this.logger.error("Error while pulsing", e);
            }
        }, 0, LanternGame.TICK_DURATION, TimeUnit.MILLISECONDS);

        this.game.postGameStateChange(SpongeEventFactory.createGameStartedServerEvent(gameCause));
    }

    /**
     * Get the socket address to bind to for a specified service.
     * 
     * @param port the port to use
     * @return the socket address
     */
    private InetSocketAddress getBindAddress(int port) {
        final String ip = this.game.getGlobalConfig().getServerIp();
        if (ip.length() == 0) {
            return new InetSocketAddress(port);
        } else {
            return new InetSocketAddress(ip, port);
        }
    }

    private void bind() throws BindException {
        final InetSocketAddress address = this.getBindAddress(this.game.getGlobalConfig().getServerPort());
        final boolean useEpollWhenAvailable = this.game.getGlobalConfig().useServerEpollWhenAvailable();

        ProtocolState.init();
        final ChannelFuture future = this.networkManager.init(address, useEpollWhenAvailable);
        final Channel channel = future.awaitUninterruptibly().channel();
        if (!channel.isActive()) {
            final Throwable cause = future.cause();
            if (cause instanceof BindException) {
                throw (BindException) cause;
            }
            throw new RuntimeException("Failed to bind to address", cause);
        }

        this.logger.info("Successfully bound to: " + channel.localAddress());
    }

    private void bindQuery() {
        if (this.queryServer == null) {
            return;
        }

        final InetSocketAddress address = getBindAddress(this.game.getGlobalConfig().getQueryPort());
        final boolean useEpollWhenAvailable = this.game.getGlobalConfig().useQueryEpollWhenAvailable();
        this.game.getLogger().info("Binding query to address: " + address + "...");

        final ChannelFuture future = this.queryServer.init(address, useEpollWhenAvailable);
        final Channel channel = future.awaitUninterruptibly().channel();
        if (!channel.isActive()) {
            this.game.getLogger().warn("Failed to bind query. Address already in use?");
        }
    }

    private void bindRcon() {
        if (this.rconServer == null) {
            return;
        }

        final InetSocketAddress address = this.getBindAddress(this.game.getGlobalConfig().getRconPort());
        final boolean useEpollWhenAvailable = this.game.getGlobalConfig().useRconEpollWhenAvailable();
        this.game.getLogger().info("Binding rcon to address: " + address + "...");

        final ChannelFuture future = this.rconServer.init(address, useEpollWhenAvailable);
        final Channel channel = future.awaitUninterruptibly().channel();
        if (!channel.isActive()) {
            this.game.getLogger().warn("Failed to bind rcon. Address already in use?");
        }
    }

    /**
     * Pulses (ticks) the game.
     */
    private void pulse() {
        this.runningTimeTicks.incrementAndGet();
        // Pulse the network sessions
        this.networkManager.pulseSessions();
        // Pulse the sync scheduler tasks
        this.game.getScheduler().pulseSyncScheduler();
        // Pulse the world threads
        this.worldManager.pulse();
        AdvancementTrees.INSTANCE.pulse();
    }

    /**
     * Gets the key pair.
     * 
     * @return the key pair
     */
    public KeyPair getKeyPair() {
        return this.keyPair;
    }

    /**
     * Gets the favicon of the server.
     * 
     * @return the favicon
     */
    public Optional<Favicon> getFavicon() {
        return Optional.ofNullable(this.favicon);
    }

    /**
     * Adds a {@link Player} to the online players lookups.
     *
     * @param player The player
     */
    public void addPlayer(LanternPlayer player) {
        this.playersByName.put(player.getName(), player);
        this.playersByUUID.put(player.getUniqueId(), player);
    }

    /**
     * Removes a {@link Player} from the online players lookups.
     *
     * @param player The player
     */
    public void removePlayer(LanternPlayer player) {
        this.playersByName.remove(player.getName());
        this.playersByUUID.remove(player.getUniqueId());
    }

    /**
     * Gets a raw collection with all the players.
     *
     * @return The players
     */
    public Collection<LanternPlayer> getRawOnlinePlayers() {
        return this.unmodifiablePlayers;
    }

    @Override
    public Collection<Player> getOnlinePlayers() {
        return ImmutableList.copyOf(this.playersByName.values());
    }

    @Override
    public int getMaxPlayers() {
        return this.maxPlayers;
    }

    @Override
    public Optional<Player> getPlayer(UUID uniqueId) {
        return Optional.ofNullable(this.playersByUUID.get(checkNotNull(uniqueId, "uniqueId")));
    }

    @Override
    public Optional<Player> getPlayer(String name) {
        return Optional.ofNullable(this.playersByName.get(checkNotNull(name, "name")));
    }

    @Override
    public Collection<World> getWorlds() {
        return this.worldManager.getWorlds();
    }

    @Override
    public Collection<WorldProperties> getUnloadedWorlds() {
        return this.worldManager.getUnloadedWorlds();
    }

    @Override
    public Collection<WorldProperties> getAllWorldProperties() {
        return this.worldManager.getAllWorldProperties();
    }

    @Override
    public Optional<World> getWorld(UUID uniqueId) {
        return this.worldManager.getWorld(uniqueId);
    }

    @Override
    public Optional<World> getWorld(String worldName) {
        return this.worldManager.getWorld(worldName);
    }

    @Override
    public Optional<WorldProperties> getDefaultWorld() {
        return this.worldManager.getDefaultWorld();
    }

    @Override
    public String getDefaultWorldName() {
        return this.game.getGlobalConfig().getRootWorldFolder();
    }

    @Override
    public Optional<World> loadWorld(String worldName) {
        return this.worldManager.loadWorld(worldName);
    }

    @Override
    public Optional<World> loadWorld(UUID uniqueId) {
        return this.worldManager.loadWorld(uniqueId);
    }

    @Override
    public Optional<World> loadWorld(WorldProperties properties) {
        return this.worldManager.loadWorld(properties);
    }

    @Override
    public Optional<WorldProperties> getWorldProperties(String worldName) {
        return this.worldManager.getWorldProperties(worldName);
    }

    @Override
    public Optional<WorldProperties> getWorldProperties(UUID uniqueId) {
        return this.worldManager.getWorldProperties(uniqueId);
    }

    @Override
    public boolean unloadWorld(World world) {
        return this.worldManager.unloadWorld(world);
    }

    @Override
    public WorldProperties createWorldProperties(String folderName, WorldArchetype worldArchetype)
            throws IOException {
        return this.worldManager.createWorldProperties(folderName, worldArchetype);
    }

    @Override
    public CompletableFuture<Optional<WorldProperties>> copyWorld(WorldProperties worldProperties,
            String copyName) {
        return this.worldManager.copyWorld(worldProperties, copyName);
    }

    @Override
    public Optional<WorldProperties> renameWorld(WorldProperties worldProperties, String newName) {
        return this.worldManager.renameWorld(worldProperties, newName);
    }

    @Override
    public CompletableFuture<Boolean> deleteWorld(WorldProperties worldProperties) {
        return this.worldManager.deleteWorld(worldProperties);
    }

    @Override
    public boolean saveWorldProperties(WorldProperties properties) {
        return this.worldManager.saveWorldProperties(properties);
    }

    @Override
    public Optional<Scoreboard> getServerScoreboard() {
        return Optional.empty();
    }

    @Override
    public ChunkLayout getChunkLayout() {
        return LanternChunkLayout.INSTANCE;
    }

    @Override
    public int getRunningTimeTicks() {
        return this.runningTimeTicks.get();
    }

    @Override
    public MessageChannel getBroadcastChannel() {
        return this.broadcastChannel;
    }

    @Override
    public void setBroadcastChannel(MessageChannel channel) {
        this.broadcastChannel = checkNotNull(channel, "channel");
    }

    @Override
    public Optional<InetSocketAddress> getBoundAddress() {
        return this.networkManager.getAddress().filter(a -> a instanceof InetSocketAddress)
                .map(a -> (InetSocketAddress) a);
    }

    @Override
    public boolean hasWhitelist() {
        return this.whitelist;
    }

    @Override
    public void setHasWhitelist(boolean enabled) {
        this.whitelist = enabled;
    }

    @Override
    public boolean getOnlineMode() {
        return this.onlineMode;
    }

    @Override
    public Text getMotd() {
        return this.game.getGlobalConfig().getMotd();
    }

    @Override
    public void shutdown() {
        shutdown(this.game.getGlobalConfig().getShutdownMessage());
    }

    @SuppressWarnings("deprecation")
    @Override
    public void shutdown(Text kickMessage) {
        checkNotNull(kickMessage, "kickMessage");
        if (this.shuttingDown) {
            return;
        }
        this.shuttingDown = true;

        final Cause gameCause = Cause.source(this.game).build();
        this.game.postGameStateChange(SpongeEventFactory.createGameStoppingServerEvent(gameCause));

        // Debug a message
        this.logger.info("Stopping the server... ({})", LanternTexts.toLegacy(kickMessage));

        // Stop the console
        this.consoleManager.shutdown();

        // Kick all the online players
        getOnlinePlayers().forEach(player -> ((LanternPlayer) player).getConnection().disconnect(kickMessage));

        // Stop the network servers - starts the shutdown process
        // It may take a second or two for Netty to totally clean up
        this.networkManager.shutdown();

        if (this.queryServer != null) {
            this.queryServer.shutdown();
        }
        if (this.rconServer != null) {
            this.rconServer.shutdown();
        }

        // Stop the world manager
        this.worldManager.shutdown();

        // Shutdown the executor
        this.executor.shutdown();

        // Stop the async scheduler
        this.game.getScheduler().shutdownAsyncScheduler(5, TimeUnit.SECONDS);

        final Collection<ProviderRegistration<?>> serviceRegistrations;
        try {
            final ServiceManager serviceManager = this.game.getServiceManager();
            checkState(serviceManager instanceof SimpleServiceManager
                    || serviceManager instanceof LanternServiceManager);

            final Field field = (serviceManager instanceof SimpleServiceManager ? SimpleServiceManager.class
                    : LanternServiceManager.class).getDeclaredField("providers");
            field.setAccessible(true);

            //noinspection unchecked
            final Map<Class<?>, ProviderRegistration<?>> map = (Map<Class<?>, ProviderRegistration<?>>) field
                    .get(serviceManager);
            serviceRegistrations = map.values();
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new IllegalStateException(e);
        }

        // Close all the services if possible
        serviceRegistrations.forEach(provider -> {
            final Object service = provider.getProvider();
            if (service instanceof CloseableService) {
                try {
                    ((CloseableService) service).close();
                } catch (Exception e) {
                    this.logger.error("A error occurred while closing the {}.", provider.getService().getName(), e);
                }
            }
        });

        // Shutdown the game profile manager
        this.game.getGameProfileManager().getDefaultCache().save();
        final GameProfileCache cache = this.game.getGameProfileManager().getCache();
        if (cache instanceof CloseableService) {
            try {
                ((CloseableService) cache).close();
            } catch (Exception e) {
                this.logger.error("A error occurred while closing the GameProfileCache.", e);
            }
        }

        try {
            this.game.getOpsConfig().save();
        } catch (IOException e) {
            this.logger.error("A error occurred while saving the ops config.", e);
        }

        this.game.postGameStateChange(SpongeEventFactory.createGameStoppedServerEvent(gameCause));
        this.game.postGameStateChange(SpongeEventFactory.createGameStoppingEvent(gameCause));
        this.game.postGameStateChange(SpongeEventFactory.createGameStoppedEvent(gameCause));

        // Wait for a while and terminate any rogue threads
        new ShutdownMonitorThread().start();
    }

    @Override
    public ConsoleSource getConsole() {
        return LanternConsoleSource.INSTANCE;
    }

    @Override
    public ChunkTicketManager getChunkTicketManager() {
        return this.game.getChunkTicketManager();
    }

    @Override
    public double getTicksPerSecond() {
        return LanternGame.TICKS_PER_SECOND; // TODO
    }

    @Override
    public Optional<ResourcePack> getDefaultResourcePack() {
        return Optional.ofNullable(this.resourcePack);
    }

    @Override
    public int getPlayerIdleTimeout() {
        return this.game.getGlobalConfig().getPlayerIdleTimeout();
    }

    @Override
    public void setPlayerIdleTimeout(int timeout) {
        this.game.getGlobalConfig().setPlayerIdleTimeout(timeout);
    }

    @Override
    public boolean isMainThread() {
        return Thread.currentThread() == this.mainThread;
    }

    @Override
    public GameProfileManager getGameProfileManager() {
        return this.game.getGameProfileManager();
    }

    /**
     * Gets the {@link LanternWorldManager}.
     *
     * @return The world manager
     */
    public LanternWorldManager getWorldManager() {
        return this.worldManager;
    }

}