diff --git a/.gitignore b/.gitignore
index 8baefa9b1..edbc2eec0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -240,3 +240,4 @@ launchSettings.json
 /Master/master/persistence
 /WebfrontCore/wwwroot/fonts
 /WebfrontCore/wwwroot/font
+/Plugins/Tests/TestSourceFiles
diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs
index 3d0bc746b..cb73d54ab 100644
--- a/Application/EventParsers/BaseEventParser.cs
+++ b/Application/EventParsers/BaseEventParser.cs
@@ -2,6 +2,8 @@
 using SharedLibraryCore.Database.Models;
 using SharedLibraryCore.Interfaces;
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Text.RegularExpressions;
 using static SharedLibraryCore.Server;
 
@@ -78,7 +80,18 @@ namespace IW4MAdmin.Application.EventParsers
 
         public virtual GameEvent GenerateGameEvent(string logLine)
         {
-            logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
+            var timeMatch = Regex.Match(logLine, @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )");
+            int gameTime = 0;
+            
+            if (timeMatch.Success)
+            {
+                gameTime = (timeMatch.Groups.Values as IEnumerable<object>)
+                    .Skip(2)
+                    .Select(_value => int.Parse(_value.ToString()))
+                    .Sum();
+                logLine = logLine.Substring(timeMatch.Value.Length);
+            } 
+
             string[] lineSplit = logLine.Split(';');
             string eventType = lineSplit[0];
 
@@ -107,7 +120,8 @@ namespace IW4MAdmin.Application.EventParsers
                                 Origin = new EFClient() { NetworkId = originId },
                                 Message = message,
                                 Extra = logLine,
-                                RequiredEntity = GameEvent.EventRequiredEntity.Origin
+                                RequiredEntity = GameEvent.EventRequiredEntity.Origin,
+                                GameTime = gameTime
                             };
                         }
 
@@ -118,7 +132,8 @@ namespace IW4MAdmin.Application.EventParsers
                             Origin = new EFClient() { NetworkId = originId },
                             Message = message,
                             Extra = logLine,
-                            RequiredEntity = GameEvent.EventRequiredEntity.Origin
+                            RequiredEntity = GameEvent.EventRequiredEntity.Origin,
+                            GameTime = gameTime
                         };
                     }
                 }
@@ -139,7 +154,8 @@ namespace IW4MAdmin.Application.EventParsers
                         Data = logLine,
                         Origin = new EFClient() { NetworkId = originId },
                         Target = new EFClient() { NetworkId = targetId },
-                        RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
+                        RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
+                        GameTime = gameTime
                     };
                 }
             }
@@ -159,7 +175,8 @@ namespace IW4MAdmin.Application.EventParsers
                         Data = logLine,
                         Origin = new EFClient() { NetworkId = originId },
                         Target = new EFClient() { NetworkId = targetId },
-                        RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
+                        RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
+                        GameTime = gameTime
                     };
                 }
             }
@@ -185,7 +202,8 @@ namespace IW4MAdmin.Application.EventParsers
                             State = EFClient.ClientState.Connecting,
                         },
                         RequiredEntity = GameEvent.EventRequiredEntity.None,
-                        IsBlocking = true
+                        IsBlocking = true,
+                        GameTime = gameTime
                     };
                 }
             }
@@ -210,7 +228,8 @@ namespace IW4MAdmin.Application.EventParsers
                             State = EFClient.ClientState.Disconnecting
                         },
                         RequiredEntity = GameEvent.EventRequiredEntity.None,
-                        IsBlocking = true
+                        IsBlocking = true,
+                        GameTime = gameTime
                     };
                 }
             }
@@ -223,7 +242,8 @@ namespace IW4MAdmin.Application.EventParsers
                     Data = logLine,
                     Origin = Utilities.IW4MAdminClient(),
                     Target = Utilities.IW4MAdminClient(),
-                    RequiredEntity = GameEvent.EventRequiredEntity.None
+                    RequiredEntity = GameEvent.EventRequiredEntity.None,
+                    GameTime = gameTime
                 };
             }
 
@@ -238,7 +258,8 @@ namespace IW4MAdmin.Application.EventParsers
                     Origin = Utilities.IW4MAdminClient(),
                     Target = Utilities.IW4MAdminClient(),
                     Extra = dump.DictionaryFromKeyValue(),
-                    RequiredEntity = GameEvent.EventRequiredEntity.None
+                    RequiredEntity = GameEvent.EventRequiredEntity.None,
+                    GameTime = gameTime
                 };
             }
 
