diff --git a/Admin/Database.cs b/Admin/Database.cs index e4a7e4508..a29c84458 100644 --- a/Admin/Database.cs +++ b/Admin/Database.cs @@ -15,6 +15,7 @@ namespace IW4MAdmin FileName = FN; DBCon = String.Format("Data Source={0}", FN); Con = new SQLiteConnection(DBCon); + Open = false; Init(); } @@ -59,7 +60,7 @@ namespace IW4MAdmin } try { - this.ExecuteNonQuery(String.Format("update {0} set {1} where {2};", tableName, vals, where)); + ExecuteNonQuery(String.Format("update {0} set {1} where {2};", tableName, vals, where)); } catch (Exception fail) { @@ -77,6 +78,7 @@ namespace IW4MAdmin protected int ExecuteNonQuery(String Request) { + waitForClose(); Con.Open(); SQLiteCommand CMD = new SQLiteCommand(Con); CMD.CommandText = Request; @@ -90,6 +92,7 @@ namespace IW4MAdmin DataTable dt = new DataTable(); try { + waitForClose(); Con.Open(); SQLiteCommand mycommand = new SQLiteCommand(Con); mycommand.CommandText = sql; @@ -105,11 +108,22 @@ namespace IW4MAdmin } return dt; } + + protected void waitForClose() + { + while (Con.State == ConnectionState.Open) + { + Utilities.Wait(0.01); + } + + return; + } //END protected String FileName; protected String DBCon; protected SQLiteConnection Con; + protected bool Open; } class ClientsDB : Database diff --git a/Admin/Event.cs b/Admin/Event.cs index f2afebff2..19563b17a 100644 --- a/Admin/Event.cs +++ b/Admin/Event.cs @@ -69,10 +69,10 @@ namespace IW4MAdmin return new Event(GType.Connect, null, SV.clientFromLine(line, 3, true), null, SV); if (eventType == "Q") - return new Event(GType.Disconnect, null, SV.clientFromLine(line, 3, false), null, null); + return new Event(GType.Disconnect, null, SV.getPlayers()[Convert.ToInt16(line[2])], null, null); if (eventType == "K") - return new Event(GType.Kill, line[9], SV.clientFromLine(line[8]), SV.clientFromLine(line[4]), null); + return new Event(GType.Kill, line[9], SV.clientFromLineArr(line, true), SV.clientFromLineArr(line, false), null); if (line[0].Substring(line[0].Length - 3).Trim() == "say") { diff --git a/Admin/IW4M ADMIN.csproj b/Admin/IW4M ADMIN.csproj index 696ee25a8..7a57e911b 100644 --- a/Admin/IW4M ADMIN.csproj +++ b/Admin/IW4M ADMIN.csproj @@ -107,6 +107,7 @@ + diff --git a/Admin/Player.cs b/Admin/Player.cs index bed847841..5e28c93b7 100644 --- a/Admin/Player.cs +++ b/Admin/Player.cs @@ -12,6 +12,9 @@ namespace IW4MAdmin Deaths = D; KDR = Math.Round(kdr,2); Skill = Math.Round(skill,2); + + lastSigma = lastMew/3; + lastMew = 25; } public void updateKDR() @@ -19,15 +22,18 @@ namespace IW4MAdmin KDR = Math.Round((double)((double)Kills / (double)Deaths), 2); } - public void updateSkill(double enemySkill) + public void updateSkill() { - Skill = Math.Round(Math.Log(KDR + 1) * ((enemySkill / 2) + 1) + (Math.Log(Deaths) * 0.3) * 12, 2); + Skill = TrueSkill.Gaussian(lastMew, lastSigma); } public int Kills; public int Deaths; public double KDR; public double Skill; + + public double lastSigma; + public double lastMew; } class Aliases @@ -98,7 +104,7 @@ namespace IW4MAdmin npID = id; Number = num; Level = (Player.Permission)l; - LastOffense = null; + LastOffense = String.Empty; Connections = 0; IP = ""; Warnings = 0; @@ -113,8 +119,11 @@ namespace IW4MAdmin Number = num; Level = l; dbID = cind; - LastOffense = lo; - Connections = con + 1; + if (lo == null) + LastOffense = String.Empty; + else + LastOffense = lo; + Connections = con; IP = IP2; Warnings = 0; } @@ -219,7 +228,7 @@ namespace IW4MAdmin private int Number; private Player.Permission Level; private int dbID; - private int Connections; + public int Connections; private String IP; public Event lastEvent; diff --git a/Admin/RCON.cs b/Admin/RCON.cs index ddeacd696..528a59a7d 100644 --- a/Admin/RCON.cs +++ b/Admin/RCON.cs @@ -99,6 +99,12 @@ namespace IW4MAdmin return newReq.waitForResponse(); } + public void Reset() + { + sv_connection.Close(); + sv_connection = new UdpClient(); + } + public void ManageRCONQueue() { while (Instance.isRunning) diff --git a/Admin/Server.cs b/Admin/Server.cs index f08381b4d..aefc3d84f 100644 --- a/Admin/Server.cs +++ b/Admin/Server.cs @@ -100,65 +100,6 @@ namespace IW4MAdmin return Bans; } - public void threadedConnect(Player P, Player NewPlayer) - { - bool updated = false; - while (!updated) - { - try - { - P.updateIP(IPS[P.getID()].Trim()); - updated = true; - Log.Write("Sucessfully updated " + NewPlayer.getName() + "'s IP to " + P.getIP(), Log.Level.Debug); - } - - catch - { - //Log.Write("Looks like the connecting player doesn't have an IP location assigned yet. Let's wait for next poll", Log.Level.Debug); - Utilities.Wait(1); - } - } - - if (NewPlayer.Alias == null) - { - aliasDB.addPlayer(new Aliases(NewPlayer.getDBID(), NewPlayer.getName(), P.getIP())); - } - - if (P.getName() != NewPlayer.getName()) - { - NewPlayer.updateName(P.getName()); - NewPlayer.Alias.addName(P.getName()); - aliasDB.updatePlayer(NewPlayer.Alias); - } - - if (P.getIP() != NewPlayer.getIP()) - { - NewPlayer.updateIP(P.getIP()); - NewPlayer.Alias.addIP(P.getIP()); - aliasDB.updatePlayer(NewPlayer.Alias); - } - - clientDB.updatePlayer(NewPlayer); - - Ban B = isBanned(NewPlayer); - if (B != null || NewPlayer.getLevel() == Player.Permission.Banned) - { - Log.Write("Banned client " + P.getName() + " trying to connect...", Log.Level.Debug); - string Reason = String.Empty; - if (B != null) - Reason = B.getReason(); - else - Reason = P.LastOffense; - - String Message = "^1Player Kicked: ^7Previously Banned for ^5" + Reason; - P.Kick(Message); - } - - players[NewPlayer.getClientNum()] = null; - players[NewPlayer.getClientNum()] = NewPlayer; - - } - //Add player object p to `players` list public bool addPlayer(Player P) { @@ -174,7 +115,6 @@ namespace IW4MAdmin aliasDB.addPlayer(new Aliases(New.getDBID(), New.getName(), New.getIP())); } - //messy way to prevent loss of last event Player NewPlayer = clientDB.getPlayer(P.getID(), P.getClientNum()); NewPlayer.stats = statDB.getStats(NewPlayer.getDBID()); @@ -193,8 +133,61 @@ namespace IW4MAdmin if (players[NewPlayer.getClientNum()] == null) { - Thread connectThread = new Thread(() => threadedConnect(P, NewPlayer)); - connectThread.Start(); // We don't want events to get behind + bool updated = false; + while (!updated) + { + try + { + P.updateIP(IPS[P.getID()].Trim()); + updated = true; + Log.Write("Sucessfully updated " + NewPlayer.getName() + "'s IP to " + P.getIP(), Log.Level.Debug); + } + + catch + { + //Log.Write("Looks like the connecting player doesn't have an IP location assigned yet. Let's wait for next poll", Log.Level.Debug); + Utilities.Wait(1); + } + } + + if (NewPlayer.Alias == null) + { + aliasDB.addPlayer(new Aliases(NewPlayer.getDBID(), NewPlayer.getName(), P.getIP())); + } + + if (P.getName() != NewPlayer.getName()) + { + NewPlayer.updateName(P.getName()); + NewPlayer.Alias.addName(P.getName()); + aliasDB.updatePlayer(NewPlayer.Alias); + } + + if (P.getIP() != NewPlayer.getIP()) + { + NewPlayer.updateIP(P.getIP()); + NewPlayer.Alias.addIP(P.getIP()); + aliasDB.updatePlayer(NewPlayer.Alias); + } + + clientDB.updatePlayer(NewPlayer); + NewPlayer.lastEvent.Owner = this; // cuz crashes + + Ban B = isBanned(NewPlayer); + if (B != null || NewPlayer.getLevel() == Player.Permission.Banned) + { + Log.Write("Banned client " + P.getName() + " trying to connect...", Log.Level.Debug); + string Reason = String.Empty; + if (B != null) + Reason = B.getReason(); + else + Reason = P.LastOffense; + + String Message = "^1Player Kicked: ^7Previously Banned for ^5" + Reason; + NewPlayer.Kick(Message); + } + + players[NewPlayer.getClientNum()] = null; + players[NewPlayer.getClientNum()] = NewPlayer; NewPlayer.Tell("Welcome ^5" + NewPlayer.getName() + " ^7this is your ^5" + Utilities.timesConnected(NewPlayer.getConnections()) + " ^7time connecting!"); Log.Write("Client " + NewPlayer.getName() + " connecting...", Log.Level.Debug); @@ -215,12 +208,22 @@ namespace IW4MAdmin //Remove player by CLIENT NUMBER public bool removePlayer(int cNum) { - Log.Write("Updating stats for " + players[cNum].getName(), Log.Level.Debug); - statDB.updatePlayer(players[cNum]); - Log.Write("Client at " + cNum + " disconnecting...", Log.Level.Debug); - players[cNum] = null; - clientnum--; - return true; + if (cNum >= 0 && cNum < 18) + { + Log.Write("Updating stats for " + players[cNum].getName(), Log.Level.Debug); + statDB.updatePlayer(players[cNum]); + + Log.Write("Client at " + cNum + " disconnecting...", Log.Level.Debug); + players[cNum] = null; + clientnum--; + return true; + } + + else + { + Log.Write("Client disconnecting has an invalid client index!", Log.Level.Debug); + return false; + } } //Get a client from players list by by log line. If create = true, it will return a new player object @@ -265,6 +268,58 @@ namespace IW4MAdmin return null; } + //Another version of client from line, written for the line created by a kill or death event + public Player clientFromLineArr(String[] L, bool kill) + { + if (L.Length < 7) + { + Log.Write("Line sent for client creation is not long enough!", Log.Level.Debug); + return null; + } + + if (kill) + { + foreach (Player P in players) + { + if (P == null) + continue; + if (P.getName().ToLower().Contains(L[8].Trim())) + return P; + } + + String killerName = L[8].Trim(); + String killerGUID = L[5].Trim(); + int killerID = -1; + int.TryParse(L[6], out killerID); + Player newPlayer = new Player(killerName, killerGUID, killerID, 0); + + addPlayer(newPlayer); + return players[newPlayer.getClientNum()]; + } + + else + { + foreach (Player P in players) + { + if (P == null) + continue; + if (P.getName().ToLower().Contains(L[4].Trim())) + return P; + } + + String victimName = L[4].Trim(); + String victimGUID = L[1].Trim(); + int victimID = -1; + int.TryParse(L[2].Trim(), out victimID); + + Player newPlayer = new Player(victimName, victimGUID, victimID, 0); + addPlayer(newPlayer); + return players[newPlayer.getClientNum()]; + } + + } + + //Check ban list for every banned player and return ban if match is found public Ban isBanned(Player C) { foreach (Ban B in Bans) @@ -286,19 +341,7 @@ namespace IW4MAdmin return null; } - public String getPlayerIP(String GUID) - { - Dictionary dict; - int count = 0; - do - { - //because rcon can be weird - dict = Utilities.IPFromStatus(RCON.addRCON("status")); - count++; - } while(dict.Count < clientnum || count < 5); - return dict[GUID]; - } - + //Procses requested command correlating to an event public Command processCommand(Event E, Command C) { E.Data = Utilities.removeWords(E.Data, 1); @@ -324,16 +367,20 @@ namespace IW4MAdmin if (Args[0] == String.Empty) return C; - if (Args[0][0] == '@') + if (Args[0][0] == '@') // user specifying target by database ID { int dbID = -1; int.TryParse(Args[0].Substring(1, Args[0].Length-1), out dbID); Player found = E.Owner.clientDB.getPlayer(dbID); if (found != null) + { E.Target = found; + E.Target.lastEvent = E; + E.Owner = this; + } } - else if(Args[0].Length < 3 && cNum > -1 && cNum < 18) + else if(Args[0].Length < 3 && cNum > -1 && cNum < 18) // user specifying target by client num { if (players[cNum] != null) E.Target = players[cNum]; @@ -351,11 +398,14 @@ namespace IW4MAdmin return C; } + //push a new event into the queue private void addEvent(Event E) { events.Enqueue(E); } + + //process new event every 100 milliseconds private void manageEventQueue() { while (isRunning) @@ -385,7 +435,8 @@ namespace IW4MAdmin Utilities.Wait(10); return; } - + + //Thread to handle polling server for IP's Thread statusUpdate = new Thread(new ThreadStart(pollServer)); statusUpdate.Start(); @@ -414,15 +465,6 @@ namespace IW4MAdmin lastMessage = DateTime.Now - start; if(lastMessage.TotalSeconds > messageTime && messages.Count > 0) { - - if (RCON.addRCON("sv_online") == null) - { - timesFailed++; - Log.Write("Server appears to be offline - " + timesFailed, Log.Level.Debug); - } - else - timesFailed = 0; - Thread.Sleep(300); initMacros(); // somethings dynamically change so we have to re-init the dictionary Broadcast(Utilities.processMacro(Macros, messages[nextMessage])); if (nextMessage == (messages.Count - 1)) @@ -508,17 +550,36 @@ namespace IW4MAdmin private void pollServer() { + int timesFailed = 0; while (isRunning) { IPS = Utilities.IPFromStatus(RCON.addRCON("status")); while (IPS == null) { + timesFailed++; + Log.Write("Server appears to be offline - " + timesFailed, Log.Level.Debug); + + if (timesFailed > 4) + { + Log.Write("Max offline attempts reached. Reinitializing RCON connection.", Log.Level.Debug); + RCON.Reset(); + timesFailed = 0; + } + + else + { + Log.Write("Server responded to status query!", Log.Level.All); + timesFailed = 0; + } + + Thread.Sleep(FLOOD_TIMEOUT); + IPS = Utilities.IPFromStatus(RCON.addRCON("status")); Utilities.Wait(1); } lastPoll = DateTime.Now; - Utilities.Wait(15); + Utilities.Wait(20); } } @@ -560,6 +621,7 @@ namespace IW4MAdmin IW_Ver = infoResponseDict["shortversion"]; maxClients = Convert.ToInt32(infoResponseDict["sv_maxclients"]); Gametype = infoResponseDict["g_gametype"]; + try { Website = infoResponseDict["_Website"]; @@ -681,7 +743,7 @@ namespace IW4MAdmin tmp.Credentials = new System.Net.NetworkCredential("*", "*"); System.IO.Stream ftpStream = tmp.GetResponse().GetResponseStream(); String ftpLog = new StreamReader(ftpStream).ReadToEnd();*/ - logPath = "games_old.log"; + //logPath = "games_old.log"; #endif return true; } @@ -695,58 +757,113 @@ namespace IW4MAdmin //Process any server event public bool processEvent(Event E) { + // if (E.Type == Event.GType.Connect) { + if (E.Origin == null) + Log.Write("Connect event triggered, but no client is detected!", Log.Level.Debug); addPlayer(E.Origin); return true; } - if (E.Type == Event.GType.Disconnect && E.Origin.getClientNum() > 0) + if (E.Type == Event.GType.Disconnect) { - if (getNumPlayers() > 0 && E.Origin != null && players[E.Origin.getClientNum()] != null) + if (E.Origin == null) { - clientDB.updatePlayer(E.Origin); - removePlayer(E.Origin.getClientNum()); + Log.Write("Disconnect event triggered, but no origin found.", Log.Level.Debug); + return false; } + + E.Origin.Connections++; + clientDB.updatePlayer(E.Origin); + removePlayer(E.Origin.getClientNum()); + return true; } if (E.Type == Event.GType.Kill) { - if (E.Origin != null && E.Target != null && E.Origin.stats != null) + if (E.Origin == null) { - E.Origin.stats.Kills++; - E.Origin.stats.updateKDR(); - E.Origin.stats.updateSkill(E.Target.stats.Skill); - - E.Target.stats.Deaths++; - E.Target.stats.updateKDR(); - //E.Target.stats.updateSkill(E.Origin.stats.Skill); + Log.Write("Kill event triggered, but no origin found!", Log.Level.Debug); + return false; } + + if (E.Target == null) + { + Log.Write("Kill event triggered, but no target found!", Log.Level.Debug); + return false; + } + + if (E.Origin.stats == null) + { + Log.Write("Kill event triggered, but no stats found for origin!", Log.Level.Debug); + E.Origin.stats = statDB.getStats(E.Origin.getDBID()); + } + + if (E.Target.stats == null) + { + Log.Write("Kill event triggered, but no stats found for target!", Log.Level.Debug); + E.Target.stats = statDB.getStats(E.Target.getDBID()); + } + + Log.Write(E.Origin.getName() + " killed " + E.Target.getName() + " with a " + E.Data, Log.Level.Debug); + E.Origin.stats.Kills++; + E.Origin.stats.updateKDR(); + + E.Origin.stats.lastMew = TrueSkill.calculateWinnerMu(E.Origin.stats, E.Target.stats); + + E.Origin.stats.lastSigma = TrueSkill.calculateWinnerSigma(E.Origin.stats, E.Target.stats); + E.Origin.stats.updateSkill(); + + E.Target.stats.Deaths++; + E.Target.stats.updateKDR(); + + E.Target.stats.lastMew = TrueSkill.calculateLoserMu(E.Target.stats, E.Origin.stats); + E.Target.stats.lastSigma = TrueSkill.calculateLoserSigma(E.Target.stats, E.Origin.stats); } - if (E.Type == Event.GType.Say && E.Origin != null) + if (E.Type == Event.GType.Say) { - if (E.Data.Length < 2) + + if (E.Data.Length < 2) // ITS A LIE! return false; + if (E.Origin == null) + { + Log.Write("Say event triggered, but no origin found! - " + E.Data, Log.Level.Debug); + return false; + } + Log.Write("Message from " + E.Origin.getName() + ": " + E.Data, Log.Level.Debug); - if (E.Data.Substring(0, 1) != "!") + if (E.Owner == null) + { + Log.Write("Say event does not have an owner!", Log.Level.Debug); + return false; + } + + if (E.Data.Substring(0, 1) != "!") // Not a command so who gives an F? return true; Command C = E.isValidCMD(commands); + if (C != null) { C = processCommand(E, C); if (C != null) { + if (C.needsTarget() && E.Target == null) + { + Log.Write("Requested event requiring target does not have a target!", Log.Level.Debug); + return false; + } C.Execute(E); return true; } else { - Log.Write("Error processing command by " + E.Origin.getName(), Log.Level.Debug); + Log.Write("Player didn't properly enter command - " + E.Origin.getName(), Log.Level.Debug); return true; } } diff --git a/Admin/TrueSkill.cs b/Admin/TrueSkill.cs new file mode 100644 index 000000000..a1d89dc29 --- /dev/null +++ b/Admin/TrueSkill.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin +{ + class TrueSkill + { + public static double calculateWinnerMu(Stats originStats, Stats targetStats) + { + double Beta = originStats.lastMew / 6; + double lastSkill = Gaussian(originStats.lastMew, originStats.lastSigma); + double c = Math.Sqrt((2 * Beta * Beta) + (originStats.lastSigma * originStats.lastSigma) + (targetStats.lastSigma * targetStats.lastSigma)); + double newMew = originStats.lastMew + ((originStats.lastSigma) * (originStats.lastSigma) / c) * ((originStats.lastMew - targetStats.lastMew) / c); + return newMew; + } + + public static double calculateLoserMu(Stats originStats, Stats targetStats) + { + double Beta = originStats.lastMew / 6; + double lastSkill = Gaussian(originStats.lastMew, originStats.lastSigma); + double c = Math.Sqrt( (2 * Beta * Beta) + (originStats.lastSigma * originStats.lastSigma) + (targetStats.lastSigma * targetStats.lastSigma)); + double newMew = originStats.lastMew - ((targetStats.lastSigma) * (targetStats.lastSigma) / c) * ((originStats.lastMew - targetStats.lastMew) / c); + return newMew; + } + + public static double calculateLoserSigma(Stats originStats, Stats targetStats) + { + double Beta = originStats.lastMew / 6; + double lastSkill = Gaussian(originStats.lastMew, originStats.lastSigma); + double c = ((2 * Beta * Beta) + originStats.lastSigma * originStats.lastSigma) + (targetStats.lastSigma * targetStats.lastSigma); + double newSigma = originStats.lastSigma * ( 1 - (targetStats.lastSigma) * (targetStats.lastSigma) / c) * ((originStats.lastMew - targetStats.lastMew) / c); + return newSigma; + } + + public static double calculateWinnerSigma(Stats originStats, Stats targetStats) + { + double Beta = originStats.lastMew / 6; + double lastSkill = Gaussian(originStats.lastMew, originStats.lastSigma); + double c = ((2 * Beta * Beta) + originStats.lastSigma * originStats.lastSigma) + (targetStats.lastSigma * targetStats.lastSigma); + double newSigma = originStats.lastSigma * (1 - (originStats.lastSigma) * (originStats.lastSigma) / c) * ((originStats.lastMew - targetStats.lastMew) / c); + return newSigma; + } + + + + //https://gist.github.com/tansey/1444070 + public static double Gaussian( double mean, double stddev) + { + + double y1 = Math.Sqrt(-2.0 * Math.Log(.5)) * Math.Cos(2.0 * Math.PI * .5); + return y1 * stddev + mean; + } + } +} diff --git a/Admin/Utilities.cs b/Admin/Utilities.cs index 7acc14c18..3eeef3558 100644 --- a/Admin/Utilities.cs +++ b/Admin/Utilities.cs @@ -148,7 +148,7 @@ namespace IW4MAdmin public static String timesConnected(int connection) { String Prefix = String.Empty; - if (connection % 10 > 3 || connection % 10 == 0) + if (connection % 10 > 3 || connection % 10 == 0 || (connection % 100 > 9 && connection % 100 < 19)) Prefix = "th"; else {