/* * 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.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; 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.Lantern; import org.lanternpowered.server.game.LanternGame; import org.lanternpowered.server.game.LanternPlatform; 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.EmptyRconService; import org.lanternpowered.server.network.rcon.RconServer; import org.lanternpowered.server.network.status.LanternFavicon; 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.spongepowered.api.GameState; 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.game.state.GameAboutToStartServerEvent; import org.spongepowered.api.event.game.state.GameStartedServerEvent; import org.spongepowered.api.event.game.state.GameStartingServerEvent; import org.spongepowered.api.event.game.state.GameStoppedServerEvent; import org.spongepowered.api.event.game.state.GameStoppingServerEvent; import org.spongepowered.api.network.status.Favicon; 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.sql.SqlService; import org.spongepowered.api.text.Text; import org.spongepowered.api.text.channel.MessageChannel; import org.spongepowered.api.util.annotation.NonnullByDefault; 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.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; 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; @NonnullByDefault public class LanternServer implements Server { public static void main(String[] args) { try { // Create the game instance final LanternGame game = new LanternGame(); game.preInitialize(); final ConsoleManager consoleManager = new ConsoleManager(); // Start the console (command input/completer) consoleManager.start(); final GlobalConfig globalConfig = game.getGlobalConfig(); if (globalConfig.getProxyType() == ProxyType.NONE && !globalConfig.isOnlineMode()) { Lantern.getLogger().warn("It is not recommend to run the server in offline mode, this allows people to"); Lantern.getLogger().warn("choose any username they want. The server does will use the account attached"); Lantern.getLogger().warn("to the username, it doesn't care if it's in offline mode, this will only"); Lantern.getLogger().warn("disable the authentication and allow non registered usernames to be used."); } RconServer rconServer = null; QueryServer queryServer = null; // Enable the query server if needed if (globalConfig.isQueryEnabled()) { queryServer = new QueryServer(game, globalConfig.getShowPluginsToQuery()); } // Enable the rcon server if needed if (globalConfig.isRconEnabled()) { rconServer = new RconServer(globalConfig.getRconPassword()); } // Create the server instance final LanternServer server = new LanternServer(game, consoleManager, rconServer, queryServer); // Send some startup info Lantern.getLogger().info("Starting Lantern Server {}", LanternPlatform.IMPL_VERSION.orElse("")); Lantern.getLogger().info(" for Minecraft {} with protocol version {}", LanternMinecraftVersion.CURRENT.getName(), LanternMinecraftVersion.CURRENT.getProtocol()); Lantern.getLogger().info(" with SpongeAPI {}", LanternPlatform.API_VERSION.orElse("")); // The root world folder final Path worldFolder = new File(game.getGlobalConfig().getRootWorldFolder()).toPath(); // Initialize the game game.initialize(server, rconServer == null ? new EmptyRconService(globalConfig.getRconPassword()) : rconServer, worldFolder); // Bind the network channel server.bind(); // Bind the query server server.bindQuery(); // Bind the rcon server server.bindRcon(); // Start the server server.start(); Lantern.getLogger().info("Ready for connections."); } catch (BindException e) { // descriptive bind error messages Lantern.getLogger().error("The server could not bind to the requested address."); if (e.getMessage().startsWith("Cannot assign requested address")) { Lantern.getLogger().error("The 'server.ip' in your global.conf file may not be valid."); Lantern.getLogger().error("Unless you are sure you need it, try removing it."); Lantern.getLogger().error(e.toString()); } else if (e.getMessage().startsWith("Address already in use")) { Lantern.getLogger().error("The address was already in use. Check that no server is"); Lantern.getLogger().error("already running on that port. If needed, try killing all"); Lantern.getLogger().error("Java processes using Task Manager or similar."); Lantern.getLogger().error(e.toString()); } else { Lantern.getLogger().error("An unknown bind error has occurred.", e); } System.exit(1); } catch (Throwable t) { // general server startup crash Lantern.getLogger().error("Error during server startup.", t); System.exit(1); } } // The executor service for the server ticks private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor( runnable -> { final Thread thread = new Thread(runnable, "server"); this.mainThread = thread; return thread; }); private Thread mainThread; // The world manager private LanternWorldManager worldManager; // The network manager private final NetworkManager networkManager = new NetworkManager(this); // The rcon server/service @Nullable private final RconServer rconServer; // The query server @Nullable private final 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 game instance private final LanternGame game; // The console manager private final ConsoleManager consoleManager; // 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; public LanternServer(LanternGame game, ConsoleManager consoleManager, @Nullable RconServer rconServer, @Nullable QueryServer queryServer) { this.consoleManager = consoleManager; this.queryServer = queryServer; this.rconServer = rconServer; this.game = game; } /** * 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); } } public 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); } Lantern.getLogger().info("Successfully bound to: " + channel.localAddress()); } private void bindQuery() { if (this.queryServer == null) { return; } final InetSocketAddress address = this.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?"); } } public void start() throws IOException { this.worldManager = new LanternWorldManager(this.game, this.game.getSavesDirectory()); this.worldManager.init(); this.game.postGameStateChange(GameState.SERVER_ABOUT_TO_START, GameAboutToStartServerEvent.class); this.game.postGameStateChange(GameState.SERVER_STARTING, GameStartingServerEvent.class); 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) { Lantern.getLogger().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) { Lantern.getLogger().warn("Couldn't find a valid resource pack at the location: {}", resourcePackPath, e); } } this.executor.scheduleAtFixedRate(() -> { try { pulse(); } catch (Exception e) { Lantern.getLogger().error("Error while pulsing", e); } }, 0, LanternGame.TICK_DURATION, TimeUnit.MILLISECONDS); this.game.postGameStateChange(GameState.SERVER_STARTED, GameStartedServerEvent.class); } /** * 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(); } /** * 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() { this.shutdown(this.game.getGlobalConfig().getShutdownMessage()); } @SuppressWarnings("deprecation") @Override public void shutdown(Text kickMessage) { checkNotNull(kickMessage, "kickMessage"); if (this.shuttingDown) { return; } this.shuttingDown = true; this.game.postGameStateChange(GameState.SERVER_STOPPING, GameStoppingServerEvent.class); // Debug a message Lantern.getLogger().info("Stopping the server... ({})", LanternTexts.toLegacy(kickMessage)); // Stop the console this.consoleManager.shutdown(); // Kick all the online players this.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(); // Close the sql service if possible this.game.getServiceManager().provide(SqlService.class).ifPresent(service -> { if (service instanceof Closeable) { try { ((Closeable) service).close(); } catch (IOException e) { Lantern.getLogger().error("A error occurred while closing the sql service.", e); } } }); // Shutdown the game profile manager this.game.getGameProfileManager().getDefaultCache().save(); try { this.game.getOpsConfig().save(); } catch (IOException e) { Lantern.getLogger().error("A error occurred while saving the ops config.", e); } try { this.game.getWhitelistConfig().save(); } catch (IOException e) { Lantern.getLogger().error("A error occurred while saving the white-list config.", e); } try { this.game.getBanConfig().save(); } catch (IOException e) { Lantern.getLogger().error("A error occurred while saving the bans config.", e); } this.game.postGameStateChange(GameState.SERVER_STOPPED, GameStoppedServerEvent.class); // 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() { // TODO Auto-generated method stub return 0; } @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; } }