// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package engine.server.login; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import engine.Enum; import engine.gameManager.*; import engine.job.JobScheduler; import engine.jobs.CSessionCleanupJob; import engine.net.Network; import engine.net.client.ClientConnection; import engine.net.client.ClientConnectionManager; import engine.net.client.Protocol; import engine.net.client.msg.login.ServerStatusMsg; import engine.net.client.msg.login.VersionInfoMsg; import engine.objects.*; import engine.server.MBServerStatics; import engine.util.ByteUtils; import engine.util.ThreadUtils; import org.pmw.tinylog.Configurator; import org.pmw.tinylog.Level; import org.pmw.tinylog.Logger; import org.pmw.tinylog.labelers.TimestampLabeler; import org.pmw.tinylog.policies.StartupPolicy; import org.pmw.tinylog.writers.RollingFileWriter; import java.io.*; import java.net.InetAddress; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDateTime; import java.util.Iterator; import static java.lang.System.exit; public class LoginServer { // Instance variables private VersionInfoMsg versionInfoMessage; public static HikariDataSource connectionPool = null; public static int population = 0; public static boolean worldServerRunning = false; public static boolean loginServerRunning = false; public static ServerStatusMsg serverStatusMsg = new ServerStatusMsg(0, (byte) 1); // This is the entrypoint for the MagicBane Login Server when // it is executed by the command line scripts. The fun begins here! public static void main(String[] args) { LoginServer loginServer; // Initialize TinyLog logger with our own format Configurator.defaultConfig() .addWriter(new RollingFileWriter("logs/login/login.txt", 30, new TimestampLabeler(), new StartupPolicy())) .level(Level.DEBUG) .formatPattern("{level} {date:yyyy-MM-dd HH:mm:ss.SSS} [{thread}] {class}.{method}({line}) : {message}") .activate(); try { // Configure the the Login Server loginServer = new LoginServer(); ConfigManager.loginServer = loginServer; ConfigManager.handler = new LoginServerMsgHandler(loginServer); ConfigManager.serverType = Enum.ServerType.LOGINSERVER; if (ConfigManager.init() == false) { Logger.error("ABORT! Missing config entry!"); return; } // Start the Login Server loginServer.init(); loginServer.exec(); exit(0); } catch (Exception e) { Logger.error(e); e.printStackTrace(); exit(1); } } // Mainline execution loop for the login server. private void exec() { LocalDateTime nextCacheTime = LocalDateTime.now(); LocalDateTime nextServerTime = LocalDateTime.now(); LocalDateTime nextDatabaseTime = LocalDateTime.now(); loginServerRunning = true; while (true) { // Invalidate cache for players driven by forum // and stored procedure forum_link_pass() try { // Run cache routine right away if requested. File cacheFile = new File("cacheInvalid"); if (cacheFile.exists() == true) { nextCacheTime = LocalDateTime.now(); Files.deleteIfExists(Paths.get("cacheInvalid")); } if (LocalDateTime.now().isAfter(nextCacheTime)) { invalidateCacheList(); nextCacheTime = LocalDateTime.now().plusSeconds(30); } if (LocalDateTime.now().isAfter(nextServerTime)) { checkServerHealth(); nextServerTime = LocalDateTime.now().plusSeconds(1); } if (LocalDateTime.now().isAfter(nextDatabaseTime)) { String pop = SimulationManager.getPopulationString(); Logger.info("Keepalive: " + pop); nextDatabaseTime = LocalDateTime.now().plusMinutes(30); } ThreadUtils.sleep(100); } catch (Exception e) { Logger.error(e); e.printStackTrace(); } } } // Constructor public LoginServer() { } private boolean init() { // Initialize Application Protocol Protocol.initProtocolLookup(); // Configure the VersionInfoMsgs: this.versionInfoMessage = new VersionInfoMsg(ConfigManager.MB_MAJOR_VER.getValue(), ConfigManager.MB_MINOR_VER.getValue()); Logger.info("Initializing Database Pool"); initDatabasePool(); Logger.info("Initializing Database layer"); initDatabaseLayer(); Logger.info("Initializing Network"); Network.init(); Logger.info("Initializing Client Connection Manager"); initClientConnectionManager(); // instantiate AccountManager Logger.info("Initializing SessionManager."); // Sets cross server behavior SessionManager.setCrossServerBehavior(0); // activate powers manager Logger.info("Initializing PowersManager."); PowersManager.initPowersManager(false); RuneBaseAttribute.LoadAllAttributes(); RuneBase.LoadAllRuneBases(); BaseClass.LoadAllBaseClasses(); Race.loadAllRaces(); RuneBaseEffect.LoadRuneBaseEffects(); Logger.info("Initializing Blueprint data."); Blueprint.loadAllBlueprints(); Logger.info("Loading Kits"); DbManager.KitQueries.GET_ALL_KITS(); Logger.info("Initializing ItemBase data."); ItemBase.loadAllItemBases(); Logger.info("Initializing Race data"); Enum.RaceType.initRaceTypeTables(); Race.loadAllRaces(); Logger.info("Initializing Errant Guild"); Guild.CreateErrantGuild(); Logger.info("Loading All Guilds"); DbManager.GuildQueries.GET_ALL_GUILDS(); Logger.info("***Boot Successful***"); return true; } private boolean initDatabaseLayer() { // Try starting a GOM <-> DB connection. try { Logger.info("Configuring GameObjectManager to use Database: '" + ConfigManager.MB_DATABASE_NAME.getValue() + "' on " + ConfigManager.MB_DATABASE_ADDRESS.getValue() + ':' + ConfigManager.MB_DATABASE_PORT.getValue()); DbManager.configureDatabaseLayer(); } catch (Exception e) { Logger.error(e.getMessage()); return false; } PreparedStatementShared.submitPreparedStatementsCleaningJob(); if (MBServerStatics.DB_DEBUGGING_ON_BY_DEFAULT) { PreparedStatementShared.enableDebugging(); } return true; } public void removeClient(ClientConnection conn) { if (conn == null) { Logger.info( "ClientConnection null in removeClient."); return; } String key = ByteUtils.byteArrayToSafeStringHex(conn .getSecretKeyBytes()); CSessionCleanupJob cscj = new CSessionCleanupJob(key); JobScheduler.getInstance().scheduleJob(cscj, MBServerStatics.SESSION_CLEANUP_TIMER_MS); } private void initClientConnectionManager() { try { String name = ConfigManager.MB_WORLD_NAME.getValue(); if (ConfigManager.MB_PUBLIC_ADDR.getValue().equals("0.0.0.0")) { // Autoconfigure IP address for use in worldserver response // . Logger.info("AUTOCONFIG PUBLIC IP ADDRESS"); URL whatismyip = new URL("http://checkip.amazonaws.com"); BufferedReader in = new BufferedReader(new InputStreamReader( whatismyip.openStream())); ConfigManager.MB_PUBLIC_ADDR.setValue(in.readLine()); } Logger.info("Public address: " + ConfigManager.MB_PUBLIC_ADDR.getValue()); Logger.info("Magicbane bind config: " + ConfigManager.MB_BIND_ADDR.getValue() + ":" + ConfigManager.MB_LOGIN_PORT.getValue()); InetAddress addy = InetAddress.getByName(ConfigManager.MB_BIND_ADDR.getValue()); int port = Integer.parseInt(ConfigManager.MB_LOGIN_PORT.getValue()); ClientConnectionManager connectionManager = new ClientConnectionManager(name + ".ClientConnMan", addy, port); connectionManager.startup(); } catch (IOException e) { Logger.error(e.toString()); } } /* * message handlers (relay) */ // ============================== // Support Functions // ============================== public VersionInfoMsg getDefaultVersionInfo() { return versionInfoMessage; } //this updates a server being up or down without resending the entire char select screen. public void updateServersForAll(boolean isRunning) { try { Iterator i = SessionManager.getAllActiveClientConnections().iterator(); while (i.hasNext()) { ClientConnection clientConnection = i.next(); if (clientConnection == null) continue; Account ac = clientConnection.getAccount(); if (ac == null) continue; boolean isUp = isRunning; if (MBServerStatics.worldAccessLevel.ordinal() > ac.status.ordinal()) isUp = false; LoginServer.serverStatusMsg.setServerID(MBServerStatics.worldMapID); LoginServer.serverStatusMsg.setIsUp(isUp ? (byte) 1 : (byte) 0); clientConnection.sendMsg(LoginServer.serverStatusMsg); } } catch (Exception e) { Logger.error(e); e.printStackTrace(); } } public void checkServerHealth() { // Check if worldserver is running if (!isPortInUse(Integer.parseInt(ConfigManager.MB_WORLD_PORT.getValue()))) { worldServerRunning = false; population = 0; updateServersForAll(worldServerRunning); return; } // Worldserver is running and writes a polling file. // Read the current population count from the server and // update player displays accordingly. worldServerRunning = true; population = readPopulationFile(); updateServersForAll(worldServerRunning); } private void initDatabasePool() { HikariConfig config = new HikariConfig(); config.setMaximumPoolSize(33); // (16 cores 1 spindle) config.setJdbcUrl("jdbc:mysql://" + ConfigManager.MB_DATABASE_ADDRESS.getValue() + ":" + ConfigManager.MB_DATABASE_PORT.getValue() + "/" + ConfigManager.MB_DATABASE_NAME.getValue()); config.setUsername(ConfigManager.MB_DATABASE_USER.getValue()); config.setPassword(ConfigManager.MB_DATABASE_PASS.getValue()); config.addDataSourceProperty("characterEncoding", "utf8"); config.addDataSourceProperty("cachePrepStmts", "true"); config.addDataSourceProperty("prepStmtCacheSize", "250"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); connectionPool = new HikariDataSource(config); // setup the connection pool Logger.info("local database connection configured"); } public void invalidateCacheList() { int objectUUID; String objectType; try (Connection connection = connectionPool.getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM `login_cachelist`"); ResultSet rs = statement.executeQuery()) { while (rs.next()) { objectUUID = rs.getInt("UID"); objectType = rs.getString("type"); Logger.info("INVALIDATED : " + objectType + " UUID: " + objectUUID); switch (objectType) { case "account": DbManager.removeFromCache(Enum.GameObjectType.Account, objectUUID); break; case "character": DbManager.removeFromCache(Enum.GameObjectType.PlayerCharacter, objectUUID); PlayerCharacter player = (PlayerCharacter) DbManager.getObject(Enum.GameObjectType.PlayerCharacter, objectUUID); PlayerCharacter.initializePlayer(player); player.getAccount().characterMap.replace(player.getObjectUUID(), player); Logger.info("Player active state is : " + player.isActive()); break; } } } catch (SQLException e) { Logger.info(e.toString()); } // clear the db table try (Connection connection = connectionPool.getConnection(); PreparedStatement statement = connection.prepareStatement("DELETE FROM `login_cachelist`")) { statement.execute(); } catch (SQLException e) { Logger.info(e.toString()); } } public static boolean getActiveBaneQuery(PlayerCharacter playerCharacter) { boolean outStatus = false; // char has never logged on so cannot have dropped a bane if (playerCharacter.getHash() == null) return outStatus; // query data warehouse for unresolved bane with this character try (Connection connection = connectionPool.getConnection(); PreparedStatement statement = buildQueryActiveBaneStatement(connection, playerCharacter); ResultSet rs = statement.executeQuery()) { while (rs.next()) { outStatus = true; } } catch (SQLException e) { Logger.error(e.toString()); } return outStatus; } private static PreparedStatement buildQueryActiveBaneStatement(Connection connection, PlayerCharacter playerCharacter) throws SQLException { PreparedStatement outStatement; String queryString = "SELECT `city_id` FROM `warehouse_banehistory` WHERE `char_id` = ? AND `RESOLUTION` = 'PENDING'"; outStatement = connection.prepareStatement(queryString); outStatement.setString(1, playerCharacter.getHash()); return outStatement; } public static boolean isPortInUse(int port) { ProcessBuilder builder = new ProcessBuilder("/bin/bash", "-c", "lsof -i tcp:" + port + " | tail -n +2 | awk '{print $2}'"); builder.redirectErrorStream(true); Process process = null; String line = null; boolean portInUse = false; try { process = builder.start(); InputStream is = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); while ((line = reader.readLine()) != null) { portInUse = true; } } catch (IOException e) { e.printStackTrace(); } return portInUse; } private int readPopulationFile() { ProcessBuilder builder = new ProcessBuilder("/bin/bash", "-c", "cat " + MBServerStatics.DEFAULT_DATA_DIR + ConfigManager.MB_WORLD_NAME.getValue().replaceAll("'","") + ".pop"); builder.redirectErrorStream(true); Process process = null; String line = null; int population = 0; try { process = builder.start(); InputStream is = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); while ((line = reader.readLine()) != null) { population = Integer.parseInt(line); } } catch (IOException e) { e.printStackTrace(); return 0; } return population; } }