@@ -250,7 +271,8 @@ namespace IW4MAdmin.Application.EventParsers
                     Type = GameEvent.EventType.JoinTeam,
                     Data = logLine,
                     Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong(Configuration.GuidNumberStyle) },
-                    RequiredEntity = GameEvent.EventRequiredEntity.Target
+                    RequiredEntity = GameEvent.EventRequiredEntity.Target,
+                    GameTime = gameTime
                 };
             }
 
@@ -267,7 +289,8 @@ namespace IW4MAdmin.Application.EventParsers
                     Data = logLine,
                     Origin = new EFClient() { NetworkId = originId },
                     Target = new EFClient() { NetworkId = targetId },
-                    RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
+                    RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
+                    GameTime = gameTime
                 };
             }
 
@@ -283,7 +306,8 @@ namespace IW4MAdmin.Application.EventParsers
                     Data = logLine,
                     Origin = new EFClient() { NetworkId = originId },
                     Target = new EFClient() { NetworkId = targetId },
-                    RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
+                    RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
+                    GameTime = gameTime
                 };
             }
 
@@ -293,7 +317,8 @@ namespace IW4MAdmin.Application.EventParsers
                 Data = logLine,
                 Origin = Utilities.IW4MAdminClient(),
                 Target = Utilities.IW4MAdminClient(),
-                RequiredEntity = GameEvent.EventRequiredEntity.None
+                RequiredEntity = GameEvent.EventRequiredEntity.None,
+                GameTime = gameTime
             };
         }
     }
diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs
index 6afdb1104..877fcc2f9 100644
--- a/Application/IW4MServer.cs
+++ b/Application/IW4MServer.cs
@@ -28,6 +28,7 @@ namespace IW4MAdmin
         private readonly ITranslationLookup _translationLookup;
         private const int REPORT_FLAG_COUNT = 4;
         private readonly IPluginImporter _pluginImporter;
+        private int lastGameTime = 0;
 
         public int Id { get; private set; }
 
@@ -93,18 +94,18 @@ namespace IW4MAdmin
             if (client.ClientNumber >= 0)
             {
 #endif
-                Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
-                Clients[client.ClientNumber] = null;
-                await client.OnDisconnect();
+            Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
+            Clients[client.ClientNumber] = null;
+            await client.OnDisconnect();
 
-                var e = new GameEvent()
-                {
-                    Origin = client,
-                    Owner = this,
-                    Type = GameEvent.EventType.Disconnect
-                };
+            var e = new GameEvent()
+            {
+                Origin = client,
+                Owner = this,
+                Type = GameEvent.EventType.Disconnect
+            };
 
-                Manager.GetEventHandler().AddEvent(e);
+            Manager.GetEventHandler().AddEvent(e);
 #if DEBUG == true
             }
 #endif
@@ -434,6 +435,14 @@ namespace IW4MAdmin
 
             else if (E.Type == GameEvent.EventType.PreDisconnect)
             {
+                bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime;
+
+                if (isPotentialFalseQuit)
+                {
+                    Logger.WriteInfo($"Receive predisconnect event for {E}, but it occured at game time {E.GameTime.Value}, which is the same last map change, so we're ignoring");
+                    return false;
+                }
+
                 // predisconnect comes from minimal rcon polled players and minimal log players
                 // so we need to disconnect the "full" version of the client
                 var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
@@ -531,11 +540,21 @@ namespace IW4MAdmin
                     string mapname = dict["mapname"];
                     UpdateMap(mapname);
                 }
+
+                if (E.GameTime.HasValue)
+                {
+                    lastGameTime = E.GameTime.Value;
+                }
             }
 
             if (E.Type == GameEvent.EventType.MapEnd)
             {
                 Logger.WriteInfo("Game ending...");
+
+                if (E.GameTime.HasValue)
+                {
+                    lastGameTime = E.GameTime.Value;
+                }
             }
 
             if (E.Type == GameEvent.EventType.Tell)
@@ -601,6 +620,12 @@ namespace IW4MAdmin
                     Logger.WriteDebug(e.GetExceptionInfo());
                 }
             }
+
+            else if (client.IPAddress != null && client.State == ClientState.Disconnecting)
+            {
+                Logger.WriteWarning($"{client} state is Disconnecting (probably kicked), but they are still connected. trying to kick again...");
+                await client.CanConnect(client.IPAddress);
+            }
         }
 
         /// <summary>
