From 56cb8c50e7b2b58a85b30b7137be8e4d0f647727 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Mon, 27 Aug 2018 17:07:54 -0500 Subject: [PATCH] reworked event management (again) almost finished --- Application/Application.csproj | 2 +- Application/Core/ClientAuthentication.cs | 4 +- Application/EventParsers/IW4EventParser.cs | 3 +- Application/EventParsers/T6MEventParser.cs | 95 -------- Application/GameEventHandler.cs | 49 +--- Application/Main.cs | 2 +- Application/Manager.cs | 205 ++++++++-------- Application/RconParsers/IW4RConParser.cs | 3 +- Application/RconParsers/IW5MRConParser.cs | 3 +- Application/RconParsers/T6MRConParser.cs | 1 + Application/Server.cs | 181 +++++++------- Plugins/Stats/Helpers/StatManager.cs | 2 +- Plugins/Tests/Plugin.cs | 224 +++++++++++------- SharedLibraryCore/Event.cs | 5 +- SharedLibraryCore/Interfaces/IEventHandler.cs | 13 +- SharedLibraryCore/Interfaces/ILogger.cs | 1 + SharedLibraryCore/Interfaces/IManager.cs | 2 +- SharedLibraryCore/Objects/Player.cs | 18 +- 18 files changed, 382 insertions(+), 431 deletions(-) diff --git a/Application/Application.csproj b/Application/Application.csproj index b7359761d..3550af1e0 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -5,7 +5,7 @@ netcoreapp2.1 false RaidMax.IW4MAdmin.Application - 2.1.4 + 2.1.5 RaidMax Forever None IW4MAdmin diff --git a/Application/Core/ClientAuthentication.cs b/Application/Core/ClientAuthentication.cs index 1f818cabc..4b73fd321 100644 --- a/Application/Core/ClientAuthentication.cs +++ b/Application/Core/ClientAuthentication.cs @@ -36,7 +36,7 @@ namespace IW4MAdmin.Application.Core if (!AuthenticatedClients.TryGetValue(client.NetworkId, out Player value)) { // authenticate them - client.IsAuthenticated = true; + client.State = Player.ClientState.Authenticated; AuthenticatedClients.Add(client.NetworkId, client); } else @@ -57,7 +57,7 @@ namespace IW4MAdmin.Application.Core if (!AuthenticatedClients.TryGetValue(clientToAuthenticate.NetworkId, out Player value)) { // authenticate them - clientToAuthenticate.IsAuthenticated = true; + clientToAuthenticate.State = Player.ClientState.Authenticated; AuthenticatedClients.Add(clientToAuthenticate.NetworkId, clientToAuthenticate); } } diff --git a/Application/EventParsers/IW4EventParser.cs b/Application/EventParsers/IW4EventParser.cs index 2f0af3947..db3f08f82 100644 --- a/Application/EventParsers/IW4EventParser.cs +++ b/Application/EventParsers/IW4EventParser.cs @@ -133,7 +133,8 @@ namespace IW4MAdmin.Application.EventParsers { Name = regexMatch.Groups[4].ToString().StripColors(), NetworkId = regexMatch.Groups[2].ToString().ConvertLong(), - ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()) + ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()), + State = Player.ClientState.Connecting } }; } diff --git a/Application/EventParsers/T6MEventParser.cs b/Application/EventParsers/T6MEventParser.cs index 901a2ce4b..1f016b0d9 100644 --- a/Application/EventParsers/T6MEventParser.cs +++ b/Application/EventParsers/T6MEventParser.cs @@ -11,101 +11,6 @@ namespace IW4MAdmin.Application.EventParsers { class T6MEventParser : IW4EventParser { - /*public GameEvent GetEvent(Server server, string logLine) - { - string cleanedEventLine = Regex.Replace(logLine, @"^ *[0-9]+:[0-9]+ *", "").Trim(); - string[] lineSplit = cleanedEventLine.Split(';'); - - if (lineSplit[0][0] == 'K') - { - return new GameEvent() - { - Type = GameEvent.EventType.Kill, - Data = cleanedEventLine, - Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)), - Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)), - Owner = server - }; - } - - if (lineSplit[0][0] == 'D') - { - return new GameEvent() - { - Type = GameEvent.EventType.Damage, - Data = cleanedEventLine, - Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)), - Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)), - Owner = server - }; - } - - if (lineSplit[0] == "say" || lineSplit[0] == "sayteam") - { - return new GameEvent() - { - Type = GameEvent.EventType.Say, - Data = lineSplit[4], - Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)), - Owner = server, - Message = lineSplit[4] - }; - } - - if (lineSplit[0].Contains("ExitLevel")) - { - return new GameEvent() - { - Type = GameEvent.EventType.MapEnd, - Data = lineSplit[0], - Origin = new Player() - { - ClientId = 1 - }, - Target = new Player() - { - ClientId = 1 - }, - Owner = server - }; - } - - if (lineSplit[0].Contains("InitGame")) - { - string dump = cleanedEventLine.Replace("InitGame: ", ""); - - return new GameEvent() - { - Type = GameEvent.EventType.MapChange, - Data = lineSplit[0], - Origin = new Player() - { - ClientId = 1 - }, - Target = new Player() - { - ClientId = 1 - }, - Owner = server, - Extra = dump.DictionaryFromKeyValue() - }; - } - - return new GameEvent() - { - Type = GameEvent.EventType.Unknown, - Origin = new Player() - { - ClientId = 1 - }, - Target = new Player() - { - ClientId = 1 - }, - Owner = server - }; - }*/ - public override string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data"; } } diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index 360204527..d23d74d95 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -8,61 +8,20 @@ namespace IW4MAdmin.Application { class GameEventHandler : IEventHandler { - private ConcurrentQueue EventQueue; - private Queue DelayedEventQueue; - private IManager Manager; + private readonly IManager Manager; public GameEventHandler(IManager mgr) { - EventQueue = new ConcurrentQueue(); - DelayedEventQueue = new Queue(); - Manager = mgr; } - public void AddEvent(GameEvent gameEvent, bool delayedExecution = false) + public void AddEvent(GameEvent gameEvent) { #if DEBUG Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}"); #endif - if (delayedExecution) - { - DelayedEventQueue.Enqueue(gameEvent); - } - else - { - EventQueue.Enqueue(gameEvent); - Manager.SetHasEvent(); - } -#if DEBUG - Manager.GetLogger().WriteDebug($"There are now {EventQueue.Count} events in queue"); -#endif - } - - public string[] GetEventOutput() - { - throw new NotImplementedException(); - } - - public GameEvent GetNextEvent() - { - if (EventQueue.Count > 0) - { -#if DEBUG - Manager.GetLogger().WriteDebug("Getting next event to be processed"); -#endif - if (!EventQueue.TryDequeue(out GameEvent newEvent)) - { - Manager.GetLogger().WriteError("Could not dequeue event for processing"); - } - - else - { - return newEvent; - } - } - - return null; + // todo: later + ((Manager as ApplicationManager).OnServerEvent)(this, new ApplicationManager.GameEventArgs(null, false, gameEvent)); } } } diff --git a/Application/Main.cs b/Application/Main.cs index d8a4787c7..21e535c79 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -166,7 +166,7 @@ namespace IW4MAdmin.Application } OnShutdownComplete.Reset(); - ServerManager.Start().Wait(); + ServerManager.Start(); ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]); OnShutdownComplete.Set(); } diff --git a/Application/Manager.cs b/Application/Manager.cs index 102c69341..8bc28edfd 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -32,7 +32,11 @@ namespace IW4MAdmin.Application public ILogger Logger { get; private set; } public bool Running { get; private set; } public bool IsInitialized { get; private set; } - public EventHandler ServerEventOccurred { get; private set; } + //public EventHandler ServerEventOccurred { get; private set; } + // define what the delagate function looks like + public delegate void OnServerEventEventHandler(object sender, GameEventArgs e); + // expose the event handler so we can execute the events + public OnServerEventEventHandler OnServerEvent { get; private set; } public DateTime StartTime { get; private set; } static ApplicationManager Instance; @@ -48,6 +52,17 @@ namespace IW4MAdmin.Application ManualResetEventSlim OnEvent; readonly IPageList PageList; + public class GameEventArgs : System.ComponentModel.AsyncCompletedEventArgs + { + + public GameEventArgs(Exception error, bool cancelled, GameEvent userState) : base(error, cancelled, userState) + { + Event = userState; + } + + public GameEvent Event { get; } + } + private ApplicationManager() { Logger = new Logger($@"{Utilities.OperatingDirectory}IW4MAdmin.log"); @@ -60,11 +75,96 @@ namespace IW4MAdmin.Application PenaltySvc = new PenaltyService(); PrivilegedClients = new Dictionary(); Api = new EventApi(); - ServerEventOccurred += Api.OnServerEvent; + //ServerEventOccurred += Api.OnServerEvent; ConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings"); StartTime = DateTime.UtcNow; OnEvent = new ManualResetEventSlim(); PageList = new PageList(); + OnServerEvent += OnServerEventAsync; + } + + private async void OnServerEventAsync(object sender, GameEventArgs args) + { + var newEvent = args.Event; + + try + { + // if the origin client is not in an authorized state (detected by RCon) don't execute the event + if (GameEvent.ShouldOriginEventBeDelayed(newEvent)) + { + Logger.WriteDebug($"Delaying origin execution of event type {newEvent.Type} for {newEvent.Origin} because they are not authed"); + // offload it to the player to keep + newEvent.Origin.DelayedEvents.Enqueue(newEvent); + return; + } + + // if the target client is not in an authorized state (detected by RCon) don't execute the event + if (GameEvent.ShouldTargetEventBeDelayed(newEvent)) + { + Logger.WriteDebug($"Delaying target execution of event type {newEvent.Type} for {newEvent.Target} because they are not authed"); + // offload it to the player to keep + newEvent.Target.DelayedEvents.Enqueue(newEvent); + return; + } + + await newEvent.Owner.ExecuteEvent(newEvent); + + //// todo: this is a hacky mess + if (newEvent.Origin?.DelayedEvents?.Count > 0 && + newEvent.Origin?.State == Player.ClientState.Connected) + { + var events = newEvent.Origin.DelayedEvents; + + // add the delayed event to the queue + while (events?.Count > 0) + { + var e = events.Dequeue(); + e.Origin = newEvent.Origin; + // check if the target was assigned + if (e.Target != null) + { + // update the target incase they left or have newer info + e.Target = newEvent.Owner.GetPlayersAsList() + .FirstOrDefault(p => p.NetworkId == e.Target.NetworkId); + // we have to throw out the event because they left + if (e.Target == null) + { + Logger.WriteWarning($"Delayed event for {e.Origin} was removed because the target has left"); + continue; + } + } + this.GetEventHandler().AddEvent(e); + } + } +#if DEBUG + Logger.WriteDebug("Processed Event"); +#endif + } + + // this happens if a plugin requires login + catch (AuthorizationException ex) + { + await newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}"); + } + + catch (NetworkException ex) + { + Logger.WriteError(ex.Message); + } + + catch (ServerException ex) + { + Logger.WriteWarning(ex.Message); + } + + catch (Exception ex) + { + Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}"); + Logger.WriteDebug("Error Message: " + ex.Message); + Logger.WriteDebug("Error Trace: " + ex.StackTrace); + } + // tell anyone waiting for the output that we're done + newEvent.OnProcessed.Set(); } public IList GetServers() @@ -91,7 +191,9 @@ namespace IW4MAdmin.Application { // select the server ids that have completed the update task var serverTasksToRemove = runningUpdateTasks - .Where(ut => ut.Value.Status != TaskStatus.Running) + .Where(ut => ut.Value.Status == TaskStatus.RanToCompletion || + ut.Value.Status == TaskStatus.Canceled || + ut.Value.Status == TaskStatus.Faulted) .Select(ut => ut.Key) .ToList(); @@ -109,7 +211,8 @@ namespace IW4MAdmin.Application } // select the servers where the tasks have completed - foreach (var server in Servers.Where(s => serverTasksToRemove.Count == 0 ? true : serverTasksToRemove.Contains(GetHashCode()))) + var serverIds = Servers.Select(s => s.GetHashCode()).Except(runningUpdateTasks.Select(r => r.Key)).ToList(); + foreach (var server in Servers.Where(s => serverIds.Contains(s.GetHashCode()))) { runningUpdateTasks.Add(server.GetHashCode(), Task.Run(async () => { @@ -133,7 +236,7 @@ namespace IW4MAdmin.Application Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks"); #endif #if DEBUG - await Task.Delay(30000); + await Task.Delay(10000); #else await Task.Delay(ConfigHandler.Configuration().RConPollRate); #endif @@ -397,7 +500,7 @@ namespace IW4MAdmin.Application } } - public async Task Start() + public void Start() { // this needs to be run seperately from the main thread #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed @@ -408,101 +511,11 @@ namespace IW4MAdmin.Application Task.Run(() => UpdateServerStates()); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - var eventList = new List(); - - async Task processEvent(GameEvent newEvent) - { - try - { - await newEvent.Owner.ExecuteEvent(newEvent); - - // todo: this is a hacky mess - if (newEvent.Origin?.DelayedEvents?.Count > 0 && - newEvent.Origin?.State == Player.ClientState.Connected) - { - var events = newEvent.Origin.DelayedEvents; - - // add the delayed event to the queue - while (events?.Count > 0) - { - var e = events.Dequeue(); - e.Origin = newEvent.Origin; - // check if the target was assigned - if (e.Target != null) - { - // update the target incase they left or have newer info - e.Target = newEvent.Owner.GetPlayersAsList() - .FirstOrDefault(p => p.NetworkId == e.Target.NetworkId); - // we have to throw out the event because they left - if (e.Target == null) - { - Logger.WriteWarning($"Delayed event for {e.Origin} was removed because the target has left"); - continue; - } - } - this.GetEventHandler().AddEvent(e); - } - } -#if DEBUG - Logger.WriteDebug("Processed Event"); -#endif - } - - // this happens if a plugin requires login - catch (AuthorizationException e) - { - await newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {e.Message}"); - } - - catch (NetworkException e) - { - Logger.WriteError(e.Message); - } - - catch (Exception E) - { - Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}"); - Logger.WriteDebug("Error Message: " + E.Message); - Logger.WriteDebug("Error Trace: " + E.StackTrace); - } - // tell anyone waiting for the output that we're done - newEvent.OnProcessed.Set(); - }; - - GameEvent queuedEvent = null; - while (Running) { - // wait for new event to be added OnEvent.Wait(); - while ((queuedEvent = Handler.GetNextEvent()) != null) - { - if (GameEvent.ShouldOriginEventBeDelayed(queuedEvent)) - { - Logger.WriteDebug($"Delaying origin execution of event type {queuedEvent.Type} for {queuedEvent.Origin} because they are not authed"); - // offload it to the player to keep - queuedEvent.Origin.DelayedEvents.Enqueue(queuedEvent); - continue; - } - - if (GameEvent.ShouldTargetEventBeDelayed(queuedEvent)) - { - Logger.WriteDebug($"Delaying target execution of event type {queuedEvent.Type} for {queuedEvent.Target} because they are not authed"); - // offload it to the player to keep - queuedEvent.Target.DelayedEvents.Enqueue(queuedEvent); - continue; - } - // for delayed events, they're added after the connect event so it should work out - await processEvent(queuedEvent); - } - - // signal that all events have been processed OnEvent.Reset(); } -#if !DEBUG - foreach (var S in _servers) - await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]); -#endif _servers.Clear(); } diff --git a/Application/RconParsers/IW4RConParser.cs b/Application/RconParsers/IW4RConParser.cs index af37227ef..ead23c3d1 100644 --- a/Application/RconParsers/IW4RConParser.cs +++ b/Application/RconParsers/IW4RConParser.cs @@ -114,7 +114,8 @@ namespace IW4MAdmin.Application.RconParsers IPAddress = ip == 0 ? int.MinValue : ip, Ping = ping, Score = score, - IsBot = ip == 0 + IsBot = ip == 0, + State = Player.ClientState.Connecting }; StatusPlayers.Add(P); diff --git a/Application/RconParsers/IW5MRConParser.cs b/Application/RconParsers/IW5MRConParser.cs index 4112e0ef4..baec8387f 100644 --- a/Application/RconParsers/IW5MRConParser.cs +++ b/Application/RconParsers/IW5MRConParser.cs @@ -153,7 +153,8 @@ namespace IW4MAdmin.WApplication.RconParsers IPAddress = ipAddress, Ping = Ping, Score = score, - IsBot = false + IsBot = false, + State = Player.ClientState.Connecting }; StatusPlayers.Add(p); diff --git a/Application/RconParsers/T6MRConParser.cs b/Application/RconParsers/T6MRConParser.cs index 48016f0b3..485d1a64d 100644 --- a/Application/RconParsers/T6MRConParser.cs +++ b/Application/RconParsers/T6MRConParser.cs @@ -185,6 +185,7 @@ namespace IW4MAdmin.Application.RconParsers IPAddress = ipAddress, Ping = Ping, Score = score, + State = Player.ClientState.Connecting, IsBot = networkId == 0 }; diff --git a/Application/Server.cs b/Application/Server.cs index 7d7542931..dc633944f 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -68,32 +68,21 @@ namespace IW4MAdmin override public async Task AddPlayer(Player polledPlayer) { - if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) || - polledPlayer.Ping < 1 || + //if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) || + // polledPlayer.Ping < 1 || + if ( polledPlayer.ClientNumber < 0) { //Logger.WriteDebug($"Skipping client not in connected state {P}"); - return true; + return false; } - if (Players[polledPlayer.ClientNumber] != null && - Players[polledPlayer.ClientNumber].NetworkId == polledPlayer.NetworkId && - Players[polledPlayer.ClientNumber].State == Player.ClientState.Connected) - { - // update their ping & score - Players[polledPlayer.ClientNumber].Ping = polledPlayer.Ping; - Players[polledPlayer.ClientNumber].Score = polledPlayer.Score; - return true; - } - - if (Players[polledPlayer.ClientNumber] == null) + // set this when they are waiting for authentication + if (Players[polledPlayer.ClientNumber] == null && + polledPlayer.State == Player.ClientState.Connecting) { Players[polledPlayer.ClientNumber] = polledPlayer; - } - - if (!polledPlayer.IsAuthenticated) - { - return true; + return false; } #if !DEBUG @@ -196,7 +185,6 @@ namespace IW4MAdmin player.ClientNumber = polledPlayer.ClientNumber; player.IsBot = polledPlayer.IsBot; player.Score = polledPlayer.Score; - player.IsAuthenticated = true; player.CurrentServer = this; Players[player.ClientNumber] = player; @@ -246,18 +234,9 @@ namespace IW4MAdmin // they didn't fully connect so empty their slot Players[player.ClientNumber] = null; - return true; + return false; } - var e = new GameEvent() - { - Type = GameEvent.EventType.Connect, - Origin = player, - Owner = this - }; - - Manager.GetEventHandler().AddEvent(e); - player.State = Player.ClientState.Connected; return true; } @@ -278,8 +257,10 @@ namespace IW4MAdmin { Player Leaving = Players[cNum]; Logger.WriteInfo($"Client {Leaving}, state {Leaving.State.ToString()} disconnecting..."); + Leaving.State = Player.ClientState.Disconnecting; - if (!Leaving.IsAuthenticated || Leaving.State != Player.ClientState.Connected) + // occurs when the player disconnects via log before being authenticated by RCon + if (Leaving.State != Player.ClientState.Connected) { Players[cNum] = null; } @@ -394,8 +375,29 @@ namespace IW4MAdmin /// override protected async Task ProcessEvent(GameEvent E) { + + if (E.Type == GameEvent.EventType.StatusUpdate) + { + // this event gets called before they're full connected + if (E.Origin != null) + { + //var existingClient = Players[E.Origin.ClientNumber] ?? E.Origin; + //existingClient.Ping = E.Origin.Ping; + //existingClient.Score = E.Origin.Score; + } + } + if (E.Type == GameEvent.EventType.Connect) { + E.Origin.State = Player.ClientState.Authenticated; + // add them to the server + if (!await AddPlayer(E.Origin)) + { + throw new ServerException("Player didn't pass authorization, so we are discontinuing event"); + } + // hack makes the event propgate with the correct info + E.Origin = Players[E.Origin.ClientNumber]; + ChatHistory.Add(new ChatInfo() { Name = E.Origin?.Name ?? "ERROR!", @@ -404,7 +406,7 @@ namespace IW4MAdmin }); if (E.Origin.Level > Player.Permission.Moderator) - await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count)); + await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count)); } else if (E.Type == GameEvent.EventType.Join) @@ -427,16 +429,11 @@ namespace IW4MAdmin Owner = this }; - if (e.Origin != null) - { - e.Origin.State = Player.ClientState.Disconnecting; - } - Manager.GetEventHandler().AddEvent(e); } else if (origin != null && - origin.State == Player.ClientState.Connecting) + origin.State != Player.ClientState.Connected) { await RemovePlayer(origin.ClientNumber); } @@ -540,61 +537,36 @@ namespace IW4MAdmin ChatHistory.Clear(); } - async Task PollPlayersAsync() + /// + /// lists the connecting and disconnecting clients via RCon response + /// array index 0 = connecting clients + /// array index 1 = disconnecting clients + /// + /// + async Task[]> PollPlayersAsync() { +#if DEBUG var now = DateTime.Now; - - List CurrentPlayers = null; - try - { - CurrentPlayers = await this.GetStatusAsync(); - } - - // when the server has lost connection - catch (NetworkException) - { - Throttled = true; - return ClientNum; - } +#endif + var currentClients = GetPlayersAsList(); + var polledClients = await this.GetStatusAsync(); #if DEBUG Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms"); #endif Throttled = false; - var clients = GetPlayersAsList(); - foreach (var client in clients) + foreach(var client in polledClients) { - // remove players that have disconnected - if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId)) - { - // the log should already have started a disconnect event - if (client.State == Player.ClientState.Disconnecting) - continue; - - var e = new GameEvent() - { - Type = GameEvent.EventType.Disconnect, - Origin = client, - Owner = this - }; - - client.State = Player.ClientState.Disconnecting; - - Manager.GetEventHandler().AddEvent(e); - // todo: needed? - // wait until the disconnect event is complete - e.OnProcessed.Wait(); - } + // todo: move out somehwere + var existingClient = Players[client.ClientNumber] ?? client; + existingClient.Ping = client.Ping; + existingClient.Score = client.Score; } - AuthQueue.AuthenticateClients(CurrentPlayers); + var disconnectingClients = currentClients.Except(polledClients); + var connectingClients = polledClients.Except(currentClients); - foreach (var c in AuthQueue.GetAuthenticatedClients()) - { - await AddPlayer(c); - } - - return CurrentPlayers.Count; + return new List[] { connectingClients.ToList(), disconnectingClients.ToList() }; } DateTime start = DateTime.Now; @@ -621,8 +593,49 @@ namespace IW4MAdmin try { - int polledPlayerCount = await PollPlayersAsync(); + var polledClients = await PollPlayersAsync(); + var waiterList = new List(); + foreach (var disconnectingClient in polledClients[1]) + { + if (disconnectingClient.State == Player.ClientState.Disconnecting) + { + continue; + } + + var e = new GameEvent() + { + Type = GameEvent.EventType.Disconnect, + Origin = disconnectingClient, + Owner = this + }; + + Manager.GetEventHandler().AddEvent(e); + // wait until the disconnect event is complete + // because we don't want to try to fill up a slot that's not empty yet + waiterList.Add(e.OnProcessed); + } + // wait for all the disconnect tasks to finish + await Task.WhenAll(waiterList.Select(t => Task.Run(() => t.Wait(5000)))); + + waiterList.Clear(); + // this are our new connecting clients + foreach (var client in polledClients[0]) + { + var e = new GameEvent() + { + Type = GameEvent.EventType.Connect, + Origin = client, + Owner = this + }; + + Manager.GetEventHandler().AddEvent(e); + waiterList.Add(e.OnProcessed); + } + + // wait for all the connect tasks to finish + await Task.WhenAll(waiterList.Select(t => Task.Run(() => t.Wait()))); + if (ConnectionErrors > 0) { Logger.WriteVerbose($"{loc["MANAGER_CONNECTION_REST"]} {IP}:{Port}"); @@ -793,7 +806,7 @@ namespace IW4MAdmin CustomCallback = await ScriptLoaded(); string mainPath = EventParser.GetGameDir(); #if DEBUG - basepath.Value = @"\\192.168.88.253\Call of Duty Black Ops II"; + basepath.Value = @""; #endif string logPath; if (GameName == Game.IW5) diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index e7e667d31..f1d4391de 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -831,7 +831,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths; double alpha = Math.Sqrt(2) / Math.Min(600, clientStats.Kills + clientStats.Deaths); clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR; - double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3); + double KDRWeight = clientStats.RollingWeightedKDR != 0 ? Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3) : 0; // calculate the weight of the new play time against last 10 hours of gameplay int totalPlayTime = (clientStats.TimePlayed == 0) ? diff --git a/Plugins/Tests/Plugin.cs b/Plugins/Tests/Plugin.cs index 76b194c56..8aaab03cb 100644 --- a/Plugins/Tests/Plugin.cs +++ b/Plugins/Tests/Plugin.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using SharedLibraryCore; using SharedLibraryCore.Interfaces; using SharedLibraryCore.Helpers; +using SharedLibraryCore.Objects; namespace IW4MAdmin.Plugins { @@ -19,34 +20,85 @@ namespace IW4MAdmin.Plugins public async Task OnEventAsync(GameEvent E, Server S) { + return; if (E.Type == GameEvent.EventType.Start) { - #region PLAYER_HISTORY - var rand = new Random(GetHashCode()); - var time = DateTime.UtcNow; + #region UNIT_TEST_LOG_CONNECT + for (int i = 1; i <= 8; i++) + { + var e = new GameEvent() + { + Type = GameEvent.EventType.Join, + Origin = new Player() + { + Name = $"Player{i}", + NetworkId = i, + ClientNumber = i - 1 + }, + Owner = S + }; - await Task.Run(() => - { - if (S.PlayerHistory.Count > 0) - return; + S.Manager.GetEventHandler().AddEvent(e); + e.OnProcessed.Wait(); + } - while (S.PlayerHistory.Count < 144) - { - S.PlayerHistory.Enqueue(new PlayerHistory(time, rand.Next(7, 18))); - time = time.AddMinutes(PlayerHistory.UpdateInterval); - } - }); + S.Logger.WriteAssert(S.ClientNum == 8, "UNIT_TEST_LOG_CONNECT failed client num check"); #endregion - #region PLUGIN_INFO - Console.WriteLine("|Name |Alias|Description |Requires Target|Syntax |Required Level|"); - Console.WriteLine("|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|"); - foreach (var command in S.Manager.GetCommands().OrderByDescending(c => c.Permission).ThenBy(c => c.Name)) + #region UNIT_TEST_RCON_AUTHENTICATE + for (int i = 1; i <= 8; i++) { - Console.WriteLine($"|{command.Name}|{command.Alias}|{command.Description}|{command.RequiresTarget}|{command.Syntax.Substring(8).EscapeMarkdown()}|{command.Permission}|"); + var e = new GameEvent() + { + Type = GameEvent.EventType.Connect, + Origin = new Player() + { + Name = $"Player{i}", + NetworkId = i, + ClientNumber = i - 1, + IPAddress = i, + Ping = 50, + CurrentServer = S + }, + Owner = S, + }; + + S.Manager.GetEventHandler().AddEvent(e); + e.OnProcessed.Wait(); } + + S.Logger.WriteAssert(S.GetPlayersAsList().Count(p => p.State == Player.ClientState.Connected) == 8, + "UNIT_TEST_RCON_AUTHENTICATE failed client num connected state check"); #endregion } + //if (E.Type == GameEvent.EventType.Start) + //{ + // #region PLAYER_HISTORY + // var rand = new Random(GetHashCode()); + // var time = DateTime.UtcNow; + + // await Task.Run(() => + // { + // if (S.PlayerHistory.Count > 0) + // return; + + // while (S.PlayerHistory.Count < 144) + // { + // S.PlayerHistory.Enqueue(new PlayerHistory(time, rand.Next(7, 18))); + // time = time.AddMinutes(PlayerHistory.UpdateInterval); + // } + // }); + // #endregion + + // #region PLUGIN_INFO + // Console.WriteLine("|Name |Alias|Description |Requires Target|Syntax |Required Level|"); + // Console.WriteLine("|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|"); + // foreach (var command in S.Manager.GetCommands().OrderByDescending(c => c.Permission).ThenBy(c => c.Name)) + // { + // Console.WriteLine($"|{command.Name}|{command.Alias}|{command.Description}|{command.RequiresTarget}|{command.Syntax.Substring(8).EscapeMarkdown()}|{command.Permission}|"); + // } + // #endregion + //} } public Task OnLoadAsync(IManager manager) => Task.CompletedTask; @@ -54,84 +106,84 @@ namespace IW4MAdmin.Plugins public Task OnTickAsync(Server S) { return Task.CompletedTask; - /* - if ((DateTime.Now - Interval).TotalSeconds > 1) - { - var rand = new Random(); - int index = rand.Next(0, 17); - var p = new Player() - { - Name = $"Test_{index}", - NetworkId = (long)$"_test_{index}".GetHashCode(), - ClientNumber = index, - Ping = 1, - IPAddress = $"127.0.0.{index}".ConvertToIP() - }; + /* + if ((DateTime.Now - Interval).TotalSeconds > 1) + { + var rand = new Random(); + int index = rand.Next(0, 17); + var p = new Player() + { + Name = $"Test_{index}", + NetworkId = (long)$"_test_{index}".GetHashCode(), + ClientNumber = index, + Ping = 1, + IPAddress = $"127.0.0.{index}".ConvertToIP() + }; - if (S.Players.ElementAt(index) != null) - await S.RemovePlayer(index); - // await S.AddPlayer(p); + if (S.Players.ElementAt(index) != null) + await S.RemovePlayer(index); + // await S.AddPlayer(p); - Interval = DateTime.Now; - if (S.ClientNum > 0) - { - 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)]; + Interval = DateTime.Now; + if (S.ClientNum > 0) + { + 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)]; - await S.ExecuteEvent(new Event(Event.GType.Say, $"test_{attackerPlayer.ClientNumber}", victimPlayer, attackerPlayer, S)); + await S.ExecuteEvent(new Event(Event.GType.Say, $"test_{attackerPlayer.ClientNumber}", victimPlayer, attackerPlayer, S)); - string[] eventLine = null; + string[] eventLine = null; - for (int i = 0; i < 1; i++) - { - if (S.GameName == Server.Game.IW4) - { + for (int i = 0; i < 1; i++) + { + if (S.GameName == Server.Game.IW4) + { - // attackerID ; victimID ; attackerOrigin ; victimOrigin ; Damage ; Weapon ; hitLocation ; meansOfDeath - var minimapInfo = StatsPlugin.MinimapConfig.IW4Minimaps().MapInfo.FirstOrDefault(m => m.MapName == S.CurrentMap.Name); - if (minimapInfo == null) - return; - eventLine = new string[] + // attackerID ; victimID ; attackerOrigin ; victimOrigin ; Damage ; Weapon ; hitLocation ; meansOfDeath + var minimapInfo = StatsPlugin.MinimapConfig.IW4Minimaps().MapInfo.FirstOrDefault(m => m.MapName == S.CurrentMap.Name); + if (minimapInfo == null) + return; + eventLine = new string[] + { + "ScriptKill", + attackerPlayer.NetworkId.ToString(), + victimPlayer.NetworkId.ToString(), + new Vector3(rand.Next(minimapInfo.MaxRight, minimapInfo.MaxLeft), rand.Next(minimapInfo.MaxBottom, minimapInfo.MaxTop), rand.Next(0, 100)).ToString(), + new Vector3(rand.Next(minimapInfo.MaxRight, minimapInfo.MaxLeft), rand.Next(minimapInfo.MaxBottom, minimapInfo.MaxTop), rand.Next(0, 100)).ToString(), + rand.Next(50, 105).ToString(), + ((StatsPlugin.IW4Info.WeaponName)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.WeaponName)).Length - 1)).ToString(), + ((StatsPlugin.IW4Info.HitLocation)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.HitLocation)).Length - 1)).ToString(), + ((StatsPlugin.IW4Info.MeansOfDeath)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.MeansOfDeath)).Length - 1)).ToString() + }; + + } + else + { + eventLine = new string[] { - "ScriptKill", - attackerPlayer.NetworkId.ToString(), - victimPlayer.NetworkId.ToString(), - new Vector3(rand.Next(minimapInfo.MaxRight, minimapInfo.MaxLeft), rand.Next(minimapInfo.MaxBottom, minimapInfo.MaxTop), rand.Next(0, 100)).ToString(), - new Vector3(rand.Next(minimapInfo.MaxRight, minimapInfo.MaxLeft), rand.Next(minimapInfo.MaxBottom, minimapInfo.MaxTop), rand.Next(0, 100)).ToString(), - rand.Next(50, 105).ToString(), - ((StatsPlugin.IW4Info.WeaponName)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.WeaponName)).Length - 1)).ToString(), - ((StatsPlugin.IW4Info.HitLocation)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.HitLocation)).Length - 1)).ToString(), - ((StatsPlugin.IW4Info.MeansOfDeath)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.MeansOfDeath)).Length - 1)).ToString() + "K", + victimPlayer.NetworkId.ToString(), + victimPlayer.ClientNumber.ToString(), + rand.Next(0, 1) == 0 ? "allies" : "axis", + victimPlayer.Name, + attackerPlayer.NetworkId.ToString(), + attackerPlayer.ClientNumber.ToString(), + rand.Next(0, 1) == 0 ? "allies" : "axis", + attackerPlayer.Name.ToString(), + ((StatsPlugin.IW4Info.WeaponName)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.WeaponName)).Length - 1)).ToString(), // Weapon + rand.Next(50, 105).ToString(), // Damage + ((StatsPlugin.IW4Info.MeansOfDeath)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.MeansOfDeath)).Length - 1)).ToString(), // Means of Death + ((StatsPlugin.IW4Info.HitLocation)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.HitLocation)).Length - 1)).ToString(), // Hit Location }; + } - } - else - { - eventLine = new string[] - { - "K", - victimPlayer.NetworkId.ToString(), - victimPlayer.ClientNumber.ToString(), - rand.Next(0, 1) == 0 ? "allies" : "axis", - victimPlayer.Name, - attackerPlayer.NetworkId.ToString(), - attackerPlayer.ClientNumber.ToString(), - rand.Next(0, 1) == 0 ? "allies" : "axis", - attackerPlayer.Name.ToString(), - ((StatsPlugin.IW4Info.WeaponName)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.WeaponName)).Length - 1)).ToString(), // Weapon - rand.Next(50, 105).ToString(), // Damage - ((StatsPlugin.IW4Info.MeansOfDeath)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.MeansOfDeath)).Length - 1)).ToString(), // Means of Death - ((StatsPlugin.IW4Info.HitLocation)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.HitLocation)).Length - 1)).ToString(), // Hit Location - }; - } - - var _event = Event.ParseEventString(eventLine, S); - await S.ExecuteEvent(_event); - } - } - } - */ + var _event = Event.ParseEventString(eventLine, S); + await S.ExecuteEvent(_event); + } + } + } + */ } public Task OnUnloadAsync() => Task.CompletedTask; diff --git a/SharedLibraryCore/Event.cs b/SharedLibraryCore/Event.cs index b5db77d50..da9606492 100644 --- a/SharedLibraryCore/Event.cs +++ b/SharedLibraryCore/Event.cs @@ -40,6 +40,8 @@ namespace SharedLibraryCore Damage, Kill, JoinTeam, + + StatusUpdate } public GameEvent(EventType t, string d, Player O, Player T, Server S) @@ -86,9 +88,9 @@ namespace SharedLibraryCore public static bool ShouldOriginEventBeDelayed(GameEvent queuedEvent) { return queuedEvent.Origin != null && - !queuedEvent.Origin.IsAuthenticated && queuedEvent.Origin.State != Player.ClientState.Connected && // we want to allow join and quit events + queuedEvent.Type != EventType.Connect && queuedEvent.Type != EventType.Join && queuedEvent.Type != EventType.Quit && // we don't care about unknown events @@ -104,7 +106,6 @@ namespace SharedLibraryCore public static bool ShouldTargetEventBeDelayed(GameEvent queuedEvent) { return queuedEvent.Target != null && - !queuedEvent.Target.IsAuthenticated && queuedEvent.Target.State != Player.ClientState.Connected && queuedEvent.Target.NetworkId != 0; } diff --git a/SharedLibraryCore/Interfaces/IEventHandler.cs b/SharedLibraryCore/Interfaces/IEventHandler.cs index 5bf54fc73..c92c5afd7 100644 --- a/SharedLibraryCore/Interfaces/IEventHandler.cs +++ b/SharedLibraryCore/Interfaces/IEventHandler.cs @@ -13,17 +13,6 @@ namespace SharedLibraryCore.Interfaces /// Add a game event event to the queue to be processed /// /// Game event - /// don't signal that an event has been aded - void AddEvent(GameEvent gameEvent, bool delayedExecution = false); - /// - /// Get the next event to be processed - /// - /// Game event that needs to be processed - GameEvent GetNextEvent(); - /// - /// If an event has output. Like executing a command wait until it's available - /// - /// List of output strings - string[] GetEventOutput(); + void AddEvent(GameEvent gameEvent); } } diff --git a/SharedLibraryCore/Interfaces/ILogger.cs b/SharedLibraryCore/Interfaces/ILogger.cs index fa04182af..57049b526 100644 --- a/SharedLibraryCore/Interfaces/ILogger.cs +++ b/SharedLibraryCore/Interfaces/ILogger.cs @@ -13,5 +13,6 @@ namespace SharedLibraryCore.Interfaces void WriteDebug(string msg); void WriteWarning(string msg); void WriteError(string msg); + void WriteAssert(bool condition, string msg); } } diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index a37bdb966..f14e0ff06 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -11,7 +11,7 @@ namespace SharedLibraryCore.Interfaces public interface IManager { Task Init(); - Task Start(); + void Start(); void Stop(); ILogger GetLogger(); IList GetServers(); diff --git a/SharedLibraryCore/Objects/Player.cs b/SharedLibraryCore/Objects/Player.cs index 334e6550d..39d8f9b98 100644 --- a/SharedLibraryCore/Objects/Player.cs +++ b/SharedLibraryCore/Objects/Player.cs @@ -11,8 +11,24 @@ namespace SharedLibraryCore.Objects { public enum ClientState { + /// + /// represents when the client has been detected as joining + /// by the log file, but has not be authenticated by RCon + /// Connecting, + /// + /// represents when the client has been parsed by RCon, + /// but has not been validated against the database + /// + Authenticated, + /// + /// represents when the client has been authenticated by RCon + /// and validated by the database + /// Connected, + /// + /// represents when the client is leaving (either through RCon or log file) + /// Disconnecting, } @@ -117,8 +133,6 @@ namespace SharedLibraryCore.Objects set { _name = value; } } [NotMapped] - public bool IsAuthenticated { get; set; } - [NotMapped] public ClientState State { get; set; } [NotMapped] public Queue DelayedEvents { get; set; }