diff --git a/Admin/Manager.cs b/Admin/Manager.cs index cb0ee736..8988867e 100644 --- a/Admin/Manager.cs +++ b/Admin/Manager.cs @@ -34,7 +34,7 @@ namespace IW4MAdmin AliasService AliasSvc; PenaltyService PenaltySvc; #if FTP_LOG - const int UPDATE_FREQUENCY = 15000; + const int UPDATE_FREQUENCY = 700; #else const int UPDATE_FREQUENCY = 300; #endif diff --git a/Admin/Server.cs b/Admin/Server.cs index 836ed3eb..7335ab82 100644 --- a/Admin/Server.cs +++ b/Admin/Server.cs @@ -19,7 +19,7 @@ namespace IW4MAdmin public override int GetHashCode() { - return IP.GetHashCode() + Port; + return Math.Abs(IP.GetHashCode() + Port); } override public async Task AddPlayer(Player polledPlayer) { @@ -405,11 +405,7 @@ namespace IW4MAdmin if (lines != oldLines) { l_size = LogFile.Length(); - int end; - if (lines.Length == oldLines.Length) - end = lines.Length - 1; - else - end = Math.Abs((lines.Length - oldLines.Length)) - 1; + int end = (lines.Length == oldLines.Length) ? lines.Length - 1 : Math.Abs((lines.Length - oldLines.Length)) - 1; for (int count = 0; count < lines.Length; count++) { @@ -434,7 +430,6 @@ namespace IW4MAdmin await ExecuteEvent(event_); } } - } } } @@ -537,8 +532,13 @@ namespace IW4MAdmin #endif } else + { +#if !DEBUG LogFile = new IFile(logPath); - +#else + } + LogFile = new RemoteFile("https://raidmax.org/IW4MAdmin/getlog.php"); +#endif Logger.WriteInfo("Log file is " + logPath); await ExecuteEvent(new Event(Event.GType.Start, "Server started", null, null, this)); #if !DEBUG diff --git a/Admin/lib/SharedLibrary.dll b/Admin/lib/SharedLibrary.dll index bbc2a686..46fc4468 100644 Binary files a/Admin/lib/SharedLibrary.dll and b/Admin/lib/SharedLibrary.dll differ diff --git a/Admin/version.txt b/Admin/version.txt index 4b1b281c..170ea549 100644 --- a/Admin/version.txt +++ b/Admin/version.txt @@ -1,4 +1,8 @@ -Version 1.6: +Version 1.7: +CHANGELOG: +-EntityFramework is now the main database system + +Version 1.6: CHANGELOG: -got rid of pesky "error on character" message -optimizations to commands diff --git a/Plugins/SimpleStats/Helpers/StatManager.cs b/Plugins/SimpleStats/Helpers/StatManager.cs index ecec31f0..d0475d33 100644 --- a/Plugins/SimpleStats/Helpers/StatManager.cs +++ b/Plugins/SimpleStats/Helpers/StatManager.cs @@ -102,10 +102,15 @@ namespace StatsPlugin.Helpers clientStats = ClientStatSvc.Insert(clientStats); } - else - lock (playerStats) + { + if (playerStats.ContainsKey(pl.ClientNumber)) + { + Log.WriteWarning($"Duplicate clientnumber in stats {pl.ClientId} vs {playerStats[pl.ClientNumber].ClientId}"); + playerStats.Remove(pl.ClientNumber); + } playerStats.Add(pl.ClientNumber, clientStats); + } return clientStats; } @@ -113,29 +118,32 @@ namespace StatsPlugin.Helpers { int serverId = pl.CurrentServer.GetHashCode(); var playerStats = Servers[serverId].PlayerStats; - // get individual client's stats var clientStats = playerStats[pl.ClientNumber]; // remove the client from the stats dictionary as they're leaving lock (playerStats) playerStats.Remove(pl.ClientNumber); - + // allow accessing certain properties + //clientStats.Client = pl; + // update skill + // clientStats = UpdateStats(clientStats); + // reset for EF cache + //clientStats.SessionDeaths = 0; + // clientStats.SessionKills = 0; + // prevent mismatched primary key + //clientStats.Client = null; // update in database - await ClientStatSvc.SaveChangesAsync(); + //await ClientStatSvc.SaveChangesAsync(); } /// /// Process stats for kill event /// /// - public async Task AddKill(Player attacker, Player victim, int serverId, string map, string hitLoc, string type, + public async Task AddScriptKill(Player attacker, Player victim, int serverId, string map, string hitLoc, string type, string damage, string weapon, string killOrigin, string deathOrigin) { - var attackerStats = Servers[serverId].PlayerStats[attacker.ClientNumber]; - attackerStats.Kills += 1; - - var victimStats = Servers[serverId].PlayerStats[victim.ClientNumber]; - victimStats.Deaths += 1; + AddStandardKill(attacker, victim); var kill = new EFClientKill() { @@ -156,10 +164,88 @@ namespace StatsPlugin.Helpers await KillSvc.SaveChangesAsync(); } - private EFClientStatistics UpdateStats(EFClientStatistics cs) + public void AddStandardKill(Player attacker, Player victim) { - // todo: everything - return cs; + var attackerStats = Servers[attacker.CurrentServer.GetHashCode()].PlayerStats[attacker.ClientNumber]; + // set to access total time + attackerStats.Client = attacker; + var victimStats = Servers[victim.CurrentServer.GetHashCode()].PlayerStats[victim.ClientNumber]; + + CalculateKill(attackerStats, victimStats); + } + + /// + /// Performs the incrementation of kills and deaths for client statistics + /// + /// Stats of the attacker + /// Stats of the victim + public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats) + { + attackerStats.Kills += 1; + attackerStats.SessionKills += 1; + attackerStats.KillStreak += 1; + attackerStats.DeathStreak = 0; + + victimStats.Deaths += 1; + victimStats.SessionDeaths += 1; + victimStats.DeathStreak += 1; + victimStats.KillStreak = 0; + + // process the attacker's stats after the kills + UpdateStats(attackerStats); + attackerStats.Client = null; + + // immediately write changes in debug +#if DEBUG + ClientStatSvc.SaveChanges(); +#endif + } + + /// + /// Update the client stats (skill etc) + /// + /// Client statistics + /// + private EFClientStatistics UpdateStats(EFClientStatistics clientStats) + { + // if it's their first kill we need to set the last kill as the time they joined + clientStats.LastStatCalculation = (clientStats.LastStatCalculation == DateTime.MinValue) ? DateTime.UtcNow : clientStats.LastStatCalculation; + double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0; + + // each 'session' is one minute + if (timeSinceLastCalc >= 1) + { + Log.WriteDebug($"Updated stats for {clientStats.ClientId} ({clientStats.SessionKills})"); + // calculate the players Score Per Minute for the current session + // todo: score should be based on gamemode + double killSPM = clientStats.SessionKills * 100.0; + + // calculate how much the KDR should weigh + // 1.637 is a Eddie-Generated number that weights the KDR nicely + double KDRWeight = Math.Round(Math.Pow(clientStats.KDR, 1.637 / Math.E), 3); + + // if no SPM, weight is 1 else the weight ishe current session's spm / lifetime average score per minute + double SPMWeightAgainstAverage = (clientStats.SPM < 1) ? 1 : killSPM / clientStats.SPM; + + // calculate the weight of the new play time against last 10 hours of gameplay + int totalConnectionTime = (clientStats.Client.TotalConnectionTime == 0) ? + (int)(DateTime.UtcNow - clientStats.Client.FirstConnection).TotalSeconds : + clientStats.Client.TotalConnectionTime + (int)(DateTime.UtcNow - clientStats.Client.LastConnection).TotalSeconds; + + double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalConnectionTime / 60.0)); + + // calculate the new weight against average times the weight against play time + clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight)); + clientStats.SPM = Math.Round(clientStats.SPM, 3); + clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight) / 10.0, 3); + + clientStats.SessionKills = 0; + clientStats.SessionDeaths = 0; + + clientStats.LastStatCalculation = DateTime.UtcNow; + } + + return clientStats; } } } diff --git a/Plugins/SimpleStats/IW4Info.cs b/Plugins/SimpleStats/IW4Info.cs index ff42a3f8..7d40cfef 100644 --- a/Plugins/SimpleStats/IW4Info.cs +++ b/Plugins/SimpleStats/IW4Info.cs @@ -1225,7 +1225,118 @@ namespace StatsPlugin nuke_mp = 1190, barrel_mp = 1191, lightstick_mp = 1192, - throwingknife_rhand_mp = 1193 + throwingknife_rhand_mp = 1193, + deserteaglegold_akimbo_mp, + deserteaglegold_fmj_mp, + deserteaglegold_tactical_mp, + deserteaglegold_akimbo_fmj_mp, + deserteaglegold_fmj_tactical_mp, + ak47classic_mp, + ak47classic_acog_mp, + ak47classic_eotech_mp, + ak47classic_fmj_mp, + ak47classic_gl_mp, + gl_ak47classic_mp, + ak47classic_heartbeat_mp, + ak47classic_reflex_mp, + ak47classic_shotgun_mp, + ak47classic_shotgun_attach_mp, + ak47classic_silencer_mp, + ak47classic_thermal_mp, + ak47classic_xmags_mp, + ak47classic_acog_fmj_mp, + ak47classic_acog_gl_mp, + ak47classic_acog_heartbeat_mp, + ak47classic_acog_shotgun_mp, + ak47classic_acog_silencer_mp, + ak47classic_acog_xmags_mp, + ak47classic_eotech_fmj_mp, + ak47classic_eotech_gl_mp, + ak47classic_eotech_heartbeat_mp, + ak47classic_eotech_shotgun_mp, + ak47classic_eotech_silencer_mp, + ak47classic_eotech_xmags_mp, + ak47classic_fmj_gl_mp, + ak47classic_fmj_heartbeat_mp, + ak47classic_fmj_reflex_mp, + ak47classic_fmj_shotgun_mp, + ak47classic_fmj_silencer_mp, + ak47classic_fmj_thermal_mp, + ak47classic_fmj_xmags_mp, + ak47classic_gl_heartbeat_mp, + ak47classic_gl_reflex_mp, + ak47classic_gl_silencer_mp, + ak47classic_gl_thermal_mp, + ak47classic_gl_xmags_mp, + ak47classic_heartbeat_reflex_mp, + ak47classic_heartbeat_shotgun_mp, + ak47classic_heartbeat_silencer_mp, + ak47classic_heartbeat_thermal_mp, + ak47classic_heartbeat_xmags_mp, + ak47classic_reflex_shotgun_mp, + ak47classic_reflex_silencer_mp, + ak47classic_reflex_xmags_mp, + ak47classic_shotgun_silencer_mp, + ak47classic_shotgun_thermal_mp, + ak47classic_shotgun_xmags_mp, + ak47classic_silencer_thermal_mp, + ak47classic_silencer_xmags_mp, + ak47classic_thermal_xmags_mp, + ak74u_mp, + ak74u_acog_mp, + ak74u_eotech_mp, + ak74u_fmj_mp, + ak74u_gl_mp, + gl_ak74u_mp, + ak74u_heartbeat_mp, + ak74u_reflex_mp, + ak74u_shotgun_mp, + ak74u_shotgun_attach_mp, + ak74u_silencer_mp, + ak74u_thermal_mp, + ak74u_xmags_mp, + ak74u_acog_fmj_mp, + ak74u_acog_gl_mp, + ak74u_acog_heartbeat_mp, + ak74u_acog_shotgun_mp, + ak74u_acog_silencer_mp, + ak74u_acog_xmags_mp, + ak74u_eotech_fmj_mp, + ak74u_eotech_gl_mp, + ak74u_eotech_heartbeat_mp, + ak74u_eotech_shotgun_mp, + ak74u_eotech_silencer_mp, + ak74u_eotech_xmags_mp, + ak74u_fmj_gl_mp, + ak74u_fmj_heartbeat_mp, + ak74u_fmj_reflex_mp, + ak74u_fmj_shotgun_mp, + ak74u_fmj_silencer_mp, + ak74u_fmj_thermal_mp, + ak74u_fmj_xmags_mp, + ak74u_gl_heartbeat_mp, + ak74u_gl_reflex_mp, + ak74u_gl_silencer_mp, + ak74u_gl_thermal_mp, + ak74u_gl_xmags_mp, + ak74u_heartbeat_reflex_mp, + ak74u_heartbeat_shotgun_mp, + ak74u_heartbeat_silencer_mp, + ak74u_heartbeat_thermal_mp, + ak74u_heartbeat_xmags_mp, + ak74u_reflex_shotgun_mp, + ak74u_reflex_silencer_mp, + ak74u_reflex_xmags_mp, + ak74u_shotgun_silencer_mp, + ak74u_shotgun_thermal_mp, + ak74u_shotgun_xmags_mp, + ak74u_silencer_thermal_mp, + ak74u_silencer_xmags_mp, + ak74u_thermal_xmags_mp, + m40a3_mp = 1194, + peacekeeper_mp, + dragunov_mp, + cobra_player_minigun_mp } public enum MapName diff --git a/Plugins/SimpleStats/Models/EFClientStatistics.cs b/Plugins/SimpleStats/Models/EFClientStatistics.cs index ead4afd1..f514af24 100644 --- a/Plugins/SimpleStats/Models/EFClientStatistics.cs +++ b/Plugins/SimpleStats/Models/EFClientStatistics.cs @@ -28,11 +28,22 @@ namespace StatsPlugin.Models [NotMapped] public double KDR { - get => Deaths == 0 ? 0.0 : Math.Round((float)Kills / (float)Deaths, 2); + get => Deaths == 0 ? Kills : Math.Round((float)Kills / (float)Deaths, 2); } [Required] public double SPM { get; set; } [Required] public double Skill { get; set; } + + [NotMapped] + public int SessionKills { get; set; } + [NotMapped] + public int SessionDeaths { get; set; } + [NotMapped] + public int KillStreak { get; set; } + [NotMapped] + public int DeathStreak { get; set; } + [NotMapped] + public DateTime LastStatCalculation { get; set; } } } diff --git a/Plugins/SimpleStats/Plugin.cs b/Plugins/SimpleStats/Plugin.cs index a1268deb..b043c670 100644 --- a/Plugins/SimpleStats/Plugin.cs +++ b/Plugins/SimpleStats/Plugin.cs @@ -62,7 +62,7 @@ namespace StatsPlugin case Event.GType.Kill: string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; if (killInfo.Length >= 9 && killInfo[0].Contains("ScriptKill")) - await Manager.AddKill(E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4]); + await Manager.AddScriptKill(E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4]); break; case Event.GType.Death: break; diff --git a/Plugins/Tests/Plugin.cs b/Plugins/Tests/Plugin.cs index 20f47e88..1bfe2b06 100644 --- a/Plugins/Tests/Plugin.cs +++ b/Plugins/Tests/Plugin.cs @@ -202,8 +202,6 @@ namespace IW4MAdmin.Plugins Interval = DateTime.Now; if (S.ClientNum > 0) { - - //"K;26d2f66b95184934;1;allies;egor;5c56fef676b3818d;0;axis;1_din;m21_heartbeat_mp;98;MOD_RIFLE_BULLET;torso_lower"; var victimPlayer = S.Players.Where(pl => pl != null).ToList()[rand.Next(0, S.ClientNum - 1)]; var attackerPlayer = S.Players.Where(pl => pl != null).ToList()[rand.Next(0, S.ClientNum - 1)]; diff --git a/SharedLibrary/Database/Models/EFClient.cs b/SharedLibrary/Database/Models/EFClient.cs index 1cb05801..8304cee1 100644 --- a/SharedLibrary/Database/Models/EFClient.cs +++ b/SharedLibrary/Database/Models/EFClient.cs @@ -18,6 +18,7 @@ namespace SharedLibrary.Database.Models [Required] public int Connections { get; set; } [Required] + // in seconds public int TotalConnectionTime { get; set; } [Required] public DateTime FirstConnection { get; set; } diff --git a/SharedLibrary/File.cs b/SharedLibrary/File.cs index f81b9b28..7a534ca8 100644 --- a/SharedLibrary/File.cs +++ b/SharedLibrary/File.cs @@ -3,19 +3,53 @@ using System.Collections.Generic; using System.Text; using System.IO; using System.Net; +using System.Net.Http; namespace SharedLibrary { + public class RemoteFile : IFile + { + string Location; + string[] FileCache; + + public RemoteFile(string location) : base(string.Empty) + { + Location = location; + } + + private void Retrieve() + { + using (var cl = new HttpClient()) + FileCache = cl.GetStringAsync(Location).Result.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + } + + public override string[] Tail(int lineCount) + { + Retrieve(); + return FileCache; + } + + public override long Length() + { + Retrieve(); + return FileCache[0].Length; + } + + } + public class IFile { public IFile(String fileName) { - Name = fileName; - Handle = new StreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); - sze = Handle.BaseStream.Length; + if (fileName != string.Empty) + { + Name = fileName; + Handle = new StreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + sze = Handle.BaseStream.Length; + } } - public long Length() + public virtual long Length() { sze = Handle.BaseStream.Length; return sze; @@ -36,7 +70,7 @@ namespace SharedLibrary return Handle?.ReadToEnd(); } - public String[] Tail(int lineCount) + public virtual String[] Tail(int lineCount) { var buffer = new List(lineCount); string line; diff --git a/SharedLibrary/Helpers/AsyncStatus.cs b/SharedLibrary/Helpers/AsyncStatus.cs index c42d2fc9..73da78a6 100644 --- a/SharedLibrary/Helpers/AsyncStatus.cs +++ b/SharedLibrary/Helpers/AsyncStatus.cs @@ -40,7 +40,7 @@ namespace SharedLibrary.Helpers public void Update(Task T) { RequestedTask = T; - Console.WriteLine($"Starting Task {T.Id} "); + // Console.WriteLine($"Starting Task {T.Id} "); RequestedTask.Start(); if (TimesRun > 25) diff --git a/SharedLibrary/RCON.cs b/SharedLibrary/RCON.cs index 9c376342..2780fc50 100644 --- a/SharedLibrary/RCON.cs +++ b/SharedLibrary/RCON.cs @@ -26,12 +26,12 @@ namespace SharedLibrary.Network static string[] SendQuery(QueryType Type, Server QueryServer, string Parameters = "") { - if ((DateTime.Now - LastQuery).TotalMilliseconds < 30) - Task.Delay(30).Wait(); + if ((DateTime.Now - LastQuery).TotalMilliseconds < 100) + Task.Delay(100).Wait(); LastQuery = DateTime.Now; var ServerOOBConnection = new UdpClient(); - ServerOOBConnection.Client.SendTimeout = 5000; - ServerOOBConnection.Client.ReceiveTimeout = 5000; + ServerOOBConnection.Client.SendTimeout = 1000; + ServerOOBConnection.Client.ReceiveTimeout = 1000; var Endpoint = new IPEndPoint(IPAddress.Parse(QueryServer.GetIP()), QueryServer.GetPort()); string QueryString = String.Empty; @@ -139,7 +139,7 @@ namespace SharedLibrary.Network public static async Task> GetStatusAsync(this Server server) { -#if DEBUG +#if DEBUG && DEBUG_PLAYERS string[] response = await Task.Run(() => System.IO.File.ReadAllLines("players.txt")); #else string[] response = await Task.FromResult(SendQuery(QueryType.DVAR, server, "status")); diff --git a/SharedLibrary/Server.cs b/SharedLibrary/Server.cs index 8d19aa72..8a6571d0 100644 --- a/SharedLibrary/Server.cs +++ b/SharedLibrary/Server.cs @@ -140,11 +140,13 @@ namespace SharedLibrary /// Message to be sent to all players public async Task Broadcast(String Message) { -#if DEBUG - //return; -#endif + string sayCommand = (GameName == Game.IW4) ? "sayraw" : "say"; +#if !DEBUG await this.ExecuteCommandAsync($"{sayCommand} {Message}"); +#else + Logger.WriteVerbose(Message.StripColors()); +#endif } /// @@ -156,8 +158,12 @@ namespace SharedLibrary { string tellCommand = (GameName == Game.IW4) ? "tellraw" : "tell"; +#if !DEBUG if (Target.ClientNumber > -1 && Message.Length > 0 && Target.Level != Player.Permission.Console) await this.ExecuteCommandAsync($"{tellCommand} {Target.ClientNumber} {Message}^7"); +#else + Logger.WriteVerbose($"{Target.ClientNumber}->{Message.StripColors()}"); +#endif if (Target.Level == Player.Permission.Console) { diff --git a/SharedLibrary/SharedLibrary.csproj b/SharedLibrary/SharedLibrary.csproj index 3deb30bd..dd143bee 100644 --- a/SharedLibrary/SharedLibrary.csproj +++ b/SharedLibrary/SharedLibrary.csproj @@ -86,6 +86,7 @@ ..\packages\Microsoft.SqlServer.Compact.4.0.8876.1\lib\net40\System.Data.SqlServerCe.dll True + diff --git a/_customcallbacks.gsc b/_customcallbacks.gsc index 9d1943af..7f1523c8 100644 --- a/_customcallbacks.gsc +++ b/_customcallbacks.gsc @@ -13,9 +13,10 @@ init() Callback_PlayerKilled( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ) { victim = self; + _attacker = attacker; if (!isDefined(attacker) || !isPlayer(attacker)) - attacker = victim; + _attacker = victim; - logPrint("ScriptKill;" + attacker.guid + ";" + victim.guid + ";" + attacker.origin + ";" + victim.origin + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + "\n"); + logPrint("ScriptKill;" + _attacker.guid + ";" + victim.guid + ";" + _attacker.origin + ";" + victim.origin + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + "\n"); self maps\mp\gametypes\_damage::Callback_PlayerKilled( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ); } \ No newline at end of file