diff --git a/Plugins/Stats/Cheat/Detection.cs b/Plugins/Stats/Cheat/Detection.cs
index a66ca7806..55f70b02f 100644
--- a/Plugins/Stats/Cheat/Detection.cs
+++ b/Plugins/Stats/Cheat/Detection.cs
@@ -70,7 +70,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
         /// </summary>
         /// <param name="hit">kill performed by the player</param>
         /// <returns>true if detection reached thresholds, false otherwise</returns>
-        public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
+        public IEnumerable<DetectionPenaltyResult> ProcessHit(EFClientKill hit)
         {
             var results = new List<DetectionPenaltyResult>();
 
@@ -81,10 +81,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
                 // hack: prevents false positives
                 (LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50))
             {
-                return new DetectionPenaltyResult()
+                return new[] {new DetectionPenaltyResult()
                 {
                     ClientPenalty = EFPenalty.PenaltyType.Any,
-                };
+                }};
             }
 
             LastWeapon = hit.Weapon;
@@ -92,7 +92,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
             HitLocationCount[hit.HitLoc].Count++;
             HitCount++;
 
-            if (!isDamage)
+            if (hit.IsKill)
             {
                 Kills++;
             }
@@ -464,12 +464,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
 
             Tracker.OnChange(snapshot);
 
-            return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
-                results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
-                new DetectionPenaltyResult()
-                {
-                    ClientPenalty = EFPenalty.PenaltyType.Any,
-                };
+            return results;
         }
     }
 }
diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs
index 79f9e93ab..0155a046d 100644
--- a/Plugins/Stats/Helpers/StatManager.cs
+++ b/Plugins/Stats/Helpers/StatManager.cs
@@ -510,7 +510,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
 
                 if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId)
                 {
-                    DetectionPenaltyResult result = new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Any };
                     clientDetection.TrackedHits.Add(hit);
 
                     if (clientDetection.TrackedHits.Count >= MIN_HITS_TO_RUN_DETECTION)
@@ -525,9 +524,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
 
                             if (oldestHit.IsAlive)
                             {
-                                result = clientDetection.ProcessHit(oldestHit, isDamage);
+                                var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint);
 #if !DEBUG
-                            await ApplyPenalty(result, attacker);
+                                await ApplyPenalty(result, attacker);
 #endif
 
                                 if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
@@ -564,6 +563,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
             }
         }
 
+        private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, long serverId)
+        {
+            // allow disabling of certain detection types
+            results = results.Where(_result => ShouldUseDetection(serverId, _result.Type));
+            return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
+                results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
+                new DetectionPenaltyResult()
+                {
+                    ClientPenalty = EFPenalty.PenaltyType.Any,
+                };
+        }
+
         public async Task SaveHitCache(long serverId)
         {
             using (var ctx = new DatabaseContext(true))
@@ -594,12 +605,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
 
         async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker)
         {
-            // allow disabling of certain detection types
-            if (!ShouldUseDetection(attacker.CurrentServer.EndPoint, penalty.Type))
-            {
-                return;
-            }
-
             var penaltyClient = Utilities.IW4MAdminClient(attacker.CurrentServer);
             switch (penalty.ClientPenalty)
             {
diff --git a/SharedLibraryCore/Events/GameEvent.cs b/SharedLibraryCore/Events/GameEvent.cs
index bf60abf70..24f53c070 100644
--- a/SharedLibraryCore/Events/GameEvent.cs
+++ b/SharedLibraryCore/Events/GameEvent.cs
@@ -217,6 +217,10 @@ namespace SharedLibraryCore
         public EventRequiredEntity RequiredEntity { get; set; }
         public string Data; // Data is usually the message sent by player
         public string Message;
+        /// <summary>
+        /// Specifies the game time offset as printed in the log
+        /// </summary>
+        public int? GameTime { get; set; }
         public EFClient Origin;
         public EFClient Target;
         public Server Owner;
diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs
index 545951ff8..1ce137eca 100644
--- a/SharedLibraryCore/PartialEntities/EFClient.cs
+++ b/SharedLibraryCore/PartialEntities/EFClient.cs
@@ -328,6 +328,7 @@ namespace SharedLibraryCore.Database.Models
                 e.FailReason = GameEvent.EventFailReason.Permission;
             }
 
+            State = ClientState.Disconnecting;
             sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
             return e;
         }
@@ -563,7 +564,7 @@ namespace SharedLibraryCore.Database.Models
             CurrentServer.Logger.WriteDebug($"OnJoin finished for {this}");
         }
 
