From cfbacabb4a200e4aa7e0504c798b877a19734025 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 4 Sep 2018 12:40:29 -0500 Subject: [PATCH] fix bug with player not getting updated on disconnect (related to issue #24) jint version downgraded for better stability (also locked the engine instance as it's not thread safe) updated readme remove vpn detection from application configuration as it's now in a seperate plugin defaulted webfront bind URl to all interfaces readd the custom say name added visibility percentage to AC --- Application/BuildScripts/PostBuild.bat | 9 +- Application/GameEventHandler.cs | 87 ++- Application/Server.cs | 4 +- Plugins/IW4ScriptCommands/Commands/Balance.cs | 348 ++++----- Plugins/ProfanityDeterment/Plugin.cs | 23 +- Plugins/ScriptPlugins/VPNDetection.js | 41 +- Plugins/Stats/Helpers/StatManager.cs | 46 +- Plugins/Stats/Models/EFClientKill.cs | 1 + Plugins/Stats/Plugin.cs | 4 +- Plugins/Tests/PluginTests.cs | 55 ++ README.md | 46 +- .../Configuration/ApplicationConfiguration.cs | 12 +- SharedLibraryCore/Interfaces/IEventHandler.cs | 2 +- ...154622_AddVisibilityPercentage.Designer.cs | 665 ++++++++++++++++++ .../20180904154622_AddVisibilityPercentage.cs | 23 + .../DatabaseContextModelSnapshot.cs | 2 + SharedLibraryCore/RCon/Connection.cs | 9 +- SharedLibraryCore/RCon/StaticHelpers.cs | 2 +- SharedLibraryCore/ScriptPlugin.cs | 13 +- SharedLibraryCore/Server.cs | 6 +- SharedLibraryCore/SharedLibraryCore.csproj | 2 +- _customcallbacks.gsc | 39 +- 22 files changed, 1126 insertions(+), 313 deletions(-) create mode 100644 Plugins/Tests/PluginTests.cs create mode 100644 SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.Designer.cs create mode 100644 SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.cs diff --git a/Application/BuildScripts/PostBuild.bat b/Application/BuildScripts/PostBuild.bat index 9f2d44d89..98932ef91 100644 --- a/Application/BuildScripts/PostBuild.bat +++ b/Application/BuildScripts/PostBuild.bat @@ -21,4 +21,11 @@ xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Pl echo Copying script plugins for publish xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\" -xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\" \ No newline at end of file +xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\" + +echo Copying GSC files for publish +xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\" +xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\" + +xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\" +xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\" \ No newline at end of file diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index 8c3b2d7db..74730ddae 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -1,12 +1,9 @@ using SharedLibraryCore; using SharedLibraryCore.Events; using SharedLibraryCore.Interfaces; -using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; namespace IW4MAdmin.Application { @@ -22,59 +19,57 @@ namespace IW4MAdmin.Application OutOfOrderEvents = new SortedList(); } - public bool AddEvent(GameEvent gameEvent) + public void AddEvent(GameEvent gameEvent) { #if DEBUG Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner} with id {gameEvent.Id}"); #endif -// GameEvent sortedEvent = null; -// lock (OutOfOrderEvents) -// { -// sortedEvent = OutOfOrderEvents.Values.FirstOrDefault(); + GameEvent sortedEvent = null; + lock (OutOfOrderEvents) + { + sortedEvent = OutOfOrderEvents.Values.FirstOrDefault(); -// while (sortedEvent?.Id == Interlocked.Read(ref NextEventId)) -// { -// if (OutOfOrderEvents.Count > 0) -// { -// OutOfOrderEvents.RemoveAt(0); -// } + while (sortedEvent?.Id == Interlocked.Read(ref NextEventId)) + { + if (OutOfOrderEvents.Count > 0) + { + OutOfOrderEvents.RemoveAt(0); + } -// AddEvent(sortedEvent); -// sortedEvent = OutOfOrderEvents.Values.FirstOrDefault(); -// } -// } + AddEvent(sortedEvent); + sortedEvent = OutOfOrderEvents.Values.FirstOrDefault(); + } + } -// // both the gameEvent Id and the LastEventId are thread safe because we want to synchronize when the -// // event occurs -// if (gameEvent.Id == Interlocked.Read(ref NextEventId)) -// { -//#if DEBUG == true -// Manager.GetLogger().WriteDebug($"sent event with id {gameEvent.Id} to be processed"); -//#endif + // both the gameEvent Id and the LastEventId are thread safe because we want to synchronize when the + // event occurs + if (gameEvent.Id == Interlocked.Read(ref NextEventId)) + { +#if DEBUG == true + Manager.GetLogger().WriteDebug($"sent event with id {gameEvent.Id} to be processed"); +#endif ((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent)); - return true; -// Interlocked.Increment(ref NextEventId); -// } + Interlocked.Increment(ref NextEventId); + } -// // a "newer" event has been added before and "older" one has been added (due to threads and context switching) -// // so me must wait until the next expected one arrives -// else -// { -//#if DEBUG == true -// Manager.GetLogger().WriteWarning("Incoming event is out of order"); -// Manager.GetLogger().WriteDebug($"Expected event Id is {Interlocked.Read(ref NextEventId)}, but got {gameEvent.Id} instead"); -//#endif + // a "newer" event has been added before and "older" one has been added (due to threads and context switching) + // so me must wait until the next expected one arrives + else + { +#if DEBUG == true + Manager.GetLogger().WriteWarning("Incoming event is out of order"); + Manager.GetLogger().WriteDebug($"Expected event Id is {Interlocked.Read(ref NextEventId)}, but got {gameEvent.Id} instead"); +#endif -// // this prevents multiple threads from adding simultaneously -// lock (OutOfOrderEvents) -// { -// if (!OutOfOrderEvents.TryGetValue(gameEvent.Id, out GameEvent discard)) -// { -// OutOfOrderEvents.Add(gameEvent.Id, gameEvent); -// } -// } -// } -// return true; + // this prevents multiple threads from adding simultaneously + lock (OutOfOrderEvents) + { + if (!OutOfOrderEvents.TryGetValue(gameEvent.Id, out GameEvent discard)) + { + OutOfOrderEvents.Add(gameEvent.Id, gameEvent); + } + } + } } } } diff --git a/Application/Server.cs b/Application/Server.cs index 7acb26af1..20c1e5769 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -258,7 +258,6 @@ namespace IW4MAdmin { Player Leaving = Players[cNum]; Logger.WriteInfo($"Client {Leaving}, state {Leaving.State.ToString()} disconnecting..."); - Leaving.State = Player.ClientState.Disconnecting; // occurs when the player disconnects via log before being authenticated by RCon if (Leaving.State != Player.ClientState.Connected) @@ -268,6 +267,7 @@ namespace IW4MAdmin else { + Leaving.State = Player.ClientState.Disconnecting; Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds; Leaving.LastConnection = DateTime.UtcNow; await Manager.GetClientService().Update(Leaving); @@ -822,7 +822,7 @@ namespace IW4MAdmin Logger.WriteInfo($"Log file is {logPath}"); - Task.Run(() => LogEvent.PollForChanges()); + _ = Task.Run(() => LogEvent.PollForChanges()); #if !DEBUG await Broadcast(loc["BROADCAST_ONLINE"]); #endif diff --git a/Plugins/IW4ScriptCommands/Commands/Balance.cs b/Plugins/IW4ScriptCommands/Commands/Balance.cs index e24492ded..2c811542c 100644 --- a/Plugins/IW4ScriptCommands/Commands/Balance.cs +++ b/Plugins/IW4ScriptCommands/Commands/Balance.cs @@ -1,197 +1,197 @@ -//using SharedLibraryCore; -//using SharedLibraryCore.Objects; -//using System; -//using System.Collections.Generic; -//using System.Linq; -//using System.Text; -//using System.Threading.Tasks; +using SharedLibraryCore; +using SharedLibraryCore.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -//namespace IW4ScriptCommands.Commands -//{ -// class Balance : Command -// { -// private class TeamAssignment -// { -// public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; } -// public int Num { get; set; } -// public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; } -// } -// public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null) -// { -// } +namespace IW4ScriptCommands.Commands +{ + class Balance : Command + { + private class TeamAssignment + { + public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; } + public int Num { get; set; } + public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; } + } + public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null) + { + } -// public override async Task ExecuteAsync(GameEvent E) -// { -// string teamsString = (await E.Owner.GetDvarAsync("sv_iw4madmin_teams")).Value; + public override async Task ExecuteAsync(GameEvent E) + { + string teamsString = (await E.Owner.GetDvarAsync("sv_iw4madmin_teams")).Value; -// var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries) -// .Select(c => c.Split(',')) -// .Select(c => new TeamAssignment() -// { -// CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]), -// Num = E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong())?.ClientNumber ?? -1, -// Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong()).ClientId, E.Owner.GetHashCode()) -// }) -// .ToList(); + var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(c => c.Split(',')) + .Select(c => new TeamAssignment() + { + CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]), + Num = E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong())?.ClientNumber ?? -1, + Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong()).ClientId, E.Owner.GetHashCode()) + }) + .ToList(); -// // at least one team is full so we can't balance -// if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(E.Owner.MaxClients / 2.0) -// || scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(E.Owner.MaxClients / 2.0)) -// { -// await E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]); -// return; -// } + // at least one team is full so we can't balance + if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(E.Owner.MaxClients / 2.0) + || scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(E.Owner.MaxClients / 2.0)) + { + await E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]); + return; + } -// List teamAssignments = new List(); + List teamAssignments = new List(); -// var activeClients = E.Owner.GetPlayersAsList().Select(c => new TeamAssignment() -// { -// Num = c.ClientNumber, -// Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()), -// CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).Team -// }) -// .Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator) -// .Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam) -// .OrderByDescending(c => c.Stats.Performance) -// .ToList(); + var activeClients = E.Owner.GetPlayersAsList().Select(c => new TeamAssignment() + { + Num = c.ClientNumber, + Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()), + CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).Team + }) + .Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator) + .Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam) + .OrderByDescending(c => c.Stats.Performance) + .ToList(); -// var alliesTeam = scriptClientTeams -// .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) -// .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) -// .ToList(); + var alliesTeam = scriptClientTeams + .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) + .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) + .ToList(); -// var axisTeam = scriptClientTeams -// .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) -// .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) -// .ToList(); + var axisTeam = scriptClientTeams + .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) + .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) + .ToList(); -// while (activeClients.Count() > 0) -// { -// int teamSizeDifference = alliesTeam.Count - axisTeam.Count; -// double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - -// axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; + while (activeClients.Count() > 0) + { + int teamSizeDifference = alliesTeam.Count - axisTeam.Count; + double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - + axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; -// if (teamSizeDifference == 0) -// { -// if (performanceDisparity == 0) -// { -// alliesTeam.Add(activeClients.First()); -// activeClients.RemoveAt(0); -// } -// else -// { -// if (performanceDisparity > 0) -// { -// axisTeam.Add(activeClients.First()); -// activeClients.RemoveAt(0); -// } -// else -// { -// alliesTeam.Add(activeClients.First()); -// activeClients.RemoveAt(0); -// } -// } -// } -// else if (teamSizeDifference > 0) -// { -// if (performanceDisparity > 0) -// { -// axisTeam.Add(activeClients.First()); -// activeClients.RemoveAt(0); -// } + if (teamSizeDifference == 0) + { + if (performanceDisparity == 0) + { + alliesTeam.Add(activeClients.First()); + activeClients.RemoveAt(0); + } + else + { + if (performanceDisparity > 0) + { + axisTeam.Add(activeClients.First()); + activeClients.RemoveAt(0); + } + else + { + alliesTeam.Add(activeClients.First()); + activeClients.RemoveAt(0); + } + } + } + else if (teamSizeDifference > 0) + { + if (performanceDisparity > 0) + { + axisTeam.Add(activeClients.First()); + activeClients.RemoveAt(0); + } -// else -// { -// axisTeam.Add(activeClients.Last()); -// activeClients.RemoveAt(activeClients.Count - 1); -// } -// } -// else -// { -// if (performanceDisparity > 0) -// { -// alliesTeam.Add(activeClients.First()); -// activeClients.RemoveAt(0); -// } + else + { + axisTeam.Add(activeClients.Last()); + activeClients.RemoveAt(activeClients.Count - 1); + } + } + else + { + if (performanceDisparity > 0) + { + alliesTeam.Add(activeClients.First()); + activeClients.RemoveAt(0); + } -// else -// { -// alliesTeam.Add(activeClients.Last()); -// activeClients.RemoveAt(activeClients.Count - 1); -// } -// } -// } + else + { + alliesTeam.Add(activeClients.Last()); + activeClients.RemoveAt(activeClients.Count - 1); + } + } + } -// alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance) -// .ToList(); + alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance) + .ToList(); -// axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance) -// .ToList(); + axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance) + .ToList(); -// while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1) -// { -// int teamSizeDifference = alliesTeam.Count - axisTeam.Count; -// double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - -// axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; + while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1) + { + int teamSizeDifference = alliesTeam.Count - axisTeam.Count; + double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - + axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; -// if (teamSizeDifference > 0) -// { -// if (performanceDisparity > 0) -// { -// axisTeam.Add(alliesTeam.First()); -// alliesTeam.RemoveAt(0); -// } + if (teamSizeDifference > 0) + { + if (performanceDisparity > 0) + { + axisTeam.Add(alliesTeam.First()); + alliesTeam.RemoveAt(0); + } -// else -// { -// axisTeam.Add(alliesTeam.Last()); -// alliesTeam.RemoveAt(axisTeam.Count - 1); -// } -// } + else + { + axisTeam.Add(alliesTeam.Last()); + alliesTeam.RemoveAt(axisTeam.Count - 1); + } + } -// else -// { -// if (performanceDisparity > 0) -// { -// alliesTeam.Add(axisTeam.Last()); -// axisTeam.RemoveAt(axisTeam.Count - 1); -// } + else + { + if (performanceDisparity > 0) + { + alliesTeam.Add(axisTeam.Last()); + axisTeam.RemoveAt(axisTeam.Count - 1); + } -// else -// { -// alliesTeam.Add(axisTeam.First()); -// axisTeam.RemoveAt(0); -// } -// } -// } + else + { + alliesTeam.Add(axisTeam.First()); + axisTeam.RemoveAt(0); + } + } + } -// foreach (var assignment in alliesTeam) -// { -// teamAssignments.Add($"{assignment.Num},2"); -// assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies; -// } -// foreach (var assignment in axisTeam) -// { -// teamAssignments.Add($"{assignment.Num},3"); -// assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis; -// } + foreach (var assignment in alliesTeam) + { + teamAssignments.Add($"{assignment.Num},2"); + assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies; + } + foreach (var assignment in axisTeam) + { + teamAssignments.Add($"{assignment.Num},3"); + assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis; + } -// if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 && -// axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0) -// { -// await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]); -// return; -// } + if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 && + axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0) + { + await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]); + return; + } -// if (E.Origin?.Level > Player.Permission.Administrator) -// { -// await E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}"); -// await E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}"); -// } + if (E.Origin?.Level > Player.Permission.Administrator) + { + await E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}"); + await E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}"); + } -// string args = string.Join(",", teamAssignments); -// await E.Owner.ExecuteCommandAsync($"sv_iw4madmin_command \"balance:{args}\""); -// await E.Origin.Tell("Balance command sent"); -// } -// } -//} + string args = string.Join(",", teamAssignments); + await E.Owner.ExecuteCommandAsync($"sv_iw4madmin_command \"balance:{args}\""); + await E.Origin.Tell("Balance command sent"); + } + } +} diff --git a/Plugins/ProfanityDeterment/Plugin.cs b/Plugins/ProfanityDeterment/Plugin.cs index b857226b3..3459544c3 100644 --- a/Plugins/ProfanityDeterment/Plugin.cs +++ b/Plugins/ProfanityDeterment/Plugin.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using System.Threading.Tasks; using SharedLibraryCore; using SharedLibraryCore.Configuration; @@ -36,6 +37,15 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment var objectionalWords = Settings.Configuration().OffensiveWords; bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Origin.Name.ToLower().Contains(w)) != null; + // we want to run regex against it just incase + if (!containsObjectionalWord) + { + foreach (string word in objectionalWords) + { + containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word); + } + } + if (containsObjectionalWord) { await E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new Player() @@ -56,7 +66,18 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment if (E.Type == GameEvent.EventType.Say) { var objectionalWords = Settings.Configuration().OffensiveWords; - bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Data.ToLower().Contains(w)) != null; + bool containsObjectionalWord = false; + + foreach (string word in objectionalWords) + { + containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word); + + // break out early because there's at least one objectional word + if (containsObjectionalWord) + { + break; + } + } if (containsObjectionalWord) { diff --git a/Plugins/ScriptPlugins/VPNDetection.js b/Plugins/ScriptPlugins/VPNDetection.js index 9ae807902..c55798b0c 100644 --- a/Plugins/ScriptPlugins/VPNDetection.js +++ b/Plugins/ScriptPlugins/VPNDetection.js @@ -1,14 +1,14 @@ -const plugin = { +var plugin = { author: 'RaidMax', version: 1.0, - name: 'VPN Kick Plugin', + name: 'VPN Detection Plugin', manager: null, logger: null, vpnExceptionIds: [], - checkForVpn(origin) { - let exempt = false; + checkForVpn: function (origin) { + var exempt = false; // prevent players that are exempt from being kicked this.vpnExceptionIds.forEach(function (id) { if (id === origin.ClientId) { @@ -21,43 +21,46 @@ const plugin = { return; } - let usingVPN = false; + var usingVPN = false; try { - let cl = new System.Net.Http.HttpClient(); - let re = cl.GetAsync('https://api.xdefcon.com/proxy/check/?ip=' + origin.IPAddressString).Result; - let co = re.Content; - let parsedJSON = JSON.parse(co.ReadAsStringAsync().Result); - //co.Dispose(); - //re.Dispose(); - //cl.Dispose(); - usingVPN = parsedJSON['success'] && parsedJSON['proxy']; + var cl = new System.Net.Http.HttpClient(); + var re = cl.GetAsync('https://api.xdefcon.com/proxy/check/?ip=' + origin.IPAddressString).Result; + var co = re.Content; + var parsedJSON = JSON.parse(co.ReadAsStringAsync().Result); + // todo: does this work as expected now? + co.Dispose(); + re.Dispose(); + cl.Dispose(); + usingVPN = parsedJSON.success && parsedJSON.proxy; } catch (e) { this.logger.WriteError(e.message); } if (usingVPN) { this.logger.WriteInfo(origin + ' is using a VPN (' + origin.IPAddressString + ')'); - let library = importNamespace('SharedLibraryCore'); - let kickOrigin = new library.Objects.Player(); + var library = importNamespace('SharedLibraryCore'); + var kickOrigin = new library.Objects.Player(); kickOrigin.ClientId = 1; origin.Kick(_localization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], kickOrigin); } }, - onEventAsync(gameEvent, server) { + onEventAsync: function (gameEvent, server) { // connect event if (gameEvent.Type === 3) { this.checkForVpn(gameEvent.Origin); } }, - onLoadAsync(manager) { + onLoadAsync: function (manager) { this.manager = manager; this.logger = manager.GetLogger(); }, - onUnloadAsync() { }, + onUnloadAsync: function () { + }, - onTickAsync(server) { } + onTickAsync: function (server) { + } }; \ No newline at end of file diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index d16f8e2cb..8e317dee9 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -43,15 +43,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers context.ChangeTracker.AutoDetectChangesEnabled = false; context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1); + var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); var iqClientRatings = (from rating in context.Set() -#if DEBUG == false - where rating.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime -#endif - where rating.RatingHistory.Client.Level != Player.Permission.Banned - where rating.RatingHistory.Client.LastConnection > thirtyDaysAgo - where rating.Newest where rating.ServerId == null + where rating.RatingHistory.Client.LastConnection > fifteenDaysAgo + where rating.RatingHistory.Client.Level != Player.Permission.Banned + where rating.Newest + where rating.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime orderby rating.Performance descending select new { @@ -67,7 +65,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var clientRatings = await iqClientRatings.ToListAsync(); - var clientIds = clientRatings.GroupBy(r => r.ClientId).Select(r => r.First().ClientId).ToList(); + var clientIds = clientRatings + .GroupBy(r => r.ClientId) + .Select(r => r.First().ClientId) + .ToList(); var iqStatsInfo = (from stat in context.Set() where clientIds.Contains(stat.ClientId) @@ -101,8 +102,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2), RatingChange = clientRatingsDict[s.ClientId].Ratings.First().Ranking - clientRatingsDict[s.ClientId].Ratings.Last().Ranking, PerformanceHistory = clientRatingsDict[s.ClientId].Ratings.Count() > 1 ? - clientRatingsDict[s.ClientId].Ratings.Select(r => r.Performance).ToList() : - new List() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance }, + clientRatingsDict[s.ClientId].Ratings.Select(r => r.Performance).ToList() : + new List() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance }, TimePlayed = Math.Round(clientRatingsDict[s.ClientId].TotalConnectionTime / 3600.0, 1).ToString("#,##0"), }) .OrderByDescending(r => r.Performance) @@ -324,7 +325,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers /// /// public async Task AddScriptHit(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type, - string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads, string fraction, string snapAngles) + string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads, + string fraction, string visibilityPercentage, string snapAngles) { var statsSvc = ContextThreads[serverId]; Vector3 vDeathOrigin = null; @@ -381,6 +383,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers IsKillstreakKill = isKillstreakKill[0] != '0', AdsPercent = float.Parse(Ads), Fraction = double.Parse(fraction), + VisibilityPercentage = double.Parse(visibilityPercentage), IsKill = !isDamage, AnglesList = snapshotAngles }; @@ -569,9 +572,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } // update their performance -//#if !DEBUG +#if !DEBUG if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5) -//#endif +#endif { await UpdateStatHistory(attacker, attackerStats); attackerStats.LastStatHistoryUpdate = DateTime.UtcNow; @@ -631,13 +634,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers ctx.Update(clientHistory); } - var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1); + var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); // get the client ranking for the current server int individualClientRanking = await ctx.Set() .Where(c => c.ServerId == clientStats.ServerId) + .Where(r => r.RatingHistory.Client.LastConnection > fifteenDaysAgo) .Where(r => r.RatingHistory.Client.Level != Player.Permission.Banned) .Where(r => r.ActivityAmount > Plugin.Config.Configuration().TopPlayersMinPlayTime) - .Where(r => r.RatingHistory.Client.LastConnection > thirtyDaysAgo) .Where(c => c.RatingHistory.ClientId != client.ClientId) .Where(r => r.Newest) .Where(c => c.Performance > clientStats.Performance) @@ -689,11 +692,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } int overallClientRanking = await ctx.Set() - .Where(r => r.RatingHistory.Client.Level != Player.Permission.Banned) + .Where(r => r.ServerId == null) + .Where(r => r.RatingHistory.ClientId != client.ClientId) + .Where(r => r.RatingHistory.Client.LastConnection > fifteenDaysAgo) + .Where(r => r.RatingHistory.Client.Level != Player.Permission.Banned) .Where(r => r.ActivityAmount > Plugin.Config.Configuration().TopPlayersMinPlayTime) - .Where(r => r.RatingHistory.Client.LastConnection > thirtyDaysAgo) - .Where(r => r.RatingHistory.ClientId != client.ClientId) - .Where(r => r.ServerId == null) .Where(r => r.Newest) .Where(r => r.Performance > performanceAverage) .CountAsync() + 1; @@ -730,9 +733,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { await ctx.SaveChangesAsync(); } - catch (DbUpdateConcurrencyException e) + // this can happen when the client disconnects without any stat changes + catch (DbUpdateConcurrencyException) { - + } } } diff --git a/Plugins/Stats/Models/EFClientKill.cs b/Plugins/Stats/Models/EFClientKill.cs index 14e4cb114..51c0ec76e 100644 --- a/Plugins/Stats/Models/EFClientKill.cs +++ b/Plugins/Stats/Models/EFClientKill.cs @@ -31,6 +31,7 @@ namespace IW4MAdmin.Plugins.Stats.Models public DateTime When { get; set; } public double Fraction { get; set; } public bool IsKill { get; set; } + public double VisibilityPercentage { get; set; } // http://wiki.modsrepository.com/index.php?title=Call_of_Duty_5:_Gameplay_standards for conversion to meters [NotMapped] public double Distance => Vector3.Distance(KillOrigin, DeathOrigin) * 0.0254; diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index c3fe6d9c5..44576c6d9 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -76,7 +76,7 @@ namespace IW4MAdmin.Plugins.Stats if (killInfo.Length >= 14) { await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], - killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14]); + killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15]); } break; case GameEvent.EventType.Kill: @@ -92,7 +92,7 @@ namespace IW4MAdmin.Plugins.Stats if (killInfo.Length >= 14) { await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], - killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14]); + killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15]); } break; } diff --git a/Plugins/Tests/PluginTests.cs b/Plugins/Tests/PluginTests.cs new file mode 100644 index 000000000..16148d50f --- /dev/null +++ b/Plugins/Tests/PluginTests.cs @@ -0,0 +1,55 @@ +using IW4MAdmin.Application; +using SharedLibraryCore; +using SharedLibraryCore.Objects; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Tests +{ + [Collection("ManagerCollection")] + public class PluginTests + { + readonly ApplicationManager Manager; + + public PluginTests(ManagerFixture fixture) + { + Manager = fixture.Manager; + } + + [Fact] + public void ClientSayObjectionalWordShouldWarn() + { + var e = new GameEvent() + { + Type = GameEvent.EventType.Connect, + Origin = new Player() + { + Name = $"Player1", + NetworkId = 1, + ClientNumber = 1 + }, + Owner = Manager.GetServers()[0] + }; + + Manager.GetEventHandler().AddEvent(e); + e.OnProcessed.Wait(); + + var client = Manager.GetServers()[0].Players[0]; + + e = new GameEvent() + { + Type = GameEvent.EventType.Say, + Origin = client, + Data = "nigger", + Owner = client.CurrentServer + }; + + Manager.GetEventHandler().AddEvent(e); + e.OnProcessed.Wait(); + + Assert.True(client.Warnings == 1, "client wasn't warned for objectional language"); + } + } +} diff --git a/README.md b/README.md index 7fe4eab41..6d5d79f47 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Latest binary builds are always available at https://raidmax.org/IW4MAdmin * [.NET Core 2.1 Runtime](https://www.microsoft.com/net/download) *or newer* 1. Extract `IW4MAdmin-.zip` 2. Run `StartIW4MAdmin.cmd` +### Help +Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3) +If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues) ___ ### Configuration @@ -30,10 +33,6 @@ When **IW4MAdmin** is launched for the _first time_, you will be prompted to set * Shows a prefix to every message send by **IW4MAdmin** -- `[Admin] message` * _This feature requires you specify a custom say name_ -`Enable client VPNs` -* Allow clients to use a [VPN](https://en.wikipedia.org/wiki/Virtual_private_network) -* _This feature requires an active api key on [iphub.info](https://iphub.info/)_ - `Enable social link` * Shows a link to your community's social media/website on the webfront @@ -82,9 +81,6 @@ If you wish to further customize your experience of **IW4MAdmin**, the following `RConPollRate` * Specifies (in milliseconds) how often to poll each server for updates -`VpnExceptionIds` -* Specifies the list of `Client IDs` exempt from the VPN check (if enabled) - `Servers` * Specifies the list of servers **IW4MAdmin** will monitor * `IPAddress` @@ -228,6 +224,9 @@ ___ #### Stats - This plugin calculates basic player performance, skill approximation, and kill/death ratio +- Skill is an number derived from an algorithmic processing of a player's Kill Death Ratio (KDR) and Score per Minute (SPM). +- Elo Rating is based off of the number of encounters a player wins. +- Performance is the average of Skill + Elo Rating **Commands added by this plugin** @@ -238,7 +237,7 @@ ___ |topstats|ts|view the top 5 players on this server|False|!ts |User| |mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User| -- To qualify for top stats, a client must have played for at least `1 hour` and connected within the past `30 days`. +- To qualify for top stats, a client must have played for at least `3 hours` and connected within the past `15 days`. #### Login - This plugin deters GUID spoofing by requiring privileged users to login with their password before executing commands @@ -254,6 +253,14 @@ ___ - This plugin warns and kicks players for using profanity - Profane words and warning message can be specified in `ProfanityDetermentSettings.json` - If a client's name contains a word listed in the settings, they will immediately be kicked + +####IW4 Script Commands +- This plugin provides additional integration to IW4x +- In order to take advantage of it, copy the `userraw` folder into your IW4x server directory + +####VPN Detection [Script Plugin] +- This plugin detects if a client is using a VPN and kicks them if they are +- To disable this plugin, delete `Plugins\VPNDetection.js` ___ ### Webfront `Home` @@ -274,34 +281,35 @@ ___ `Web Console` * Allows logged in privileged users to execute commands as if they are in-game - --- ### Extending Plugins #### Code -IW4Madmin functionality can be extended by writing additional plugins in C#. +IW4Madmin functionality can be extended by writing additional plugins in C#. +Each class library must implement the `IPlugin` interface. +See the existing plugins for examples. #### JavaScript IW4MAdmin functionality can be extended using JavaScript. -The JavaScript parser supports [some](https://github.com/sebastienros/jint/issues/343) of ECMAScript 6's new features. +The JavaScript parser supports [ECMA 5.1](https://ecma-international.org/ecma-262/5.1/) standards. #### Plugin Object Template In order to be properly parsed by the JavaScript engine, every plugin must conform to the following template. ```js -const plugin = { +var plugin = { author: 'YourHandle', version: 1.0, name: 'Sample JavaScript Plugin', - onEventAsync(gameEvent, server) { + onEventAsync: function (gameEvent, server) { }, - onLoadAsync(manager) { + onLoadAsync: function (manager) { }, - onUnloadAsync() { + onUnloadAsync: function () { }, - onTickAsync(server) { + onTickAsync: function (server) { } -} +}; ``` #### Required Properties - `author` — [string] Author of the plugin (usually your name or online name/alias) @@ -333,9 +341,9 @@ setuptools>=39.0.1 urllib3>=1.23 ``` #### Configuration Options -- `IW4MAdminUrl` — Base url corresponding to your IW4MAdmin `WebfrontBindUrl`. +- `IW4MAdminUrl` — Base url corresponding to your IW4MAdmin `WebfrontBindUrl`. Example http://127.0.0.1 -- `DiscordWebhookNotificationUrl` — [required] Discord generated URL to send notifications/alerts to; this includes **Reports** and **Bans** +- `DiscordWebhookNotificationUrl` — [required] Discord generated URL to send notifications/alerts to; this includes **Reports** and **Bans** Example https://discordapp.com/api/webhooks/id/token - `DiscordWebhookInformationUrl` — [optional] Discord generated URL to send information to; this includes information such as player messages - `NotifyRoleIds` — [optional] List of [discord role ids](https://discordhelp.net/role-id) to mention when notification hook is sent diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index 4944e63e5..380b1a8c5 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -10,19 +10,16 @@ namespace SharedLibraryCore.Configuration public bool EnableWebFront { get; set; } public bool EnableMultipleOwners { get; set; } public bool EnableSteppedHierarchy { get; set; } - public bool EnableClientVPNs { get; set; } public bool EnableSocialLink { get; set; } public bool EnableCustomSayName { get; set; } public string CustomSayName { get; set; } public string SocialLinkAddress { get; set; } public string SocialLinkTitle { get; set; } - public string IPHubAPIKey { get; set; } public string WebfrontBindUrl { get; set; } public string CustomParserEncoding { get; set; } public string CustomLocale { get; set; } public string ConnectionString { get; set; } public int RConPollRate { get; set; } = 5000; - public List VpnExceptionIds { get; set; } public string Id { get; set; } public List Servers { get; set; } public int AutoMessagePeriod { get; set; } @@ -43,16 +40,11 @@ namespace SharedLibraryCore.Configuration bool useCustomParserEncoding = Utilities.PromptBool(loc["SETUP_USE_CUSTOMENCODING"]); CustomParserEncoding = useCustomParserEncoding ? Utilities.PromptString(loc["SETUP_ENCODING_STRING"]) : "windows-1252"; - WebfrontBindUrl = "http://127.0.0.1:1624"; + WebfrontBindUrl = "http://0.0.0.0:1624"; if (EnableCustomSayName) CustomSayName = Utilities.PromptString(loc["SETUP_SAY_NAME"]); - EnableClientVPNs = Utilities.PromptBool(loc["SETUP_ENABLE_VPNS"]); - - if (!EnableClientVPNs) - IPHubAPIKey = Utilities.PromptString(loc["SETUP_IPHUB_KEY"]); - EnableSocialLink = Utilities.PromptBool(loc["SETUP_DISPLAY_SOCIAL"]); if (EnableSocialLink) @@ -60,7 +52,7 @@ namespace SharedLibraryCore.Configuration SocialLinkTitle = Utilities.PromptString(loc["SETUP_SOCIAL_TITLE"]); SocialLinkAddress = Utilities.PromptString(loc["SETUP_SOCIAL_LINK"]); } - VpnExceptionIds = new List(); + RConPollRate = 5000; return this; diff --git a/SharedLibraryCore/Interfaces/IEventHandler.cs b/SharedLibraryCore/Interfaces/IEventHandler.cs index 3127c1eb8..c92c5afd7 100644 --- a/SharedLibraryCore/Interfaces/IEventHandler.cs +++ b/SharedLibraryCore/Interfaces/IEventHandler.cs @@ -13,6 +13,6 @@ namespace SharedLibraryCore.Interfaces /// Add a game event event to the queue to be processed /// /// Game event - bool AddEvent(GameEvent gameEvent); + void AddEvent(GameEvent gameEvent); } } diff --git a/SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.Designer.cs b/SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.Designer.cs new file mode 100644 index 000000000..226e330dc --- /dev/null +++ b/SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.Designer.cs @@ -0,0 +1,665 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SharedLibraryCore.Database; + +namespace SharedLibraryCore.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20180904154622_AddVisibilityPercentage")] + partial class AddVisibilityPercentage + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.2-rtm-30932"); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("CurrentSessionLength"); + + b.Property("CurrentStrain"); + + b.Property("CurrentViewAngleVector3Id"); + + b.Property("Deaths"); + + b.Property("Distance"); + + b.Property("EloRating"); + + b.Property("HitDestinationVector3Id"); + + b.Property("HitLocation"); + + b.Property("HitOriginVector3Id"); + + b.Property("HitType"); + + b.Property("Hits"); + + b.Property("Kills"); + + b.Property("LastStrainAngleVector3Id"); + + b.Property("SessionAngleOffset"); + + b.Property("SessionSPM"); + + b.Property("SessionScore"); + + b.Property("StrainAngleBetween"); + + b.Property("TimeSinceLastEvent"); + + b.Property("WeaponId"); + + b.Property("When"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleVector3Id"); + + b.HasIndex("HitDestinationVector3Id"); + + b.HasIndex("HitOriginVector3Id"); + + b.HasIndex("LastStrainAngleVector3Id"); + + b.ToTable("EFACSnapshot"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AttackerId"); + + b.Property("Damage"); + + b.Property("DeathOriginVector3Id"); + + b.Property("DeathType"); + + b.Property("Fraction"); + + b.Property("HitLoc"); + + b.Property("IsKill"); + + b.Property("KillOriginVector3Id"); + + b.Property("Map"); + + b.Property("ServerId"); + + b.Property("VictimId"); + + b.Property("ViewAnglesVector3Id"); + + b.Property("VisibilityPercentage"); + + b.Property("Weapon"); + + b.Property("When"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("TimeSent"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId"); + + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Deaths"); + + b.Property("EloRating"); + + b.Property("Kills"); + + b.Property("MaxStrain"); + + b.Property("RollingWeightedKDR"); + + b.Property("SPM"); + + b.Property("Skill"); + + b.Property("TimePlayed"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId") + .HasColumnName("EFClientStatistics_ClientId"); + + b.Property("HitCount"); + + b.Property("HitOffsetAverage"); + + b.Property("Location"); + + b.Property("MaxAngleDistance"); + + b.Property("ServerId") + .HasColumnName("EFClientStatistics_ServerId"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ActivityAmount"); + + b.Property("Newest"); + + b.Property("Performance"); + + b.Property("Ranking"); + + b.Property("RatingHistoryId"); + + b.Property("ServerId"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFRating"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Port"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ServerId"); + + b.Property("TotalKills"); + + b.Property("TotalPlayTime"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("DateAdded"); + + b.Property("IPAddress"); + + b.Property("LinkId"); + + b.Property("Name") + .IsRequired(); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("Comment") + .HasMaxLength(128); + + b.Property("OriginEntityId"); + + b.Property("TargetEntityId"); + + b.Property("TimeChanged"); + + b.Property("TypeOfChange"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AliasLinkId"); + + b.Property("Connections"); + + b.Property("CurrentAliasId"); + + b.Property("FirstConnection"); + + b.Property("LastConnection"); + + b.Property("Level"); + + b.Property("Masked"); + + b.Property("NetworkId"); + + b.Property("Password"); + + b.Property("PasswordSalt"); + + b.Property("TotalConnectionTime"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Created"); + + b.Property("Extra"); + + b.Property("Key") + .IsRequired(); + + b.Property("Updated"); + + b.Property("Value") + .IsRequired(); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AutomatedOffense"); + + b.Property("Expires"); + + b.Property("LinkId"); + + b.Property("OffenderId"); + + b.Property("Offense") + .IsRequired(); + + b.Property("PunisherId"); + + b.Property("Type"); + + b.Property("When"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd(); + + b.Property("EFACSnapshotSnapshotId"); + + b.Property("X"); + + b.Property("Y"); + + b.Property("Z"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics") + .WithMany("HitLocations") + .HasForeignKey("ClientId", "ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("EFACSnapshotSnapshotId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.cs b/SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.cs new file mode 100644 index 000000000..b30d91207 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180904154622_AddVisibilityPercentage.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class AddVisibilityPercentage : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "VisibilityPercentage", + table: "EFClientKills", + nullable: false, + defaultValue: 0.0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "VisibilityPercentage", + table: "EFClientKills"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index 718924cfd..65ae83a50 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -111,6 +111,8 @@ namespace SharedLibraryCore.Migrations b.Property("ViewAnglesVector3Id"); + b.Property("VisibilityPercentage"); + b.Property("Weapon"); b.Property("When"); diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs index 21231dd8b..99b8d0429 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/SharedLibraryCore/RCon/Connection.cs @@ -17,19 +17,14 @@ namespace SharedLibraryCore.RCon public int BufferSize { get; private set; } public byte[] Buffer { get; private set; } - private readonly StringBuilder sb; - - public StringBuilder ResponseString - { - get => sb; - } + public StringBuilder ResponseString { get; } public ConnectionState(Socket cl) { BufferSize = 8192; Buffer = new byte[BufferSize]; Client = cl; - sb = new StringBuilder(); + ResponseString = new StringBuilder(); } } diff --git a/SharedLibraryCore/RCon/StaticHelpers.cs b/SharedLibraryCore/RCon/StaticHelpers.cs index 52dec73da..0782d0391 100644 --- a/SharedLibraryCore/RCon/StaticHelpers.cs +++ b/SharedLibraryCore/RCon/StaticHelpers.cs @@ -13,6 +13,6 @@ namespace SharedLibraryCore.RCon } public static char SeperatorChar = (char)int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier); - public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 5); + public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 10); } } diff --git a/SharedLibraryCore/ScriptPlugin.cs b/SharedLibraryCore/ScriptPlugin.cs index 7bed5aee7..7df66f17e 100644 --- a/SharedLibraryCore/ScriptPlugin.cs +++ b/SharedLibraryCore/ScriptPlugin.cs @@ -58,8 +58,8 @@ namespace SharedLibraryCore Manager = mgr; string script = File.ReadAllText(FileName); - ScriptEngine = new Jint.Engine(cfg => - cfg.AllowClr(new[] + ScriptEngine = new Jint.Engine(cfg => + cfg.AllowClr(new[] { typeof(System.Net.Http.HttpClient).Assembly, typeof(Objects.Player).Assembly, @@ -82,9 +82,12 @@ namespace SharedLibraryCore public Task OnEventAsync(GameEvent E, Server S) { - ScriptEngine.SetValue("_gameEvent", E); - ScriptEngine.SetValue("_server", S); - return Task.FromResult(ScriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue()); + lock (ScriptEngine) + { + ScriptEngine.SetValue("_gameEvent", E); + ScriptEngine.SetValue("_server", S); + return Task.FromResult(ScriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue()); + } } public Task OnLoadAsync(IManager manager) diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index e02c4d066..6963c8b48 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -117,7 +117,7 @@ namespace SharedLibraryCore public async Task Broadcast(String Message) { #if DEBUG == false - string formattedMessage = String.Format(RconParser.GetCommandPrefixes().Say, Message); + string formattedMessage = String.Format(RconParser.GetCommandPrefixes().Say, $"{(CustomSayEnabled ? $"{CustomSayName}: " : "")}{Message}"); #else Logger.WriteVerbose(Message.StripColors()); #endif @@ -145,7 +145,7 @@ namespace SharedLibraryCore public async Task Tell(String Message, Player Target) { #if !DEBUG - string formattedMessage = String.Format(RconParser.GetCommandPrefixes().Tell, Target.ClientNumber, Message); + string formattedMessage = String.Format(RconParser.GetCommandPrefixes().Tell, Target.ClientNumber, $"{(CustomSayEnabled ? $"{CustomSayName}: " : "")}{Message}"); if (Target.ClientNumber > -1 && Message.Length > 0 && Target.Level != Player.Permission.Console) await this.ExecuteCommandAsync(formattedMessage); #else @@ -160,9 +160,11 @@ namespace SharedLibraryCore Console.ForegroundColor = ConsoleColor.Gray; } + // prevent this from queueing up too many command responses if (CommandResult.Count > 15) CommandResult.RemoveAt(0); + // it was a remote command so we need to add it to the command result queue if (Target.ClientNumber < 0) { CommandResult.Add(new CommandResponseInfo() diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 539fcfad7..4722fc270 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -17,7 +17,7 @@ - + diff --git a/_customcallbacks.gsc b/_customcallbacks.gsc index bda6876af..3f719013e 100644 --- a/_customcallbacks.gsc +++ b/_customcallbacks.gsc @@ -12,6 +12,26 @@ init() level waittill("prematch_over"); level.callbackPlayerKilled = ::Callback_PlayerKilled; level.callbackPlayerDamage = ::Callback_PlayerDamage; + level.playerTags = []; + level.playerTags[0] = "j_head"; + level.playerTags[1] = "j_neck"; + level.playerTags[2] = "j_spineupper"; + level.playerTags[3] = "j_spinelower"; + level.playerTags[4] = "j_shoulder_ri"; + level.playerTags[5] = "j_shoulder_le"; + level.playerTags[6] = "j_elbow_ri"; + level.playerTags[7] = "j_spineupper"; + level.playerTags[8] = "j_spineupper"; + level.playerTags[9] = "j_elbow_le"; + level.playerTags[10] = "j_wrist_ri"; + level.playerTags[11] = "j_wrist_le"; + level.playerTags[12] = "j_hip_ri"; + level.playerTags[13] = "j_hip_le"; + level.playerTags[14] = "j_knee_ri"; + level.playerTags[15] = "j_knee_le"; + level.playerTags[16] = "j_ankle_ri"; + level.playerTags[17] = "j_ankle_le"; + level.playerTags[18] = "j_helmet"; } @@ -112,6 +132,21 @@ waitForAdditionalAngles(logString) logPrint(logString + ";" + anglesStr + "\n"); } +runVisibilityCheck(attacker, victim) +{ + start = attacker getTagOrigin("tag_eye"); + traceSucceedCount = 0; + + for (i = 0; i < 19; i++) + { + if (sightTracePassed(start, victim getTagOrigin(level.playerTags[i]), false, attacker)) + { + traceSucceedCount += 1; + } + } + return traceSucceedCount / 20; +} + vectorScale(vector, scale) { return (vector[0] * scale, vector[1] * scale, vector[2] * scale); @@ -134,7 +169,9 @@ Process_Hit(type, attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon) end = location; trace = bulletTrace(start, end, true, _attacker); - logLine = "Script" + type + ";" + _attacker.guid + ";" + victim.guid + ";" + _attacker GetTagOrigin("tag_eye") + ";" + location + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + ";" + _attacker getPlayerAngles() + ";" + gettime() + ";" + isKillstreakKill + ";" + _attacker playerADS() + ";" + trace["fraction"]; + playerVisibilityPercentage = runVisibilityCheck(_attacker, victim); + + logLine = "Script" + type + ";" + _attacker.guid + ";" + victim.guid + ";" + _attacker GetTagOrigin("tag_eye") + ";" + location + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + ";" + _attacker getPlayerAngles() + ";" + gettime() + ";" + isKillstreakKill + ";" + _attacker playerADS() + ";" + trace["fraction"] + ";" + playerVisibilityPercentage; attacker thread waitForAdditionalAngles(logLine); }