You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
17 KiB
527 lines
17 KiB
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . |
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· |
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ |
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ |
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ |
|
// 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<ClientConnection> 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; |
|
} |
|
|
|
}
|
|
|