diff --git a/Application/API/Master/Heartbeat.cs b/Application/API/Master/Heartbeat.cs index 0c14f1f7d..f36d80ab2 100644 --- a/Application/API/Master/Heartbeat.cs +++ b/Application/API/Master/Heartbeat.cs @@ -8,9 +8,13 @@ using SharedLibraryCore; namespace IW4MAdmin.Application.API.Master { + public class HeartbeatState + { + public bool Connected { get; set; } + } + public class Heartbeat { - public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false) { var api = Endpoint.Get(); diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index a6f55b5b3..86842bceb 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -1,6 +1,7 @@ using SharedLibraryCore; using SharedLibraryCore.Interfaces; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; @@ -8,12 +9,14 @@ namespace IW4MAdmin.Application { class GameEventHandler : IEventHandler { - private Queue EventQueue; + private ConcurrentQueue EventQueue; + private ConcurrentQueue StatusSensitiveQueue; private IManager Manager; public GameEventHandler(IManager mgr) { - EventQueue = new Queue(); + EventQueue = new ConcurrentQueue(); + StatusSensitiveQueue = new ConcurrentQueue(); Manager = mgr; } @@ -22,7 +25,21 @@ namespace IW4MAdmin.Application #if DEBUG Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}"); #endif - EventQueue.Enqueue(gameEvent); + // we need this to keep accurate track of the score + if (gameEvent.Type == GameEvent.EventType.Script || + gameEvent.Type == GameEvent.EventType.Kill) + { +#if DEBUG + Manager.GetLogger().WriteDebug($"Added sensitive event to queue"); +#endif + StatusSensitiveQueue.Enqueue(gameEvent); + return; + } + + else + { + EventQueue.Enqueue(gameEvent); + } #if DEBUG Manager.GetLogger().WriteDebug($"There are now {EventQueue.Count} events in queue"); #endif @@ -34,13 +51,43 @@ namespace IW4MAdmin.Application throw new NotImplementedException(); } + public GameEvent GetNextSensitiveEvent() + { + if (StatusSensitiveQueue.Count > 0) + { + if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent)) + { + Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing"); + } + + else + { + return newEvent; + } + } + + return null; + } + public GameEvent GetNextEvent() { + if (EventQueue.Count > 0) + { #if DEBUG - Manager.GetLogger().WriteDebug("Getting next event to be processed"); + Manager.GetLogger().WriteDebug("Getting next event to be processed"); #endif + if (!EventQueue.TryDequeue(out GameEvent newEvent)) + { + Manager.GetLogger().WriteWarning("Could not dequeue event for processing"); + } - return EventQueue.Count > 0 ? EventQueue.Dequeue() : null; + else + { + return newEvent; + } + } + + return null; } } } diff --git a/Application/IO/GameLogEvent.cs b/Application/IO/GameLogEvent.cs index cbd14712b..f7e1430c8 100644 --- a/Application/IO/GameLogEvent.cs +++ b/Application/IO/GameLogEvent.cs @@ -11,41 +11,46 @@ namespace IW4MAdmin.Application.IO { class GameLogEvent { - FileSystemWatcher LogPathWatcher; Server Server; long PreviousFileSize; GameLogReader Reader; Timer RefreshInfoTimer; string GameLogFile; + class EventState + { + public ILogger Log { get; set; } + public string ServerId { get; set; } + } + public GameLogEvent(Server server, string gameLogPath, string gameLogName) { GameLogFile = gameLogPath; Reader = new GameLogReader(gameLogPath, server.EventParser); Server = server; - RefreshInfoTimer = new Timer((sender) => + RefreshInfoTimer = new Timer(OnEvent, new EventState() { - long newLength = new FileInfo(GameLogFile).Length; - UpdateLogEvents(newLength); - - }, null, 0, 100); - /*LogPathWatcher = new FileSystemWatcher() - { - Path = gameLogPath.Replace(gameLogName, ""), - Filter = gameLogName, - NotifyFilter = (NotifyFilters)383, - InternalBufferSize = 4096 - }; - - // LogPathWatcher.Changed += LogPathWatcher_Changed; - LogPathWatcher.EnableRaisingEvents = true;*/ + Log = server.Manager.GetLogger(), + ServerId = server.ToString() + }, 0, 100); } - /* - ~GameLogEvent() + private void OnEvent(object state) { - LogPathWatcher.EnableRaisingEvents = false; - }*/ + long newLength = new FileInfo(GameLogFile).Length; + + try + { + UpdateLogEvents(newLength); + } + + catch (Exception e) + { + ((EventState)state).Log.WriteWarning($"Failed to update log event for {((EventState)state).ServerId}"); + ((EventState)state).Log.WriteDebug($"Exception: {e.Message}"); + ((EventState)state).Log.WriteDebug($"StackTrace: {e.StackTrace}"); + } + } private void UpdateLogEvents(long fileSize) { diff --git a/Application/Main.cs b/Application/Main.cs index 9ee3ff3c7..a44e586ff 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -127,7 +127,19 @@ namespace IW4MAdmin.Application if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront) { - Task.Run(() => WebfrontCore.Program.Init(ServerManager)); + Task.Run(() => + { + try + { + WebfrontCore.Program.Init(ServerManager); + } + + catch (Exception e) + { + ServerManager.Logger.WriteWarning("Webfront had unhandled exception"); + ServerManager.Logger.WriteDebug(e.Message); + } + }); } } diff --git a/Application/Manager.cs b/Application/Manager.cs index b9ade6e12..96916009f 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -19,6 +19,7 @@ using SharedLibraryCore.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Text; +using IW4MAdmin.Application.API.Master; namespace IW4MAdmin.Application { @@ -43,12 +44,7 @@ namespace IW4MAdmin.Application EventApi Api; GameEventHandler Handler; ManualResetEventSlim OnEvent; - Timer StatusUpdateTimer; -#if FTP_LOG - const int UPDATE_FREQUENCY = 700; -#else - const int UPDATE_FREQUENCY = 2000; -#endif + Timer HeartbeatTimer; private ApplicationManager() { @@ -90,16 +86,62 @@ namespace IW4MAdmin.Application return Instance ?? (Instance = new ApplicationManager()); } - public void UpdateStatus(object state) + public async Task UpdateStatus(object state) { var taskList = new List(); - foreach (var server in Servers) + while (Running) { - taskList.Add(Task.Run(() => server.ProcessUpdatesAsync(new CancellationToken()))); - } + taskList.Clear(); + foreach (var server in Servers) + { + taskList.Add(Task.Run(async () => + { + try + { + await server.ProcessUpdatesAsync(new CancellationToken()); + } - Task.WaitAll(taskList.ToArray()); + catch (Exception e) + { + Logger.WriteWarning($"Failed to update status for {server}"); + Logger.WriteDebug($"Exception: {e.Message}"); + Logger.WriteDebug($"StackTrace: {e.StackTrace}"); + } + })); + } +#if DEBUG + Logger.WriteDebug($"{taskList.Count} servers queued for stats updates"); + ThreadPool.GetMaxThreads(out int workerThreads, out int n); + ThreadPool.GetAvailableThreads(out int availableThreads, out int m); + Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks"); +#endif + + await Task.WhenAll(taskList.ToArray()); + + GameEvent sensitiveEvent; + while ((sensitiveEvent = Handler.GetNextSensitiveEvent()) != null) + { + try + { + await sensitiveEvent.Owner.ExecuteEvent(sensitiveEvent); +#if DEBUG + Logger.WriteDebug($"Processed Sensitive Event {sensitiveEvent.Type}"); +#endif + } + + catch (Exception E) + { + Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationSet["SERVER_ERROR_EXCEPTION"]} {sensitiveEvent.Owner}"); + Logger.WriteDebug("Error Message: " + E.Message); + Logger.WriteDebug("Error Trace: " + E.StackTrace); + sensitiveEvent.OnProcessed.Set(); + continue; + } + } + + await Task.Delay(5000); + } } public async Task Init() @@ -286,41 +328,35 @@ namespace IW4MAdmin.Application } await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray()); - // start polling servers - StatusUpdateTimer = new Timer(UpdateStatus, null, 0, 10000); - #endregion Running = true; } - private void HeartBeatThread() + private void SendHeartbeat(object state) { - bool successfulConnection = false; - restartConnection: - while (!successfulConnection) + var heartbeatState = (HeartbeatState)state; + + if (!heartbeatState.Connected) { try { - API.Master.Heartbeat.Send(this, true).Wait(); - successfulConnection = true; + Heartbeat.Send(this, true).Wait(); + heartbeatState.Connected = true; } catch (Exception e) { - successfulConnection = false; + heartbeatState.Connected = false; Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}"); } - - Thread.Sleep(30000); } - while (Running) + else { - Logger.WriteDebug("Sending heartbeat..."); try { - API.Master.Heartbeat.Send(this).Wait(); + Heartbeat.Send(this).Wait(); } catch (System.Net.Http.HttpRequestException e) { @@ -336,8 +372,7 @@ namespace IW4MAdmin.Application { if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized) { - successfulConnection = false; - goto restartConnection; + heartbeatState.Connected = false; } } } @@ -347,55 +382,65 @@ namespace IW4MAdmin.Application Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - successfulConnection = false; - goto restartConnection; + heartbeatState.Connected = false; } } - Thread.Sleep(30000); } } public void Start() { - Task.Run(() => HeartBeatThread()); - GameEvent newEvent; - while (Running) - { - // wait for new event to be added - OnEvent.Wait(); - - // todo: sequencially or parallelize? - while ((newEvent = Handler.GetNextEvent()) != null) - { - try - { - newEvent.Owner.ExecuteEvent(newEvent).Wait(); -#if DEBUG - Logger.WriteDebug("Processed Event"); -#endif - } - - catch (Exception E) - { - Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationSet["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}"); - Logger.WriteDebug("Error Message: " + E.Message); - Logger.WriteDebug("Error Trace: " + E.StackTrace); - newEvent.OnProcessed.Set(); - continue; - } - // tell anyone waiting for the output that we're done - newEvent.OnProcessed.Set(); - } - - // signal that all events have been processed - OnEvent.Reset(); - } #if !DEBUG + // start heartbeat + HeartbeatTimer = new Timer(SendHeartbeat, new HeartbeatState(), 0, 30000); +#endif + // start polling servers + // StatusUpdateTimer = new Timer(UpdateStatus, null, 0, 5000); + Task.Run(() => UpdateStatus(null)); + GameEvent newEvent; + + Task.Run(async () => + { + while (Running) + { + // wait for new event to be added + OnEvent.Wait(); + + // todo: sequencially or parallelize? + while ((newEvent = Handler.GetNextEvent()) != null) + { + try + { + await newEvent.Owner.ExecuteEvent(newEvent); +#if DEBUG + Logger.WriteDebug("Processed Event"); +#endif + } + + catch (Exception E) + { + Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationSet["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}"); + Logger.WriteDebug("Error Message: " + E.Message); + Logger.WriteDebug("Error Trace: " + E.StackTrace); + newEvent.OnProcessed.Set(); + continue; + } + // tell anyone waiting for the output that we're done + newEvent.OnProcessed.Set(); + } + + // signal that all events have been processed + OnEvent.Reset(); + } +#if !DEBUG + HeartbeatTimer.Change(0, Timeout.Infinite); + foreach (var S in Servers) S.Broadcast(Utilities.CurrentLocalization.LocalizationSet["BROADCAST_OFFLINE"]).Wait(); #endif - _servers.Clear(); + _servers.Clear(); + }).Wait(); } diff --git a/Application/RconParsers/IW4RConParser.cs b/Application/RconParsers/IW4RConParser.cs index 6cb5126d2..d2ea8d63e 100644 --- a/Application/RconParsers/IW4RConParser.cs +++ b/Application/RconParsers/IW4RConParser.cs @@ -91,7 +91,7 @@ namespace Application.RconParsers int Ping = -1; Int32.TryParse(playerInfo[2], out Ping); String cName = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(responseLine.Substring(46, 18).StripColors().Trim()))); - long npID = Regex.Match(responseLine, @"([a-z]|[0-9]){16}", RegexOptions.IgnoreCase).Value.ConvertLong(); + long npID = Regex.Match(responseLine, @"([a-z]|[0-9]){16}|bot[0-9]+", RegexOptions.IgnoreCase).Value.ConvertLong(); int.TryParse(playerInfo[0], out cID); var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}"); int cIP = regex.Value.Split(':')[0].ConvertToIP(); @@ -105,8 +105,9 @@ namespace Application.RconParsers IPAddress = cIP, Ping = Ping, Score = score, - IsBot = npID == 0 + IsBot = cIP == 0 }; + StatusPlayers.Add(P); } } diff --git a/Application/RconParsers/IW5MRConParser.cs b/Application/RconParsers/IW5MRConParser.cs index ef5c42d26..ab00e9a37 100644 --- a/Application/RconParsers/IW5MRConParser.cs +++ b/Application/RconParsers/IW5MRConParser.cs @@ -147,7 +147,7 @@ namespace Application.RconParsers regex = Regex.Match(responseLine, @" +(\d+ +){3}"); int score = Int32.Parse(regex.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0]); - StatusPlayers.Add(new Player() + var p = new Player() { Name = name, NetworkId = networkId, @@ -156,7 +156,12 @@ namespace Application.RconParsers Ping = Ping, Score = score, IsBot = networkId == 0 - }); + }; + + StatusPlayers.Add(p); + + if (p.IsBot) + p.NetworkId = -p.ClientNumber; } } diff --git a/Application/RconParsers/T6MRConParser.cs b/Application/RconParsers/T6MRConParser.cs index 7ef408549..555e327b5 100644 --- a/Application/RconParsers/T6MRConParser.cs +++ b/Application/RconParsers/T6MRConParser.cs @@ -177,8 +177,7 @@ namespace Application.RconParsers int score = 0; // todo: fix this when T6M score is valid ;) //int score = Int32.Parse(playerInfo[1]); - - StatusPlayers.Add(new Player() + var p = new Player() { Name = name, NetworkId = networkId, @@ -187,7 +186,12 @@ namespace Application.RconParsers Ping = Ping, Score = score, IsBot = networkId == 0 - }); + }; + + if (p.IsBot) + p.NetworkId = -p.ClientNumber; + + StatusPlayers.Add(p); } } diff --git a/Application/Server.cs b/Application/Server.cs index b382696c1..b45b69e3c 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -54,7 +54,7 @@ namespace IW4MAdmin { if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) || - polledPlayer.Ping < 1 || polledPlayer.ClientNumber > (MaxClients) || + polledPlayer.Ping < 1 || polledPlayer.ClientNumber < 0) { //Logger.WriteDebug($"Skipping client not in connected state {P}"); @@ -415,7 +415,7 @@ namespace IW4MAdmin if (E.Type == GameEvent.EventType.Connect) { // special case for IW5 when connect is from the log - if (E.Extra != null) + if (E.Extra != null && GameName == Game.IW5) { var logClient = (Player)E.Extra; var client = (await this.GetStatusAsync()) @@ -573,10 +573,12 @@ namespace IW4MAdmin Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms"); #endif Throttled = false; - for (int i = 0; i < Players.Count; i++) + + var clients = GetPlayersAsList(); + foreach(var client in clients) { - if (CurrentPlayers.Find(p => p.ClientNumber == i) == null && Players[i] != null) - await RemovePlayer(i); + if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId)) + await RemovePlayer(client.ClientNumber); } for (int i = 0; i < CurrentPlayers.Count; i++) @@ -673,15 +675,15 @@ namespace IW4MAdmin } return true; } - catch (NetworkException) - { - Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}"); - return false; - } - catch (InvalidOperationException) + // this one is ok + catch (ServerException e) { - Logger.WriteWarning("Event could not parsed properly"); + if (e is NetworkException) + { + Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}"); + } + return false; } @@ -778,7 +780,7 @@ namespace IW4MAdmin CustomCallback = await ScriptLoaded(); string mainPath = EventParser.GetGameDir(); #if DEBUG - basepath.Value = @"\\192.168.88.253\Call of Duty 4\"; + basepath.Value = @"\\192.168.88.253\mw2"; #endif string logPath; if (GameName == Game.IW5) diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 652f08792..a0daec403 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}" ProjectSection(SolutionItems) = preProject + _commands.gsc = _commands.gsc _customcallbacks.gsc = _customcallbacks.gsc README.md = README.md version.txt = version.txt @@ -28,7 +29,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Logi EndProject Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -260,6 +263,30 @@ Global {B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.Build.0 = Release|Any CPU {B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.ActiveCfg = Release|Any CPU {B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.Build.0 = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.ActiveCfg = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.Build.0 = Debug|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.Build.0 = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.ActiveCfg = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU + {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -270,6 +297,7 @@ Global {958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F} {D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F} {B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F} + {6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} diff --git a/Master/Master.pyproj b/Master/Master.pyproj index 78f61c93c..4ba5bcb13 100644 --- a/Master/Master.pyproj +++ b/Master/Master.pyproj @@ -7,8 +7,7 @@ f5051a32-6bd0-4128-abba-c202ee15fc5c . {789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52} - - + master\runserver.py . @@ -19,6 +18,11 @@ Master Master MSBuild|dev_env|$(MSBuildProjectFullPath) + False + master\runserver + master\runserver + module + module true @@ -73,6 +77,9 @@ Code + + Code + Code diff --git a/Master/master/runserver.py b/Master/master/runserver.py new file mode 100644 index 000000000..c28d6abc5 --- /dev/null +++ b/Master/master/runserver.py @@ -0,0 +1,9 @@ +""" +This script runs the Master application using a development server. +""" + +from os import environ +from master import app + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=80, debug=True) \ No newline at end of file diff --git a/Plugins/IW4ScriptCommands/Commands/Balance.cs b/Plugins/IW4ScriptCommands/Commands/Balance.cs new file mode 100644 index 000000000..10fea7fdc --- /dev/null +++ b/Plugins/IW4ScriptCommands/Commands/Balance.cs @@ -0,0 +1,19 @@ +using SharedLibraryCore; +using SharedLibraryCore.Objects; +using System.Threading.Tasks; + +namespace IW4ScriptCommands.Commands +{ + class Balance : Command + { + public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null) + { + } + + public override async Task ExecuteAsync(GameEvent E) + { + await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance"); + await E.Origin.Tell("Balance command sent"); + } + } +} diff --git a/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj new file mode 100644 index 000000000..5102af65e --- /dev/null +++ b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj @@ -0,0 +1,22 @@ + + + + Library + netcoreapp2.0 + + + + + + + + + + + + + + + + + diff --git a/Plugins/IW4ScriptCommands/Plugin.cs b/Plugins/IW4ScriptCommands/Plugin.cs new file mode 100644 index 000000000..4b84874a1 --- /dev/null +++ b/Plugins/IW4ScriptCommands/Plugin.cs @@ -0,0 +1,26 @@ +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace IW4ScriptCommands +{ + class Plugin : IPlugin + { + public string Name => "IW4 Script Commands"; + + public float Version => 1.0f; + + public string Author => "RaidMax"; + + public Task OnEventAsync(GameEvent E, Server S) => Task.CompletedTask; + + public Task OnLoadAsync(IManager manager) => Task.CompletedTask; + + public Task OnTickAsync(Server S) => Task.CompletedTask; + + public Task OnUnloadAsync() => Task.CompletedTask; + } +} diff --git a/Plugins/Stats/Commands/ResetStats.cs b/Plugins/Stats/Commands/ResetStats.cs index 8ec347ad8..185233c2a 100644 --- a/Plugins/Stats/Commands/ResetStats.cs +++ b/Plugins/Stats/Commands/ResetStats.cs @@ -25,6 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands stats.Kills = 0; stats.SPM = 0.0; stats.Skill = 0.0; + stats.TimePlayed = 0; // reset the cached version Plugin.Manager.ResetStats(E.Origin.ClientId, E.Owner.GetHashCode()); diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 8f7c02855..8e48c6233 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -190,15 +190,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // get individual client's stats var clientStats = playerStats[pl.ClientId]; - // sync their score - clientStats.SessionScore += (pl.Score - clientStats.LastScore); + /*// sync their score + clientStats.SessionScore += pl.Score;*/ // remove the client from the stats dictionary as they're leaving playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3); detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4); - // sync their stats before they leave - clientStats = UpdateStats(clientStats); + /* // sync their stats before they leave + clientStats = UpdateStats(clientStats);*/ // todo: should this be saved every disconnect? statsSvc.ClientStatSvc.Update(clientStats); @@ -229,8 +229,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers catch (FormatException) { - Log.WriteWarning("Could not parse kill or death origin vector"); - Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin}"); + Log.WriteWarning("Could not parse kill or death origin or viewangle vectors"); + Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAgnel - {viewAngles}"); await AddStandardKill(attacker, victim); return; } @@ -342,15 +342,27 @@ namespace IW4MAdmin.Plugins.Stats.Helpers return; } +#if DEBUG + Log.WriteDebug("Calculating standard kill"); +#endif + // update the total stats Servers[serverId].ServerStatistics.TotalKills += 1; - attackerStats.SessionScore += (attacker.Score - attackerStats.LastScore); - victimStats.SessionScore += (victim.Score - victimStats.LastScore); + // this happens when the round has changed + if (attackerStats.SessionScore == 0) + attackerStats.LastScore = 0; + + if (victimStats.SessionScore == 0) + victimStats.LastScore = 0; + + attackerStats.SessionScore = attacker.Score; + victimStats.SessionScore = victim.Score; // calculate for the clients CalculateKill(attackerStats, victimStats); // this should fix the negative SPM + // updates their last score after being calculated attackerStats.LastScore = attacker.Score; victimStats.LastScore = victim.Score; @@ -427,14 +439,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // prevent NaN or inactive time lowering SPM if ((DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0 < 0.01 || (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0 > 3 || - clientStats.SessionScore < 1) + clientStats.SessionScore == 0) + { + // prevents idle time counting + clientStats.LastStatCalculation = DateTime.UtcNow; return clientStats; + } double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0; double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0; // calculate the players Score Per Minute for the current session - int scoreDifference = clientStats.LastScore == 0 ? 0 : clientStats.SessionScore - clientStats.LastScore; + int scoreDifference = clientStats.RoundScore - clientStats.LastScore; double killSPM = scoreDifference / timeSinceLastCalc; // calculate how much the KDR should weigh @@ -442,9 +458,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers double kdr = clientStats.Deaths == 0 ? clientStats.Kills : clientStats.KDR; double KDRWeight = Math.Round(Math.Pow(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 totalPlayTime = (clientStats.TimePlayed == 0) ? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds : @@ -504,10 +517,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { var serverStats = Servers[serverId]; foreach (var stat in serverStats.PlayerStats.Values) - { - stat.KillStreak = 0; - stat.DeathStreak = 0; - } + stat.StartNewSession(); } public void ResetStats(int clientId, int serverId) @@ -517,6 +527,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers stats.Deaths = 0; stats.SPM = 0; stats.Skill = 0; + stats.TimePlayed = 0; } public async Task AddMessageAsync(int clientId, int serverId, string message) diff --git a/Plugins/Stats/Models/EFClientStatistics.cs b/Plugins/Stats/Models/EFClientStatistics.cs index 8c114b399..610f4ab9b 100644 --- a/Plugins/Stats/Models/EFClientStatistics.cs +++ b/Plugins/Stats/Models/EFClientStatistics.cs @@ -56,7 +56,34 @@ namespace IW4MAdmin.Plugins.Stats.Models public int LastScore { get; set; } [NotMapped] public DateTime LastActive { get; set; } + public void StartNewSession() + { + KillStreak = 0; + DeathStreak = 0; + LastScore = 0; + SessionScores.Add(0); + } [NotMapped] - public int SessionScore { get; set; } + public int SessionScore + { + set + { + SessionScores[SessionScores.Count - 1] = value; + } + get + { + return SessionScores.Sum(); + } + } + [NotMapped] + public int RoundScore + { + get + { + return SessionScores[SessionScores.Count - 1]; + } + } + [NotMapped] + private List SessionScores = new List() { 0 }; } } diff --git a/SharedLibraryCore/Services/PenaltyService.cs b/SharedLibraryCore/Services/PenaltyService.cs index d0bb799a6..0c6288476 100644 --- a/SharedLibraryCore/Services/PenaltyService.cs +++ b/SharedLibraryCore/Services/PenaltyService.cs @@ -262,14 +262,17 @@ namespace SharedLibraryCore.Services { p.Active = false; // reset the player levels - if (p.Type == Objects.Penalty.PenaltyType.Ban) + if (p.Type == Penalty.PenaltyType.Ban) { using (var internalContext = new DatabaseContext()) { await internalContext.Clients .Where(c => c.AliasLinkId == p.LinkId) .ForEachAsync(c => c.Level = Objects.Player.Permission.User); + await internalContext.SaveChangesAsync(); } + + } }); diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 38d294df3..afc4f3b35 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -183,6 +183,9 @@ namespace SharedLibraryCore { if (Int64.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long id)) return id; + var bot = Regex.Match(str, @"bot[0-9]+").Value; + if (!string.IsNullOrEmpty(bot)) + return -Convert.ToInt64(bot.Substring(3)); return 0; } diff --git a/_commands.gsc b/_commands.gsc new file mode 100644 index 000000000..bda9f3a91 --- /dev/null +++ b/_commands.gsc @@ -0,0 +1,30 @@ +#include maps\mp\_utility; +#include maps\mp\gametypes\_hud_util; +#include common_scripts\utility; + +init() +{ + level thread WaitForCommand(); +} + +WaitForCommand() +{ + for(;;) + { + command = getDvar("sv_iw4madmin_command"); + switch(command) + { + case "balance": + if (isRoundBased()) + { + iPrintLnBold("Balancing Teams.."); + level maps\mp\gametypes\_teams::balanceTeams(); + } + break; + } + + setDvar("sv_iw4madmin_command", ""); + + wait(1); + } +} \ No newline at end of file