/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package gcb; import gcb.bot.ChatThread; import gcb.bot.SQLThread; import gcb.plugin.PluginManager; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.lang.management.ManagementFactory; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.Timer; import java.util.TimerTask; /** * * @author wizardus */ public class Main { public static String VERSION = "gcb 1.0.2-dev"; public static boolean DEBUG; public static final String DATE_FORMAT = "yyyy-MM-dd"; public static Timer TIMER; public static Random RANDOM; static boolean log; static boolean logCommands; /* * logLevel determines what messages will be printed/discarded * it uses a binary flag system, flags can be added to produce desired result: * 1 (2^0) - uncategorized system messages (should be enabled) * 2 (2^1) - uncategorized error messages (should be enabled) * 4 (2^2) - logging-related notifications * 8 (2^3) - startup-related messages (can be disabled, since errors will be logged under 2^1) * 16 (2^4) - unimportant system messages * 32 (2^5) - uncategorized room messages (should be enabled) * 64 (2^6) - uncategorized room messages (should be enabled) * 128 (2^7) - unimportant room messages (includes many room connection messages) * 256 (2^8) - room messages that come from the Garena server * 512 (2^9) - room messages that come from other players * 1024 (2^10) - debug room messages * 2048 (2^11) - debug system messages * 4096 (2^12) - TCP debug messages (should be disabled except for debugging) * WARNING: in the source code, println method uses the x in 2^x to identify the different logging levels, * while in configuration you should add the 1, 2, 4, 8, etc. values */ static int logLevel = 1023; boolean botDisable; boolean reverse; long lastLog; //when the log file(s) were created static int newLogInterval; //ms interval between creating new log file(s) static PrintWriter log_out; static PrintWriter log_cmd_out; Map<Integer, GChatBot> bots; PluginManager plugins; Map<Integer, GarenaInterface> garenaConnections; Map<Integer, GarenaThread> gspThreads; Map<Integer, GarenaThread> gcrpThreads; Map<Integer, GarenaThread> peerThreads; WC3Interface wc3i; ChatThread chatthread; SQLThread sqlthread; GarenaReconnect reconnect; GCBRcon rcon; ConnectWorkerPool connectPool; GarenaTCPPool tcpPool; //determine what will be loaded, what won't be loaded boolean loadBot; boolean loadPlugins; boolean loadWC3; boolean loadPL; boolean loadSQL; boolean loadChat; boolean loadRcon; public void init(String[] args) { System.out.println(VERSION); GCBConfig.load(args); //determine what to load based on gcb_reverse and gcb_bot loadBot = GCBConfig.configuration.getBoolean("gcb_bot", false); loadRcon = GCBConfig.configuration.getBoolean("gcb_rcon", false); botDisable = GCBConfig.configuration.getBoolean("gcb_bot_disable", true); reverse = GCBConfig.configuration.getBoolean("gcb_reverse", false); log = GCBConfig.configuration.getBoolean("gcb_log", false); logCommands = GCBConfig.configuration.getBoolean("gcb_log_commands", false); newLogInterval = GCBConfig.configuration.getInt("gcb_log_new_file", 86400000); logLevel = GCBConfig.configuration.getInt("gcb_loglevel", 1023); //first load all the defaults loadPlugins = true; loadWC3 = true; loadPL = true; loadSQL = false; loadChat = false; if(loadBot) { loadSQL = true; loadChat = true; } if(loadBot && botDisable) { loadWC3 = false; loadPL = false; } if(reverse) { loadWC3 = false; //or else we'll broadcast our own packets } lastLog = System.currentTimeMillis(); bots = new HashMap<Integer, GChatBot>(); garenaConnections = new HashMap<Integer, GarenaInterface>(); gspThreads = new HashMap<Integer, GarenaThread>(); gcrpThreads = new HashMap<Integer, GarenaThread>(); peerThreads = new HashMap<Integer, GarenaThread>(); TIMER.schedule(new StatusTask(), 10000, GCBConfig.configuration.getInt("gcb_status", 60) * 1000); } public void initPlugins() { if(loadPlugins) { plugins = new PluginManager(); } } public void loadPlugins() { if(loadPlugins) { plugins.setGarena(garenaConnections, wc3i, sqlthread, chatthread); plugins.initPlugins(); plugins.loadPlugins(); } } //garena is null when first starting public boolean initGarenaAll(boolean restart) { if(connectPool == null) { connectPool = new ConnectWorkerPool(this); } if(tcpPool == null) { tcpPool = new GarenaTCPPool(); tcpPool.start(); } if(loadWC3 && !restart) { //setup wc3 broadcast reader wc3i = new WC3Interface(garenaConnections); if(!wc3i.init()) { return false; } } //connect to garena if(!restart) { reconnect = new GarenaReconnect(this); //initiate each connetion for(int i = 1; i < 65536; i++) { if(GCBConfig.configuration.containsKey("garena" + i + "_roomid") || GCBConfig.configuration.containsKey("garena" + i + "_roomname")) { GarenaInterface garena = new GarenaInterface(plugins, i); garena.registerListener(reconnect); garena.setGarenaTCPPool(tcpPool); synchronized(garenaConnections) { garenaConnections.put(i, garena); } connectPool.push(0, garena); } } } //wait for all connection jobs to finish connectPool.waitFor(); if(loadChat && !restart) { chatthread = new ChatThread(garenaConnections); chatthread.start(); } if(loadWC3 && !restart) { //start receiving and broadcasting wc3 packets GarenaThread wc3_thread = new GarenaThread(null, wc3i, GarenaThread.WC3_BROADCAST); wc3_thread.start(); } if(loadRcon && !restart) { try { rcon = new GCBRcon(this); } catch(IOException ioe) { println(1, "[Main] Error: failed to load rcon server: " + ioe.getLocalizedMessage()); } } //make sure we get correct external ip/port; do on restart in case they changed lookup(); return true; } public void initRoomAll() { synchronized(garenaConnections) { Iterator<GarenaInterface> it = garenaConnections.values().iterator(); while(it.hasNext()) { GarenaInterface garena = it.next(); connectPool.push(1, garena); } } //wait for connection jobs to finish, then close the worker pool connectPool.waitFor(); connectPool.close(); } public boolean initGarena(GarenaInterface garena, boolean restart) { garena.clear(); if(!garena.init()) return false; if(loadWC3) { garena.setWC3Interface(wc3i); } if(!restart && !initPeer(garena, restart)) return false; //authenticate with login server if(!garena.sendGSPSessionInit()) return false; if(!garena.readGSPSessionInitReply()) return false; if(!garena.sendGSPSessionHello()) return false; if(!garena.readGSPSessionHelloReply()) return false; if(!garena.sendGSPSessionLogin()) return false; if(!garena.readGSPSessionLoginReply()) return false; if(!this.gspThreads.containsKey(garena.id)) { GarenaThread gsp_thread = new GarenaThread(garena, null, GarenaThread.GSP_LOOP); gsp_thread.start(); this.gspThreads.put(garena.id, gsp_thread); } if(restart) { //make sure we get correct external ip/port; do on restart in case they changed //if this is initial load, this will be done elsewhere garena.sendPeerLookup(); } return true; } public boolean initPeer(GarenaInterface garena, boolean restart) { if(!garena.initPeer()) return false; if(loadPL && !this.peerThreads.containsKey(garena.id)) { //startup GP2PP system GarenaThread pl = new GarenaThread(garena, wc3i, GarenaThread.PEER_LOOP); pl.start(); this.peerThreads.put(garena.id, pl); } return true; } public void lookup(GarenaInterface garena) { //only lookup if this garena connection has peer initiated if(garena.peer_socket == null) { return; } Main.println(0, "[Main] Waiting for lookup response on connection " + garena.id + "..."); int counter = 0; //resend lookup every second while(garena.iExternal == null) { try { Thread.sleep(100); } catch(InterruptedException e) {} counter++; if(counter % 4 == 0) { Main.println(0, "[Main] Resending lookup on connection " + garena.id); garena.sendPeerLookup(); } } Main.println(0, "[Main] Received lookup response!"); } public void lookup() { //send lookup packets synchronized(garenaConnections) { if(loadPL && !garenaConnections.isEmpty()) { Iterator<GarenaInterface> it = garenaConnections.values().iterator(); while(it.hasNext()) { it.next().sendPeerLookup(); } } } //wait for lookup response (and send more packets if needed) synchronized(garenaConnections) { if(loadPL && !garenaConnections.isEmpty()) { Iterator<GarenaInterface> it = garenaConnections.values().iterator(); while(it.hasNext()) { lookup(it.next()); } } } } //returns whether init succeeded; restart=true indicates this isn't the first time we're calling public boolean initRoom(GarenaInterface garena, boolean restart) { //connect to room if(!garena.initRoom()) return false; if(!garena.sendGCRPMeJoin()) return false; if(!this.gcrpThreads.containsKey(garena.id)) { GarenaThread gcrp_thread = new GarenaThread(garena, wc3i, GarenaThread.GCRP_LOOP); gcrp_thread.start(); this.gcrpThreads.put(garena.id, gcrp_thread); } // we ought to say we're starting the game; we'll do later too garena.startPlaying(); return true; } public void initBot() { if(loadBot) { synchronized(garenaConnections) { Iterator<Integer> garenaIds = garenaConnections.keySet().iterator(); while(garenaIds.hasNext()) { int garenaId = garenaIds.next(); GarenaInterface garena = garenaConnections.get(garenaId); GChatBot bot = new GChatBot(this); bot.init(); bot.garena = garena; bot.plugins = plugins; bot.sqlthread = sqlthread; bot.chatthread = chatthread; garena.registerListener(bot); bots.put(garenaId, bot); } } } if(loadSQL) { //initiate mysql thread sqlthread = new SQLThread(bots); sqlthread.init(); sqlthread.start(); } if(loadBot) { synchronized(bots) { Iterator<GChatBot> it = bots.values().iterator(); while(it.hasNext()) { it.next().sqlthread = sqlthread; } } } } public void newLogLoop() { if(newLogInterval != 0) { while(true) { if(System.currentTimeMillis() - lastLog > newLogInterval) { String currentDate = date(); if(log_out != null) { println(2, "[Main] Closing old log file and creating new log file"); log_out.close(); File log_directory = new File("log/"); if(!log_directory.exists()) { log_directory.mkdir(); } File log_target = new File(log_directory, currentDate + ".log"); try { log_out = new PrintWriter(new FileWriter(log_target, true), true); } catch(IOException e) { if(DEBUG) { e.printStackTrace(); } println(2, "[Main] Failed to change log file date: " + e.getLocalizedMessage()); } } if(logCommands && log_cmd_out != null) { log_cmd_out.close(); File log_cmd_directory = new File("cmd_log/"); if(!log_cmd_directory.exists()) { log_cmd_directory.mkdir(); } File log_cmd_target = new File(log_cmd_directory, currentDate + ".log"); try { log_cmd_out = new PrintWriter(new FileWriter(log_cmd_target, true), true); } catch(IOException e) { if(DEBUG) { e.printStackTrace(); } println(2, "[Main] Failed to change cmd log file date: " + e.getLocalizedMessage()); } } lastLog = System.currentTimeMillis(); } try { Thread.sleep(10000); } catch(InterruptedException e) { println(2, "[Main] New day loop sleep interrupted"); } } } } /** * @param args the command line arguments */ public static void main(String[] args) throws IOException { /* Use this to decrypt Garena packets try { GarenaEncrypt encrypt = new GarenaEncrypt(); encrypt.initRSA(); byte[] data = readWS(args[0]); byte[] plain = encrypt.rsaDecryptPrivate(data); byte[] key = new byte[32]; byte[] init_vector = new byte[16]; System.arraycopy(plain, 0, key, 0, 32); System.arraycopy(plain, 32, init_vector, 0, 16); encrypt.initAES(key, init_vector); data = readWS(args[1]); byte[] out = encrypt.aesDecrypt(data); Main.println(encrypt.hexEncode(out)); } catch (Exception e) { e.printStackTrace(); } System.exit(0);*/ Main.TIMER = new Timer(); Main.RANDOM = new Random(); Main main = new Main(); main.init(args); //init log if(log) { if(newLogInterval != 0) { File log_directory = new File("log/"); if(!log_directory.exists()) { log_directory.mkdir(); } File log_target = new File(log_directory, date() + ".log"); log_out = new PrintWriter(new FileWriter(log_target, true), true); } } if(logCommands) { if(newLogInterval != 0) { File log_cmd_directory = new File("cmd_log/"); if(!log_cmd_directory.exists()) { log_cmd_directory.mkdir(); } File log_cmd_target = new File(log_cmd_directory, date() + ".log"); log_cmd_out = new PrintWriter(new FileWriter(log_cmd_target, true), true); } else { log_cmd_out = new PrintWriter(new FileWriter("gcb_cmd.log", true), true); } } DEBUG = GCBConfig.configuration.getBoolean("gcb_debug", false); main.initPlugins(); if(!main.initGarenaAll(false)) { System.exit(-1); return; } main.initRoomAll(); main.initBot(); main.loadPlugins(); main.newLogLoop(); } public static synchronized void println(int level, String str) { //don't output this message if we're not at the correct log level //note that here, level is the base (2^level is the flag value) if((logLevel & (1 << level)) == 0) { return; } Date date = new Date(); String dateString = DateFormat.getDateTimeInstance().format(date); System.out.println(str); if(log_out == null && newLogInterval == 0) { try { log_out = new PrintWriter(new FileWriter("gcb.log", true), true); } catch(IOException ioe) { System.err.println("Unable to print to file: " + ioe.getLocalizedMessage()); ioe.printStackTrace(); return; } } if(log_out != null) { log_out.println("[" + dateString + "] " + str); if(newLogInterval == 0) { log_out.close(); log_out = null; } } } public static void cmdprintln(String str) { Date date = new Date(); String dateString = DateFormat.getDateTimeInstance().format(date); if(log_cmd_out != null) { log_cmd_out.println("[" + dateString + "] " + str); } } public static String date() { Calendar cal = Calendar.getInstance(); SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); return sdf.format(cal.getTime()); } //hexadecimal string to byte array public static byte[] readWS(String s) { int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } return data; } class StatusTask extends TimerTask { public void run() { int numTCPConnections = tcpPool.count(); int rooms = 0; int users = 0; synchronized(garenaConnections) { Iterator<GarenaInterface> it = garenaConnections.values().iterator(); while(it.hasNext()) { rooms++; users += it.next().numRoomUsers(); } } int memory = (int) (Runtime.getRuntime().totalMemory() / 1024); int threads = ManagementFactory.getThreadMXBean().getThreadCount(); int uptime = (int) (ManagementFactory.getRuntimeMXBean().getUptime() / 1000); int upDays = uptime / 86400; int upHours = (uptime % 86400) / 3600; int upMinutes = (uptime % 3600) / 60; int upSeconds = uptime % 60; String uptimeString = ""; if(upDays != 0) uptimeString += upDays + "d"; if(uptime >= 3600) uptimeString += upHours + "h"; if(uptime >= 60) uptimeString += upMinutes + "m"; uptimeString += upSeconds + "s"; String statusString = String.format( "[STATUS] connected: %d; mem: %d KB; threads: %d; rooms/users: %d/%d; up: %s", numTCPConnections, memory, threads, rooms, users, uptimeString); Main.println(0, statusString); //TCP-specific stats if(tcpPool.isStatisticsEnabled()) { long transmitPackets = tcpPool.getStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_PACKETS); long transmitBytes = tcpPool.getStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_BYTES); long receivePackets = tcpPool.getStatistics(GarenaTCPPool.STATISTIC_RECEIVE_PACKETS); long receiveBytes = tcpPool.getStatistics(GarenaTCPPool.STATISTIC_RECEIVE_BYTES); long retransmissionCount = tcpPool.getStatistics(GarenaTCPPool.STATISTIC_RETRANSMISSION_COUNT); double retransmitPercent = (double) retransmissionCount / transmitPackets; double receiveBytesPerPacket = (double) receiveBytes / receivePackets; double transmitBytesPerPacket = (double) transmitBytes / transmitPackets; double packetsPerSecond = (double) (transmitPackets + receivePackets) / uptime; String tcpStatusString = String.format( "[STATUS TCP] r%%: %.2f; rx b/p: %.1f; tx b/p: %.1f; pps: %.1f", retransmitPercent, receiveBytesPerPacket, transmitBytesPerPacket, packetsPerSecond); Main.println(0, tcpStatusString); } } } }