|
|
|
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
|
|
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
|
|
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
|
|
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
|
|
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|