-        private async Task<bool> CanConnect(int? ipAddress)
+        public async Task<bool> CanConnect(int? ipAddress)
         {
             var loc = Utilities.CurrentLocalization.LocalizationIndex;
             var autoKickClient = Utilities.IW4MAdminClient(CurrentServer);
diff --git a/SharedLibraryCore/Services/ClientService.cs b/SharedLibraryCore/Services/ClientService.cs
index bd3efa8a2..5cc6e9b88 100644
--- a/SharedLibraryCore/Services/ClientService.cs
+++ b/SharedLibraryCore/Services/ClientService.cs
@@ -110,11 +110,14 @@ namespace SharedLibraryCore.Services
                 .Include(a => a.Link)
                 // we only want alias that have the same IP address or share a link
                 .Where(_alias => _alias.IPAddress == ip || (_alias.LinkId == entity.AliasLinkId));
-
+            
             var aliases = await iqAliases.ToListAsync();
+            var currentIPs = aliases.Where(_a2 => _a2.IPAddress != null).Select(_a2 => _a2.IPAddress).Distinct();
+            var floatingIPAliases = await context.Aliases.Where(_alias => currentIPs.Contains(_alias.IPAddress)).ToListAsync();
+             aliases.AddRange(floatingIPAliases);
 
             // see if they have a matching IP + Name but new NetworkId
-            var existingExactAlias = aliases.FirstOrDefault(a => a.Name == name && a.IPAddress == ip);
+            var existingExactAlias = aliases.OrderBy(_alias => _alias.LinkId).FirstOrDefault(a => a.Name == name && a.IPAddress == ip);
             bool hasExactAliasMatch = existingExactAlias != null;
 
             // if existing alias matches link them
@@ -128,17 +131,22 @@ namespace SharedLibraryCore.Services
             bool isAliasLinkUpdated = newAliasLink.AliasLinkId != entity.AliasLink.AliasLinkId;
 
             await context.SaveChangesAsync();
-
+            int distinctLinkCount = aliases.Select(_alias => _alias.LinkId).Distinct().Count();
             // this happens when the link we found is different than the one we create before adding an IP
-            if (isAliasLinkUpdated)
+            if (isAliasLinkUpdated || distinctLinkCount > 1)
             {
                 entity.CurrentServer.Logger.WriteDebug($"[updatealias] found a link for {entity} so we are updating link from {entity.AliasLink.AliasLinkId} to {newAliasLink.AliasLinkId}");
 
-                var oldAliasLink = entity.AliasLink;
+                var completeAliasLinkIds = aliases.Select(_item => _item.LinkId)
+                    .Append(entity.AliasLinkId)
+                    .Distinct()
+                    .ToList();
+
+                entity.CurrentServer.Logger.WriteDebug($"[updatealias] updating aliasLinks {string.Join(',', completeAliasLinkIds)} for IP {ip} to {newAliasLink.AliasLinkId}");
 
                 // update all the clients that have the old alias link
                 await context.Clients
-                    .Where(_client => _client.AliasLinkId == oldAliasLink.AliasLinkId)
+                    .Where(_client => completeAliasLinkIds.Contains(_client.AliasLinkId))
                     .ForEachAsync(_client => _client.AliasLinkId = newAliasLink.AliasLinkId);
 
                 // we also need to update all the penalties or they get deleted
@@ -151,7 +159,7 @@ namespace SharedLibraryCore.Services
                 // link2 is deleted
                 // link2 penalties are orphaned
                 await context.Penalties
-                    .Where(_penalty => _penalty.LinkId == oldAliasLink.AliasLinkId)
+                    .Where(_penalty => completeAliasLinkIds.Contains(_penalty.LinkId))
                     .ForEachAsync(_penalty => _penalty.LinkId = newAliasLink.AliasLinkId);
 
                 entity.AliasLink = newAliasLink;
@@ -159,13 +167,16 @@ namespace SharedLibraryCore.Services
 
                 // update all previous aliases
                 await context.Aliases
-                    .Where(_alias => _alias.LinkId == oldAliasLink.AliasLinkId)
+                    .Where(_alias => completeAliasLinkIds.Contains(_alias.LinkId))
                     .ForEachAsync(_alias => _alias.LinkId = newAliasLink.AliasLinkId);
 
                 await context.SaveChangesAsync();
                 // we want to delete the now inactive alias
-                context.AliasLinks.Remove(oldAliasLink);
-                await context.SaveChangesAsync();
+                if (newAliasLink.AliasLinkId != entity.AliasLinkId)
+                {
+                    context.AliasLinks.Remove(entity.AliasLink);
+                    await context.SaveChangesAsync();
+                }
             }
 
             // the existing alias matches ip and name, so we can just ignore the temporary one