/* This file is part of the OdinMS Maple Story Server Copyright (C) 2008 Patrick Huy Matthias Butz Jan Christian Meyer This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation version 3 as published by the Free Software Foundation. You may not use, modify or distribute this program under any other version of the GNU Affero General Public License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package net.server; import net.server.worker.CouponWorker; import net.server.worker.RankingWorker; import java.io.FileInputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import tools.locks.MonitoredReentrantLock; import java.util.concurrent.locks.Lock; import net.MapleServerHandler; import net.mina.MapleCodecFactory; import net.server.channel.Channel; import net.server.guild.MapleAlliance; import net.server.guild.MapleGuild; import net.server.guild.MapleGuildCharacter; import net.server.world.World; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.buffer.SimpleBufferAllocator; import org.apache.mina.core.filterchain.IoFilter; import org.apache.mina.core.service.IoAcceptor; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.transport.socket.nio.NioSocketAcceptor; import server.CashShop.CashItemFactory; import server.TimerManager; import tools.DatabaseConnection; import tools.FilePrinter; import tools.Pair; import client.MapleClient; import client.MapleCharacter; import client.SkillFactory; import client.newyear.NewYearCardRecord; import constants.ItemConstants; import constants.ServerConstants; import java.security.Security; import java.util.Calendar; import net.server.audit.ThreadTracker; import server.quest.MapleQuest; import tools.locks.MonitoredLockType; import tools.AutoJCE; public class Server { private static final Set activeFly = new HashSet<>(); private static final Map couponRates = new HashMap<>(30); private static final List activeCoupons = new LinkedList<>(); private IoAcceptor acceptor; private List> channels = new LinkedList<>(); private List worlds = new ArrayList<>(); private final Properties subnetInfo = new Properties(); private static Server instance = null; private final Map> accountChars = new HashMap<>(); private final Map transitioningChars = new HashMap<>(); private List> worldRecommendedList = new LinkedList<>(); private final Map guilds = new HashMap<>(100); private final Map inLoginState = new HashMap<>(100); private final Lock srvLock = new MonitoredReentrantLock(MonitoredLockType.SERVER); private final Lock lgnLock = new MonitoredReentrantLock(MonitoredLockType.SERVER); private final PlayerBuffStorage buffStorage = new PlayerBuffStorage(); private final Map alliances = new HashMap<>(100); private final Map newyears = new HashMap<>(); private boolean online = false; public static long uptime = System.currentTimeMillis(); public static Server getInstance() { if (instance == null) { instance = new Server(); } return instance; } public boolean isOnline() { return online; } public List> worldRecommendedList() { return worldRecommendedList; } public void setNewYearCard(NewYearCardRecord nyc) { newyears.put(nyc.getId(), nyc); } public NewYearCardRecord getNewYearCard(int cardid) { return newyears.get(cardid); } public NewYearCardRecord removeNewYearCard(int cardid) { return newyears.remove(cardid); } /* public void removeChannel(int worldid, int channel) { //lol don't! channels.remove(channel); World world = worlds.get(worldid); if (world != null) { world.removeChannel(channel); } } */ public Channel getChannel(int world, int channel) { return worlds.get(world).getChannel(channel); } public List getChannelsFromWorld(int world) { return worlds.get(world).getChannels(); } public List getAllChannels() { List channelz = new ArrayList<>(); for (World world : worlds) { for (Channel ch : world.getChannels()) { channelz.add(ch); } } return channelz; } public String getIP(int world, int channel) { return channels.get(world).get(channel); } private long getTimeLeftForNextHour() { Calendar nextHour = Calendar.getInstance(); nextHour.add(Calendar.HOUR, 1); nextHour.set(Calendar.MINUTE, 0); nextHour.set(Calendar.SECOND, 0); return Math.max(0, nextHour.getTimeInMillis() - System.currentTimeMillis()); } public Map getCouponRates() { return couponRates; } private void loadCouponRates(Connection c) throws SQLException { PreparedStatement ps = c.prepareStatement("SELECT couponid, rate FROM nxcoupons"); ResultSet rs = ps.executeQuery(); while(rs.next()) { int cid = rs.getInt("couponid"); int rate = rs.getInt("rate"); couponRates.put(cid, rate); } rs.close(); ps.close(); } public List getActiveCoupons() { synchronized(activeCoupons) { return activeCoupons; } } public void commitActiveCoupons() { for(World world: getWorlds()) { for(MapleCharacter chr: world.getPlayerStorage().getAllCharacters()) { if(!chr.isLoggedin()) continue; chr.updateCouponRates(); } } } public void toggleCoupon(Integer couponId) { if(ItemConstants.isRateCoupon(couponId)) { synchronized(activeCoupons) { if(activeCoupons.contains(couponId)) { activeCoupons.remove(couponId); } else { activeCoupons.add(couponId); } commitActiveCoupons(); } } } public void updateActiveCoupons() throws SQLException { synchronized(activeCoupons) { activeCoupons.clear(); Calendar c = Calendar.getInstance(); int weekDay = c.get(Calendar.DAY_OF_WEEK); int hourDay = c.get(Calendar.HOUR_OF_DAY); Connection con = null; try { con = DatabaseConnection.getConnection(); int weekdayMask = (1 << weekDay); PreparedStatement ps = con.prepareStatement("SELECT couponid FROM nxcoupons WHERE (activeday & ?) = ? AND starthour <= ? AND endhour > ?"); ps.setInt(1, weekdayMask); ps.setInt(2, weekdayMask); ps.setInt(3, hourDay); ps.setInt(4, hourDay); ResultSet rs = ps.executeQuery(); while(rs.next()) { activeCoupons.add(rs.getInt("couponid")); } rs.close(); ps.close(); con.close(); } catch (SQLException ex) { ex.printStackTrace(); try { if(con != null && !con.isClosed()) { con.close(); } } catch (SQLException ex2) { ex2.printStackTrace(); } } } } public void init() { Properties p = new Properties(); try { p.load(new FileInputStream("world.ini")); } catch (Exception e) { e.printStackTrace(); System.out.println("Please start create_server.bat"); System.exit(0); } System.out.println("HeavenMS v" + ServerConstants.VERSION + " starting up.\r\n"); if(ServerConstants.SHUTDOWNHOOK) Runtime.getRuntime().addShutdownHook(new Thread(shutdown(false))); Connection c = null; try { c = DatabaseConnection.getConnection(); PreparedStatement ps = c.prepareStatement("UPDATE accounts SET loggedin = 0"); ps.executeUpdate(); ps.close(); ps = c.prepareStatement("UPDATE characters SET HasMerchant = 0"); ps.executeUpdate(); ps.close(); loadCouponRates(c); updateActiveCoupons(); c.close(); } catch (SQLException sqle) { sqle.printStackTrace(); } IoBuffer.setUseDirectBuffer(false); IoBuffer.setAllocator(new SimpleBufferAllocator()); acceptor = new NioSocketAcceptor(); acceptor.getFilterChain().addLast("codec", (IoFilter) new ProtocolCodecFilter(new MapleCodecFactory())); TimerManager tMan = TimerManager.getInstance(); tMan.start(); tMan.register(tMan.purge(), ServerConstants.PURGING_INTERVAL);//Purging ftw... disconnectIdlesOnLoginTask(); long timeLeft = getTimeLeftForNextHour(); tMan.register(new CouponWorker(), ServerConstants.COUPON_INTERVAL, timeLeft); tMan.register(new RankingWorker(), ServerConstants.RANKING_INTERVAL, timeLeft); long timeToTake = System.currentTimeMillis(); SkillFactory.loadAllSkills(); System.out.println("Skills loaded in " + ((System.currentTimeMillis() - timeToTake) / 1000.0) + " seconds"); timeToTake = System.currentTimeMillis(); //MapleItemInformationProvider.getInstance().getAllItems(); //unused, rofl CashItemFactory.getSpecialCashItems(); System.out.println("Items loaded in " + ((System.currentTimeMillis() - timeToTake) / 1000.0) + " seconds"); timeToTake = System.currentTimeMillis(); MapleQuest.loadAllQuest(); System.out.println("Quest loaded in " + ((System.currentTimeMillis() - timeToTake) / 1000.0) + " seconds\r\n"); NewYearCardRecord.startPendingNewYearCardRequests(); if(ServerConstants.USE_THREAD_TRACKER) ThreadTracker.getInstance().registerThreadTrackerTask(); try { Integer worldCount = Math.min(ServerConstants.WORLD_NAMES.length, Integer.parseInt(p.getProperty("worlds"))); for (int i = 0; i < worldCount; i++) { System.out.println("Starting world " + i); World world = new World(i, Integer.parseInt(p.getProperty("flag" + i)), p.getProperty("eventmessage" + i), ServerConstants.EXP_RATE, ServerConstants.DROP_RATE, ServerConstants.MESO_RATE, ServerConstants.QUEST_RATE); worldRecommendedList.add(new Pair<>(i, p.getProperty("whyamirecommended" + i))); worlds.add(world); channels.add(new HashMap()); for (int j = 0; j < Integer.parseInt(p.getProperty("channels" + i)); j++) { int channelid = j + 1; Channel channel = new Channel(i, channelid); world.addChannel(channel); channels.get(i).put(channelid, channel.getIP()); } world.setServerMessage(p.getProperty("servermessage" + i)); System.out.println("Finished loading world " + i + "\r\n"); } } catch (Exception e) { e.printStackTrace();//For those who get errors System.out.println("Error in moople.ini, start CreateINI.bat to re-make the file."); System.exit(0); } acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 30); acceptor.setHandler(new MapleServerHandler()); try { acceptor.bind(new InetSocketAddress(8484)); } catch (IOException ex) { ex.printStackTrace(); } System.out.println("Listening on port 8484\r\n\r\n"); System.out.println("HeavenMS is now online.\r\n"); online = true; } public void shutdown() { try { TimerManager.getInstance().stop(); acceptor.unbind(); } catch (NullPointerException e) { FilePrinter.printError(FilePrinter.EXCEPTION_CAUGHT, e); } System.out.println("Server offline."); System.exit(0);// BOEIEND :D } public static void main(String args[]) { System.setProperty("wzpath", "wz"); Security.setProperty("crypto.policy", "unlimited"); AutoJCE.removeCryptographyRestrictions(); Server.getInstance().init(); } public Properties getSubnetInfo() { return subnetInfo; } public MapleAlliance getAlliance(int id) { synchronized (alliances) { if (alliances.containsKey(id)) { return alliances.get(id); } return null; } } public void addAlliance(int id, MapleAlliance alliance) { synchronized (alliances) { if (!alliances.containsKey(id)) { alliances.put(id, alliance); } } } public void disbandAlliance(int id) { synchronized (alliances) { MapleAlliance alliance = alliances.get(id); if (alliance != null) { for (Integer gid : alliance.getGuilds()) { guilds.get(gid).setAllianceId(0); } alliances.remove(id); } } } public void allianceMessage(int id, final byte[] packet, int exception, int guildex) { MapleAlliance alliance = alliances.get(id); if (alliance != null) { for (Integer gid : alliance.getGuilds()) { if (guildex == gid) { continue; } MapleGuild guild = guilds.get(gid); if (guild != null) { guild.broadcast(packet, exception); } } } } public boolean addGuildtoAlliance(int aId, int guildId) { MapleAlliance alliance = alliances.get(aId); if (alliance != null) { alliance.addGuild(guildId); guilds.get(guildId).setAllianceId(aId); return true; } return false; } public boolean removeGuildFromAlliance(int aId, int guildId) { MapleAlliance alliance = alliances.get(aId); if (alliance != null) { alliance.removeGuild(guildId); guilds.get(guildId).setAllianceId(0); return true; } return false; } public boolean setAllianceRanks(int aId, String[] ranks) { MapleAlliance alliance = alliances.get(aId); if (alliance != null) { alliance.setRankTitle(ranks); return true; } return false; } public boolean setAllianceNotice(int aId, String notice) { MapleAlliance alliance = alliances.get(aId); if (alliance != null) { alliance.setNotice(notice); return true; } return false; } public boolean increaseAllianceCapacity(int aId, int inc) { MapleAlliance alliance = alliances.get(aId); if (alliance != null) { alliance.increaseCapacity(inc); return true; } return false; } public Set getChannelServer(int world) { return new HashSet<>(channels.get(world).keySet()); } public byte getHighestChannelId() { byte highest = 0; for (Iterator it = channels.get(0).keySet().iterator(); it.hasNext();) { Integer channel = it.next(); if (channel != null && channel.intValue() > highest) { highest = channel.byteValue(); } } return highest; } public int createGuild(int leaderId, String name) { return MapleGuild.createGuild(leaderId, name); } public MapleGuild getGuildByName(String name) { synchronized (guilds) { for(MapleGuild mg: guilds.values()) { if(mg.getName().equalsIgnoreCase(name)) { return mg; } } return null; } } public MapleGuild getGuild(int id) { synchronized (guilds) { if (guilds.get(id) != null) { return guilds.get(id); } return null; } } public MapleGuild getGuild(int id, int world) { return getGuild(id, world, null); } public MapleGuild getGuild(int id, int world, MapleCharacter mc) { synchronized (guilds) { if (guilds.get(id) != null) { return guilds.get(id); } MapleGuild g = new MapleGuild(id, world); if (g.getId() == -1) { return null; } if(mc != null) { mc.setMGC(g.getMGC(mc.getId())); if(g.getMGC(mc.getId()) == null) System.out.println("null for " + mc.getName() + " when loading " + id); g.getMGC(mc.getId()).setCharacter(mc); g.setOnline(mc.getId(), true, mc.getClient().getChannel()); } guilds.put(id, g); return g; } } public void clearGuilds() {//remake synchronized (guilds) { guilds.clear(); } //for (List world : worlds.values()) { //reloadGuildCharacters(); } public void setGuildMemberOnline(MapleCharacter mc, boolean bOnline, int channel) { MapleGuild g = getGuild(mc.getGuildId(), mc.getWorld(), mc); g.setOnline(mc.getId(), bOnline, channel); } public int addGuildMember(MapleGuildCharacter mgc, MapleCharacter chr) { MapleGuild g = guilds.get(mgc.getGuildId()); if (g != null) { return g.addGuildMember(mgc, chr); } return 0; } public boolean setGuildAllianceId(int gId, int aId) { MapleGuild guild = guilds.get(gId); if (guild != null) { guild.setAllianceId(aId); return true; } return false; } public void resetAllianceGuildPlayersRank(int gId) { guilds.get(gId).resetAllianceGuildPlayersRank(); } public void leaveGuild(MapleGuildCharacter mgc) { MapleGuild g = guilds.get(mgc.getGuildId()); if (g != null) { g.leaveGuild(mgc); } } public void guildChat(int gid, String name, int cid, String msg) { MapleGuild g = guilds.get(gid); if (g != null) { g.guildChat(name, cid, msg); } } public void changeRank(int gid, int cid, int newRank) { MapleGuild g = guilds.get(gid); if (g != null) { g.changeRank(cid, newRank); } } public void expelMember(MapleGuildCharacter initiator, String name, int cid) { MapleGuild g = guilds.get(initiator.getGuildId()); if (g != null) { g.expelMember(initiator, name, cid); } } public void setGuildNotice(int gid, String notice) { MapleGuild g = guilds.get(gid); if (g != null) { g.setGuildNotice(notice); } } public void memberLevelJobUpdate(MapleGuildCharacter mgc) { MapleGuild g = guilds.get(mgc.getGuildId()); if (g != null) { g.memberLevelJobUpdate(mgc); } } public void changeRankTitle(int gid, String[] ranks) { MapleGuild g = guilds.get(gid); if (g != null) { g.changeRankTitle(ranks); } } public void setGuildEmblem(int gid, short bg, byte bgcolor, short logo, byte logocolor) { MapleGuild g = guilds.get(gid); if (g != null) { g.setGuildEmblem(bg, bgcolor, logo, logocolor); } } public void disbandGuild(int gid) { synchronized (guilds) { MapleGuild g = guilds.get(gid); g.disbandGuild(); guilds.remove(gid); } } public boolean increaseGuildCapacity(int gid) { MapleGuild g = guilds.get(gid); if (g != null) { return g.increaseCapacity(); } return false; } public void gainGP(int gid, int amount) { MapleGuild g = guilds.get(gid); if (g != null) { g.gainGP(amount); } } public void guildMessage(int gid, byte[] packet) { guildMessage(gid, packet, -1); } public void guildMessage(int gid, byte[] packet, int exception) { MapleGuild g = guilds.get(gid); if(g != null) { g.broadcast(packet, exception); } } public PlayerBuffStorage getPlayerBuffStorage() { return buffStorage; } public void deleteGuildCharacter(MapleCharacter mc) { setGuildMemberOnline(mc, false, (byte) -1); if (mc.getMGC().getGuildRank() > 1) { leaveGuild(mc.getMGC()); } else { disbandGuild(mc.getMGC().getGuildId()); } } public void deleteGuildCharacter(MapleGuildCharacter mgc) { if(mgc.getCharacter() != null) setGuildMemberOnline(mgc.getCharacter(), false, (byte) -1); if (mgc.getGuildRank() > 1) { leaveGuild(mgc); } else { disbandGuild(mgc.getGuildId()); } } public void reloadGuildCharacters(int world) { World worlda = getWorld(world); for (MapleCharacter mc : worlda.getPlayerStorage().getAllCharacters()) { if (mc.getGuildId() > 0) { setGuildMemberOnline(mc, true, worlda.getId()); memberLevelJobUpdate(mc.getMGC()); } } worlda.reloadGuildSummary(); } public void broadcastMessage(int world, final byte[] packet) { for (Channel ch : getChannelsFromWorld(world)) { ch.broadcastPacket(packet); } } public void broadcastGMMessage(int world, final byte[] packet) { for (Channel ch : getChannelsFromWorld(world)) { ch.broadcastGMPacket(packet); } } public boolean isGmOnline(int world) { for (Channel ch : getChannelsFromWorld(world)) { for (MapleCharacter player : ch.getPlayerStorage().getAllCharacters()) { if (player.isGM()){ return true; } } } return false; } public void changeFly(Integer accountid, boolean canFly) { if(canFly) { activeFly.add(accountid); } else { activeFly.remove(accountid); } } public boolean canFly(Integer accountid) { return activeFly.contains(accountid); } public World getWorld(int id) { return worlds.get(id); } public List getWorlds() { return worlds; } private static void loadCharacteridsFromDb(Integer accountid, Set accChars) { try { try (Connection con = DatabaseConnection.getConnection()) { try (PreparedStatement ps = con.prepareStatement("SELECT id FROM characters WHERE accountid = ?")) { ps.setInt(1, accountid); try (ResultSet rs = ps.executeQuery()) { while(rs.next()) { accChars.add(rs.getInt("id")); } } } } } catch (SQLException sqle) { sqle.printStackTrace(); } } public boolean haveCharacterid(Integer accountid, Integer chrid) { lgnLock.lock(); try { Set accChars = accountChars.get(accountid); if(accChars == null) { accChars = new HashSet<>(5); loadCharacteridsFromDb(accountid, accChars); accountChars.put(accountid, accChars); } return accChars.contains(chrid); } finally { lgnLock.unlock(); } } public void createCharacterid(Integer accountid, Integer chrid) { lgnLock.lock(); try { Set accChars = accountChars.get(accountid); if(accChars == null) { accChars = new HashSet<>(5); accountChars.put(accountid, accChars); } accChars.add(chrid); } finally { lgnLock.unlock(); } } public void deleteCharacterid(Integer accountid, Integer chrid) { lgnLock.lock(); try { Set accChars = accountChars.get(accountid); if(accChars != null) { accChars.remove(chrid); } } finally { lgnLock.unlock(); } } /* public void deleteAccount(Integer accountid) { is this even a thing? lgnLock.lock(); try { accountChars.remove(accountid); } finally { lgnLock.unlock(); } } */ private static String getRemoteIp(InetSocketAddress isa) { return isa.getAddress().getHostAddress(); } public void setCharacteridInTransition(InetSocketAddress isa, int charId) { String remoteIp = getRemoteIp(isa); lgnLock.lock(); try { transitioningChars.put(remoteIp, charId); } finally { lgnLock.unlock(); } } public boolean validateCharacteridInTransition(InetSocketAddress isa, int charId) { String remoteIp = getRemoteIp(isa); lgnLock.lock(); try { Integer cid = transitioningChars.remove(remoteIp); return cid != null && cid.equals(charId); } finally { lgnLock.unlock(); } } public void registerLoginState(MapleClient c) { srvLock.lock(); try { inLoginState.put(c, System.currentTimeMillis() + 600000); } finally { srvLock.unlock(); } } public void unregisterLoginState(MapleClient c) { srvLock.lock(); try { inLoginState.remove(c); } finally { srvLock.unlock(); } } private void disconnectIdlesOnLoginState() { srvLock.lock(); try { List toDisconnect = new LinkedList<>(); long timeNow = System.currentTimeMillis(); for(Entry mc : inLoginState.entrySet()) { if(timeNow > mc.getValue()) { toDisconnect.add(mc.getKey()); } } for(MapleClient c : toDisconnect) { if(c.isLoggedIn()) { c.disconnect(false, false); } else { c.getSession().close(true); } inLoginState.remove(c); } } finally { srvLock.unlock(); } } private void disconnectIdlesOnLoginTask() { TimerManager.getInstance().register(new Runnable() { @Override public void run() { disconnectIdlesOnLoginState(); } }, 300000); } public final Runnable shutdown(final boolean restart) {//no player should be online when trying to shutdown! return new Runnable() { @Override public void run() { srvLock.lock(); try { System.out.println((restart ? "Restarting" : "Shutting down") + " the server!\r\n"); if (getWorlds() == null) return;//already shutdown for (World w : getWorlds()) { w.shutdown(); } /*for (World w : getWorlds()) { while (w.getPlayerStorage().getAllCharacters().size() > 0) { try { Thread.sleep(1000); } catch (InterruptedException ie) { System.err.println("FUCK MY LIFE"); } } } for (Channel ch : getAllChannels()) { while (ch.getConnectedClients() > 0) { try { Thread.sleep(1000); } catch (InterruptedException ie) { System.err.println("FUCK MY LIFE"); } } }*/ if(ServerConstants.USE_THREAD_TRACKER) ThreadTracker.getInstance().cancelThreadTrackerTask(); TimerManager.getInstance().purge(); TimerManager.getInstance().stop(); for (Channel ch : getAllChannels()) { while (!ch.finishedShutdown()) { try { Thread.sleep(1000); } catch (InterruptedException ie) { ie.printStackTrace(); System.err.println("FUCK MY LIFE"); } } } worlds.clear(); worlds = null; channels.clear(); channels = null; worldRecommendedList.clear(); worldRecommendedList = null; System.out.println("Worlds + Channels are offline."); acceptor.unbind(); acceptor = null; if (!restart) { System.exit(0); } else { System.out.println("\r\nRestarting the server....\r\n"); try { instance.finalize();//FUU I CAN AND IT'S FREE } catch (Throwable ex) { ex.printStackTrace(); } instance = null; System.gc(); getInstance().init();//DID I DO EVERYTHING?! D: } } finally { srvLock.unlock(); } } }; } }